create-ailk 0.13.0 → 0.15.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 (188) hide show
  1. package/README.md +12 -4
  2. package/component-catalog.json +250 -36
  3. package/component-catalog.md +10 -6
  4. package/dist/add-module.d.ts +7 -0
  5. package/dist/add-module.js +34 -2
  6. package/dist/add-route.js +18 -2
  7. package/dist/apply-design.d.ts +32 -2
  8. package/dist/apply-design.js +155 -22
  9. package/dist/cli.js +24 -3
  10. package/dist/derive-architecture.d.ts +15 -2
  11. package/dist/derive-architecture.js +34 -6
  12. package/dist/design-compile.d.ts +70 -2
  13. package/dist/design-compile.js +97 -1
  14. package/dist/design-home.d.ts +74 -0
  15. package/dist/design-home.js +256 -0
  16. package/dist/design-package.d.ts +39 -2
  17. package/dist/design-package.js +224 -9
  18. package/dist/design-presets.d.ts +17 -1
  19. package/dist/design-presets.js +17 -5
  20. package/dist/design-scaffold.d.ts +6 -25
  21. package/dist/design-scaffold.js +26 -74
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.js +1 -1
  24. package/dist/lib/chrome-readers.d.ts +21 -0
  25. package/dist/lib/chrome-readers.js +100 -0
  26. package/dist/lib/design-checks.d.ts +12 -1
  27. package/dist/lib/design-checks.js +38 -2
  28. package/dist/lib/extract-module-bundle.d.ts +11 -0
  29. package/dist/lib/extract-module-bundle.js +36 -6
  30. package/dist/lib/license-key-files.d.ts +6 -1
  31. package/dist/lib/license-key-files.js +9 -4
  32. package/dist/lib/module-license-gate.d.ts +9 -0
  33. package/dist/lib/module-license-gate.js +18 -0
  34. package/dist/module-architecture.d.ts +9 -0
  35. package/dist/module-architecture.js +6 -0
  36. package/dist/parse-args.d.ts +22 -0
  37. package/dist/parse-args.js +73 -0
  38. package/dist/programmatic.d.ts +20 -0
  39. package/dist/programmatic.js +180 -35
  40. package/dist/site-chrome.d.ts +42 -0
  41. package/dist/site-chrome.js +140 -2
  42. package/dist/site-fonts.d.ts +21 -0
  43. package/dist/site-fonts.js +54 -0
  44. package/dist/site-manifest.d.ts +12 -5
  45. package/dist/site-manifest.js +25 -11
  46. package/dist/site-roster.d.ts +16 -0
  47. package/dist/site-roster.js +81 -0
  48. package/dist/starter-grid.js +15 -0
  49. package/dist/surfaces.js +12 -0
  50. package/dist/sync-routes.js +11 -2
  51. package/dist/team-profiles.d.ts +29 -0
  52. package/dist/team-profiles.js +43 -0
  53. package/dist/topology.d.ts +131 -0
  54. package/dist/topology.js +533 -0
  55. package/package.json +3 -3
  56. package/release-scope.json +1 -1
  57. package/site-templates/_base/content/en/TeamList/team.mdx +68 -0
  58. package/site-templates/_base/content/en/TeamMember/first-member.mdx +22 -0
  59. package/site-templates/_base/content/en/TeamMember/second-member.mdx +22 -0
  60. package/site-templates/_base/content/en/TeamMember/third-member.mdx +22 -0
  61. package/site-templates/designs/minimalism/DESIGN.md +166 -36
  62. package/site-templates/designs/minimalism/home/local-business-service.mdx +119 -0
  63. package/site-templates/designs/minimalism/home/organization-product.mdx +132 -0
  64. package/site-templates/designs/minimalism/home/organization-service.mdx +131 -0
  65. package/site-templates/designs/minimalism/home/person-service.mdx +101 -0
  66. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +34 -1
  67. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +20 -0
  68. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +34 -1
  69. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +22 -1
  70. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  71. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +34 -1
  72. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -7
  73. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  74. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +34 -1
  75. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -7
  76. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  77. package/templates/README.md +2 -0
  78. package/templates/apps/api/CLAUDE.md +6 -1
  79. package/templates/apps/api/package.json +1 -0
  80. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +5 -1
  81. package/templates/apps/api/src/lib/__mocks__/prisma.ts +10 -0
  82. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +114 -1
  83. package/templates/apps/api/src/lib/__tests__/site.test.ts +54 -0
  84. package/templates/apps/api/src/lib/agent-store.ts +95 -3
  85. package/templates/apps/api/src/lib/site.ts +37 -0
  86. package/templates/apps/api/src/lib/spoke-store.ts +18 -2
  87. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +4 -0
  88. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +22 -0
  89. package/templates/apps/api/src/middleware/auth.ts +3 -0
  90. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +23 -0
  91. package/templates/apps/api/src/openapi/spec.ts +38 -2
  92. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +50 -2
  93. package/templates/apps/api/src/routes/checkout/sessions.ts +7 -0
  94. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +275 -0
  95. package/templates/apps/api/src/routes/leads/index.ts +69 -6
  96. package/templates/apps/api/src/routes/webhooks/README.md +39 -0
  97. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +366 -0
  98. package/templates/apps/api/src/routes/webhooks/stripe.ts +61 -1
  99. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +199 -0
  100. package/templates/apps/api/src/services/default-tenant.ts +41 -7
  101. package/templates/apps/api/src/services/lead-routing.ts +148 -29
  102. package/templates/apps/api/src/services/public-site-resolver.ts +48 -0
  103. package/templates/apps/mcp/__tests__/index.test.ts +5 -5
  104. package/templates/apps/mcp/package.json +1 -1
  105. package/templates/apps/mcp/src/index.ts +6 -4
  106. package/templates/apps/mcp/src/lib/content-adapter.ts +2 -2
  107. package/templates/apps/mcp/src/lib/topology.generated.ts +24 -0
  108. package/templates/apps/web/CLAUDE.md +12 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +10 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts +42 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +8 -0
  112. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
  113. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
  114. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
  115. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  116. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +14 -1
  117. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +66 -14
  118. package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +163 -0
  119. package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +68 -0
  120. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +4 -4
  121. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +15 -3
  122. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +17 -6
  123. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +14 -1
  124. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +2 -2
  125. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +4 -6
  126. package/templates/apps/web/app/[locale]/layout.tsx +212 -57
  127. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
  128. package/templates/apps/web/app/[locale]/page.tsx +37 -0
  129. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  130. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +4 -6
  131. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
  132. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +15 -2
  133. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +43 -6
  134. package/templates/apps/web/app/[locale]/services/page.tsx +14 -2
  135. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +2 -2
  136. package/templates/apps/web/app/[locale]/team/[slug]/page.tsx +45 -0
  137. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +155 -0
  138. package/templates/apps/web/app/[locale]/team/page.tsx +49 -0
  139. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +2 -2
  140. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +14 -1
  141. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -0
  142. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +2 -2
  143. package/templates/apps/web/app/not-found.tsx +47 -0
  144. package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +89 -1
  145. package/templates/apps/web/app/openapi.json/route.ts +69 -1
  146. package/templates/apps/web/instrumentation.ts +7 -4
  147. package/templates/apps/web/lib/__tests__/closing-footer-path.test.ts +96 -0
  148. package/templates/apps/web/lib/__tests__/profile-back-link.test.ts +95 -0
  149. package/templates/apps/web/lib/__tests__/relay.test.ts +290 -0
  150. package/templates/apps/web/lib/__tests__/site-brand.test.ts +52 -0
  151. package/templates/apps/web/lib/__tests__/site-theme-head.test.ts +70 -0
  152. package/templates/apps/web/lib/__tests__/site-theme.test.ts +64 -1
  153. package/templates/apps/web/lib/closing-footer-path.ts +50 -0
  154. package/templates/apps/web/lib/profile-back-link.ts +51 -0
  155. package/templates/apps/web/lib/relay.ts +206 -2
  156. package/templates/apps/web/lib/site-brand.tsx +22 -1
  157. package/templates/apps/web/lib/site-chrome.ts +15 -0
  158. package/templates/apps/web/lib/site-fonts.ts +15 -0
  159. package/templates/apps/web/lib/site-logo.tsx +39 -1
  160. package/templates/apps/web/lib/site-theme-head.ts +33 -0
  161. package/templates/apps/web/lib/site-theme.ts +13 -1
  162. package/templates/apps/web/lib/topology.generated.ts +23 -0
  163. package/templates/apps/web/messages/en.json +7 -1
  164. package/templates/apps/web/package.json +1 -1
  165. package/templates/database/CHANGELOG.md +32 -0
  166. package/templates/database/__tests__/agents.db.test.ts +65 -0
  167. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +92 -0
  168. package/templates/database/auth/schema.prisma +23 -0
  169. package/templates/database/inbox/schema.prisma +13 -0
  170. package/templates/database/jest.config.cjs +7 -11
  171. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +35 -0
  172. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +20 -0
  173. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +33 -0
  174. package/templates/database/package.json +1 -1
  175. package/versions.json +35 -34
  176. package/site-templates/designs/minimalism/sample/README.md +0 -48
  177. package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +0 -244
  178. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-1.svg +0 -7
  179. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-2.svg +0 -7
  180. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-3.svg +0 -7
  181. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-4.svg +0 -7
  182. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-5.svg +0 -7
  183. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-6.svg +0 -7
  184. package/site-templates/designs/minimalism/sample/public/hero-backdrop.svg +0 -17
  185. package/site-templates/designs/minimalism/sample/public/logo-placeholder-1.svg +0 -6
  186. package/site-templates/designs/minimalism/sample/public/logo-placeholder-2.svg +0 -6
  187. package/site-templates/designs/minimalism/sample/public/logo-placeholder-3.svg +0 -6
  188. package/site-templates/designs/minimalism/sample/public/wordmark-placeholder.svg +0 -28
