@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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// What the CMS holds: collections, their fields, item counts, a few sample items each.
|
|
2
|
+
// node <SKILL_ROOT>/references/cms/seed/read-site.mjs [--site <siteId>] [--limit <n>]
|
|
3
|
+
import { runReader } from "../../shared/seed/read-site.mjs";
|
|
4
|
+
|
|
5
|
+
const D = "https://dev.wix.com/docs/api-reference/business-solutions/cms";
|
|
6
|
+
|
|
7
|
+
await runReader({
|
|
8
|
+
vertical: "cms",
|
|
9
|
+
async read(api, { limit }) {
|
|
10
|
+
const r = await api.call({ method: "GET", path: "/wix-data/v2/collections", docs: `${D}/collection-management/data-collections/list-data-collections` });
|
|
11
|
+
const all = r.collections ?? r.dataCollections ?? [];
|
|
12
|
+
const own = all.filter((c) => c.collectionType !== "WIX_APP");
|
|
13
|
+
const collections = [];
|
|
14
|
+
for (const c of own.slice(0, limit)) {
|
|
15
|
+
const items = await api.tryCall({ path: "/wix-data/v2/items/query", body: { dataCollectionId: c.id, query: { paging: { limit: 3 } }, returnTotalCount: true }, docs: `${D}/data-items/query-data-items` });
|
|
16
|
+
collections.push({ id: c.id, displayName: c.displayName, fields: (c.fields ?? []).map((f) => `${f.key}:${f.type}`), itemCount: items?.pagingMetadata?.total ?? null, sample: (items?.dataItems ?? []).map((i) => i.data?.title ?? i.data?.name ?? i.id) });
|
|
17
|
+
}
|
|
18
|
+
return { collectionCount: own.length, collections, appCollections: all.filter((c) => c.collectionType === "WIX_APP").map((c) => c.id) };
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -260,6 +260,8 @@ plugins — deploy already added the dep). Routes: `/events` → your listing; `
|
|
|
260
260
|
callbacks point at that path). Deploy wrote the public client id into `wix/config.ts`; nothing
|
|
261
261
|
else to configure.
|
|
262
262
|
|
|
263
|
+
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).
|
|
264
|
+
|
|
263
265
|
## Hard rules
|
|
264
266
|
|
|
265
267
|
- **Registration logic only through the shipped exports** — `useEventRegistration` /
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// What the events site holds: events and their ticket definitions.
|
|
2
|
+
// node <SKILL_ROOT>/references/events/seed/read-site.mjs [--site <siteId>] [--limit <n>]
|
|
3
|
+
import { runReader } from "../../shared/seed/read-site.mjs";
|
|
4
|
+
|
|
5
|
+
const EVENTS_APP_ID = "140603ad-af8d-84a5-2c80-a0f60cb47351";
|
|
6
|
+
const D = "https://dev.wix.com/docs/api-reference/business-solutions/events/event-management";
|
|
7
|
+
|
|
8
|
+
await runReader({
|
|
9
|
+
vertical: "events",
|
|
10
|
+
appId: EVENTS_APP_ID,
|
|
11
|
+
async read(api, { limit }) {
|
|
12
|
+
const ev = await api.call({ path: "/events/v3/events/query", body: { query: { filter: { status: { $ne: "CANCELED" } }, paging: { limit } }, fields: ["DETAILS", "REGISTRATION"] }, docs: `${D}/events-v3/query-events` });
|
|
13
|
+
const td = await api.tryCall({ path: "/events-ticket-definitions/v3/ticket-definitions/query", body: { query: { paging: { limit: 100 } } }, docs: `${D}/ticket-definitions-v3/query-ticket-definitions` });
|
|
14
|
+
const defs = td?.ticketDefinitions ?? [];
|
|
15
|
+
return {
|
|
16
|
+
eventCount: ev.pagingMetadata?.total ?? (ev.events ?? []).length,
|
|
17
|
+
events: (ev.events ?? []).map((e) => ({
|
|
18
|
+
title: e.title, slug: e.slug, status: e.status, start: e.dateAndTimeSettings?.startDate ?? null,
|
|
19
|
+
location: e.location?.name ?? e.location?.type ?? null, registration: e.registration?.type ?? null,
|
|
20
|
+
tickets: defs.filter((d) => d.eventId === e.id).map((d) => `${d.name}${d.pricingMethod?.fixedPrice?.formattedValue ? " " + d.pricingMethod.fixedPrice.formattedValue : ""}`),
|
|
21
|
+
image: !!e.mainImage,
|
|
22
|
+
})),
|
|
23
|
+
ticketDefinitionCount: defs.length,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -285,6 +285,8 @@ plugins — deploy already added the dep). `useWixForm(FORM_ID)` with no `initia
|
|
|
285
285
|
skeleton while `form` is null. Deploy wrote the public client id into `wix/config.ts`; nothing
|
|
286
286
|
else to configure.
|
|
287
287
|
|
|
288
|
+
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).
|
|
289
|
+
|
|
288
290
|
## Hard rules
|
|
289
291
|
|
|
290
292
|
- **Never name a field in code.** Map `form.fields`. A page with `values.email` hardcoded
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// What forms the site has, with their fields.
|
|
2
|
+
// node <SKILL_ROOT>/references/forms/seed/read-site.mjs [--site <siteId>] [--limit <n>]
|
|
3
|
+
import { runReader } from "../../shared/seed/read-site.mjs";
|
|
4
|
+
|
|
5
|
+
const FORMS_APP_ID = "225dd912-7dea-4738-8688-4b8c6955ffc2";
|
|
6
|
+
const NAMESPACE = "wix.form_app.form";
|
|
7
|
+
const D = "https://dev.wix.com/docs/api-reference/crm/forms/form-schemas";
|
|
8
|
+
|
|
9
|
+
await runReader({
|
|
10
|
+
vertical: "forms",
|
|
11
|
+
appId: FORMS_APP_ID,
|
|
12
|
+
async read(api, { limit }) {
|
|
13
|
+
const r = await api.call({ method: "GET", path: `/form-schema-service/v4/forms?namespace=${encodeURIComponent(NAMESPACE)}`, docs: `${D}/list-forms` });
|
|
14
|
+
const forms = (r.forms ?? []).slice(0, limit);
|
|
15
|
+
return {
|
|
16
|
+
formCount: (r.forms ?? []).length,
|
|
17
|
+
forms: forms.map((f) => ({ id: f.id, name: f.name, fields: (f.fields ?? []).filter((x) => !x.hidden).map((x) => `${x.view?.label ?? x.target ?? x.id} (${x.view?.fieldType ?? "?"})${x.validation?.required ? "*" : ""}`), steps: (f.steps ?? []).length })),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -271,6 +271,8 @@ Mount `MemberMenu` in the header (pass `LinkComponent` for router links). Deploy
|
|
|
271
271
|
client id into `wix/config.ts` (`WIX_CLIENT_ID` and `WIX_MEMBERS_CLIENT_ID`); nothing else to
|
|
272
272
|
configure.
|
|
273
273
|
|
|
274
|
+
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).
|
|
275
|
+
|
|
274
276
|
## Hard rules
|
|
275
277
|
|
|
276
278
|
- **No Wix-hosted login flow** — no `/api/auth/login`, no OAuth callback page, no callback URI for
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Whether the site has a members area, and how many members.
|
|
2
|
+
// node <SKILL_ROOT>/references/members/seed/read-site.mjs [--site <siteId>]
|
|
3
|
+
import { runReader } from "../../shared/seed/read-site.mjs";
|
|
4
|
+
|
|
5
|
+
const MEMBERS_AREA_APP_ID = "14cc59bc-f0b7-15b8-e1c7-89ce41d0e0c9";
|
|
6
|
+
const D = "https://dev.wix.com/docs/api-reference/crm/members-contacts/members/members";
|
|
7
|
+
|
|
8
|
+
await runReader({
|
|
9
|
+
vertical: "members",
|
|
10
|
+
appId: MEMBERS_AREA_APP_ID,
|
|
11
|
+
async read(api) {
|
|
12
|
+
const r = await api.call({ method: "GET", path: "/members/v1/members?fieldsets=PUBLIC&paging.limit=1", docs: `${D}/list-members` });
|
|
13
|
+
return { memberCount: r.metadata?.total ?? r.pagingMetadata?.total ?? null };
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -280,6 +280,8 @@ deploy added the dep). Routes: `/portfolio` → your gallery on `useCollections(
|
|
|
280
280
|
`useProjectDetail(slug)` (the hooks fetch client-side when no `initial*` is passed; route the 404
|
|
281
281
|
view off `notFound`). Deploy wrote the public client id into `wix/config.ts`; nothing else to configure.
|
|
282
282
|
|
|
283
|
+
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).
|
|
284
|
+
|
|
283
285
|
## Hard rules
|
|
284
286
|
|
|
285
287
|
- **Reads only through the shipped exports** — they filter `hidden`, sort collections by the
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// What the portfolio holds: collections and projects.
|
|
2
|
+
// node <SKILL_ROOT>/references/portfolio/seed/read-site.mjs [--site <siteId>] [--limit <n>]
|
|
3
|
+
import { runReader } from "../../shared/seed/read-site.mjs";
|
|
4
|
+
|
|
5
|
+
const PORTFOLIO_APP_ID = "d90652a2-f5a1-4c7c-84c4-d4cdcc41f130";
|
|
6
|
+
const D = "https://dev.wix.com/docs/api-reference/business-solutions/portfolio";
|
|
7
|
+
|
|
8
|
+
await runReader({
|
|
9
|
+
vertical: "portfolio",
|
|
10
|
+
appId: PORTFOLIO_APP_ID,
|
|
11
|
+
async read(api, { limit }) {
|
|
12
|
+
const cols = await api.call({ method: "GET", path: "/portfolio/v1/collections", docs: `${D}/collections/list-collections` });
|
|
13
|
+
const projs = await api.tryCall({ method: "GET", path: "/portfolio/v1/projects", docs: `${D}/projects/list-projects` });
|
|
14
|
+
return {
|
|
15
|
+
collections: (cols.collections ?? []).map((c) => ({ id: c.id, title: c.title, slug: c.slug, hidden: c.hidden ?? false, cover: !!c.coverImage })),
|
|
16
|
+
projectCount: (projs?.projects ?? []).length,
|
|
17
|
+
projects: (projs?.projects ?? []).slice(0, limit).map((p) => ({ title: p.title, slug: p.slug, hidden: p.hidden ?? false, collections: (p.collectionIds ?? []).length, cover: !!p.coverImage })),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -125,22 +125,25 @@ export { importImage } from "../../shared/seed/images.mjs";
|
|
|
125
125
|
// Cover = the listing-card thumbnail. PATCH per entity, echoing the current revision (missing/
|
|
126
126
|
// stale revision fails); height + width are required alongside the imported file id.
|
|
127
127
|
// docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/projects/update-project.md
|
|
128
|
-
|
|
128
|
+
// One PATCH per entity; a failure on one must not cost the rest theirs. Returns
|
|
129
|
+
// { attached: [id], failures: [{ id, error }] } — the caller counts what persisted, not what was sent.
|
|
130
|
+
async function attachEach(ctx, items, urlOf, bodyOf) {
|
|
131
|
+
const attached = [];
|
|
132
|
+
const failures = [];
|
|
129
133
|
for (const it of items) {
|
|
130
|
-
await req(ctx,
|
|
131
|
-
|
|
132
|
-
body: { project: { id: it.id, revision: it.revision, coverImage: { imageInfo: { id: it.imageId, height: it.height, width: it.width } } } },
|
|
133
|
-
});
|
|
134
|
+
try { await req(ctx, urlOf(it), { method: "PATCH", body: bodyOf(it) }); attached.push(it.id); }
|
|
135
|
+
catch (e) { failures.push({ id: it.id, error: e?.message ?? String(e) }); }
|
|
134
136
|
}
|
|
137
|
+
return { attached, failures };
|
|
138
|
+
}
|
|
139
|
+
export async function attachProjectCovers(ctx, items) {
|
|
140
|
+
return attachEach(ctx, items, (it) => `/portfolio/v1/projects/${it.id}`,
|
|
141
|
+
(it) => ({ project: { id: it.id, revision: it.revision, coverImage: { imageInfo: { id: it.imageId, height: it.height, width: it.width } } } }));
|
|
135
142
|
}
|
|
136
143
|
// docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/collections/update-collection.md
|
|
137
144
|
export async function attachCollectionCovers(ctx, items) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
method: "PATCH",
|
|
141
|
-
body: { collection: { id: it.id, revision: it.revision, coverImage: { imageInfo: { id: it.imageId, height: it.height, width: it.width } } } },
|
|
142
|
-
});
|
|
143
|
-
}
|
|
145
|
+
return attachEach(ctx, items, (it) => `/portfolio/v1/collections/${it.id}`,
|
|
146
|
+
(it) => ({ collection: { id: it.id, revision: it.revision, coverImage: { imageInfo: { id: it.imageId, height: it.height, width: it.width } } } }));
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
// The detail-page gallery is a SEPARATE `item` entity — one POST per image; sortOrder (1,2,3…)
|
|
@@ -218,23 +221,18 @@ export async function setupPortfolio(ctx, { collections = [], projects = [] } =
|
|
|
218
221
|
? { id: c.id, revision: c.revision, imageId: files[colCoverAt + i].id, ...dims }
|
|
219
222
|
: null))
|
|
220
223
|
.filter(Boolean);
|
|
224
|
+
// coversAttached counts the PATCHes that succeeded; coverFailures names the rest. Neither blocks the seed.
|
|
221
225
|
let coversAttached = 0;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
/* those projects stay cover-less */
|
|
226
|
-
}
|
|
227
|
-
try {
|
|
228
|
-
if (colCovers.length) { await attachCollectionCovers(ctx, colCovers); coversAttached += colCovers.length; }
|
|
229
|
-
} catch {
|
|
230
|
-
/* those collections stay cover-less */
|
|
231
|
-
}
|
|
226
|
+
const coverFailures = [];
|
|
227
|
+
if (projCovers.length) { const r = await attachProjectCovers(ctx, projCovers); coversAttached += r.attached.length; coverFailures.push(...r.failures.map((f) => ({ project: f.id, error: f.error }))); }
|
|
228
|
+
if (colCovers.length) { const r = await attachCollectionCovers(ctx, colCovers); coversAttached += r.attached.length; coverFailures.push(...r.failures.map((f) => ({ collection: f.id, error: f.error }))); }
|
|
232
229
|
|
|
233
230
|
return {
|
|
234
231
|
collections: cols,
|
|
235
232
|
projects: projs,
|
|
236
233
|
itemsCreated: items.length,
|
|
237
234
|
coversAttached,
|
|
235
|
+
coverFailures,
|
|
238
236
|
};
|
|
239
237
|
}
|
|
240
238
|
|
|
@@ -238,6 +238,8 @@ when no `initialPlans` is passed); `/plans/:slug` → fetch with `fetchPlanBySlu
|
|
|
238
238
|
client-side, then your detail surface (null → your not-found state). Deploy wrote the public
|
|
239
239
|
client id into `wix/config.ts`; nothing else to configure.
|
|
240
240
|
|
|
241
|
+
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).
|
|
242
|
+
|
|
241
243
|
## Hard rules
|
|
242
244
|
|
|
243
245
|
- **Purchase only through the shipped exports** — `SubscribeButton` / `usePlanPurchase` /
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// What the site sells as plans: public pricing plans.
|
|
2
|
+
// node <SKILL_ROOT>/references/pricing-plans/seed/read-site.mjs [--site <siteId>] [--limit <n>]
|
|
3
|
+
import { runReader } from "../../shared/seed/read-site.mjs";
|
|
4
|
+
|
|
5
|
+
const PRICING_PLANS_APP_ID = "1522827f-c56c-a5c9-2ac9-00f9e6ae12d3";
|
|
6
|
+
const D = "https://dev.wix.com/docs/api-reference/business-solutions/pricing-plans";
|
|
7
|
+
|
|
8
|
+
await runReader({
|
|
9
|
+
vertical: "pricing-plans",
|
|
10
|
+
appId: PRICING_PLANS_APP_ID,
|
|
11
|
+
async read(api, { limit }) {
|
|
12
|
+
const r = await api.call({ path: "/pricing-plans/v3/plans/query", body: { query: { filter: { visibility: "PUBLIC" }, cursorPaging: { limit } } }, docs: `${D}/plans-v3/query-plans` });
|
|
13
|
+
return {
|
|
14
|
+
planCount: r.pagingMetadata?.total ?? (r.plans ?? []).length,
|
|
15
|
+
plans: (r.plans ?? []).map((p) => {
|
|
16
|
+
const v = p.pricingVariants?.[0]; const s = v?.pricingStrategies?.[0];
|
|
17
|
+
return { name: p.name, slug: p.slug ?? null, visibility: p.visibility, price: s?.flatRate?.amount ?? null, currency: p.currency ?? null, billing: v?.billingTerms?.billingCycle ? `${v.billingTerms.billingCycle.count} ${v.billingTerms.billingCycle.period}` : (v?.billingTerms?.startType ?? null), perks: (p.perks?.values ?? []).length };
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -298,6 +298,8 @@ client-side when no `initialMenus`); `/reservations` → your reservation surfac
|
|
|
298
298
|
order button in the header and your drawer once. Deploy wrote the public client id into
|
|
299
299
|
`wix/config.ts`; nothing else to configure.
|
|
300
300
|
|
|
301
|
+
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).
|
|
302
|
+
|
|
301
303
|
## Hard rules
|
|
302
304
|
|
|
303
305
|
- **Data and ordering logic only through the shipped exports** — `useOrderCart`/`addToOrder`
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// What the restaurant holds: menus, sections, items, locations.
|
|
2
|
+
// node <SKILL_ROOT>/references/restaurants/seed/read-site.mjs [--site <siteId>] [--limit <n>]
|
|
3
|
+
import { runReader } from "../../shared/seed/read-site.mjs";
|
|
4
|
+
|
|
5
|
+
const MENUS_APP_ID = "b278a256-2757-4f19-9313-c05c783bec92";
|
|
6
|
+
const D = "https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus";
|
|
7
|
+
const DL = "https://dev.wix.com/docs/api-reference/business-management/locations";
|
|
8
|
+
|
|
9
|
+
await runReader({
|
|
10
|
+
vertical: "restaurants",
|
|
11
|
+
appId: MENUS_APP_ID,
|
|
12
|
+
async read(api, { limit }) {
|
|
13
|
+
const menus = await api.call({ method: "GET", path: "/restaurants/menus/v1/menus", docs: `${D}/menus/list-menus` });
|
|
14
|
+
const sections = await api.tryCall({ method: "GET", path: "/restaurants/menus/v1/sections", docs: `${D}/sections/list-sections` });
|
|
15
|
+
const items = await api.tryCall({ method: "GET", path: "/restaurants/menus/v1/items", docs: `${D}/items/items/list-items` });
|
|
16
|
+
const locs = await api.tryCall({ method: "GET", path: "/locations/v1/locations", docs: `${DL}/list-locations` });
|
|
17
|
+
const secs = sections?.sections ?? []; const its = items?.items ?? [];
|
|
18
|
+
return {
|
|
19
|
+
menus: (menus.menus ?? []).map((m) => ({ name: m.name, visible: m.visible !== false, sections: (m.sectionIds ?? []).map((id) => secs.find((s) => s.id === id)?.name ?? id) })),
|
|
20
|
+
sectionCount: secs.length, itemCount: its.length,
|
|
21
|
+
items: its.slice(0, limit).map((i) => ({ name: i.name, price: i.priceInfo?.price ?? i.priceVariants ? "variants" : null, image: !!i.image, labels: (i.labels ?? []).length })),
|
|
22
|
+
locations: (locs?.locations ?? []).map((l) => ({ name: l.name, default: !!l.default, address: l.address?.formattedAddress ?? null })),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -207,8 +207,10 @@ export { importImage } from "../../shared/seed/images.mjs";
|
|
|
207
207
|
// the binding field is the Wix Media file `id`.
|
|
208
208
|
// items: [{ id, revision, price, image: { id, url, height, width } }]
|
|
209
209
|
// docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/items/items/bulk-update-item.md
|
|
210
|
+
// The bulk update returns 200 on PARTIAL failure: each item's outcome is in
|
|
211
|
+
// results[].itemMetadata (success, error, originalIndex). Returns { attached: [id], failures: [{ id, error }] }.
|
|
210
212
|
export async function attachItemImages(ctx, items) {
|
|
211
|
-
|
|
213
|
+
const r = await req(ctx, "/restaurants/menus/v1/bulk/items/update", {
|
|
212
214
|
body: {
|
|
213
215
|
items: items.map((it) => ({
|
|
214
216
|
item: {
|
|
@@ -220,6 +222,21 @@ export async function attachItemImages(ctx, items) {
|
|
|
220
222
|
})),
|
|
221
223
|
},
|
|
222
224
|
});
|
|
225
|
+
return bulkOutcome(items, r.results);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Pair a bulk response's results[] to the inputs by originalIndex; an input with no result did not persist.
|
|
229
|
+
export function bulkOutcome(items, results) {
|
|
230
|
+
const attached = [];
|
|
231
|
+
const failures = [];
|
|
232
|
+
for (const x of results ?? []) {
|
|
233
|
+
const src = items[x.itemMetadata?.originalIndex];
|
|
234
|
+
if (!src) continue;
|
|
235
|
+
if (x.itemMetadata?.success) attached.push(src.id);
|
|
236
|
+
else failures.push({ id: src.id, error: x.itemMetadata?.error?.description ?? x.itemMetadata?.error?.code ?? "unknown" });
|
|
237
|
+
}
|
|
238
|
+
for (const it of items) if (!attached.includes(it.id) && !failures.some((f) => f.id === it.id)) failures.push({ id: it.id, error: "no result for item" });
|
|
239
|
+
return { attached, failures };
|
|
223
240
|
}
|
|
224
241
|
|
|
225
242
|
// ---- business location (shared STEP 0 for ordering + reservations) ------------------------------
|
|
@@ -383,12 +400,14 @@ export async function setupRestaurants(ctx, plan) {
|
|
|
383
400
|
image: { id: files[i].id, url: files[i].url, width: 1024, height: 1024 } }
|
|
384
401
|
: null))
|
|
385
402
|
.filter(Boolean);
|
|
403
|
+
const imageFailures = [];
|
|
386
404
|
if (toAttach.length) {
|
|
387
405
|
try {
|
|
388
|
-
await attachItemImages(ctx, toAttach);
|
|
389
|
-
imagesAttached =
|
|
390
|
-
|
|
391
|
-
|
|
406
|
+
const r = await attachItemImages(ctx, toAttach);
|
|
407
|
+
imagesAttached = r.attached.length;
|
|
408
|
+
for (const f of r.failures) imageFailures.push(f);
|
|
409
|
+
} catch (e) {
|
|
410
|
+
for (const it of toAttach) imageFailures.push({ id: it.id, error: e?.message ?? String(e) });
|
|
392
411
|
}
|
|
393
412
|
}
|
|
394
413
|
|
|
@@ -453,6 +472,7 @@ export async function setupRestaurants(ctx, plan) {
|
|
|
453
472
|
return {
|
|
454
473
|
menus: createdMenus.map(({ items, ...m }) => m),
|
|
455
474
|
imagesAttached,
|
|
475
|
+
imageFailures,
|
|
456
476
|
sampleMenuRemoved,
|
|
457
477
|
ordering,
|
|
458
478
|
reservations,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// The shared runner behind every vertical's `seed/read-site.mjs` — the build-time read of what a
|
|
2
|
+
// site holds, so the agent sizes the content without composing a single Wix request.
|
|
3
|
+
//
|
|
4
|
+
// node <SKILL_ROOT>/references/<vertical>/seed/read-site.mjs [--site <siteId>] [--limit <n>]
|
|
5
|
+
//
|
|
6
|
+
// The site is `--site`, else the `siteId` in ./wix.config.json. The token is minted by the Wix CLI
|
|
7
|
+
// inside this process and never written anywhere. Output is ONE JSON object on stdout:
|
|
8
|
+
// { vertical, siteId, installed, ...what the vertical reads..., calls: [{ method, path, status, docs }] }
|
|
9
|
+
// `calls` lists every request made, with the method's documentation URL — open one of those (the
|
|
10
|
+
// URL plus `.md` is the full page) when the brief needs more than the summary shows. A failed call
|
|
11
|
+
// is reported in `calls` with its status and message; the reader continues and marks the field null.
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
const API = "https://www.wixapis.com";
|
|
16
|
+
const DOCS_INSTALLED_APPS = "https://dev.wix.com/docs/api-reference/business-management/app-installation/app-installation/get-installed-apps";
|
|
17
|
+
|
|
18
|
+
function flag(name) {
|
|
19
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
20
|
+
return i !== -1 && process.argv[i + 1] && !process.argv[i + 1].startsWith("--") ? process.argv[i + 1] : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function siteIdFromArgsOrConfig() {
|
|
24
|
+
const fromFlag = flag("site");
|
|
25
|
+
if (fromFlag) return fromFlag;
|
|
26
|
+
if (existsSync("wix.config.json")) {
|
|
27
|
+
const c = JSON.parse(readFileSync("wix.config.json", "utf8"));
|
|
28
|
+
return c.siteId ?? c.projectId ?? null;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function makeApi(siteId) {
|
|
34
|
+
const token = execFileSync("npx", ["-y", "@wix/cli@latest", "token", "--site", siteId], { encoding: "utf8" }).trim();
|
|
35
|
+
if (!token) throw new Error("the Wix CLI returned no token — run `npx @wix/cli@latest login` first");
|
|
36
|
+
const calls = [];
|
|
37
|
+
async function call({ method = "POST", path, body, docs }) {
|
|
38
|
+
const entry = { method, path: path.split("?")[0], docs };
|
|
39
|
+
calls.push(entry);
|
|
40
|
+
const res = await fetch(API + path, {
|
|
41
|
+
method,
|
|
42
|
+
headers: { Authorization: token, "wix-site-id": siteId, "Content-Type": "application/json" },
|
|
43
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
44
|
+
});
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
entry.status = res.status;
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
entry.error = text.slice(0, 200) || "(empty body)";
|
|
49
|
+
const e = new Error(`${method} ${entry.path} → ${res.status}: ${entry.error}`);
|
|
50
|
+
e.status = res.status;
|
|
51
|
+
throw e;
|
|
52
|
+
}
|
|
53
|
+
try { return text ? JSON.parse(text) : {}; } catch { return {}; }
|
|
54
|
+
}
|
|
55
|
+
// Same call, but a failure becomes null and stays in `calls` — for reads whose absence is data.
|
|
56
|
+
async function tryCall(opts) {
|
|
57
|
+
try { return await call(opts); } catch { return null; }
|
|
58
|
+
}
|
|
59
|
+
async function installed(appId) {
|
|
60
|
+
const r = await tryCall({ method: "GET", path: "/apps-installer-service/v1/app-instances", docs: DOCS_INSTALLED_APPS });
|
|
61
|
+
if (!r) return null;
|
|
62
|
+
return (r.appInstances ?? []).some((a) => a.appDefId === appId || a.appId === appId);
|
|
63
|
+
}
|
|
64
|
+
return { call, tryCall, installed, calls };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const limit = () => Number(flag("limit") ?? 12);
|
|
68
|
+
|
|
69
|
+
// read(api, { limit }) returns the vertical's summary; the runner adds the envelope and prints.
|
|
70
|
+
export async function runReader({ vertical, appId, read }) {
|
|
71
|
+
const siteId = siteIdFromArgsOrConfig();
|
|
72
|
+
if (!siteId) {
|
|
73
|
+
console.log(JSON.stringify({ error: "no site: pass --site <siteId> or run in a folder with wix.config.json" }));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const api = makeApi(siteId);
|
|
77
|
+
const out = { vertical, siteId, scope: null };
|
|
78
|
+
out.installed = appId ? await api.installed(appId) : undefined;
|
|
79
|
+
let data = {};
|
|
80
|
+
if (out.installed === false) {
|
|
81
|
+
out.scope = `${vertical}: the app is not installed on this site — nothing to read; the vertical's seed installs it`;
|
|
82
|
+
} else {
|
|
83
|
+
try { data = await read(api, { limit: limit() }); }
|
|
84
|
+
catch (e) { out.error = String(e.message).slice(0, 300); }
|
|
85
|
+
}
|
|
86
|
+
// What this output is and is not: a SIZING. Every list is a sample page; the counts are the site.
|
|
87
|
+
const partial = Object.entries(data)
|
|
88
|
+
.filter(([k, v]) => Array.isArray(v) && typeof data[`${k.replace(/s$/, "")}Count`] === "number" && data[`${k.replace(/s$/, "")}Count`] > v.length)
|
|
89
|
+
.map(([k, v]) => `${k}: ${v.length} of ${data[`${k.replace(/s$/, "")}Count`]} shown`);
|
|
90
|
+
if (!out.scope) {
|
|
91
|
+
out.scope = "A sizing, not the content. Lists are one page (--limit, max 100); counts are the whole site. " +
|
|
92
|
+
(partial.length ? `Partial here: ${partial.join("; ")}. ` : "") +
|
|
93
|
+
"The pages read everything live through the deployed data layer. To read more or filter, page the same call: its method and documentation URL are in `calls`.";
|
|
94
|
+
}
|
|
95
|
+
Object.assign(out, data);
|
|
96
|
+
out.calls = api.calls;
|
|
97
|
+
out.docs = [...new Set(api.calls.map((c) => c.docs).filter(Boolean))];
|
|
98
|
+
console.log(JSON.stringify(out, null, 2));
|
|
99
|
+
}
|
|
@@ -404,6 +404,8 @@ export default function ProductDetailView(props: {
|
|
|
404
404
|
// to full-width images stacked down the column (products with per-color
|
|
405
405
|
// linked media carry several gallery urls, so that stacks big duplicates).
|
|
406
406
|
// A single-image gallery is just the one primary — no empty strip.
|
|
407
|
+
// When d.variant resolves and carries imageUrl, that image becomes the primary (it is
|
|
408
|
+
// one of the gallery urls) — a shopper who picks a color sees that color.
|
|
407
409
|
// • name, EVERY ribbon (d.product.ribbons), live d.price (the range until every option
|
|
408
410
|
// is picked) with d.compareAtPrice as a labelled "was" when present — never invent one;
|
|
409
411
|
// descriptionHtml rendered as HTML, then d.product.infoSections as sections/accordions
|
|
@@ -546,6 +548,8 @@ client-side when no `initial` is passed).
|
|
|
546
548
|
Mount the shipped `CartButton` in the header and `CartDrawer` once. Deploy wrote the public
|
|
547
549
|
client id into `wix/config.ts`; nothing else to configure.
|
|
548
550
|
|
|
551
|
+
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).
|
|
552
|
+
|
|
549
553
|
## Hard rules
|
|
550
554
|
|
|
551
555
|
- **Data and commerce logic only through the shipped exports** — never rewrite their
|
|
@@ -232,7 +232,10 @@ export function toModifiers(raw: Raw): ProductModifier[] {
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
/** Variants keyed by optionName → choiceName. Buyable when in stock OR pre-orderable. */
|
|
235
|
-
|
|
235
|
+
// `variant.media` is read-only on the API: Wix derives it from the media linked to the variant's
|
|
236
|
+
// choice (a product with several options derives it only when the choices agree). Mapped to a
|
|
237
|
+
// URL so a PDP can show the selected variant's picture without knowing how Wix derives it.
|
|
238
|
+
export function toVariants(raw: Raw, imgSrc: ImgSrc): ProductVariant[] {
|
|
236
239
|
return ((raw.variantsInfo?.variants ?? []) as Raw[])
|
|
237
240
|
.filter((v) => v.visible !== false)
|
|
238
241
|
.map((v) => {
|
|
@@ -242,6 +245,7 @@ export function toVariants(raw: Raw): ProductVariant[] {
|
|
|
242
245
|
if (on && cn) choices[on] = cn;
|
|
243
246
|
}
|
|
244
247
|
const { current, original } = sellingPrice(v.price);
|
|
248
|
+
const vm = v.media ? (v.media.image ?? v.media) : null;
|
|
245
249
|
return {
|
|
246
250
|
variantId: id(v),
|
|
247
251
|
choices,
|
|
@@ -249,6 +253,7 @@ export function toVariants(raw: Raw): ProductVariant[] {
|
|
|
249
253
|
compareAtPrice: strike(original, current),
|
|
250
254
|
inStock: v.inventoryStatus?.inStock !== false,
|
|
251
255
|
preorderEnabled: v.inventoryStatus?.preorderEnabled === true,
|
|
256
|
+
imageUrl: vm ? imgSrc(vm, 1200, 1200) || null : null,
|
|
252
257
|
};
|
|
253
258
|
});
|
|
254
259
|
}
|
|
@@ -264,7 +269,7 @@ export function toDetail(raw: Raw, imgSrc: ImgSrc, mediaKey: MediaKey): ProductD
|
|
|
264
269
|
gallery: mediaEntries(raw, mediaKey).map((m) => imgSrc(m, 1200, 1200)).filter(Boolean),
|
|
265
270
|
options: toOptions(raw),
|
|
266
271
|
modifiers: toModifiers(raw),
|
|
267
|
-
variants: toVariants(raw),
|
|
272
|
+
variants: toVariants(raw, imgSrc),
|
|
268
273
|
};
|
|
269
274
|
}
|
|
270
275
|
|
|
@@ -82,6 +82,8 @@ export interface ProductVariant {
|
|
|
82
82
|
inStock: boolean;
|
|
83
83
|
/** Out of stock but pre-orderable — still buyable (the add carries preOrderRequested). */
|
|
84
84
|
preorderEnabled: boolean;
|
|
85
|
+
/** This variant's own image (from its choice's linked media), resolved to an https URL; null when it has none. */
|
|
86
|
+
imageUrl: string | null;
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/** A product as the detail page needs it. */
|
|
@@ -17,7 +17,7 @@ node <SKILL_ROOT>/references/storefront/seed/seed-store.mjs plan.json
|
|
|
17
17
|
|
|
18
18
|
Run this way, the result is the process itself: exit code `0` and the JSON on stdout (redirect it
|
|
19
19
|
to a file if you want it later). `.seed-exit` and `seed-result.json` are written by
|
|
20
|
-
`install/
|
|
20
|
+
`install/setup.mjs` when IT starts the seed — don't wait for them after a manual run.
|
|
21
21
|
|
|
22
22
|
`plan.json` is plain data — write it from the brief:
|
|
23
23
|
|
|
@@ -97,6 +97,12 @@ own sample products to a new catalog; they stay, and the owner removes them in t
|
|
|
97
97
|
(the Manage products link is in your summary). If a cleanup genuinely seems needed, ask the user
|
|
98
98
|
first. Categories are idempotent by name — a re-run reuses "Donuts" instead of creating a second one.
|
|
99
99
|
|
|
100
|
+
**Images are confirmed, not assumed.** `imagesAttached` counts the attaches the API reported as
|
|
101
|
+
successful; `imageFailures: [{ name, error }]` names the products left without an image and why
|
|
102
|
+
(one miss out of three was seen live — a revision moved between read and update). The seed already
|
|
103
|
+
retries a miss once. To retry again, re-run the same plan: existing products are reused and their
|
|
104
|
+
images attached; nothing is duplicated.
|
|
105
|
+
|
|
100
106
|
**A bulk create can partially succeed.** The result carries `failures: [{ name, error }]` next
|
|
101
107
|
to `products` — read it. A non-empty `failures` means those products are genuinely absent, not
|
|
102
108
|
mis-mapped, so the rest of the catalog is fine to build on. To retry, re-run the **same** plan:
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// What the store holds: products (names, prices, options, media, stock), categories. Run it before
|
|
2
|
+
// designing a storefront for a site that already has content:
|
|
3
|
+
// node <SKILL_ROOT>/references/storefront/seed/read-site.mjs [--site <siteId>] [--limit <n>]
|
|
4
|
+
// Every request here is the one the deployed data layer makes (rest/catalog.ts); the doc URL of
|
|
5
|
+
// each method is in the output's `calls`.
|
|
6
|
+
import { runReader } from "../../shared/seed/read-site.mjs";
|
|
7
|
+
|
|
8
|
+
const STORES_APP_ID = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
|
|
9
|
+
const D = "https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3";
|
|
10
|
+
const LIST_FIELDS = ["CURRENCY", "MEDIA_ITEMS_INFO", "MIN_PRICE_VARIANT", "DISCOUNT_INFO"];
|
|
11
|
+
const VISIBLE = { $and: [{ visible: true }] };
|
|
12
|
+
const TREE = { appNamespace: "@wix/stores", treeKey: null };
|
|
13
|
+
|
|
14
|
+
await runReader({
|
|
15
|
+
vertical: "storefront",
|
|
16
|
+
appId: STORES_APP_ID,
|
|
17
|
+
async read(api, { limit }) {
|
|
18
|
+
const count = await api.tryCall({ path: "/stores/v3/products/count", body: { filter: VISIBLE }, docs: `${D}/products-v3/count-products` });
|
|
19
|
+
const search = await api.call({ path: "/stores/v3/products/search", body: { fields: LIST_FIELDS, search: { filter: VISIBLE, cursorPaging: { limit } } }, docs: `${D}/products-v3/search-products` });
|
|
20
|
+
const cats = await api.tryCall({ path: "/categories/v1/categories/query", body: { treeReference: TREE, query: { filter: { name: { $exists: true } }, cursorPaging: { limit: 100 } } }, docs: `${D}/categories/query-categories` });
|
|
21
|
+
const products = (search.products ?? []).map((p) => ({
|
|
22
|
+
name: p.name, slug: p.slug,
|
|
23
|
+
price: p.actualPriceRange?.minValue?.formattedAmount ?? null,
|
|
24
|
+
priceMax: p.actualPriceRange?.maxValue?.formattedAmount ?? null,
|
|
25
|
+
compareAt: p.compareAtPriceRange?.minValue?.formattedAmount ?? null,
|
|
26
|
+
options: (p.options ?? []).map((o) => `${o.name} (${o.choicesSettings?.choices?.length ?? 0})`),
|
|
27
|
+
media: p.media?.itemsInfo?.items?.length ?? 0,
|
|
28
|
+
availability: p.inventory?.availabilityStatus ?? null,
|
|
29
|
+
}));
|
|
30
|
+
return {
|
|
31
|
+
currency: search.products?.[0]?.currency ?? null,
|
|
32
|
+
productCount: typeof count?.count === "number" ? count.count : null,
|
|
33
|
+
products,
|
|
34
|
+
productsShown: products.length,
|
|
35
|
+
categories: (cats?.categories ?? []).filter((c) => c.slug !== "all-products").map((c) => ({ id: c.id, name: c.name, slug: c.slug, visible: c.visible !== false })),
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
});
|