@wix/agent-skills 1.22.0 → 1.24.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 (45) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +12 -4
  3. package/skills/wix-app/references/APP_VALIDATION.md +16 -10
  4. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +64 -50
  5. package/skills/wix-app/references/SDK_MODULE_MAP.md +2 -0
  6. package/skills/wix-app/references/dashboard-page/DRAFT_TEMPLATE.md +1 -1
  7. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +4 -3
  8. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +28 -25
  9. package/skills/wix-app/references/editor-react-component/BRANDED-COMPONENTS.md +8 -2
  10. package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +12 -14
  11. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +4 -3
  12. package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +66 -9
  13. package/skills/wix-app/references/editor-react-component/EDITOR-EXTENSION-CONFIGURATION.md +25 -0
  14. package/skills/wix-app/references/editor-react-component/OVERFLOW.md +146 -0
  15. package/skills/wix-app/references/editor-react-component/PARTS.md +6 -0
  16. package/skills/wix-app/scripts/scan-a11y-eslint.cjs +25 -1
  17. package/skills/{wix-headless-fast → wix-headless-kit}/SKILL.md +96 -50
  18. package/skills/{wix-headless-fast → wix-headless-kit}/cold-start/bootstrap.mjs +2 -2
  19. package/skills/{wix-headless-fast → wix-headless-kit}/cold-start/cold-start.md +26 -31
  20. package/skills/wix-headless-kit/guides/capabilities.md +76 -0
  21. package/skills/{wix-headless-fast → wix-headless-kit}/guides/existing-site.md +3 -1
  22. package/skills/wix-headless-kit/guides/feedback.md +98 -0
  23. package/skills/wix-headless-kit/guides/migration.md +54 -0
  24. package/skills/{wix-headless-fast → wix-headless-kit}/guides/reference-mode.md +4 -1
  25. package/skills/{wix-headless-fast → wix-headless-kit}/install/AGENTS.md +6 -1
  26. package/skills/{wix-headless-fast → wix-headless-kit}/install/agents-md.mjs +5 -1
  27. package/skills/{wix-headless-fast → wix-headless-kit}/install/attach.mjs +14 -1
  28. package/skills/wix-headless-kit/install/check.mjs +87 -0
  29. package/skills/wix-headless-kit/install/context.mjs +169 -0
  30. package/skills/{wix-headless-fast → wix-headless-kit}/install/deploy.mjs +101 -23
  31. package/skills/{wix-headless-fast → wix-headless-kit}/install/setup.mjs +115 -41
  32. package/skills/{wix-headless-fast → wix-headless-kit}/install/templates.mjs +1 -1
  33. package/skills/wix-manage/SKILL.md +3 -3
  34. package/skills/wix-manage/references/app-installation/install-wix-apps.md +68 -2
  35. package/skills/wix-manage/references/cms/cms-dashboard-navigation.md +2 -2
  36. package/skills/wix-manage/references/cms/cms-data-items-crud.md +16 -16
  37. package/skills/wix-manage/references/cms/cms-publishing-flow.md +15 -15
  38. package/skills/wix-manage/references/cms/cms-schema-management.md +6 -6
  39. package/skills/wix-manage/references/contacts/bulk-label-and-unlabel-contacts.md +37 -4
  40. package/skills/wix-manage/references/ecommerce/ecom-pricing.md +29 -3
  41. package/skills/wix-manage/references/sites/query-sites.md +1 -8
  42. package/skills/wix-manage/references/sites/site-import.md +101 -37
  43. package/skills/wix-auth/SKILL.md +0 -12
  44. package/skills/wix-auth/references/device-flow.md +0 -140
  45. /package/skills/{wix-headless-fast → wix-headless-kit}/install/lock.mjs +0 -0