@@ -85,13 +85,18 @@ function upsertEnvLine(existing, key) {
85
85
  * `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
86
86
  * `ailk.key` when no existing line covers it. Idempotent — a second call with
87
87
  * the same key changes no byte.
88
+ *
89
+ * `apiEnv: false` skips the `apps/api/.env` line — a spoke (#7426 D6) has no
90
+ * `apps/api`, so it keeps the key in `ailk.key` alone.
88
91
  */
89
- export function writeLicenseKeyFiles(dir, key) {
92
+ export function writeLicenseKeyFiles(dir, key, opts = {}) {
90
93
  assertLicenseKeyShape(key);
91
94
  writeOwnerOnly(join(dir, LICENSE_KEY_FILE), `${key}\n`);
92
- const envPath = join(dir, LICENSE_ENV_FILE);
93
- const env = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
94
- writeOwnerOnly(envPath, upsertEnvLine(env, key));
95
+ if (opts.apiEnv !== false) {
96
+ const envPath = join(dir, LICENSE_ENV_FILE);
97
+ const env = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
98
+ writeOwnerOnly(envPath, upsertEnvLine(env, key));
99
+ }
95
100
  const ignorePath = join(dir, ".gitignore");
96
101
  const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
97
102
  const covered = ignore
@@ -139,6 +139,15 @@ export declare function deriveLevel(options: GateOptions): Promise<{
139
139
  level: Level;
140
140
  bundles: Map<ModuleName, Uint8Array>;
141
141
  }>;
142
+ /**
143
+ * The level a `--modules`-keep run's chosen non-Free design is gated on
144
+ * (#7010). `resolveModuleSet`'s keep branch composes no level (NFR-2), so
145
+ * `scaffold()` derives one here through `deriveLevel`, as the addition branch
146
+ * does. A key the registry refuses for a Pro module — lapsed, revoked, or one
147
+ * that does not cover Pro — reads as `"free"`, so `assertDesignAllowed`
148
+ * refuses with its own message (fail closed); any other failure propagates.
149
+ */
150
+ export declare function deriveDesignLevel(options: GateOptions): Promise<Level>;
142
151
  /**
143
152
  * The one entry point that turns `--modules` + the key into the effective
144
153
  * module set and its resolved bundles (D5) — `programmatic.ts`'s `scaffold()`
@@ -294,6 +294,24 @@ export async function deriveLevel(options) {
294
294
  throw err;
295
295
  }
296
296
  }
297
+ /**
298
+ * The level a `--modules`-keep run's chosen non-Free design is gated on
299
+ * (#7010). `resolveModuleSet`'s keep branch composes no level (NFR-2), so
300
+ * `scaffold()` derives one here through `deriveLevel`, as the addition branch
301
+ * does. A key the registry refuses for a Pro module — lapsed, revoked, or one
302
+ * that does not cover Pro — reads as `"free"`, so `assertDesignAllowed`
303
+ * refuses with its own message (fail closed); any other failure propagates.
304
+ */
305
+ export async function deriveDesignLevel(options) {
306
+ try {
307
+ return (await deriveLevel(options)).level;
308
+ }
309
+ catch (err) {
310
+ if (err instanceof LicenseRequiredError)
311
+ return "free";
312
+ throw err;
313
+ }
314
+ }
297
315
  /**
298
316
  * The one entry point that turns `--modules` + the key into the effective
299
317
  * module set and its resolved bundles (D5) — `programmatic.ts`'s `scaffold()`
@@ -83,6 +83,15 @@ export interface PageRosterRow {
83
83
  * scaffold carries (#6940).
84
84
  */
85
85
  export interface ModuleSlice {
86
+ /**
87
+ * Admits the module on a spoke (`create-ailk --topology spoke`, #7426 D7).
88
+ * `true` → a spoke may carry it, and its bundle installs through the spoke
89
+ * filter (`topology.ts` `isSpokeExcluded`), so any `apps/api` or
90
+ * `database` part it carries is dropped. Unset → refused on a spoke.
91
+ * `topology.ts` `spokeModules()` reads it; a sibling issue admits its
92
+ * module by setting it here, never in a second list.
93
+ */
94
+ spoke?: true;
86
95
  /**
87
96
  * The paid UI package base-tree code loads through
88
97
  * `apps/web/lib/paid-sections.generated.ts` (#7536). Present → one row in the
@@ -206,6 +206,7 @@ function payloadPaths(kind, name, destRoot) {
206
206
  export const MODULE_SLICES = {
207
207
  // ── marketing — thin presentation module (the default) ───────────────────
208
208
  marketing: {
209
+ spoke: true,
209
210
  webImports: [
210
211
  WEB_AUTH_ADAPTER_IMPORT,
211
212
  "packages/content-adapters",
@@ -287,6 +288,7 @@ export const MODULE_SLICES = {
287
288
  },
288
289
  // ── docs-blog — Pro module: both fumadocs surfaces (D-46) ────────────────
289
290
  "docs-blog": {
291
+ spoke: true,
290
292
  webImports: [WEB_AUTH_ADAPTER_IMPORT, "packages/fumadocs-schema"],
291
293
  packages: {
292
294
  "fumadocs-schema": {
@@ -1388,6 +1390,10 @@ export const MODULE_SLICES = {
1388
1390
  "apps/api/src/bin/deliverable-upload.ts",
1389
1391
  "apps/api/src/bin/session-retention-sweep.ts",
1390
1392
  "apps/api/src/bin/seed-presets.ts",
1393
+ // #7844 D7 — the operator flow-copy CLI and its service: flows code by
1394
+ // subject, and a Free scaffold carries no flows.
1395
+ "apps/api/src/bin/flows-copy.ts",
1396
+ "apps/api/src/services/flow-copy.ts",
1391
1397
  "apps/api/src/bin/listing-csv-sweep.ts",
1392
1398
  "apps/api/src/bin/followup-sweep.ts",
1393
1399
  "apps/api/src/bin/seed-waitlist-experiments.ts",
@@ -26,6 +26,15 @@ export declare const RESERVED_SHAPES: readonly ["local-business-product"];
26
26
  /** The `--design` vocabulary (#6754 D1) — `STARTER_DESIGNS`, pinned by test. */
27
27
  export declare const RECOGNIZED_DESIGNS: readonly ["standard", "minimalism"];
28
28
  export type DesignName = (typeof RECOGNIZED_DESIGNS)[number];
29
+ /**
30
+ * The `--topology` vocabulary (#7426 D1) — orthogonal to `--shape`: any shape
31
+ * can be scaffolded as either. Declared here rather than imported from
32
+ * `topology.ts` for the reason `RECOGNIZED_MODULES` is: that module reads
33
+ * `MODULE_SLICES`, and importing it here closes a runtime cycle through
34
+ * `lib/module-tier.ts`. `spoke-topology.test.ts` pins the two lists equal.
35
+ */
36
+ export declare const RECOGNIZED_TOPOLOGIES: readonly ["hub", "spoke"];
37
+ export type TopologyName = (typeof RECOGNIZED_TOPOLOGIES)[number];
29
38
  export interface CliOptions {
30
39
  /** Positional target directory (the dir to scaffold into). Optional only
31
40
  * when `help` or `version` is set. */
@@ -94,6 +103,19 @@ export interface CliOptions {
94
103
  * flag is.
95
104
  */
96
105
  license?: string;
106
+ /**
107
+ * The scaffold's topology (`--topology`, #7426 D1). `hub` — the default,
108
+ * and the only value before #7426 — unless `--topology spoke`.
109
+ */
110
+ topology: TopologyName;
111
+ /**
112
+ * The raw `--hub-url` value (#7426 D2), present only on a spoke run that
113
+ * passed it. `scaffold()` validates and normalizes it, falling back to
114
+ * `AILK_API_URL`.
115
+ */
116
+ hubUrl?: string;
117
+ /** Spoke only: `false` with `--without-mcp` (#7426 D3), else `true`. */
118
+ mcp?: boolean;
97
119
  }
98
120
  export declare class ParseError extends Error {
99
121
  constructor(message: string);
@@ -59,6 +59,14 @@ const SHAPE_DESCRIPTIONS = {
59
59
  "person-service": "a person selling their own service",
60
60
  };
61
61
  const SHAPE_LIST = RECOGNIZED_SHAPES.join(", ");
62
+ /**
63
+ * The `--topology` vocabulary (#7426 D1) — orthogonal to `--shape`: any shape
64
+ * can be scaffolded as either. Declared here rather than imported from
65
+ * `topology.ts` for the reason `RECOGNIZED_MODULES` is: that module reads
66
+ * `MODULE_SLICES`, and importing it here closes a runtime cycle through
67
+ * `lib/module-tier.ts`. `spoke-topology.test.ts` pins the two lists equal.
68
+ */
69
+ export const RECOGNIZED_TOPOLOGIES = ["hub", "spoke"];
62
70
  export class ParseError extends Error {
63
71
  constructor(message) {
64
72
  super(message);
@@ -158,6 +166,18 @@ Options:
158
166
  --license <key> Your licence key: fetch paid modules with it and
159
167
  write it into the scaffold (apps/api/.env and a
160
168
  gitignored ailk.key, both owner-only).
169
+ --topology <hub|spoke> hub (the default) scaffolds the whole site: web,
170
+ API, MCP and database. spoke scaffolds a web-only
171
+ site that sends its forms, flows and listings to a
172
+ hub — no apps/api, no database, no sign-in, and
173
+ apps/mcp kept as a stateless front. Composes with
174
+ any --shape or --template. A spoke carries
175
+ marketing and docs-blog.
176
+ --hub-url <origin> A spoke's hub, e.g. https://hub.example.com (https:,
177
+ or http: on localhost). Defaults to AILK_API_URL;
178
+ a spoke with neither is refused. Written to
179
+ apps/web/.env.local — the spoke key never is.
180
+ --without-mcp A spoke without apps/mcp.
161
181
  --install After scaffolding, run \`pnpm install\` in the
162
182
  target directory. Off by default.
163
183
  --help, -h Print this message.
@@ -194,6 +214,9 @@ export function parseCliArgs(argv, env = { interactive: false }) {
194
214
  shape: { type: "string" },
195
215
  design: { type: "string" },
196
216
  license: { type: "string" },
217
+ topology: { type: "string" },
218
+ "hub-url": { type: "string" },
219
+ "without-mcp": { type: "boolean", default: false },
197
220
  surfaces: { type: "string" },
198
221
  "sync-architecture": { type: "string" },
199
222
  "sync-catalog": { type: "boolean", default: false },
@@ -224,6 +247,10 @@ export function parseCliArgs(argv, env = { interactive: false }) {
224
247
  const syncClaudeMd = parsed.values["sync-claude-md"] === true;
225
248
  const withCatalog = parsed.values["with-catalog"] === true;
226
249
  const license = parseLicense(parsed.values.license);
250
+ const rawTopology = parsed.values.topology;
251
+ const topology = parseTopology(rawTopology);
252
+ const hubUrl = parsed.values["hub-url"];
253
+ const withoutMcp = parsed.values["without-mcp"] === true;
227
254
  // The sync modes each exit after rewriting their one file, so asking for
228
255
  // more than one in one invocation has no coherent meaning.
229
256
  const syncFlags = [
@@ -234,6 +261,21 @@ export function parseCliArgs(argv, env = { interactive: false }) {
234
261
  if (syncFlags.length > 1) {
235
262
  throw new ParseError(`${syncFlags.join(" and ")} cannot be combined; run them one at a time`);
236
263
  }
264
+ // #7426 D1 — a sync reads the topology from the site's manifest; naming one
265
+ // here has no coherent meaning.
266
+ if (rawTopology !== undefined && syncFlags.length > 0) {
267
+ throw new ParseError(`--topology cannot be combined with ${syncFlags[0]}: a sync reads the site's topology from its .ailk-site-manifest.json`);
268
+ }
269
+ // #7426 D2/D3 — the spoke-only flags.
270
+ if (topology !== "spoke") {
271
+ const spokeOnly = [
272
+ hubUrl !== undefined ? "--hub-url" : undefined,
273
+ withoutMcp ? "--without-mcp" : undefined,
274
+ ].filter((f) => f !== undefined);
275
+ if (spokeOnly.length > 0) {
276
+ throw new ParseError(`${spokeOnly.join(" and ")} only apply${spokeOnly.length === 1 ? "s" : ""} to a spoke. Pass --topology spoke, or drop ${spokeOnly.length === 1 ? "it" : "them"}.`);
277
+ }
278
+ }
237
279
  // Validate the sync-architecture value now, through the SAME parseModules
