create-ailk 0.6.1 → 0.7.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 (232) hide show
  1. package/README.md +43 -4
  2. package/component-catalog.json +146 -21
  3. package/component-catalog.md +9 -6
  4. package/dist/add-module-core.d.ts +22 -0
  5. package/dist/add-module-core.js +50 -0
  6. package/dist/add-module.d.ts +89 -0
  7. package/dist/add-module.js +334 -0
  8. package/dist/category-profile.d.ts +126 -0
  9. package/dist/category-profile.js +262 -0
  10. package/dist/cli.js +73 -11
  11. package/dist/index.d.ts +4 -2
  12. package/dist/index.js +6 -1
  13. package/dist/lib/extract-module-bundle.d.ts +56 -1
  14. package/dist/lib/extract-module-bundle.js +165 -13
  15. package/dist/lib/license-key-files.d.ts +15 -0
  16. package/dist/lib/license-key-files.js +102 -0
  17. package/dist/lib/pro-callout.d.ts +16 -10
  18. package/dist/lib/pro-callout.js +26 -3
  19. package/dist/license-set.d.ts +34 -0
  20. package/dist/license-set.js +194 -0
  21. package/dist/module-architecture.d.ts +20 -0
  22. package/dist/module-architecture.js +77 -19
  23. package/dist/modules/commerce/0.6.1.tar.gz +0 -0
  24. package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
  25. package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
  26. package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
  27. package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
  28. package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
  29. package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
  30. package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
  31. package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
  32. package/dist/modules/workspace/0.6.1.tar.gz +0 -0
  33. package/dist/parse-args.d.ts +7 -0
  34. package/dist/parse-args.js +23 -1
  35. package/dist/programmatic.d.ts +46 -0
  36. package/dist/programmatic.js +130 -9
  37. package/dist/site-chrome.d.ts +18 -7
  38. package/dist/site-chrome.js +32 -14
  39. package/dist/site-roster.d.ts +127 -0
  40. package/dist/site-roster.js +494 -0
  41. package/dist/surfaces.d.ts +20 -7
  42. package/dist/surfaces.js +149 -28
  43. package/dist/sync-routes.d.ts +9 -0
  44. package/dist/sync-routes.js +2 -2
  45. package/dist/template-manifest.d.ts +58 -2
  46. package/dist/template-manifest.js +196 -69
  47. package/package.json +6 -4
  48. package/site-templates/_base/content/en/BookCall/book-call.mdx +53 -0
  49. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +66 -0
  50. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +68 -0
  51. package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
  52. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +2 -3
  53. package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
  54. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +23 -0
  55. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +23 -0
  56. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +23 -0
  57. package/site-templates/designs/minimalism/DESIGN.md +162 -0
  58. package/site-templates/designs/standard/DESIGN.md +162 -0
  59. package/site-templates/marketing-site/DESIGN.md +55 -41
  60. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +312 -57
  61. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +209 -90
  62. package/site-templates/marketing-site/ailk-template.json +1 -0
  63. package/site-templates/marketing-site-free/DESIGN.md +55 -41
  64. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +260 -66
  65. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +294 -114
  66. package/site-templates/marketing-site-free/ailk-template.json +3 -2
  67. package/site-templates/minimalism-site/DESIGN.md +54 -40
  68. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
  69. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +185 -51
  70. package/site-templates/minimalism-site/ailk-template.json +3 -2
  71. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +53 -0
  72. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
  73. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
  74. package/site-templates/saas-product/DESIGN.md +55 -41
  75. package/site-templates/simple-service/DESIGN.md +55 -41
  76. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +135 -26
  77. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +114 -59
  78. package/site-templates/simple-service/ailk-template.json +4 -3
  79. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +53 -0
  80. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +1 -36
  81. package/site-templates/simple-service/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
  82. package/templates/.claude/settings.json +15 -0
  83. package/templates/.claude/skills/README.md +8 -8
  84. package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
  85. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
  86. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
  87. package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
  88. package/templates/.claude/skills/add-page/references/page-types.md +111 -0
  89. package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
  90. package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
  91. package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
  92. package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
  93. package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
  94. package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
  95. package/templates/.claude/skills/fill-page/SKILL.md +180 -0
  96. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
  97. package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
  98. package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +8 -8
  99. package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +2 -2
  100. package/templates/.claude/skills/{provision-config → setup-deploy}/references/runbook-template.md +8 -8
  101. package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
  102. package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
  103. package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
  104. package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
  105. package/templates/.github/workflows/ci.yml +5 -0
  106. package/templates/CLAUDE.md +8 -2
  107. package/templates/DESIGN.md +52 -45
  108. package/templates/README.md +108 -46
  109. package/templates/apps/api/.env.example +2 -1
  110. package/templates/apps/api/package.json +1 -1
  111. package/templates/apps/api/src/config/__tests__/index.test.ts +37 -2
  112. package/templates/apps/api/src/config/index.ts +19 -6
  113. package/templates/apps/api/src/lib/license-key-store.ts +45 -0
  114. package/templates/apps/api/src/lib/site.ts +5 -3
  115. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
  116. package/templates/apps/api/src/middleware/auth.ts +9 -0
  117. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
  118. package/templates/apps/api/src/routes/content/get-page.ts +86 -2
  119. package/templates/apps/api/src/server.ts +6 -0
  120. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
  121. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
  122. package/templates/apps/mcp/package.json +2 -1
  123. package/templates/apps/mcp/src/index.ts +7 -0
  124. package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
  125. package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
  126. package/templates/apps/web/__tests__/setup.ts +18 -0
  127. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
  128. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
  129. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
  130. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
  131. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
  132. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
  133. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
  134. package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
  135. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
  136. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  137. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  138. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
  139. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +94 -33
  140. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +53 -23
  141. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +96 -0
  142. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +84 -2
  143. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -6
  144. package/templates/apps/web/app/[locale]/contact/page.tsx +25 -7
  145. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  146. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
  147. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
  148. package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
  149. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  150. package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
  151. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  152. package/templates/apps/web/app/[locale]/layout.tsx +113 -51
  153. package/templates/apps/web/app/[locale]/newsletter/page.tsx +50 -0
  154. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  155. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  156. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
  157. package/templates/apps/web/app/[locale]/{shipping → purchase-policy}/page.tsx +15 -10
  158. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
  159. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  160. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
  161. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  162. package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
  163. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  164. package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
  165. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
  166. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +48 -0
  167. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
  168. package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
  169. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  170. package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
  171. package/templates/apps/web/app/error-fallback.tsx +17 -45
  172. package/templates/apps/web/app/not-found.tsx +14 -38
  173. package/templates/apps/web/app/og/route.tsx +6 -0
  174. package/templates/apps/web/eslint.config.mjs +3 -0
  175. package/templates/apps/web/instrumentation.ts +14 -0
  176. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
  177. package/templates/apps/web/lib/license-key-store.ts +52 -0
  178. package/templates/apps/web/lib/nav-config.ts +64 -12
  179. package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
  180. package/templates/apps/web/lib/site-brand.tsx +1 -1
  181. package/templates/apps/web/lib/site-chrome.ts +3 -3
  182. package/templates/apps/web/lib/site-identity.ts +134 -0
  183. package/templates/apps/web/lib/site-theme.ts +11 -0
  184. package/templates/apps/web/messages/en.json +52 -37
  185. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  186. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  187. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  188. package/templates/apps/web/public/favicon-16x16.png +0 -0
  189. package/templates/apps/web/public/favicon-32x32.png +0 -0
  190. package/templates/apps/web/public/favicon.ico +0 -0
  191. package/templates/apps/web/public/favicon.svg +3 -3
  192. package/templates/apps/web/public/logomark.svg +6 -3
  193. package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
  194. package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
  195. package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
  196. package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
  197. package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
  198. package/templates/apps/web/public/logos/salessmyth.svg +4 -0
  199. package/templates/apps/web/public/site.webmanifest +4 -4
  200. package/templates/apps/web/public/wordmark-light.svg +4 -0
  201. package/templates/apps/web/public/wordmark.svg +3 -3
  202. package/templates/apps/web/security-headers.config.ts +5 -1
  203. package/templates/content/_site.mdx +23 -0
  204. package/templates/content/en/AboutPage/about.mdx +150 -144
  205. package/templates/content/en/BlogList/blog.mdx +105 -0
  206. package/templates/content/en/ContactPage/contact.mdx +44 -85
  207. package/templates/content/en/FAQPage/faq.mdx +86 -57
  208. package/templates/content/en/HomePage/home.mdx +276 -217
  209. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +182 -0
  210. package/templates/content/en/SoftwareProduct/product.mdx +175 -0
  211. package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
  212. package/templates/database/CHANGELOG.md +7 -0
  213. package/templates/database/package.json +1 -1
  214. package/templates/package.json +3 -1
  215. package/templates/tools/lint-architecture.ts +315 -0
  216. package/templates/tools/tsconfig.json +14 -0
  217. package/versions.json +33 -33
  218. package/templates/.claude/skills/add-schema/SKILL.md +0 -277
  219. package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
  220. package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
  221. package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
  222. package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
  223. package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
  224. package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
  225. package/templates/apps/web/app/[locale]/cancellation/page.tsx +0 -44
  226. package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
  227. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
  228. package/templates/content/en/LandingPage/blog.mdx +0 -83
  229. package/templates/content/en/LandingPage/features.mdx +0 -103
  230. package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
  231. package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
  232. package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