@@ -0,0 +1,169 @@
1
+ // The site context of a project folder — the one place that says which site a call targets.
2
+ //
3
+ // node <SKILL_ROOT>/install/context.mjs [--refresh] [--no-pull] (prints one JSON object)
4
+ //
5
+ // Two identities live in a Wix project and are the same site almost always:
6
+ // deploy — `wix.config.json` (`siteId`, `appId`): where `wix release` uploads. The CLI's business.
7
+ // content — `.env.local` (`WIX_CLIENT_ID`, what `wix env pull` writes): the app the SDK client
8
+ // runs as, on every stack (the Astro integration reads WIX_CLIENT_ID from the env and
9
+ // never the config; the other stacks get it copied into src/wix/config.ts by deploy.mjs).
10
+ // They differ on a MIGRATION PREVIEW: a project whose config points at a fresh site created only to
11
+ // host the deployment, while `.env.local` carries the credentials of the site being migrated (the
12
+ // parent) and says so. Then every admin, discovery and seed call targets the parent, the SDK client
13
+ // is the parent's, and only the release goes to the child. Completing the migration is a CLI step
14
+ // that does not exist yet; this module only reads the state.
15
+ //
16
+ // `env pull` runs here when `.env.local` is missing (or --refresh): it is the source of the content
17
+ // identity, and the Astro build refuses to run without it. Non-interactive (CI=1); a failure is
18
+ // reported in `pullError` and the config's ids stand in, so a caller can still work offline.
19
+ import { spawnSync } from "node:child_process";
20
+ import { existsSync, readFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+
23
+ // The variables the editor-to-headless migration writes into the child project's `prod`
24
+ // environment (headless-bo, editorMigrations/environmentMetadata.ts), which `wix env pull` copies
25
+ // into `.env.local` unchanged. The names are the platform's and are spelled once, here.
26
+ export const ENV = {
27
+ clientId: "WIX_CLIENT_ID",
28
+ parentSiteId: "EDITOR_MIGRATION_PARENT_SITE_ID",
29
+ /** ACTIVE while the migration is under way; COMPLETED once the parent serves the child's frontend. */
30
+ status: "EDITOR_MIGRATION_STATUS",
31
+ childSiteId: "EDITOR_MIGRATION_CHILD_SITE_ID",
32
+ childAppId: "EDITOR_MIGRATION_CHILD_APP_ID",
33
+ };
34
+
35
+ /** KEY=value lines of a dotenv file, quotes stripped; null when the file is absent. */
36
+ export function readEnvFile(file) {
37
+ if (!existsSync(file)) return null;
38
+ const out = {};
39
+ for (const raw of readFileSync(file, "utf8").split(/\r?\n/)) {
40
+ const line = raw.trim();
41
+ if (!line || line.startsWith("#")) continue;
42
+ const eq = line.indexOf("=");
43
+ if (eq === -1) continue;
44
+ const key = line.slice(0, eq).trim().replace(/^export\s+/, "");
45
+ let value = line.slice(eq + 1).trim();
46
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
47
+ value = value.slice(1, -1).replace(/\\"/g, '"');
48
+ }
49
+ out[key] = value;
50
+ }
51
+ return out;
52
+ }
53
+
54
+ export function readWixConfig(cwd) {
55
+ const file = join(cwd, "wix.config.json");
56
+ if (!existsSync(file)) return null;
57
+ try { return JSON.parse(readFileSync(file, "utf8")); } catch { return null; }
58
+ }
59
+
60
+ const ENV_PULL = ["-y", "@wix/cli@latest", "env", "pull"];
61
+ const runPull = (dir) => spawnSync("npx", ENV_PULL, { cwd: dir, env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 180_000 });
62
+
63
+ /**
64
+ * `wix env pull` into `cwd/.env.local`, in place. Every project shape gets the command since Wix CLI
65
+ * 1.1.253 (2026-09-29): before it, a config with `site.outputDirectory` (the static stack, a site
66
+ * published through the drop flow) landed in a limited command set without `env`, and this pulled
67
+ * through a temp copy of the config. Non-interactive; a failure comes back as `error`.
68
+ */
69
+ export function pullEnv(cwd) {
70
+ const envFile = join(cwd, ".env.local");
71
+ const r = runPull(cwd);
72
+ 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) };
74
+ }
75
+
76
+ /**
77
+ * Whether a frontend exists in this folder: a `package.json` (a bundled project), an `index.html` at
78
+ * the root (plain pages), or an `index.html` inside the folder the config's `site.outputDirectory`
79
+ * names (a static site laid out for release, pages under site/). Nothing else counts — not this
80
+ * skill's code, not the skills folder, not AGENTS.md: those say who ran here, not what is here.
81
+ */
82
+ export function frontendPresent(cwd = process.cwd()) {
83
+ const has = (p) => existsSync(join(cwd, p));
84
+ const outDir = (readWixConfig(cwd)?.site?.outputDirectory ?? "").replace(/^\.\//, "").replace(/\/$/, "");
85
+ return {
86
+ packageJson: has("package.json"),
87
+ rootIndex: has("index.html"),
88
+ outputIndex: !!outDir && outDir !== "." && has(join(outDir, "index.html")),
89
+ outputDirectory: outDir || null,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * What the folder IS, from five file facts — the one classification every script and SKILL.md
95
+ * step 3 share: wix.config.json, its site.outputDirectory, the migration variables in .env.local,
96
+ * package.json, index.html. `migrationActive` comes from siteContext (it needs `.env.local`).
97
+ *
98
+ * empty nothing that reads as a project → setup CREATE: the run makes the site, seeds the plan
99
+ * project a frontend, no wix.config.json → setup ADOPT: `init` gives it a new, empty site, seeds the plan
100
+ * config-only a config, no frontend → attach.mjs on the config's site; nothing seeded
101
+ * migration a config whose .env.local declares an ACTIVE editor migration, with or without the blank
102
+ * Astro starter the download carries → setup MIGRATE; nothing seeded. Decided before the
103
+ * frontend test: the starter's package.json must not read as a project to iterate on
104
+ * wix-project a config AND a frontend (package.json, or index.html in the output folder) → iterate:
105
+ * never scaffold, init or reseed; deploy.mjs adds a solution, edits, release
106
+ * published-static a config, index.html at the ROOT, no package.json, no laid-out output folder (a site
107
+ * published through the drop flow and downloaded) → setup makes site/ the upload and
108
+ * deploys the REST layer; the config's site, no init, nothing seeded
109
+ */
110
+ export function folderShape(cwd = process.cwd(), { migrationActive = false } = {}) {
111
+ const config = existsSync(join(cwd, "wix.config.json"));
112
+ const f = frontendPresent(cwd);
113
+ const next = {
114
+ empty: "setup.mjs --vertical <v> --business-name <brand> [--plan]: creates the site here and seeds the plan (CREATE)",
115
+ project: "setup.mjs --vertical <v> --stack <stack> [--plan]: init links the folder to a new, empty site, seeds the plan and deploys (ADOPT)",
116
+ "config-only": "attach.mjs: the site exists and has no frontend yet; read what it holds (the vertical's read-site.mjs) — nothing is seeded; the vertical's seed module with a plan only when the brief supplies or describes content",
117
+ migration: "setup.mjs (MIGRATE): the shipped code into the starter the download carries (or the composed template around a bare config); the site being migrated owns its content — guides/migration.md",
118
+ "wix-project": "iterate: never scaffold, init or reseed. deploy.mjs <vertical…> --stack <stack> adds a solution, then ONE npm install; file edits for a change; release. Read the site (read-site.mjs) before any seed module runs",
119
+ "published-static": "setup.mjs --vertical <v>: the config's site, no init; site/ becomes the upload and the REST layer lands in site/js/wix/; move the pages, styles and assets into site/. Nothing is seeded: read the site, then run the vertical's seed module with a plan when the brief gives content; release keeps the URL",
120
+ };
121
+ let shape;
122
+ if (!config) shape = f.packageJson || f.rootIndex ? "project" : "empty";
123
+ else if (migrationActive) shape = "migration";
124
+ else if (f.packageJson || f.outputIndex) shape = "wix-project";
125
+ else if (f.rootIndex) shape = "published-static";
126
+ else shape = "config-only";
127
+ return { shape, next: next[shape], facts: { config, ...f } };
128
+ }
129
+
130
+ /**
131
+ * `{ folder: { shape, next, tells }, deploy: { siteId, appId }, content: { siteId, clientId },
132
+ * migration: { active, parentSiteId }, env: { file, present, pulled }, pullError?, warnings: [] }`.
133
+ * `pull`: "auto" (default) pulls when `.env.local` is missing; true always; false never.
134
+ */
135
+ export function siteContext({ cwd = process.cwd(), pull = "auto" } = {}) {
136
+ const config = readWixConfig(cwd) ?? {};
137
+ const deploy = { siteId: config.siteId ?? config.projectId ?? null, appId: config.appId ?? null };
138
+ const envFile = join(cwd, ".env.local");
139
+ let pulled = false, pullError;
140
+ if (deploy.siteId && (pull === true || (pull === "auto" && !existsSync(envFile)))) {
141
+ const r = pullEnv(cwd);
142
+ if (r.ok) pulled = r.via;
143
+ else pullError = r.error;
144
+ }
145
+ const env = readEnvFile(envFile) ?? {};
146
+ const parentSiteId = env[ENV.parentSiteId] || null;
147
+ const status = env[ENV.status];
148
+ // ACTIVE (or a parent id with no status yet) is a migration under way; COMPLETED means the parent
149
+ // already serves this frontend and the project is an ordinary one again.
150
+ const active = !!parentSiteId && (status === undefined || status.trim().toUpperCase() === "ACTIVE");
151
+ const migration = { active, parentSiteId: active ? parentSiteId : null, ...(status ? { status: status.trim().toUpperCase() } : {}) };
152
+ const clientId = env[ENV.clientId] || deploy.appId;
153
+ const content = { siteId: active ? parentSiteId : deploy.siteId, clientId };
154
+ const warnings = [];
155
+ if (env[ENV.clientId] && deploy.appId && env[ENV.clientId] !== deploy.appId && !active && migration.status !== "COMPLETED") {
156
+ warnings.push(`.env.local ${ENV.clientId} differs from wix.config.json appId and no migration is declared — the SDK client runs as the env's app, the release goes to the config's site`);
157
+ }
158
+ if (active && migration.parentSiteId === deploy.siteId) {
159
+ warnings.push("the migration's parent site is the deploy site itself — nothing is being migrated");
160
+ }
161
+ return { folder: folderShape(cwd, { migrationActive: active }), deploy, content, migration, env: { file: envFile, present: Object.keys(env).length > 0, pulled }, ...(pullError ? { pullError } : {}), warnings };
162
+ }
163
+
164
+ // ---- CLI ----------------------------------------------------------------------------------------
165
+ if (process.argv[1] && /context\.mjs$/.test(process.argv[1])) {
166
+ const argv = process.argv.slice(2);
167
+ const pull = argv.includes("--no-pull") ? false : argv.includes("--refresh") ? true : "auto";
168
+ console.log(JSON.stringify(siteContext({ pull }), null, 2));
169
+ }
@@ -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: {
@@ -106,6 +111,18 @@ const VERTICAL_DEPS = {
106
111
  "@wix/essentials": "^1.0.6",
107
112
  },
108
113
  },
114
+ rentals: {
115
+ // A rental is a Bookings service with rentals-specific values: the same SDK modules as bookings.
116
+ core: {
117
+ "@wix/bookings": "^1.0.1650",
118
+ "@wix/auto_sdk_ecom_cart-v-2": "^1.0.192",
119
+ "@wix/redirects": "^1.0.125",
120
+ },
121
+ astro: {
122
+ "@wix/seo": "^1.0.79",
123
+ "@wix/essentials": "^1.0.6",
124
+ },
125
+ },
109
126
  blog: {
110
127
  core: {
111
128
  "@wix/blog": "^1.0.645",
@@ -160,6 +177,19 @@ const VERTICAL_DEPS = {
160
177
  "@wix/redirects": "^1.0.125",
161
178
  },
162
179
  },
180
+ faq: {
181
+ core: {
182
+ "@wix/faq": "^1.0.76",
183
+ },
184
+ // no astro block: the FAQ page is a plain page (no wixMetadata item routing); its JSON-LD is inline
185
+ },
186
+ donations: {
187
+ core: {
188
+ "@wix/donations": "^1.0.64",
189
+ "@wix/ecom": "^1.0.2454",
190
+ "@wix/redirects": "^1.0.125",
191
+ },
192
+ },
163
193
  };
164
194
 
165
195
  const COPY = { recursive: true, force: false, errorOnExist: false };
@@ -211,6 +241,27 @@ if (!Array.isArray(uploadPolicies)) {
211
241
  console.log(JSON.stringify({ error: "plan.capabilities.mediaUpload.policies must be an array" }));
212
242
  process.exit(1);
213
243
  }
244
+ // Site search is on when the plan carries the entry (`{}` is enough; `true` too). Its fields are
245
+ // data for the generated config; the shipped core drops unknown type names, so only shapes are checked.
246
+ const siteSearchEntry = plan.capabilities?.siteSearch;
247
+ const siteSearch = siteSearchEntry === true ? {} : siteSearchEntry && typeof siteSearchEntry === "object" ? siteSearchEntry : null;
248
+ if (siteSearchEntry != null && siteSearchEntry !== false && !siteSearch) {
249
+ console.log(JSON.stringify({ error: "plan.capabilities.siteSearch must be an object (or true)" }));
250
+ process.exit(1);
251
+ }
252
+ if (siteSearch) {
253
+ const strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
254
+ const map = (v) => v === undefined || (v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string"));
255
+ if ((siteSearch.types !== undefined && !strings(siteSearch.types)) || !map(siteSearch.routes) || !map(siteSearch.labels)) {
256
+ console.log(JSON.stringify({ error: "plan.capabilities.siteSearch: types must be string[]; routes and labels must be objects of strings" }));
257
+ process.exit(1);
258
+ }
259
+ }
260
+ // The generated file is data only; the shipped core resolves defaults (types, routes, labels) from it.
261
+ const siteSearchConfigSource = () =>
262
+ `// Generated by wix-headless-kit install/deploy.mjs from plan.capabilities.siteSearch.\n` +
263
+ `// Edit the plan and redeploy. Defaults for anything omitted are in search-core.ts (resolveConfig).\n` +
264
+ `export const siteSearchPlan = ${JSON.stringify(siteSearch, null, 2)};\n`;
214
265
  // ---- static stack: the REST layer, composed flat and stripped ------------------------------------
215
266
  if (stack === "static") {
216
267
  const { spawnSync } = await import("node:child_process");
@@ -218,10 +269,9 @@ if (stack === "static") {
218
269
  const OUT = outDir ? resolve(PROJECT, outDir) : PROJECT;
219
270
  const JS = join(OUT, "js", "wix");
220
271
  result.out = outDir ?? ".";
221
- let clientId = clientIdFlag;
222
- if (!clientId && existsSync(join(PROJECT, "wix.config.json"))) {
223
- clientId = JSON.parse(readFileSync(join(PROJECT, "wix.config.json"), "utf8")).appId ?? null;
224
- }
272
+ // The client id the browser mints visitor tokens with: --client-id, else the CONTENT app from
273
+ // .env.local (`wix env pull`; on a migration preview the parent site's app), else the config's appId.
274
+ let clientId = clientIdFlag ?? (existsSync(join(PROJECT, "wix.config.json")) ? siteContext({ cwd: PROJECT }).content.clientId : null);
225
275
  if (!clientId) {
226
276
  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
277
  process.exit(1);
@@ -233,19 +283,23 @@ if (stack === "static") {
233
283
  }
234
284
  // Only files that aren't there yet — a re-run restores, never clobbers (same as the copy below).
235
285
  cpSync(join(REF, "shared", "rest"), JS, COPY);
236
- for (const vertical of requested) {
237
- cpSync(join(REF, vertical, "rest"), JS, COPY);
286
+ // Every layer with a rest/ twin: the requested verticals, plus the site-search capability when the
287
+ // plan turns it on (same shape — rest/ beside app/wix/<name>/ — at templates/shared/capabilities).
288
+ const layers = requested.map((vertical) => [vertical, join(REF, vertical)]);
289
+ if (siteSearch) layers.push(["site-search", join(REF, SITE_SEARCH)]);
290
+ for (const [vertical, root] of layers) {
291
+ cpSync(join(root, "rest"), JS, COPY);
238
292
  // The vertical's transport-agnostic core: types, every *-core.ts, and every *-store.ts under its
239
293
  // app/wix/<vertical>/ — the stores are the hooks' logic without React, over the same function
240
294
  // names the REST twin exports, so they run against it unchanged.
241
- const appWix = join(REF, vertical, "app", "wix", vertical);
295
+ const appWix = join(root, "app", "wix", vertical);
242
296
  for (const f of readdirSync(appWix)) {
243
297
  // Every vertical has a types.ts; flat in one folder they would collide, so each lands as
244
298
  // <vertical>-types.ts and the vertical's own files are pointed at it below.
245
299
  if (f === "types.ts") cpSync(join(appWix, f), join(JS, `${vertical}-types.ts`), COPY);
246
300
  else if (f.endsWith("-core.ts") || f.endsWith("-store.ts")) cpSync(join(appWix, f), join(JS, f), COPY);
247
301
  }
248
- const own = new Set([...readdirSync(join(REF, vertical, "rest")), ...readdirSync(appWix)].filter((f) => f.endsWith(".ts") && f !== "types.ts"));
302
+ const own = new Set([...readdirSync(join(root, "rest")), ...readdirSync(appWix)].filter((f) => f.endsWith(".ts") && f !== "types.ts"));
249
303
  for (const f of own) {
250
304
  const fp = join(JS, f);
251
305
  if (!existsSync(fp)) continue;
@@ -253,7 +307,12 @@ if (stack === "static") {
253
307
  const fixed = src.replace(/from\s+"\.\/types(?:\.js)?"/g, `from "./${vertical}-types.js"`);
254
308
  if (fixed !== src) writeFileSync(fp, fixed);
255
309
  }
256
- result.verticals.push(vertical);
310
+ if (VERTICALS.includes(vertical)) result.verticals.push(vertical);
311
+ }
312
+ if (siteSearch) {
313
+ // The REST twin imports the plan as data from beside it (shared/rest already owns config.ts).
314
+ writeFileSync(join(JS, "site-search-config.generated.ts"), siteSearchConfigSource());
315
+ result.capabilities = { ...result.capabilities, siteSearch: true };
257
316
  }
258
317
  // Browser ESM resolves nothing: every relative import needs its .js. The rest/ files are written
259
318
  // that way; the files borrowed from app/wix/ are bundler-style and get the suffix here.
@@ -286,7 +345,7 @@ if (stack === "static") {
286
345
  result.js = `${outDir ? outDir.replace(/\/$/, "") + "/" : ""}js/wix/*.js`;
287
346
  // The note lists what actually landed, so it holds for every vertical (and several at once).
288
347
  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}`);
348
+ 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
349
  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
350
  console.log(JSON.stringify(result));
292
351
  process.exit(0);
@@ -319,11 +378,20 @@ if (uploadPolicies.length) {
319
378
  cpSync(join(REF, "shared", "capabilities", "media-upload", "app"), SRC, COPY);
320
379
  cpSync(join(REF, "shared", "capabilities", "media-upload", "app-astro"), SRC, COPY);
321
380
  }
381
+ // Site search ships like a vertical (data layer, stores, hooks, components, a search page) at a
382
+ // disjoint path (src/wix/site-search/, src/components/site-search/, …), so the same per-stack rule
383
+ // applies: lib takes only app/wix/, astro adds app-astro/.
384
+ if (siteSearch) {
385
+ if (stack === "lib") cpSync(join(REF, SITE_SEARCH, "app", "wix"), join(SRC, "wix"), COPY);
386
+ else cpSync(join(REF, SITE_SEARCH, "app"), SRC, COPY);
387
+ if (stack === "astro") cpSync(join(REF, SITE_SEARCH, "app-astro"), SRC, COPY);
388
+ }
322
389
 
323
390
  const unknown = requested.filter((v) => !VERTICALS.includes(v));
324
391
  // lib: only @wix/sdk from the shared deps — the Tailwind pair serves the components, which don't ship there.
325
392
  const wantedDeps = stack === "lib" ? { "@wix/sdk": SHARED_DEPS["@wix/sdk"] } : { ...SHARED_DEPS };
326
393
  if (uploadPolicies.length) Object.assign(wantedDeps, CAPABILITY_DEPS["media-upload"]);
394
+ if (siteSearch) Object.assign(wantedDeps, CAPABILITY_DEPS["site-search"]);
327
395
  for (const vertical of requested.filter((v) => VERTICALS.includes(v))) {
328
396
  if (stack === "lib") cpSync(join(REF, vertical, "app", "wix"), join(SRC, "wix"), COPY);
329
397
  else cpSync(join(REF, vertical, "app"), SRC, COPY);
@@ -345,12 +413,21 @@ if (uploadPolicies.length) {
345
413
  // The generated file is data only. The shipped endpoint owns validation and elevation.
346
414
  writeFileSync(
347
415
  configFile,
348
- `// Generated by wix-headless-fast install/deploy.mjs from plan.capabilities.mediaUpload.\n` +
416
+ `// Generated by wix-headless-kit install/deploy.mjs from plan.capabilities.mediaUpload.\n` +
349
417
  `// Edit the plan and redeploy; do not add browser-controlled policy selection here.\n` +
350
418
  `export const mediaUploadPolicies = ${JSON.stringify(uploadPolicies, null, 2)} as const;\n`,
351
419
  );
352
420
  result.capabilities = { mediaUpload: uploadPolicies.map((policy) => policy.id) };
353
421
  }
422
+ if (siteSearch) {
423
+ // The SDK transport imports the plan as data from beside it; defaults resolve in the shipped core.
424
+ writeFileSync(join(SRC, "wix", "site-search", "config.generated.ts"), siteSearchConfigSource());
425
+ result.capabilities = { ...result.capabilities, siteSearch: true };
426
+ if (siteSearch.install) {
427
+ // Indexing needs the Wix Site Search app on the site — a site mutation the deploy never makes.
428
+ 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("; ");
429
+ }
430
+ }
354
431
 
355
432
  // ---- dependency patch (fill-only) ----------------------------------------------------------------
356
433
  if (result.verticals.length && existsSync(PKG_JSON)) {
@@ -416,12 +493,15 @@ if (stack === "astro" && result.verticals.length && existsSync(ASTRO_CONFIG)) {
416
493
 
417
494
  // ---- ready-made links -----------------------------------------------------------------------------
418
495
  // Emit the siteId and the dashboard deep links so nothing downstream re-derives or retypes them.
496
+ // The site the links point at is the CONTENT site — the parent on a migration preview, where the
497
+ // config's site only hosts the deployment (install/context.mjs).
419
498
  const WIX_CONFIG = join(PROJECT, "wix.config.json");
420
- if (existsSync(WIX_CONFIG)) {
421
- const config = JSON.parse(readFileSync(WIX_CONFIG, "utf8"));
422
- const siteId = config.siteId ?? config.projectId;
499
+ const ctx = existsSync(WIX_CONFIG) ? siteContext({ cwd: PROJECT }) : null;
500
+ if (ctx) {
501
+ const siteId = ctx.content.siteId;
423
502
  if (siteId) {
424
503
  result.siteId = siteId;
504
+ if (ctx.migration.active) result.migration = { parentSiteId: ctx.migration.parentSiteId, deploySiteId: ctx.deploy.siteId };
425
505
  result.dashboardUrl = `https://manage.wix.com/dashboard/${siteId}`;
426
506
  if (result.verticals.includes("storefront")) {
427
507
  result.productsUrl = `https://manage.wix.com/dashboard/${siteId}/wix-stores/products`;
@@ -438,15 +518,13 @@ if (!requested.length) {
438
518
  }
439
519
 
440
520
  // ---- client ids ---------------------------------------------------------------------------------
441
- // The shared data client is ambient on managed Astro. Members are different: their shipped
442
- // custom credential flow always needs an explicit public OAuth client, including on Astro.
443
- let clientId = clientIdFlag;
444
- if (!clientId && existsSync(join(PROJECT, "wix.config.json"))) {
445
- // On a Wix-managed project the public OAuth client id IS the appId.
446
- clientId =
447
- JSON.parse(readFileSync(join(PROJECT, "wix.config.json"), "utf8")).appId ??
448
- null;
449
- }
521
+ // The shared data client is ambient on managed Astro, and so is the members flow there (the
522
+ // integration's built-in /api/auth routes). WIX_MEMBERS_CLIENT_ID is still filled for projects whose
523
+ // members code predates the shared seam; the shipped code reads WIX_CLIENT_ID only.
524
+ // --client-id, else the CONTENT app: .env.local's WIX_CLIENT_ID (what `wix env pull` writes — on a
525
+ // migration preview the parent site's app, the one the Astro integration itself runs as), else the
526
+ // config's appId. Never the config first: on a preview that is the deploy-only child app.
527
+ const clientId = clientIdFlag ?? ctx?.content.clientId ?? null;
450
528
  if (stack === "react" || stack === "lib") {
451
529
  if (clientId) {
452
530
  const current = readFileSync(CONFIG_TS, "utf8");