@wix/agent-skills 1.31.1 → 1.31.2

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 (90) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/STORES_VERSIONING.md +1 -1
  3. package/skills/wix-headless-kit/SKILL.md +16 -7
  4. package/skills/wix-headless-kit/guides/api-run.md +81 -51
  5. package/skills/wix-headless-kit/guides/existing-site.md +6 -0
  6. package/skills/wix-headless-kit/install/AGENTS.md +3 -2
  7. package/skills/wix-headless-kit/install/attach.mjs +32 -27
  8. package/skills/wix-headless-kit/install/background.mjs +32 -0
  9. package/skills/wix-headless-kit/install/bootstrap.mjs +28 -11
  10. package/skills/wix-headless-kit/install/check.mjs +2 -2
  11. package/skills/wix-headless-kit/install/context.mjs +49 -5
  12. package/skills/wix-headless-kit/install/deploy.mjs +33 -8
  13. package/skills/wix-headless-kit/install/env-ignore.mjs +16 -0
  14. package/skills/wix-headless-kit/install/install.mjs +95 -0
  15. package/skills/wix-headless-kit/install/pins.json +1 -1
  16. package/skills/wix-headless-kit/install/proc.mjs +46 -0
  17. package/skills/wix-headless-kit/install/setup.mjs +16 -25
  18. package/skills/wix-headless-kit/install/templates.mjs +2 -2
  19. package/skills/wix-headless-templates/blog/app/components/blog/RichContent.tsx +3 -2
  20. package/skills/wix-headless-templates/blog/project/package-lock.json +1287 -1299
  21. package/skills/wix-headless-templates/blog/project/package.json +1 -1
  22. package/skills/wix-headless-templates/blog/project/src/components/blog/RichContent.tsx +3 -2
  23. package/skills/wix-headless-templates/blog/rest/authors.ts +5 -5
  24. package/skills/wix-headless-templates/blog/rest/comments.ts +4 -4
  25. package/skills/wix-headless-templates/blog/rest/post-likes.ts +3 -3
  26. package/skills/wix-headless-templates/blog/rest/posts.ts +4 -4
  27. package/skills/wix-headless-templates/blog/rest/taxonomy.ts +5 -5
  28. package/skills/wix-headless-templates/blog/seed/seed-blog.mjs +2 -1
  29. package/skills/wix-headless-templates/bookings/rest/booking.ts +4 -4
  30. package/skills/wix-headless-templates/bookings/rest/services.ts +4 -4
  31. package/skills/wix-headless-templates/bookings/seed/seed-bookings.mjs +2 -1
  32. package/skills/wix-headless-templates/cms/rest/collections.ts +3 -3
  33. package/skills/wix-headless-templates/cms/rest/items.ts +4 -4
  34. package/skills/wix-headless-templates/cms/seed/seed-cms.mjs +2 -1
  35. package/skills/wix-headless-templates/donations/rest/campaigns.ts +4 -4
  36. package/skills/wix-headless-templates/donations/rest/donate.ts +3 -3
  37. package/skills/wix-headless-templates/donations/seed/seed-donations.mjs +2 -1
  38. package/skills/wix-headless-templates/events/rest/events.ts +4 -4
  39. package/skills/wix-headless-templates/events/rest/orders.ts +4 -4
  40. package/skills/wix-headless-templates/events/rest/registration.ts +4 -4
  41. package/skills/wix-headless-templates/events/seed/seed-events.mjs +2 -1
  42. package/skills/wix-headless-templates/faq/rest/faq.ts +4 -4
  43. package/skills/wix-headless-templates/faq/seed/seed-faq.mjs +2 -1
  44. package/skills/wix-headless-templates/forms/rest/forms.ts +4 -4
  45. package/skills/wix-headless-templates/forms/rest/submissions.ts +4 -4
  46. package/skills/wix-headless-templates/forms/seed/seed-forms.mjs +2 -1
  47. package/skills/wix-headless-templates/members/rest/auth.ts +3 -3
  48. package/skills/wix-headless-templates/members/rest/members.ts +4 -4
  49. package/skills/wix-headless-templates/members/seed/seed-members.mjs +2 -1
  50. package/skills/wix-headless-templates/portfolio/rest/portfolio.ts +4 -4
  51. package/skills/wix-headless-templates/portfolio/seed/seed-portfolio.mjs +2 -1
  52. package/skills/wix-headless-templates/pricing-plans/rest/plan-access.ts +3 -3
  53. package/skills/wix-headless-templates/pricing-plans/rest/plans.ts +4 -4
  54. package/skills/wix-headless-templates/pricing-plans/rest/purchase.ts +3 -3
  55. package/skills/wix-headless-templates/pricing-plans/seed/seed-pricing-plans.mjs +2 -1
  56. package/skills/wix-headless-templates/rentals/rest/rentals.ts +4 -4
  57. package/skills/wix-headless-templates/rentals/seed/seed-rentals.mjs +2 -1
  58. package/skills/wix-headless-templates/restaurants/rest/menu.ts +4 -4
  59. package/skills/wix-headless-templates/restaurants/rest/ordering.ts +5 -5
  60. package/skills/wix-headless-templates/restaurants/rest/reservations.ts +4 -4
  61. package/skills/wix-headless-templates/restaurants/seed/seed-restaurants.mjs +2 -1
  62. package/skills/wix-headless-templates/shared/capabilities/site-search/rest/search.ts +4 -4
  63. package/skills/wix-headless-templates/shared/capabilities/site-search/seed/install.mjs +6 -4
  64. package/skills/wix-headless-templates/shared/seed/main.mjs +15 -0
  65. package/skills/wix-headless-templates/shared/seed/wix-cli.mjs +43 -11
  66. package/skills/wix-headless-templates/storefront/INSTRUCTIONS.md +1 -1
  67. package/skills/wix-headless-templates/storefront/rest/cart.ts +5 -5
  68. package/skills/wix-headless-templates/storefront/rest/catalog.ts +5 -5
  69. package/skills/wix-headless-templates/storefront/seed/SEED.md +27 -9
  70. package/skills/wix-headless-templates/storefront/seed/seed-store.mjs +2 -1
  71. package/skills/wix-manage/SKILL.md +3 -3
  72. package/skills/wix-manage/references/app-installation/install-wix-apps.md +5 -1
  73. package/skills/wix-manage/references/app-installation/list-installed-apps.md +19 -9
  74. package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +3 -0
  75. package/skills/wix-manage/references/bookings/external-calendar-integration.md +3 -0
  76. package/skills/wix-manage/references/bookings/multi-resource-service-creation.md +3 -0
  77. package/skills/wix-manage/references/cms/cms-data-items-crud.md +11 -2
  78. package/skills/wix-manage/references/cms/cms-publishing-flow.md +63 -16
  79. package/skills/wix-manage/references/forms/update-form.md +3 -0
  80. package/skills/wix-manage/references/pricing-plans/create-and-update-pricing-plans.md +3 -0
  81. package/skills/wix-manage/references/restaurants/wix-restaurants-setup.md +3 -0
  82. package/skills/wix-manage/references/sites/create-headless-site.md +7 -50
  83. package/skills/wix-manage/references/sites/manage-oauth-apps.md +4 -4
  84. package/skills/wix-manage/references/sites/read-site-context.md +4 -0
  85. package/skills/wix-manage/references/sites/upload-static-site.md +67 -31
  86. package/skills/wix-manage/references/stores/add-store-pages-to-site.md +3 -0
  87. package/skills/wix-manage/references/stores/change-store-currency.md +3 -0
  88. package/skills/wix-manage/references/stores/create-product-catalog-v3.md +3 -0
  89. package/skills/wix-manage/references/stores/find-products-query-and-search-catalog-v3.md +3 -0
  90. package/skills/wix-manage/references/stores/update-product-with-options.md +192 -22