238
280
  // path `--modules` uses, so an unknown module throws ParseError at parse time
239
281
  // (cli.ts re-parses the raw value to the module list in sync mode).
@@ -255,6 +297,7 @@ export function parseCliArgs(argv, env = { interactive: false }) {
255
297
  syncClaudeMd: false,
256
298
  withCatalog: false,
257
299
  syncArchitecture: undefined,
300
+ topology: "hub",
258
301
  };
259
302
  }
260
303
  // Sync modes: the target dir is optional (positional if given, else cwd).
@@ -275,6 +318,7 @@ export function parseCliArgs(argv, env = { interactive: false }) {
275
318
  syncClaudeMd,
276
319
  withCatalog,
277
320
  syncArchitecture,
321
+ topology: "hub",
278
322
  };
279
323
  }
280
324
  if (parsed.positionals.length === 0) {
@@ -292,6 +336,13 @@ export function parseCliArgs(argv, env = { interactive: false }) {
292
336
  : "--design";
293
337
  throw new ParseError(`--template cannot be combined with ${flags}: a template already fixes its shape and design. Pass --template alone, or --shape with an optional --design.`);
294
338
  }
339
+ // #7426 D2 — a spoke needs its hub's origin. Presence only, here: the value
340
+ // is validated and normalized by `scaffold()` (`topology.ts`
341
+ // `normalizeHubUrl`), still before the target directory exists.
342
+ if (topology === "spoke" &&
343
+ (hubUrl ?? process.env["AILK_API_URL"] ?? "").trim() === "") {
344
+ throw new ParseError("a spoke needs its hub's origin, and none was given. Pass --hub-url <origin>, or set AILK_API_URL (e.g. https://hub.example.com).");
345
+ }
295
346
  // R1 — no silent default: with neither a template nor a shape, ask on a
296
347
  // terminal and refuse without one.
297
348
  const needsShapePrompt = template === undefined && shape === undefined;
@@ -314,8 +365,22 @@ export function parseCliArgs(argv, env = { interactive: false }) {
314
365
  withCatalog,
315
366
  syncArchitecture: undefined,
316
367
  ...(license !== undefined ? { license } : {}),
368
+ topology,
369
+ ...(topology === "spoke"
370
+ ? { ...(hubUrl !== undefined ? { hubUrl } : {}), mcp: !withoutMcp }
371
+ : {}),
317
372
  };
318
373
  }
