@wix/agent-skills 1.22.0 → 1.23.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.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +12 -4
- package/skills/wix-app/references/APP_VALIDATION.md +16 -10
- package/skills/wix-app/references/SDK_MODULE_MAP.md +2 -0
- package/skills/wix-app/references/dashboard-page/DRAFT_TEMPLATE.md +1 -1
- package/skills/wix-headless-fast/SKILL.md +35 -8
- package/skills/wix-headless-fast/cold-start/cold-start.md +6 -2
- package/skills/wix-headless-fast/guides/existing-site.md +3 -1
- package/skills/wix-headless-fast/guides/migration.md +43 -0
- package/skills/wix-headless-fast/guides/reference-mode.md +4 -1
- package/skills/wix-headless-fast/install/AGENTS.md +3 -1
- package/skills/wix-headless-fast/install/agents-md.mjs +5 -1
- package/skills/wix-headless-fast/install/attach.mjs +10 -0
- package/skills/wix-headless-fast/install/check.mjs +87 -0
- package/skills/wix-headless-fast/install/context.mjs +126 -0
- package/skills/wix-headless-fast/install/deploy.mjs +85 -20
- package/skills/wix-headless-fast/install/setup.mjs +52 -12
- package/skills/wix-manage/SKILL.md +3 -3
- package/skills/wix-manage/references/app-installation/install-wix-apps.md +68 -2
- package/skills/wix-manage/references/contacts/bulk-label-and-unlabel-contacts.md +37 -4
- package/skills/wix-manage/references/ecommerce/ecom-pricing.md +29 -3
- package/skills/wix-manage/references/sites/site-import.md +101 -37
|
@@ -45,6 +45,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
45
45
|
import { fileURLToPath } from "node:url";
|
|
46
46
|
import { templatesDir } from "./templates.mjs";
|
|
47
47
|
import { syncLockRoot } from "./lock.mjs";
|
|
48
|
+
import { siteContext } from "./context.mjs";
|
|
48
49
|
|
|
49
50
|
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
50
51
|
// The shipped code: the repository's templates/ (a checkout, the cache, or fetched now).
|
|
@@ -80,7 +81,11 @@ const CAPABILITY_DEPS = {
|
|
|
80
81
|
"@wix/media": "^1.0.271",
|
|
81
82
|
"@wix/essentials": "^1.0.10",
|
|
82
83
|
},
|
|
84
|
+
"site-search": {
|
|
85
|
+
"@wix/search": "^1.0.90",
|
|
86
|
+
},
|
|
83
87
|
};
|
|
88
|
+
const SITE_SEARCH = join("shared", "capabilities", "site-search");
|
|
84
89
|
const VERTICAL_DEPS = {
|
|
85
90
|
storefront: {
|
|
86
91
|
core: {
|
|
@@ -160,6 +165,19 @@ const VERTICAL_DEPS = {
|
|
|
160
165
|
"@wix/redirects": "^1.0.125",
|
|
161
166
|
},
|
|
162
167
|
},
|
|
168
|
+
faq: {
|
|
169
|
+
core: {
|
|
170
|
+
"@wix/faq": "^1.0.76",
|
|
171
|
+
},
|
|
172
|
+
// no astro block: the FAQ page is a plain page (no wixMetadata item routing); its JSON-LD is inline
|
|
173
|
+
},
|
|
174
|
+
donations: {
|
|
175
|
+
core: {
|
|
176
|
+
"@wix/donations": "^1.0.64",
|
|
177
|
+
"@wix/ecom": "^1.0.2454",
|
|
178
|
+
"@wix/redirects": "^1.0.125",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
163
181
|
};
|
|
164
182
|
|
|
165
183
|
const COPY = { recursive: true, force: false, errorOnExist: false };
|
|
@@ -211,6 +229,27 @@ if (!Array.isArray(uploadPolicies)) {
|
|
|
211
229
|
console.log(JSON.stringify({ error: "plan.capabilities.mediaUpload.policies must be an array" }));
|
|
212
230
|
process.exit(1);
|
|
213
231
|
}
|
|
232
|
+
// Site search is on when the plan carries the entry (`{}` is enough; `true` too). Its fields are
|
|
233
|
+
// data for the generated config; the shipped core drops unknown type names, so only shapes are checked.
|
|
234
|
+
const siteSearchEntry = plan.capabilities?.siteSearch;
|
|
235
|
+
const siteSearch = siteSearchEntry === true ? {} : siteSearchEntry && typeof siteSearchEntry === "object" ? siteSearchEntry : null;
|
|
236
|
+
if (siteSearchEntry != null && siteSearchEntry !== false && !siteSearch) {
|
|
237
|
+
console.log(JSON.stringify({ error: "plan.capabilities.siteSearch must be an object (or true)" }));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
if (siteSearch) {
|
|
241
|
+
const strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
242
|
+
const map = (v) => v === undefined || (v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string"));
|
|
243
|
+
if ((siteSearch.types !== undefined && !strings(siteSearch.types)) || !map(siteSearch.routes) || !map(siteSearch.labels)) {
|
|
244
|
+
console.log(JSON.stringify({ error: "plan.capabilities.siteSearch: types must be string[]; routes and labels must be objects of strings" }));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// The generated file is data only; the shipped core resolves defaults (types, routes, labels) from it.
|
|
249
|
+
const siteSearchConfigSource = () =>
|
|
250
|
+
`// Generated by wix-headless-fast install/deploy.mjs from plan.capabilities.siteSearch.\n` +
|
|
251
|
+
`// Edit the plan and redeploy. Defaults for anything omitted are in search-core.ts (resolveConfig).\n` +
|
|
252
|
+
`export const siteSearchPlan = ${JSON.stringify(siteSearch, null, 2)};\n`;
|
|
214
253
|
// ---- static stack: the REST layer, composed flat and stripped ------------------------------------
|
|
215
254
|
if (stack === "static") {
|
|
216
255
|
const { spawnSync } = await import("node:child_process");
|
|
@@ -218,10 +257,9 @@ if (stack === "static") {
|
|
|
218
257
|
const OUT = outDir ? resolve(PROJECT, outDir) : PROJECT;
|
|
219
258
|
const JS = join(OUT, "js", "wix");
|
|
220
259
|
result.out = outDir ?? ".";
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
260
|
+
// The client id the browser mints visitor tokens with: --client-id, else the CONTENT app from
|
|
261
|
+
// .env.local (`wix env pull`; on a migration preview the parent site's app), else the config's appId.
|
|
262
|
+
let clientId = clientIdFlag ?? (existsSync(join(PROJECT, "wix.config.json")) ? siteContext({ cwd: PROJECT }).content.clientId : null);
|
|
225
263
|
if (!clientId) {
|
|
226
264
|
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" }));
|
|
227
265
|
process.exit(1);
|
|
@@ -233,19 +271,23 @@ if (stack === "static") {
|
|
|
233
271
|
}
|
|
234
272
|
// Only files that aren't there yet — a re-run restores, never clobbers (same as the copy below).
|
|
235
273
|
cpSync(join(REF, "shared", "rest"), JS, COPY);
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
// Every layer with a rest/ twin: the requested verticals, plus the site-search capability when the
|
|
275
|
+
// plan turns it on (same shape — rest/ beside app/wix/<name>/ — at templates/shared/capabilities).
|
|
276
|
+
const layers = requested.map((vertical) => [vertical, join(REF, vertical)]);
|
|
277
|
+
if (siteSearch) layers.push(["site-search", join(REF, SITE_SEARCH)]);
|
|
278
|
+
for (const [vertical, root] of layers) {
|
|
279
|
+
cpSync(join(root, "rest"), JS, COPY);
|
|
238
280
|
// The vertical's transport-agnostic core: types, every *-core.ts, and every *-store.ts under its
|
|
239
281
|
// app/wix/<vertical>/ — the stores are the hooks' logic without React, over the same function
|
|
240
282
|
// names the REST twin exports, so they run against it unchanged.
|
|
241
|
-
const appWix = join(
|
|
283
|
+
const appWix = join(root, "app", "wix", vertical);
|
|
242
284
|
for (const f of readdirSync(appWix)) {
|
|
243
285
|
// Every vertical has a types.ts; flat in one folder they would collide, so each lands as
|
|
244
286
|
// <vertical>-types.ts and the vertical's own files are pointed at it below.
|
|
245
287
|
if (f === "types.ts") cpSync(join(appWix, f), join(JS, `${vertical}-types.ts`), COPY);
|
|
246
288
|
else if (f.endsWith("-core.ts") || f.endsWith("-store.ts")) cpSync(join(appWix, f), join(JS, f), COPY);
|
|
247
289
|
}
|
|
248
|
-
const own = new Set([...readdirSync(join(
|
|
290
|
+
const own = new Set([...readdirSync(join(root, "rest")), ...readdirSync(appWix)].filter((f) => f.endsWith(".ts") && f !== "types.ts"));
|
|
249
291
|
for (const f of own) {
|
|
250
292
|
const fp = join(JS, f);
|
|
251
293
|
if (!existsSync(fp)) continue;
|
|
@@ -253,7 +295,12 @@ if (stack === "static") {
|
|
|
253
295
|
const fixed = src.replace(/from\s+"\.\/types(?:\.js)?"/g, `from "./${vertical}-types.js"`);
|
|
254
296
|
if (fixed !== src) writeFileSync(fp, fixed);
|
|
255
297
|
}
|
|
256
|
-
result.verticals.push(vertical);
|
|
298
|
+
if (VERTICALS.includes(vertical)) result.verticals.push(vertical);
|
|
299
|
+
}
|
|
300
|
+
if (siteSearch) {
|
|
301
|
+
// The REST twin imports the plan as data from beside it (shared/rest already owns config.ts).
|
|
302
|
+
writeFileSync(join(JS, "site-search-config.generated.ts"), siteSearchConfigSource());
|
|
303
|
+
result.capabilities = { ...result.capabilities, siteSearch: true };
|
|
257
304
|
}
|
|
258
305
|
// Browser ESM resolves nothing: every relative import needs its .js. The rest/ files are written
|
|
259
306
|
// that way; the files borrowed from app/wix/ are bundler-style and get the suffix here.
|
|
@@ -286,7 +333,7 @@ if (stack === "static") {
|
|
|
286
333
|
result.js = `${outDir ? outDir.replace(/\/$/, "") + "/" : ""}js/wix/*.js`;
|
|
287
334
|
// The note lists what actually landed, so it holds for every vertical (and several at once).
|
|
288
335
|
const stores = readdirSync(JS).filter((f) => f.endsWith("-store.js")).map((f) => `./js/wix/${f}`);
|
|
289
|
-
const dataFiles = readdirSync(JS).filter((f) => f.endsWith(".js") && !f.endsWith("-store.js") && !f.endsWith("-core.js") && !f.endsWith("-types.js") && !["client.js", "config.js", "media.js"].includes(f)).map((f) => `./js/wix/${f}`);
|
|
336
|
+
const dataFiles = readdirSync(JS).filter((f) => f.endsWith(".js") && !f.endsWith("-store.js") && !f.endsWith("-core.js") && !f.endsWith("-types.js") && !f.endsWith(".generated.js") && !["client.js", "config.js", "media.js"].includes(f)).map((f) => `./js/wix/${f}`);
|
|
290
337
|
result.note = `import the data layer (${dataFiles.join(", ")})${stores.length ? ` and the stores (${stores.join(", ")})` : ""} relative to ${outDir ?? "the project root"} in a <script type="module">; the .ts beside them are the same files with types, for reading; point wix.config.json site.outputDirectory at "./${outDir ?? "."}"`;
|
|
291
338
|
console.log(JSON.stringify(result));
|
|
292
339
|
process.exit(0);
|
|
@@ -319,11 +366,20 @@ if (uploadPolicies.length) {
|
|
|
319
366
|
cpSync(join(REF, "shared", "capabilities", "media-upload", "app"), SRC, COPY);
|
|
320
367
|
cpSync(join(REF, "shared", "capabilities", "media-upload", "app-astro"), SRC, COPY);
|
|
321
368
|
}
|
|
369
|
+
// Site search ships like a vertical (data layer, stores, hooks, components, a search page) at a
|
|
370
|
+
// disjoint path (src/wix/site-search/, src/components/site-search/, …), so the same per-stack rule
|
|
371
|
+
// applies: lib takes only app/wix/, astro adds app-astro/.
|
|
372
|
+
if (siteSearch) {
|
|
373
|
+
if (stack === "lib") cpSync(join(REF, SITE_SEARCH, "app", "wix"), join(SRC, "wix"), COPY);
|
|
374
|
+
else cpSync(join(REF, SITE_SEARCH, "app"), SRC, COPY);
|
|
375
|
+
if (stack === "astro") cpSync(join(REF, SITE_SEARCH, "app-astro"), SRC, COPY);
|
|
376
|
+
}
|
|
322
377
|
|
|
323
378
|
const unknown = requested.filter((v) => !VERTICALS.includes(v));
|
|
324
379
|
// lib: only @wix/sdk from the shared deps — the Tailwind pair serves the components, which don't ship there.
|
|
325
380
|
const wantedDeps = stack === "lib" ? { "@wix/sdk": SHARED_DEPS["@wix/sdk"] } : { ...SHARED_DEPS };
|
|
326
381
|
if (uploadPolicies.length) Object.assign(wantedDeps, CAPABILITY_DEPS["media-upload"]);
|
|
382
|
+
if (siteSearch) Object.assign(wantedDeps, CAPABILITY_DEPS["site-search"]);
|
|
327
383
|
for (const vertical of requested.filter((v) => VERTICALS.includes(v))) {
|
|
328
384
|
if (stack === "lib") cpSync(join(REF, vertical, "app", "wix"), join(SRC, "wix"), COPY);
|
|
329
385
|
else cpSync(join(REF, vertical, "app"), SRC, COPY);
|
|
@@ -351,6 +407,15 @@ if (uploadPolicies.length) {
|
|
|
351
407
|
);
|
|
352
408
|
result.capabilities = { mediaUpload: uploadPolicies.map((policy) => policy.id) };
|
|
353
409
|
}
|
|
410
|
+
if (siteSearch) {
|
|
411
|
+
// The SDK transport imports the plan as data from beside it; defaults resolve in the shipped core.
|
|
412
|
+
writeFileSync(join(SRC, "wix", "site-search", "config.generated.ts"), siteSearchConfigSource());
|
|
413
|
+
result.capabilities = { ...result.capabilities, siteSearch: true };
|
|
414
|
+
if (siteSearch.install) {
|
|
415
|
+
// Indexing needs the Wix Site Search app on the site — a site mutation the deploy never makes.
|
|
416
|
+
result.note = [result.note, "siteSearch.install: run `node <SKILL_ROOT>/templates/shared/capabilities/site-search/seed/install.mjs` from the project root after the seed (results appear about half a minute later)"].filter(Boolean).join("; ");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
354
419
|
|
|
355
420
|
// ---- dependency patch (fill-only) ----------------------------------------------------------------
|
|
356
421
|
if (result.verticals.length && existsSync(PKG_JSON)) {
|
|
@@ -416,12 +481,15 @@ if (stack === "astro" && result.verticals.length && existsSync(ASTRO_CONFIG)) {
|
|
|
416
481
|
|
|
417
482
|
// ---- ready-made links -----------------------------------------------------------------------------
|
|
418
483
|
// Emit the siteId and the dashboard deep links so nothing downstream re-derives or retypes them.
|
|
484
|
+
// The site the links point at is the CONTENT site — the parent on a migration preview, where the
|
|
485
|
+
// config's site only hosts the deployment (install/context.mjs).
|
|
419
486
|
const WIX_CONFIG = join(PROJECT, "wix.config.json");
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const siteId =
|
|
487
|
+
const ctx = existsSync(WIX_CONFIG) ? siteContext({ cwd: PROJECT }) : null;
|
|
488
|
+
if (ctx) {
|
|
489
|
+
const siteId = ctx.content.siteId;
|
|
423
490
|
if (siteId) {
|
|
424
491
|
result.siteId = siteId;
|
|
492
|
+
if (ctx.migration.active) result.migration = { parentSiteId: ctx.migration.parentSiteId, deploySiteId: ctx.deploy.siteId };
|
|
425
493
|
result.dashboardUrl = `https://manage.wix.com/dashboard/${siteId}`;
|
|
426
494
|
if (result.verticals.includes("storefront")) {
|
|
427
495
|
result.productsUrl = `https://manage.wix.com/dashboard/${siteId}/wix-stores/products`;
|
|
@@ -440,13 +508,10 @@ if (!requested.length) {
|
|
|
440
508
|
// ---- client ids ---------------------------------------------------------------------------------
|
|
441
509
|
// The shared data client is ambient on managed Astro. Members are different: their shipped
|
|
442
510
|
// custom credential flow always needs an explicit public OAuth client, including on Astro.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
JSON.parse(readFileSync(join(PROJECT, "wix.config.json"), "utf8")).appId ??
|
|
448
|
-
null;
|
|
449
|
-
}
|
|
511
|
+
// --client-id, else the CONTENT app: .env.local's WIX_CLIENT_ID (what `wix env pull` writes — on a
|
|
512
|
+
// migration preview the parent site's app, the one the Astro integration itself runs as), else the
|
|
513
|
+
// config's appId. Never the config first: on a preview that is the deploy-only child app.
|
|
514
|
+
const clientId = clientIdFlag ?? ctx?.content.clientId ?? null;
|
|
450
515
|
if (stack === "react" || stack === "lib") {
|
|
451
516
|
if (clientId) {
|
|
452
517
|
const current = readFileSync(CONFIG_TS, "utf8");
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
//
|
|
8
8
|
// - `wix.config.json` present → refuses. The folder is already a Wix project; deploy.mjs adds
|
|
9
9
|
// this skill's code or a solution to it, then one install, then the seed if there is content.
|
|
10
|
+
// One exception: a config and NO project whose `.env.local` (`wix env pull`, run here) declares a
|
|
11
|
+
// MIGRATION PREVIEW — the config names a site created only to host the deployment, the env names
|
|
12
|
+
// the site being migrated. Then → MIGRATE: the composed template is copied in around the config,
|
|
13
|
+
// the code deploys with the parent's app as its client, the install starts, and NO seed runs: the
|
|
14
|
+
// parent owns its content. Release still goes where the config says (install/context.mjs).
|
|
10
15
|
// - a project but no `wix.config.json` (a package.json, or an index.html at the root) → CONNECT:
|
|
11
16
|
// `npm create @wix/new@latest init` in place creates the site and the config, then deploy.
|
|
12
17
|
// - otherwise (empty, or loose files such as a CSV or a brief) → CREATE: `wix create` with the
|
|
@@ -23,11 +28,12 @@
|
|
|
23
28
|
// steps — the dependency install and the seed — running detached in the background (logs and
|
|
24
29
|
// completion markers in the final event), so the caller can build the brand layer while they finish.
|
|
25
30
|
import { spawn, spawnSync } from "node:child_process";
|
|
26
|
-
import { existsSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
31
|
+
import { cpSync, existsSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
27
32
|
import { basename, dirname, join, resolve } from "node:path";
|
|
28
33
|
import { fileURLToPath } from "node:url";
|
|
29
34
|
import { AGENT_CONFIG_FILES, writeAgentsMd } from "./agents-md.mjs";
|
|
30
35
|
import { listVerticals, templatesDir } from "./templates.mjs";
|
|
36
|
+
import { siteContext } from "./context.mjs";
|
|
31
37
|
|
|
32
38
|
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
39
|
|
|
@@ -67,17 +73,23 @@ if (planPath && !existsSync(planPath)) fail("args", `plan file not found: ${plan
|
|
|
67
73
|
// ---- 0 · read the folder --------------------------------------------------------------------------
|
|
68
74
|
const cwd = process.cwd();
|
|
69
75
|
const has = (p) => existsSync(join(cwd, p));
|
|
70
|
-
if (has("wix.config.json")) {
|
|
71
|
-
fail("place", "this folder is already a Wix project (wix.config.json): nothing to set up. deploy.mjs <vertical> adds this skill's code or a solution to it; then ONE npm install; then the vertical's seed module if there is content to create");
|
|
72
|
-
}
|
|
73
76
|
const pkg = has("package.json") ? JSON.parse(readFileSync(join(cwd, "package.json"), "utf8")) : null;
|
|
74
77
|
const hasProject = !!pkg || has("index.html");
|
|
75
|
-
|
|
78
|
+
// A config here: refuse, unless it is a migration preview with no project yet (the site context
|
|
79
|
+
// pulls .env.local when missing — that is where the migration is declared).
|
|
80
|
+
let ctx = null;
|
|
81
|
+
if (has("wix.config.json")) {
|
|
82
|
+
ctx = siteContext({ cwd });
|
|
83
|
+
if (hasProject || !ctx.migration.active) {
|
|
84
|
+
fail("place", "this folder is already a Wix project (wix.config.json): nothing to set up. deploy.mjs <vertical> adds this skill's code or a solution to it; then ONE npm install; then the vertical's seed module if there is content to create. A config with no project is attach.mjs's case (SKILL.md step 3)" + (ctx.pullError ? ` — note: env pull failed here (${ctx.pullError.slice(0, 120)})` : ""));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const mode = ctx ? "migrate" : hasProject ? "connect" : "create";
|
|
76
88
|
if (mode === "connect" && !stackFlag) {
|
|
77
89
|
fail("args", "connecting a project on disk needs --stack astro|react|lib|static — the stack you resolved in SKILL.md step 1 for this project");
|
|
78
90
|
}
|
|
79
91
|
const stack = stackFlag ?? "astro";
|
|
80
|
-
emit("folder", { mode, stack, project: hasProject ? (pkg?.name ?? basename(cwd)) : null });
|
|
92
|
+
emit("folder", { mode, stack, project: hasProject ? (pkg?.name ?? basename(cwd)) : null, ...(ctx ? { migration: ctx.migration, deploySiteId: ctx.deploy.siteId, warnings: ctx.warnings } : {}) });
|
|
81
93
|
|
|
82
94
|
let projectDir = cwd;
|
|
83
95
|
let folderName = null;
|
|
@@ -119,6 +131,25 @@ if (mode === "create") {
|
|
|
119
131
|
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));
|
|
120
132
|
}
|
|
121
133
|
}
|
|
134
|
+
} else if (mode === "migrate") {
|
|
135
|
+
// ---- 1 · a migration preview: the config and credentials came with the folder -------------------
|
|
136
|
+
// No site is created and nothing is provisioned: `wix.config.json` already names the deploy site
|
|
137
|
+
// and its app, `.env.local` the parent's. Managed Astro gets the vertical's composed template copied
|
|
138
|
+
// in around them (what `wix create` would have copied; the config and env are never overwritten);
|
|
139
|
+
// any other stack is code-only and deploy below adds it.
|
|
140
|
+
if (planPath) emit("seed_skipped", { reason: `migration preview of site ${ctx.migration.parentSiteId}: the site owns its content and nothing is seeded — the plan is ignored` });
|
|
141
|
+
if (stack === "astro") {
|
|
142
|
+
const template = join(templatesDir({ need: `${vertical}/project` }), vertical, "project");
|
|
143
|
+
emit("scaffolding", { folder: cwd, template, from: "migration preview" });
|
|
144
|
+
cpSync(template, cwd, { recursive: true, force: false, errorOnExist: false });
|
|
145
|
+
const pkgPath = join(cwd, "package.json");
|
|
146
|
+
const scaffolded = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
147
|
+
scaffolded.name = basename(cwd).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "site";
|
|
148
|
+
writeFileSync(pkgPath, JSON.stringify(scaffolded, null, 2) + "\n");
|
|
149
|
+
}
|
|
150
|
+
const gi = join(cwd, ".gitignore");
|
|
151
|
+
const cur = existsSync(gi) ? readFileSync(gi, "utf8") : "";
|
|
152
|
+
if (!/\.env/.test(cur)) writeFileSync(gi, cur + "\n# local env (pulled from Wix)\n.env.local\n.env\n");
|
|
122
153
|
} else {
|
|
123
154
|
// ---- 1 · init in place --------------------------------------------------------------------------
|
|
124
155
|
// The CLI's `init` links the folder to a NEW site: creates the site and its OAuth app, writes
|
|
@@ -141,7 +172,10 @@ if (stack === "static") {
|
|
|
141
172
|
wixConfig.site = { ...(wixConfig.site ?? {}), outputDirectory: `./${STATIC_OUT}` };
|
|
142
173
|
writeFileSync(join(projectDir, "wix.config.json"), JSON.stringify(wixConfig, null, 2) + "\n");
|
|
143
174
|
}
|
|
144
|
-
emit(mode === "create" ? "scaffolded" : "
|
|
175
|
+
emit(mode === "create" ? "scaffolded" : mode === "migrate" ? "migration_preview" : "connected", {
|
|
176
|
+
folder: folderName ?? cwd, stack,
|
|
177
|
+
...(ctx ? { deploySiteId: ctx.deploy.siteId, contentSiteId: ctx.content.siteId } : { siteId }),
|
|
178
|
+
});
|
|
145
179
|
|
|
146
180
|
// ---- 2 · deploy shipped code + deps ---------------------------------------------------------------
|
|
147
181
|
// A CREATE from the composed template already holds the code and the lock: deploy adds nothing and
|
|
@@ -189,7 +223,7 @@ if (mode === "create" && !subfolder && projectDir !== cwd) {
|
|
|
189
223
|
// ---- 2d · the agent config files `wix create` would have written --------------------------------
|
|
190
224
|
// Skipped by the CLI because of --skip-install. Fill-only: a project that has its own AGENTS.md
|
|
191
225
|
// keeps it (the event says `kept`).
|
|
192
|
-
emit("agent_configs", writeAgentsMd(projectDir, { skill: basename(SKILL_ROOT), stack }));
|
|
226
|
+
emit("agent_configs", writeAgentsMd(projectDir, { skill: basename(SKILL_ROOT), stack, ...(ctx ? { migration: { parentSiteId: ctx.migration.parentSiteId, deploySiteId: ctx.deploy.siteId } } : {}) }));
|
|
193
227
|
|
|
194
228
|
// ---- 3 · start the dependency install, detached --------------------------------------------------
|
|
195
229
|
// ONE install, here, for any project with a package.json (deploy patched it). The static stack has
|
|
@@ -215,7 +249,7 @@ if (stack !== "static" && existsSync(join(projectDir, "package.json"))) {
|
|
|
215
249
|
// result JSON + exit-code marker land as files the caller syncs on before release. Seeding is
|
|
216
250
|
// additive: it never deletes or overwrites what the site holds.
|
|
217
251
|
let seed = null;
|
|
218
|
-
if (planPath) {
|
|
252
|
+
if (planPath && mode !== "migrate") {
|
|
219
253
|
const seedDir = join(TEMPLATES, vertical, "seed");
|
|
220
254
|
const seedName = existsSync(seedDir)
|
|
221
255
|
? readdirSync(seedDir).find((f) => f.startsWith("seed-") && f.endsWith(".mjs"))
|
|
@@ -240,20 +274,26 @@ const release = {
|
|
|
240
274
|
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)",
|
|
241
275
|
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`,
|
|
242
276
|
}[stack];
|
|
277
|
+
// On a migration preview the links and `siteId` are the PARENT's — the site whose content the pages
|
|
278
|
+
// show and whose dashboard manages it; `deploySiteId` is where `wix release` goes.
|
|
243
279
|
emit("ready_for_brand_layer", {
|
|
244
280
|
mode,
|
|
245
281
|
stack,
|
|
246
282
|
projectDir,
|
|
247
|
-
siteId,
|
|
283
|
+
siteId: ctx ? ctx.content.siteId : siteId,
|
|
284
|
+
...(ctx ? { deploySiteId: ctx.deploy.siteId, migration: { parentSiteId: ctx.migration.parentSiteId } } : {}),
|
|
248
285
|
dashboardUrl: deployResult.dashboardUrl,
|
|
249
286
|
productsUrl: deployResult.productsUrl,
|
|
250
287
|
categoriesUrl: deployResult.categoriesUrl,
|
|
251
288
|
install,
|
|
252
289
|
seed,
|
|
253
290
|
next:
|
|
254
|
-
(
|
|
291
|
+
(mode === "migrate"
|
|
292
|
+
? "a migration preview: the site being migrated owns its content (read it with the vertical's read-site.mjs when the brief allows probing; never seed it); theme + write the home page; "
|
|
293
|
+
: planPath
|
|
255
294
|
? "theme + write the home page; "
|
|
256
295
|
: "the site is new and empty — seed it (a plan per step 2, the vertical's seed module) or say so; theme + write the home page; ") +
|
|
257
296
|
(install || seed ? "then wait for the done markers" + (seed ? ", verify .seed-exit is 0 (else read seed.log and re-run the seed module)" : "") + "; " : "") +
|
|
258
|
-
`then ${release}
|
|
297
|
+
`then ${release}` +
|
|
298
|
+
(mode === "migrate" ? "; the release is the PREVIEW on the deploy site — close with its URL and the parent's dashboard, say the original site is unchanged and that completing the migration is the user's next step in the Wix CLI once they approve" : ""),
|
|
259
299
|
});
|
|
@@ -163,7 +163,7 @@ Create and modify CMS collection structures. Covers listing collections, creatin
|
|
|
163
163
|
Deletes multiple contacts using filter-based bulk delete. Covers safe deletion patterns, GDPR compliance, soft delete alternatives, and batch processing strategies.
|
|
164
164
|
|
|
165
165
|
### [Bulk Label and Unlabel Contacts](references/contacts/bulk-label-and-unlabel-contacts.md)
|
|
166
|
-
|
|
166
|
+
Creates contact label definitions or adds/removes labels from matching contacts. Use Find or Create Label for label creation alone; use bulk labeling only when the user requests contact assignments.
|
|
167
167
|
|
|
168
168
|
### [Create a Contact](references/contacts/create-a-contact.md)
|
|
169
169
|
Creates a contact with the Contacts API. Covers the minimum identifying fields, the single-object shape of `email` and `phone`, and adding a physical address with the ISO 3166-2 subdivision format required for state, region, and province codes.
|
|
@@ -211,7 +211,7 @@ Buy a domain through Wix or connect one the user already owns — intent, availa
|
|
|
211
211
|
**Entry point for all eCommerce recommendation requests.** Unified skill that analyzes site data across ALL domains (discounts + shipping + gift cards), generates up to 5 cross-domain recommendations, and persists them to the tracking database. Covers discount strategies (seasonal, upsell, stock mover, bundling), shipping optimization (coverage gaps, free shipping, rate strategy, carrier backup), AND selling gift cards (denominations sized from the site's own AOV and catalog prices). Use this for business improvement requests about earning more from existing visitors. **Traffic acquisition (SEO, ads, social, content) is out of scope** — route "grow my traffic" to marketing.
|
|
212
212
|
|
|
213
213
|
### [Pricing & Promotions](references/ecommerce/ecom-pricing.md)
|
|
214
|
-
|
|
214
|
+
Routes discounts, coupons, sales and bundles to promotion recipes, and visual product ribbons to the Catalog Ribbons API. Load this dispatcher for mixed pricing/refund/payment/product-price/shipping requests to choose the appropriate APIs.
|
|
215
215
|
|
|
216
216
|
### [Shipping](references/ecommerce/ecom-shipping.md)
|
|
217
217
|
**Dispatcher** — routes shipping-setup requests (rates, regions, pickup, free shipping, fix coverage, optimize rates) to the right leaf recipe. The Shipping Options + Delivery Profiles APIs have no public docs page; `ecom-shipping-api.md` is the authoritative inline reference.
|
|
@@ -422,7 +422,7 @@ List, count, and find the sites in a Wix account. Covers the namespace filter fo
|
|
|
422
422
|
Probe a Wix site or account for full context in one call — installed apps by display name, locale, currency, timezone, and status. Account token + siteId targets one site; account token alone returns up to 10; site-scoped token alone returns the site it is scoped to.
|
|
423
423
|
|
|
424
424
|
### [Site Import](references/sites/site-import.md)
|
|
425
|
-
Drive the Wix Site Import agent to migrate an existing store or site from another platform (Shopify, WooCommerce, Magento, or any URL) into Wix
|
|
425
|
+
Drive the Wix Site Import agent to migrate an existing store or site from another platform (Shopify, WooCommerce, Magento, or any URL) into Wix — as a brand-new site or into the user's existing one — or to import from CSV/TSV export files with no source site. Use this skill whenever the user wants to import, migrate, or clone a store/site into Wix, mentions moving off Shopify/WooCommerce/Magento, or gives a source store URL and asks to bring it into Wix. Covers starting the import, polling progress, answering the agent's mid-import questions, handling deploy/failure/auth-expiry states, and sending post-deploy follow-up changes.
|
|
426
426
|
|
|
427
427
|
### [Sites Dashboard Navigation](references/sites/sites-dashboard-navigation.md)
|
|
428
428
|
"Builds direct links to the account-level sites pages on manage.wix.com — the My Sites list (all sites in the account) and each site's own dashboard. Pairs the site list with the Query Sites read API. Use when the user asks where something is in the Wix dashboard, wants a direct link to a dashboard page, or you need a dashboard URL to include with the result of an API operation."
|
|
@@ -107,15 +107,81 @@ Some common apps:
|
|
|
107
107
|
- NEVER guess the `appDefId`. For Wix-built apps, use the table above. For any other app, resolve the ID using Step 0 (Search Market Listings).
|
|
108
108
|
- The `tenantType` MUST be `SITE`
|
|
109
109
|
- The `id` in tenant is the site's metaSiteId
|
|
110
|
+
- The endpoint, request body, and appDefId table above are complete and canonical for a listed Wix-built app — don't re-verify them with a separate REST/API doc search first; that's a redundant round-trip.
|
|
111
|
+
- Don't spend a call checking whether the app is already installed before installing it. If the task or the error you're handling already tells you it isn't installed (e.g. a fresh site, or a `*_NOT_INSTALLED` error), just call install directly.
|
|
110
112
|
|
|
111
113
|
---
|
|
112
114
|
|
|
113
115
|
## Error Handling
|
|
114
116
|
|
|
115
117
|
### App Not Installed Error
|
|
116
|
-
If you receive an error indicating a required app is not installed,
|
|
118
|
+
If you receive an error indicating a required app is not installed (e.g. a `428` or a `*_NOT_INSTALLED` error code), install the app it names using its appDefId, then retry the call that failed. If the user's own request already implies this app is needed — i.e. doing what they asked requires a feature only that app provides — just install it as part of doing that task; don't stop to ask permission or name the app first. Confirm first only when the app isn't implied by anything the user asked for.
|
|
119
|
+
|
|
120
|
+
**Example** (install the missing app, then retry the call that surfaced the error — all in one script):
|
|
121
|
+
```javascript
|
|
122
|
+
async function() {
|
|
123
|
+
const siteId = "<SITE_ID>";
|
|
124
|
+
|
|
125
|
+
await wix.request({
|
|
126
|
+
method: "POST",
|
|
127
|
+
url: "https://www.wixapis.com/apps-installer-service/v1/app-instance/install",
|
|
128
|
+
body: {
|
|
129
|
+
tenant: { tenantType: "SITE", id: siteId },
|
|
130
|
+
appInstance: { appDefId: "<APP_DEF_ID>" }
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return await wix.request({
|
|
135
|
+
method: "<METHOD>",
|
|
136
|
+
url: "<ORIGINAL_URL_THAT_FAILED>",
|
|
137
|
+
body: { /* original request body */ }
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### App-Dependent Call Fails Right After Install (Propagation Delay)
|
|
143
|
+
Installing an app and then immediately calling one of its own APIs — e.g. calling Set Multilingual Mode right after installing Wix Multilingual — can race the platform's install propagation, surfacing as a not-found error on the dependent call even though the install itself already succeeded.
|
|
144
|
+
|
|
145
|
+
**Example** (retry with backoff past the propagation delay instead of surfacing the error, all in one script):
|
|
146
|
+
```javascript
|
|
147
|
+
async function() {
|
|
148
|
+
const siteId = "<SITE_ID>";
|
|
149
|
+
|
|
150
|
+
async function requestWithRetry(requestOptions, delaysMs = [1000, 2000, 4000]) {
|
|
151
|
+
let lastError;
|
|
152
|
+
try {
|
|
153
|
+
return await wix.request(requestOptions);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
lastError = err;
|
|
156
|
+
}
|
|
157
|
+
for (const delayMs of delaysMs) {
|
|
158
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
159
|
+
try {
|
|
160
|
+
return await wix.request(requestOptions);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
lastError = err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw lastError;
|
|
166
|
+
}
|
|
117
167
|
|
|
118
|
-
|
|
168
|
+
await wix.request({
|
|
169
|
+
method: "POST",
|
|
170
|
+
url: "https://www.wixapis.com/apps-installer-service/v1/app-instance/install",
|
|
171
|
+
body: {
|
|
172
|
+
tenant: { tenantType: "SITE", id: siteId },
|
|
173
|
+
appInstance: { appDefId: "14d84998-ae09-1abf-c6fc-3f3cace5bf19" }
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return await requestWithRetry({
|
|
178
|
+
method: "POST",
|
|
179
|
+
url: "https://www.wixapis.com/locale-settings/v2/settings/mode",
|
|
180
|
+
body: { multilingualModeEnabled: true }
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
Chain any further calls inside this same function through the same `requestWithRetry` helper, after the first one succeeds.
|
|
119
185
|
|
|
120
186
|
---
|
|
121
187
|
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "Bulk Label and Unlabel Contacts"
|
|
3
|
-
description:
|
|
3
|
+
description: Creates contact label definitions or adds/removes labels from matching contacts. Use Find or Create Label for label creation alone; use bulk labeling only when the user requests contact assignments.
|
|
4
4
|
---
|
|
5
5
|
# Bulk Label And Unlabel Contacts
|
|
6
6
|
|
|
7
7
|
## Description
|
|
8
8
|
Adds and removes labels from multiple contacts using the Wix Contacts REST API.
|
|
9
9
|
|
|
10
|
+
## Create labels or assign labels?
|
|
11
|
+
|
|
12
|
+
If the user asks to **create a label** (for example, "Create VIP, Wholesale, and Newsletter labels"), use [Find or Create Label](https://dev.wix.com/docs/api-reference/crm/members-contacts/contacts/labels/find-or-create-label) directly. Creating a label definition does not require finding contacts or starting a bulk labeling job.
|
|
13
|
+
|
|
14
|
+
```http
|
|
15
|
+
POST https://www.wixapis.com/contacts/v4/labels
|
|
16
|
+
Content-Type: application/json
|
|
17
|
+
|
|
18
|
+
{"displayName":"VIP"}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Use the returned `label` and its `key`; `newLabel` distinguishes a newly created label from an existing one. For several names, make one call per name and check each result. Stop here when the request is only to create labels: do not assign them to contacts.
|
|
22
|
+
|
|
23
|
+
If the user asks to **label or unlabel contacts**, resolve the requested label keys first, then follow the bulk assignment flow below. Create a missing label only when that is part of the authorized request. An explicit request to create labels or apply them to a specified set of contacts authorizes that operation; otherwise confirm the target and intended change before mutating.
|
|
24
|
+
|
|
25
|
+
## Bulk assignment flow
|
|
26
|
+
|
|
10
27
|
Labels are added to and removed from all contacts that meet the specified `filter` and `search` criteria.
|
|
11
28
|
The request should specify a `filter` value, a `search` value, or both.
|
|
12
29
|
To perform a dry run, call [Query Contacts](https://dev.wix.com/docs/api-reference/crm/members-contacts/contacts/contacts/contact-v4/query-contacts) with the intended filter options.
|
|
@@ -17,6 +34,22 @@ The job's status can be retrieved with [Get Bulk Job](https://dev.wix.com/docs/a
|
|
|
17
34
|
|
|
18
35
|
**IMPORTANT NOTE:** When specific contacts are to be labeled, they should be filtered by id.
|
|
19
36
|
|
|
37
|
+
### Steps
|
|
38
|
+
|
|
39
|
+
1. **Resolve label keys.** When adding, Find or Create Label (above) returns the `key`. To remove a label, or to
|
|
40
|
+
use only a label that already exists, look it up by name instead of creating it:
|
|
41
|
+
`POST https://www.wixapis.com/contacts/v4/labels/query` with
|
|
42
|
+
`{"query":{"filter":{"displayName":{"$eq":"Newsletter"}}}}`, and take each returned label's `key`.
|
|
43
|
+
2. **Resolve named contacts to IDs** with Search Contacts, as in
|
|
44
|
+
[Update a Contact](update-a-contact.md) — Query Contacts cannot filter on a name:
|
|
45
|
+
`POST https://www.wixapis.com/contacts/v5/contacts/search` with `{"search":{"search":{"expression":"Leo Marsh"}}}`.
|
|
46
|
+
Each result in `contacts` has an `id` and `name.first` / `name.last`; keep only exact name matches. If none or
|
|
47
|
+
more than one contact matches, stop and ask the user.
|
|
48
|
+
3. **Start the job** with the endpoint below, filtering by the resolved IDs:
|
|
49
|
+
`{"filter":{"id":{"$in":["<CONTACT_ID>"]}},"labelKeysToAdd":["<LABEL_KEY>"]}` (or `labelKeysToRemove`).
|
|
50
|
+
4. **Confirm the job finished** with `GET https://www.wixapis.com/contacts/v4/bulk/jobs/{jobId}`; repeat until
|
|
51
|
+
`job.status` is `COMPLETED`, then report `job.successTotal` and `job.failedTotal`.
|
|
52
|
+
|
|
20
53
|
## API Endpoint
|
|
21
54
|
`POST https://www.wixapis.com/contacts/v4/bulk/contacts/add-remove-labels`
|
|
22
55
|
|
|
@@ -29,10 +62,10 @@ curl -X POST \
|
|
|
29
62
|
-H 'Content-Type: application/json' \
|
|
30
63
|
-d '{
|
|
31
64
|
"filter": {
|
|
32
|
-
"
|
|
65
|
+
"id": { "$in": ["<CONTACT_ID_1>", "<CONTACT_ID_2>"] }
|
|
33
66
|
},
|
|
34
|
-
"labelKeysToAdd": ["custom.
|
|
35
|
-
"labelKeysToRemove": ["custom.
|
|
67
|
+
"labelKeysToAdd": ["custom.newsletter"],
|
|
68
|
+
"labelKeysToRemove": ["custom.prospect"]
|
|
36
69
|
}'
|
|
37
70
|
```
|
|
38
71
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "Pricing & Promotions"
|
|
3
|
-
description:
|
|
3
|
+
description: Routes discounts, coupons, sales and bundles to promotion recipes, and visual product ribbons to the Catalog Ribbons API. Load this dispatcher for mixed pricing/refund/payment/product-price/shipping requests to choose the appropriate APIs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Pricing & Promotions
|
|
@@ -14,7 +14,9 @@ Discount rules, coupon codes, sales, ribbons, bundles, tiered pricing, and the s
|
|
|
14
14
|
- A standing $0 shipping option/region rate → see **Shipping & fulfillment**.
|
|
15
15
|
- Refunding a previous discounted order → route to verified Get Paid/payment docs or Dashboard guidance.
|
|
16
16
|
|
|
17
|
-
> **
|
|
17
|
+
> **Ribbon-only requests** — go directly to the ribbon path below; skip MerchantContext and promotion-strategy discovery.
|
|
18
|
+
>
|
|
19
|
+
> **Before dispatching promotion requests** — confirm MerchantContext is loaded. If `siteData.country` is not in your conversation context, load it via [Load Merchant Context](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/skills/e-commerce-load-context). Skip if already loaded.
|
|
18
20
|
>
|
|
19
21
|
> **Promotion dispatch.** Score each entry below by (a) the merchant's query → `intent:*` tags, (b) MerchantContext → context tags. Load the **highest-scoring** entry. Ties → highest `priority`. No match → follow the base recipe at the bottom.
|
|
20
22
|
>
|
|
@@ -24,9 +26,33 @@ Discount rules, coupon codes, sales, ribbons, bundles, tiered pricing, and the s
|
|
|
24
26
|
|
|
25
27
|
> - [Create coupon](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/skills/pricing-create-coupon) — tags: `[intent:create-coupon]` · priority 0
|
|
26
28
|
> - [Create discount rule (auto-apply)](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/skills/pricing-create-discount-rule) — tags: `[intent:create-discount-rule]` · priority 0
|
|
27
|
-
> - [
|
|
29
|
+
> - [Create or reuse a Catalog V3 ribbon](https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/ribbons-v3/get-or-create-ribbon) — tags: `[intent:add-ribbon]` · priority 0 · *a ribbon is a visual product label, not a discount rule; follow the ribbon path below*
|
|
28
30
|
> - [Schedule a future sale](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/skills/pricing-create-discount-rule) — tags: `[intent:schedule-sale]` · priority 0 · *uses Discount Rules with `startTime` in the future*
|
|
29
31
|
|
|
32
|
+
### Ribbon path
|
|
33
|
+
|
|
34
|
+
For a Catalog V3 request such as "Create a Bestseller ribbon", use the idempotent name-based [Get or Create Ribbon](https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/ribbons-v3/get-or-create-ribbon) method:
|
|
35
|
+
|
|
36
|
+
```http
|
|
37
|
+
POST https://www.wixapis.com/stores/v3/ribbons/get-or-create
|
|
38
|
+
Content-Type: application/json
|
|
39
|
+
|
|
40
|
+
{"ribbonName":"Bestseller"}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Read the returned `ribbon.id`. If the user requested only the ribbon definition, report that result and stop. If they also requested assignment to a product, read the product's current `revision` with `GET https://www.wixapis.com/stores/v3/products/{productId}`, as in [Update Product with Options](../stores/update-product-with-options.md). Then send only the ribbon with that `revision` through [Update Product](https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/update-product); fields you omit are left unchanged:
|
|
44
|
+
|
|
45
|
+
```http
|
|
46
|
+
PATCH https://www.wixapis.com/stores/v3/products/{productId}
|
|
47
|
+
Content-Type: application/json
|
|
48
|
+
|
|
49
|
+
{"product":{"id":"<PRODUCT_ID>","revision":"<CURRENT_REVISION>","ribbon":{"id":"<RIBBON_ID>"}}}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This path covers Catalog V3.
|
|
53
|
+
|
|
54
|
+
Do not create a coupon, discount rule, or price change just because a ribbon says "Sale". Those require a separate discount request. An explicit ribbon creation or assignment request authorizes that operation; otherwise confirm the intended change first. Merchant context needed for promotion recommendations is not a prerequisite to creating a named visual ribbon.
|
|
55
|
+
|
|
30
56
|
### Business flows — the orchestrator
|
|
31
57
|
|
|
32
58
|
The single business-flow orchestrator (`recommend-ecommerce-strategy`) handles all strategic discount intents. It classifies internally (SEASONAL / UPSELL_BOOST / STOCK_MOVER / BUNDLE_AND_SAVE / ABANDONED_CART) and loads its `goal-*` / `flow-*` support files from the kept ecommerce-root siblings.
|