@@ -16,9 +16,9 @@
16
16
  // `env pull` runs here when `.env.local` is missing (or --refresh): it is the source of the content
17
17
  // identity, and the Astro build refuses to run without it. Non-interactive (CI=1); a failure is
18
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
19
  import { existsSync, readFileSync } from "node:fs";
21
20
  import { join } from "node:path";
21
+ import { failure, npmSync } from "./proc.mjs";
22
22
  const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
23
23
  const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
24
24
 
@@ -60,7 +60,7 @@ export function readWixConfig(cwd) {
60
60
  }
61
61
 
62
62
  const ENV_PULL = ["-y", WIX_CLI, "env", "pull"];
63
- const runPull = (dir) => spawnSync("npx", ENV_PULL, { cwd: dir, env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 180_000 });
63
+ const runPull = (dir) => npmSync("npx", ENV_PULL, { cwd: dir, env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 180_000 });
64
64
 
65
65
  /**
66
66
  * `wix env pull` into `cwd/.env.local`, in place. Every project shape gets the command since Wix CLI
@@ -72,7 +72,7 @@ export function pullEnv(cwd) {
72
72
  const envFile = join(cwd, ".env.local");
73
73
  const r = runPull(cwd);
74
74
  if (r.status === 0 && existsSync(envFile)) return { ok: true, via: "in place" };
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) };
75
+ return { ok: false, error: failure(r, `env pull produced no .env.local — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).trim().slice(-400) };
76
76
  }
77
77
 
78
78
  /**
@@ -96,12 +96,46 @@ export function frontendPresent(cwd = process.cwd()) {
96
96
  };
97
97
  }
98
98
 
99
+ /**
100
+ * The Wix CLI's blank Astro starter (`wix-astro-blank`, templates/blank/project), still as the CLI
101
+ * left it: every package it declares is one of the blank template's. That is the one frontend the
102
+ * kit knows carries no work of its own — a Dev Machine's never-released site, a bare `wix create`
103
+ * — so a vertical's composed template can take its place (deploy.mjs). A starter with any package
104
+ * of its own is someone's project. The blank template's package.json is read when the templates
105
+ * are on disk; otherwise the starter's known package names stand in.
106
+ */
107
+ const BLANK_STARTER = {
108
+ name: "wix-astro-blank",
109
+ packages: ["@astrojs/react", "@types/react", "@types/react-dom", "@wix/astro", "@wix/astro-pages", "@wix/astro-wix-hosting-adapter", "@wix/cli", "@wix/dashboard", "@wix/essentials", "@wix/sdk", "astro", "react", "react-dom", "typescript"],
110
+ };
111
+ export function blankStarter(cwd = process.cwd(), blankPkgPath = null) {
112
+ const pkgPath = join(cwd, "package.json");
113
+ if (!existsSync(pkgPath)) return false;
114
+ let pkg;
115
+ try { pkg = JSON.parse(readFileSync(pkgPath, "utf8")); } catch { return false; }
116
+ if (pkg.name !== BLANK_STARTER.name) return false;
117
+ let known = BLANK_STARTER.packages;
118
+ if (blankPkgPath && existsSync(blankPkgPath)) {
119
+ try {
120
+ const blank = JSON.parse(readFileSync(blankPkgPath, "utf8"));
121
+ known = Object.keys({ ...blank.dependencies, ...blank.devDependencies });
122
+ } catch { /* the known names stand in */ }
123
+ }
124
+ return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).every((name) => known.includes(name));
125
+ }
126
+
99
127
  /**
100
128
  * What the folder IS, from a few file facts — the one classification every script and SKILL.md
101
129
  * step 3 share: wix.config.json, its site.outputDirectory, the migration variables in .env.local,
102
130
  * package.json, index.html (at the root or in public/), src/pages/. `migrationActive` comes from
103
131
  * siteContext (it needs `.env.local`).
104
132
  *
133
+ * Two axes decide it: whether a site is bound to the folder (the config), and what frontend is
134
+ * here (`facts.frontend`: none, the CLI's untouched blank starter, the starter with plain pages in
135
+ * public/, plain pages with no bundler, or someone's project). The migration flag is read first.
136
+ * Anything the rows below do not name is `wix-project`, the fill-only path: never scaffold, init,
137
+ * reseed or delete.
138
+ *
105
139
  * empty nothing that reads as a project → setup CREATE: the run makes the site, seeds the plan
106
140
  * project a frontend, no wix.config.json → setup ADOPT: `init` gives it a new, empty site, seeds the plan
107
141
  * config-only a config, no frontend → attach.mjs on the config's site; nothing seeded
@@ -111,6 +145,9 @@ export function frontendPresent(cwd = process.cwd()) {
111
145
  * static-in-astro a config, a package.json, public/index.html and no src/pages/ (a site published
112
146
  * through the drop flow, on its Wix Dev Machine) → the pages stay plain files in
113
147
  * public/; the static REST layer deploys beside them; build and release
148
+ * blank-starter a config and the CLI's blank starter with no work of its own (a Dev Machine's
149
+ * never-released site, a bare `wix create`) → attach.mjs on the config's site: the
150
+ * vertical's composed template takes the starter's place, install and seed run detached
114
151
  * wix-project a config AND a frontend (package.json, or index.html in the output folder) → iterate:
115
152
  * never scaffold, init or reseed; deploy.mjs adds a solution, edits, release
116
153
  * published-static a config, index.html at the ROOT, no package.json, no laid-out output folder (a site
@@ -120,23 +157,30 @@ export function frontendPresent(cwd = process.cwd()) {
120
157
  export function folderShape(cwd = process.cwd(), { migrationActive = false } = {}) {
121
158
  const config = existsSync(join(cwd, "wix.config.json"));
122
159
  const f = frontendPresent(cwd);
160
+ const starter = f.packageJson && blankStarter(cwd);
123
161
  const next = {
124
162
  empty: "setup.mjs --vertical <v> --business-name <brand> [--plan]: creates the site here and seeds the plan (CREATE)",
125
163
  project: "setup.mjs --vertical <v> --stack <stack> [--plan]: init links the folder to a new, empty site, seeds the plan and deploys (ADOPT)",
126
164
  "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",
127
165
  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",
128
166
  "static-in-astro": "a dropped site on its Dev Machine: the pages are plain files in public/, served as they are. deploy.mjs <vertical…> --stack static --out public puts the REST layer in public/js/wix/; wire the pages in place with <script type=\"module\">: do the least that makes them work: connect each control the pages show and add only what a control can't work without (more only when the brief asks), and no Astro pages. Read the site (read-site.mjs); run the vertical's seed module with a plan only for content the brief supplies or describes. Release with npx @wix/cli@latest build && npx @wix/cli@latest release; the URL stays",
129
- "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",
167
+ "blank-starter": "attach.mjs --business-name <brand> --vertical <v…> [--plan plan.json] (the site comes from wix.config.json): the CLI's blank starter with no work of its own, as a Dev Machine or a bare wix create leaves it. attach deploys the vertical onto it with its composed lock and starts the install and the seed detached: build the brand layer while they run, then sync on .install-exit and .seed-exit before release (SKILL.md step 3). Read the site first (read-site.mjs) when it may already hold content",
168
+ "wix-project": "iterate: never scaffold, init or reseed. deploy.mjs <vertical…> --stack <stack> adds a solution, then one install: node <SKILL_ROOT>/install/install.mjs (it skips when node_modules already satisfies the lock); file edits for a change; release. Read the site (read-site.mjs) before any seed module runs",
130
169
  "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",
131
170
  };
132
171
  let shape;
133
172
  if (!config) shape = f.packageJson || f.rootIndex ? "project" : "empty";
134
173
  else if (migrationActive) shape = "migration";
135
174
  else if (f.packageJson && f.publicIndex && !f.astroPages) shape = "static-in-astro";
175
+ else if (starter) shape = "blank-starter";
136
176
  else if (f.packageJson || f.outputIndex) shape = "wix-project";
137
177
  else if (f.rootIndex) shape = "published-static";
138
178
  else shape = "config-only";
139
- return { shape, next: next[shape], facts: { config, ...f } };
179
+ const frontend = !(f.packageJson || f.rootIndex || f.outputIndex) ? "none"
180
+ : starter ? (f.publicIndex && !f.astroPages ? "starter+static" : "starter")
181
+ : f.packageJson ? "project"
182
+ : "static";
183
+ return { shape, next: next[shape], facts: { config, frontend, starter, ...f } };
140
184
  }
141
185
 
142
186
  /**
@@ -45,7 +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
+ import { blankStarter, siteContext } from "./context.mjs";
49
49
  const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
50
50
  const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
51
51
 
@@ -130,7 +130,7 @@ const VERTICAL_DEPS = {
130
130
  blog: {
131
131
  core: {
132
132
  "@wix/blog": "^1.0.645",
133
- "@wix/ricos": "^11.12.0",
133
+ "@wix/ricos": "^11.17.0", // the viewer-only entry @wix/ricos/viewer
134
134
  },
135
135
  astro: {
136
136
  "@wix/seo": "^1.0.79",
@@ -268,7 +268,7 @@ const siteSearchConfigSource = () =>
268
268
  `export const siteSearchPlan = ${JSON.stringify(siteSearch, null, 2)};\n`;
269
269
  // ---- static stack: the REST layer, composed flat and stripped ------------------------------------
270
270
  if (stack === "static") {
271
- const { spawnSync } = await import("node:child_process");
271
+ const { npmSync } = await import("./proc.mjs");
272
272
  const { rmSync } = await import("node:fs");
273
273
  const OUT = outDir ? resolve(PROJECT, outDir) : PROJECT;
274
274
  const JS = join(OUT, "js", "wix");
@@ -309,7 +309,11 @@ if (stack === "static") {
309
309
  const fp = join(JS, f);
310
310
  if (!existsSync(fp)) continue;
311
311
  const src = readFileSync(fp, "utf8");
312
- const fixed = src.replace(/from\s+"\.\/types(?:\.js)?"/g, `from "./${vertical}-types.js"`);
312
+ // The rest/ files import their real paths in the templates (../../shared/rest/client.js,
313
+ // ../app/wix/<vertical>/cart-core.js); flat in one folder, every import is a sibling.
314
+ const fixed = src
315
+ .replace(/from\s+"\.{1,2}\/(?:[^"]*\/)([^"/]+)"/g, 'from "./$1"')
316
+ .replace(/from\s+"\.\/types(?:\.js)?"/g, `from "./${vertical}-types.js"`);
313
317
  if (fixed !== src) writeFileSync(fp, fixed);
314
318
  }
315
319
  if (VERTICALS.includes(vertical)) result.verticals.push(vertical);
@@ -336,14 +340,14 @@ if (stack === "static") {
336
340
  }
337
341
  // Strip to ESM with tsc — comments are the spec for whoever reads js/wix/, so they stay.
338
342
  const sources = readdirSync(JS).filter((f) => f.endsWith(".ts")).map((f) => join(JS, f));
339
- const tsc = spawnSync(
343
+ const tsc = npmSync(
340
344
  "npx",
341
345
  ["-y", "-p", `typescript@${PINS.typescript}`, "tsc", ...sources, "--outDir", JS, "--module", "esnext", "--target", "es2022",
342
346
  "--moduleResolution", "bundler", "--lib", "es2022,dom", "--strict", "--skipLibCheck", "--removeComments", "false"],
343
347
  { encoding: "utf8", timeout: 180_000 },
344
348
  );
345
349
  if (tsc.status !== 0) {
346
- console.log(JSON.stringify({ ...result, error: `tsc strip failed: ${(tsc.stdout || tsc.stderr || "").slice(-800)}` }));
350
+ console.log(JSON.stringify({ ...result, error: `tsc strip failed: ${(tsc.stdout || tsc.stderr || tsc.error?.message || `exit ${tsc.status}`).slice(-800)}` }));
347
351
  process.exit(1);
348
352
  }
349
353
  rmSync(join(JS, "tsconfig.json"), { force: true });
@@ -435,6 +439,27 @@ if (siteSearch) {
435
439
  }
436
440
  }
437
441
 
442
+ // ---- the blank starter takes the vertical's composed template --------------------------------
443
+ // A project that is still the blank starter (context.mjs blankStarter: a Dev Machine's, or a bare
444
+ // `wix create`) holds only the blank template's packages. Deploying one vertical onto it makes
445
+ // exactly that vertical's composed template (templates/<vertical>/project: the blank scaffold plus
446
+ // the vertical), so the starter takes the composed package.json dependencies and lockfile, and the
447
+ // install reads the lock instead of resolving the vertical's tree. A project with any package of its
448
+ // own, a second vertical, or two verticals in one run is left alone.
449
+ if (stack === "astro" && result.verticals.length === 1 && blankStarter(PROJECT, join(REF, "blank", "project", "package.json"))) {
450
+ // The composed project/ is left out of a committed templates copy; templatesDir fetches it on need.
451
+ const composed = join(templatesDir({ need: `${result.verticals[0]}/project` }), result.verticals[0], "project");
452
+ if (existsSync(join(composed, "package-lock.json"))) {
453
+ const pkg = JSON.parse(readFileSync(PKG_JSON, "utf8"));
454
+ const template = JSON.parse(readFileSync(join(composed, "package.json"), "utf8"));
455
+ pkg.dependencies = template.dependencies;
456
+ pkg.devDependencies = template.devDependencies;
457
+ writeFileSync(PKG_JSON, JSON.stringify(pkg, null, 2) + "\n");
458
+ cpSync(join(composed, "package-lock.json"), join(PROJECT, "package-lock.json"));
459
+ result.lockFromTemplate = result.verticals[0];
460
+ }
461
+ }
462
+
438
463
  // ---- dependency patch (fill-only) ----------------------------------------------------------------
439
464
  if (result.verticals.length && existsSync(PKG_JSON)) {
440
465
  const pkg = JSON.parse(readFileSync(PKG_JSON, "utf8"));
@@ -468,9 +493,9 @@ if (result.verticals.length && existsSync(PKG_JSON)) {
468
493
  if (existsSync(PROJECT_LOCK)) {
469
494
  const sync = syncLockRoot(PROJECT);
470
495
  if (sync && (sync.promoted.length || sync.missing.length)) result.lock = sync;
471
- result.note = [result.note, "install with `npm ci --ignore-scripts || npm install --ignore-scripts`"].filter(Boolean).join("; ");
496
+ result.note = [result.note, "install with node <SKILL_ROOT>/install/install.mjs (nothing when node_modules already satisfies the lock)"].filter(Boolean).join("; ");
472
497
  } else if (result.depsAdded?.length) {
473
- result.note = [result.note, "run `npm install --ignore-scripts` to pick up depsAdded"].filter(Boolean).join("; ");
498
+ result.note = [result.note, "run node <SKILL_ROOT>/install/install.mjs to pick up depsAdded"].filter(Boolean).join("; ");
474
499
  }
475
500
  }
476
501
 
@@ -0,0 +1,16 @@
1
+ // .env.local holds the app's WIX_CLIENT_SECRET (written by `wix env pull`, `init` and attach.mjs), so
2
+ // every project setup.mjs and attach.mjs leave behind ignores it in git. A line `.env` alone does not
3
+ // cover `.env.local`; `.env.local`, `.env*`, `.env.*` or `*.local` do.
4
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ const COVERS_ENV_LOCAL = /^\s*(\.env\.local|\.env\*|\.env\.\*|\*\.local)\s*$/m;
8
+
9
+ /** Appends `.env.local` and `.env` to dir/.gitignore unless it already ignores `.env.local`. */
10
+ export function ignoreEnvLocal(dir) {
11
+ const gi = join(dir, ".gitignore");
12
+ const cur = existsSync(gi) ? readFileSync(gi, "utf8") : "";
13
+ if (COVERS_ENV_LOCAL.test(cur)) return false;
14
+ writeFileSync(gi, cur + (cur && !cur.endsWith("\n") ? "\n" : "") + "\n# local env (pulled from Wix)\n.env.local\n.env\n");
15
+ return true;
16
+ }
@@ -0,0 +1,95 @@
1
+ // The project's one dependency install, decided from what is already on disk. Run from the
2
+ // PROJECT ROOT:
3
+ //
4
+ // node <SKILL_ROOT>/install/install.mjs runs it, prints {decision, lock, status}
5
+ // node <SKILL_ROOT>/install/install.mjs --decide prints the decision without installing
6
+ //
7
+ // What the lock asks for is compared with node_modules (each entry's package.json version):
8
+ // none every entry is there at its version — nothing runs. A machine image that pre-warms the
9
+ // solutions' packages, or a second deploy that added nothing, ends here in under a second.
10
+ // install node_modules exists and holds most of the lock — `npm install --ignore-scripts` keeps what
11
+ // is there and writes only the difference (seconds for a few packages).
12
+ // ci no node_modules, or a tree that lacks most of the lock (the blank starter under a
13
+ // solution's lock: measured 67 s for ci against 84 s for install there) —
14
+ // `npm ci --ignore-scripts || npm install --ignore-scripts` (ci needs a lock that agrees
15
+ // with package.json; without one, install).
16
+ // Every path ends with `.install-exit` holding the exit code (0 for none), the marker setup.mjs,
17
+ // attach.mjs and SKILL.md step 3 sync on. Never two installs concurrently in one project: a second
18
+ // npm in the same node_modules redoes the first one's work.
19
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { npmSync } from "./proc.mjs";
22
+
23
+ /** {total, missing, sample}: the lock's node_modules entries the tree lacks or holds at another version. */
24
+ export function lockGaps(projectDir) {
25
+ const lockPath = join(projectDir, "package-lock.json");
26
+ if (!existsSync(lockPath)) return null;
27
+ let lock;
28
+ try { lock = JSON.parse(readFileSync(lockPath, "utf8")); } catch { return null; }
29
+ // Optional entries (a platform's native binaries, an optional peer) are absent on every other platform and
30
+ // npm leaves them out without complaint, so they never count as gaps.
31
+ const entries = Object.entries(lock.packages ?? {}).filter(([path, e]) => path.startsWith("node_modules/") && !e.link && !e.optional && !e.devOptional);
32
+ let missing = 0;
33
+ const sample = [];
34
+ // A package.json dependency the lock's root entry does not declare (deploy's fill-only patch
35
+ // added it after the lock was written) is a gap too: the lock no longer describes the project.
36
+ try {
37
+ const pkg = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8"));
38
+ const root = lock.packages?.[""] ?? {};
39
+ for (const field of ["dependencies", "devDependencies"]) {
40
+ for (const name of Object.keys(pkg[field] ?? {})) {
41
+ if (!(root[field]?.[name] ?? root.dependencies?.[name] ?? root.devDependencies?.[name])) {
42
+ missing++;
43
+ if (sample.length < 5) sample.push(`${name} (in package.json, not in the lock)`);
44
+ }
45
+ }
46
+ }
47
+ } catch { /* no package.json: the lock entries decide */ }
48
+ for (const [path, e] of entries) {
49
+ let version = null;
50
+ try { version = JSON.parse(readFileSync(join(projectDir, path, "package.json"), "utf8")).version; } catch { /* absent */ }
51
+ if (version !== e.version) {
52
+ missing++;
53
+ if (sample.length < 5) sample.push(`${path.replace(/^node_modules\//, "")}@${e.version}${version ? ` (have ${version})` : ""}`);
54
+ }
55
+ }
56
+ return { total: entries.length, missing, sample };
57
+ }
58
+
59
+ export function installDecision(projectDir = process.cwd()) {
60
+ const hasModules = existsSync(join(projectDir, "node_modules"));
61
+ const lock = lockGaps(projectDir);
62
+ if (!hasModules) return { decision: existsSync(join(projectDir, "package-lock.json")) ? "ci" : "install", lock };
63
+ if (!lock || lock.total === 0) return { decision: "install", lock };
64
+ if (lock.missing === 0) return { decision: "none", lock };
65
+ return { decision: lock.missing > lock.total / 2 ? "ci" : "install", lock };
66
+ }
67
+
68
+ /** Runs the decided install; stdio defaults to inherited. Returns {decision, lock, status}. */
69
+ export function runInstall(projectDir = process.cwd(), { stdio = "inherit" } = {}) {
70
+ const { decision, lock } = installDecision(projectDir);
71
+ const flags = ["--ignore-scripts"];
72
+ let status = 0;
73
+ if (decision === "ci") {
74
+ let r = npmSync("npm", ["ci", ...flags], { cwd: projectDir, stdio });
75
+ if (r.status !== 0) r = npmSync("npm", ["install", ...flags], { cwd: projectDir, stdio });
76
+ if (r.error) console.error(`npm could not start: ${r.error.message}`);
77
+ status = r.status ?? 1;
78
+ } else if (decision === "install") {
79
+ const r = npmSync("npm", ["install", ...flags], { cwd: projectDir, stdio });
80
+ if (r.error) console.error(`npm could not start: ${r.error.message}`);
81
+ status = r.status ?? 1;
82
+ }
83
+ writeFileSync(join(projectDir, ".install-exit"), `${status}\n`);
84
+ return { decision, lock, status };
85
+ }
86
+
87
+ if (process.argv[1] && /install\.mjs$/.test(process.argv[1])) {
88
+ if (process.argv.includes("--decide")) {
89
+ console.log(JSON.stringify(installDecision(), null, 2));
90
+ } else {
91
+ const r = runInstall(process.cwd());
92
+ console.log(JSON.stringify(r, null, 2));
93
+ process.exit(r.status);
94
+ }
95
+ }
@@ -3,6 +3,6 @@
3
3
  "@wix/create-new": "0.0.124",
