@wix/agent-skills 1.27.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +1 -1
  3. package/skills/wix-app/references/APP_VALIDATION.md +5 -1
  4. package/skills/wix-app/references/SDK_MODULE_MAP.md +1 -1
  5. package/skills/wix-app/references/stores/GET_PRODUCT.md +9 -1
  6. package/skills/wix-app/references/stores/INVENTORY.md +2 -0
  7. package/skills/wix-app/references/stores/QUERY.md +2 -2
  8. package/skills/wix-app/references/stores/WRITE.md +1 -1
  9. package/skills/wix-headless-kit/SKILL.md +4 -0
  10. package/skills/wix-headless-kit/guides/no-machine.md +115 -0
  11. package/skills/wix-headless-kit/install/attach.mjs +5 -3
  12. package/skills/wix-headless-kit/install/bootstrap.mjs +3 -1
  13. package/skills/wix-headless-kit/install/context.mjs +4 -2
  14. package/skills/wix-headless-kit/install/deploy.mjs +4 -2
  15. package/skills/wix-headless-kit/install/pins.json +8 -0
  16. package/skills/wix-headless-kit/install/setup.mjs +10 -8
  17. package/skills/wix-headless-kit/install/templates.mjs +18 -8
  18. package/skills/wix-headless-templates/donations/INSTRUCTIONS.md +8 -9
  19. package/skills/wix-headless-templates/donations/app/wix/donations/donate.ts +18 -45
  20. package/skills/wix-headless-templates/donations/app/wix/donations/donations-core.ts +18 -19
  21. package/skills/wix-headless-templates/donations/project/src/wix/donations/donate.ts +18 -45
  22. package/skills/wix-headless-templates/donations/project/src/wix/donations/donations-core.ts +18 -19
  23. package/skills/wix-headless-templates/donations/rest/donate.ts +13 -39
  24. package/skills/wix-headless-templates/pricing-plans/INSTRUCTIONS.md +4 -2
  25. package/skills/wix-headless-templates/shared/capabilities/site-search/seed/install.mjs +2 -2
  26. package/skills/wix-headless-templates/shared/seed/wix-cli.mjs +6 -3
  27. package/skills/wix-headless-templates/storefront/app/wix/storefront/cart.ts +1 -2
  28. package/skills/wix-headless-templates/storefront/project/src/wix/storefront/cart.ts +1 -2
  29. package/skills/wix-manage/references/sites/upload-static-site.md +61 -42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/agent-skills",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "description": "Wix Skills for AI coding agents — versioned npm distribution of the wix/skills content.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -492,7 +492,7 @@ Execute these steps sequentially after all implementation is complete. See [APP_
492
492
  1. **Package Installation** — Detect package manager, run install
493
493
  2. **TypeScript Compilation** — `npx tsc --noEmit -p .`
494
494
  3. **Build** — `npx wix build`
495
- 4. **Preview** — `npx wix preview`, in the foreground: it uploads, prints the preview URLs and exits on its own, so no `timeout`, backgrounding or `sleep`
495
+ 4. **Preview** — `npx wix preview`, in the foreground and unpiped: it uploads, prints the preview URLs and exits on its own, so no `timeout`, backgrounding, `sleep` or `| tail`. If your shell moves it to the background anyway, it stalled: read what it printed and `.wix/debug.log` once, then report that the preview did not complete. Don't poll, and don't promise a URL
496
496
 
497
497
  Stop and report errors if any step fails. Check `.wix/debug.log` on failures.
498
498
 
@@ -85,14 +85,18 @@ npx wix build
85
85
 
86
86
  ### Step 4: Preview Deployment
87
87
 
88
- `npx wix preview` is a one-shot command, not a server: it uploads a preview build, prints the preview URLs and **exits on its own**, in seconds. Run it in the foreground and read its output:
88
+ `npx wix preview` is a one-shot command, not a server: it uploads a preview build, prints the preview URLs and **exits on its own**, in seconds. Run it in the foreground, exactly as written, and read its output:
89
89
 
90
90
  ```bash
91
91
  npx wix preview
92
92
  ```
93
93
 
94
+ Don't pipe it (`| tail`, `| head`, `| grep`). The output is short, and a pipe holds back everything until the command exits, so a slow preview shows nothing at all.
95
+
94
96
  Don't wrap it in `timeout` (macOS has none, so the call fails before preview runs), background it, `sleep` or poll a log for it, or `kill` it afterwards. There is no process left to wait for or stop.
95
97
 
98
+ **If it doesn't return:** your shell may move a long call to the background on its own (for example, after a 120s limit). That means the preview stalled, not that it's still working normally. Read the output it left and `.wix/debug.log` **once**. If neither has the preview URLs, report "preview did not complete" with whatever they show and stop validation. Don't sleep, poll, schedule a wake-up or start the preview again, and don't tell the user a URL is coming.
99
+
96
100
  **Success criteria:**
97
101
  - The command exits 0
98
102
  - It printed `✔ Preview created successfully!` and the preview URLs
@@ -15,7 +15,7 @@ Data owned by an existing Wix business app is read and written through that app'
15
15
  | pricing plans / subscriptions | `@wix/pricing-plans` |
16
16
  | bookings / services / staff / time slots | `@wix/bookings` |
17
17
  | calendar events / schedules | `@wix/calendar` |
18
- | table reservations | `@wix/table-reservations` |
18
+ | table reservations | `@wix/table-reservations` (namespace `reservations`) — not `@wix/restaurants` |
19
19
  | restaurant menus / online orders | `@wix/restaurants` |
20
20
  | blog posts | `@wix/blog` |
21
21
  | site events / tickets / RSVPs | `@wix/events` |
@@ -4,13 +4,21 @@
4
4
 
5
5
  ```typescript
6
6
  if (v === 'V3_CATALOG') {
7
- const product = await productsV3.getProduct(id); // returns Product directly
7
+ const product = await productsV3.getProduct(id); // returns V3Product directly
8
8
  return product;
9
9
  }
10
10
  const { product } = await products.getProduct(id); // V1 wraps in { product }
11
11
  return product;
12
12
  ```
13
13
 
14
+ The V3 entity type is `productsV3.V3Product`.
15
+
16
+ ---
17
+
18
+ ## Product page URL
19
+
20
+ V3 `product.url` is populated only when the request's `fields` includes `'URL'` — `getProduct(id, { fields: ['URL'] })`, or the direct-call `searchProducts` / `queryProducts(query, { fields: ['URL'] })` (the fluent builder's `.find()` takes no options). V1 `productPageUrl` is an object, `{ base, path }`, not a string.
21
+
14
22
  ---
15
23
 
16
24
  ## Read product options and choices (V1 vs V3)
@@ -18,6 +18,8 @@ if (v === 'V3_CATALOG') {
18
18
  }
19
19
  ```
20
20
 
21
+ V3 increments work only for inventory items with `trackQuantity: true`; the entries are `{ inventoryItemId, incrementBy }` with `incrementBy` 1 to 99999.
22
+
21
23
  To find a V3 inventory item ID, use `inventoryItemsV3.searchInventoryItems` filtered by `productId` / `variantId`.
22
24
 
23
25
  ---
@@ -8,7 +8,7 @@ Both versions expose a fluent query builder, but the paging method differs:
8
8
  |--------|--------------------------------|----------------------------------|
9
9
  | API style | Fluent builder: `.skip().limit().find()` | Fluent builder: `.skipTo(cursor).limit().find()` |
10
10
  | Pagination | Offset (`.skip(n)`) | Cursor (`.skipTo(cursor)`) |
11
- | Result `items` | `res.items` (V1 `Product[]`) | `res.items` (V3 `Product[]`) |
11
+ | Result `items` | `res.items` (V1 `Product[]`) | `res.items` (V3 `productsV3.V3Product[]`) |
12
12
  | Total count | `res.totalCount` | **None** — V3 only has `cursors.next` + `hasNext()` |
13
13
  | `hasNext` | `res.hasNext()` (method) | `res.hasNext()` (method) |
14
14
  | Next cursor | n/a | `res.cursors.next` (string) |
@@ -35,7 +35,7 @@ export async function listProductsPage(
35
35
  if (v === 'V3_CATALOG') {
36
36
  let builder = productsV3.queryProducts().limit(limit);
37
37
  if (typeof cursorOrSkip === 'string') builder = builder.skipTo(cursorOrSkip);
38
- // Do NOT chain a sort — see OVERVIEW.md gotcha #10.
38
+ // Do NOT chain a sort — see [STORES_VERSIONING.md](../STORES_VERSIONING.md) gotcha #10.
39
39
  const res = await builder.find();
40
40
  return {
41
41
  products: res.items,
@@ -32,7 +32,7 @@ return product;
32
32
 
33
33
  ```typescript
