@wix/agent-skills 1.24.0 → 1.25.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/README.md +1 -0
- package/package.json +2 -1
- package/skills/wix-app/SKILL.md +4 -1
- package/skills/wix-app/references/BUSINESS_MANAGER_THEME.md +108 -0
- package/skills/wix-app/references/BUSINESS_MANAGER_TOKENS.md +81 -0
- package/skills/wix-app/references/DASHBOARD_MODAL.md +41 -0
- package/skills/wix-app/references/DASHBOARD_PAGE.md +2 -0
- package/skills/wix-app/references/DASHBOARD_PLUGIN.md +19 -4
- package/skills/wix-app/references/dashboard-page/DRAFT_TEMPLATE.md +5 -4
- package/skills/wix-app/references/dashboard-page/DRAFT_TEMPLATE_ROUTER.md +8 -7
- package/skills/wix-app/references/dashboard-page/DYNAMIC_PARAMETERS.md +21 -38
- package/skills/wix-headless/SKILL.md +0 -1
- package/skills/wix-headless/references/CAPABILITIES.md +3 -3
- package/skills/wix-headless/references/DISCOVERY.md +1 -1
- package/skills/wix-headless/references/SDK_HANDOFF.md +1 -1
- package/skills/wix-headless/references/SEED.md +1 -1
- package/skills/wix-headless/references/SETUP.md +1 -1
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-reservations.md +1 -1
- package/skills/wix-headless/references/inline-recipes/setup-restaurant-experiences.md +2 -2
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +3 -14
- package/skills/wix-headless/scripts/bootstrap.mjs +156 -0
- package/skills/wix-headless-kit/SKILL.md +4 -4
- package/skills/wix-headless-kit/install/AGENTS.md +2 -2
- package/skills/wix-headless-kit/install/bootstrap.mjs +156 -0
- package/skills/wix-headless-kit/install/check.mjs +1 -1
- package/skills/wix-manage/SKILL.md +2 -8
- package/skills/wix-manage/references/cms/cms-data-items-crud.md +250 -18
- package/skills/wix-manage/references/cms/cms-schema-management.md +52 -1
- package/skills/wix-manage/references/cms/cms-data-operations-extended.md +0 -263
- package/skills/wix-manage/references/cms/cms-references-and-relationships.md +0 -162
|
@@ -39,7 +39,7 @@ Each entry states only the entities, where their count/content comes from, the p
|
|
|
39
39
|
- **bookings** — *Bookable services.* Recipe: **`setup-bookings.md`**. `intent.bookings.serviceCount` services (name + short description fitting `brand`, a simple duration and price); keep the schedule minimal. **Keep:** `serviceIds[]` as **seed-time state** (pricing-plans' coverage step keys on them, below); nothing for the handoff — services are discovered **live** (`queryServices`).
|
|
40
40
|
- **rentals** — *Rentable resources.* Recipe: **`setup-rentals.md`**. A resource **type** per kind of thing rented (rooms, vans, cameras), then `intent.rentals.resourceCount` **resources** inside it (Room A / Room B — parallel capacity comes from more resources, not higher capacity), then `intent.rentals.serviceCount` **rental services**, each carrying the Wix Rentals `appId`, `serviceResources` naming its resource type, a `primaryResourceType`, and a `durationRange` (`HOUR` or `DAY`) with the min/max the request implies — defaulting to **60/480 minutes** hourly and **1/5 days** daily when the brief doesn't say — and a **per-unit** price. **No category** — unlike bookings, rental services don't use one; don't create or assign one. **Order is load-bearing: resource type → resources → services** — a service whose resource type holds no resources has permanently empty availability. Seed resources **24/7** (no working-hours schedule) unless the request names opening hours; that keeps a multi-day rental to a single booking. **Keep:** `resourceTypeIds[]` as seed-time state (services key on them); nothing for the handoff — rentals are discovered **live** (`queryServicesByFilters` filtered by the rentals `appId`).
|
|
41
41
|
- **pricing-plans** — *Membership tiers.* Recipe: **`setup-pricing-plans.md`**. `intent.pricing-plans.planCount` recurring plans (name, price, a monthly billing cycle) fitting `brand`. **When bookings is also in this run and the intent is a membership that covers services** (a studio/gym/class pass), also **attach the covered bookings services to the plan** — seed the bookings services first, since coverage keys on their service ids (see the recipe). **Keep:** the bookings coverage only transiently, to wire it during seeding; nothing for the handoff — plans are discovered **live** (`queryPlans`) and plan→service coverage is read **live** at checkout, never from a frozen map (see `how-to-code-pricing-plans.md`).
|
|
42
|
-
- **restaurants** — *A menu.* Recipe: **`setup-restaurants.md`** (add-ons have their own recipes, below). Create the `itemCount` items per section first (name, description, price fitting `brand`), then the `intent.restaurants.sections` referencing those item ids, then the menu referencing the section ids — **items → sections → menu** (build bottom-up: a section is created with its `itemIds`, a menu with its `sectionIds`, so each child must exist before its parent — see `setup-restaurants.md` STEP 1–3). Text-only. **Keep (seed-time only):** `menuId`, `sectionIds[]`, `itemIds[]` to build the tree; nothing goes to the handoff — the frontend reads menus **live** (`listMenus`/`listSections`/`listItems`). **Online ordering (add-on, on demand)** — only when the request calls for ordering (not just a displayed menu): install the Orders app (per `SETUP.md` §2) and follow **`setup-restaurant-orders.md`**; **seed the menu FIRST, then ordering, in the same unit** (ordering auto-provisions per menu either way — see the concurrency note above — but keeping them one unit gives the ordering verify a menu to confirm against). **Configure the business-location address (`setup-restaurant-orders.md` STEP 0) — required for ordering to work end-to-end; without it Wix limits ordering to "testing only" and checkout breaks.** If the brief gives no address, set a placeholder and flag the owner in the handoff. Completing a *paid* order additionally needs premium + a payment method (dashboard/premium — surface, don't fail). **Keep:** nothing for the handoff — read **live** (`listOperations`/`listFulfillmentMethods`); track `operationId`/`fulfillmentMethodIds[]` transiently. **Table reservations (add-on, on demand, INDEPENDENT of ordering/menu)** — only when the request calls for reservations: install the Table Reservations app (per `SETUP.md` §2) and follow **`setup-restaurant-reservations.md`**; **no menu dependency** (reservations bind to a location, not a menu — do NOT apply the menu-first rule) and **nothing to bulk-seed** (visitors create reservations at runtime); **configure the business-location address (`setup-restaurant-reservations.md` STEP 0)** — the reservation location is the site's business location, so set a real address (placeholder + owner flag if the brief names none; shared with ordering if both are present); enabling online reservations is **premium-gated** (record the precondition, don't fail). **Keep:** nothing for the handoff — read **live** (`listReservationLocations`); track `reservationLocationId` transiently. **Experiences (add-on within Table Reservations, on demand)** — only when the request names **special dining occasions guests reserve** (
|
|
42
|
+
- **restaurants** — *A menu.* Recipe: **`setup-restaurants.md`** (add-ons have their own recipes, below). Create the `itemCount` items per section first (name, description, price fitting `brand`), then the `intent.restaurants.sections` referencing those item ids, then the menu referencing the section ids — **items → sections → menu** (build bottom-up: a section is created with its `itemIds`, a menu with its `sectionIds`, so each child must exist before its parent — see `setup-restaurants.md` STEP 1–3). Text-only. **Keep (seed-time only):** `menuId`, `sectionIds[]`, `itemIds[]` to build the tree; nothing goes to the handoff — the frontend reads menus **live** (`listMenus`/`listSections`/`listItems`). **Online ordering (add-on, on demand)** — only when the request calls for ordering (not just a displayed menu): install the Orders app (per `SETUP.md` §2) and follow **`setup-restaurant-orders.md`**; **seed the menu FIRST, then ordering, in the same unit** (ordering auto-provisions per menu either way — see the concurrency note above — but keeping them one unit gives the ordering verify a menu to confirm against). **Configure the business-location address (`setup-restaurant-orders.md` STEP 0) — required for ordering to work end-to-end; without it Wix limits ordering to "testing only" and checkout breaks.** If the brief gives no address, set a placeholder and flag the owner in the handoff. Completing a *paid* order additionally needs premium + a payment method (dashboard/premium — surface, don't fail). **Keep:** nothing for the handoff — read **live** (`listOperations`/`listFulfillmentMethods`); track `operationId`/`fulfillmentMethodIds[]` transiently. **Table reservations (add-on, on demand, INDEPENDENT of ordering/menu)** — only when the request calls for reservations: install the Table Reservations app (per `SETUP.md` §2) and follow **`setup-restaurant-reservations.md`**; **no menu dependency** (reservations bind to a location, not a menu — do NOT apply the menu-first rule) and **nothing to bulk-seed** (visitors create reservations at runtime); **configure the business-location address (`setup-restaurant-reservations.md` STEP 0)** — the reservation location is the site's business location, so set a real address (placeholder + owner flag if the brief names none; shared with ordering if both are present); enabling online reservations is **premium-gated** (record the precondition, don't fail). **Keep:** nothing for the handoff — read **live** (`listReservationLocations`); track `reservationLocationId` transiently. **Experiences (add-on within Table Reservations, on demand)** — only when the request names **special dining occasions guests reserve** (chef's table, tasting menu, tasting-menu evening — an *experience*, not a `bookings` service; see `CAPABILITIES.md`): they need the **Table Reservations app** (no extra install) and a configured **business-location address** (`setup-restaurant-reservations.md` STEP 0, same as reservations) and, unlike plain reservations, they **are** seeded — create one Experience per named occasion on the default reservation location, following **`setup-restaurant-experiences.md`**. Booking an experience is **premium-gated** like online reservations (record the precondition, don't fail). **Keep:** nothing for the handoff — experiences are read **live** (`queryExperiences`); track `experienceId`(s) transiently.
|
|
43
43
|
- **portfolio** — *A project showcase.* Recipe: **`setup-portfolio.md`**. Create the `intent.portfolio.collections` (title + short description fitting `brand`), then `intent.portfolio.projectCount` projects assigned to them — **collections before projects** (a project's `collectionIds` must hold real collection ids; see the recipe). Text-only by default (omit `coverImage`; images only when `imagery` is on — §1, §4). **Keep (seed-time only):** `collectionIds{<name>}` to assign projects into their collections; nothing goes to the handoff — the frontend lists collections and projects **live** (`collections.queryCollections` / `projects.queryProjects`), so owner-added content appears with no code change.
|
|
44
44
|
|
|
45
45
|
> **Cross-cutting (on demand).** `coupons` is not a standalone capability (`CAPABILITIES.md` § "Cross-cutting capabilities") — there's no app to install and it's not in `verticals[]`. If intent calls for discounts **and** a parent vertical (stores / bookings / events / pricing-plans) is in this run, create coupons scoped to that parent and add the coupon to the `seeded` map (`seeded.coupons = { couponIds[] }`). **Coupons has no inline recipe** — read the create shape from the docs (`DOC_DISCOVERY.md` is the fallback): every coupon needs a scope with a namespace and its parent vertical's app installed (<https://dev.wix.com/docs/api-reference/business-solutions/coupons/about-wix-coupons.md>), and the create-coupon spec needs name/code/start + a scope namespace + exactly one coupon-type field (<https://dev.wix.com/docs/api-reference/business-solutions/coupons/coupons/create-a-coupon.md>). eCommerce needs no seeding — it's the runtime checkout layer that rides along.
|
|
@@ -25,7 +25,7 @@ For each capability in `verticals[]`, install its app by `appDefId` (these are t
|
|
|
25
25
|
- **bookings** → `13d21c63-b5ec-5912-8397-c3a5ddb27a97`
|
|
26
26
|
- **rentals** → `ff5d6eb1-65e4-4f9a-8b14-64d34c12cc2e` (Wix Rentals). Rentals provisions the Bookings infrastructure it runs on, so a Rentals-only install is enough — its availability comes back without adding Wix Bookings separately. Rentals has **no APIs of its own**: it is Services V2 with rentals-specific field values (`setup-rentals.md`), and its frontend package is `@wix/bookings` — there is no `@wix/rentals`. Rentals uses no categories; to also offer plain fixed-duration services, install Wix Bookings (`13d21c63-b5ec-5912-8397-c3a5ddb27a97`) as well, seed those with `setup-bookings.md`, and filter every catalog read by `appId` so the two sets stay separate.
|
|
27
27
|
- **pricing-plans** → `1522827f-c56c-a5c9-2ac9-00f9e6ae12d3`
|
|
28
|
-
- **restaurants** → `b278a256-2757-4f19-9313-c05c783bec92` (Wix Restaurants **Menus** — the seedable core). Online ordering and table reservations are **separate optional apps**, install only if intent calls for them: Orders (New) → `9a5d83fd-8570-482e-81ab-cfa88942ee60`, Table Reservations → `f9c07de2-5341-40c6-b096-8eb39de391fb`. **Experiences** (special dining occasions guests reserve —
|
|
28
|
+
- **restaurants** → `b278a256-2757-4f19-9313-c05c783bec92` (Wix Restaurants **Menus** — the seedable core). Online ordering and table reservations are **separate optional apps**, install only if intent calls for them: Orders (New) → `9a5d83fd-8570-482e-81ab-cfa88942ee60`, Table Reservations → `f9c07de2-5341-40c6-b096-8eb39de391fb`. **Experiences** (special dining occasions guests reserve — chef's tables, tasting menus; see `CAPABILITIES.md`) are a **feature of the Table Reservations app, not a separate install** — installing Table Reservations is all that's needed to create and book them (`setup-restaurant-experiences.md`).
|
|
29
29
|
- **portfolio** → `d90652a2-f5a1-4c7c-84c4-d4cdcc41f130` (Wix Portfolio; the install ships a default sample collection + projects — `setup-portfolio.md` STEP 0 cleans them)
|
|
30
30
|
- **cms** → **no install** (Wix Data is core) — skip
|
|
31
31
|
|
package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-reservations.md
CHANGED
|
@@ -166,7 +166,7 @@ Doc: <https://dev.wix.com/docs/api-reference/business-solutions/restaurants/rese
|
|
|
166
166
|
|
|
167
167
|
### Booking an experience (only when experiences are in the run)
|
|
168
168
|
|
|
169
|
-
An **experience** is a reservation that *is* a curated dining occasion (
|
|
169
|
+
An **experience** is a reservation that *is* a curated dining occasion (chef's table, tasting menu). It uses the **same** booking mechanism as a table, with two differences: you **list experiences** to show and choose from, and you offer bookable times **projected from the experience's own schedule** (there is *no* experience-scoped slot API — see Gotcha C) rather than from `getScheduledTimeSlots`, then stamp the chosen `experienceId` onto the reservation. Read the shapes from the docs (`.md?apiView=SDK`); the earned drift gotchas below are what the docs won't tell you.
|
|
170
170
|
|
|
171
171
|
Flow (mirrors the docs' sample flow — <https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/experiences/sample-flows.md>):
|
|
172
172
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "Setup Restaurant Experiences"
|
|
3
|
-
description: Seeds Wix Table Reservations **Experiences** — a reservation that IS a special dining occasion at the restaurant (
|
|
3
|
+
description: Seeds Wix Table Reservations **Experiences** — a reservation that IS a special dining occasion at the restaurant (chef's table, cheese pairing, tasting-menu evening), as opposed to a plain table booking or a Bookings service. Points to the live docs for the full create payload and carries only the earned gotchas the docs don't state (field-shape drift, payment policy, premium gating, when-to-use). The occasions/prices/schedule come from the request.
|
|
4
4
|
---
|
|
5
5
|
**RECIPE**: Business Recipe – Experiences Setup for Wix Table Reservations (Experiences API)
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ description: Seeds Wix Table Reservations **Experiences** — a reservation that
|
|
|
8
8
|
|
|
9
9
|
## When to use experiences (and when NOT to)
|
|
10
10
|
|
|
11
|
-
An **experience** is a **reservation that is itself a curated dining occasion at the restaurant** — it books seats at the restaurant's own reservation location but overrides the location's defaults with its own name, description, per-guest payment policy, party-size, schedule, and booking form. Reach for it when the brief names a **
|
|
11
|
+
An **experience** is a **reservation that is itself a curated dining occasion at the restaurant** — it books seats at the restaurant's own reservation location but overrides the location's defaults with its own name, description, per-guest payment policy, party-size, schedule, and booking form. Reach for it when the brief names a **chef's table, cheese pairing, or tasting-menu evening**.
|
|
12
12
|
|
|
13
13
|
- **Experience vs plain reservation:** a plain reservation just books a table; an experience books a *named, scheduled, often paid occasion*. Same booking mechanism, different intent.
|
|
14
14
|
- **Experience vs `bookings`:** the separate `bookings` vertical is for a **service/appointment/class with a provider** (spa, tutor, studio). If a restaurant *hosts and seats* the occasion, it's an experience — not a booking. (See `CAPABILITIES.md` — "Experiences vs bookings".)
|
|
@@ -16,7 +16,7 @@ npx @wix/cli@latest whoami # exits 0 when logged in; non-zero when logged out
|
|
|
16
16
|
If it's non-zero, **log in yourself** — don't punt to the user and stop. A foreground command that exits in seconds, one JSON event per line:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
node ../../
|
|
19
|
+
node ../../scripts/bootstrap.mjs # path relative to this file
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
| Event | Do |
|
|
@@ -25,20 +25,9 @@ node ../../entry/bootstrap.mjs # path relative to this file
|
|
|
25
25
|
| `awaiting_user` (`verificationUri`, `userCode`, `message`) | Send `message` verbatim, then stop. Re-run the script when the user says they're done → `logged_in`. Re-running early returns the **same** code, not a new one. |
|
|
26
26
|
| `cli_unreachable` / `login_failed` (`detail`) | Show `detail` and stop. |
|
|
27
27
|
|
|
28
|
-
###
|
|
28
|
+
### What the script does
|
|
29
29
|
|
|
30
|
-
Read `../../
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
# AI_AGENT must be set, or the CLI renders an interactive Ink TUI and emits no JSON.
|
|
34
|
-
# Detach, redirecting to a FILE — a pipe dies with your process and the CLI gets EPIPE.
|
|
35
|
-
AI_AGENT=my-agent nohup npx -y @wix/cli@latest login > /tmp/wix-login.log 2>&1 &
|
|
36
|
-
|
|
37
|
-
grep -m1 awaiting_user /tmp/wix-login.log
|
|
38
|
-
# {"event":"awaiting_user","expiresInSeconds":600,"userCode":"…","verificationUri":"…"}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Send the user that URL + code, then stop — nothing moves until they act. The detached login lands the session on its own; resume with `whoami` (exit 0). Codes last ~10 min; after that, start a new login and surface the new code.
|
|
30
|
+
Read `../../scripts/bootstrap.mjs` if you want to see it: it sets `AI_AGENT` so the CLI emits JSON events instead of an interactive TUI, starts `wix login` detached with its output in a file under the OS temp dir (a pipe would die with the parent), and prints the device code. It is the one login path; there is no manual variant. Codes last ~10 min; after that, run it again and surface the new code.
|
|
42
31
|
|
|
43
32
|
## 2 · Mint the token
|
|
44
33
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// ── tiny event protocol (one JSON object per line) ───────────────────────────
|
|
8
|
+
const emit = (event, extra = {}) =>
|
|
9
|
+
process.stdout.write(JSON.stringify({ event, ...extra }) + '\n');
|
|
10
|
+
const fail = (event, extra = {}) => {
|
|
11
|
+
emit(event, { ok: false, ...extra });
|
|
12
|
+
process.exit(1);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// ── platform-safe binary names (npm/npx are .cmd on Windows) ─────────────────
|
|
16
|
+
const isWin = process.platform === 'win32';
|
|
17
|
+
const bin = (name) => (isWin ? `${name}.cmd` : name);
|
|
18
|
+
const WIX = [bin('npx'), '-y', '@wix/cli@latest']; // run the CLI via npx — no global install/mutation
|
|
19
|
+
|
|
20
|
+
// Force the CLI into non-interactive "agent" mode. Without an agent signal in
|
|
21
|
+
// the env, `wix login` renders an interactive Ink TUI (device code + keypress)
|
|
22
|
+
// that needs a raw TTY and crashes in an agent sandbox — and it never emits the
|
|
23
|
+
// JSON login events this script forwards. The CLI uses @vercel/detect-agent,
|
|
24
|
+
// whose first check is the AI_AGENT env var, so setting it guarantees agent mode
|
|
25
|
+
// (and the awaiting_user/success/logged_in events) for every child we spawn.
|
|
26
|
+
// Respect an existing value so a known runner (claude, cursor, …) keeps its name.
|
|
27
|
+
const AGENT_ENV = { ...process.env, AI_AGENT: process.env.AI_AGENT || 'wix-headless-skill' };
|
|
28
|
+
|
|
29
|
+
// Where the detached login parks its output between runs. It has to outlive this
|
|
30
|
+
// process, so it can't be a pipe and can't live in the project.
|
|
31
|
+
const STATE_DIR = join(tmpdir(), 'wix-headless-login');
|
|
32
|
+
const LOG = join(STATE_DIR, 'login.log');
|
|
33
|
+
const PIDFILE = join(STATE_DIR, 'login.pid');
|
|
34
|
+
|
|
35
|
+
// run a command, capture stdout+stderr (combined), return {status, out}
|
|
36
|
+
function capture(cmd, args, opts = {}) {
|
|
37
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', shell: isWin, env: AGENT_ENV, ...opts });
|
|
38
|
+
return { status: r.status ?? 1, out: `${r.stdout || ''}${r.stderr || ''}`, error: r.error };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── 1. CLI reachable (via npx — no install) ──────────────────────────────────
|
|
42
|
+
function checkCli() {
|
|
43
|
+
const r = capture(WIX[0], [...WIX.slice(1), '--version']);
|
|
44
|
+
if (r.status !== 0) fail('cli_unreachable', { detail: r.out.trim().slice(0, 400) });
|
|
45
|
+
// npx interleaves "npm notice …" lines with the version, so don't just take the
|
|
46
|
+
// last line — pick the first semver-looking token, falling back to the last line.
|
|
47
|
+
const version = (r.out.match(/\d+\.\d+\.\d+[^\s]*/) || [])[0] || r.out.trim().split('\n').pop();
|
|
48
|
+
emit('cli_ok', { version });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── 2. Reuse an existing session, or start device login ──────────────────────
|
|
52
|
+
const hasExistingSession = () => capture(WIX[0], [...WIX.slice(1), 'whoami']).status === 0;
|
|
53
|
+
|
|
54
|
+
function loginEvents() {
|
|
55
|
+
if (!existsSync(LOG)) return [];
|
|
56
|
+
return readFileSync(LOG, 'utf8')
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map((l) => l.trim())
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.map((l) => {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(l);
|
|
63
|
+
} catch {
|
|
64
|
+
return null; // non-JSON CLI chatter
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.filter((e) => e && e.event);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const alive = (pid) => {
|
|
71
|
+
try {
|
|
72
|
+
process.kill(pid, 0);
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// A login this machine started earlier may still be waiting on the browser. Reuse
|
|
80
|
+
// its code instead of minting a second one — a new login invalidates nothing, but
|
|
81
|
+
// it hands the user a different code than the one already on their screen.
|
|
82
|
+
function pendingLogin() {
|
|
83
|
+
if (!existsSync(PIDFILE)) return null;
|
|
84
|
+
let state;
|
|
85
|
+
try {
|
|
86
|
+
state = JSON.parse(readFileSync(PIDFILE, 'utf8'));
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (!state.pid || !alive(state.pid)) return null;
|
|
91
|
+
const ev = loginEvents().find((e) => e.event === 'awaiting_user');
|
|
92
|
+
if (!ev) return null;
|
|
93
|
+
// Don't hand back a code that's about to expire mid-typing.
|
|
94
|
+
const ageSeconds = Math.round((Date.now() - state.startedAt) / 1000);
|
|
95
|
+
if (ageSeconds > (ev.expiresInSeconds ?? 600) - 60) return null;
|
|
96
|
+
return ev;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Detach so the login keeps polling after this process exits. stdio goes to a
|
|
100
|
+
// file, not a pipe: a pipe dies with the parent, and the CLI would get EPIPE.
|
|
101
|
+
function startLogin() {
|
|
102
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
103
|
+
writeFileSync(LOG, '');
|
|
104
|
+
const out = openSync(LOG, 'a');
|
|
105
|
+
const child = spawn(WIX[0], [...WIX.slice(1), 'login'], {
|
|
106
|
+
detached: true,
|
|
107
|
+
stdio: ['ignore', out, out],
|
|
108
|
+
env: AGENT_ENV,
|
|
109
|
+
shell: isWin,
|
|
110
|
+
});
|
|
111
|
+
child.on('error', (e) => fail('login_failed', { detail: String(e) }));
|
|
112
|
+
writeFileSync(PIDFILE, JSON.stringify({ pid: child.pid, startedAt: Date.now() }));
|
|
113
|
+
child.unref();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function waitForCode(timeoutMs = 60000) {
|
|
117
|
+
const deadline = Date.now() + timeoutMs;
|
|
118
|
+
while (Date.now() < deadline) {
|
|
119
|
+
const events = loginEvents();
|
|
120
|
+
const ev = events.find((e) => e.event === 'awaiting_user');
|
|
121
|
+
if (ev) return ev;
|
|
122
|
+
const bad = events.find((e) => e.event === 'login_failed');
|
|
123
|
+
if (bad) fail('login_failed', bad);
|
|
124
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
125
|
+
}
|
|
126
|
+
const detail = existsSync(LOG) ? readFileSync(LOG, 'utf8').trim().slice(-1500) : '';
|
|
127
|
+
fail('login_failed', {
|
|
128
|
+
detail: detail || `wix login produced no device code within ${timeoutMs / 1000}s.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The next move is the user's, so hand the code back and exit rather than holding
|
|
133
|
+
// the caller open for a browser round-trip. `message` is the sentence to relay.
|
|
134
|
+
function surrenderTo(ev) {
|
|
135
|
+
emit('awaiting_user', {
|
|
136
|
+
...ev,
|
|
137
|
+
message:
|
|
138
|
+
`To connect your Wix account, open ${ev.verificationUri} and enter the code ` +
|
|
139
|
+
`${ev.userCode}. Tell me once you're done and I'll continue.`,
|
|
140
|
+
});
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── main ────────────────────────────────────────────────────────────────────
|
|
145
|
+
checkCli();
|
|
146
|
+
|
|
147
|
+
if (hasExistingSession()) {
|
|
148
|
+
emit('logged_in');
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const pending = pendingLogin();
|
|
153
|
+
if (pending) surrenderTo(pending);
|
|
154
|
+
|
|
155
|
+
startLogin();
|
|
156
|
+
surrenderTo(await waitForCode());
|
|
@@ -86,10 +86,10 @@ doesn't express — or once the site exists and the work turns to managing or ex
|
|
|
86
86
|
|
|
87
87
|
Needed throughout: Node ≥ 20.11, git, a logged-in Wix CLI (`npx @wix/cli@latest whoami`;
|
|
88
88
|
`npx @wix/cli@latest login` is a device-code flow: surface the URL and code to the user, never
|
|
89
|
-
read tokens into context), and the two companion skills beside this one, `wix-docs`
|
|
90
|
-
`wix-manage`
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
read tokens into context), and the two companion skills installed beside this one, `wix-docs`
|
|
90
|
+
and `wix-manage`. `node <SKILL_ROOT>/install/bootstrap.mjs` checks the CLI and runs the login
|
|
91
|
+
when there is none; the cold-start page, `https://www.wix.com/skills/headless-cold-start/headless-kit.md`,
|
|
92
|
+
gets a machine with none of this, the skills included, to that point. In a folder that
|
|
93
93
|
already holds a `wix.config.json`, `node <SKILL_ROOT>/install/context.mjs` first: it runs
|
|
94
94
|
`wix env pull` when `.env.local` is missing and prints the folder's **shape** (`folder.shape`, the
|
|
95
95
|
cases of step 3, with the `next` for each) and the two identities a project has — the deploy site
|
|
@@ -17,8 +17,8 @@ call is memory, and an empty or error reply to it says nothing about the site.
|
|
|
17
17
|
|
|
18
18
|
## Skills
|
|
19
19
|
|
|
20
|
-
Installed at `.agents/skills
|
|
21
|
-
`
|
|
20
|
+
Installed at `.agents/skills/` (by the Wix plugin, or by
|
|
21
|
+
`npx skills add wix/skills --skill {{SKILL}} --skill wix-docs --skill wix-manage`).
|
|
22
22
|
|
|
23
23
|
- `{{SKILL}}` — the code in this app and how to extend it. The shipped code sits in
|
|
24
24
|
`.agents/skills/{{SKILL}}/templates/`, fetched once from the skill's repository and kept with this
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// ── tiny event protocol (one JSON object per line) ───────────────────────────
|
|
8
|
+
const emit = (event, extra = {}) =>
|
|
9
|
+
process.stdout.write(JSON.stringify({ event, ...extra }) + '\n');
|
|
10
|
+
const fail = (event, extra = {}) => {
|
|
11
|
+
emit(event, { ok: false, ...extra });
|
|
12
|
+
process.exit(1);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// ── platform-safe binary names (npm/npx are .cmd on Windows) ─────────────────
|
|
16
|
+
const isWin = process.platform === 'win32';
|
|
17
|
+
const bin = (name) => (isWin ? `${name}.cmd` : name);
|
|
18
|
+
const WIX = [bin('npx'), '-y', '@wix/cli@latest']; // run the CLI via npx — no global install/mutation
|
|
19
|
+
|
|
20
|
+
// Force the CLI into non-interactive "agent" mode. Without an agent signal in
|
|
21
|
+
// the env, `wix login` renders an interactive Ink TUI (device code + keypress)
|
|
22
|
+
// that needs a raw TTY and crashes in an agent sandbox — and it never emits the
|
|
23
|
+
// JSON login events this script forwards. The CLI uses @vercel/detect-agent,
|
|
24
|
+
// whose first check is the AI_AGENT env var, so setting it guarantees agent mode
|
|
25
|
+
// (and the awaiting_user/success/logged_in events) for every child we spawn.
|
|
26
|
+
// Respect an existing value so a known runner (claude, cursor, …) keeps its name.
|
|
27
|
+
const AGENT_ENV = { ...process.env, AI_AGENT: process.env.AI_AGENT || 'wix-headless-kit-skill' };
|
|
28
|
+
|
|
29
|
+
// Where the detached login parks its output between runs. It has to outlive this
|
|
30
|
+
// process, so it can't be a pipe and can't live in the project.
|
|
31
|
+
const STATE_DIR = join(tmpdir(), 'wix-headless-kit-login');
|
|
32
|
+
const LOG = join(STATE_DIR, 'login.log');
|
|
33
|
+
const PIDFILE = join(STATE_DIR, 'login.pid');
|
|
34
|
+
|
|
35
|
+
// run a command, capture stdout+stderr (combined), return {status, out}
|
|
36
|
+
function capture(cmd, args, opts = {}) {
|
|
37
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', shell: isWin, env: AGENT_ENV, ...opts });
|
|
38
|
+
return { status: r.status ?? 1, out: `${r.stdout || ''}${r.stderr || ''}`, error: r.error };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── 1. CLI reachable (via npx — no install) ──────────────────────────────────
|
|
42
|
+
function checkCli() {
|
|
43
|
+
const r = capture(WIX[0], [...WIX.slice(1), '--version']);
|
|
44
|
+
if (r.status !== 0) fail('cli_unreachable', { detail: r.out.trim().slice(0, 400) });
|
|
45
|
+
// npx interleaves "npm notice …" lines with the version, so don't just take the
|
|
46
|
+
// last line — pick the first semver-looking token, falling back to the last line.
|
|
47
|
+
const version = (r.out.match(/\d+\.\d+\.\d+[^\s]*/) || [])[0] || r.out.trim().split('\n').pop();
|
|
48
|
+
emit('cli_ok', { version });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── 2. Reuse an existing session, or start device login ──────────────────────
|
|
52
|
+
const hasExistingSession = () => capture(WIX[0], [...WIX.slice(1), 'whoami']).status === 0;
|
|
53
|
+
|
|
54
|
+
function loginEvents() {
|
|
55
|
+
if (!existsSync(LOG)) return [];
|
|
56
|
+
return readFileSync(LOG, 'utf8')
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map((l) => l.trim())
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.map((l) => {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(l);
|
|
63
|
+
} catch {
|
|
64
|
+
return null; // non-JSON CLI chatter
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.filter((e) => e && e.event);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const alive = (pid) => {
|
|
71
|
+
try {
|
|
72
|
+
process.kill(pid, 0);
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// A login this machine started earlier may still be waiting on the browser. Reuse
|
|
80
|
+
// its code instead of minting a second one — a new login invalidates nothing, but
|
|
81
|
+
// it hands the user a different code than the one already on their screen.
|
|
82
|
+
function pendingLogin() {
|
|
83
|
+
if (!existsSync(PIDFILE)) return null;
|
|
84
|
+
let state;
|
|
85
|
+
try {
|
|
86
|
+
state = JSON.parse(readFileSync(PIDFILE, 'utf8'));
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (!state.pid || !alive(state.pid)) return null;
|
|
91
|
+
const ev = loginEvents().find((e) => e.event === 'awaiting_user');
|
|
92
|
+
if (!ev) return null;
|
|
93
|
+
// Don't hand back a code that's about to expire mid-typing.
|
|
94
|
+
const ageSeconds = Math.round((Date.now() - state.startedAt) / 1000);
|
|
95
|
+
if (ageSeconds > (ev.expiresInSeconds ?? 600) - 60) return null;
|
|
96
|
+
return ev;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Detach so the login keeps polling after this process exits. stdio goes to a
|
|
100
|
+
// file, not a pipe: a pipe dies with the parent, and the CLI would get EPIPE.
|
|
101
|
+
function startLogin() {
|
|
102
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
103
|
+
writeFileSync(LOG, '');
|
|
104
|
+
const out = openSync(LOG, 'a');
|
|
105
|
+
const child = spawn(WIX[0], [...WIX.slice(1), 'login'], {
|
|
106
|
+
detached: true,
|
|
107
|
+
stdio: ['ignore', out, out],
|
|
108
|
+
env: AGENT_ENV,
|
|
109
|
+
shell: isWin,
|
|
110
|
+
});
|
|
111
|
+
child.on('error', (e) => fail('login_failed', { detail: String(e) }));
|
|
112
|
+
writeFileSync(PIDFILE, JSON.stringify({ pid: child.pid, startedAt: Date.now() }));
|
|
113
|
+
child.unref();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function waitForCode(timeoutMs = 60000) {
|
|
117
|
+
const deadline = Date.now() + timeoutMs;
|
|
118
|
+
while (Date.now() < deadline) {
|
|
119
|
+
const events = loginEvents();
|
|
120
|
+
const ev = events.find((e) => e.event === 'awaiting_user');
|
|
121
|
+
if (ev) return ev;
|
|
122
|
+
const bad = events.find((e) => e.event === 'login_failed');
|
|
123
|
+
if (bad) fail('login_failed', bad);
|
|
124
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
125
|
+
}
|
|
126
|
+
const detail = existsSync(LOG) ? readFileSync(LOG, 'utf8').trim().slice(-1500) : '';
|
|
127
|
+
fail('login_failed', {
|
|
128
|
+
detail: detail || `wix login produced no device code within ${timeoutMs / 1000}s.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The next move is the user's, so hand the code back and exit rather than holding
|
|
133
|
+
// the caller open for a browser round-trip. `message` is the sentence to relay.
|
|
134
|
+
function surrenderTo(ev) {
|
|
135
|
+
emit('awaiting_user', {
|
|
136
|
+
...ev,
|
|
137
|
+
message:
|
|
138
|
+
`To connect your Wix account, open ${ev.verificationUri} and enter the code ` +
|
|
139
|
+
`${ev.userCode}. Tell me once you're done and I'll continue.`,
|
|
140
|
+
});
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── main ────────────────────────────────────────────────────────────────────
|
|
145
|
+
checkCli();
|
|
146
|
+
|
|
147
|
+
if (hasExistingSession()) {
|
|
148
|
+
emit('logged_in');
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const pending = pendingLogin();
|
|
153
|
+
if (pending) surrenderTo(pending);
|
|
154
|
+
|
|
155
|
+
startLogin();
|
|
156
|
+
surrenderTo(await waitForCode());
|
|
@@ -80,7 +80,7 @@ rmSync(tmp, { recursive: true, force: true });
|
|
|
80
80
|
// ---- what would act on each ------------------------------------------------------------------
|
|
81
81
|
const rel = relative(process.cwd(), SKILL_ROOT) || ".";
|
|
82
82
|
out.update = {
|
|
83
|
-
skill: `
|
|
83
|
+
skill: `update ${skillName} the way it was installed (the plugin's update, or the skills CLI's)`,
|
|
84
84
|
templates: `node ${rel}/install/templates.mjs --refresh (then commit the changed files under ${rel}/templates)`,
|
|
85
85
|
note: "neither touches src/; a shipped fix reaches the project by comparing its copy with the template",
|
|
86
86
|
};
|
|
@@ -135,19 +135,13 @@ Uses Calendar Events API to create WORKING_HOURS events on the business schedule
|
|
|
135
135
|
## CMS
|
|
136
136
|
|
|
137
137
|
### [CMS Data Items CRUD](references/cms/cms-data-items-crud.md)
|
|
138
|
-
"Add, query, update, and delete items in CMS collections
|
|
139
|
-
|
|
140
|
-
### [CMS Data Operations Extended](references/cms/cms-data-operations-extended.md)
|
|
141
|
-
Additional CMS data operations including count, upsert (bulk save), and update by filter patterns.
|
|
138
|
+
"Add, query, update, and delete items in CMS collections, one at a time or in bulk. Also covers counting items, upserting with bulk save, truncating a collection, aggregating data with a pipeline, linking items through single- and multi-reference fields, and reading items with their referenced items expanded."
|
|
142
139
|
|
|
143
140
|
### [CMS eCommerce Catalog Integration](references/cms/cms-ecommerce-catalog-integration.md)
|
|
144
141
|
The recommended way to sell existing CMS collection items (tickets, bookings, memberships) through Wix checkout. Add the CATALOG plugin to convert any CMS collection into purchasable products with cart and payment integration.
|
|
145
142
|
|
|
146
|
-
### [CMS References And Relationships](references/cms/cms-references-and-relationships.md)
|
|
147
|
-
"Add, replace, or remove items from MULTI_REFERENCE fields. Use insert-references, replace-references, remove-references endpoints. Required for managing multi-reference relationships - these CANNOT be set via regular insert/update/patch operations. Also covers single references and querying with expanded references."
|
|
148
|
-
|
|
149
143
|
### [CMS Schema Management](references/cms/cms-schema-management.md)
|
|
150
|
-
Create and modify CMS collection structures. Covers listing collections, creating collections with fields, adding/removing fields, and updating collection settings.
|
|
144
|
+
Create and modify CMS collection structures. Covers listing collections, creating collections with fields, adding/removing fields (including single- and multi-reference fields that link two collections), and updating collection settings.
|
|
151
145
|
|
|
152
146
|
### [CMS Draft & Publish Workflow (Draft Items plugin)](references/cms/cms-publishing-flow.md)
|
|
153
147
|
"Interact with CMS collections that gate their items behind a draft/publish workflow via the Draft Items plugin. Covers detecting the plugin, locating the paired drafts collection, reading published vs draft items, authoring/editing drafts, and publishing, unpublishing, reverting, and deleting items. Key endpoints: /wix-data/v2/items/publish-draft, /wix-data/v2/items/unpublish, /wix-data/v2/collections/add-draft-items-plugin, and the paired drafts collection referenced by draftItemsPluginOptions.draftsCollectionId."
|