create-ailk 0.8.0 → 0.9.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 (211) hide show
  1. package/README.md +68 -0
  2. package/component-catalog.json +279 -45
  3. package/dist/add-module.d.ts +20 -0
  4. package/dist/add-module.js +43 -7
  5. package/dist/add-route.d.ts +56 -0
  6. package/dist/add-route.js +286 -0
  7. package/dist/apply-design-md.d.ts +7 -0
  8. package/dist/apply-design-md.js +50 -0
  9. package/dist/apply-design.d.ts +64 -0
  10. package/dist/apply-design.js +273 -0
  11. package/dist/claude-md.d.ts +111 -0
  12. package/dist/claude-md.js +334 -0
  13. package/dist/cli.js +92 -4
  14. package/dist/derive-architecture.js +41 -36
  15. package/dist/design-compile.d.ts +71 -0
  16. package/dist/design-compile.js +98 -0
  17. package/dist/design-package.d.ts +83 -0
  18. package/dist/design-package.js +468 -0
  19. package/dist/design-presets.d.ts +86 -0
  20. package/dist/design-presets.js +642 -0
  21. package/dist/design-scaffold.d.ts +76 -0
  22. package/dist/design-scaffold.js +220 -0
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.js +4 -1
  25. package/dist/legacy-nav-renderers.d.ts +45 -0
  26. package/dist/legacy-nav-renderers.js +545 -0
  27. package/dist/lib/auth-secret-file.d.ts +19 -0
  28. package/dist/lib/auth-secret-file.js +112 -0
  29. package/dist/lib/design-checks.d.ts +78 -0
  30. package/dist/lib/design-checks.js +174 -0
  31. package/dist/lib/license-key-files.d.ts +9 -0
  32. package/dist/lib/license-key-files.js +2 -2
  33. package/dist/lib/module-license-gate.d.ts +19 -9
  34. package/dist/lib/module-license-gate.js +19 -8
  35. package/dist/lib/render-verdict.d.ts +52 -0
  36. package/dist/lib/render-verdict.js +77 -0
  37. package/dist/lib/scaffold-render-sweep.d.ts +105 -0
  38. package/dist/lib/scaffold-render-sweep.js +236 -0
  39. package/dist/module-architecture.d.ts +19 -16
  40. package/dist/module-architecture.js +64 -21
  41. package/dist/parse-args.d.ts +12 -2
  42. package/dist/parse-args.js +74 -13
  43. package/dist/programmatic.d.ts +30 -6
  44. package/dist/programmatic.js +206 -17
  45. package/dist/site-chrome.d.ts +19 -9
  46. package/dist/site-chrome.js +23 -13
  47. package/dist/site-manifest.d.ts +25 -0
  48. package/dist/site-manifest.js +254 -0
  49. package/dist/site-roster.d.ts +41 -0
  50. package/dist/site-roster.js +141 -8
  51. package/dist/starter-grid.d.ts +32 -16
  52. package/dist/starter-grid.js +129 -33
  53. package/dist/surfaces.d.ts +41 -9
  54. package/dist/surfaces.js +200 -83
  55. package/dist/template-manifest.d.ts +30 -13
  56. package/dist/template-manifest.js +49 -24
  57. package/package.json +5 -4
  58. package/site-templates/_base/content/en/UseCase/persona-1.mdx +7 -6
  59. package/site-templates/_base/content/en/UseCase/persona-2.mdx +7 -6
  60. package/site-templates/_base/content/en/UseCase/persona-3.mdx +7 -6
  61. package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
  62. package/site-templates/designs/minimalism/DESIGN.md +98 -6
  63. package/site-templates/designs/minimalism/sample/README.md +48 -0
  64. package/site-templates/designs/standard/DESIGN.md +98 -6
  65. package/site-templates/marketing-site/DESIGN.md +98 -6
  66. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +60 -27
  67. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +50 -30
  68. package/site-templates/marketing-site-free/DESIGN.md +98 -6
  69. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +60 -24
  70. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +44 -27
  71. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  72. package/site-templates/saas-product/DESIGN.md +98 -6
  73. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +127 -18
  74. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +74 -20
  75. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  76. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +127 -18
  77. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +74 -20
  78. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  79. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +127 -18
  80. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +75 -21
  81. package/site-templates/shapes/person-service/ailk-template.json +1 -1
  82. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +1 -1
  83. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +2 -2
  84. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +4 -2
  85. package/site-templates/shapes/person-service/content/en/ServicesList/services.mdx +1 -1
  86. package/templates/.claude/rules/architecture.md +7 -0
  87. package/templates/.claude/skills/README.md +3 -2
  88. package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
  89. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
  90. package/templates/.claude/skills/add-page/SKILL.md +241 -238
  91. package/templates/.claude/skills/add-page/references/page-types.md +85 -76
  92. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
  93. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
  94. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
  95. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
  96. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
  97. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
  98. package/templates/.claude/skills/check-site/SKILL.md +23 -0
  99. package/templates/.claude/skills/fill-page/SKILL.md +26 -7
  100. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
  101. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +28 -11
  102. package/templates/.claude/skills/update-page/SKILL.md +117 -0
  103. package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
  104. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
  105. package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
  106. package/templates/.github/workflows/ci.yml +4 -0
  107. package/templates/.prettierignore +1 -0
  108. package/templates/CLAUDE.md +146 -103
  109. package/templates/DESIGN.md +98 -6
  110. package/templates/README.md +50 -38
  111. package/templates/apps/api/.env.example +2 -1
  112. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
  113. package/templates/apps/api/src/__tests__/server.test.ts +116 -0
  114. package/templates/apps/api/src/lib/stripe.ts +9 -0
  115. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
  116. package/templates/apps/api/src/middleware/auth.ts +54 -0
  117. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
  118. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
  119. package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
  120. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
  121. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
  122. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
  123. package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
  124. package/templates/apps/api/src/server.ts +28 -0
  125. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
  126. package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
  127. package/templates/apps/api/src/services/revalidation.ts +60 -15
  128. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
  129. package/templates/apps/mcp/src/tools/purchase.ts +4 -2
  130. package/templates/apps/web/.env.example +13 -0
  131. package/templates/apps/web/CLAUDE.md +22 -28
  132. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
  133. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
  134. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
  135. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
  136. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
  137. package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
  138. package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
  139. package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
  140. package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +4 -3
  141. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
  142. package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
  143. package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
  144. package/templates/apps/web/app/[locale]/layout.tsx +35 -19
  145. package/templates/apps/web/app/[locale]/newsletter/page.tsx +1 -1
  146. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +1 -1
  147. package/templates/apps/web/app/[locale]/portfolio/page.tsx +1 -1
  148. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
  149. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
  150. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
  151. package/templates/apps/web/app/api/revalidate/route.ts +78 -36
  152. package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
  153. package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
  154. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
  155. package/templates/apps/web/lib/locale-guard.ts +18 -0
  156. package/templates/apps/web/lib/meet-placeholder.ts +5 -0
  157. package/templates/apps/web/lib/nav-config.ts +14 -13
  158. package/templates/apps/web/lib/site-brand.tsx +13 -9
  159. package/templates/apps/web/lib/site-logo.tsx +36 -0
  160. package/templates/apps/web/lib/site-theme.ts +46 -26
  161. package/templates/apps/web/messages/en.json +23 -9
  162. package/templates/apps/web/middleware.ts +28 -4
  163. package/templates/content/en/BookCall/book-a-call.mdx +80 -0
  164. package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
  165. package/templates/content/en/BookCall/onsite-service.mdx +56 -0
  166. package/templates/content/en/BookCall/start-project.mdx +56 -0
  167. package/templates/content/en/ContactPage/contact.mdx +40 -1
  168. package/templates/content/en/HomePage/home.mdx +2 -6
  169. package/templates/content/en/LandingPage/meet.mdx +59 -0
  170. package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
  171. package/templates/database/CHANGELOG.md +7 -0
  172. package/templates/database/package.json +1 -1
  173. package/templates/package.json +2 -1
  174. package/templates/tools/check-claude-md.ts +128 -0
  175. package/versions.json +33 -33
  176. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -52
  177. package/site-templates/minimalism-site/DESIGN.md +0 -162
  178. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -458
  179. package/site-templates/minimalism-site/ailk-template.json +0 -23
  180. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
  181. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -52
  182. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
  183. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
  184. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
  185. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
  186. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -123
  187. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
  188. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
  189. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
  190. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
  191. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +0 -38
  192. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +0 -38
  193. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +0 -38
  194. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +0 -73
  195. package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
  196. package/site-templates/minimalism-site/provenance.json +0 -4
  197. package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
  198. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +0 -50
  199. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
  200. /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
  201. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
  202. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
  203. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
  204. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
  205. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
  206. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
  207. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
  208. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
  209. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
  210. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
  211. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * The verdicts of the scaffold render sweep (#6146).
3
+ *
4
+ * `scripts/verify-scaffold.mjs` serves a built scaffold and fetches every
5
+ * declared route. Until #6146 it served on `-H 127.0.0.1`, which made Next
6
+ * proxy next-intl's locale rewrite as an EXTERNAL one back into the same
7
+ * server, so every locale page answered a 307 to itself — and the driver
8
+ * recorded each as a "known gap" and passed. No request in a leg rendered a
9
+ * page, and a scaffold that 500'd on every page (#6107) stayed green.
10
+ *
11
+ * The rules that decide whether a route really rendered live here, apart from
12
+ * the driver, for the reason `api-boot-assertion.ts` does: proving them must
13
+ * not cost a templates build, an install and a `next build`. Each is a pure
14
+ * function of what the driver observed, so
15
+ * `__tests__/verify-scaffold-render-sweep.test.ts` drives them directly. The
16
+ * driver loads this module from `dist/`. Only `listPublicFiles` touches the
17
+ * filesystem, and none of it touches the network.
18
+ */
19
+ import { readdirSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ /** More hops than this is a redirect chain, not a render. */
22
+ export const MAX_REDIRECTS = 5;
23
+ /**
24
+ * Routes that are correct to answer 404 in every leg, and the licence claim
25
+ * that gates them. No leg's served app holds a signed licence — the driver
26
+ * scrubs `AILK_LICENSE_KEY`, and the mock key is a fixture string, not a JWT —
27
+ * so each entry must answer exactly 404. A 200 means the gate stopped gating.
28
+ */
29
+ export const CLAIM_GATED_ROUTES = {
30
+ "/purchase/success": "commerce",
31
+ "/purchase/cancel": "commerce",
32
+ };
33
+ /**
34
+ * A route whose live page needs an env var the driver never sets (#6146).
35
+ * While it is unset the route serves its Free placeholder seed instead
36
+ * (#6942): a 200 whose `<h1>` carries the `CONFIG_GATED_PLACEHOLDERS`
37
+ * heading. A 404 is the bug — the site's own nav link dead — and a rendered
38
+ * page without that heading means the gate stopped gating. Unlike
39
+ * `CLAIM_GATED_ROUTES`, a route absent from the route table is not stale: the
40
+ * module that owns it may simply not be installed in this leg.
41
+ */
42
+ export const CONFIG_GATED_ROUTES = {
43
+ "/meet": "SCHEDULING_CAL_LINK",
44
+ };
45
+ /**
46
+ * The placeholder heading each `CONFIG_GATED_ROUTES` entry renders while its
47
+ * env var is unset — for `/meet`, the hero headline of the overlay's
48
+ * `content/en/LandingPage/meet.mdx` seed. A fresh scaffold never edits it.
49
+ */
50
+ export const CONFIG_GATED_PLACEHOLDERS = {
51
+ "/meet": "{Book a time with us}",
52
+ };
53
+ const LOOPBACK_BASE = "http://localhost";
54
+ function pathnameOf(location, relativeTo) {
55
+ return new URL(location, new URL(relativeTo, LOOPBACK_BASE)).pathname;
56
+ }
57
+ /**
58
+ * Rules 1 and 2 of every verdict: no self-redirect, at most `MAX_REDIRECTS`
59
+ * hops, and a final 200. A hop is a self-redirect when its `Location`
60
+ * pathname equals the pathname that hop answered — the route itself for the
61
+ * first hop, the previous hop's target after that. Query and host are not
62
+ * compared: the next-intl loop this catches differs from its request in
63
+ * neither.
64
+ */
65
+ function assessReachable(route, hops, status) {
66
+ let answered = route;
67
+ for (const hop of hops) {
68
+ if (hop.location === null)
69
+ break;
70
+ const target = pathnameOf(hop.location, answered);
71
+ if (target === new URL(answered, LOOPBACK_BASE).pathname) {
72
+ return { ok: false, reason: `${answered} -> ${hop.status} self-redirect` };
73
+ }
74
+ answered = target;
75
+ }
76
+ if (hops.length > MAX_REDIRECTS) {
77
+ return { ok: false, reason: `${route} -> redirect chain of more than ${MAX_REDIRECTS} hops` };
78
+ }
79
+ if (status !== 200)
80
+ return { ok: false, reason: `${route} -> status ${status}` };
81
+ return { ok: true, reason: "200" };
82
+ }
83
+ /** Reachability only — for routes that are not pages (`/og`, `/llms.txt`, …). */
84
+ export function assessRouteStatus(input) {
85
+ return assessReachable(input.route, input.hops, input.status);
86
+ }
87
+ /**
88
+ * A `CLAIM_GATED_ROUTES` entry: it must be in the route table (a missing one
89
+ * is stale) and answer exactly 404.
90
+ */
91
+ export function assessClaimGatedRoute(input) {
92
+ const { route, routeTable, hops, status } = input;
93
+ const claim = CLAIM_GATED_ROUTES[route];
94
+ if (!routeTable.some((r) => (r.replace("/[locale]", "") || "/") === route)) {
95
+ return { ok: false, reason: `${route} -> stale CLAIM_GATED_ROUTES entry: not in the route table` };
96
+ }
97
+ if (hops.length > 0 || status !== 404) {
98
+ const first = hops[0];
99
+ const seen = first ? `${first.status} redirect` : `status ${status}`;
100
+ const why = status === 200 ? "gate open in a keyless serve" : `expected 404, got ${seen}`;
101
+ return { ok: false, reason: `${route} -> ${why} (claim-gated: ${claim})` };
102
+ }
103
+ return { ok: true, reason: `404 (claim-gated: ${claim})` };
104
+ }
105
+ /** The `lang` of the document's `<html>` element, or null when it has none. */
106
+ function htmlLang(html) {
107
+ return /<html\b[^>]*\slang="([^"]*)"/i.exec(html)?.[1] ?? null;
108
+ }
109
+ /**
110
+ * The first React Server Components error digest in the flight payload, or
111
+ * null. It is serialized inside a script string as `\"digest\":\"<value>\"`.
112
+ * An error or not-found boundary streamed after the shell answers 200 and can
113
+ * carry its own `<h1>`; this entry is the one reliable trace it leaves.
114
+ */
115
+ function flightDigest(html) {
116
+ return /\\"digest\\":\\"([^"\\]*)\\"/.exec(html)?.[1] ?? null;
117
+ }
118
+ /**
119
+ * Whether an `<h1>`'s inner HTML carries a name: text once tags are stripped,
120
+ * or an image with a non-empty `alt` (the Hero's lockup renders the wordmark
121
+ * inside the `<h1>`, and the heading takes its accessible name from it).
122
+ */
123
+ function headingHasName(inner) {
124
+ if (inner.replace(/<[^>]*>/g, "").trim() !== "")
125
+ return true;
126
+ return [...inner.matchAll(/<img\b[^>]*\salt="([^"]*)"/gi)].some((m) => (m[1] ?? "").trim() !== "");
127
+ }
128
+ /** The first own-content marker the page carries, in the order checked. */
129
+ function ownContentMarker(html) {
130
+ for (const m of html.matchAll(/<h1\b[^>]*>([\s\S]*?)<\/h1>/gi)) {
131
+ if (headingHasName(m[1] ?? ""))
132
+ return "h1";
133
+ }
134
+ if (html.includes('data-testid="auth-card"'))
135
+ return "auth-card";
136
+ if (html.includes('data-testid="sign-in-gate'))
137
+ return "sign-in-gate";
138
+ return null;
139
+ }
140
+ /**
141
+ * Decide whether a page route rendered, under the real locale, with its own
142
+ * content. The first failed rule is the reason:
143
+ *
144
+ * 1. no self-redirect, and at most `MAX_REDIRECTS` hops;
145
+ * 2. a final 200;
146
+ * 3. an HTML response;
147
+ * 4. `<html lang>` equal to `locale` — a page under a bogus locale fails;
148
+ * 5. no streamed error or not-found digest;
149
+ * 6. an own-content marker — a non-empty `<h1>` (a content page), an
150
+ * `auth-card` testid (an `(auth)` page), or a `sign-in-gate` testid (an
151
+ * `(authed)` page seen anonymously). The layout carries none of them.
152
+ */
153
+ export function assessPageRender(input) {
154
+ const { route, hops, status, contentType, html, locale } = input;
155
+ const reachable = assessReachable(route, hops, status);
156
+ if (!reachable.ok)
157
+ return reachable;
158
+ if (!(contentType ?? "").startsWith("text/html")) {
159
+ return { ok: false, reason: `${route} -> not html (${contentType})` };
160
+ }
161
+ const lang = htmlLang(html);
162
+ if (lang !== locale)
163
+ return { ok: false, reason: `${route} -> rendered under locale "${lang}"` };
164
+ const digest = flightDigest(html);
165
+ if (digest !== null) {
166
+ const why = digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;404")
167
+ ? "streamed not-found"
168
+ : `streamed error (digest ${digest})`;
169
+ return { ok: false, reason: `${route} -> ${why}` };
170
+ }
171
+ const marker = ownContentMarker(html);
172
+ if (marker === null) {
173
+ return { ok: false, reason: `${route} -> no own content (no <h1>, auth-card or sign-in-gate)` };
174
+ }
175
+ return { ok: true, reason: marker, marker };
176
+ }
177
+ /**
178
+ * A `CONFIG_GATED_ROUTES` entry. Unlike `assessClaimGatedRoute`, a route
179
+ * absent from the route table is not stale — the module that owns it may
180
+ * simply not be installed in this leg — so it PASSES, skipped, with no
181
+ * network call needed to decide that (mirroring how `assessClaimGatedRoute`
182
+ * is called with sentinel `hops`/`status` for its own absent case, never
183
+ * fetched). Once the route IS present it is scored as an ordinary page via
184
+ * `assessPageRender`; with its gating env var unset it must ALSO be the
185
+ * placeholder — an `<h1>` carrying its `CONFIG_GATED_PLACEHOLDERS` heading.
186
+ * Every failure with the env var unset names it, pinning the cause.
187
+ */
188
+ export function assessConfigGatedRoute(input) {
189
+ const { route, envVar, envSet, routeTable, hops, status, contentType, html, locale } = input;
190
+ if (!routeTable.some((r) => (r.replace("/[locale]", "") || "/") === route)) {
191
+ return { ok: true, reason: `${route} -> skipped: not in the route table (module not installed)` };
192
+ }
193
+ const rendered = assessPageRender({ route, hops, status, contentType, html, locale });
194
+ if (envSet)
195
+ return rendered;
196
+ if (!rendered.ok)
197
+ return { ok: false, reason: `${rendered.reason} (${envVar} unset)` };
198
+ const placeholder = CONFIG_GATED_PLACEHOLDERS[route];
199
+ if (placeholder === undefined) {
200
+ return { ok: false, reason: `${route} -> no CONFIG_GATED_PLACEHOLDERS entry to check with ${envVar} unset` };
201
+ }
202
+ if (!headingTexts(html).some((text) => text.includes(placeholder))) {
203
+ return {
204
+ ok: false,
205
+ reason: `${route} -> expected the "${placeholder}" placeholder with ${envVar} unset, rendered another page`,
206
+ };
207
+ }
208
+ return { ok: true, reason: `placeholder (${envVar} unset)` };
209
+ }
210
+ /** The text of every `<h1>` in the document, tags stripped. */
211
+ function headingTexts(html) {
212
+ return [...html.matchAll(/<h1\b[^>]*>([\s\S]*?)<\/h1>/gi)].map((m) => (m[1] ?? "").replace(/<[^>]*>/g, ""));
213
+ }
214
+ /**
215
+ * Every FILE under `dir`, recursing into directories, as sorted slash-joined
216
+ * paths relative to it — so `public/.well-known/security.txt` is listed and
217
+ * the `.well-known` directory never is. Anything that is neither (a symlink,
218
+ * a socket) throws rather than being silently followed or skipped.
219
+ */
220
+ export function listPublicFiles(dir) {
221
+ return listFilesUnder(dir, "");
222
+ }
223
+ function listFilesUnder(dir, relPrefix) {
224
+ const out = [];
225
+ const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1));
226
+ for (const entry of entries) {
227
+ const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
228
+ if (entry.isDirectory())
229
+ out.push(...listFilesUnder(join(dir, entry.name), rel));
230
+ else if (entry.isFile())
231
+ out.push(rel);
232
+ else
233
+ throw new Error(`public/${rel} is neither a file nor a directory`);
234
+ }
235
+ return out;
236
+ }
@@ -73,15 +73,19 @@ export interface PageRosterRow {
73
73
  /**
74
74
  * The architecture fragment one module contributes.
75
75
  *
76
- * `apps/web` (the presentation shell) is BASE — always present — so a module
77
- * extends it via `webImports` (packages added to `apps/web.may_import_from`)
78
- * rather than redeclaring it. Additional apps (api, mcp), packages, namespaces,
79
- * and top-level forbidden patterns are contributed outright.
76
+ * `apps/web`, `apps/api` and `apps/mcp` are BASE — always present. A module
77
+ * extends apps/web via `webImports` (added to `apps/web.may_import_from`)
78
+ * rather than redeclaring it; an `apps` entry replaces a base api/mcp block or
79
+ * adds an app (admin). Packages, namespaces and top-level forbidden patterns
80
+ * are contributed outright. `webImports`, `packages` and every `packages/*`
81
+ * entry describe this monorepo's boundaries: a scaffold has no `packages/`
82
+ * tree, so `deriveArchitecture` keeps only the entries naming a path the
83
+ * scaffold carries (#6940).
80
84
  */
81
85
  export interface ModuleSlice {
82
86
  /** Packages this module adds to apps/web.may_import_from (its presentation surface). */
83
87
  webImports?: string[];
84
- /** Additional apps this module introduces (api, mcp). web is BASE, never here. */
88
+ /** Apps this module declares (api, mcp replace the base block; admin is new). web is BASE, never here. */
85
89
  apps?: Record<string, ArchApp>;
86
90
  /** Packages this module contributes. */
87
91
  packages?: Record<string, ArchPackage>;
@@ -308,33 +312,32 @@ export interface AddTransformOp {
308
312
  value: string;
309
313
  }
310
314
  /**
311
- * The module-independent base: `apps/web` (the Next.js presentation shell every
312
- * AILK site has) plus the standalone forbidden-pattern floor. `may_import_from`
313
- * starts EMPTY — the selected modules fill it via `webImports`.
315
+ * The module-independent base: the three apps every scaffold ships on disk
316
+ * (`apps/web`, `apps/api`, `apps/mcp` — template-overlay/apps carries all
317
+ * three whatever the module set, #6940) plus the standalone forbidden-pattern
318
+ * floor. apps/web's `may_import_from` starts EMPTY — the selected modules fill
319
+ * it via `webImports`; a module that declares `api`/`mcp` replaces the base
320
+ * block with its own.
314
321
  */
315
322
  export declare const ARCHITECTURE_BASE: {
316
323
  readonly version: 1;
317
324
  readonly composition_model: {
318
- readonly reference: "claude-setup rules/need-to-know.md § Vocabulary (Module / Surface)";
319
325
  readonly tiers: {
320
326
  readonly data: "database/ — Prisma schema + migrations";
321
327
  readonly api: "apps/api (public HTTP API) + apps/mcp (agent tools)";
322
328
  readonly presentation: "apps/web — frontend surfaces";
323
329
  };
324
330
  };
331
+ /** Every declared app's own forbidden patterns (merged ahead of any the app adds). */
332
+ readonly appForbiddenPatterns: ForbiddenPattern[];
325
333
  /** The base apps/web shell. may_import_from is filled from the selected modules' webImports. */
326
334
  readonly web: {
327
335
  readonly path: "apps/web";
328
336
  readonly description: "Next.js — presentation tier. Frontend surfaces composed from the selected modules.";
329
337
  readonly may_not_import_from: readonly ["apps/api", "apps/mcp"];
330
- readonly forbidden_patterns: readonly [{
331
- readonly pattern: string;
332
- readonly reason: "A scaffolded AILK site is standalone — no upstream-monorepo imports";
333
- }, {
334
- readonly pattern: "@alkit/";
335
- readonly reason: "Renamed to @working-theory/* on extraction";
336
- }];
337
338
  };
339
+ /** The base apps/api and apps/mcp — present in every scaffold (#6940). */
340
+ readonly apps: Record<string, ArchApp>;
338
341
  /** Standalone-site forbidden-pattern floor (top-level, always present). */
339
342
  readonly forbiddenPatterns: ForbiddenPattern[];
340
343
  };
@@ -83,35 +83,59 @@ const PUBLIC_SCHEDULING_VAR_PATTERN = [
83
83
  */
84
84
  const STRIPE_LIVE_KEY_PATTERN = ["(sk|pk|rk)_li", "ve_[A-Za-z0-9]"].join("");
85
85
  /**
86
- * The module-independent base: `apps/web` (the Next.js presentation shell every
87
- * AILK site has) plus the standalone forbidden-pattern floor. `may_import_from`
88
- * starts EMPTY — the selected modules fill it via `webImports`.
86
+ * The per-app forbidden-pattern floor every declared app carries — so each app
87
+ * a scaffold ships has its own rule in `architecture.yaml`, not only the
88
+ * top-level floor below.
89
+ */
90
+ const APP_FORBIDDEN_FLOOR = [
91
+ {
92
+ pattern: NO_UPSTREAM_IMPORT_PATTERN,
93
+ reason: "A scaffolded AILK site is standalone — no upstream-monorepo imports",
94
+ },
95
+ {
96
+ pattern: "@alkit/",
97
+ reason: "Renamed to @working-theory/* on extraction",
98
+ },
99
+ ];
100
+ /**
101
+ * The module-independent base: the three apps every scaffold ships on disk
102
+ * (`apps/web`, `apps/api`, `apps/mcp` — template-overlay/apps carries all
103
+ * three whatever the module set, #6940) plus the standalone forbidden-pattern
104
+ * floor. apps/web's `may_import_from` starts EMPTY — the selected modules fill
105
+ * it via `webImports`; a module that declares `api`/`mcp` replaces the base
106
+ * block with its own.
89
107
  */
90
108
  export const ARCHITECTURE_BASE = {
91
109
  version: 1,
92
110
  composition_model: {
93
- reference: "claude-setup rules/need-to-know.md § Vocabulary (Module / Surface)",
94
111
  tiers: {
95
112
  data: "database/ — Prisma schema + migrations",
96
113
  api: "apps/api (public HTTP API) + apps/mcp (agent tools)",
97
114
  presentation: "apps/web — frontend surfaces",
98
115
  },
99
116
  },
117
+ /** Every declared app's own forbidden patterns (merged ahead of any the app adds). */
118
+ appForbiddenPatterns: APP_FORBIDDEN_FLOOR,
100
119
  /** The base apps/web shell. may_import_from is filled from the selected modules' webImports. */
101
120
  web: {
102
121
  path: "apps/web",
103
122
  description: "Next.js — presentation tier. Frontend surfaces composed from the selected modules.",
104
123
  may_not_import_from: ["apps/api", "apps/mcp"],
105
- forbidden_patterns: [
106
- {
107
- pattern: NO_UPSTREAM_IMPORT_PATTERN,
108
- reason: "A scaffolded AILK site is standalone — no upstream-monorepo imports",
109
- },
110
- {
111
- pattern: "@alkit/",
112
- reason: "Renamed to @working-theory/* on extraction",
113
- },
114
- ],
124
+ },
125
+ /** The base apps/api and apps/mcp — present in every scaffold (#6940). */
126
+ apps: {
127
+ api: {
128
+ path: "apps/api",
129
+ description: "Public HTTP API — routes + services (Fastify).",
130
+ may_import_from: ["database"],
131
+ may_not_import_from: ["apps/mcp", "apps/web"],
132
+ },
133
+ mcp: {
134
+ path: "apps/mcp",
135
+ description: "MCP server — agent-facing tools. Calls apps/api for writes.",
136
+ may_import_from: ["database"],
137
+ may_not_import_from: ["apps/api", "apps/web"],
138
+ },
115
139
  },
116
140
  /** Standalone-site forbidden-pattern floor (top-level, always present). */
117
141
  forbiddenPatterns: [
@@ -595,12 +619,12 @@ export const MODULE_SLICES = {
595
619
  {
596
620
  pattern: "import.*['\"]stripe['\"]",
597
621
  paths_excluded: ["packages/stripe-client/**"],
598
- reason: "Stripe SDK imports go through packages/stripe-client only — single point of config",
622
+ reason: "Stripe SDK imports go through @working-theory/stripe-client only — single point of config",
599
623
  },
600
624
  {
601
625
  pattern: "import.*['\"]resend['\"]|['\"]nodemailer['\"]",
602
626
  paths_excluded: ["packages/email/**"],
603
- reason: "Email provider SDKs go through packages/email — provider swap is a one-package change",
627
+ reason: "Email provider SDKs go through @working-theory/email — provider swap is a one-package change",
604
628
  },
605
629
  ],
606
630
  // ownedFiles (S1, #5088). Verified against the current filesystem and
@@ -1413,6 +1437,9 @@ export const MODULE_SLICES = {
1413
1437
  // upgrade-path fixture to protect either — it is net-new here, so nothing
1414
1438
  // in `templates/` can regress by its absence.
1415
1439
  "apps/web/app/[locale]/reports/[slug]/page.tsx",
1440
+ // spec 6892 D2 — the /reports hub, alongside its reader above: also a
1441
+ // URL segment MODULE_SURFACES["signal-collectors"] does not gate.
1442
+ "apps/web/app/[locale]/reports/page.tsx",
1416
1443
  //
1417
1444
  // The two sections that route composes — `report-header` and
1418
1445
  // `reports-list` — are deliberately NOT owned here. They are
@@ -1446,11 +1473,13 @@ export const MODULE_SLICES = {
1446
1473
  // #5562 — the report reader. Unlike the three rows above, this row's
1447
1474
  // code IS this module's own `ownedFiles` (see the note beside it), so
1448
1475
  // the contribution and the delivery shape carrying it agree here. Its
1449
- // seeds are a directory a template supplies (minimalism-site's four
1450
- // reports), so the row declares that directory (#6646).
1476
+ // seeds are SELF-SEEDING (spec 6892 D3): this module's own `installMap`
1477
+ // lands the four `ReportPage` samples below, so every template taking
1478
+ // the module gets them — `moduleRosterRows` derives the code-route form
1479
+ // for this row rather than requiring a template to ship one (D9).
1451
1480
  { route: "/reports/[slug]", contentDir: "content/en/ReportPage" },
1452
1481
  ],
1453
- // installMap (#5618 D6.3) — the module's ten sample files, moved here out
1482
+ // installMap (#5618 D6.3) — the module's sample files, moved here out
1454
1483
  // of `site-templates/marketing-site/` so the module OWNS its samples
1455
1484
  // (Rule B) and a keyed `--modules signal-collectors` scaffold with no
1456
1485
  // template lands them. Their in-repo home is
@@ -1463,12 +1492,19 @@ export const MODULE_SLICES = {
1463
1492
  // `module-templates/` the two never meet.
1464
1493
  //
1465
1494
  // Four entries are real renames (the flow and waitlist seeds the API
1466
- // reads, and the report PDF under apps/web/public/); the six MDX entries
1495
+ // reads, and the report PDF under apps/web/public/); the ten MDX entries
1467
1496
  // are self-addressed. Their `/projects`, `/projects/[slug]` and
1468
1497
  // `/thanks/[slug]` rows are declared by the templates that install them
1469
1498
  // (`template-manifest.ts`'s signal-collectors sample rows, #6646), not
1470
1499
  // here — a module contribution would change the enabled-surface set and
1471
- // so the Pro scaffold's file set (spec 5618 § 7 finding 7).
1500
+ // so the Pro scaffold's file set (spec 5618 § 7 finding 7). The four
1501
+ // `ReportPage` seeds and the `ReportsList` hub seed are the one exception
1502
+ // (spec 6892 D3): their `/reports/[slug]` and `/reports` rows ARE this
1503
+ // module's own `pageContributions`/`starter-grid.ts` rows respectively,
1504
+ // so this module installs its own seeds for them directly, unlike the
1505
+ // sample rows above. `/referral`'s seed and flow move here the same way,
1506
+ // moved out of the retired `minimalism-site` tree (spec 6892 D3), which
1507
+ // owned no code of its own for either route.
1472
1508
  // `generate-sample-report.py` keeps its root-level destination
1473
1509
  // (finding 1 — AC7 forbids changing the Pro file set).
1474
1510
  installMap: {
@@ -1478,7 +1514,14 @@ export const MODULE_SLICES = {
1478
1514
  "packages/create-ailk/module-templates/signal-collectors/content/en/ThankYouPage/contact.mdx": "content/en/ThankYouPage/contact.mdx",
1479
1515
  "packages/create-ailk/module-templates/signal-collectors/content/en/ThankYouPage/newsletter.mdx": "content/en/ThankYouPage/newsletter.mdx",
1480
1516
  "packages/create-ailk/module-templates/signal-collectors/content/en/ThankYouPage/apply.mdx": "content/en/ThankYouPage/apply.mdx",
1517
+ "packages/create-ailk/module-templates/signal-collectors/content/en/ReportPage/annual-letter-2024.mdx": "content/en/ReportPage/annual-letter-2024.mdx",
1518
+ "packages/create-ailk/module-templates/signal-collectors/content/en/ReportPage/field-notes-2023.mdx": "content/en/ReportPage/field-notes-2023.mdx",
1519
+ "packages/create-ailk/module-templates/signal-collectors/content/en/ReportPage/platform-shift-2025.mdx": "content/en/ReportPage/platform-shift-2025.mdx",
1520
+ "packages/create-ailk/module-templates/signal-collectors/content/en/ReportPage/state-of-the-market-2026.mdx": "content/en/ReportPage/state-of-the-market-2026.mdx",
1521
+ "packages/create-ailk/module-templates/signal-collectors/content/en/ReportsList/reports.mdx": "content/en/ReportsList/reports.mdx",
1522
+ "packages/create-ailk/module-templates/signal-collectors/content/en/LandingPage/referral.mdx": "content/en/LandingPage/referral.mdx",
1481
1523
  "packages/create-ailk/module-templates/signal-collectors/flows/fit-check.flow.json": "database/flows/fit-check.flow.json",
1524
+ "packages/create-ailk/module-templates/signal-collectors/flows/referral.flow.json": "database/flows/referral.flow.json",
1482
1525
  "packages/create-ailk/module-templates/signal-collectors/waitlist/launch-waitlist.experiment.json": "database/waitlist/launch-waitlist.experiment.json",
1483
1526
  "packages/create-ailk/module-templates/signal-collectors/reports/sample-report.pdf": "apps/web/public/reports/sample-report.pdf",
1484
1527
  "packages/create-ailk/module-templates/signal-collectors/reports/generate-sample-report.py": "reports/generate-sample-report.py",
@@ -39,8 +39,12 @@ export interface CliOptions {
39
39
  template: string | undefined;
40
40
  /** The starter's shape (`--shape`, #6754), normalized. Never set with `template`. */
41
41
  shape?: ShapeName;
42
- /** The starter's design (`--design`, #6754). Never set with `template`. */
43
- design?: DesignName;
42
+ /**
43
+ * The starter's design (`--design`, #6754; widened to a path #6813 D1/D3):
44
+ * a recognized name, lowercased, or an absolute directory holding a
45
+ * `DESIGN.md`, case preserved. Never set with `template`.
46
+ */
47
+ design?: string;
44
48
  /**
45
49
  * True when a scaffold run named neither `--shape` nor `--template` on a
46
50
  * terminal: `resolveCliShape` must ask before anything is scaffolded.
@@ -66,6 +70,12 @@ export interface CliOptions {
66
70
  * `--sync-architecture`: data-only, one file, an already-scaffolded target.
67
71
  */
68
72
  syncCatalog: boolean;
73
+ /**
74
+ * When set, rewrite the template-owned sections of the target's CLAUDE.md
75
+ * to the template this create-ailk ships and exit (#6904). The same sync
76
+ * shape: one file, an already-scaffolded target.
77
+ */
78
+ syncClaudeMd: boolean;
69
79
  /**
70
80
  * When set, a fresh scaffold ALSO gets COMPONENT-CATALOG.md. Off by default
71
81
  * so a scaffold that does not ask for the catalog is byte-identical to the
@@ -7,6 +7,8 @@
7
7
  * On any parse / validation failure: throws ParseError. The orchestrator's
8
8
  * top-level catch turns this into a stderr write + non-zero exit.
9
9
  */
10
+ import { existsSync, statSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
10
12
  import { createInterface } from "node:readline/promises";
11
13
  import { parseArgs } from "node:util";
12
14
  import { assertLicenseKeyShape } from "./lib/license-key-files.js";
@@ -95,8 +97,9 @@ Options:
95
97
  Installs that shape's Free starter. A run naming
96
98
  neither --shape nor --template asks for a shape on
97
99
  a terminal, and refuses without one.
98
- --design <design> The look to render the starter in (recognized:
99
- ${RECOGNIZED_DESIGNS.join(", ")}). Defaults to the
100
+ --design <design> The look to render the starter in: a design name
101
+ (recognized: ${RECOGNIZED_DESIGNS.join(", ")}) or a
102
+ directory holding a DESIGN.md. Defaults to the
100
103
  starter's own.
101
104
  --template <name> The site template to install (recognized:
102
105
  ${RECOGNIZED_TEMPLATES.join(", ")}).
@@ -120,8 +123,9 @@ Options:
120
123
  Surfaces the declared architecture does not carry are
121
124
  NOT scaffolded — their routes are absent, so a fresh
122
125
  site never serves a stock page with placeholder
123
- content. Add one later with the \`add-page\`
124
- skill, or re-run with the surface named here.
126
+ content. Add one later with
127
+ \`create-ailk add-route <surface>\`, or re-run with
128
+ the surface named here.
125
129
  --sync-architecture <csv> Regenerate architecture.yaml in place in an
126
130
  already-scaffolded repo from the given module set
127
131
  (recognized: ${RECOGNIZED_MODULES.join(", ")}), then
@@ -140,6 +144,12 @@ Options:
140
144
  file, at the target dir (positional arg, else cwd).
141
145
  Bypasses the scaffold path — copies no template, runs
142
146
  no install. Re-run after upgrading create-ailk.
147
+ --sync-claude-md Rewrite the AI Launch Kit sections of CLAUDE.md in
148
+ an already-scaffolded repo to the template this
149
+ create-ailk ships, then exit. Only CLAUDE.md
150
+ changes; your Site notes and anything between the
151
+ sections are kept byte for byte. Target dir is the
152
+ positional arg, else cwd.
143
153
  --license <key> Your licence key: fetch paid modules with it and
144
154
  write it into the scaffold (apps/api/.env and a
145
155
  gitignored ailk.key, both owner-only).
@@ -180,6 +190,7 @@ export function parseCliArgs(argv, env = { interactive: false }) {
180
190
  surfaces: { type: "string" },
181
191
  "sync-architecture": { type: "string" },
182
192
  "sync-catalog": { type: "boolean", default: false },
193
+ "sync-claude-md": { type: "boolean", default: false },
183
194
  "with-catalog": { type: "boolean", default: false },
184
195
  install: { type: "boolean", default: false },
185
196
  help: { type: "boolean", default: false, short: "h" },
@@ -203,12 +214,18 @@ export function parseCliArgs(argv, env = { interactive: false }) {
203
214
  const surfaces = parseSurfaceList(parsed.values.surfaces);
204
215
  const syncArchitecture = parsed.values["sync-architecture"];
205
216
  const syncCatalog = parsed.values["sync-catalog"] === true;
217
+ const syncClaudeMd = parsed.values["sync-claude-md"] === true;
206
218
  const withCatalog = parsed.values["with-catalog"] === true;
207
219
  const license = parseLicense(parsed.values.license);
208
- // The two sync modes each exit after rewriting their one file, so asking for
209
- // both in one invocation has no coherent meaning.
210
- if (syncCatalog && syncArchitecture !== undefined) {
211
- throw new ParseError("--sync-catalog and --sync-architecture cannot be combined; run them one at a time");
220
+ // The sync modes each exit after rewriting their one file, so asking for
221
+ // more than one in one invocation has no coherent meaning.
222
+ const syncFlags = [
223
+ syncCatalog ? "--sync-catalog" : undefined,
224
+ syncArchitecture !== undefined ? "--sync-architecture" : undefined,
225
+ syncClaudeMd ? "--sync-claude-md" : undefined,
226
+ ].filter((f) => f !== undefined);
227
+ if (syncFlags.length > 1) {
228
+ throw new ParseError(`${syncFlags.join(" and ")} cannot be combined; run them one at a time`);
212
229
  }
213
230
  // Validate the sync-architecture value now, through the SAME parseModules
214
231
  // path `--modules` uses, so an unknown module throws ParseError at parse time
@@ -228,12 +245,13 @@ export function parseCliArgs(argv, env = { interactive: false }) {
228
245
  modules,
229
246
  surfaces,
230
247
  syncCatalog: false,
248
+ syncClaudeMd: false,
231
249
  withCatalog: false,
232
250
  syncArchitecture: undefined,
233
251
  };
234
252
  }
235
253
  // Sync modes: the target dir is optional (positional if given, else cwd).
236
- if (syncArchitecture !== undefined || syncCatalog) {
254
+ if (syncArchitecture !== undefined || syncCatalog || syncClaudeMd) {
237
255
  if (parsed.positionals.length > 1) {
238
256
  throw new ParseError(`expected at most one target directory, got ${parsed.positionals.length}`);
239
257
  }
@@ -247,6 +265,7 @@ export function parseCliArgs(argv, env = { interactive: false }) {
247
265
  modules,
248
266
  surfaces,
249
267
  syncCatalog,
268
+ syncClaudeMd,
250
269
  withCatalog,
251
270
  syncArchitecture,
252
271
  };
@@ -284,6 +303,7 @@ export function parseCliArgs(argv, env = { interactive: false }) {
284
303
  modules,
285
304
  surfaces,
286
305
  syncCatalog,
306
+ syncClaudeMd,
287
307
  withCatalog,
288
308
  syncArchitecture: undefined,
289
309
  ...(license !== undefined ? { license } : {}),
@@ -305,15 +325,56 @@ function parseShape(raw) {
305
325
  }
306
326
  return name;
307
327
  }
308
- /** Validate `--design` (#6754 D2 step 2): trimmed and lowercased. */
328
+ /** `path` is an existing directory. */
329
+ function isExistingDir(path) {
330
+ try {
331
+ return statSync(path).isDirectory();
332
+ }
333
+ catch {
334
+ return false;
335
+ }
336
+ }
337
+ /**
338
+ * `dir` (resolved from `raw`) as a design path: an existing directory
339
+ * holding a `DESIGN.md`, else `ParseError` naming `raw` (#6813 D1 rule 2).
340
+ */
341
+ function validateDesignDir(dir, raw) {
342
+ if (!isExistingDir(dir)) {
343
+ throw new ParseError(`design directory "${raw}" does not exist`);
344
+ }
345
+ if (!existsSync(join(dir, "DESIGN.md"))) {
346
+ throw new ParseError(`design directory "${raw}" holds no DESIGN.md`);
347
+ }
348
+ return dir;
349
+ }
350
+ /**
351
+ * Validate `--design` (#6754 D2 step 2, widened #6813 D1): a recognized
352
+ * name, or a directory holding a `DESIGN.md`. A value with no `/` or `\` is
353
+ * classified as a name first (trimmed and lowercased, filesystem never
354
+ * consulted) — falling back to a path only when it names an existing
355
+ * directory in `process.cwd()`, so `--design mydesign` and `--design
356
+ * ./mydesign` can differ. A value with a separator is always a path,
357
+ * resolved against `process.cwd()`, case preserved. The package's CONTENT is
358
+ * validated later, in `scaffold()` (`resolveDesign`) — this parser checks
359
+ * presence only.
360
+ */
309
361
  function parseDesign(raw) {
310
362
  if (raw === undefined)
311
363
  return undefined;
312
- const name = raw.trim().toLowerCase();
313
- if (!RECOGNIZED_DESIGNS.includes(name)) {
364
+ const trimmed = raw.trim();
365
+ const hasSeparator = trimmed.includes("/") || trimmed.includes("\\");
366
+ if (!hasSeparator) {
367
+ const name = trimmed.toLowerCase();
368
+ if (RECOGNIZED_DESIGNS.includes(name)) {
369
+ return name;
370
+ }
371
+ const dir = resolve(process.cwd(), trimmed);
372
+ if (isExistingDir(dir)) {
373
+ return validateDesignDir(dir, raw);
374
+ }
314
375
  throw new ParseError(`unknown design "${raw}". Recognized: ${RECOGNIZED_DESIGNS.join(", ")}`);
315
376
  }
316
- return name;
377
+ return validateDesignDir(resolve(process.cwd(), trimmed), raw);
317
378
  }
318
379
  /** How many answers the prompt takes before it gives up. */
319
380
  const PROMPT_ATTEMPTS = 3;