34
34
  if (v === 'V3_CATALOG') {
35
- // Signature: updateProduct(_id, productFields). Returns Product directly.
35
+ // Signature: updateProduct(_id, productFields). Returns V3Product directly.
36
36
  const current = await productsV3.getProduct(id);
37
37
  if (current.revision == null) throw new Error(`Product ${id} has no revision`);
38
38
  return await productsV3.updateProduct(id, {
@@ -302,6 +302,10 @@ browser versus the server, the OAuth allow-list for a self-hosted origin, pre-re
302
302
  how to close such a run. A public site is still better served by managed Astro; say so when you
303
303
  close.
304
304
 
305
+ Without a machine at all, or when the install, the CLI or the login is blocked where you are,
306
+ read `<SKILL_ROOT>/guides/no-machine.md`: the same run, step by step, as the Wix API calls the
307
+ scripts make and the files beside this skill that carry the contracts.
308
+
305
309
  ## Verticals
306
310
 
307
311
  The shortlist. Match the brief against the first column; when it names a Wix product or a
@@ -0,0 +1,115 @@
1
+ # No machine: the kit's run through the Wix APIs
2
+
3
+ Read when you cannot run the commands this skill is built on: no shell, no file system, no Node, a
4
+ sandbox that blocks the install, the CLI or the login, or a host that only talks to Wix through
5
+ the Wix MCP. Nothing in this guide installs, downloads or runs anything. It walks the run of
6
+ SKILL.md step by step and names, for each, the Wix API call the CLI or the script performs, and
7
+ the file beside this skill that carries the contract.
8
+
9
+ ## Where these skills are
10
+
11
+ This skill is one of a set, and the guide reads from three of them:
12
+
13
+ - `<SKILL_ROOT>` — this skill, `wix-headless-kit`: the run, its guides, the Verticals table.
14
+ - `<TEMPLATES>` — `wix-headless-templates`: the shipped, verified code per Wix Business Solution,
15
+ one folder each, with its `INSTRUCTIONS.md` (the contracts), `seed/` (the seed script and
16
+ `SEED.md`) and `rest/` (the data layer over plain `fetch`). Installed beside this skill, or
17
+ fetched to `<SKILL_ROOT>/templates/`.
18
+ - `<MANAGE>` — `wix-manage`: REST recipes to configure and manage a site's business solutions, one
19
+ recipe per operation with the exact endpoint and payload; its `SKILL.md` is the index.
20
+ - `<DOCS>` — `wix-docs`: how to look up the Wix API and SDK documentation and confirm a method's
21
+ exact shape before writing a call.
22
+
23
+ They install together and sit beside each other. When you are reading this skill online rather
24
+ than from an install, the others are published alongside it, by those names, and each skill's
25
+ manifest lists its files. Read each file in full: a fetch tool that summarizes drops the exact
26
+ calls and shapes; a tool that returns the file's text, or one that runs code and can fetch a URL,
27
+ does not.
28
+
29
+ Two things such a run does not produce, and never claims: a frontend built from the shipped `app/`
30
+ code (that needs a build), and a release of one. Everything else the kit does to a site, it does
31
+ through calls you can make.
32
+
33
+ ## 0. Identity
34
+
35
+ Every call below runs as the site owner, with whatever identity your host gives you: the Wix
36
+ MCP's `ExecuteWixAPI` carries the user's login; an API key from the user's account goes in the
37
+ `Authorization` header of any call, as `<MANAGE>/SKILL.md` describes; a site token from the Wix CLI
38
+ works the same way when the user runs the CLI for you. The site's OAuth app (`client_id`) mints
39
+ visitor tokens only, which cover what a visitor may see and nothing here.
40
+
41
+ ## 1. The brief → the solutions
42
+
43
+ Unchanged from SKILL.md step 2 and its Verticals table: the brief names the business, the table
44
+ names the solutions it needs. Read each solution's `<TEMPLATES>/<solution>/INSTRUCTIONS.md` for
45
+ what it covers and `<TEMPLATES>/<solution>/seed/SEED.md` for what a seeded site holds.
46
+
47
+ ## 2. The site
48
+
49
+ The CLI's `npm create @wix/new` is one call: `POST /headless-business-setup/v1/headless-business/provision`,
50
+ with `origin: agent-<your-id>`, a name, and the solutions to install as `seedOptions`. The recipe
51
+ is `<MANAGE>/references/sites/create-headless-site.md`; it returns the `metaSiteId` and the
52
+ site's OAuth client `appId`. Do not create a site any other way: a project from Create Project or
53
+ a template is neither headless nor publishable, and a later run with a machine cannot attach to it.
54
+
55
+ A site that already exists (the brief names it) is read first, never seeded unasked: SKILL.md
56
+ step 2 and `<SKILL_ROOT>/guides/existing-site.md` apply as written.
57
+
58
+ ## 3. The apps
59
+
60
+ A solution the provision call did not install, or a capability's app (Members Area, Forms,
61
+ Restaurants, Donations, FAQ), goes on with the Apps Installer:
62
+ `<MANAGE>/references/app-installation/install-wix-apps.md`. The app ids are in each
63
+ `seed-<solution>.mjs`, which installs them before seeding.
64
+
65
+ ## 4. The seed
66
+
67
+ `seed-<solution>.mjs` is a sequence of REST calls, and `SEED.md` is its contract: the plan shape
68
+ (what to create, how many, which images), the order the entities need, and the traps the script
69
+ encodes (a Bookings service needs a category to be visible and takes resource ids, not staff ids;
70
+ a Forms field is registered by its `validation` block; a product's choice photos are linked after
71
+ the gallery holds them). Read `SEED.md`, read the script for any call `SEED.md` only names, and make the same
72
+ calls through `<MANAGE>`'s recipe for each (services, products, posts, events, collections). Keep
73
+ the script's rules: create, never delete, and report what the site already held. Image prompts
74
+ need the Media Manager; without it, products and services stay text-only, say so.
75
+
76
+ ## 5. The frontend
77
+
78
+ The shipped `app/` code cannot be built here, and the Astro pages cannot be released. A page can
79
+ still be shipped: one self-contained `index.html` that loads the Wix SDK from a package CDN and
80
+ talks to the site as a visitor, dropped onto the site from step 2 through
81
+ `<MANAGE>/references/sites/upload-static-site.md`. The kit's own frontend for this site is a run
82
+ with a machine, which attaches to this site (`guides/existing-site.md`) rather than making a new one.
83
+
84
+ **The page.** One file, a few hundred lines, written in full before the first drop: the drop takes
85
+ the complete file set in one call, and a response has a hard size limit, so there is no second
86
+ pass. The SDK comes from a pinned package URL, `https://esm.sh/@wix/sdk@<version>` and the
87
+ solution's package (`@wix/bookings`, `@wix/stores`, `@wix/blog`, …); the client is
88
+ `createClient({ modules: { … }, auth: OAuthStrategy({ clientId }) })` with the `appId` the provision
89
+ call returned, and the SDK mints and refreshes the visitor token itself. Markup, a render function
90
+ per state, the event handlers: that is what you write. Keep what the page shows to what the site
91
+ holds; nothing invented, as in SKILL.md step 4.
92
+
93
+ **The calls.** Take every SDK call's shape from the kit's own transport for the solution,
94
+ `<TEMPLATES>/<solution>/app/wix/<solution>/*.ts` (`services.ts`, `booking.ts`, `catalog.ts`, …,
95
+ with `<TEMPLATES>/shared/app/wix/sdk.ts` for the client wiring): those files call the same SDK
96
+ modules the page imports, and they are verified. Copy the form of the call, not the file. The
97
+ cores beside them (`*-core.ts`) hold the rules the page needs and nothing else does: which fields
98
+ to read, how a price or a duration is formatted, which slot is bookable, how an id is read.
99
+
100
+ **A call those files don't cover** is confirmed before it is written, the way `<DOCS>` describes:
101
+ the method's SDK page, and when the page and the package disagree, the package's own type
102
+ declarations (the `@wix/auto_sdk_<solution>_<module>` package the solution package depends on,
103
+ its `index.d.ts`) are the truth. Two shapes that recur, as examples of what the types settle and
104
+ the kit's files already encode: SDK query methods return a query builder finished with `.find()`;
105
+ and an entity's id arrives as `_id` on some objects and `id` on others (the kit reads both,
106
+ `rawId`). Dates travel as local
107
+ wall-clock strings in the business time zone. A page cannot be run here, so a wrong shape fails
108
+ silently in the browser; the file beside the skill is the check that is available.
109
+
110
+ ## 6. Closing
111
+
112
+ As in SKILL.md step 5, minus the live URL when nothing was released: what exists on the site, by
113
+ name; the dashboard link `https://manage.wix.com/dashboard/<metaSiteId>` and each solution's own
114
+ pages from its INSTRUCTIONS.md table; what is placeholder; payments, when the site takes money;
115
+ and what this run could not do, with the site id, so a run with a machine continues here.
@@ -44,7 +44,7 @@
44
44
  // business instead.
45
45
  //
46
46
  // Emits ONE JSON event per line (attached, scaffolded, deployed, install_started,
47
- // ready_for_brand_layer, or error). Requires a logged-in Wix CLI (`npx @wix/cli@latest whoami`)
47
+ // ready_for_brand_layer, or error). Requires a logged-in Wix CLI (`npx @wix/cli@<pinned> whoami`, the version in pins.json)
48
48
  // whose account owns or co-manages the site.
49
49
  import { spawn, spawnSync, execFileSync } from "node:child_process";
50
50
  import { cpSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
@@ -53,6 +53,8 @@ import { fileURLToPath } from "node:url";
53
53
  import { writeAgentsMd } from "./agents-md.mjs";
54
54
  import { frontendPresent, siteContext } from "./context.mjs";
55
55
  import { listVerticals, templatesDir } from "./templates.mjs";
56
+ const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
57
+ const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
56
58
 
57
59
  const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
58
60
  const MANAGE = "https://manage.wix.com";
@@ -135,9 +137,9 @@ emit("folder", { mode, stack, hosting: hosting0, project: hasProject, config: cw
135
137
 
136
138
  // ---- http ---------------------------------------------------------------------------------------
137
139
  const cliToken = (site) => {
138
- const r = spawnSync("npx", ["-y", "@wix/cli@latest", "token", ...(site ? ["--site", site] : [])], { encoding: "utf8", timeout: 120_000 });
140
+ const r = spawnSync("npx", ["-y", WIX_CLI, "token", ...(site ? ["--site", site] : [])], { encoding: "utf8", timeout: 120_000 });
139
141
  const t = (r.stdout || "").trim();
140
- if (r.status !== 0 || !t) fail("auth", (r.stderr || r.stdout || "no token — is the Wix CLI logged in? (npx @wix/cli@latest whoami)").slice(-400));
142
+ if (r.status !== 0 || !t) fail("auth", (r.stderr || r.stdout || `no token — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).slice(-400));
141
143
  return t;
142
144
  };
143
145
  async function call(base, path, { method = "POST", token, site, body, query } = {}) {
@@ -3,6 +3,8 @@ import { spawn, spawnSync } from 'node:child_process';
3
3
  import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
+ const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
7
+ const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
6
8
 
7
9
  // ── tiny event protocol (one JSON object per line) ───────────────────────────
8
10
  const emit = (event, extra = {}) =>
@@ -15,7 +17,7 @@ const fail = (event, extra = {}) => {
15
17
  // ── platform-safe binary names (npm/npx are .cmd on Windows) ─────────────────
16
18
  const isWin = process.platform === 'win32';
17
19
  const bin = (name) => (isWin ? `${name}.cmd` : name);
18
- const WIX = [bin('npx'), '-y', '@wix/cli@latest']; // run the CLI via npx — no global install/mutation
20
+ const WIX = [bin('npx'), '-y', WIX_CLI]; // run the CLI via npx — no global install/mutation
19
21
 
20
22
  // Force the CLI into non-interactive "agent" mode. Without an agent signal in
21
23
  // the env, `wix login` renders an interactive Ink TUI (device code + keypress)
@@ -19,6 +19,8 @@
19
19
  import { spawnSync } from "node:child_process";
20
20
  import { existsSync, readFileSync } from "node:fs";
21
21
  import { join } from "node:path";
22
+ const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
23
+ const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
22
24
 
23
25
  // The variables the editor-to-headless migration writes into the child project's `prod`
24
26
  // environment (headless-bo, editorMigrations/environmentMetadata.ts), which `wix env pull` copies
@@ -57,7 +59,7 @@ export function readWixConfig(cwd) {
57
59
  try { return JSON.parse(readFileSync(file, "utf8")); } catch { return null; }
58
60
  }
59
61
 
60
- const ENV_PULL = ["-y", "@wix/cli@latest", "env", "pull"];
62
+ const ENV_PULL = ["-y", WIX_CLI, "env", "pull"];
61
63
  const runPull = (dir) => spawnSync("npx", ENV_PULL, { cwd: dir, env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 180_000 });
62
64
 
63
65
  /**
@@ -70,7 +72,7 @@ export function pullEnv(cwd) {
70
72
  const envFile = join(cwd, ".env.local");
71
73
  const r = runPull(cwd);
72
74
  if (r.status === 0 && existsSync(envFile)) return { ok: true, via: "in place" };
73
- return { ok: false, error: (r.stderr || r.stdout || "env pull produced no .env.local — is the Wix CLI logged in? (npx @wix/cli@latest whoami)").trim().slice(-400) };
75
+ return { ok: false, error: (r.stderr || r.stdout || `env pull produced no .env.local — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).trim().slice(-400) };
74
76
  }
75
77
 
76
78
  /**
@@ -46,6 +46,8 @@ import { fileURLToPath } from "node:url";
46
46
  import { templatesDir } from "./templates.mjs";
47
47
  import { syncLockRoot } from "./lock.mjs";
48
48
  import { siteContext } from "./context.mjs";
49
+ const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
50
+ const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
49
51
 
50
52
  const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
51
53
  // The shipped code: the repository's templates/ (a checkout, the cache, or fetched now).
@@ -275,7 +277,7 @@ if (stack === "static") {
275
277
  // .env.local (`wix env pull`; on a migration preview the parent site's app), else the config's appId.
276
278
  let clientId = clientIdFlag ?? (existsSync(join(PROJECT, "wix.config.json")) ? siteContext({ cwd: PROJECT }).content.clientId : null);
277
279
  if (!clientId) {
278
- console.log(JSON.stringify({ error: "static stack needs the public OAuth client id — run `npm create @wix/new@latest init` here first, or pass --client-id" }));
280
+ console.log(JSON.stringify({ error: `static stack needs the public OAuth client id — run \`npm create @wix/new@${PINS["@wix/create-new"]} init\` here first, or pass --client-id` }));
279
281
  process.exit(1);
280
282
  }
281
283
  const missing = requested.filter((v) => !existsSync(join(REF, v, "rest")));
@@ -336,7 +338,7 @@ if (stack === "static") {
336
338
  const sources = readdirSync(JS).filter((f) => f.endsWith(".ts")).map((f) => join(JS, f));
337
339
  const tsc = spawnSync(
338
340
  "npx",
339
- ["-y", "-p", "typescript@5", "tsc", ...sources, "--outDir", JS, "--module", "esnext", "--target", "es2022",
341
+ ["-y", "-p", `typescript@${PINS.typescript}`, "tsc", ...sources, "--outDir", JS, "--module", "esnext", "--target", "es2022",
340
342
  "--moduleResolution", "bundler", "--lib", "es2022,dom", "--strict", "--skipLibCheck", "--removeComments", "false"],
341
343
  { encoding: "utf8", timeout: 180_000 },
342
344
  );
@@ -0,0 +1,8 @@
1
+ {
2
+ "@wix/cli": "1.1.257",
3
+ "@wix/create-new": "0.0.123",
4
+ "typescript": "5.8.3",
5
+ "templates": {
6
+ "ref": "v1.28.0"
7
+ }
8
+ }
@@ -37,6 +37,8 @@ import { fileURLToPath } from "node:url";
37
37
  import { AGENT_CONFIG_FILES, writeAgentsMd } from "./agents-md.mjs";
38
38
  import { listVerticals, templatesDir } from "./templates.mjs";
39
39
  import { folderShape, siteContext } from "./context.mjs";
40
+ const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
41
+ const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
40
42
 
41
43
  const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
42
44
 
@@ -134,14 +136,14 @@ if (mode === "create") {
134
136
  // commit" is noise here, and becomes a nested-repo (submodule gitlink) hazard
135
137
  // if the project is later placed inside an existing repo. --skip-install for the
136
138
  // same reason: deps install in a detached step below.
137
- ["create", "@wix/new@latest", "--", "headless",
139
+ ["create", `@wix/new@${PINS["@wix/create-new"]}`, "--", "headless",
138
140
  "--folder-name", folderName, "--business-name", businessName,
139
141
  "--site-template", "blank", ...(template ? ["--template-path", template] : []),
140
142
  "--skip-install", "--skip-git", "--no-publish"],
141
143
  { env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 300_000 },
142
144
  );
143
145
  if (scaffold.status !== 0 || !existsSync(join(projectDir, "wix.config.json"))) {
144
- fail("scaffold", (scaffold.stderr || scaffold.stdout || "scaffold produced no wix.config.json — is the Wix CLI logged in? (npx @wix/cli@latest whoami)").slice(-600));
146
+ fail("scaffold", (scaffold.stderr || scaffold.stdout || `scaffold produced no wix.config.json — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).slice(-600));
145
147
  }
146
148
  }
147
149
  } else if (mode === "migrate") {
@@ -177,10 +179,10 @@ if (mode === "create") {
177
179
  // wix.config.json and .env.local, touches nothing else. Non-interactive under CI=1; the site is
178
180
  // named after the folder (rename it in the dashboard).
179
181
  emit("adopting", { folder: cwd });
180
- const init = spawnSync("npm", ["create", "@wix/new@latest", "--", "init"],
182
+ const init = spawnSync("npm", ["create", `@wix/new@${PINS["@wix/create-new"]}`, "--", "init"],
181
183
  { cwd, env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 300_000 });
182
184
  if (init.status !== 0 || !has("wix.config.json")) {
183
- fail("init", (init.stderr || init.stdout || "init produced no wix.config.json — is the Wix CLI logged in? (npx @wix/cli@latest whoami)").slice(-600));
185
+ fail("init", (init.stderr || init.stdout || `init produced no wix.config.json — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).slice(-600));
184
186
  }
185
187
  }
186
188
  const wixConfig = JSON.parse(readFileSync(join(projectDir, "wix.config.json"), "utf8"));
@@ -296,10 +298,10 @@ if (planPath && madeTheSite) {
296
298
 
297
299
  // ---- done ----------------------------------------------------------------------------------------
298
300
  const release = {
299
- astro: "npx @wix/cli@latest build, then npx @wix/cli@latest release",
300
- react: "the project's own build, then npx @wix/cli@latest release of the build folder named in wix.config.json (what Wix hosting serves and how routes must be shaped: SKILL.md step 1)",
301
- lib: "the project's own build, then npx @wix/cli@latest release of the build folder named in wix.config.json (SKILL.md step 1)",
302
- static: `npx @wix/cli@latest release — no build; it uploads ${STATIC_OUT}/ whole (wix.config.json site.outputDirectory), so the pages, styles and assets move into ${STATIC_OUT}/ first and import the modules from ./js/wix/ there; the root keeps the config, the plan, the seed output and the skills`,
301
+ astro: `npx ${WIX_CLI} build, then npx ${WIX_CLI} release`,
302
+ react: `the project's own build, then npx ${WIX_CLI} release of the build folder named in wix.config.json (what Wix hosting serves and how routes must be shaped: SKILL.md step 1)`,
303
+ lib: `the project's own build, then npx ${WIX_CLI} release of the build folder named in wix.config.json (SKILL.md step 1)`,
304
+ static: `npx ${WIX_CLI} release — no build; it uploads ${STATIC_OUT}/ whole (wix.config.json site.outputDirectory), so the pages, styles and assets move into ${STATIC_OUT}/ first and import the modules from ./js/wix/ there; the root keeps the config, the plan, the seed output and the skills`,
303
305
  }[stack];
304
306
  // On a migration preview the links and `siteId` are the PARENT's — the site whose content the pages
305
307
  // show and whose dashboard manages it; `deploySiteId` is where `wix release` goes.
@@ -11,9 +11,13 @@
11
11
  // 2. the cache `<SKILL_ROOT>/templates/`, filled by an earlier call (`--refresh` refetches).
12
12
  // 3. a fetch: a sparse, shallow clone of `skills/wix-headless-templates/` from the repository the skill was installed
13
13
  // from (skills-lock.json's `source`, default wix/skills), at the branch or tag the install
14
- // named (its `ref`, falling back to the default branch when that ref no longer exists) or the
15
- // repository's default branch. The lock records no commit, so there is nothing more exact to
16
- // pin to; `WIX_HEADLESS_FAST_TEMPLATES_REF=<branch|tag|sha>` overrides the ref.
14
+ // named (its `ref`, falling back to the default branch when that ref no longer exists), else
15
+ // the repository's default branch: a skill installed from the repository tracks the repository,
16
+ // so the templates match the kit beside them. A kit that came from a package (no skills-lock.json
17
+ // above it) fetches at the release tag in `install/pins.json` (`templates.ref`, kept equal to the
18
+ // package version by the release flow), the code it was released with; the package carries the
19
+ // templates skill beside the kit anyway, so that fetch is the rare path.
20
+ // `WIX_HEADLESS_FAST_TEMPLATES_REF=<branch|tag|sha>` overrides all of it.
17
21
  // The cache stays with the project: its `.gitignore` leaves out only the composed `project/`
18
22
  // folders (the scaffolds with their lockfiles, read once, at create or attach) and the repository
19
23
  // tooling, so the code layers, playbooks, seeds and readers are committed at the commit the project
@@ -29,6 +33,8 @@ export const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..")
29
33
  export const DEFAULT_REPO = "https://github.com/wix/skills.git";
30
34
  /** The templates skill's folder in the repository. */
31
35
  export const TEMPLATES_PATH = "skills/wix-headless-templates";
36
+ /** Pinned versions, shared by the install scripts; `templates.ref` is the release tag the fetch defaults to. */
37
+ export const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
32
38
 
33
39
  const git = (args, opts = {}) => spawnSync("git", args, { encoding: "utf8", timeout: 180_000, ...opts });
34
40
 
@@ -46,7 +52,7 @@ export function installSource() {
46
52
  if (typeof entry?.source === "string" && entry.source) {
47
53
  const parsed = parseSource(entry.source);
48
54
  // `ref` is the branch or tag the install named (skills-lock.json v1 keeps it beside `source`)
49
- return { ...parsed, ref: parsed.ref ?? (typeof entry.ref === "string" && entry.ref ? entry.ref : null) };
55
+ return { ...parsed, ref: parsed.ref ?? (typeof entry.ref === "string" && entry.ref ? entry.ref : null), fromLock: true };
50
56
  }
51
57
  } catch { /* fall through to the default */ }
52
58
  break;
@@ -55,7 +61,7 @@ export function installSource() {
55
61
  if (up === dir) break;
56
62
  dir = up;
57
63
  }
58
- return { repo: DEFAULT_REPO, ref: null };
64
+ return { repo: DEFAULT_REPO, ref: null, fromLock: false };
59
65
  }
60
66
 
61
67
  function parseSource(src) {
@@ -112,15 +118,19 @@ export function templatesSource(dir) {
112
118
  }
113
119
 
114
120
  function fetchTemplates(cache) {
115
- const { repo, ref: lockRef } = installSource();
121
+ const { repo, ref: lockRef, fromLock } = installSource();
116
122
  const envRef = process.env.WIX_HEADLESS_FAST_TEMPLATES_REF || null;
117
- let ref = envRef || lockRef || null;
123
+ // Installed from the repository (a lock above the skill) and no ref named: the default branch, so
124
+ // the templates match the kit. No lock at all is a package install: the release tag it shipped with.
125
+ const pinnedRef = fromLock ? null : (PINS.templates?.ref || null);
126
+ let ref = envRef || lockRef || pinnedRef || null;
118
127
  let r = cloneSparse(repo, ref);
119
128
  // The branch the skill was installed from can be gone by the time the code is first needed
120
129
  // (merged and deleted). The lock's ref then falls back to the default branch; an explicit env
121
130
  // ref does not.
122
131
  let fellBack = false;
123
132
  if (r.status !== 0 && ref && !envRef && /not found|couldn't find remote ref|unknown revision/i.test(r.stderr || "")) {
133
+ // (the pinned tag, too: a fork or a pre-release checkout may not carry it)
124
134
  rmSync(r.tmp, { recursive: true, force: true });
125
135
  fellBack = true; ref = null;
126
136
  r = cloneSparse(repo, null);
@@ -141,7 +151,7 @@ function fetchTemplates(cache) {
141
151
  catch { cpSync(join(tmp, TEMPLATES_PATH), cache, { recursive: true }); }
142
152
  rmSync(tmp, { recursive: true, force: true });
143
153
  writeFileSync(join(cache, ".gitignore"), IGNORED.join("\n") + "\n");
144
- writeFileSync(join(cache, ".source"), JSON.stringify({ repo, ref: ref ?? (fellBack ? `default branch (${lockRef} not found)` : "default branch"), commit, fetchedAt: new Date().toISOString() }, null, 2) + "\n");
154
+ writeFileSync(join(cache, ".source"), JSON.stringify({ repo, ref: ref ?? (fellBack ? `default branch (${lockRef || pinnedRef} not found)` : "default branch"), commit, fetchedAt: new Date().toISOString() }, null, 2) + "\n");
145
155
  }
146
156
 
147
157
  // A sparse, shallow clone of the templates skill's folder at a ref (branch, tag, or commit id) into a temp dir.
@@ -6,8 +6,9 @@ typed end-to-end. **The presentation is yours**: you design and implement the ca
6
6
  campaigns index, the campaign page's layout around the shipped form, the home page and the brand. You
7
7
  never write checkout logic; you never skip designing.
8
8
 
9
- A donation is an eCom checkout with one line that references the campaign; Wix's hosted checkout takes
10
- the payment and the donor's details, and the eCom order is the receipt. The campaign entity carries
9
+ A donation is an eCom cart (Cart V2) with one line that references the campaign; in V2 the cart IS the
10
+ checkout, so the cart's id is the id the redirect session takes — there is no separate checkout entity.
11
+ Wix's hosted checkout takes the payment and the donor's details, and the eCom order is the receipt. The campaign entity carries
11
12
  **no description and no slug**: the story on a campaign page is the site's own copy, and routes key by
12
13
  the campaign id (`/donate/<id>`).
13
14
 
@@ -29,7 +30,7 @@ Astro markup), your campaign page layout, and your home page.
29
30
  | `wix/media.ts` · `wix/money.ts` | `imgAttrs(url, sizes, ratio)` — every `<img>` attribute for a DTO image: `<img {...imgAttrs(c.imageUrl, "50vw", 9 / 16)} alt={c.name} />`; `imgSrc()` / `imgSrcSet()` underneath |
30
31
  | `wix/donations/types.ts` | the DTOs (`CampaignSummary`, `CampaignDetail`, `GoalProgress`, `DonationOptions`, `DonationReceipt`) — contracts inlined below |
31
32
  | `wix/donations/campaigns.ts` | `fetchCampaigns`, `fetchCampaign(id)`, `fetchDefaultCampaign` — the transport; the rules and DTO mappers are in `donations-core.ts` beside it (shared with the REST layer) |
32
- | `wix/donations/donate.ts` | `donationCheckoutUrl(campaignId, input)` — the checkout line + hosted redirect session; `fetchDonationReceipt(orderId)` — the thank-you page's order read; bodies in `donations-core.ts` |
33
+ | `wix/donations/donate.ts` | `donationCheckoutUrl(campaignId, input)` — the donation cart + hosted redirect session; `fetchDonationReceipt(orderId)` — the thank-you page's order read; bodies in `donations-core.ts` |
33
34
  | `wix/donations/donation-store.ts` · `campaigns-store.ts` · `receipt-store.ts` | the form, listing and receipt state machines, framework-free (`createDonationStore(campaign)`, `createCampaignsStore()`, `createReceiptStore()` — `getState`/`subscribe` + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
34
35
  | `hooks/donations/useDonation.ts` | React binding of `donation-store.ts`: the donate form's state and actions — contract below |
35
36
  | `hooks/donations/useCampaigns.ts` | React binding of `campaigns-store.ts`: the listing — contract below |
@@ -328,12 +329,10 @@ first real site and read the owning file if one bites.
328
329
  shows) and `options.currency` "" (the button reads "Donate" without an amount; preset labels still
329
330
  carry the API's own formatted amounts). If the campaign query itself is refused, the pages render
330
331
  their empty state.
331
- - **Direct Create Checkout vs the Wix widget's cart route.** The default creates the checkout directly
332
- (a donation must never merge into a storefront visitor's cart); if the Donations catalog refuses to
333
- price that line, `donationCheckoutUrl` falls back to the widget's own Create Cart → Create Checkout
334
- (`channelType: OTHER_PLATFORM`), reachable by name as `createDonationCheckoutViaCart`. The cart route
335
- can answer `CURRENT_CART_ALREADY_EXISTS` for a visitor who already holds a cart — surfaced as the
336
- form's error.
332
+ - **Create Cart is the whole path (Cart V2).** `donationCheckoutUrl` creates one fresh cart with the
333
+ donation line (`cart.source.channelType: WEB`, the donor note as `cart.note`) and hands that cart's
334
+ id straight to the redirect session — the cart is the checkout, so there is no separate Create
335
+ Checkout call and nothing merges into a storefront visitor's current cart.
337
336
  - **Site currency.** Comes from the metrics response ("returns only the site's default currency");
338
337
  On a campaign nobody has donated to yet the metrics carry no currency either, so the transports read the site's currency once from the eCommerce settings (BUSINESS_INFO); unknown → "".
339
338
  - **Reading the order on the thank-you page.** `GET /ecom/v1/orders/{id}` with the visitor's token is
@@ -1,29 +1,19 @@
1
- // The donation flow over the SDK: a visitor's choice → an eCom checkout with one donation line →
2
- // the Wix-hosted checkout URL; and the thank-you page's order read. Payment, donor details and
3
- // receipts are Wix's hosted checkout and the eCom order — nothing here creates an order or builds
4
- // a URL by hand. Bodies and result readers live in ./donations-core (shared with the REST twin);
5
- // this file is the transport only. Copy as-is.
6
- //
7
- // Two routes to a checkout id, both kept:
8
- // createDonationCheckout — Create Checkout with the line (a donation never merges into a
9
- // storefront visitor's current cart). The default.
10
- // createDonationCheckoutViaCart — the Wix widget's own sequence: Create Cart → Create Checkout
11
- // from the cart, channelType OTHER_PLATFORM. The fallback when
12
- // the direct route is refused; also reachable by name.
13
- // Whether the Donations catalog plugin prices a line created directly is not yet verified live,
14
- // hence the automatic fallback.
15
- // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/checkout/create-checkout.md
1
+ // The donation flow over the SDK: a visitor's choice → an eCom cart with one donation line → the
2
+ // Wix-hosted checkout URL; and the thank-you page's order read. In Cart V2 there is no separate
3
+ // checkout entity: the cart IS the checkout, so its id is the id the redirect session takes.
4
+ // Payment, donor details and receipts are Wix's hosted checkout and the eCom order — nothing here
5
+ // creates an order or builds a URL by hand. Bodies and result readers live in ./donations-core
6
+ // (shared with the REST twin); this file is the transport only. Copy as-is.
7
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md
16
8
  // docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
17
9
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/orders/get-order.md
18
- import { cart as cartModule, checkout as checkoutModule, orders as ordersModule } from "@wix/ecom";
10
+ import { cartV2 as cartV2Module, orders as ordersModule } from "@wix/ecom";
19
11
  import { redirects as redirectsModule } from "@wix/redirects";
20
12
  import { wixModule } from "../sdk";
21
13
  import {
22
14
  cartBody,
23
- checkoutBody,
15
+ cartIdOf,
24
16
  checkoutError,
25
- checkoutIdOf,
26
- rawId,
27
17
  redirectBody,
28
18
  redirectUrl,
29
19
  toReceipt,
@@ -34,8 +24,7 @@ import type { DonationInput, DonationReceipt } from "./types";
34
24
 
35
25
  export type { DonatePaths };
36
26
 
37
- const checkoutApi = wixModule(checkoutModule);
38
- const cartApi = wixModule(cartModule);
27
+ const cartV2Api = wixModule(cartV2Module);
39
28
  const ordersApi = wixModule(ordersModule);
40
29
  const redirects = wixModule(redirectsModule);
41
30
 
@@ -46,37 +35,21 @@ export interface DonateOptions {
46
35
  paths?: DonatePaths;
47
36
  }
48
37
 
49
- /** Create Checkout with the donation line → checkout id. */
50
- export async function createDonationCheckout(campaignId: string, input: DonationInput): Promise<string> {
51
- const checkout: Raw = await checkoutApi.createCheckout(checkoutBody(campaignId, input) as any);
52
- return checkoutIdOf(checkout);
53
- }
54
-
55
- /** The Wix widget's route: Create Cart (the note as buyerNote) → Create Checkout from the cart → checkout id. */
56
- export async function createDonationCheckoutViaCart(campaignId: string, input: DonationInput): Promise<string> {
57
- const cart: Raw = await cartApi.createCart(cartBody(campaignId, input) as any);
58
- const res: Raw = await cartApi.createCheckout(rawId(cart), { channelType: "OTHER_PLATFORM" });
59
- return checkoutIdOf(res);
60
- }
61
-
62
38
  /**
63
39
  * Start the hosted checkout for a donation; resolves to the URL to navigate the FULL document to.
64
- * Direct Create Checkout first, the cart route when that is refused, then one redirect session
65
- * whose thank-you callback lands on `/donate/thank-you?orderId=`.
40
+ * One Create Cart (the cart is the checkout) → one redirect session whose thank-you callback lands
41
+ * on `/donate/thank-you?orderId=`.
66
42
  */
67
43
  export async function donationCheckoutUrl(campaignId: string, input: DonationInput, { origin, paths }: DonateOptions = {}): Promise<string> {
68
44
  const site = origin ?? (typeof window !== "undefined" ? window.location.origin : "");
69
- let checkoutId: string;
45
+ let cartId: string;
70
46
  try {
71
- checkoutId = await createDonationCheckout(campaignId, input);
72
- } catch (direct) {
73
- try {
74
- checkoutId = await createDonationCheckoutViaCart(campaignId, input);
75
- } catch {
76
- throw checkoutError(direct);
77
- }
47
+ const cart: Raw = await cartV2Api.createCart(cartBody(campaignId, input) as any);
48
+ cartId = cartIdOf(cart);
49
+ } catch (e) {
50
+ throw checkoutError(e);
78
51
  }
79
- const session: Raw = await redirects.createRedirectSession(redirectBody(checkoutId, campaignId, site, paths) as any);
52
+ const session: Raw = await redirects.createRedirectSession(redirectBody(cartId, campaignId, site, paths) as any);
80
53
  return redirectUrl(session);
81
54
  }
82
55
 
@@ -1,7 +1,7 @@
1
1
  // Donation rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./campaigns.ts
2
2
  // + ./donate.ts (the SDK, managed Astro and React) and the REST twins in templates/donations/rest/
3
3
  // (fetch, a static site or a port to another language). Every rule about amounts, fees, frequency,
4
- // goal math, validation, the checkout line and the redirect body lives HERE, once. A raw campaign
4
+ // goal math, validation, the cart line and the redirect body lives HERE, once. A raw campaign
5
5
  // may come from the SDK (`_id`, coverImage as a `wix:image://` string) or from REST (`id`,
6
6
  // coverImage as { id, url }); the mappers accept both. Has its own formatMoney so it stands alone
7
7
  // when stripped. Imports are type-only so a strip to JS emits no imports.
@@ -351,10 +351,10 @@ export function toInput(options: DonationOptions, sel: DonationSelection): Donat
351
351
  return { amount, frequency: sel.frequency, coverFee: options.askCoverFee && sel.coverFee, note: sel.note.trim() };
352
352
  }
353
353
 
354
- // ---- checkout -------------------------------------------------------------------------------------
354
+ // ---- cart -----------------------------------------------------------------------------------------
355
355
 
356
356
  /**
357
- * The one line item a donation checkout carries. `amount` is a NUMBER and `frequency` the enum
357
+ * The one catalog item a donation cart carries. `amount` is a NUMBER and `frequency` the enum
358
358
  * string; `donorCoveringFees` is sent only when the donor opted in (the Donations catalog plugin
359
359
  * prices the line from these options — never a customLineItem, never a computed price).
360
360
  */
@@ -370,27 +370,26 @@ export function donationLineItem(campaignId: string, input: DonationInput): Raw
370
370
  };
371
371
  }
372
372
 
373
- /** Create Checkout body: the donation line, channelType WEB, the note as the checkout's buyerNote. */
374
- export function checkoutBody(campaignId: string, input: DonationInput): Raw {
375
- return {
376
- lineItems: [donationLineItem(campaignId, input)],
377
- channelType: "WEB",
378
- ...(input.note ? { checkoutInfo: { buyerNote: input.note } } : {}),
379
- };
380
- }
381
-
382
- /** Create Cart body — the Wix widget's own route (fallback): the note as the cart's buyerNote. */
373
+ /**
374
+ * Create Cart body (Cart V2): the single donation catalog item, the WEB channel on the cart's
375
+ * `source`, the donor note as the cart's `note`. In V2 the created cart IS the checkout — its id is
376
+ * the id the redirect session takes; there is no separate Create Checkout step.
377
+ */
383
378
  export function cartBody(campaignId: string, input: DonationInput): Raw {
384
379
  return {
385
- lineItems: [donationLineItem(campaignId, input)],
386
- ...(input.note ? { cartInfo: { buyerNote: input.note } } : {}),
380
+ cart: { source: { channelType: "WEB" }, ...(input.note ? { note: input.note } : {}) },
381
+ catalogItems: [donationLineItem(campaignId, input)],
387
382
  };
388
383
  }
389
384
 
390
- /** The checkout id out of either transport's response for either route. */
391
- export function checkoutIdOf(res: Raw | null | undefined): string {
392
- const id = res?._id ?? res?.id ?? res?.checkoutId ?? res?.checkout?.id ?? res?.checkout?._id ?? "";
393
- if (!id) throw new Error("Checkout couldn't start: no checkout id returned.");
385
+ /**
386
+ * The cart id out of the Create Cart response — the id a V2 donation hands the redirect session
387
+ * (the cart is the checkout). The SDK returns a Cart directly (`_id`); REST wraps it as
388
+ * `{ cart: { id } }`.
389
+ */
390
+ export function cartIdOf(res: Raw | null | undefined): string {
391
+ const id = res?._id ?? res?.id ?? res?.cart?._id ?? res?.cart?.id ?? "";
392
+ if (!id) throw new Error("Checkout couldn't start: no cart id returned.");
394
393
  return String(id);
395
394
  }
396
395
 
@@ -1,29 +1,19 @@
1
- // The donation flow over the SDK: a visitor's choice → an eCom checkout with one donation line →
2
- // the Wix-hosted checkout URL; and the thank-you page's order read. Payment, donor details and
3
- // receipts are Wix's hosted checkout and the eCom order — nothing here creates an order or builds
4
- // a URL by hand. Bodies and result readers live in ./donations-core (shared with the REST twin);
5
- // this file is the transport only. Copy as-is.
6
- //
7
- // Two routes to a checkout id, both kept:
8
- // createDonationCheckout — Create Checkout with the line (a donation never merges into a
9
- // storefront visitor's current cart). The default.
10
- // createDonationCheckoutViaCart — the Wix widget's own sequence: Create Cart → Create Checkout
11
- // from the cart, channelType OTHER_PLATFORM. The fallback when
12
- // the direct route is refused; also reachable by name.
13
- // Whether the Donations catalog plugin prices a line created directly is not yet verified live,
14
- // hence the automatic fallback.
15
- // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/checkout/create-checkout.md
1
+ // The donation flow over the SDK: a visitor's choice → an eCom cart with one donation line → the
2
+ // Wix-hosted checkout URL; and the thank-you page's order read. In Cart V2 there is no separate
3
+ // checkout entity: the cart IS the checkout, so its id is the id the redirect session takes.
4
+ // Payment, donor details and receipts are Wix's hosted checkout and the eCom order — nothing here
5
+ // creates an order or builds a URL by hand. Bodies and result readers live in ./donations-core
6
+ // (shared with the REST twin); this file is the transport only. Copy as-is.
7
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md
16
8
  // docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
17
9
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/orders/get-order.md
18
- import { cart as cartModule, checkout as checkoutModule, orders as ordersModule } from "@wix/ecom";
10
+ import { cartV2 as cartV2Module, orders as ordersModule } from "@wix/ecom";
19
11
  import { redirects as redirectsModule } from "@wix/redirects";
20
12
  import { wixModule } from "../sdk";
21
13
  import {
22
14
  cartBody,
23
- checkoutBody,
15
+ cartIdOf,
24
16
  checkoutError,
25
- checkoutIdOf,
26
- rawId,
27
17
  redirectBody,
28
18
  redirectUrl,
29
19
  toReceipt,
@@ -34,8 +24,7 @@ import type { DonationInput, DonationReceipt } from "./types";
34
24
 
35
25
  export type { DonatePaths };
36
26
 
37
- const checkoutApi = wixModule(checkoutModule);
38
- const cartApi = wixModule(cartModule);
27
+ const cartV2Api = wixModule(cartV2Module);
39
28
  const ordersApi = wixModule(ordersModule);
40
29
  const redirects = wixModule(redirectsModule);
41
30
 
@@ -46,37 +35,21 @@ export interface DonateOptions {
46
35
  paths?: DonatePaths;
47
36
  }
48
37
 
49
- /** Create Checkout with the donation line → checkout id. */
50
- export async function createDonationCheckout(campaignId: string, input: DonationInput): Promise<string> {
51
- const checkout: Raw = await checkoutApi.createCheckout(checkoutBody(campaignId, input) as any);
52
- return checkoutIdOf(checkout);
53
- }
54
-
55
- /** The Wix widget's route: Create Cart (the note as buyerNote) → Create Checkout from the cart → checkout id. */
56
- export async function createDonationCheckoutViaCart(campaignId: string, input: DonationInput): Promise<string> {
57
- const cart: Raw = await cartApi.createCart(cartBody(campaignId, input) as any);
58
- const res: Raw = await cartApi.createCheckout(rawId(cart), { channelType: "OTHER_PLATFORM" });
59
- return checkoutIdOf(res);
60
- }
61
-
62
38
  /**
63
39
  * Start the hosted checkout for a donation; resolves to the URL to navigate the FULL document to.
64
- * Direct Create Checkout first, the cart route when that is refused, then one redirect session
65
- * whose thank-you callback lands on `/donate/thank-you?orderId=`.
40
+ * One Create Cart (the cart is the checkout) → one redirect session whose thank-you callback lands
41
+ * on `/donate/thank-you?orderId=`.
66
42
  */
67
43
  export async function donationCheckoutUrl(campaignId: string, input: DonationInput, { origin, paths }: DonateOptions = {}): Promise<string> {
68
44
  const site = origin ?? (typeof window !== "undefined" ? window.location.origin : "");
69
- let checkoutId: string;
45
+ let cartId: string;
70
46
  try {
71
- checkoutId = await createDonationCheckout(campaignId, input);
72
- } catch (direct) {
73
- try {
74
- checkoutId = await createDonationCheckoutViaCart(campaignId, input);
75
- } catch {
76
- throw checkoutError(direct);
77
- }
47
+ const cart: Raw = await cartV2Api.createCart(cartBody(campaignId, input) as any);
48
+ cartId = cartIdOf(cart);
49
+ } catch (e) {
50
+ throw checkoutError(e);
78
51
  }
79
- const session: Raw = await redirects.createRedirectSession(redirectBody(checkoutId, campaignId, site, paths) as any);
52
+ const session: Raw = await redirects.createRedirectSession(redirectBody(cartId, campaignId, site, paths) as any);
80
53
  return redirectUrl(session);
81
54
  }
82
55
 
@@ -1,7 +1,7 @@
1
1
  // Donation rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./campaigns.ts
2
2
  // + ./donate.ts (the SDK, managed Astro and React) and the REST twins in templates/donations/rest/
3
3
  // (fetch, a static site or a port to another language). Every rule about amounts, fees, frequency,
4
- // goal math, validation, the checkout line and the redirect body lives HERE, once. A raw campaign
4
+ // goal math, validation, the cart line and the redirect body lives HERE, once. A raw campaign
5
5
  // may come from the SDK (`_id`, coverImage as a `wix:image://` string) or from REST (`id`,
6
6
  // coverImage as { id, url }); the mappers accept both. Has its own formatMoney so it stands alone
7
7
  // when stripped. Imports are type-only so a strip to JS emits no imports.
@@ -351,10 +351,10 @@ export function toInput(options: DonationOptions, sel: DonationSelection): Donat
351
351
  return { amount, frequency: sel.frequency, coverFee: options.askCoverFee && sel.coverFee, note: sel.note.trim() };
352
352
  }
353
353
 
354
- // ---- checkout -------------------------------------------------------------------------------------
354
+ // ---- cart -----------------------------------------------------------------------------------------
355
355
 
356
356
  /**
357
- * The one line item a donation checkout carries. `amount` is a NUMBER and `frequency` the enum
357
+ * The one catalog item a donation cart carries. `amount` is a NUMBER and `frequency` the enum
358
358
  * string; `donorCoveringFees` is sent only when the donor opted in (the Donations catalog plugin
359
359
  * prices the line from these options — never a customLineItem, never a computed price).
360
360
  */
@@ -370,27 +370,26 @@ export function donationLineItem(campaignId: string, input: DonationInput): Raw
370
370
  };
371
371
  }
372
372
 
373
- /** Create Checkout body: the donation line, channelType WEB, the note as the checkout's buyerNote. */
374
- export function checkoutBody(campaignId: string, input: DonationInput): Raw {
375
- return {
376
- lineItems: [donationLineItem(campaignId, input)],
377
- channelType: "WEB",
378
- ...(input.note ? { checkoutInfo: { buyerNote: input.note } } : {}),
379
- };
380
- }
381
-
382
- /** Create Cart body — the Wix widget's own route (fallback): the note as the cart's buyerNote. */
373
+ /**
374
+ * Create Cart body (Cart V2): the single donation catalog item, the WEB channel on the cart's
375
+ * `source`, the donor note as the cart's `note`. In V2 the created cart IS the checkout — its id is
376
+ * the id the redirect session takes; there is no separate Create Checkout step.
377
+ */
383
378
  export function cartBody(campaignId: string, input: DonationInput): Raw {
384
379
  return {
385
- lineItems: [donationLineItem(campaignId, input)],
386
- ...(input.note ? { cartInfo: { buyerNote: input.note } } : {}),
380
+ cart: { source: { channelType: "WEB" }, ...(input.note ? { note: input.note } : {}) },
381
+ catalogItems: [donationLineItem(campaignId, input)],
387
382
  };
388
383
  }
389
384
 
390
- /** The checkout id out of either transport's response for either route. */
391
- export function checkoutIdOf(res: Raw | null | undefined): string {
392
- const id = res?._id ?? res?.id ?? res?.checkoutId ?? res?.checkout?.id ?? res?.checkout?._id ?? "";
393
- if (!id) throw new Error("Checkout couldn't start: no checkout id returned.");
385
+ /**
386
+ * The cart id out of the Create Cart response — the id a V2 donation hands the redirect session
387
+ * (the cart is the checkout). The SDK returns a Cart directly (`_id`); REST wraps it as
388
+ * `{ cart: { id } }`.
389
+ */
390
+ export function cartIdOf(res: Raw | null | undefined): string {
391
+ const id = res?._id ?? res?.id ?? res?.cart?._id ?? res?.cart?.id ?? "";
392
+ if (!id) throw new Error("Checkout couldn't start: no cart id returned.");
394
393
  return String(id);
395
394
  }
396
395
 
@@ -1,19 +1,17 @@
1
1
  // The donation flow over REST — the twin of app/wix/donations/donate.ts. Same exports, same
2
2
  // signatures; the bodies and result readers come from donations-core (the SAME file the SDK
3
- // transport uses). A donation is an eCom checkout with one catalog line; the hosted checkout takes
4
- // payment and donor details. Never create the order yourself, never hand-build a checkout URL.
5
- // Two routes to a checkout id, both kept: Create Checkout directly (default), and the Wix widget's
6
- // Create Cart → Create Checkout from the cart (the fallback when the direct route is refused).
7
- // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/checkout/create-checkout.md
3
+ // transport uses). A donation is an eCom cart with one catalog line; the hosted checkout takes
4
+ // payment and donor details. In Cart V2 the cart IS the checkout — its id is the id the redirect
5
+ // session takes, so there is one route and no separate Create Checkout call. Never create the
6
+ // order yourself, never hand-build a checkout URL.
7
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md
8
8
  // docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
9
9
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/orders/get-order.md
10
10
  import { wixRequest } from "./client.js";
11
11
  import {
12
12
  cartBody,
13
- checkoutBody,
13
+ cartIdOf,
14
14
  checkoutError,
15
- checkoutIdOf,
16
- rawId,
17
15
  redirectBody,
18
16
  redirectUrl,
19
17
  toReceipt,
@@ -31,46 +29,22 @@ export interface DonateOptions {
31
29
  paths?: DonatePaths;
32
30
  }
33
31
 
34
- /**
35
- * Create Checkout with the donation line → checkout id.
36
- * POST /ecom/v1/checkouts { lineItems: [{ quantity: 1, catalogReference: { appId, catalogItemId, options: { amount, frequency, donorCoveringFees? } } }], channelType: "WEB", checkoutInfo?: { buyerNote } } → { checkout: { id } }
37
- */
38
- export async function createDonationCheckout(campaignId: string, input: DonationInput): Promise<string> {
39
- const res = await wixRequest<Raw>("/ecom/v1/checkouts", { body: checkoutBody(campaignId, input) });
40
- return checkoutIdOf(res);
41
- }
42
-
43
- /**
44
- * The Wix widget's route → checkout id.
45
- * POST /ecom/v1/carts { lineItems, cartInfo?: { buyerNote } } → { cart: { id } }
46
- * POST /ecom/v1/carts/{id}/create-checkout { channelType: "OTHER_PLATFORM" } → { checkoutId }
47
- */
48
- export async function createDonationCheckoutViaCart(campaignId: string, input: DonationInput): Promise<string> {
49
- const created = await wixRequest<Raw>("/ecom/v1/carts", { body: cartBody(campaignId, input) });
50
- const cartId = rawId(created?.cart);
51
- if (!cartId) throw new Error("Checkout couldn't start: no cart id returned.");
52
- const res = await wixRequest<Raw>(`/ecom/v1/carts/${cartId}/create-checkout`, { body: { channelType: "OTHER_PLATFORM" } });
53
- return checkoutIdOf(res);
54
- }
55
-
56
32
  /**
57
33
  * Start the hosted checkout for a donation; resolves to the URL to navigate the FULL document to.
58
34
  * `origin` must be the site's real https origin as registered on the OAuth app's allowed domains.
35
+ * POST /ecom/v2/carts { cart: { source: { channelType: "WEB" }, note? }, catalogItems: [{ quantity: 1, catalogReference: { appId, catalogItemId, options: { amount, frequency, donorCoveringFees? } } }] } → { cart: { id } } (the cart is the checkout)
59
36
  * POST /headless/v1/redirect-session { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl, thankYouPageUrl } } → { redirectSession: { fullUrl } }
60
37
  */
61
38
  export async function donationCheckoutUrl(campaignId: string, input: DonationInput, { origin, paths }: DonateOptions = {}): Promise<string> {
62
39
  const site = origin ?? (typeof window !== "undefined" ? window.location.origin : "");
63
- let checkoutId: string;
40
+ let cartId: string;
64
41
  try {
65
- checkoutId = await createDonationCheckout(campaignId, input);
66
- } catch (direct) {
67
- try {
68
- checkoutId = await createDonationCheckoutViaCart(campaignId, input);
69
- } catch {
70
- throw checkoutError(direct);
71
- }
42
+ const res = await wixRequest<Raw>("/ecom/v2/carts", { body: cartBody(campaignId, input) });
43
+ cartId = cartIdOf(res);
44
+ } catch (e) {
45
+ throw checkoutError(e);
72
46
  }
73
- const session = await wixRequest<Raw>("/headless/v1/redirect-session", { body: redirectBody(checkoutId, campaignId, site, paths) });
47
+ const session = await wixRequest<Raw>("/headless/v1/redirect-session", { body: redirectBody(cartId, campaignId, site, paths) });
74
48
  return redirectUrl(session);
75
49
  }
76
50
 
@@ -336,8 +336,10 @@ other road — an eCom checkout with one PLAN line item (`catalogItemId: plan.id
336
336
  must share one checkout with products (a cart holding a membership and a T-shirt). Both end on a
337
337
  Wix-hosted checkout with coupons, notes, and login handled there. The DTO carries
338
338
  `pricingVariantId` so the eCom path is reachable without touching the DTO if a brief needs it;
339
- building it means `@wix/ecom` `checkout.createCheckout` plus a redirect session with
340
- `ecomCheckout: { checkoutId }` — a new function beside `purchase.ts`, not a change to it.
339
+ building it means adding the plan line (and any product lines) to the current cart with `@wix/ecom`
340
+ `currentCartV2` — in Cart V2 the cart IS the checkout, so the cart's id is the id passed to a
341
+ redirect session with `ecomCheckout: { checkoutId }` — a new function beside `purchase.ts`, not a
342
+ change to it.
341
343
 
342
344
  ## Gated content — requires the `members` vertical
343
345
 
@@ -29,9 +29,9 @@ if (!siteId) {
29
29
  console.log(JSON.stringify({ error: "no site: pass --site <siteId> or run in a folder with wix.config.json" }));
30
30
  process.exit(1);
31
31
  }
32
- const token = execFileSync("npx", ["-y", "@wix/cli@latest", "token", "--site", siteId], { encoding: "utf8" }).trim();
32
+ const token = execFileSync("npx", ["-y", "@wix/cli@1.1.257", "token", "--site", siteId], { encoding: "utf8" }).trim();
33
33
  if (!token) {
34
- console.log(JSON.stringify({ error: "the Wix CLI returned no token — run `npx @wix/cli@latest login` first" }));
34
+ console.log(JSON.stringify({ error: "the Wix CLI returned no token — run `npx @wix/cli@1.1.257 login` first" }));
35
35
  process.exit(1);
36
36
  }
37
37
  try {
@@ -1,12 +1,15 @@
1
1
  // The Wix CLI the seeds call for a site token and for `wix env pull`.
2
2
  //
3
3
  // Order: the project's own copy (node_modules/.bin/wix, installed with the template's lock), then
4
- // a `wix` on PATH, then `npx -y @wix/cli@latest` — which installs the CLI before running it, 20 to
4
+ // a `wix` on PATH, then `npx -y @wix/cli@<pinned>` — which installs the CLI before running it, 20 to
5
5
  // 60 s and a wall of npm output on a fresh machine. The first two take under two seconds.
6
6
  import { existsSync } from "node:fs";
7
7
  import { delimiter, join } from "node:path";
8
8
  import { execFileSync, spawnSync } from "node:child_process";
9
9
 
10
+ // The CLI version the fallback installs; equal to `@wix/cli` in wix-headless-kit/install/pins.json.
11
+ const WIX_CLI_VERSION = "1.1.257";
12
+
10
13
  export function resolveWixCli(cwd = process.cwd()) {
11
14
  const bin = process.platform === "win32" ? "wix.cmd" : "wix";
12
15
  const local = join(cwd, "node_modules", ".bin", bin);
@@ -19,7 +22,7 @@ export function resolveWixCli(cwd = process.cwd()) {
19
22
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
20
23
  if (dir && existsSync(join(dir, bin))) return { file: join(dir, bin), prefix: [] };
21
24
  }
22
- return { file: "npx", prefix: ["-y", "@wix/cli@latest"] };
25
+ return { file: "npx", prefix: ["-y", `@wix/cli@${WIX_CLI_VERSION}`] };
23
26
  }
24
27
 
25
28
  /**
@@ -33,7 +36,7 @@ export function wixToken(siteId, cwd = process.cwd()) {
33
36
  // An older CLI starts its "update available" box on the token's own line, so a line split is
34
37
  // not enough: the token is the first long run of token characters.
35
38
  const token = out.match(/[A-Za-z0-9._-]{40,}/)?.[0];
36
- if (!token) throw new Error("The Wix CLI returned no token — run `wix login` (or `npx @wix/cli@latest login`) first.");
39
+ if (!token) throw new Error(`The Wix CLI returned no token — run \`wix login\` (or \`npx @wix/cli@${WIX_CLI_VERSION} login\`) first.`);
37
40
  return token;
38
41
  }
39
42
 
@@ -167,8 +167,7 @@ export async function setNote(note: string): Promise<Cart> {
167
167
  /**
168
168
  * Start the Wix-hosted checkout for the current cart and return the URL to navigate to.
169
169
  * The cart's id IS the checkout id — no separate checkout-creation call (Wix's own storefront
170
- * does the same; `createCheckoutFromCurrentCart` is the alternative when a checkout must exist
171
- * before redirecting).
170
+ * does the same).
172
171
  * Call from the browser: the return origin must be the site's real https origin
173
172
  * (window.location.origin), never a server-derived request origin.
174
173
  */
@@ -167,8 +167,7 @@ export async function setNote(note: string): Promise<Cart> {
167
167
  /**
168
168
  * Start the Wix-hosted checkout for the current cart and return the URL to navigate to.
169
169
  * The cart's id IS the checkout id — no separate checkout-creation call (Wix's own storefront
170
- * does the same; `createCheckoutFromCurrentCart` is the alternative when a checkout must exist
171
- * before redirecting).
170
+ * does the same).
172
171
  * Call from the browser: the return origin must be the site's real https origin
173
172
  * (window.location.origin), never a server-derived request origin.
174
173
  */
@@ -16,7 +16,7 @@ Five ways to get the files live; what you have decides which are open to you.
16
16
  | Option | Needs | Carries | The user ends up with |
17
17
  | --- | --- | --- | --- |
18
18
  | **A.** `curl` + CLI token → [into the account](#publish-into-the-users-account) | A shell; a Wix CLI login | Anything on disk | A site in their account, final URL |
19
- | **B.** `ExecuteWixAPI` → [into the account](#publish-into-the-users-account) | The Wix MCP | Small text files already in the conversation | A site in their account, final URL |
19
+ | **B.** `ExecuteWixAPI` → [into the account](#publish-into-the-users-account) | The Wix MCP | Files already in the conversation — text as is, small binaries as base64 — and anything downloaded from the site | A site in their account, final URL |
20
20
  | **C.** `curl` → [anonymous](#publish-anonymously) | A shell | Anything on disk | A live site for one hour; kept by a [claim](#claim-it-into-the-users-account) (through the Wix MCP or a CLI token) or the save link |
21
21
  | **D.** [The drop page](#the-drop-page) | Nothing | Whatever the user uploads | The same, after they upload it themselves |
22
22
  | **E.** [The headless skill](#keep-building-add-a-backend-when-you-need-one) | A shell; Node; a Wix CLI login | A project folder, source included (built for you) | A site in their account as a Wix Headless project, released with the Wix CLI, ready for Wix Business Solutions |
@@ -30,8 +30,13 @@ What sets them apart:
30
30
  byte is written out inside the call. That costs nothing extra for a page you
31
31
  generated or the user pasted (it's already in the conversation); for files on
32
32
  disk it means reading them in and writing them back out, and each change
33
- resends all of it. A string body is sent as UTF-8, so binary files (PNG, JPG,
34
- fonts, zips) arrive corrupted; link images by absolute URL.
33
+ resends all of it. Files travel in the tool's `files` param — text raw, a
34
+ binary file (PNG, JPG, fonts) as base64 — and `wix.multipart()` builds the
35
+ upload body (below). Base64 costs about a third more than the file and every
36
+ byte is tokens, so it suits small assets (an icon, a logo, a font) and files
37
+ you downloaded from the site to change; a photo goes in by absolute URL
38
+ (`<img src="https://…">`) or with `curl` from a shell. The bundle is capped at
39
+ 4M characters.
35
40
  - **A CLI login** is one approval by the user in the browser: run
36
41
  `npx @wix/cli login` and have them approve; `npx @wix/cli token` then prints a
37
42
  token (see [Before the calls](#before-the-calls)). It also unlocks later
@@ -43,8 +48,8 @@ What sets them apart:
43
48
  the only one that takes framework source as is and leaves a project ready for a
44
49
  backend. A drop site can still move to it [later](#keep-building-add-a-backend-when-you-need-one).
45
50
 
46
- So: a small page already in the conversation, with the Wix MCP connected — B,
47
- shell or not. Static files on disk — A with a CLI login, else C (claimed through
51
+ So: a site already in the conversation — pages, styles, a logo — with the Wix
52
+ MCP connected — B, shell or not. Static files on disk — A with a CLI login, else C (claimed through
48
53
  the Wix MCP when it's connected). A framework project (a `package.json`), or a
49
54
  site that needs stores, bookings, a CMS or members from the start — E. Files out
50
55
  of your reach, or nothing above fits — D.
@@ -108,8 +113,27 @@ curl -sS -X POST \
108
113
  "siteUrl": "https://headless-zjfqzddjtww-northwind-1406.wix-site-host.com" }
109
114
  ```
110
115
 
111
- Option B — the same two calls as one `ExecuteWixAPI` script, for small text files
112
- already in the conversation:
116
+ Option B — the same two calls as one `ExecuteWixAPI` script, for files already
117
+ in the conversation. The files go in the tool's **`files` param**, not in
118
+ `code`: one bundle, each file introduced by a line `=== FILE: <path> ===` and
119
+ followed by its raw text, `<path>` relative to the site root. A binary file is
120
+ introduced by `=== FILE: <path> base64 ===` and followed by its base64. Nothing
121
+ in it is escaped, so quotes, backticks, `${}` and backslashes arrive as written.
122
+
123
+ ```
124
+ === FILE: index.html ===
125
+ <!doctype html><html><head><title>Northwind Studio</title>
126
+ <link rel="stylesheet" href="assets/styles.css"></head><body><h1>Northwind Studio</h1><img src="assets/logo.png"></body></html>
127
+ === FILE: assets/styles.css ===
128
+ body { font-family: sans-serif; margin: 0; padding: 4rem; }
129
+ === FILE: assets/logo.png base64 ===
130
+ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
131
+ ```
132
+
133
+ In `code`, the bundle is the `files` global (`[{ path, content, encoding? }]`,
134
+ `encoding: 'base64'` on the binary entries) and `wix.multipart()` turns it into
135
+ the upload body: one part named `files` per file, the path as its filename, a
136
+ content type guessed from the extension, base64 entries decoded to their bytes.
113
137
 
114
138
  ```javascript
115
139
  async function run() {
@@ -117,31 +141,21 @@ async function run() {
117
141
  url: 'https://www.wixapis.com/headless-business-setup/v1/headless-business/provision',
118
142
  body: { origin: 'drop', newMetasite: { namingStrategy: { metaSiteName: 'Northwind Studio' }, seedOptions: [] },
119
143
  synchronousSteps: ['SET_METASITE_NAME', 'CONFIGURE_HEADLESS_APP'] } });
120
- // path → text content, as template literals. Escape \ as \\ first, then ` as \` and ${ as \${
121
- // (an unescaped \ is dropped or reinterpreted: /\d+/ would arrive as /d+/).
122
- const files = {
123
- 'index.html': `<!doctype html><html><head><title>Northwind Studio</title>
124
- <link rel="stylesheet" href="assets/styles.css"></head><body><h1>Northwind Studio</h1></body></html>`,
125
- 'assets/styles.css': `body { font-family: sans-serif; margin: 0; padding: 4rem; }`,
126
- };
127
- const boundary = '----wixdropboundary';
128
- const body = Object.entries(files).flatMap(([path, text]) => [
129
- '--' + boundary,
130
- `Content-Disposition: form-data; name="files"; filename="${path}"`,
131
- '', text,
132
- ]).concat('--' + boundary + '--', '').join('\r\n');
144
+ const mp = wix.multipart(); // the `files` param; or pass [{ path, content | base64 | bytes }]
133
145
  return await wix.request({ scope: 'account', method: 'POST',
134
146
  url: `https://www.wixapis.com/headless-business-setup/v1/headless-business/${created.data.metaSiteId}/drop`,
135
- headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary },
136
- body }); // a string body is sent verbatim
147
+ headers: { 'Content-Type': mp.contentType },
148
+ body: mp.body });
137
149
  }
138
150
  ```
139
151
 
140
152
  `wix.request` returns `{ status, data }`; read a response's fields from `data`
141
- (`created.data.metaSiteId`). Keep the body's shape exactly. It's a string; an
142
- object is sent as JSON and rejected. Lines end in `\r\n`, the body ends with
143
- `--<boundary>--`, and the header names the same boundary the body uses. Break any of these and the drop
144
- fails with a bare `500`.
153
+ (`created.data.metaSiteId`). Don't build the multipart body by hand — a missing
154
+ `\r\n` or a boundary mismatch is a bare `500` — and don't put file text inside
155
+ `code` as string literals: that second layer of escaping is what corrupts
156
+ backslashes and `${}`. Don't put image bytes in `code` either — they're a
157
+ `base64` entry in `files`. The one thing `files` cannot carry is a file with a
158
+ line that reads exactly `=== FILE: … ===`.
145
159
 
146
160
  `siteUrl` is the site's final address — it's already in the user's account. Give
147
161
  the user two links: `siteUrl`, and its dashboard at
@@ -169,24 +183,28 @@ unzip -o current.zip -d current # the site's files + wix.config.json
169
183
  ```
170
184
 
171
185
  In an `ExecuteWixAPI` script, request the same URL with `responseType: 'base64'`
172
- and read the zip in memory. Entries are stored (method 0) or deflated (method 8):
186
+ and unzip it in memory with `wix.unzip()`. Each entry is `{ path, bytes, text() }`:
187
+ `bytes` is the file as stored, `text()` decodes it when it's text. Don't decode
188
+ every entry — an image run through a text decoder is corrupted.
173
189
 
174
190
  ```javascript
175
191
  const r = await wix.request({ scope: 'account', method: 'GET', responseType: 'base64',
176
192
  url: `https://www.wix.com/_api/wixstro-deployments/v1/instant-sites/${metaSiteId}/download.zip` });
177
- const z = Uint8Array.from(atob(r.data), (c) => c.charCodeAt(0));
178
- const u16 = (o) => z[o] | (z[o + 1] << 8), u32 = (o) => (u16(o) | (u16(o + 2) << 16)) >>> 0;
179
- let e = z.length - 22; while (u32(e) !== 0x06054b50) e--; // end of central directory
180
- const files = {};
181
- for (let i = 0, p = u32(e + 16); i < u16(e + 10); i++) { // one central-directory entry each
182
- const name = new TextDecoder().decode(z.slice(p + 46, p + 46 + u16(p + 28)));
183
- const at = u32(p + 42), start = at + 30 + u16(at + 26) + u16(at + 28);
184
- const raw = z.slice(start, start + u32(p + 20));
185
- files[name] = u16(p + 10) === 0 ? new TextDecoder().decode(raw)
186
- : await new Response(new Blob([raw]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).text();
187
- p += 46 + u16(p + 28) + u16(p + 30) + u16(p + 32);
188
- }
189
- // files: { 'index.html': '…', 'assets/styles.css': '…', 'wix.config.json': '…' }
193
+ const entries = await wix.unzip(r.data);
194
+ // entries: [{ path: 'index.html', bytes, text() }, { path: 'assets/logo.png', bytes, text() }, { path: 'wix.config.json', … }]
195
+ ```
196
+
197
+ Edit in memory — text through `text()`, binaries left as they are — then drop
198
+ the full set back in the same script, leaving `wix.config.json` out. Entries go
199
+ to `wix.multipart()` as they are; an edited file replaces its entry with
200
+ `{ path, content }`:
201
+
202
+ ```javascript
203
+ const edited = entries.filter((e) => e.path !== 'wix.config.json').map((e) =>
204
+ e.path === 'index.html' ? { path: e.path, content: e.text().replace('Northwind Studio', 'Northwind Studio — Est. 1998') } : e);
205
+ const mp = wix.multipart(edited);
206
+ await wix.request({ scope: 'account', method: 'POST', headers: { 'Content-Type': mp.contentType }, body: mp.body,
207
+ url: `https://www.wixapis.com/headless-business-setup/v1/headless-business/${metaSiteId}/drop` });
190
208
  ```
191
209
 
192
210
  The live URL itself (`*.wix-site-host.com`) can't be read from a script; this
@@ -219,7 +237,8 @@ Keep both: `metaSiteId` addresses the site, `projectId` builds the save link.
219
237
  ### 2. Upload the files
220
238
 
221
239
  Same multipart shape as the [drop](#2-drop-the-files--the-site-goes-live), to the
222
- upload path, with the attribution parameters:
240
+ upload path, with the attribution parameters (in a script: `wix.multipart()` with
241
+ the `files` param, posted to this URL):
223
242
 
224
243
  ```bash
225
244
  curl -sS -X POST \
@@ -323,7 +342,7 @@ Failures come back as HTTP 400 with a code in `details.applicationError.code`:
323
342
  - **`403 PERMISSION_DENIED` on a drop** — the token expired or is missing (see
324
343
  [Before the calls](#before-the-calls)), or the site isn't the caller's. A site
325
344
  that's still anonymous is changed by upload + release, not by a drop.
326
- - **`500` on a drop or upload** — the multipart body is malformed (see the shape
345
+ - **`500` on a drop or upload** — the multipart body was built by hand and is malformed (see the shape
327
346
  above). Rebuild it from the example; sending it again unchanged fails the
328
347
  same way.
329
348
  - **`404` on the anonymous route after step 1** — the hour passed or the site was