@@ -15,21 +15,23 @@
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, statSync, writeFileSync, } from "node:fs";
19
- import { basename, dirname, join, resolve } from "node:path";
18
+ import { existsSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
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
22
  import { CATALOG_TARGET_PATH, writeComponentCatalog, } from "./component-catalog.js";
22
23
  import { buildScaffoldMarkers, copyTemplate } from "./copy.js";
23
24
  import { deriveArchitecture } from "./derive-architecture.js";
24
25
  import { extractModuleBundle, installBundleTree, } from "./lib/extract-module-bundle.js";
25
26
  import { fetchTemplate } from "./lib/fetch-template.js";
27
+ import { assertLicenseKeyShape, writeLicenseKeyFiles, } from "./lib/license-key-files.js";
26
28
  import { resolvePaidModules } from "./lib/module-license-gate.js";
27
29
  import { classifyModules } from "./lib/module-tier.js";
28
30
  import { normalizeModules } from "./module-architecture.js";
29
31
  import { SITE_CHROME_PATH, renderSiteChrome, resolveSiteChrome, } from "./site-chrome.js";
30
32
  import { GATEABLE_SURFACES, MODULE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
31
33
  import { syncRoutes } from "./sync-routes.js";
32
- import { BASE_LAYER_DIR, mergeRoster, resolveTemplate, resolveTemplateLineage, templateDelivery, } from "./template-manifest.js";
34
+ import { BASE_LAYER_DIR, STARTER_DESIGNS, TemplateError, mergeRoster, resolveTemplate, resolveTemplateLineage, rosterDeclares, templateDelivery, } from "./template-manifest.js";
33
35
  import { validateTarget } from "./validate.js";
34
36
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
35
37
  const TEMPLATES_DIR = resolve(PKG_ROOT, "templates");
@@ -110,7 +112,7 @@ export function scaffoldSync(opts) {
110
112
  // template's `layout` block; a bare `scaffoldSync` call (every existing
111
113
  // synchronous caller) writes DEFAULT_SITE_CHROME, byte-identical to the
112
114
  // monorepo's own committed apps/web/lib/site-chrome.ts.
113
- writeFileSync(join(targetAbs, SITE_CHROME_PATH), renderSiteChrome(resolveSiteChrome(opts.layout)));
115
+ writeFileSync(join(targetAbs, SITE_CHROME_PATH), renderSiteChrome(resolveSiteChrome(opts.layout, opts.design)));
114
116
  // Opt-in only (#4636). Every write above this line is unconditional, so a
115
117
  // scaffold with `withCatalog` unset touches exactly the files it did before.
116
118
  if (opts.withCatalog === true)
@@ -146,10 +148,21 @@ export function scaffoldSync(opts) {
146
148
  * with a paid module in `opts.modules` exercises the gate at all.
147
149
  */
148
150
  export async function scaffold(opts) {
151
+ // #6683 D9 — refused before anything is fetched or written, like the
152
+ // template and design refusals below. The message never repeats the value.
153
+ if (opts.license !== undefined)
154
+ assertLicenseKeyShape(opts.license);
155
+ const licenseKey = opts.license ?? opts.licenseKey;
149
156
  // Resolved first, and by the registry rather than re-validated here: the
150
157
  // name already passed `parse-args.ts`, and `resolveTemplate` throws a
151
158
  // `TemplateError` for a programmatic caller that skipped the parser.
152
159
  const template = opts.template !== undefined ? resolveTemplate(opts.template) : undefined;
160
+ // #6638 — the design this scaffold renders: the caller's override, else the
161
+ // template's own. Unset for a scaffold naming neither.
162
+ const design = opts.design ?? template?.design;
163
+ if (design !== undefined && !STARTER_DESIGNS.includes(design)) {
164
+ throw new TemplateError(`unknown design "${design}". Declared: ${STARTER_DESIGNS.join(", ")}`);
165
+ }
153
166
  // #5618 D2 — the `extends` chain is resolved in the client registry before
154
167
  // any fetch: an unknown parent or a cycle is refused here, with the target
155
168
  // still non-existent, like the four refusals below.
@@ -168,7 +181,7 @@ export async function scaffold(opts) {
168
181
  const { paid } = classifyModules(modules);
169
182
  const bundles = await resolvePaidModules({
170
183
  paidModules: paid,
171
- licenseKey: opts.licenseKey,
184
+ licenseKey,
172
185
  ...(template !== undefined ? { templateName: template.name } : {}),
173
186
  ...(opts.moduleRegistryUrl !== undefined
174
187
  ? { registryUrl: opts.moduleRegistryUrl }
@@ -189,7 +202,7 @@ export async function scaffold(opts) {
189
202
  const templateBundle = template !== undefined && templateDelivery(template) === "paid"
190
203
  ? await fetchTemplate({
191
204
  templateName: template.name,
192
- token: opts.licenseKey ?? process.env["AILK_LICENSE_KEY"] ?? "",
205
+ token: licenseKey ?? process.env["AILK_LICENSE_KEY"] ?? "",
193
206
  ...(opts.templateRegistryUrl !== undefined
194
207
  ? { registryUrl: opts.templateRegistryUrl }
195
208
  : {}),
@@ -224,9 +237,11 @@ export async function scaffold(opts) {
224
237
  ...(surfaces !== undefined ? { surfaces } : {}),
225
238
  // #5612 D1/D2 — the code registry's own `layout` block, known
226
239
  // synchronously (unlike a bundle's `ailk-template.json`, only readable
227
- // after install below). A template declaring none resolves to the same
228
- // defaults `scaffoldSync` already writes for a templateless scaffold.
240
+ // after install below). A template declaring none resolves to its
241
+ // design's knobs (#6638), and a `standard` one to the same defaults
242
+ // `scaffoldSync` already writes for a templateless scaffold.
229
243
  ...(template?.layout !== undefined ? { layout: template.layout } : {}),
244
+ ...(design !== undefined ? { design } : {}),
230
245
  });
231
246
  // Threaded to extractModuleBundle for `package.json` patch targets'
232
247
  // `workspace:*` substitution (spec 5418 § 3 D2) — the same version map
@@ -327,11 +342,66 @@ export async function scaffold(opts) {
327
342
  });
328
343
  })();
329
344
  if (installed.layout !== undefined) {
330
- writeFileSync(join(result.targetDir, SITE_CHROME_PATH), renderSiteChrome(resolveSiteChrome(installed.layout)));
345
+ writeFileSync(join(result.targetDir, SITE_CHROME_PATH), renderSiteChrome(resolveSiteChrome(installed.layout, design)));
331
346
  }
347
+ pruneUnrosteredSeeds(result.targetDir, mergeRoster(template, template.requires), result.surfaces, ownership);
348
+ }
349
+ // #6683 D9 — persisted only after every layer landed, and only when the
350
+ // caller passed `license`: a scaffold without it writes nothing new.
351
+ if (opts.license !== undefined) {
352
+ writeLicenseKeyFiles(result.targetDir, opts.license);
332
353
  }
333
354
  return result;
334
355
  }
356
+ /**
357
+ * The per-row install of the two SHARED seed layers (#6646 D1, disposition
358
+ * (b)): with every layer landed, remove each `content/en/**.mdx` that the
359
+ * `templates/` snapshot (the overlay — no `ownership` entry) or the `_base`
360
+ * layer wrote and no merged roster row names. Those two layers serve every
361
+ * template, so each carries seeds some templates do not roster (`/pricing`
362
+ * on a services site); a template's own tree and its modules' bundles are
363
+ * never pruned — a seed THEY ship without a row is drift, which the manifest
364
+ * generator's `--check` fails on instead.
365
+ *
366
+ * Merged over the template's own `requires`, as `rosterSurfaces` is: the
367
+ * rows that name shared-layer seeds are the template's own. A seed inside
368
+ * the content directory of a surface this scaffold ENABLES is kept too, so
369
+ * an explicit `--surfaces pricing` on a template that does not roster
370
+ * `/pricing` gets its route and its seed together.
371
+ */
372
+ function pruneUnrosteredSeeds(targetDir, roster, surfaces, ownership) {
373
+ // `installBundleTree`'s ownership label for the base layer: it installs
374
+ // with `templateRequires: []`, so it records as a template. The overlay
375
+ // (the `templates/` snapshot) records no owner at all.
376
+ const shared = new Set([undefined, `template "${BASE_LAYER_DIR}"`]);
377
+ const enabledDirs = GATEABLE_SURFACES.filter((surface) => surfaces.includes(surface.id)).flatMap((surface) => surface.content.map((dir) => `${dir.replace("<locale>", "en")}/`));
378
+ const root = join(targetDir, "content", "en");
379
+ if (!existsSync(root))
380
+ return;
381
+ const stack = [root];
382
+ while (stack.length > 0) {
383
+ const dir = stack.pop();
384
+ if (dir === undefined)
385
+ break;
386
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
387
+ const abs = join(dir, entry.name);
388
+ if (entry.isDirectory()) {
389
+ stack.push(abs);
390
+ continue;
391
+ }
392
+ if (!entry.name.endsWith(".mdx"))
393
+ continue;
394
+ const path = relative(targetDir, abs).split(sep).join("/");
395
+ if (shared.has(ownership.get(path)) &&
396
+ !rosterDeclares(roster, path) &&
397
+ !enabledDirs.some((prefix) => path.startsWith(prefix))) {
398
+ rmSync(abs);
399
+ if (readdirSync(dir).length === 0)
400
+ rmdirSync(dir);
401
+ }
402
+ }
403
+ }
404
+ }
335
405
  /**
336
406
  * The MODULE-LESS gateable-surface ids a template's merged roster names
337
407
  * (#5469 D10): every `GATEABLE_SURFACES` entry no module contributes
@@ -478,6 +548,57 @@ export function syncRoutesSync(opts) {
478
548
  versions: readVersionMap(),
479
549
  });
480
550
  }
551
+ /**
552
+ * Apply a purchased module to an already-scaffolded site in place
553
+ * (`create-ailk add-module`, #6653). The thin package-rooted wrapper around
554
+ * `add-module.ts`'s `applyModuleToScaffold`, the `syncRoutesSync` split:
555
+ *
556
+ * 1. the target must be an existing scaffold — the same check as the sync
557
+ * subcommands above;
558
+ * 2. a free module is refused before any key read or network call — it
559
+ * ships with every scaffold, there is no bundle to add;
560
+ * 3. `resolvePaidModules` — the SAME gate `scaffold()` runs, so a missing
561
+ * key or a key that does not cover the module is refused with the
562
+ * scaffold-time message, before anything is written;
563
+ * 4. the fetched bundle is merged under the plus-config policy.
564
+ */
565
+ export async function addModule(opts) {
566
+ const targetAbs = resolve(opts.targetDir);
567
+ if (!existsSync(targetAbs)) {
568
+ throw new Error(`add-module target does not exist: ${targetAbs}. Point add-module at an already-scaffolded repo.`);
569
+ }
570
+ if (!statSync(targetAbs).isDirectory()) {
571
+ throw new Error(`add-module target is not a directory: ${targetAbs}`);
572
+ }
573
+ if (!existsSync(join(targetAbs, "architecture.yaml"))) {
574
+ throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
575
+ }
576
+ const { paid } = classifyModules([opts.moduleName]);
577
+ if (paid.length === 0) {
578
+ throw new AddModuleRefusedError(`"${opts.moduleName}" ships with every scaffold — there is no module bundle to add`);
579
+ }
580
+ const bundles = await resolvePaidModules({
581
+ paidModules: paid,
582
+ licenseKey: opts.licenseKey,
583
+ ...(opts.moduleRegistryUrl !== undefined
584
+ ? { registryUrl: opts.moduleRegistryUrl }
585
+ : {}),
586
+ ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
587
+ });
588
+ const bundleBytes = bundles.get(opts.moduleName);
589
+ if (bundleBytes === undefined) {
590
+ throw new Error(`module "${opts.moduleName}": no bundle was resolved`);
591
+ }
592
+ return applyModuleToScaffold({
593
+ targetDir: targetAbs,
594
+ moduleName: opts.moduleName,
595
+ bundleBytes,
596
+ versions: readVersionMap(),
597
+ markers: buildScaffoldMarkers(basename(targetAbs)),
598
+ templatesDir: TEMPLATES_DIR,
599
+ siteTemplatesDir: SITE_TEMPLATES_DIR,
600
+ });
601
+ }
481
602
  /** Re-exported so the CLI can name the file it wrote without duplicating it. */
482
603
  export { CATALOG_TARGET_PATH };
483
604
  /**
@@ -15,11 +15,17 @@
15
15
  * `apps/web/lib/site-chrome.ts` every scaffold gets — beside `nav-config.ts`,
16
16
  * the same GENERATED-header idiom (`surfaces.ts` `renderNavConfig`).
17
17
  *
18
- * Every field is optional and DEFAULTS to today's render (`full` / `columns`
19
- * / `true` / `default`) — a scaffold naming no template, or a template naming
20
- * no `layout` block, gets these defaults written, byte-identical to the
21
- * monorepo's own committed `apps/web/lib/site-chrome.ts`.
18
+ * Every field is optional and DEFAULTS to `full` / `columns` / `false` /
19
+ * `default` — the announcement banner is off unless a template turns it on
20
+ * (#6469). A scaffold naming no template, or a template naming no `layout`
21
+ * block, gets these defaults written, byte-identical to the monorepo's own
22
+ * committed `apps/web/lib/site-chrome.ts`.
23
+ *
24
+ * A template's DESIGN supplies layout knobs too (#6638): `DESIGN_LAYOUTS` in
25
+ * `template-manifest.ts`, read by `resolveSiteChrome`'s optional `design`
26
+ * argument between an explicit `layout` and these defaults.
22
27
  */
28
+ import { type StarterDesign } from "./template-manifest.js";
23
29
  /** The `nav` knob's vocabulary. `mark-only` → the layout passes Nav an empty
24
30
  * `navItems` (brand mark + the existing actions/theme/locale controls only). */
25
31
  export type SiteChromeNav = "full" | "mark-only";
@@ -47,7 +53,7 @@ export interface ResolvedSiteChrome {
47
53
  announcementBanner: boolean;
48
54
  home: SiteChromeHome;
49
55
  }
50
- /** Byte-identical-to-today defaults (#5612 D1). */
56
+ /** Defaults (#5612 D1; banner off since #6469). */
51
57
  export declare const DEFAULT_SITE_CHROME: ResolvedSiteChrome;
52
58
  /** Template-relative path of the generated site-chrome registry — beside `NAV_CONFIG_PATH`. */
53
59
  export declare const SITE_CHROME_PATH = "apps/web/lib/site-chrome.ts";
@@ -68,8 +74,13 @@ export declare class TemplateLayoutError extends Error {
68
74
  * @param subject Caller-supplied context for the error message (e.g. `template "foo"`).
69
75
  */
70
76
  export declare function validateTemplateLayout(value: unknown, subject: string): TemplateLayout;
71
- /** Fill every field a `TemplateLayout` leaves unset with `DEFAULT_SITE_CHROME`'s value. */
72
- export declare function resolveSiteChrome(layout?: TemplateLayout): ResolvedSiteChrome;
77
+ /**
78
+ * Fill every field a `TemplateLayout` leaves unset — from `design`'s
79
+ * `DESIGN_LAYOUTS` entry when one is named (#6638), then from
80
+ * `DEFAULT_SITE_CHROME`. The explicit `layout` wins field by field, so a
81
+ * template can wear a design and still override one knob of it.
82
+ */
83
+ export declare function resolveSiteChrome(layout?: TemplateLayout, design?: StarterDesign): ResolvedSiteChrome;
73
84
  /**
74
85
  * Render `apps/web/lib/site-chrome.ts` for a resolved layout — the same
75
86
  * GENERATED-header idiom `renderNavConfig` uses. `layout.tsx` imports
@@ -15,16 +15,22 @@
15
15
  * `apps/web/lib/site-chrome.ts` every scaffold gets — beside `nav-config.ts`,
16
16
  * the same GENERATED-header idiom (`surfaces.ts` `renderNavConfig`).
17
17
  *
18
- * Every field is optional and DEFAULTS to today's render (`full` / `columns`
19
- * / `true` / `default`) — a scaffold naming no template, or a template naming
20
- * no `layout` block, gets these defaults written, byte-identical to the
21
- * monorepo's own committed `apps/web/lib/site-chrome.ts`.
18
+ * Every field is optional and DEFAULTS to `full` / `columns` / `false` /
19
+ * `default` — the announcement banner is off unless a template turns it on
20
+ * (#6469). A scaffold naming no template, or a template naming no `layout`
21
+ * block, gets these defaults written, byte-identical to the monorepo's own
22
+ * committed `apps/web/lib/site-chrome.ts`.
23
+ *
24
+ * A template's DESIGN supplies layout knobs too (#6638): `DESIGN_LAYOUTS` in
25
+ * `template-manifest.ts`, read by `resolveSiteChrome`'s optional `design`
26
+ * argument between an explicit `layout` and these defaults.
22
27
  */
23
- /** Byte-identical-to-today defaults (#5612 D1). */
28
+ import { DESIGN_LAYOUTS, TemplateError, } from "./template-manifest.js";
29
+ /** Defaults (#5612 D1; banner off since #6469). */
24
30
  export const DEFAULT_SITE_CHROME = {
25
31
  nav: "full",
26
32
  footer: "columns",
27
- announcementBanner: true,
33
+ announcementBanner: false,
28
34
  home: "default",
29
35
  };
30
36
  /** Template-relative path of the generated site-chrome registry — beside `NAV_CONFIG_PATH`. */
@@ -85,13 +91,25 @@ export function validateTemplateLayout(value, subject) {
85
91
  }
86
92
  return result;
87
93
  }
88
- /** Fill every field a `TemplateLayout` leaves unset with `DEFAULT_SITE_CHROME`'s value. */
89
- export function resolveSiteChrome(layout) {
94
+ /**
95
+ * Fill every field a `TemplateLayout` leaves unset — from `design`'s
96
+ * `DESIGN_LAYOUTS` entry when one is named (#6638), then from
97
+ * `DEFAULT_SITE_CHROME`. The explicit `layout` wins field by field, so a
98
+ * template can wear a design and still override one knob of it.
99
+ */
100
+ export function resolveSiteChrome(layout, design) {
101
+ const fromDesign = design !== undefined ? DESIGN_LAYOUTS[design] : {};
102
+ // A JS caller (or a cast) can reach here with an undeclared design.
103
+ if (fromDesign === undefined) {
104
+ throw new TemplateError(`unknown design "${design}". Declared: ${Object.keys(DESIGN_LAYOUTS).join(", ")}`);
105
+ }
90
106
  return {
91
- nav: layout?.nav ?? DEFAULT_SITE_CHROME.nav,
92
- footer: layout?.footer ?? DEFAULT_SITE_CHROME.footer,
93
- announcementBanner: layout?.announcementBanner ?? DEFAULT_SITE_CHROME.announcementBanner,
94
- home: layout?.home ?? DEFAULT_SITE_CHROME.home,
107
+ nav: layout?.nav ?? fromDesign.nav ?? DEFAULT_SITE_CHROME.nav,
108
+ footer: layout?.footer ?? fromDesign.footer ?? DEFAULT_SITE_CHROME.footer,
109
+ announcementBanner: layout?.announcementBanner ??
110
+ fromDesign.announcementBanner ??
111
+ DEFAULT_SITE_CHROME.announcementBanner,
112
+ home: layout?.home ?? fromDesign.home ?? DEFAULT_SITE_CHROME.home,
95
113
  };
96
114
  }
97
115
  /**
@@ -106,8 +124,8 @@ export function renderSiteChrome(resolved) {
106
124
  //
107
125
  // GENERATED by create-ailk from the declared template's \`layout\` block
108
126
  // (#5612). A scaffold with no template, or a template with no \`layout\`
109
- // block, gets these defaults — byte-identical to every scaffold before this
110
- // file existed. \`apps/web/app/[locale]/layout.tsx\` reads this to decide:
127
+ // block, gets the defaults below (the announcement banner is off unless a
128
+ // template turns it on, #6469). \`apps/web/app/[locale]/layout.tsx\` reads this to decide:
111
129
  // - nav: "mark-only" → Nav renders the brand mark + right-hand controls
112
130
  // only (no nav links).
113
131
  // - footer: "centered" → Footer renders its existing centered variant.
@@ -0,0 +1,127 @@
1
+ import { type PageRosterRow } from "./module-architecture.js";
2
+ import type { ModuleName } from "./parse-args.js";
3
+ import { type MarkerMap } from "./substitute.js";
4
+ import { type TemplateManifest } from "./template-manifest.js";
5
+ /** The site-root file that is the roster of record (#6658 D7). */
6
+ export declare const SITE_ROSTER_PATH = ".ailk-site-manifest.json";
7
+ /**
8
+ * Where a site scaffolded before #6658 carries its roster: the template's
9
+ * manifest copied to the root under its own name. Read only when
10
+ * `SITE_ROSTER_PATH` is absent, and written back in place.
11
+ */
12
+ export declare const LEGACY_SITE_ROSTER_PATH = "TEMPLATE-MANIFEST.json";
13
+ /** One `rows[]` element: its parsed value and its verbatim text in the file. */
14
+ export interface RosterRowSlice {
15
+ value: Record<string, unknown>;
16
+ text: string;
17
+ }
18
+ /** A parsed manifest file, with the `rows` array located in its text. */
19
+ interface RosterFile {
20
+ doc: Record<string, unknown>;
21
+ text: string;
22
+ rows: RosterRowSlice[];
23
+ /** Index of the `rows` array's `[`. */
24
+ open: number;
25
+ /** Index of the `rows` array's `]`. */
26
+ close: number;
27
+ /** The whitespace before the first row and after the last — prettier's. */
28
+ lead: string;
29
+ trail: string;
30
+ }
31
+ /** The site's roster of record. */
32
+ export interface SiteRoster extends RosterFile {
33
+ /** The registry entry the file's `template` names. */
34
+ template: TemplateManifest;
35
+ /** The site-relative file it was read from — `SITE_ROSTER_PATH`, or the legacy root name. */
36
+ path: string;
37
+ }
38
+ /** One module contribution, with the generator's row(s) for its route. */
39
+ export interface ModuleRosterRow {
40
+ module: ModuleName;
41
+ contribution: PageRosterRow;
42
+ /**
43
+ * The generator's row for the route. For a `contentDir` contribution this
44
+ * is the code-route form (`kind: "route"`), used where the site has no
45
+ * seeds under the directory.
46
+ */
47
+ row: RosterRowSlice;
48
+ /** A `contentDir` contribution's seeded page form. */
49
+ seeded?: RosterRowSlice;
50
+ }
51
+ /**
52
+ * The site's roster of record, or `undefined` when the site has none (a
53
+ * scaffold that named no template installs no manifest). A file that is not
54
+ * a JSON object, has no `rows` array or names a template the registry does
55
+ * not declare is refused: guessing would silently mis-roster the site.
56
+ */
57
+ export declare function readSiteRoster(targetDir: string): SiteRoster | undefined;
58
+ /**
59
+ * The generator's row for each of a module's `pageContributions`, in
60
+ * declaration order — read from the committed manifest of the first
61
+ * template (in `TEMPLATE_MANIFESTS` order) that requires the module, ships a
62
+ * manifest, and carries the route. A `contentDir` contribution resolves both
63
+ * of its forms. A route no shipped manifest carries is refused.
64
+ */
65
+ export declare function moduleRosterRows(moduleName: ModuleName, siteTemplatesDir: string): ModuleRosterRow[];
66
+ /** What becomes of a superseded template row's seed file. */
67
+ export type SupersededSeed = {
68
+ path: string;
69
+ remove: true;
70
+ } | {
71
+ path: string;
72
+ remove: false;
73
+ wanted: string;
74
+ };
75
+ export interface RosterAppendPlan {
76
+ /** The rewritten file text, or undefined when the rows are unchanged. */
77
+ next: string | undefined;
78
+ /** Routes filled from the shipped reference rows. */
79
+ appended: string[];
80
+ /** The seeds of the template rows a module took over. */
81
+ seeds: SupersededSeed[];
82
+ }
83
+ export interface PlanRosterAppendOptions {
84
+ targetDir: string;
85
+ roster: SiteRoster;
86
+ /** The site's module set after the add. */
87
+ modules: readonly ModuleName[];
88
+ /** `moduleRosterRows` of every module in `modules`, resolved up front. */
89
+ references: readonly ModuleRosterRow[];
90
+ siteTemplatesDir: string;
91
+ templatesDir: string;
92
+ markers: MarkerMap;
93
+ }
94
+ /**
95
+ * The site roster's next `rows`: the template rows in file order, then each
96
+ * module's rows in `normalizeModules` order and, within a module, in
97
+ * `pageContributions` order — `mergeRoster`'s order, so adding A then B
98
+ * gives the roster adding B then A does.
99
+ *
100
+ * A row on a route a module in the set contributes is SUPERSEDED when it is
101
+ * the site template's own shipped row, untouched (the Free `/blog` teaser
102
+ * under docs-blog): the module's row takes its place, and its seed is
103
+ * removed only when its bytes are exactly what the scaffold installed. Any
104
+ * other row on a contributed route is the site's own and is kept as is.
105
+ */
106
+ export declare function planRosterAppend(opts: PlanRosterAppendOptions): RosterAppendPlan;
107
+ export interface SiteRosterCheckOptions {
108
+ targetDir: string;
109
+ siteTemplatesDir: string;
110
+ templatesDir: string;
111
+ }
112
+ /**
113
+ * The site's roster problems — empty means clean. The scaffold-grained twin
114
+ * of `build-template-manifest.mjs --check`, over the same `rosterDeclares`
115
+ * predicate:
116
+ *
117
+ * 1. a route on more than one row;
118
+ * 2. a module in the site's set whose contributed row is missing, or is not
119
+ * the shipped reference row;
120
+ * 3. an installed seed — one some layer lands, present in the site — that
121
+ * the effective roster does not declare. A page the customer wrote is
122
+ * installed by no layer and is never reported.
123
+ *
124
+ * A site with no roster (a template-less scaffold) has no problems.
125
+ */
126
+ export declare function siteRosterProblems(opts: SiteRosterCheckOptions): string[];
127
+ export {};