4
4
  "typescript": "5.8.3",
5
5
  "templates": {
6
- "ref": "v1.31.1"
6
+ "ref": "v1.31.2"
7
7
  }
8
8
  }
@@ -0,0 +1,46 @@
1
+ // npm and npx, started the same way on every OS, and the two detached jobs (the dependency install
2
+ // and the seed) without a POSIX shell.
3
+ //
4
+ // On Windows npm and npx are .cmd shims: Node starts a .cmd only through a shell (ENOENT without
5
+ // one, EINVAL for an explicit .cmd since Node 20.12). npm ships their JS entry points beside
6
+ // node.exe, and those start directly with node. A Node without npm beside it (a version manager's
7
+ // shim) gets the .cmd through cmd.exe, as one quoted command line.
8
+ import { spawn, spawnSync } from "node:child_process";
9
+ import { existsSync, openSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const isWin = process.platform === "win32";
14
+ const BACKGROUND = join(dirname(fileURLToPath(import.meta.url)), "background.mjs");
15
+
16
+ const cmdQuote = (a) => (/^[\w@%+=:,./\\-]+$/.test(a) ? a : `"${String(a).replace(/"/g, '""')}"`);
17
+
18
+ /** How to start `npm` or `npx` with `args` here: `{ file, args, shell }` for spawn/spawnSync. */
19
+ export function npmCommand(name, args) {
20
+ if (!isWin) return { file: name, args, shell: false };
21
+ const js = join(dirname(process.execPath), "node_modules", "npm", "bin", `${name}-cli.js`);
22
+ if (existsSync(js)) return { file: process.execPath, args: [js, ...args], shell: false };
23
+ return { file: [`${name}.cmd`, ...args.map(cmdQuote)].join(" "), args: [], shell: true };
24
+ }
25
+
26
+ /** spawnSync for npm / npx; same return shape. */
27
+ export function npmSync(name, args, opts = {}) {
28
+ const c = npmCommand(name, args);
29
+ return spawnSync(c.file, c.args, { ...opts, shell: c.shell, windowsHide: true });
30
+ }
31
+
32
+ /** Why a spawnSync result failed: its output, else the spawn error, else the exit status. */
33
+ export const failure = (r, fallback = "") =>
34
+ String(r.stderr || r.stdout || r.error?.message || fallback || `exit ${r.status ?? r.signal}`);
35
+
36
+ /**
37
+ * Starts install/background.mjs detached: `install` (npm ci, else npm install, output to `log`) or
38
+ * `seed` (node <seed> <plan>: result JSON, log and exit-code marker as files in `cwd`).
39
+ */
40
+ export function startBackground(job, args, { cwd, log }) {
41
+ const fd = log ? openSync(log, "a") : "ignore";
42
+ const child = spawn(process.execPath, [BACKGROUND, job, ...args], {
43
+ cwd, detached: true, windowsHide: true, stdio: ["ignore", fd, fd],
44
+ });
45
+ child.unref();
46
+ }
@@ -30,13 +30,15 @@
30
30
  // to recover one failed step. Emits ONE JSON event per line and exits in ~35s with the two long
31
31
  // steps — the dependency install and the seed — running detached in the background (logs and
32
32
  // completion markers in the final event), so the caller can build the brand layer while they finish.
33
- import { spawn, spawnSync } from "node:child_process";
34
- import { cpSync, existsSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
33
+ import { spawnSync } from "node:child_process";
34
+ import { cpSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
35
35
  import { basename, dirname, join, resolve } from "node:path";
36
36
  import { fileURLToPath } from "node:url";
37
37
  import { AGENT_CONFIG_FILES, writeAgentsMd } from "./agents-md.mjs";
38
+ import { ignoreEnvLocal } from "./env-ignore.mjs";
38
39
  import { listVerticals, templatesDir } from "./templates.mjs";
39
40
  import { folderShape, siteContext } from "./context.mjs";
41
+ import { failure, npmSync, startBackground } from "./proc.mjs";
40
42
  const PINS = JSON.parse(readFileSync(new URL("./pins.json", import.meta.url), "utf8"));
41
43
  const WIX_CLI = `@wix/cli@${PINS["@wix/cli"]}`;
42
44
 
@@ -129,7 +131,7 @@ if (mode === "create") {
129
131
  // a committed copy fetches it here, at the commit the rest came from.
130
132
  const template = stack === "astro" ? join(templatesDir({ need: `${vertical}/project` }), vertical, "project") : null;
131
133
  emit("scaffolding", { folder: folderName, template });
132
- const scaffold = spawnSync(
134
+ const scaffold = npmSync(
133
135
  "npm",
134
136
  // --skip-git: this wrapper composes its own steps and leaves version control to
135
137
  // the caller / the enclosing repo; the scaffold's own `git init` + "Initial
@@ -143,7 +145,7 @@ if (mode === "create") {
143
145
  { env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 300_000 },
144
146
  );
145
147
  if (scaffold.status !== 0 || !existsSync(join(projectDir, "wix.config.json"))) {
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));
148
+ fail("scaffold", failure(scaffold, `scaffold produced no wix.config.json — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).slice(-600));
147
149
  }
148
150
  }
149
151
  } else if (mode === "migrate") {
@@ -164,9 +166,6 @@ if (mode === "create") {
164
166
  scaffolded.name = basename(cwd).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "site";
165
167
  writeFileSync(pkgPath, JSON.stringify(scaffolded, null, 2) + "\n");
166
168
  }
167
- const gi = join(cwd, ".gitignore");
168
- const cur = existsSync(gi) ? readFileSync(gi, "utf8") : "";
169
- if (!/\.env/.test(cur)) writeFileSync(gi, cur + "\n# local env (pulled from Wix)\n.env.local\n.env\n");
170
169
  } else if (publishedStatic) {
171
170
  // ---- 1 · a published static site: the config came with the folder, the site exists -------------
172
171
  // No init: `wix.config.json` already names the site the pages are published on (and its app). The
@@ -179,10 +178,10 @@ if (mode === "create") {
179
178
  // wix.config.json and .env.local, touches nothing else. Non-interactive under CI=1; the site is
180
179
  // named after the folder (rename it in the dashboard).
181
180
  emit("adopting", { folder: cwd });
182
- const init = spawnSync("npm", ["create", `@wix/new@${PINS["@wix/create-new"]}`, "--", "init"],
181
+ const init = npmSync("npm", ["create", `@wix/new@${PINS["@wix/create-new"]}`, "--", "init"],
183
182
  { cwd, env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 300_000 });
184
183
  if (init.status !== 0 || !has("wix.config.json")) {
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
+ fail("init", failure(init, `init produced no wix.config.json — is the Wix CLI logged in? (npx ${WIX_CLI} whoami)`).slice(-600));
186
185
  }
187
186
  }
188
187
  const wixConfig = JSON.parse(readFileSync(join(projectDir, "wix.config.json"), "utf8"));
@@ -208,7 +207,7 @@ const deploy = spawnSync(
208
207
  [join(SKILL_ROOT, "install", "deploy.mjs"), ...verticals, "--stack", stack, ...(stack === "static" ? ["--out", STATIC_OUT] : []), ...(planPath ? ["--plan", resolve(planPath)] : [])],
209
208
  { cwd: projectDir, encoding: "utf8", timeout: 60_000 },
210
209
  );
211
- if (deploy.status !== 0) fail("deploy", deploy.stderr || deploy.stdout);
210
+ if (deploy.status !== 0) fail("deploy", failure(deploy));
212
211
  let deployResult = {};
213
212
  try { deployResult = JSON.parse(deploy.stdout); } catch { /* keep going with raw output below */ }
214
213
  if (deployResult.error) fail("deploy", deployResult.error);
@@ -243,7 +242,10 @@ if (mode === "create" && !subfolder && projectDir !== cwd) {
243
242
  }
244
243
  }
245
244
 
246
- // ---- 2d · the agent config files `wix create` would have written --------------------------------
245
+ // ---- 2d · .env.local ignored, and the agent config files `wix create` would have written ---------
246
+ // Every mode leaves a .env.local with the app secret (env pull, init, the migration download), and a
247
+ // folder's own .gitignore may not cover it.
248
+ ignoreEnvLocal(projectDir);
247
249
  // Skipped by the CLI because of --skip-install. Fill-only: a project that has its own AGENTS.md
248
250
  // keeps it (the event says `kept`).
249
251
  emit("agent_configs", writeAgentsMd(projectDir, { skill: basename(SKILL_ROOT), stack, ...(migrating ? { migration: { parentSiteId: ctx.migration.parentSiteId, deploySiteId: ctx.deploy.siteId } } : {}) }));
@@ -255,14 +257,8 @@ emit("agent_configs", writeAgentsMd(projectDir, { skill: basename(SKILL_ROOT), s
255
257
  let install = null;
256
258
  if (stack !== "static" && existsSync(join(projectDir, "package.json"))) {
257
259
  const installLog = join(projectDir, "npm-install.log");
258
- const logFd = openSync(installLog, "a");
259
- const child = spawn("sh", ["-c", existsSync(join(projectDir, "package-lock.json")) ? "npm ci --ignore-scripts || npm install --ignore-scripts" : "npm install --ignore-scripts"], {
260
- cwd: projectDir,
261
- detached: true,
262
- stdio: ["ignore", logFd, logFd],
263
- });
264
- child.unref();
265
- install = { log: installLog, doneMarker: "node_modules/.package-lock.json" };
260
+ startBackground("install", [], { cwd: projectDir, log: installLog });
261
+ install = { log: installLog, doneMarker: ".install-exit", success: "file contains 0" };
266
262
  emit("install_started", install);
267
263
  }
268
264
 
@@ -286,12 +282,7 @@ if (planPath && madeTheSite) {
286
282
  if (!seedName) fail("seed", `no seed module found under ${seedDir}`);
287
283
  const seedFile = join(seedDir, seedName);
288
284
  const planAbs = resolve(planPath);
289
- const seedChild = spawn(
290
- "sh",
291
- ["-c", `node "${seedFile}" "${planAbs}" > seed-result.json 2> seed.log; echo $? > .seed-exit`],
292
- { cwd: projectDir, detached: true, stdio: "ignore" },
293
- );
294
- seedChild.unref();
285
+ startBackground("seed", [seedFile, planAbs], { cwd: projectDir });
295
286
  seed = { resultFile: "seed-result.json", log: "seed.log", doneMarker: ".seed-exit", success: "file contains 0" };
296
287
  emit("seeding_started", { vertical, ...seed, ...(verticals.length > 1 ? { note: `the plan seeds ${vertical}; the other verticals' seeds run afterwards, each with its own plan` } : {}) });
297
288
  }
@@ -26,7 +26,7 @@
26
26
  // `need`; when a committed copy lacks it, that part is fetched at the same commit.
27
27
  import { cpSync, existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
28
28
  import { tmpdir } from "node:os";
29
- import { dirname, join, resolve } from "node:path";
29
+ import { basename, dirname, join, resolve } from "node:path";
30
30
  import { spawnSync } from "node:child_process";
31
31
  import { fileURLToPath } from "node:url";
32
32
 
@@ -43,7 +43,7 @@ const git = (args, opts = {}) => spawnSync("git", args, { encoding: "utf8", time
43
43
  // skill folder, its entry for this skill. "wix/skills" → the default branch of that repository;
44
44
  // "https://github.com/owner/repo/tree/<ref>" → that ref.
45
45
  export function installSource() {
46
- const skill = SKILL_ROOT.split("/").pop();
46
+ const skill = basename(SKILL_ROOT);
47
47
  let dir = SKILL_ROOT;
48
48
  for (let i = 0; i < 6; i++) {
49
49
  const p = join(dir, "skills-lock.json");
@@ -1,12 +1,13 @@
1
1
  // The post-body renderer — wire AS-IS (machinery, not a reference). A post body is a Ricos
2
2
  // document, not HTML and not text: never innerHTML it, never String() its nodes. This is the
3
- // documented render path (@wix/ricos viewer + quick-start plugins).
3
+ // documented render path (@wix/ricos/viewer + quick-start plugins: the viewer-only entry, 11.17+,
4
+ // without the editor, its dev tools and CodeMirror that the root import drags into every bundle).
4
5
  //
5
6
  // In Astro this component MUST be a `client:only="react"` island — the ricos viewer breaks
6
7
  // under SSR. Dark theme: the ricos CSS hardcodes near-black text; scope an override on
7
8
  // `.ricos-content` forcing the foreground token (in Astro use <style is:global> — React
8
9
  // islands don't inherit scoped Astro styles).
9
- import { quickStartViewerPlugins, RicosViewer } from "@wix/ricos";
10
+ import { quickStartViewerPlugins, RicosViewer } from "@wix/ricos/viewer";
10
11
  import "@wix/ricos/css/all-plugins-viewer.css";
11
12
 
12
13
  // Module-level, once — never rebuild the plugin list per render.