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
@@ -15,33 +15,36 @@
15
15
  * import { scaffoldSync } from './programmatic.js';
16
16
  * const result = scaffoldSync({ targetDir: '/tmp/my-site', projectName: 'my-site' });
17
17
  */
18
- import { existsSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
18
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
19
19
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
- import { AddModuleRefusedError, applyModuleToScaffold, } from "./add-module.js";
21
+ import { AddModuleRefusedError, addModuleSiteTopology, applyModuleToScaffold, } from "./add-module.js";
22
22
  import { planDesignMd } from "./apply-design-md.js";
23
23
  import { renderClaudeMd, syncClaudeMd, } from "./claude-md.js";
24
24
  import { CATALOG_TARGET_PATH, writeComponentCatalog, } from "./component-catalog.js";
25
25
  import { buildScaffoldMarkers, copyTemplate } from "./copy.js";
26
26
  import { deriveArchitecture } from "./derive-architecture.js";
27
- import { compileDesignTheme } from "./design-compile.js";
27
+ import { compileDesignFonts, compileDesignTheme } from "./design-compile.js";
28
+ import { HOME_PAGE_PATH, designHomeSeeds, planDesignHome, shapeHomePath, shippedHomeSeeds, } from "./design-home.js";
28
29
  import { applyDesignPresets, readSectionLookCatalog, shippedPageTypes, validateDesignPresets, } from "./design-presets.js";
29
- import { installDesignSample, resolveDesign, spliceThemeBlock, } from "./design-scaffold.js";
30
+ import { resolveDesign, spliceThemeBlock, } from "./design-scaffold.js";
30
31
  import { writeAuthSecretFile } from "./lib/auth-secret-file.js";
31
32
  import { extractModuleBundle, installBundleTree, } from "./lib/extract-module-bundle.js";
32
33
  import { fetchTemplate } from "./lib/fetch-template.js";
33
34
  import { assertLicenseKeyShape, writeLicenseKeyFiles, } from "./lib/license-key-files.js";
34
- import { assertDesignAllowed, assertReleased, deriveLevel, resolveModuleSet, resolvePaidModules, } from "./lib/module-license-gate.js";
35
+ import { assertDesignAllowed, assertReleased, deriveDesignLevel, deriveLevel, resolveModuleSet, resolvePaidModules, } from "./lib/module-license-gate.js";
35
36
  import { classifyModules } from "./lib/module-tier.js";
36
37
  import { normalizeModules } from "./module-architecture.js";
37
38
  import { writePaidSections } from "./paid-sections.js";
38
39
  import { RECOGNIZED_SHAPES, RESERVED_SHAPES, } from "./parse-args.js";
39
40
  import { SITE_CHROME_PATH, renderSiteChrome, resolveSiteChrome, } from "./site-chrome.js";
41
+ import { SITE_FONTS_PATH, renderSiteFonts } from "./site-fonts.js";
40
42
  import { planSiteManifest } from "./site-manifest.js";
41
- import { SITE_ROSTER_PATH, planSiteManifestDesign, readSiteRoster, withMeetPlaceholderSeeds, } from "./site-roster.js";
43
+ import { SITE_ROSTER_PATH, planHomeRecord, planSiteManifestDesign, readSiteRoster, withMeetPlaceholderSeeds, } from "./site-roster.js";
42
44
  import { GATEABLE_SURFACES, MODULE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
43
45
  import { syncRoutes } from "./sync-routes.js";
44
46
  import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, TemplateError, mergeRoster, resolveTemplate, resolveTemplateLineage, rosterDeclares, templateDelivery, templateTreeDir, } from "./template-manifest.js";
47
+ import { TopologyError, applySpokeTransforms, isSpokeExcluded, normalizeHubUrl, readSiteTopology, spokeExclusions, spokeModuleRefusal, spokeModules, writeHubOriginEnv, } from "./topology.js";
45
48
  import { validateTarget } from "./validate.js";
46
49
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
47
50
  const TEMPLATES_DIR = resolve(PKG_ROOT, "templates");
@@ -149,14 +152,23 @@ export function scaffoldSync(opts) {
149
152
  // The declared route surface (#4154). Resolved BEFORE the copy: it decides
150
153
  // both which route directories are skipped and what architecture.yaml records.
151
154
  const enabledSurfaces = resolveSurfaces(opts.modules ?? ["marketing"], opts.surfaces ?? []);
155
+ // #7426 D4 — a spoke is this same copy with the spoke's exclusions added:
156
+ // one registry over the one template tree, never a second tree.
157
+ const spoke = opts.topology === "spoke" ? { mcp: opts.mcp !== false } : undefined;
152
158
  const versions = readVersionMap();
153
159
  copyTemplate({
154
160
  templatesDir: TEMPLATES_DIR,
155
161
  targetDir: targetAbs,
156
162
  versions,
157
- excludePaths: excludedRoutePaths(enabledSurfaces),
163
+ excludePaths: [
164
+ ...excludedRoutePaths(enabledSurfaces),
165
+ ...(spoke !== undefined ? spokeExclusions(TEMPLATES_DIR, spoke) : []),
166
+ ],
158
167
  markers: buildScaffoldMarkers(projectName),
159
168
  });
169
+ // #7426 D5/D6 — the root files a spoke rewrites, and the spoke seams.
170
+ if (spoke !== undefined)
171
+ applySpokeTransforms(targetAbs, spoke);
160
172
  // Stamp the copied CLAUDE.md's template regions with their hashes (#6904
161
173
  // D2). No later layer writes a root CLAUDE.md, so this is the final file.
162
174
  writeFileSync(join(targetAbs, "CLAUDE.md"), renderClaudeMd(readFileSync(join(TEMPLATES_DIR, "CLAUDE.md"), "utf8"), projectName));
@@ -165,7 +177,7 @@ export function scaffoldSync(opts) {
165
177
  // closes the dangling `architecture_rules: architecture.yaml` reference in
166
178
  // the scaffolded project.yaml. Written AFTER the template copy, at the target
167
179
  // root — a deterministic function of the selected module set.
168
- writeFileSync(join(targetAbs, "architecture.yaml"), deriveArchitecture(opts.modules ?? ["marketing"], enabledSurfaces));
180
+ writeFileSync(join(targetAbs, "architecture.yaml"), deriveArchitecture(opts.modules ?? ["marketing"], enabledSurfaces, spoke !== undefined ? { topology: "spoke", mcp: spoke.mcp } : {}));
169
181
  // Regenerate the site nav from the same surface set (#4154). The nav is a
170
182
  // static list the layout renders into the header AND the footer, so leaving
171
183
  // the template's copy in place would link /docs and /blog sitewide in a
@@ -250,6 +262,9 @@ export async function scaffold(opts) {
250
262
  ? resolveShapeTemplate(opts.shape)
251
263
  : undefined;
252
264
  assertReleased("modules", [...(opts.modules ?? []), ...(template?.requires ?? [])]);
265
+ // #7426 D1-D3/D7 — a spoke's refusals, before anything is fetched or
266
+ // written: the target directory never exists for a refused spoke.
267
+ const spoke = resolveSpoke(opts, template);
253
268
  // #6813 D8 step 3 — the design this scaffold renders, resolved from its
254
269
  // package ONCE, before any fetch or write: the caller's override, else the
255
270
  // template's own. Unset for a scaffold naming neither. Replaces the
@@ -267,14 +282,22 @@ export async function scaffold(opts) {
267
282
  // (D2): a non-look field or unknown variant throws `DesignPackageError`
268
283
  // naming page type, section type and field, and the target is never
269
284
  // created. A design with no `sections` block executes no new read (D8).
285
+ let lookCatalog;
286
+ const catalog = () => (lookCatalog ??= readSectionLookCatalog(join(PKG_ROOT, "component-catalog.json")));
270
287
  if (resolvedDesign !== undefined &&
271
288
  Object.keys(resolvedDesign.pkg.sections).length > 0) {
272
289
  validateDesignPresets(resolvedDesign.pkg.sections, {
273
- catalog: readSectionLookCatalog(join(PKG_ROOT, "component-catalog.json")),
290
+ catalog: catalog(),
274
291
  pageTypes: shippedPageTypes(SITE_TEMPLATES_DIR),
275
292
  file: join(resolvedDesign.dir, "DESIGN.md"),
276
293
  });
277
294
  }
295
+ // #7743 D5 — the design's home seeds are read and validated beside its
296
+ // presets: a malformed seed throws `DesignPackageError`, naming the file,
297
+ // before the target is created.
298
+ const designSeeds = resolvedDesign !== undefined
299
+ ? designHomeSeeds(resolvedDesign, { designsDir: DESIGNS_DIR, catalog })
300
+ : [];
278
301
  // #6813 D7 — the resolved design only when the CALLER chose one (`opts.design`);
279
302
  // a template's own declared design is never gated (#6778 option (c)).
280
303
  const chosenDesign = opts.design !== undefined ? resolvedDesign : undefined;
@@ -298,10 +321,12 @@ export async function scaffold(opts) {
298
321
  // list.
299
322
  let modules;
300
323
  let bundles;
324
+ // #7426 D7 — the key's default modules a spoke does not admit.
325
+ let dropped = [];
301
326
  // The run's derived level (#6759), for `assertDesignAllowed` below.
302
327
  // Undefined on the alias branch and on a `--modules`-keep run (D2/D4 in
303
328
  // module-license-gate.ts — the level plays no part in composing either),
304
- // so a non-`standard` design on either path derives it separately, below.
329
+ // so a non-`standard` design on either path derives it separately.
305
330
  let level;
306
331
  if (opts.template !== undefined) {
307
332
  // Alias run — untouched (NFR-4): the union line and its
@@ -350,6 +375,27 @@ export async function scaffold(opts) {
350
375
  });
351
376
  bundles = chosen.bundles;
352
377
  level = chosen.level;
378
+ // #7426 D7 — a key-derived default set keeps only what a spoke admits;
379
+ // the run names what it dropped (`droppedModules`).
380
+ if (spoke !== undefined && opts.modules === undefined) {
381
+ const admitted = new Set(spokeModules());
382
+ dropped = chosen.modules.filter((m) => !admitted.has(m));
383
+ chosen.modules = chosen.modules.filter((m) => admitted.has(m));
384
+ for (const name of dropped)
385
+ bundles.delete(name);
386
+ }
387
+ // #7010 — a `--modules`-keep run composes no level (NFR-2), so a chosen
388
+ // non-Free design derives one from the key here, as the addition branch
389
+ // does; a plain keep run makes no extra network call.
390
+ if (level === undefined && chosenDesign !== undefined && !chosenDesign.free) {
391
+ level = await deriveDesignLevel({
392
+ licenseKey,
393
+ ...(opts.moduleRegistryUrl !== undefined
394
+ ? { registryUrl: opts.moduleRegistryUrl }
395
+ : {}),
396
+ ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
397
+ });
398
+ }
353
399
  modules =
354
400
  template !== undefined
355
401
  ? normalizeModules([...template.requires, ...chosen.modules])
@@ -378,9 +424,9 @@ export async function scaffold(opts) {
378
424
  }
379
425
  // #6760 D4 — after the level is resolved, before any fetch or write below.
380
426
  // Only the caller's chosen design is gated (#6778 option (c)); a template's
381
- // own declared design is not. `level ?? "free"` fails closed: a
382
- // `--modules`-keep run under-reports its level by design (NFR-2), so a
383
- // chosen non-Free design on that path is refused rather than trusted. The
427
+ // own declared design is not. `level ?? "free"` fails closed: a level left
428
+ // underived (a plain alias or `--modules`-keep run with no chosen non-Free
429
+ // design) is never trusted. The
384
430
  // refusal names `chosen.display` — the name, or the absolute directory
385
431
  // (#6813 D7) — and `chosen.free` decides Free-ness by identity (D2), never
386
432
  // by `FREE_DESIGNS.includes(name)` alone (a path is never in that list).
@@ -431,6 +477,7 @@ export async function scaffold(opts) {
431
477
  const result = scaffoldSync({
432
478
  ...opts,
433
479
  modules,
480
+ ...(spoke !== undefined ? { topology: "spoke", mcp: spoke.mcp } : {}),
434
481
  ...(surfaces !== undefined ? { surfaces } : {}),
435
482
  // #5612 D1/D2 — the code registry's own `layout` block, known
436
483
  // synchronously (unlike a bundle's `ailk-template.json`, only readable
@@ -464,6 +511,14 @@ export async function scaffold(opts) {
464
511
  // anything. Starts empty at the base-layer boundary: the `templates/`
465
512
  // snapshot (layer 0) is deliberately outside it.
466
513
  const ownership = new Map();
514
+ // #7426 D4 — every layer below lands through the spoke filter: a module
515
+ // bundle's or the template's destination a spoke does not carry is dropped.
516
+ const exclusions = spoke !== undefined ? spokeExclusions(TEMPLATES_DIR, spoke) : [];
517
+ const spokeSkip = spoke !== undefined
518
+ ? {
519
+ skip: (destination) => isSpokeExcluded(destination, { mcp: spoke.mcp, exclusions }),
520
+ }
521
+ : {};
467
522
  // The BASE LAYER lands first (#5618 D1) — the seed files every template
468
523
  // reaches by `extends` (today the three module-less base pages), read off
469
524
  // the `site-templates/_base/` tree this package ships, through the same
@@ -483,6 +538,7 @@ export async function scaffold(opts) {
483
538
  versions,
484
539
  markers,
485
540
  ownership,
541
+ ...spokeSkip,
486
542
  });
487
543
  }
488
544
  for (const [moduleName, bytes] of bundles) {
@@ -493,6 +549,7 @@ export async function scaffold(opts) {
493
549
  versions,
494
550
  markers,
495
551
  ownership,
552
+ ...spokeSkip,
496
553
  });
497
554
  }
498
555
  // The template lands LAST — after `scaffoldSync` and after every module
@@ -528,6 +585,7 @@ export async function scaffold(opts) {
528
585
  versions,
529
586
  markers,
530
587
  ownership,
588
+ ...spokeSkip,
531
589
  })
532
590
  : (() => {
533
591
  const sourceRoot = join(SITE_TEMPLATES_DIR, templateTreeDir(template.name));
@@ -543,6 +601,7 @@ export async function scaffold(opts) {
543
601
  versions,
544
602
  markers,
545
603
  ownership,
604
+ ...spokeSkip,
546
605
  });
547
606
  })();
548
607
  installedLayout = installed.layout;
@@ -568,22 +627,6 @@ export async function scaffold(opts) {
568
627
  : [];
569
628
  writeFileSync(join(result.targetDir, NAV_CONFIG_PATH), renderNavConfig(result.surfaces, { serviceSlugs }));
570
629
  pruneUnrosteredSeeds(result.targetDir, mergeRoster(template, template.requires), result.surfaces, ownership);
571
- // spec 6892 D4 — the resolved design's own sample (the minimalism design's
572
- // nine-slide deck home + its SVGs; no template declares `minimalism`
573
- // since #6976, so none installs it today), installed AFTER the template
574
- // layer so it wins over the shape's `HomePage/home.mdx`, and ONLY when
575
- // the resolved design is the template's OWN declared design: a caller
576
- // combination like `--shape organization-product --design minimalism`
577
- // installs no sample (its roster carries neither the deck's link targets
578
- // nor, in its generated record, the deck's sections).
579
- if (resolvedDesign !== undefined &&
580
- resolvedDesign.recorded === template.design) {
581
- installDesignSample({
582
- designDir: resolvedDesign.dir,
583
- targetDir: result.targetDir,
584
- ownership,
585
- });
586
- }
587
630
  }
588
631
  // #6813 D5/D8 steps 9-10 — whenever a design is resolved (the caller's,
589
632
  // else the template's): the design's DESIGN.md (#6939); the compiled theme,
@@ -595,6 +638,15 @@ export async function scaffold(opts) {
595
638
  // #6939 — the scaffold's DESIGN.md is the design it wears, not the one
596
639
  // its template layer shipped; the site's own header lines are kept.
597
640
  writeFileSync(join(result.targetDir, "DESIGN.md"), planDesignMd(resolvedDesign.dir, result.targetDir));
641
+ // #7720 D4.1 — the faces the design sources, beside site-chrome.ts. A
642
+ // design that sources none renders the overlay's committed default, so
643
+ // the file is left as the template tree wrote it.
644
+ const siteFontsPath = join(result.targetDir, SITE_FONTS_PATH);
645
+ const siteFonts = renderSiteFonts(compileDesignFonts(resolvedDesign.pkg));
646
+ if (!existsSync(siteFontsPath) || readFileSync(siteFontsPath, "utf8") !== siteFonts) {
647
+ mkdirSync(dirname(siteFontsPath), { recursive: true });
648
+ writeFileSync(siteFontsPath, siteFonts);
649
+ }
598
650
  const compiledTheme = compileDesignTheme(resolvedDesign.pkg);
599
651
  const siteMdxPath = join(result.targetDir, "content", "_site.mdx");
600
652
  if (existsSync(siteMdxPath)) {
@@ -613,6 +665,27 @@ export async function scaffold(opts) {
613
665
  writeFileSync(rosterPath, nextManifest);
614
666
  }
615
667
  }
668
+ // #7743 D5 — the design's home arc, seeded over a placeholder home once
669
+ // every layer has landed, before the presets dress it. A home that is not
670
+ // the shape's placeholder is never written; `--design standard` (no
671
+ // seeds) leaves the shape's home byte-identical. The one
672
+ // `planDesignHome` call site in scaffold().
673
+ const homePath = join(result.targetDir, ...HOME_PAGE_PATH.split("/"));
674
+ let homeSeeded = false;
675
+ if (template !== undefined) {
676
+ const homePlan = planDesignHome({
677
+ current: existsSync(homePath) ? readFileSync(homePath, "utf8") : undefined,
678
+ shape: template.shape,
679
+ target: designSeeds.find((seed) => seed.shape === template.shape),
680
+ shapeHome: readFileSync(shapeHomePath(template.shape, { siteTemplatesDir: SITE_TEMPLATES_DIR, templatesDir: TEMPLATES_DIR }), "utf8"),
681
+ shippedSeeds: shippedHomeSeeds({ designsDir: DESIGNS_DIR, catalog }),
682
+ catalog: catalog(),
683
+ });
684
+ if (homePlan.outcome === "seeded" && homePlan.text !== undefined) {
685
+ writeFileSync(homePath, homePlan.text);
686
+ homeSeeded = true;
687
+ }
688
+ }
616
689
  // #6816 D6 step 10b — the design's section presets, written onto the
617
690
  // pages of the page types the roster of record places, once every layer
618
691
  // has landed. The one `applyDesignPresets` call site. A scaffold with no
@@ -637,6 +710,14 @@ export async function scaffold(opts) {
637
710
  writeFileSync(page.path, page.text);
638
711
  }
639
712
  }
713
+ // #7743 D6 — the manifest's `/` row records the seeded home's sections,
714
+ // as the presets left them.
715
+ const rosterPath = join(result.targetDir, SITE_ROSTER_PATH);
716
+ if (homeSeeded && existsSync(rosterPath)) {
717
+ const nextManifest = planHomeRecord(readFileSync(rosterPath, "utf8"), readFileSync(homePath, "utf8"), catalog());
718
+ if (nextManifest !== undefined)
719
+ writeFileSync(rosterPath, nextManifest);
720
+ }
640
721
  }
641
722
  // #6944 — the manifest records what this scaffold IS, once every layer has
642
723
  // landed: the effective module set, a row for every route on disk, and the
@@ -652,6 +733,8 @@ export async function scaffold(opts) {
652
733
  roster,
653
734
  modules: result.modules,
654
735
  level,
736
+ topology: spoke !== undefined ? "spoke" : "hub",
737
+ patternVersion: markers["CREATE_AILK_VERSION"] ?? "",
655
738
  siteTemplatesDir: SITE_TEMPLATES_DIR,
656
739
  templatesDir: TEMPLATES_DIR,
657
740
  markers,
@@ -663,20 +746,70 @@ export async function scaffold(opts) {
663
746
  writePaidSections(result.targetDir);
664
747
  // #6107 D2 — after every layer landed, so nothing later overwrites it: a
665
748
  // served scaffold needs BETTER_AUTH_SECRET (#6044), and this is its value.
666
- writeAuthSecretFile(result.targetDir);
749
+ // A spoke has no sign-in (#7426 D4), so it gets its hub's origin instead
750
+ // (D2) — never the spoke key, which the hub issues.
751
+ if (spoke !== undefined)
752
+ writeHubOriginEnv(result.targetDir, spoke.hubOrigin);
753
+ else
754
+ writeAuthSecretFile(result.targetDir);
667
755
  // #6683 D9 — persisted only after every layer landed, and only when the
668
- // caller passed `license`: a scaffold without it writes nothing new.
756
+ // caller passed `license`: a scaffold without it writes nothing new. A
757
+ // spoke has no apps/api, so it keeps the key in ailk.key alone (#7426 D6).
669
758
  if (opts.license !== undefined) {
670
- writeLicenseKeyFiles(result.targetDir, opts.license);
759
+ writeLicenseKeyFiles(result.targetDir, opts.license, {
760
+ apiEnv: spoke === undefined,
761
+ });
671
762
  }
672
763
  // #6754 D4/D5 — the resolved starter, so the CLI names what it built.
764
+ const withDropped = {
765
+ ...result,
766
+ ...(dropped.length > 0 ? { droppedModules: dropped } : {}),
767
+ ...(spoke !== undefined ? { hubOrigin: spoke.hubOrigin } : {}),
768
+ };
673
769
  if (template !== undefined && resolvedDesign !== undefined) {
674
770
  return {
675
- ...result,
771
+ ...withDropped,
676
772
  starter: { template: template.name, shape: template.shape, design: resolvedDesign.recorded },
677
773
  };
678
774
  }
679
- return result;
775
+ return withDropped;
776
+ }
777
+ /**
778
+ * A spoke's resolved inputs (#7426 D1-D3/D7), or undefined for a hub. Every
779
+ * refusal is a `TemplateError` thrown before anything is fetched or written.
780
+ */
781
+ function resolveSpoke(opts, template) {
782
+ if (opts.topology !== "spoke") {
783
+ const spokeOnly = [
784
+ opts.hubUrl !== undefined ? "hubUrl" : undefined,
785
+ opts.mcp !== undefined ? "mcp" : undefined,
786
+ ].filter((f) => f !== undefined);
787
+ if (spokeOnly.length > 0) {
788
+ throw new TemplateError(`${spokeOnly.join(" and ")} only apply to a spoke — pass topology: "spoke"`);
789
+ }
790
+ return undefined;
791
+ }
792
+ if (template === undefined) {
793
+ throw new TemplateError("a spoke needs a starter — pass --shape <shape> or --template <name>: the site manifest is where the topology is recorded");
794
+ }
795
+ let hubOrigin;
796
+ try {
797
+ hubOrigin = normalizeHubUrl(opts.hubUrl ?? process.env["AILK_API_URL"]);
798
+ }
799
+ catch (err) {
800
+ if (err instanceof TopologyError)
801
+ throw new TemplateError(err.message);
802
+ throw err;
803
+ }
804
+ for (const name of normalizeModules([...(opts.modules ?? []), ...template.requires])) {
805
+ const refusal = spokeModuleRefusal(name);
806
+ if (refusal !== undefined) {
807
+ throw new TemplateError(template.requires.includes(name) && !(opts.modules ?? []).includes(name)
808
+ ? `template "${template.name}" requires ${refusal}`
809
+ : refusal);
810
+ }
811
+ }
812
+ return { mcp: opts.mcp !== false, hubOrigin };
680
813
  }
681
814
  /**
682
815
  * The per-row install of the two SHARED seed layers (#6646 D1, disposition
@@ -798,7 +931,12 @@ export function syncArchitectureSync(opts) {
798
931
  throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
799
932
  }
800
933
  const enabledSurfaces = resolveSurfaces(opts.modules, opts.surfaces ?? []);
801
- writeFileSync(archPath, deriveArchitecture(opts.modules, enabledSurfaces));
934
+ // #7426 D13 — a spoke stays a spoke: its topology is read from its
935
+ // manifest, and `mcp` from whether it still carries apps/mcp.
936
+ const topology = readSiteTopology(targetAbs);
937
+ writeFileSync(archPath, deriveArchitecture(opts.modules, enabledSurfaces, topology === "spoke"
938
+ ? { topology, mcp: existsSync(join(targetAbs, "apps", "mcp")) }
939
+ : {}));
802
940
  return {
803
941
  targetDir: targetAbs,
804
942
  surfaces: enabledSurfaces,
@@ -916,6 +1054,13 @@ export async function addModule(opts) {
916
1054
  if (!existsSync(join(targetAbs, "architecture.yaml"))) {
917
1055
  throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
918
1056
  }
1057
+ // #7426 D13 — a spoke refuses a module it does not admit before any key
1058
+ // read or network call (`applyModuleToScaffold` refuses it again).
1059
+ if (addModuleSiteTopology(targetAbs) === "spoke") {
1060
+ const refusal = spokeModuleRefusal(opts.moduleName);
1061
+ if (refusal !== undefined)
1062
+ throw new AddModuleRefusedError(refusal);
1063
+ }
919
1064
  const { paid } = classifyModules([opts.moduleName]);
920
1065
  if (paid.length === 0) {
921
1066
  throw new AddModuleRefusedError(`"${opts.moduleName}" ships with every scaffold — there is no module bundle to add`);
@@ -38,6 +38,24 @@ export type SiteChromeFooter = "columns" | "centered";
38
38
  /** The `home` knob's vocabulary. `deck` → the locale-root route mounts
39
39
  * MarketingShell with `snap="mandatory"` + `deckHero` (spec 5426 D7). */
40
40
  export type SiteChromeHome = "default" | "deck";
41
+ /** The `innerPages` knob's vocabulary (#7721). `bare` → every non-home page
42
+ * gets the bare header and the one-bar footer. */
43
+ export type SiteChromeInnerPages = "default" | "bare";
44
+ /** The `colorMode` knob's vocabulary (#7742). `light` pins the site to light
45
+ * and removes every theme switch; dark then appears only as a deck ground. */
46
+ export type SiteChromeColorMode = "user" | "light";
47
+ /** The `chromeStyle` knob's vocabulary (#7742). `minimal` → the deck's closing
48
+ * slide is the wordmark + contact lines + one legal line, and the consent bar
49
+ * is a ground-coloured hairline bar that never covers the down cue. */
50
+ export type SiteChromeChromeStyle = "default" | "minimal";
51
+ /**
52
+ * The `deckGrounds` knob's vocabulary (#7719 D3) — the shared section surface
53
+ * enum, duplicated from `@working-theory/validation`'s `SectionSurfaceSchema`
54
+ * because `create-ailk` cannot import it (one runtime dependency, § 1.5);
55
+ * `site-chrome.test.ts` proves the two lists equal.
56
+ */
57
+ export declare const DECK_GROUND_VALUES: readonly ["none", "gray-100", "wash", "wash-secondary", "gradient-brand", "gradient-dark", "solid-dark"];
58
+ export type DeckGround = (typeof DECK_GROUND_VALUES)[number];
41
59
  /**
42
60
  * A template's declared layout block — every field optional. Lives on
43
61
  * `TemplateManifest.layout` (the code registry, `template-manifest.ts`) and
@@ -48,6 +66,18 @@ export interface TemplateLayout {
48
66
  footer?: SiteChromeFooter;
49
67
  announcementBanner?: boolean;
50
68
  home?: SiteChromeHome;
69
+ /** #7719 D3 — the surfaces a deck home cycles, slide by slide, onto slides that author none. */
70
+ deckGrounds?: readonly DeckGround[];
71
+ /** #7721 D1 — `bare`: every non-home page wears the bare header and the one-bar footer. */
72
+ innerPages?: SiteChromeInnerPages;
73
+ /** #7740 D1 — `true`: the deck home presents slide by slide (a deck home only). */
74
+ deckPresentation?: boolean;
75
+ /** #7742 D1 — `light`: the site is pinned to light, with no theme switch. */
76
+ colorMode?: SiteChromeColorMode;
77
+ /** #7742 D1 — `minimal`: the minimal closing slide, consent bar and themed 404. */
78
+ chromeStyle?: SiteChromeChromeStyle;
79
+ /** #7797 D1 — route paths whose page ends on the closing footer (wordmark, contact lines, legal line). */
80
+ closingFooterPaths?: readonly string[];
51
81
  }
52
82
  /** `TemplateLayout` with every field resolved — what `site-chrome.ts` exports. */
53
83
  export interface ResolvedSiteChrome {
@@ -55,6 +85,18 @@ export interface ResolvedSiteChrome {
55
85
  footer: SiteChromeFooter;
56
86
  announcementBanner: boolean;
57
87
  home: SiteChromeHome;
88
+ /** Present only when a list resolves AND `home` is `deck` (#7719 D3) — never an `undefined` key. */
89
+ deckGrounds?: readonly DeckGround[];
90
+ /** Present only when it resolves to "bare" — never "default" (#7721 D1). */
91
+ innerPages?: "bare";
92
+ /** Present only when it resolves to `true` AND `home` is `deck` (#7740 D1) — never `false`. */
93
+ deckPresentation?: true;
94
+ /** Present only when it resolves to "light" — never "user" (#7742 D1). */
95
+ colorMode?: "light";
96
+ /** Present only when it resolves to "minimal" — never "default" (#7742 D1). */
97
+ chromeStyle?: "minimal";
98
+ /** Present only when a non-empty list resolves — never `[]` (#7797 D1). */
99
+ closingFooterPaths?: readonly string[];
58
100
  }
59
101
  /** Defaults (#5612 D1; banner off since #6469). */
60
102
  export declare const DEFAULT_SITE_CHROME: ResolvedSiteChrome;