@wix/agent-skills 1.20.1 → 1.21.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 (49) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +14 -112
  3. package/skills/wix-app/references/APP_TOOLS.md +2 -1
  4. package/skills/wix-app/references/BACKEND_EVENT.md +3 -3
  5. package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +41 -152
  6. package/skills/wix-app/references/DASHBOARD_MODAL.md +4 -4
  7. package/skills/wix-app/references/SDK_MODULE_MAP.md +44 -0
  8. package/skills/wix-app/references/SITE_PLUGIN.md +36 -190
  9. package/skills/wix-app/references/app-tools/TOOLS.md +3 -1
  10. package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +21 -0
  11. package/skills/wix-app/references/dashboard-page/ECOM_NAVIGATION.md +17 -0
  12. package/skills/wix-app/references/dashboard-plugin/SLOTS.md +4 -18
  13. package/skills/wix-app/references/site-plugin/EXAMPLES.md +1 -1
  14. package/skills/wix-headless-fast/SKILL.md +134 -49
  15. package/skills/wix-headless-fast/entry/skill.md +4 -3
  16. package/skills/wix-headless-fast/install/AGENTS.md +32 -0
  17. package/skills/wix-headless-fast/install/agents-md.mjs +39 -0
  18. package/skills/wix-headless-fast/install/attach.mjs +149 -42
  19. package/skills/wix-headless-fast/install/setup.mjs +239 -0
  20. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +2 -0
  21. package/skills/wix-headless-fast/references/blog/seed/read-site.mjs +22 -0
  22. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +2 -0
  23. package/skills/wix-headless-fast/references/bookings/seed/read-site.mjs +28 -0
  24. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +2 -0
  25. package/skills/wix-headless-fast/references/cms/seed/read-site.mjs +20 -0
  26. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +2 -0
  27. package/skills/wix-headless-fast/references/events/seed/read-site.mjs +26 -0
  28. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +2 -0
  29. package/skills/wix-headless-fast/references/forms/seed/read-site.mjs +20 -0
  30. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +2 -0
  31. package/skills/wix-headless-fast/references/members/seed/read-site.mjs +15 -0
  32. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +2 -0
  33. package/skills/wix-headless-fast/references/portfolio/seed/read-site.mjs +20 -0
  34. package/skills/wix-headless-fast/references/portfolio/seed/seed-portfolio.mjs +19 -21
  35. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +2 -0
  36. package/skills/wix-headless-fast/references/pricing-plans/seed/read-site.mjs +21 -0
  37. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +2 -0
  38. package/skills/wix-headless-fast/references/restaurants/seed/read-site.mjs +25 -0
  39. package/skills/wix-headless-fast/references/restaurants/seed/seed-restaurants.mjs +25 -5
  40. package/skills/wix-headless-fast/references/shared/seed/read-site.mjs +99 -0
  41. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +4 -0
  42. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +7 -2
  43. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +2 -0
  44. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +7 -1
  45. package/skills/wix-headless-fast/references/storefront/seed/read-site.mjs +38 -0
  46. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +52 -16
  47. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-create-coupon.md +10 -19
  48. package/skills/wix-manage/references/events/create-wix-event.md +5 -1
  49. package/skills/wix-headless-fast/install/fast-path.mjs +0 -181
@@ -1,21 +1,41 @@
1
- // Attach a NEW headless frontend to an EXISTING Wix site — the fast path for a site that already
1
+ // Attach a NEW headless frontend to an EXISTING Wix site — setup's sibling for a site that already
2
2
  // exists (named in the prompt by its id). `init`/`wix create` cannot do this: they always create
3
3
  // a site. This script does what they do AFTER creating one — OAuth app, managed hosting, env
4
4
  // vars, `wix.config.json` — against the site id given, then scaffolds and deploys like
5
- // fast-path.mjs. Nothing on the site is created, changed or deleted; there is no seed step.
5
+ // setup.mjs. Nothing on the site is created, changed or deleted; there is no seed step.
6
6
  //
7
- // node <SKILL_ROOT>/install/attach.mjs --site <metaSiteId> --business-name "<Brand>" \
8
- // --vertical <a>[,<b>…] [--stack astro|react|static] [--subfolder [--folder-name <name>]]
7
+ // node <SKILL_ROOT>/install/attach.mjs [--site <metaSiteId>] --business-name "<Brand>" \
8
+ // --vertical <a>[,<b>…] [--stack astro|react|lib|static] [--hosting wix|self [--origin <url>[,<url>]]] \
9
+ // [--subfolder [--folder-name <name>]]
9
10
  //
11
+ // --hosting wix (default): the frontend releases to Wix hosting — the site's app project is
12
+ // created or reused and `wix release` uploads to its *.wix-site-host.com address.
13
+ // self: the frontend is hosted elsewhere (Vercel, a Flask server, …): no app
14
+ // project, no Wix address, no Astro template; the OAuth app, wix.config.json and
15
+ // .env.local are still set up, the code deploys for --stack, and the origins given
16
+ // in --origin go on the OAuth app's redirect allow-list, which is what a Wix-hosted
17
+ // flow (checkout) needs to return to a frontend on another host.
18
+ //
19
+ // --site the site. May be omitted when the folder holds a wix.config.json: then it is
20
+ // the site in that config (what `init` left behind in an empty folder).
10
21
  // --business-name the site's name (names the folder, the hosting slug and the app project).
11
22
  // --vertical which shipped code deploys (no seed runs — the site owns its content). Both are
12
23
  // the caller's decision, read off the site before calling this (SKILL.md step 3).