374
+ /** Validate `--topology` (#7426 D1): `hub` when absent, trimmed and lowercased. */
375
+ function parseTopology(raw) {
376
+ if (raw === undefined)
377
+ return "hub";
378
+ const name = raw.trim().toLowerCase();
379
+ if (!RECOGNIZED_TOPOLOGIES.includes(name)) {
380
+ throw new ParseError(`unknown topology "${raw}". Recognized: ${RECOGNIZED_TOPOLOGIES.join(", ")}`);
381
+ }
382
+ return name;
383
+ }
319
384
  /**
320
385
  * Validate `--shape` (#6754 D2 step 2): trimmed and lowercased. A reserved
321
386
  * shape is refusal R2; any other unrecognized value names the recognized set.
@@ -467,6 +532,14 @@ export function toScaffoldOptions(opts) {
467
532
  ...(opts.shape !== undefined ? { shape: opts.shape } : {}),
468
533
  ...(opts.design !== undefined ? { design: opts.design } : {}),
469
534
  ...(opts.license !== undefined ? { license: opts.license } : {}),
535
+ // #7426 — a hub run hands `scaffold()` the object it did before.
536
+ ...(opts.topology === "spoke"
537
+ ? {
538
+ topology: "spoke",
539
+ ...(opts.hubUrl !== undefined ? { hubUrl: opts.hubUrl } : {}),
540
+ mcp: opts.mcp !== false,
541
+ }
542
+ : {}),
470
543
  };
471
544
  }
472
545
  /**
@@ -22,6 +22,7 @@ import { type ModuleName } from "./parse-args.js";
22
22
  import { type TemplateLayout } from "./site-chrome.js";
23
23
  import { type SyncRoutesResult } from "./sync-routes.js";
24
24
  import { type StarterShape } from "./template-manifest.js";
25
+ import { type Topology } from "./topology.js";
25
26
  export interface ScaffoldOptions {
26
27
  /** Absolute or relative path to the target directory. Must not exist or be empty. */
