@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.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +14 -112
- package/skills/wix-app/references/APP_TOOLS.md +2 -1
- package/skills/wix-app/references/BACKEND_EVENT.md +3 -3
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +41 -152
- package/skills/wix-app/references/DASHBOARD_MODAL.md +4 -4
- package/skills/wix-app/references/SDK_MODULE_MAP.md +44 -0
- package/skills/wix-app/references/SITE_PLUGIN.md +36 -190
- package/skills/wix-app/references/app-tools/TOOLS.md +3 -1
- package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +21 -0
- package/skills/wix-app/references/dashboard-page/ECOM_NAVIGATION.md +17 -0
- package/skills/wix-app/references/dashboard-plugin/SLOTS.md +4 -18
- package/skills/wix-app/references/site-plugin/EXAMPLES.md +1 -1
- package/skills/wix-headless-fast/SKILL.md +134 -49
- package/skills/wix-headless-fast/entry/skill.md +4 -3
- package/skills/wix-headless-fast/install/AGENTS.md +32 -0
- package/skills/wix-headless-fast/install/agents-md.mjs +39 -0
- package/skills/wix-headless-fast/install/attach.mjs +149 -42
- package/skills/wix-headless-fast/install/setup.mjs +239 -0
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +2 -0
- package/skills/wix-headless-fast/references/blog/seed/read-site.mjs +22 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +2 -0
- package/skills/wix-headless-fast/references/bookings/seed/read-site.mjs +28 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +2 -0
- package/skills/wix-headless-fast/references/cms/seed/read-site.mjs +20 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +2 -0
- package/skills/wix-headless-fast/references/events/seed/read-site.mjs +26 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +2 -0
- package/skills/wix-headless-fast/references/forms/seed/read-site.mjs +20 -0
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +2 -0
- package/skills/wix-headless-fast/references/members/seed/read-site.mjs +15 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +2 -0
- package/skills/wix-headless-fast/references/portfolio/seed/read-site.mjs +20 -0
- package/skills/wix-headless-fast/references/portfolio/seed/seed-portfolio.mjs +19 -21
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +2 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/read-site.mjs +21 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +2 -0
- package/skills/wix-headless-fast/references/restaurants/seed/read-site.mjs +25 -0
- package/skills/wix-headless-fast/references/restaurants/seed/seed-restaurants.mjs +25 -5
- package/skills/wix-headless-fast/references/shared/seed/read-site.mjs +99 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +4 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +7 -2
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +2 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +7 -1
- package/skills/wix-headless-fast/references/storefront/seed/read-site.mjs +38 -0
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +52 -16
- package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-create-coupon.md +10 -19
- package/skills/wix-manage/references/events/create-wix-event.md +5 -1
- 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 —
|
|
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
|
-
//
|
|
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] [--
|
|
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
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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
|
|
48
|
-
const
|
|
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 (
|
|
64
|
-
|
|
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(
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
349
|
+
mode,
|
|
350
|
+
stack,
|
|
351
|
+
install,
|
|
251
352
|
seed: null,
|
|
252
|
-
next:
|
|
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
|