13
- // --stack astro (default): scaffolds the CLI's blank Astro template with the hosting adapter,
14
- // ready for `wix build` / `wix release`. react|static: writes wix.config.json into the
15
- // folder and stops — the caller scaffolds (Vite / plain HTML) per SKILL.md.
16
- // The project is created in the CURRENT DIRECTORY — the folder that already holds the installed
17
- // skills — so it is self-contained. `--subfolder` (opt-in) creates it in a new folder named after
18
- // the business instead.
24
+ // --stack astro (default) in a folder without a project: scaffolds the CLI's blank Astro
25
+ // template with the hosting adapter. react|static there: writes wix.config.json
26
+ // and stops — the caller scaffolds (Vite / plain HTML) per SKILL.md. In a folder
27
+ // that holds a project, --stack is required and nothing is scaffolded.
28
+ //
29
+ // The FOLDER decides, on file markers only:
30
+ // - a project (package.json or index.html) AND wix.config.json → refuses: a Wix project with a
31
+ // frontend already; deploy.mjs adds a solution to it.
32
+ // - a project, no config → LINK: the hosting calls, then wix.config.json and .env.local are
33
+ // written into the project as it is, the shipped code deploys for --stack, the install starts.
34
+ // - no project (empty, or only a config for this same site) → the scaffold path above.
35
+ // - a config naming a different site → refuses: never re-point a folder; use --subfolder.
36
+ // The project lives in the CURRENT DIRECTORY — the folder that already holds the installed skills —
37
+ // so it is self-contained. `--subfolder` (opt-in) creates it in a new folder named after the
38
+ // business instead.
19
39
  //
20
40
  // Emits ONE JSON event per line (attached, scaffolded, deployed, install_started,
21
41
  // ready_for_brand_layer, or error). Requires a logged-in Wix CLI (`npx @wix/cli@latest whoami`)
@@ -23,11 +43,13 @@
23
43
  import { spawn, spawnSync, execFileSync } from "node:child_process";
