@wix/agent-skills 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +3 -7
  2. package/package.json +1 -1
  3. package/skills/wix-app/SKILL.md +2 -0
  4. package/skills/wix-app/references/STORES_VERSIONING.md +1 -0
  5. package/skills/wix-design-system/SKILL.md +4 -0
  6. package/skills/wix-headless/SKILL.md +55 -32
  7. package/skills/wix-headless/references/BUILD-astro.md +246 -0
  8. package/skills/wix-headless/references/BUILD-own-build.md +167 -0
  9. package/skills/wix-headless/references/BUILD.md +50 -252
  10. package/skills/wix-headless/references/DESIGN_SYSTEM.md +94 -34
  11. package/skills/wix-headless/references/DISCOVERY-connect.md +55 -0
  12. package/skills/wix-headless/references/DISCOVERY-create.md +238 -0
  13. package/skills/wix-headless/references/DISCOVERY.md +41 -422
  14. package/skills/wix-headless/references/PLAN-connect.md +19 -0
  15. package/skills/wix-headless/references/PLAN-create.md +17 -0
  16. package/skills/wix-headless/references/PLAN.md +37 -27
  17. package/skills/wix-headless/references/SEED.md +27 -18
  18. package/skills/wix-headless/references/SETUP.md +32 -213
  19. package/skills/wix-headless/references/astro/blog/BLOG_PAGES.md +5 -4
  20. package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +337 -0
  21. package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +428 -0
  22. package/skills/wix-headless/references/astro/cms/CMS_FOUNDATIONS.md +5 -5
  23. package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +10 -10
  24. package/skills/wix-headless/references/astro/cms/PORTFOLIO.md +12 -13
  25. package/skills/wix-headless/references/astro/cms/RESOURCE_LIBRARY.md +10 -12
  26. package/skills/wix-headless/references/astro/cms/TEAM_DIRECTORY.md +8 -8
  27. package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +50 -27
  28. package/skills/wix-headless/references/astro/ecom/CART_PAGES.md +7 -8
  29. package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +5 -5
  30. package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +3 -3
  31. package/skills/wix-headless/references/astro/forms/CONTACT_FORM.md +5 -19
  32. package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +2 -2
  33. package/skills/wix-headless/references/astro/gift-cards/PAGES.md +1 -1
  34. package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +6 -6
  35. package/skills/wix-headless/references/astro/stores/CATEGORY_PAGES.md +4 -4
  36. package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +3 -3
  37. package/skills/wix-headless/references/astro/stores/HOME_AND_NAV.md +3 -3
  38. package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +9 -9
  39. package/skills/wix-headless/references/astro/stores/SHARED_WIRING.md +5 -4
  40. package/skills/wix-headless/references/astro/templates/Footer.astro +1 -1
  41. package/skills/wix-headless/references/astro/templates/Layout.astro +1 -1
  42. package/skills/wix-headless/references/astro/templates/Navigation.astro +1 -1
  43. package/skills/wix-headless/references/astro/templates/astro.config.mjs +1 -1
  44. package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +218 -0
  45. package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +194 -0
  46. package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +84 -0
  47. package/skills/wix-headless/references/astro/templates/bookings/SeoTags.astro +25 -0
  48. package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +50 -0
  49. package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +45 -0
  50. package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +34 -0
  51. package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +74 -0
  52. package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +111 -0
  53. package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +413 -0
  54. package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +76 -0
  55. package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +81 -0
  56. package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +65 -0
  57. package/skills/wix-headless/references/astro/templates/global.css +2 -2
  58. package/skills/wix-headless/references/astro/templates/index.astro +1 -1
  59. package/skills/wix-headless/references/blog/BLOG_CONTENT.md +19 -9
  60. package/skills/wix-headless/references/blog/INSTRUCTIONS.md +3 -3
  61. package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +106 -0
  62. package/skills/wix-headless/references/bookings/SERVICES_DATA.md +295 -0
  63. package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +4 -2
  64. package/skills/wix-headless/references/cms/INSTRUCTIONS.md +27 -1
  65. package/skills/wix-headless/references/commands/known-apps.json +2 -1
  66. package/skills/wix-headless/references/custom/CONNECTION_PLAN.md +142 -0
  67. package/skills/wix-headless/references/custom/INSTRUCTIONS.md +71 -21
  68. package/skills/wix-headless/references/custom/blog/WIRING.md +52 -0
  69. package/skills/wix-headless/references/custom/bookings/WIRING.md +88 -0
  70. package/skills/wix-headless/references/custom/cms/WIRING.md +97 -0
  71. package/skills/wix-headless/references/custom/ecom/WIRING.md +67 -0
  72. package/skills/wix-headless/references/custom/forms/WIRING.md +101 -0
  73. package/skills/wix-headless/references/custom/stores/WIRING.md +80 -0
  74. package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +5 -5
  75. package/skills/wix-headless/references/forms/INSTRUCTIONS.md +2 -2
  76. package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +3 -3
  77. package/skills/wix-headless/references/images/INSTRUCTIONS.md +16 -15
  78. package/skills/wix-headless/references/seed-recipes.md +2 -2
  79. package/skills/wix-headless/references/shared/AGENTS_MD.md +47 -0
  80. package/skills/wix-headless/references/shared/AUTHENTICATION.md +3 -5
  81. package/skills/wix-headless/references/shared/DESIGN_MD.md +85 -0
  82. package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +29 -28
  83. package/skills/wix-headless/references/shared/IMPLEMENTER.md +87 -27
  84. package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +2 -2
  85. package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +24 -313
  86. package/skills/wix-headless/references/shared/STYLING.md +8 -18
  87. package/skills/wix-headless/references/stores/INSTRUCTIONS.md +39 -9
  88. package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +17 -14
  89. package/skills/wix-headless/references/verticals/_schema.json +1 -1
  90. package/skills/wix-headless/references/verticals/_schema.md +3 -3
  91. package/skills/wix-headless/references/verticals/blog.md +1 -1
  92. package/skills/wix-headless/references/verticals/bookings.md +41 -0
  93. package/skills/wix-headless/references/verticals/cms.md +1 -1
  94. package/skills/wix-headless/references/verticals/ecom.md +2 -2
  95. package/skills/wix-headless/references/verticals/gift-cards.md +1 -1
  96. package/skills/wix-headless/references/verticals/stores.md +1 -1
  97. package/skills/wix-headless/scripts/check-manifest.mjs +143 -6
  98. package/skills/wix-headless/scripts/compose.mjs +540 -0
  99. package/skills/wix-headless/scripts/emit-design-tokens.mjs +128 -51
  100. package/skills/wix-headless/scripts/flatten.sh +61 -0
  101. package/skills/wix-headless/scripts/generate-images.mjs +242 -0
  102. package/skills/wix-headless/scripts/scaffold.sh +47 -16
  103. package/skills/wix-headless/scripts/write-decorative-json.mjs +62 -0
  104. package/skills/wix-manage/SKILL.md +1 -1
  105. package/skills/wix-manage/references/blog/how-to-create-blog-posts.md +1 -1
  106. package/skills/wix-manage/references/cms/cms-data-items-crud.md +1 -1
  107. package/skills/wix-manage/references/cms/cms-references-and-relationships.md +1 -1
  108. package/skills/wix-manage/references/cms/cms-schema-management.md +1 -1
  109. package/skills/wix-manage/references/domains/domain-search-and-purchase.md +10 -7
  110. package/skills/wix-manage/references/forms/create-form.md +49 -1
  111. package/skills/wix-manage/references/stores/bulk-create-products-with-options.md +1 -1
  112. package/skills/wix-manage/references/stores/setup-online-store-catalog-v3.md +1 -1
  113. package/skills/wix-headless/references/astro/COMPOSE.md +0 -142
  114. package/skills/wix-headless/scripts/finalize-run-json.mjs +0 -141
  115. package/skills/wix-headless/scripts/init-site-json.mjs +0 -101
  116. package/skills/wix-headless/scripts/preview.sh +0 -45
  117. package/skills/wix-headless/scripts/release.sh +0 -73