27
28
  targetDir: string;
@@ -136,6 +137,18 @@ export interface ScaffoldOptions {
136
137
  * code registry that has since been deleted (#6638's per-name table).
137
138
  */
138
139
  designLayout?: TemplateLayout;
140
+ /**
141
+ * The scaffold's topology (`--topology`, #7426 D1). `"hub"` — the default
142
+ * — is the whole site. `"spoke"` is the same scaffold with nodes removed
143
+ * (`topology.ts`): no `apps/api`, no `database/`, no sign-in, `apps/mcp`
144
+ * a stateless front unless `mcp: false`. A spoke needs a `template` or a
145
+ * `shape` (the manifest records the topology) and a hub origin.
146
+ */
147
+ topology?: Topology;
148
+ /** A spoke's hub origin (`--hub-url`), else `AILK_API_URL` (#7426 D2). */
149
+ hubUrl?: string;
150
+ /** Spoke only: `false` drops `apps/mcp` (`--without-mcp`, #7426 D3). Default true. */
151
+ mcp?: boolean;
139
152
  }
140
153
  export interface ScaffoldResult {
141
154
  /** Resolved absolute path to the scaffolded directory. */
@@ -161,6 +174,13 @@ export interface ScaffoldResult {
161
174
  * a keyed run with no `--modules` carries the key's derived level here.
162
175
  */
163
176
  modules: ModuleName[];
177
+ /**
178
+ * A spoke only (#7426 D7): modules the key's default set carried that a
179
+ * spoke does not admit, dropped from this scaffold. Absent when none were.
180
+ */
181
+ droppedModules?: ModuleName[];
182
+ /** A spoke only (#7426 D2): the normalized hub origin written to apps/web/.env.local. */
183
+ hubOrigin?: string;
164
184
  }
165
185
  /**
166
186
  * Scaffold an AI Launch Kit starter project into targetDir (synchronous).