24
44
  import { cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
25
45
  import { tmpdir } from "node:os";
26
- import { dirname, join, resolve } from "node:path";
46
+ import { basename, dirname, join, resolve } from "node:path";
27
47
  import { fileURLToPath } from "node:url";
48
+ import { writeAgentsMd } from "./agents-md.mjs";
28
49
 
29
50
  const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
30
51
  const MANAGE = "https://manage.wix.com";
52
+ const API = "https://www.wixapis.com";
31
53
  const HEADLESS_PROJECT_TYPE_ID = "eb363dea-85a0-4159-9b05-949542be5079";
32
54
  const TEMPLATES_REPO = "https://github.com/wix/headless-templates.git";
33
55
  const TEMPLATE_PATH = "astro/blank";
@@ -44,9 +66,17 @@ const flag = (name) => {
44
66
  const i = argv.indexOf(`--${name}`);
45
67
  return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[i + 1] : null;
46
68
  };
47
- const siteId = flag("site");
48
- const stack = flag("stack") ?? "astro";
69
+ const stackFlag = flag("stack");
70
+ const hosting0 = flag("hosting") ?? "wix";
71
+ const origins = (flag("origin") ?? "").split(",").map((o) => o.trim().replace(/\/$/, "")).filter(Boolean);
49
72
  const subfolder = argv.includes("--subfolder");
73
+ // ---- the folder ---------------------------------------------------------------------------------
74
+ const cwd = process.cwd();
75
+ const has = (p) => existsSync(join(cwd, p));
76
+ const cwdConfig = has("wix.config.json") ? JSON.parse(readFileSync(join(cwd, "wix.config.json"), "utf8")) : null;
77
+ const hasProject = has("package.json") || has("index.html");
78
+ const siteId = flag("site") ?? cwdConfig?.siteId ?? cwdConfig?.projectId ?? null;
79
+ const stack = stackFlag ?? "astro";
50
80
  const knownVerticals = readdirSync(join(SKILL_ROOT, "references"), { withFileTypes: true })
51
81
  .filter((d) => d.isDirectory() && d.name !== "shared" && existsSync(join(SKILL_ROOT, "references", d.name, "app")))
52
82
  .map((d) => d.name);
@@ -56,13 +86,28 @@ const verticals = argv
56
86
  .filter(Boolean);
57
87
  const businessName = flag("business-name");
58
88
  if (!siteId || !/^[0-9a-f-]{36}$/i.test(siteId) || !businessName || !verticals.length) {
59
- fail("args", `usage: attach.mjs --site <metaSiteId> --business-name "<Brand>" --vertical <${knownVerticals.join("|")}>[,…] [--stack astro|react|static]`);
89
+ fail("args", `usage: attach.mjs [--site <metaSiteId>] --business-name "<Brand>" --vertical <${knownVerticals.join("|")}>[,…] [--stack astro|react|lib|static] (--site may be omitted when wix.config.json is here)`);
60
90
  }
61
91
  for (const v of verticals) if (!knownVerticals.includes(v)) fail("args", `unknown vertical "${v}" — shipped verticals: ${knownVerticals.join(", ")}`);
62
- if (!["astro", "react", "static"].includes(stack)) fail("args", `unknown stack "${stack}" — astro, react or static`);
63
- if (existsSync(join(process.cwd(), "wix.config.json"))) {
64
- fail("args", "the current directory is already a Wix project (wix.config.json) — attach creates a new frontend; run deploy.mjs here instead");
92
+ if (!["astro", "react", "lib", "static"].includes(stack)) fail("args", `unknown stack "${stack}" — astro, react, lib or static`);
93
+ if (!["wix", "self"].includes(hosting0)) fail("args", `unknown --hosting "${hosting0}" — wix or self`);
94
+ for (const o of origins) if (!/^https?:\/\/[^/\s]+$/.test(o)) fail("args", `--origin must be a scheme and host such as https://shop.example.com or http://localhost:8000, got "${o}"`);
95
+ if (hosting0 === "self" && stack === "astro" && !hasProject && !stackFlag) {
96
+ fail("args", "--hosting self scaffolds nothing: pass --stack for the frontend you will host (react|lib|static), or attach from inside the project on disk");
97
+ }
98
+ if (!subfolder) {
99
+ if (hasProject && cwdConfig) {
100
+ fail("place", "this folder is already a Wix project with a frontend (wix.config.json and a project): nothing to attach. deploy.mjs <vertical> adds this skill's code or a solution to it; then ONE npm install");
101
+ }
102
+ if (cwdConfig && (cwdConfig.siteId ?? cwdConfig.projectId) !== siteId) {
103
+ fail("place", `this folder is attached to site ${cwdConfig.siteId ?? cwdConfig.projectId}, not ${siteId} — a folder is never re-pointed; attach the other site with --subfolder`);
104
+ }
105
+ if (hasProject && !stackFlag) {
106
+ fail("args", "attaching a project on disk needs --stack astro|react|lib|static — the stack you resolved in SKILL.md step 1 for this project");
107
+ }
65
108
  }
109
+ const mode = !subfolder && hasProject ? "link" : hosting0 === "self" ? "config-only" : "scaffold";
110
+ emit("folder", { mode, stack, hosting: hosting0, project: hasProject, config: cwdConfig ? "same site" : null });
66
111
 
67
112
  // ---- http ---------------------------------------------------------------------------------------
68
113
  const cliToken = (site) => {
@@ -92,25 +137,27 @@ async function call(base, path, { method = "POST", token, site, body, query } =
92
137
  }
93
138
 
94
139
  const folderName = flag("folder-name") ?? (businessName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "site");
95
- const projectDir = subfolder ? resolve(process.cwd(), folderName) : process.cwd();
140
+ const projectDir = subfolder ? resolve(cwd, folderName) : cwd;
96
141
  if (subfolder && existsSync(join(projectDir, "wix.config.json"))) fail("args", `${folderName}/ is already a Wix project — pick --folder-name, or run deploy.mjs there`);
97
142
 
98
143
  // ---- 1 · attach: OAuth app + hosting + env, on the manage host with a site token -----------------
99
144
  // The same calls `init` makes after it has created a site, in the same order.
100
145
  const siteToken = cliToken(siteId);
101
146
  const opts = { token: siteToken, site: siteId };
102
- let appId, appProject, instanceId, secrets, hosting = "created";
147
+ let appId, appProject = null, instanceId, secrets, hosting = hosting0 === "self" ? "self" : "created";
103
148
  try {
104
149
  const comp = await call(MANAGE, "/_api/companion-apps/v1/companion-apps/get-or-create", { ...opts, body: {} });
105
150
  appId = comp.companionApp?.id;
106
151
  if (!appId) throw new Error("get-or-create returned no companion app id");
107
152
  const slug = folderName;
108
- try { appProject = (await call(MANAGE, `/_api/wix-code-app-projects/v1/app-projects/${appId}`, { ...opts, method: "GET" })).appProject; } catch { appProject = null; }
109
- if (appProject) {
110
- hosting = "reused";
111
- } else {
112
- try { await call(MANAGE, `/apps-service/v1/apps/${appId}/set-namespace`, { ...opts, method: "PATCH", body: { appId, appName: businessName, namespace: slug } }); }
113
- catch (e) { emit("note", { step: "set-namespace", detail: String(e.message).slice(0, 200) }); }
153
+ if (hosting0 === "wix") {
154
+ try { appProject = (await call(MANAGE, `/_api/wix-code-app-projects/v1/app-projects/${appId}`, { ...opts, method: "GET" })).appProject; } catch { appProject = null; }
155
+ if (appProject) {
156
+ hosting = "reused";
157
+ } else {
158
+ try { await call(MANAGE, `/apps-service/v1/apps/${appId}/set-namespace`, { ...opts, method: "PATCH", body: { appId, appName: businessName, namespace: slug } }); }
159
+ catch (e) { emit("note", { step: "set-namespace", detail: String(e.message).slice(0, 200) }); }
160
+ }
114
161
  }
115
162
  try {
116
163
  const inst = await call(MANAGE, "/apps-installer-service/v1/app-instance/install", { ...opts, body: { tenant: { id: siteId, tenantType: "SITE" }, appInstance: { appDefId: appId, version: "latest" } } });
@@ -118,9 +165,23 @@ try {
118
165
  } catch (e) { emit("note", { step: "install", detail: String(e.message).slice(0, 200) }); }
119
166
  secrets = (await call(MANAGE, `/apps-service/v1/apps/${appId}`, { ...opts, method: "GET", query: { withSecrets: true } })).app?.appSecrets;
120
167
  if (!secrets?.appSecret) throw new Error("app secrets unavailable for this app");
121
- if (!appProject) {
168
+ if (hosting0 === "self") {
169
+ // No Wix hosting. The one thing a frontend on another host needs from the site: its origins on
170
+ // the OAuth app's redirect allow-list, so checkout and the other Wix-hosted flows can return.
171
+ // The public OAuth Apps API (wix-manage: Manage OAuth Apps), merged with what is already there.
172
+ if (origins.length) {
173
+ const current = (await call(API, `/oauth-app/v1/oauth-apps/${appId}`, { ...opts, method: "GET" })).oAuthApp ?? {};
174
+ const merged = [...new Set([...(current.allowedRedirectDomains ?? []), ...origins])];
175
+ await call(API, `/oauth-app/v1/oauth-apps/${appId}`, { ...opts, method: "PATCH", body: { oAuthApp: { allowedRedirectDomains: merged }, mask: { paths: ["allowedRedirectDomains"] } } });
176
+ emit("origins_allowed", { appId, allowedRedirectDomains: merged });
177
+ } else {
178
+ emit("note", { step: "origins", detail: "no --origin given: add the frontend's origins to the OAuth app's allowedRedirectDomains (wix-manage: Manage OAuth Apps) before the first checkout test" });
179
+ }
180
+ }
181
+ if (hosting0 === "wix" && !appProject) {
122
182
  appProject = (await call(MANAGE, "/_api/wix-code-app-projects/v1/app-projects", { ...opts, body: { appProject: { id: appId, displayName: businessName.slice(0, 50), slug, appProjectTypeId: HEADLESS_PROJECT_TYPE_ID } } })).appProject;
123
183
  }
184
+ if (hosting0 === "wix") {
124
185
  if (!appProject?.baseUrl) throw new Error("app project has no baseUrl");
125
186
  const prod = String(appProject.baseUrl).replace(/\/$/, "");
126
187
  const host = new URL(appProject.baseUrl).hostname;
@@ -146,15 +207,45 @@ try {
146
207
  await call(MANAGE, `/_api/wix-code-app-environments/v2/bulk/app-projects/${appProject.id}/app-environment-variables/upsert`, { ...opts, body: {
147
208
  appProjectId: appProject.id, environment: "system_global", mutability: "STATIC", returnEntity: true, returnAllEnvironment: true, variables,
148
209
  } });
210
+ }
149
211
  } catch (e) {
150
212
  fail("attach", e?.message || e);
151
213
  }
152
- const baseUrl = String(appProject.baseUrl).replace(/\/$/, "");
153
- emit("attached", { siteId, appId, baseUrl, hosting, note: hosting === "reused" ? "this site already has a headless frontend at baseUrl; `wix release` from this project replaces it" : undefined });
214
+ const baseUrl = appProject ? String(appProject.baseUrl).replace(/\/$/, "") : null;
215
+ // Is a frontend serving at that address? The hosting project says nothing about it (init and the
216
+ // provisioning call create one with nothing behind it). The site's release slots do: the release
217
+ // with slug `prod` is production traffic and names the deployment it serves — the same read the
218
+ // headless back office makes for its frontend status. Absent means nothing was ever released.
219
+ // Deployments keep their own addresses, so a replaced one is not lost; prod can be pointed back.
220
+ // A folder that reaches this point is not the one that released (a linked project never runs
221
+ // attach), so a `prod` release here means a NEW project is about to take over a live frontend.
222
+ let frontend = null;
223
+ if (appProject) {
224
+ try {
225
+ const r = await call(MANAGE, `/_api/wix-code-app-releases/v1/app-projects/${appProject.id}/app-releases/query`, { ...opts, body: { query: { cursorPaging: { limit: 20 } } } });
226
+ const prod = (r.appReleases ?? []).find((x) => x.releaseSlug === "prod");
227
+ frontend = prod
228
+ ? { serving: true, releasedAt: prod.updatedDate ?? prod.createdDate ?? null, deploymentId: prod.appDeploymentId ?? null }
229
+ : { serving: false };
230
+ } catch (e) {
231
+ emit("note", { step: "releases", detail: String(e.message).slice(0, 200) });
232
+ frontend = { serving: null };
233
+ }
234
+ }
235
+ const note =
236
+ hosting === "self" ? "no Wix hosting: the frontend is yours to host; its origins must be on the OAuth app's allow-list"
237
+ : frontend?.serving === true
238
+ ? `a frontend is SERVING at ${baseUrl}, released ${frontend.releasedAt}. This is a new project: a \`wix release\` from here replaces it (the current deployment keeps its own address and prod can be pointed back). Tell the user before releasing; unless the brief asked for a new frontend, ask.`
239
+ : frontend?.serving === false
240
+ ? `${hosting === "created" ? "hosting created" : "hosting exists"} at ${baseUrl}; nothing served yet — the first release from this project fills it`
241
+ : frontend
242
+ ? `could not read whether a frontend is serving at ${baseUrl}; check the address before releasing`
243
+ : undefined;
244
+ emit("attached", { siteId, appId, baseUrl, hosting, frontend, origins: hosting0 === "self" ? origins : undefined, note });
154
245
 
155
- // ---- 2 · scaffold -------------------------------------------------------------------------------
246
+ // ---- 2 · scaffold (only where there is no project) -------------------------------------------------
156
247
  mkdirSync(projectDir, { recursive: true });
157
- if (stack === "astro") {
248
+ if (stack === "astro" && mode === "scaffold") {
158
249
  // The CLI's own blank template (what `wix create` copies), then what its extender adds: the
159
250
  // hosting adapter, React islands, the `wix` scripts. Pinned like a freshly created project.
160
251
  const tmp = mkdtempSync(join(tmpdir(), "wix-template-"));
@@ -211,11 +302,14 @@ writeFileSync(join(projectDir, ".env.local"), [
211
302
  `WIX_CLIENT_SECRET=${quote(secrets.appSecret)}`,
212
303
  "",
213
304
  ].join("\n"));
214
- emit("scaffolded", { folder: folderName, stack, template: stack === "astro" ? `${TEMPLATES_REPO}#${TEMPLATE_PATH}` : null });
305
+ emit(mode === "link" ? "linked" : mode === "config-only" ? "configured" : "scaffolded", { folder: folderName, stack, template: mode === "scaffold" && stack === "astro" ? `${TEMPLATES_REPO}#${TEMPLATE_PATH}` : null });
306
+ // the agent config files `wix create` writes (attach never runs the CLI's scaffold at all); fill-only
307
+ emit("agent_configs", writeAgentsMd(projectDir, { skill: basename(SKILL_ROOT), stack }));
215
308
 
216
- if (stack !== "astro") {
217
- emit("ready", { projectDir, siteId, appId, baseUrl, hosting, stack, dashboardUrl: `https://manage.wix.com/dashboard/${siteId}`,
218
- next: `scaffold the ${stack} project in this folder per SKILL.md, then deploy.mjs <vertical…> --stack ${stack} (the client id is read from wix.config.json); no seed — the site owns its content` });
309
+ if (mode !== "link" && (stack !== "astro" || mode === "config-only")) {
310
+ emit("ready", { projectDir, siteId, appId, baseUrl, hosting, frontend, stack, dashboardUrl: `https://manage.wix.com/dashboard/${siteId}`,
311
+ next: `scaffold the ${stack} project in this folder per SKILL.md, then deploy.mjs <vertical…> --stack ${stack} (the client id is read from wix.config.json); no seed — the site owns its content` +
312
+ (hosting === "self" ? `; you host it: origins on the OAuth app allow-list now: ${origins.join(", ") || "none — add them before the first checkout test"}` : "") });
219
313
  process.exit(0);
220
314
  }
221
315
 
@@ -229,12 +323,16 @@ let deployResult = {};
229
323
  emit("deployed", deployResult);
230
324
  }
231
325
 
232
- // ---- 4 · dependency install, detached -----------------------------------------------------------
233
- const installLog = join(projectDir, "npm-install.log");
234
- const logFd = openSync(installLog, "a");
235
- const install = spawn("sh", ["-c", "npm ci --ignore-scripts || npm install --ignore-scripts"], { cwd: projectDir, detached: true, stdio: ["ignore", logFd, logFd] });
236
- install.unref();
237
- emit("install_started", { log: installLog, doneMarker: "node_modules/.package-lock.json" });
326
+ // ---- 4 · dependency install, detached (any project with a package.json) --------------------------
327
+ let install = null;
328
+ if (existsSync(join(projectDir, "package.json"))) {
329
+ const installLog = join(projectDir, "npm-install.log");
330
+ const logFd = openSync(installLog, "a");
331
+ const child = spawn("sh", ["-c", existsSync(join(projectDir, "package-lock.json")) ? "npm ci --ignore-scripts || npm install --ignore-scripts" : "npm install --ignore-scripts"], { cwd: projectDir, detached: true, stdio: ["ignore", logFd, logFd] });
332
+ child.unref();
333
+ install = { log: installLog, doneMarker: "node_modules/.package-lock.json" };
334
+ emit("install_started", install);
335
+ }
238
336
 
239
337
  // ---- done ----------------------------------------------------------------------------------------
240
338
  emit("ready_for_brand_layer", {
@@ -243,11 +341,20 @@ emit("ready_for_brand_layer", {
243
341
  appId,
244
342
  baseUrl,
245
343
  hosting,
344
+ frontend,
246
345
  verticals,
247
346
  dashboardUrl: `https://manage.wix.com/dashboard/${siteId}`,
248
347
  productsUrl: deployResult.productsUrl,
249
348
  categoriesUrl: deployResult.categoriesUrl,
250
- install: { log: installLog, doneMarker: "node_modules/.package-lock.json" },
349
+ mode,
350
+ stack,
351
+ install,
251
352
  seed: null,
252
- next: "the site's content is live already — nothing to seed; read what it holds through the deployed data layer, theme SiteLayout + write the pages; wait for the install marker, build, release",
353
+ next:
354
+ "the site's content is live already — nothing to seed; get the measure of the site (SKILL.md step 3), theme + write the pages" +
355
+ (mode === "link" && hosting !== "self" ? "; make the project what its stack needs on Wix hosting (SKILL.md step 1)" : "") +
356
+ (install ? "; wait for the install marker" : "") +
357
+ (hosting === "self"
358
+ ? `; you host it — no wix release; origins on the OAuth app allow-list now: ${origins.join(", ") || "none — add them before the first checkout test"}; add the public one when it goes live`
359
+ : "; then release as step 5 says for the stack"),
253
360
  });
@@ -0,0 +1,239 @@
1
+ // Set up the project in the current folder — one deterministic call from "the folder and the
2
+ // brief" to "brand layer can start", for a folder that is NOT yet a Wix project:
3
+ //
4
+ // node <SKILL_ROOT>/install/setup.mjs --vertical <storefront|bookings|…> \
5
+ // [--plan plan.json] [--business-name "<Brand>"] [--stack astro|react|lib|static] \
6
+ // [--subfolder [--folder-name <npm-safe-name>]]
7
+ //
8
+ // - `wix.config.json` present → refuses. The folder is already a Wix project; deploy.mjs adds
9
+ // this skill's code or a solution to it, then one install, then the seed if there is content.
10
+ // - a project but no `wix.config.json` (a package.json, or an index.html at the root) → CONNECT:
11
+ // `npm create @wix/new@latest init` in place creates the site and the config, then deploy.
12
+ // - otherwise (empty, or loose files such as a CSV or a brief) → CREATE: scaffold the Wix CLI's
13
+ // Astro template and place it in the current directory (`--business-name` required).
14
+ //
15
+ // The script reads file markers only (wix.config.json, a package.json or index.html); everything
16
+ // else is the agent's call: when connecting, `--stack` is required and comes from SKILL.md step 1,
17
+ // and what the project needs for that stack on Wix hosting is prose there, not detection here. The
18
+ // seed runs only when `--plan` is given: a brief that has not supplied any content gets no seed.
19
+ //
20
+ // Composes pieces that also remain individually runnable (deploy.mjs, the vertical's seed module)
21
+ // to recover one failed step. Emits ONE JSON event per line and exits in ~35s with the two long
22
+ // steps — the dependency install and the seed — running detached in the background (logs and
23
+ // completion markers in the final event), so the caller can build the brand layer while they finish.
24
+ import { spawn, spawnSync } from "node:child_process";
25
+ import { existsSync, openSync, readFileSync, readdirSync, renameSync, rmSync } from "node:fs";
26
+ import { basename, dirname, join, resolve } from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+ import { AGENT_CONFIG_FILES, writeAgentsMd } from "./agents-md.mjs";
29
+
30
+ const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
+
32
+ const emit = (event, extra = {}) => console.log(JSON.stringify({ event, ...extra }));
33
+ const fail = (step, detail) => {
34
+ emit("error", { step, detail: String(detail).slice(0, 600) });
35
+ process.exit(1);
36
+ };
37
+
38
+ // ---- args ---------------------------------------------------------------------------------------
39
+ const argv = process.argv.slice(2);
40
+ const flag = (name) => {
41
+ const i = argv.indexOf(`--${name}`);
42
+ return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[i + 1] : null;
43
+ };
44
+ const businessName = flag("business-name");
45
+ const planPath = flag("plan");
46
+ const vertical = flag("vertical");
47
+ const stackFlag = flag("stack");
48
+ // Opt-in: keep a CREATE's project in a subfolder instead of the current directory.
49
+ const subfolder = argv.includes("--subfolder");
50
+ const knownVerticals = readdirSync(join(SKILL_ROOT, "references"), { withFileTypes: true })
51
+ .filter((d) => d.isDirectory() && d.name !== "shared" && existsSync(join(SKILL_ROOT, "references", d.name, "app")))
52
+ .map((d) => d.name);
53
+ const usage = `usage: setup.mjs --vertical <${knownVerticals.join("|")}> [--plan plan.json] [--business-name "<Brand>"] [--stack astro|react|lib|static] [--subfolder]`;
54
+ // --vertical is REQUIRED: a defaulted vertical deploys the wrong code and runs the wrong seed.
55
+ if (!vertical) fail("args", usage);
56
+ if (!knownVerticals.includes(vertical)) {
57
+ fail("args", `unknown vertical "${vertical}" — shipped verticals: ${knownVerticals.join(", ")}`);
58
+ }
59
+ if (stackFlag && !["astro", "react", "lib", "static"].includes(stackFlag)) {
60
+ fail("args", `unknown --stack "${stackFlag}" — astro, react, lib or static`);
61
+ }
62
+ if (planPath && !existsSync(planPath)) fail("args", `plan file not found: ${planPath}`);
63
+
64
+ // ---- 0 · read the folder --------------------------------------------------------------------------
65
+ const cwd = process.cwd();
66
+ const has = (p) => existsSync(join(cwd, p));
67
+ if (has("wix.config.json")) {
68
+ 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");
69
+ }
70
+ const pkg = has("package.json") ? JSON.parse(readFileSync(join(cwd, "package.json"), "utf8")) : null;
71
+ const hasProject = !!pkg || has("index.html");
72
+ const mode = hasProject ? "connect" : "create";
73
+ if (mode === "connect" && !stackFlag) {
74
+ 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");
75
+ }
76
+ const stack = stackFlag ?? "astro";
77
+ emit("folder", { mode, stack, project: hasProject ? (pkg?.name ?? basename(cwd)) : null });
78
+
79
+ let projectDir = cwd;
80
+ let folderName = null;
81
+
82
+ if (mode === "create") {
83
+ // ---- 1 · scaffold -----------------------------------------------------------------------------
84
+ if (!businessName) fail("args", `an empty folder is a CREATE run and needs --business-name. ${usage}`);
85
+ folderName =
86
+ flag("folder-name") ??
87
+ businessName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
88
+ // `let`: the Wix CLI can only scaffold into a subfolder; by default we then move the scaffold up
89
+ // into the current directory and repoint projectDir there, so the detached install/seed and every
90
+ // reported path use the final location.
91
+ projectDir = resolve(cwd, folderName);
92
+ if (existsSync(join(projectDir, "wix.config.json"))) {
93
+ emit("scaffold_skipped", { reason: "project already exists", folder: folderName });
94
+ } else {
95
+ emit("scaffolding", { folder: folderName });
96
+ const scaffold = spawnSync(
97
+ "npm",
98
+ // --skip-git: this wrapper composes its own steps and leaves version control to
99
+ // the caller / the enclosing repo; the scaffold's own `git init` + "Initial
100
+ // commit" is noise here, and becomes a nested-repo (submodule gitlink) hazard
101
+ // if the project is later placed inside an existing repo. --skip-install for the
102
+ // same reason: deps install in a detached step below.
103
+ ["create", "@wix/new@latest", "--", "headless",
104
+ "--folder-name", folderName, "--business-name", businessName,
105
+ "--site-template", "--skip-install", "--skip-git", "--no-publish"],
106
+ { env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 300_000 },
107
+ );
108
+ if (scaffold.status !== 0 || !existsSync(join(projectDir, "wix.config.json"))) {
109
+ 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));
110
+ }
111
+ }
112
+ } else {
113
+ // ---- 1 · init in place --------------------------------------------------------------------------
114
+ // The CLI's `init` links the folder to a NEW site: creates the site and its OAuth app, writes
115
+ // wix.config.json and .env.local, touches nothing else. Non-interactive under CI=1; the site is
116
+ // named after the folder (rename it in the dashboard).
117
+ emit("initializing", { folder: cwd });
118
+ const init = spawnSync("npm", ["create", "@wix/new@latest", "--", "init"],
119
+ { cwd, env: { ...process.env, CI: "1" }, encoding: "utf8", timeout: 300_000 });
120
+ if (init.status !== 0 || !has("wix.config.json")) {
121
+ fail("init", (init.stderr || init.stdout || "init produced no wix.config.json — is the Wix CLI logged in? (npx @wix/cli@latest whoami)").slice(-600));
122
+ }
123
+ }
124
+ const wixConfig = JSON.parse(readFileSync(join(projectDir, "wix.config.json"), "utf8"));
125
+ const siteId = wixConfig.siteId ?? wixConfig.projectId;
126
+ emit(mode === "create" ? "scaffolded" : "connected", { folder: folderName ?? cwd, siteId, stack });
127
+
128
+ // ---- 2 · deploy shipped code + deps + lockfile ---------------------------------------------------
129
+ const deploy = spawnSync(
130
+ "node",
131
+ [join(SKILL_ROOT, "install", "deploy.mjs"), vertical, "--stack", stack, ...(planPath ? ["--plan", resolve(planPath)] : [])],
132
+ { cwd: projectDir, encoding: "utf8", timeout: 60_000 },
133
+ );
134
+ if (deploy.status !== 0) fail("deploy", deploy.stderr || deploy.stdout);
135
+ let deployResult = {};
136
+ try { deployResult = JSON.parse(deploy.stdout); } catch { /* keep going with raw output below */ }
137
+ if (deployResult.error) fail("deploy", deployResult.error);
138
+ emit("deployed", deployResult);
139
+
140
+ // ---- 2c · place a CREATE's project in the current directory -------------------------------------
141
+ // The Wix CLI scaffolds into a subfolder, so the scaffold is moved up into the current directory —
142
+ // the folder that already holds the installed skills — and the subfolder removed. Done HERE, before
143
+ // the detached install below, on purpose: no node_modules exists yet, so the move is instant and
144
+ // cannot collide with a running install. A pure move: the scaffold was created with --skip-git, so
145
+ // there is no nested repo to reconcile — git is whatever the folder already is. Refuses rather than
146
+ // overwrite: an entry that already exists in the current directory stops the move before anything
147
+ // is touched. `--subfolder` skips this step.
148
+ if (mode === "create" && !subfolder && projectDir !== cwd) {
149
+ // The CLI's own agent config files, when its generator managed to write them, are dropped from
150
+ // the scaffold: ours (written below, after the move) carries the same CLI section plus this
151
+ // project's skills. A file the user's folder already has is never overwritten or merged into —
152
+ // it is kept as is, and the agent_configs event says so.
153
+ for (const f of AGENT_CONFIG_FILES) rmSync(join(projectDir, f), { recursive: true, force: true });
154
+ const entries = readdirSync(projectDir);
155
+ const clashes = entries.filter((e) => existsSync(join(cwd, e)));
156
+ if (clashes.length) {
157
+ fail("place", `the current directory already has: ${clashes.join(", ")} — run with --subfolder to keep the project in ${folderName}/, or start in an empty folder`);
158
+ }
159
+ try {
160
+ for (const entry of entries) renameSync(join(projectDir, entry), join(cwd, entry));
161
+ rmSync(projectDir, { recursive: true, force: true });
162
+ projectDir = cwd;
163
+ emit("project_placed", { into: cwd });
164
+ } catch (e) {
165
+ fail("place", e?.stack || e);
166
+ }
167
+ }
168
+
169
+ // ---- 2d · the agent config files `wix create` would have written --------------------------------
170
+ // Skipped by the CLI because of --skip-install. Fill-only: a project that has its own AGENTS.md
171
+ // keeps it (the event says `kept`).
172
+ emit("agent_configs", writeAgentsMd(projectDir, { skill: basename(SKILL_ROOT), stack }));
173
+
174
+ // ---- 3 · start the dependency install, detached --------------------------------------------------
175
+ // ONE install, here, for any project with a package.json (deploy patched it). The static stack has
176
+ // no package.json and nothing to install. `npm ci` only where a lockfile exists: without one it fails
177
+ // with a usage error that sits at the top of the log and reads as a failed install (run 109).
178
+ let install = null;
179
+ if (stack !== "static" && existsSync(join(projectDir, "package.json"))) {
180
+ const installLog = join(projectDir, "npm-install.log");
181
+ const logFd = openSync(installLog, "a");
182
+ const child = spawn("sh", ["-c", existsSync(join(projectDir, "package-lock.json")) ? "npm ci --ignore-scripts || npm install --ignore-scripts" : "npm install --ignore-scripts"], {
183
+ cwd: projectDir,
184
+ detached: true,
185
+ stdio: ["ignore", logFd, logFd],
186
+ });
187
+ child.unref();
188
+ install = { log: installLog, doneMarker: "node_modules/.package-lock.json" };
189
+ emit("install_started", install);
190
+ }
191
+
192
+ // ---- 4 · start the seed, detached (only with a plan) ---------------------------------------------
193
+ // The seed includes a Wix-side provisioning wait of unpredictable length (10-80s); running it in
194
+ // the caller's foreground would idle the agent for exactly that long. Detach it like the install:
195
+ // result JSON + exit-code marker land as files the caller syncs on before release. Seeding is
196
+ // additive: it never deletes or overwrites what the site holds.
197
+ let seed = null;
198
+ if (planPath) {
199
+ const seedDir = join(SKILL_ROOT, "references", vertical, "seed");
200
+ const seedName = existsSync(seedDir)
201
+ ? readdirSync(seedDir).find((f) => f.startsWith("seed-") && f.endsWith(".mjs"))
202
+ : undefined;
203
+ if (!seedName) fail("seed", `no seed module found under ${seedDir}`);
204
+ const seedFile = join(seedDir, seedName);
205
+ const planAbs = resolve(planPath);
206
+ const seedChild = spawn(
207
+ "sh",
208
+ ["-c", `node "${seedFile}" "${planAbs}" > seed-result.json 2> seed.log; echo $? > .seed-exit`],
209
+ { cwd: projectDir, detached: true, stdio: "ignore" },
210
+ );
211
+ seedChild.unref();
212
+ seed = { resultFile: "seed-result.json", log: "seed.log", doneMarker: ".seed-exit", success: "file contains 0" };
213
+ emit("seeding_started", { vertical, ...seed });
214
+ }
215
+
216
+ // ---- done ----------------------------------------------------------------------------------------
217
+ const release = {
218
+ astro: "npx @wix/cli@latest build, then npx @wix/cli@latest release",
219
+ react: "the project's own build, then npx @wix/cli@latest release of the build folder named in wix.config.json (what Wix hosting serves and how routes must be shaped: SKILL.md step 1)",
220
+ 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)",
221
+ static: "npx @wix/cli@latest release — no build; wix.config.json site.outputDirectory points at the folder the pages live in",
222
+ }[stack];
223
+ emit("ready_for_brand_layer", {
224
+ mode,
225
+ stack,
226
+ projectDir,
227
+ siteId,
228
+ dashboardUrl: deployResult.dashboardUrl,
229
+ productsUrl: deployResult.productsUrl,
230
+ categoriesUrl: deployResult.categoriesUrl,
231
+ install,
232
+ seed,
233
+ next:
234
+ (planPath
235
+ ? "theme + write the home page; "
236
+ : "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; ") +
237
+ (install || seed ? "then wait for the done markers" + (seed ? ", verify .seed-exit is 0 (else read seed.log and re-run the seed module)" : "") + "; " : "") +
238
+ `then ${release}`,
239
+ });
@@ -253,6 +253,8 @@ plugins — deploy added the dep). Routes: `/blog` → your feed on `useBlogFeed
253
253
  shape; `notFound` → your 404 view). Deploy wrote the public client id into `wix/config.ts`;
254
254
  nothing else to configure.
255
255
 
256
+ Routes on Wix hosting: the host serves files only, so a clean route answers 404 when loaded directly — hash routes, or one HTML file per route, decided before the first route is written; any URL handed to Wix as a return target must be one the host serves (SKILL.md step 1).
257
+
256
258
  ## Hard rules
257
259
 
258
260
  - **The body renders only through `RichContent`** where it deploys (Astro, React) — a post body
@@ -0,0 +1,22 @@
1
+ // What the blog holds: posts, categories, tags.
2
+ // node <SKILL_ROOT>/references/blog/seed/read-site.mjs [--site <siteId>] [--limit <n>]
3
+ import { runReader } from "../../shared/seed/read-site.mjs";
4
+
5
+ const BLOG_APP_ID = "14bcded7-0066-7c35-14d7-466cb3f09103";
6
+ const D = "https://dev.wix.com/docs/api-reference/business-solutions/blog";
7
+
8
+ await runReader({
9
+ vertical: "blog",
10
+ appId: BLOG_APP_ID,
11
+ async read(api, { limit }) {
12
+ const posts = await api.call({ path: "/v3/posts/query", body: { fieldsets: ["URL"], query: { paging: { limit } } }, docs: `${D}/posts-stats/query-posts` });
13
+ const cats = await api.tryCall({ path: "/blog/v3/categories/query", body: { query: { paging: { limit: 100 } } }, docs: `${D}/category/query-categories` });
14
+ const tags = await api.tryCall({ path: "/v3/tags/query", body: { query: { paging: { limit: 100 } } }, docs: `${D}/tags/query-tags` });
15
+ return {
16
+ postCount: posts.metaData?.total ?? posts.pagingMetadata?.total ?? (posts.posts ?? []).length,
17
+ posts: (posts.posts ?? []).map((p) => ({ title: p.title, slug: p.slug, published: p.firstPublishedDate ?? null, cover: !!p.coverMedia?.image, categories: (p.categoryIds ?? []).length, tags: (p.tagIds ?? []).length, minutesToRead: p.minutesToRead ?? null })),
18
+ categories: (cats?.categories ?? []).map((c) => ({ id: c.id, label: c.label, slug: c.slug, postCount: c.postCount ?? null })),
19
+ tags: (tags?.tags ?? []).map((t) => ({ id: t.id, label: t.label, postCount: t.postCount ?? null })),
20
+ };
21
+ },
22
+ });
@@ -223,6 +223,8 @@ fetch with `fetchServiceBySlug(slug)` client-side, then your booking surface on
223
223
  `useBookingFlow(service)`. Deploy wrote the public client id into `wix/config.ts`; nothing else
224
224
  to configure.
225
225
 
226
+ Routes on Wix hosting: the host serves files only, so a clean route answers 404 when loaded directly — hash routes, or one HTML file per route, decided before the first route is written; any URL handed to Wix as a return target must be one the host serves (SKILL.md step 1).
227
+
226
228
  ## Hard rules
227
229
 
228
230
  - **Booking logic only through the shipped exports** — `useBookingFlow`/`bookService` own the
@@ -0,0 +1,28 @@
1
+ // What the booking business holds: services, staff, categories.
2
+ // node <SKILL_ROOT>/references/bookings/seed/read-site.mjs [--site <siteId>] [--limit <n>]
3
+ import { runReader } from "../../shared/seed/read-site.mjs";
4
+
5
+ const BOOKINGS_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
6
+ const D = "https://dev.wix.com/docs/api-reference/business-solutions/bookings";
7
+
8
+ await runReader({
9
+ vertical: "bookings",
10
+ appId: BOOKINGS_APP_ID,
11
+ async read(api, { limit }) {
12
+ const services = await api.call({ path: "/_api/bookings/v2/services/query", body: { query: { paging: { limit }, filter: { hidden: { $eq: false } } } }, docs: `${D}/services/services-v2/query-services` });
13
+ const staff = await api.tryCall({ path: "/bookings/v1/staff-members/query", body: { query: {}, fields: ["RESOURCE_DETAILS"] }, docs: `${D}/staff-members/staff-members/query-staff-members` });
14
+ const cats = await api.tryCall({ path: "/bookings/v2/categories/query", body: { query: {} }, docs: `${D}/services/categories-v2/query-categories` });
15
+ return {
16
+ serviceCount: services.pagingMetadata?.total ?? (services.services ?? []).length,
17
+ services: (services.services ?? []).map((s) => ({
18
+ name: s.name, slug: s.mainSlug?.name ?? null, type: s.type ?? null,
19
+ category: s.category?.name ?? null,
20
+ price: s.payment?.fixed?.price ? `${s.payment.fixed.price.value} ${s.payment.fixed.price.currency}` : s.payment?.varied?.defaultPrice ? `from ${s.payment.varied.defaultPrice.value} ${s.payment.varied.defaultPrice.currency}` : (s.payment?.rateType ?? null),
21
+ durationMinutes: s.schedule?.availabilityConstraints?.sessionDurations?.[0] ?? null,
22
+ online: s.payment?.options?.online ?? false, inPerson: s.payment?.options?.inPerson ?? false, media: s.media?.mainMedia ? 1 : 0,
23
+ })),
24
+ staff: (staff?.staffMembers ?? []).map((m) => m.name),
25
+ categories: (cats?.categories ?? []).map((c) => ({ id: c.id, name: c.name })),
26
+ };
27
+ },
28
+ });
@@ -289,6 +289,8 @@ plugins — deploy added the dep). Routes are yours: a list route per collection
289
289
  fetch client-side when no `initialItems` / `initialItem` is passed). Deploy wrote the public
290
290
  client id into `wix/config.ts`; nothing else to configure.
291
291
 
292
+ Routes on Wix hosting: the host serves files only, so a clean route answers 404 when loaded directly — hash routes, or one HTML file per route, decided before the first route is written; any URL handed to Wix as a return target must be one the host serves (SKILL.md step 1).
293
+
292
294
  ## Hard rules
293
295
 
294
296
  - **Data only through the shipped exports** — never re-derive a Wix Data request, never