@@ -0,0 +1,540 @@
1
+ #!/usr/bin/env node
2
+ // ── compose.mjs — the deterministic Composer (astro design-system phase) ──────
3
+ //
4
+ // Replaces the former `design-system-composer` LLM subagent (this script is now
5
+ // the sole spec). The Composer's job is MECHANICAL: take the Designer's framework-
6
+ // agnostic spec + the application inputs and SUBSTITUTE them into six pinned
7
+ // skeletons at references/astro/templates/. There is no judgment in re-emitting
8
+ // the fixed bulk (View-Transitions script, @utility btn family, view-transition
9
+ // CSS), so an LLM is pure cost + run-to-run variance. This script does it in
10
+ // sub-second, byte-reproducibly.
11
+ //
12
+ // It is a sibling of emit-design-tokens.mjs / patch-decorative-slots.mjs and,
13
+ // like them, reads its input as a JSON object on stdin and takes the project
14
+ // dir as argv[2]. It locates the skeletons relative to its own path (no
15
+ // SKILL_ROOT needed).
16
+ //
17
+ // Usage:
18
+ // node compose.mjs <project-dir> <<'JSON'
19
+ // { "shell": {...}, "brand": {...}, "navLinks": [...],
20
+ // "loadedPacks": [...], "packsWithComponents": [...], "disabledPacks": [...] }
21
+ // JSON
22
+ //
23
+ // Tokens come from DESIGN.md — the single design format (no inline token JSON).
24
+ // compose reads `<project-dir>/DESIGN.md` (or `designMdPath`), parses its
25
+ // FRONTMATTER ONLY (the markdown body is documentation, never read), and applies
26
+ // a role-translation table (references/shared/DESIGN_MD.md) so a DESIGN.md
27
+ // authored with standard roles (primary/surface/on-surface/…) maps onto the wix
28
+ // `--color-*` vocabulary; wix-native keys pass through losslessly. `rounded` →
29
+ // radii; a custom `containers` group + a `googleFontsHref` key are honored.
30
+ // The Designer authors that DESIGN.md directly; emit-design-tokens.mjs projects
31
+ // the token CSS/types from it. DESIGN.md exists on disk before compose runs.
32
+ //
33
+ // stdin JSON (the application inputs — NOT the design tokens, which live in DESIGN.md):
34
+ // - designMdPath — optional path to the DESIGN.md (default <project-dir>/DESIGN.md).
35
+ // - shell — { heroHeadline, heroSub, footerTagline, navBrandMark }.
36
+ // - brand — { name, description }.
37
+ // - navLinks — [ { href, label } ]; labels used VERBATIM.
38
+ // - loadedPacks — string[] of loaded vertical packs.
39
+ // - packsWithComponents — string[]; one components-<pack>.css import per entry,
40
+ // in order (COMPOSE § Layout.astro).
41
+ // - disabledPacks — string[]; dormant packs still get their home/nav markers,
42
+ // never a visible entry point.
43
+ //
44
+ // What it writes (the 6 design-system files), by substituting {{…}} slots into
45
+ // the pinned skeletons — the fixed bulk is copied byte-for-byte:
46
+ // 1. src/styles/global.css — {{theme}} ← @theme palette
47
+ // 2. astro.config.mjs — MERGE (anchored codemod, not clobber)
48
+ // 3. src/layouts/Layout.astro — imports, fonts href, brand title
49
+ // 4. src/components/Navigation.astro — brand mark, nav links
50
+ // 5. src/components/Footer.astro — brand, tagline, nav links
51
+ // 6. src/pages/index.astro — hero copy, brand, home markers
52
+ //
53
+ // Token contract: guarantees the required-token set (STYLING.md § "Required
54
+ // tokens — the component-CSS template contract") resolves in @theme. Missing
55
+ // roles are DERIVED as a fail-safe (the Designer is expected to emit a complete
56
+ // set + a googleFontsHref; derivation is a safety net, not the hot path).
57
+ //
58
+ // Output: a single manifest JSON object on stdout (the orchestrator parses it
59
+ // the same way it parsed the subagent's return — same { status, phase, data,
60
+ // files } shape). Diagnostics go to stderr. Exit 0 on complete, 1 on a hard
61
+ // failure (missing scaffold / unrecoverable token gap / missing config anchor).
62
+ //
63
+ // astro only. Custom (non-astro) frontends never reach the Composer — the
64
+ // orchestrator only invokes this when frontend === "astro".
65
+
66
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
67
+ import { join, dirname, resolve, isAbsolute } from "node:path";
68
+ import { fileURLToPath } from "node:url";
69
+ import { execSync } from "node:child_process";
70
+
71
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
72
+ const TEMPLATES = resolve(SCRIPT_DIR, "..", "references", "astro", "templates");
73
+
74
+ const projectDir = process.argv[2] ?? process.cwd();
75
+
76
+ function die(code, message) {
77
+ // Emit a structured failure manifest on stdout so the orchestrator parses a
78
+ // return either way, plus a human line on stderr. Hard-fail with exit 1.
79
+ console.error(`compose: ${message}`);
80
+ console.log(JSON.stringify({ status: "failed", phase: "compose", data: {}, files: [], errors: [{ code, message }] }, null, 2));
81
+ process.exit(1);
82
+ }
83
+
84
+ // ── read + validate input ─────────────────────────────────────────────────────
85
+ if (process.stdin.isTTY) die("NO_INPUT", "expected the compose input JSON on stdin");
86
+ let input;
87
+ try {
88
+ const raw = readFileSync(0, "utf8").trim();
89
+ if (!raw) die("EMPTY_INPUT", "stdin was empty — pass the compose input JSON object");
90
+ input = JSON.parse(raw);
91
+ } catch (e) {
92
+ die("BAD_JSON", `stdin is not valid JSON (${e.message})`);
93
+ }
94
+
95
+ // ── token source: DESIGN.md frontmatter (portable) ───────────────────────────
96
+ // Minimal frontmatter parser for the restricted DESIGN.md shape (groups of
97
+ // `key: scalar`, plus typography tokens with an indented `fontFamily:`). Not a
98
+ // general YAML parser — the body is never read. Color values are quoted in the
99
+ // emitted DESIGN.md because an unquoted `#hex` after `: ` is a YAML comment.
100
+ function unquote(v) {
101
+ v = v.trim();
102
+ const qc = v[0];
103
+ if (qc === '"' || qc === "'") {
104
+ // Return the quoted content up to the closing quote (handles a trailing
105
+ // ` # comment` after the closing quote, and \" / \\ escapes in "…").
106
+ let out = "";
107
+ for (let i = 1; i < v.length; i++) {
108
+ const c = v[i];
109
+ if (qc === '"' && c === "\\" && i + 1 < v.length) { out += v[++i]; continue; }
110
+ if (c === qc) break;
111
+ out += c;
112
+ }
113
+ return out;
114
+ }
115
+ const hash = v.search(/\s#/); // strip trailing inline comment on bare scalars
116
+ if (hash !== -1) v = v.slice(0, hash).trim();
117
+ return v;
118
+ }
119
+ // Parse a flow-style inline object — `{ fontFamily: "X", fontWeight: 600 }` —
120
+ // splitting top-level commas while respecting quotes (so `"Helvetica, Arial"`
121
+ // stays one value). Used for typography tokens authored in flow style.
122
+ function parseInlineObject(s) {
123
+ const inner = s.trim().replace(/^\{/, "").replace(/\}$/, "");
124
+ const obj = {};
125
+ let buf = "", inq = null;
126
+ const parts = [];
127
+ for (const ch of inner) {
128
+ if (inq) { if (ch === inq) inq = null; buf += ch; continue; }
129
+ if (ch === '"' || ch === "'") { inq = ch; buf += ch; continue; }
130
+ if (ch === ",") { parts.push(buf); buf = ""; continue; }
131
+ buf += ch;
132
+ }
133
+ if (buf.trim()) parts.push(buf);
134
+ for (const p of parts) {
135
+ const ci = p.indexOf(":");
136
+ if (ci === -1) continue;
137
+ obj[p.slice(0, ci).trim()] = unquote(p.slice(ci + 1));
138
+ }
139
+ return obj;
140
+ }
141
+ function parseFrontmatter(text) {
142
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
143
+ if (!m) return null;
144
+ const root = {};
145
+ let group = null, sub = null;
146
+ for (const raw of m[1].split(/\r?\n/)) {
147
+ if (!raw.trim() || /^\s*#/.test(raw)) continue;
148
+ const indent = raw.match(/^ */)[0].length;
149
+ const ci = raw.indexOf(":");
150
+ if (ci === -1) continue;
151
+ const key = raw.slice(0, ci).trim();
152
+ const rest = raw.slice(ci + 1).trim();
153
+ if (indent === 0) {
154
+ if (rest === "") { root[key] = {}; group = key; sub = null; }
155
+ else if (rest.startsWith("{")) { root[key] = parseInlineObject(rest); group = null; sub = null; }
156
+ else { root[key] = unquote(rest); group = null; sub = null; }
157
+ } else if (indent === 2 && group) {
158
+ if (rest === "") { root[group][key] = {}; sub = key; }
159
+ else if (rest.startsWith("{")) { root[group][key] = parseInlineObject(rest); sub = null; }
160
+ else { root[group][key] = unquote(rest); sub = null; }
161
+ } else if (indent >= 4 && group && sub) {
162
+ if (typeof root[group][sub] !== "object") root[group][sub] = {};
163
+ root[group][sub][key] = unquote(rest);
164
+ }
165
+ }
166
+ return root;
167
+ }
168
+ // Standard DESIGN.md color roles → wix editorial `--color-*` names. wix-native
169
+ // keys (paper/ink/accent/…) are NOT in this table and pass through unchanged,
170
+ // winning on any conflict — so our own emission is lossless.
171
+ const COLOR_ROLE_TABLE = {
172
+ primary: "accent", secondary: "paper-warm", tertiary: "ink-soft", neutral: "mute",
173
+ surface: "paper", "on-surface": "ink", background: "paper", "on-background": "ink",
174
+ outline: "rule", error: "error",
175
+ };
176
+ function designMdToTokens(fm) {
177
+ // Single pass preserving source key order: each key maps via the role table
178
+ // (standard DESIGN.md role → wix name) or passes through unchanged (wix-native
179
+ // keys aren't in the table). Order-preserving so our own round-trip is
180
+ // byte-identical to the JSON path. (A file mixing a standard role and its wix
181
+ // synonym — e.g. both `surface` and `paper` — is pathological; last wins.)
182
+ const colors = {};
183
+ for (const [k, v] of Object.entries(fm.colors ?? {})) colors[COLOR_ROLE_TABLE[k] ?? k] = v;
184
+ const typ = fm.typography ?? {};
185
+ const famOf = (...keys) => { for (const key of keys) if (typ[key] && typ[key].fontFamily) return typ[key].fontFamily; return undefined; };
186
+ const fonts = {};
187
+ const disp = famOf("display", "h1", "heading", "title");
188
+ const body = famOf("body", "p", "text", "base", "paragraph");
189
+ if (disp) fonts.display = disp;
190
+ if (body) fonts.body = body;
191
+ if (typ.mono && typ.mono.fontFamily) fonts.mono = typ.mono.fontFamily;
192
+ return {
193
+ colors,
194
+ fonts,
195
+ spacing: fm.spacing ?? {},
196
+ radii: fm.rounded ?? {}, // DESIGN.md calls corner radii `rounded`
197
+ containers: fm.containers ?? {}, // custom group
198
+ ...(typeof fm.googleFontsHref === "string" ? { googleFontsHref: fm.googleFontsHref } : {}),
199
+ };
200
+ }
201
+
202
+ // DESIGN.md is the single token source — read it, parse frontmatter only.
203
+ const designMdRel = input.designMdPath ?? "DESIGN.md";
204
+ const designMdPath = isAbsolute(designMdRel) ? designMdRel : join(projectDir, designMdRel);
205
+ if (!existsSync(designMdPath)) die("DESIGN_MD_MISSING", `no DESIGN.md at ${designMdPath} — the Designer must author it first`);
206
+ const fm = parseFrontmatter(readFileSync(designMdPath, "utf8"));
207
+ if (!fm) die("DESIGN_MD_BAD", `could not parse YAML frontmatter from ${designMdPath}`);
208
+ const designTokens = designMdToTokens(fm);
209
+ const shell = input.shell ?? {};
210
+ const brand = input.brand ?? {};
211
+ const navLinks = Array.isArray(input.navLinks) ? input.navLinks : [];
212
+ const loadedPacks = Array.isArray(input.loadedPacks) ? input.loadedPacks : [];
213
+ const packsWithComponents = Array.isArray(input.packsWithComponents) ? input.packsWithComponents : [];
214
+ const disabledPacks = Array.isArray(input.disabledPacks) ? input.disabledPacks : [];
215
+
216
+ const brandName = brand.name ?? "Brand";
217
+
218
+ // ── small color helper (derivation fail-safe only) ────────────────────────────
219
+ function parseHex(hex) {
220
+ if (typeof hex !== "string") return null;
221
+ let h = hex.trim().replace(/^#/, "");
222
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
223
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
224
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
225
+ }
226
+ function toHex([r, g, b]) {
227
+ return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")).join("");
228
+ }
229
+ // mix `a` toward `b` by weight w (0..1). Falls back to `a` if either unparseable.
230
+ function mix(a, b, w) {
231
+ const ca = parseHex(a), cb = parseHex(b);
232
+ if (!ca || !cb) return a ?? b ?? "#000000";
233
+ return toHex(ca.map((v, i) => v + (cb[i] - v) * w));
234
+ }
235
+
236
+ // ── build the @theme palette + guarantee the required-token contract ──────────
237
+ const SYSTEM_FONTS = new Set([
238
+ "system-ui", "-apple-system", "blinkmacsystemfont", "segoe ui", "sans-serif",
239
+ "serif", "monospace", "ui-sans-serif", "ui-serif", "ui-monospace", "inherit",
240
+ ]);
241
+
242
+ const colors = { ...(designTokens.colors ?? {}) };
243
+ const fonts = { ...(designTokens.fonts ?? {}) };
244
+ const spacing = { ...(designTokens.spacing ?? {}) };
245
+ const radii = { ...(designTokens.radii ?? {}) };
246
+ const containers = { ...(designTokens.containers ?? {}) };
247
+
248
+ const derived = [];
249
+ function ensure(group, key, value, prefix) {
250
+ if (group[key] === undefined || group[key] === null || group[key] === "") {
251
+ group[key] = value;
252
+ derived.push(`${prefix}${key}`);
253
+ }
254
+ }
255
+
256
+ // Colors — required core first (sane neutral defaults if the Designer omitted a
257
+ // core role), then warm/soft derivations described in COMPOSE § Token contract.
258
+ ensure(colors, "paper", "#ffffff", "--color-");
259
+ ensure(colors, "ink", "#1a1a1a", "--color-");
260
+ ensure(colors, "accent", colors.ink, "--color-");
261
+ ensure(colors, "mute", mix(colors.ink, colors.paper, 0.45), "--color-");
262
+ ensure(colors, "rule", mix(colors.ink, colors.paper, 0.85), "--color-");
263
+ ensure(colors, "paper-warm", mix(colors.paper, colors.accent, 0.06), "--color-"); // paper warmed
264
+ ensure(colors, "ink-soft", mix(colors.ink, colors.paper, 0.25), "--color-"); // ink lightened
265
+ // NOTE: cream is derived but not in REQUIRED; gift-cards CSS expects
266
+ // --color-cream-deep (undeclared) and relies on the var() fallback — reconcile
267
+ // with STYLING.md token contract.
268
+ ensure(colors, "cream", mix(colors.paper, "#fbf3e0", 0.5), "--color-");
269
+ ensure(colors, "error", "#c0392b", "--color-");
270
+
271
+ // Fonts — display + body required.
272
+ ensure(fonts, "display", "Georgia, serif", "--font-");
273
+ ensure(fonts, "body", "system-ui, sans-serif", "--font-");
274
+
275
+ // Spacing — full 2xs..4xl scale.
276
+ const SPACING_DEFAULTS = {
277
+ "2xs": "0.25rem", xs: "0.5rem", sm: "0.75rem", md: "1rem", lg: "1.5rem",
278
+ xl: "2rem", "2xl": "3rem", "3xl": "4rem", "4xl": "6rem",
279
+ };
280
+ for (const [k, v] of Object.entries(SPACING_DEFAULTS)) ensure(spacing, k, v, "--spacing-");
281
+
282
+ // Radii — sm + md required.
283
+ ensure(radii, "sm", "0.25rem", "--radius-");
284
+ ensure(radii, "md", "0.5rem", "--radius-");
285
+
286
+ // Containers — content widths, a separate axis from spacing.
287
+ const CONTAINER_DEFAULTS = { prose: "42rem", md: "48rem", "3xl": "60rem", "6xl": "72rem" };
288
+ for (const [k, v] of Object.entries(CONTAINER_DEFAULTS)) ensure(containers, k, v, "--container-");
289
+
290
+ // Assemble the @theme block, grouped + ordered for readability.
291
+ const themeLines = [];
292
+ const emitGroup = (prefix, obj) => {
293
+ for (const [k, v] of Object.entries(obj)) themeLines.push(` ${prefix}${k}: ${v};`);
294
+ };
295
+ emitGroup("--color-", colors);
296
+ themeLines.push("");
297
+ emitGroup("--font-", fonts);
298
+ themeLines.push("");
299
+ emitGroup("--spacing-", spacing);
300
+ themeLines.push("");
301
+ emitGroup("--radius-", radii);
302
+ themeLines.push("");
303
+ emitGroup("--container-", containers);
304
+ const themeBlock = themeLines.join("\n");
305
+
306
+ // ── self-checks (assertions) ──────────────────────────────────────────────────
307
+ const errors = [];
308
+
309
+ // Required-token coverage (after derivation, every required role must resolve).
310
+ const REQUIRED = [
311
+ ...["paper", "paper-warm", "ink", "mute", "rule", "accent"].map((k) => `--color-${k}`),
312
+ "--font-display", "--font-body",
313
+ ...["2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl"].map((k) => `--spacing-${k}`),
314
+ "--radius-sm", "--radius-md",
315
+ ...["prose", "md", "3xl", "6xl"].map((k) => `--container-${k}`),
316
+ ];
317
+ const present = new Set([
318
+ ...Object.keys(colors).map((k) => `--color-${k}`),
319
+ ...Object.keys(fonts).map((k) => `--font-${k}`),
320
+ ...Object.keys(spacing).map((k) => `--spacing-${k}`),
321
+ ...Object.keys(radii).map((k) => `--radius-${k}`),
322
+ ...Object.keys(containers).map((k) => `--container-${k}`),
323
+ ]);
324
+ for (const tok of REQUIRED) {
325
+ if (!present.has(tok)) errors.push({ code: "MISSING_REQUIRED_TOKEN", token: tok });
326
+ }
327
+
328
+ // Container ≠ spacing: a --container-* value must never equal a --spacing-* value.
329
+ const spacingValues = new Set(Object.values(spacing));
330
+ for (const [k, v] of Object.entries(containers)) {
331
+ if (spacingValues.has(v)) errors.push({ code: "CONTAINER_EQUALS_SPACING", token: `--container-${k}`, value: v });
332
+ }
333
+
334
+ // ── skeleton load + substitution helpers ──────────────────────────────────────
335
+ function readTemplate(name) {
336
+ const p = join(TEMPLATES, name);
337
+ if (!existsSync(p)) die("TEMPLATE_MISSING", `skeleton not found at ${p}`);
338
+ return readFileSync(p, "utf8");
339
+ }
340
+ function writeProject(rel, content) {
341
+ const dest = join(projectDir, rel);
342
+ mkdirSync(dirname(dest), { recursive: true });
343
+ writeFileSync(dest, content);
344
+ }
345
+ function escAttr(s) {
346
+ return String(s ?? "").replace(/"/g, "&quot;");
347
+ }
348
+ // Strip the leading "// ── Composer skeleton: … ──" authoring-comment block from
349
+ // an .astro skeleton's frontmatter. Those lines document the {{…}} slots (and
350
+ // thus contain placeholder text that must NOT be substituted) and are not part
351
+ // of the shipped site — the real Composer output drops them too. Removes the
352
+ // contiguous run of `//` comment lines immediately after the opening `---`.
353
+ function stripAstroHeader(src) {
354
+ return src.replace(/^(---\n)((?:[ \t]*\/\/.*\n)+)/, "$1");
355
+ }
356
+
357
+ // ── Google Fonts href ─────────────────────────────────────────────────────────
358
+ // Prefer the Designer-supplied href; otherwise build one deterministically with
359
+ // a standard weight set. Returns ""
360
+ // when both families are system fonts (caller drops the <link>).
361
+ function buildGoogleHref() {
362
+ if (typeof designTokens.googleFontsHref === "string" && designTokens.googleFontsHref.trim()) {
363
+ return designTokens.googleFontsHref.trim();
364
+ }
365
+ const families = [];
366
+ for (const fam of [fonts.display, fonts.body]) {
367
+ if (typeof fam !== "string") continue;
368
+ const first = fam.split(",")[0].trim();
369
+ if (!first || SYSTEM_FONTS.has(first.toLowerCase())) continue;
370
+ if (!families.includes(first)) families.push(first);
371
+ }
372
+ if (families.length === 0) return "";
373
+ const q = families
374
+ .map((f) => `family=${f.replace(/\s+/g, "+")}:wght@400;500;600;700`)
375
+ .join("&");
376
+ return `https://fonts.googleapis.com/css2?${q}&display=swap`;
377
+ }
378
+
379
+ // ── 1. global.css ──────────────────────────────────────────────────────────────
380
+ {
381
+ const skeleton = readTemplate("global.css");
382
+ // Replace the first `@theme { … }` block (its comment has no braces, so the
383
+ // first `}` is its close). Everything else is literal fixed bulk.
384
+ const out = skeleton.replace(/@theme\s*\{[\s\S]*?\n\}/, `@theme {\n${themeBlock}\n}`);
385
+ if (out === skeleton) die("ANCHOR_THEME", "could not locate the @theme block in global.css skeleton");
386
+ writeProject("src/styles/global.css", out);
387
+ }
388
+
389
+ // ── 2. astro.config.mjs (anchored MERGE, fail loud on missing anchor) ─────────
390
+ {
391
+ const dest = join(projectDir, "astro.config.mjs");
392
+ // The scaffold may briefly be in flight; the new wiring invokes compose only
393
+ // after Setup Step 1 awaited the scaffold, so a short retry is belt-and-braces.
394
+ let src = null;
395
+ for (let attempt = 0; attempt < 10; attempt++) {
396
+ if (existsSync(dest)) { src = readFileSync(dest, "utf8"); break; }
397
+ try { execSync("sleep 1"); } catch { /* ignore */ }
398
+ }
399
+ if (src === null) die("SCAFFOLD_NOT_COMPLETE", `astro.config.mjs not found at ${dest} after retries`);
400
+
401
+ const hasImport = /import\s+tailwindcss\s+from\s+["']@tailwindcss\/vite["']/.test(src);
402
+ const hasCall = /tailwindcss\s*\(\s*\)/.test(src);
403
+
404
+ // (1a) import — insert after the last top-level import statement.
405
+ if (!hasImport) {
406
+ const importRe = /^import\b.*$/gm;
407
+ let last = null, m;
408
+ while ((m = importRe.exec(src)) !== null) last = m;
409
+ if (!last) die("ANCHOR_IMPORT", "no import statement found in astro.config.mjs to anchor the @tailwindcss/vite import");
410
+ const insertAt = last.index + last[0].length;
411
+ src = src.slice(0, insertAt) + `\nimport tailwindcss from "@tailwindcss/vite";` + src.slice(insertAt);
412
+ }
413
+
414
+ // (1b) merge tailwindcss() into vite.plugins (idempotent on hasCall).
415
+ if (!hasCall) {
416
+ if (/plugins\s*:\s*\[/.test(src)) {
417
+ src = src.replace(/plugins\s*:\s*\[/, (mm) => `${mm}tailwindcss(), `);
418
+ } else if (/\bvite\s*:\s*\{/.test(src)) {
419
+ src = src.replace(/(\bvite\s*:\s*\{)/, `$1\n plugins: [tailwindcss()],`);
420
+ } else if (/defineConfig\s*\(\s*\{/.test(src)) {
421
+ src = src.replace(/(defineConfig\s*\(\s*\{)/, `$1\n vite: { plugins: [tailwindcss()] },`);
422
+ } else {
423
+ die("ANCHOR_VITE", "could not find vite.plugins, a vite block, or defineConfig({ to register @tailwindcss/vite");
424
+ }
425
+ }
426
+
427
+ // (2) process.env → globalThis guard (TS-safe under strict tsc --noEmit).
428
+ const GUARD = `const isBuild =\n (/** @type {any} */ (globalThis)).process?.env?.NODE_ENV === "production";`;
429
+ const alreadyGuarded = /\(globalThis\)\)\.process\?\.env\?\.NODE_ENV/.test(src);
430
+ const bareRe = /const\s+isBuild\s*=\s*process\.env\.NODE_ENV\s*===?\s*["']production["']\s*;/;
431
+ if (!alreadyGuarded) {
432
+ if (bareRe.test(src)) {
433
+ src = src.replace(bareRe, GUARD);
434
+ } else if (/process\.env/.test(src)) {
435
+ die("ANCHOR_PROCESS_ENV", "found a bare process.env reference but not the expected `const isBuild = process.env.NODE_ENV …` line to guard");
436
+ }
437
+ // else: no process.env at all — nothing to guard, leave as-is.
438
+ }
439
+
440
+ writeProject("astro.config.mjs", src);
441
+ }
442
+
443
+ // ── 3. Layout.astro ────────────────────────────────────────────────────────────
444
+ const componentCssImports = packsWithComponents.map(
445
+ (pack) => `import '../styles/components-${pack}.css';`,
446
+ );
447
+ {
448
+ let out = stripAstroHeader(readTemplate("Layout.astro"));
449
+ // {{components-css-imports}} — one import per pack, or drop the whole line.
450
+ if (componentCssImports.length) {
451
+ out = out.replace(/^\{\{components-css-imports\}\}\s*$/m, componentCssImports.join("\n"));
452
+ } else {
453
+ out = out.replace(/^\{\{components-css-imports\}\}\s*\n/m, "");
454
+ }
455
+ // {{fonts.googleHref}} — drop the <link> when no web fonts.
456
+ const href = buildGoogleHref();
457
+ if (href) {
458
+ out = out.replace(/\{\{fonts\.googleHref\}\}/g, escAttr(href));
459
+ } else {
460
+ out = out.replace(/^.*\{\{fonts\.googleHref\}\}.*\n/m, "");
461
+ }
462
+ out = out.replaceAll("{{brand.name}}", brandName);
463
+ writeProject("src/layouts/Layout.astro", out);
464
+ }
465
+
466
+ // ── nav-link rendering ──────────────────────────────────────────────────────────
467
+ const navItems = navLinks
468
+ .filter((l) => l && l.href != null && l.label != null)
469
+ .map((l) => ({ href: String(l.href), label: String(l.label) }));
470
+
471
+ // ── 4. Navigation.astro ──────────────────────────────────────────────────────────
472
+ {
473
+ let out = stripAstroHeader(readTemplate("Navigation.astro"));
474
+ out = out.replaceAll("{{shell.navBrandMark}}", shell.navBrandMark ?? brandName);
475
+ const links = navItems
476
+ .map((l) => ` <li class="site-nav-item"><a href="${escAttr(l.href)}">${l.label}</a></li>`)
477
+ .join("\n");
478
+ out = out.replace(/^\s*\{\{nav\.links\}\}\s*$/m, links);
479
+ writeProject("src/components/Navigation.astro", out);
480
+ }
481
+
482
+ // ── 5. Footer.astro ──────────────────────────────────────────────────────────────
483
+ {
484
+ let out = stripAstroHeader(readTemplate("Footer.astro"));
485
+ out = out.replaceAll("{{brand.name}}", brandName);
486
+ out = out.replaceAll("{{shell.footerTagline}}", shell.footerTagline ?? "");
487
+ const links = navItems
488
+ .map((l) => ` <li><a href="${escAttr(l.href)}">${l.label}</a></li>`)
489
+ .join("\n");
490
+ out = out.replace(/^\s*\{\{nav\.links\}\}\s*$/m, links);
491
+ writeProject("src/components/Footer.astro", out);
492
+ }
493
+
494
+ // ── 6. index.astro ────────────────────────────────────────────────────────────────
495
+ // Home markers: one `<!-- home:<pack> -->` per contributing pack. Today
496
+ // stores + bookings + gift-cards contribute a home section. Disabled packs
497
+ // (gift-cards) still get their marker (markers are their only acceptable touchpoint).
498
+ const HOME_CONTRIBUTING = ["stores", "bookings", "gift-cards"]; // canonical order
499
+ const homePool = new Set([...loadedPacks, ...disabledPacks]);
500
+ const homeMarkerPacks = HOME_CONTRIBUTING.filter((p) => homePool.has(p));
501
+ {
502
+ let out = stripAstroHeader(readTemplate("index.astro"));
503
+ out = out.replaceAll("{{shell.heroHeadline}}", shell.heroHeadline ?? brandName);
504
+ out = out.replaceAll("{{shell.heroSub}}", shell.heroSub ?? "");
505
+ out = out.replaceAll("{{brand.name}}", brandName);
506
+ const markers = homeMarkerPacks.map((p) => ` <!-- home:${p} -->`).join("\n");
507
+ out = out.replace(/^\s*\{\{home-markers\}\}\s*$/m, markers);
508
+ writeProject("src/pages/index.astro", out);
509
+ }
510
+
511
+ // ── manifest (the orchestrator parses this off stdout) ────────────────────────
512
+ const filesWritten = [
513
+ "src/styles/global.css",
514
+ "astro.config.mjs",
515
+ "src/layouts/Layout.astro",
516
+ "src/components/Navigation.astro",
517
+ "src/components/Footer.astro",
518
+ "src/pages/index.astro",
519
+ ];
520
+ const manifest = {
521
+ status: errors.length ? "partial" : "complete",
522
+ phase: "compose",
523
+ data: {
524
+ filesWritten,
525
+ componentCssImports: packsWithComponents,
526
+ homeMarkers: homeMarkerPacks.map((p) => `home:${p}`),
527
+ tokensApplied: {
528
+ colors: Object.keys(colors).length,
529
+ spacing: Object.keys(spacing).length,
530
+ containers: Object.keys(containers).length,
531
+ radii: Object.keys(radii).length,
532
+ fonts: Object.keys(fonts).length,
533
+ },
534
+ ...(derived.length ? { derivedTokens: derived } : {}),
535
+ },
536
+ files: filesWritten,
537
+ ...(errors.length ? { errors } : {}),
538
+ };
539
+ console.log(JSON.stringify(manifest, null, 2));
540
+ process.exit(0);