create-ailk 0.6.0 → 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 (248) hide show
  1. package/README.md +44 -5
  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 +124 -13
  11. package/dist/copy.d.ts +14 -0
  12. package/dist/copy.js +38 -1
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +6 -1
  15. package/dist/lib/extract-module-bundle.d.ts +76 -2
  16. package/dist/lib/extract-module-bundle.js +219 -8
  17. package/dist/lib/license-key-files.d.ts +15 -0
  18. package/dist/lib/license-key-files.js +102 -0
  19. package/dist/lib/module-license-gate.d.ts +6 -1
  20. package/dist/lib/module-license-gate.js +6 -1
  21. package/dist/lib/pro-callout.d.ts +16 -10
  22. package/dist/lib/pro-callout.js +27 -4
  23. package/dist/license-set.d.ts +34 -0
  24. package/dist/license-set.js +194 -0
  25. package/dist/module-architecture.d.ts +20 -0
  26. package/dist/module-architecture.js +77 -19
  27. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  28. package/dist/modules/commerce/0.6.1.tar.gz +0 -0
  29. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  30. package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
  31. package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
  32. package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
  33. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  34. package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
  35. package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
  36. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  37. package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
  38. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  39. package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
  40. package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
  41. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  42. package/dist/modules/workspace/0.6.1.tar.gz +0 -0
  43. package/dist/parse-args.d.ts +7 -0
  44. package/dist/parse-args.js +23 -1
  45. package/dist/programmatic.d.ts +46 -0
  46. package/dist/programmatic.js +144 -19
  47. package/dist/site-chrome.d.ts +18 -7
  48. package/dist/site-chrome.js +32 -14
  49. package/dist/site-roster.d.ts +127 -0
  50. package/dist/site-roster.js +494 -0
  51. package/dist/surfaces.d.ts +20 -7
  52. package/dist/surfaces.js +149 -28
  53. package/dist/sync-routes.d.ts +9 -0
  54. package/dist/sync-routes.js +2 -2
  55. package/dist/template-manifest.d.ts +58 -2
  56. package/dist/template-manifest.js +196 -69
  57. package/package.json +6 -4
  58. package/site-templates/_base/content/en/BookCall/book-call.mdx +53 -0
  59. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +66 -0
  60. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +68 -0
  61. package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
  62. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +2 -3
  63. package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
  64. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +23 -0
  65. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +23 -0
  66. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +23 -0
  67. package/site-templates/designs/minimalism/DESIGN.md +162 -0
  68. package/site-templates/designs/standard/DESIGN.md +162 -0
  69. package/site-templates/marketing-site/DESIGN.md +162 -0
  70. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +312 -57
  71. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +209 -90
  72. package/site-templates/marketing-site/ailk-template.json +1 -0
  73. package/site-templates/marketing-site-free/DESIGN.md +162 -0
  74. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +260 -66
  75. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +294 -114
  76. package/site-templates/marketing-site-free/ailk-template.json +3 -2
  77. package/site-templates/minimalism-site/DESIGN.md +162 -0
  78. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
  79. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +185 -51
  80. package/site-templates/minimalism-site/ailk-template.json +3 -2
  81. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +53 -0
  82. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
  83. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
  84. package/site-templates/saas-product/DESIGN.md +162 -0
  85. package/site-templates/simple-service/DESIGN.md +162 -0
  86. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +135 -26
  87. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +114 -59
  88. package/site-templates/simple-service/ailk-template.json +4 -3
  89. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +53 -0
  90. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +1 -36
  91. package/site-templates/simple-service/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
  92. package/templates/.claude/settings.json +15 -0
  93. package/templates/.claude/skills/README.md +8 -8
  94. package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
  95. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
  96. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
  97. package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
  98. package/templates/.claude/skills/add-page/references/page-types.md +111 -0
  99. package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
  100. package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
  101. package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
  102. package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
  103. package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
  104. package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
  105. package/templates/.claude/skills/fill-page/SKILL.md +180 -0
  106. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
  107. package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
  108. package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +8 -8
  109. package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +2 -2
  110. package/templates/.claude/skills/{provision-config → setup-deploy}/references/runbook-template.md +8 -8
  111. package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
  112. package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
  113. package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
  114. package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
  115. package/templates/.github/workflows/ci.yml +5 -0
  116. package/templates/CLAUDE.md +10 -5
  117. package/templates/DESIGN.md +162 -0
  118. package/templates/README.md +134 -38
  119. package/templates/apps/api/.env.example +2 -1
  120. package/templates/apps/api/package.json +1 -1
  121. package/templates/apps/api/src/config/__tests__/index.test.ts +37 -2
  122. package/templates/apps/api/src/config/index.ts +19 -6
  123. package/templates/apps/api/src/lib/license-key-store.ts +45 -0
  124. package/templates/apps/api/src/lib/site.ts +5 -3
  125. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
  126. package/templates/apps/api/src/middleware/auth.ts +9 -0
  127. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
  128. package/templates/apps/api/src/routes/content/get-page.ts +86 -2
  129. package/templates/apps/api/src/server.ts +6 -0
  130. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
  131. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
  132. package/templates/apps/mcp/package.json +2 -1
  133. package/templates/apps/mcp/src/index.ts +7 -0
  134. package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
  135. package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
  136. package/templates/apps/web/PRODUCT.md +86 -0
  137. package/templates/apps/web/__tests__/setup.ts +18 -0
  138. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
  139. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
  140. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
  141. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
  142. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
  143. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
  144. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
  145. package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
  146. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
  147. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  148. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  149. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
  150. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +94 -33
  151. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +53 -23
  152. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +96 -0
  153. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +84 -2
  154. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -6
  155. package/templates/apps/web/app/[locale]/contact/page.tsx +25 -7
  156. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  157. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
  158. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
  159. package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
  160. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  161. package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
  162. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  163. package/templates/apps/web/app/[locale]/layout.tsx +113 -51
  164. package/templates/apps/web/app/[locale]/newsletter/page.tsx +50 -0
  165. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  166. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  167. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
  168. package/templates/apps/web/app/[locale]/{shipping → purchase-policy}/page.tsx +15 -10
  169. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
  170. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  171. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
  172. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  173. package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
  174. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  175. package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
  176. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
  177. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +48 -0
  178. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
  179. package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
  180. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  181. package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
  182. package/templates/apps/web/app/error-fallback.tsx +17 -45
  183. package/templates/apps/web/app/not-found.tsx +14 -38
  184. package/templates/apps/web/app/og/route.tsx +6 -0
  185. package/templates/apps/web/eslint.config.mjs +3 -0
  186. package/templates/apps/web/instrumentation.ts +14 -0
  187. package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
  188. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
  189. package/templates/apps/web/lib/license-key-store.ts +52 -0
  190. package/templates/apps/web/lib/nav-config.ts +64 -12
  191. package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
  192. package/templates/apps/web/lib/site-brand.tsx +4 -8
  193. package/templates/apps/web/lib/site-chrome.ts +3 -3
  194. package/templates/apps/web/lib/site-identity.ts +134 -0
  195. package/templates/apps/web/lib/site-theme.ts +11 -0
  196. package/templates/apps/web/messages/en.json +52 -37
  197. package/templates/apps/web/middleware.ts +1 -1
  198. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  199. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  200. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  201. package/templates/apps/web/public/favicon-16x16.png +0 -0
  202. package/templates/apps/web/public/favicon-32x32.png +0 -0
  203. package/templates/apps/web/public/favicon.ico +0 -0
  204. package/templates/apps/web/public/favicon.svg +3 -3
  205. package/templates/apps/web/public/logomark.svg +6 -3
  206. package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
  207. package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
  208. package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
  209. package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
  210. package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
  211. package/templates/apps/web/public/logos/salessmyth.svg +4 -0
  212. package/templates/apps/web/public/site.webmanifest +4 -4
  213. package/templates/apps/web/public/wordmark-light.svg +4 -0
  214. package/templates/apps/web/public/wordmark.svg +3 -3
  215. package/templates/apps/web/security-headers.config.ts +5 -1
  216. package/templates/apps/web/src/lib/commerce-sections.ts +35 -3
  217. package/templates/content/_site.mdx +41 -3
  218. package/templates/content/en/AboutPage/about.mdx +150 -144
  219. package/templates/content/en/BlogList/blog.mdx +105 -0
  220. package/templates/content/en/ContactPage/contact.mdx +44 -85
  221. package/templates/content/en/FAQPage/faq.mdx +86 -57
  222. package/templates/content/en/HomePage/home.mdx +276 -217
  223. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +182 -0
  224. package/templates/content/en/SoftwareProduct/product.mdx +175 -0
  225. package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
  226. package/templates/database/CHANGELOG.md +14 -0
  227. package/templates/database/package.json +3 -2
  228. package/templates/package.json +3 -1
  229. package/templates/tools/lint-architecture.ts +315 -0
  230. package/templates/tools/tsconfig.json +14 -0
  231. package/versions.json +33 -33
  232. package/templates/.claude/skills/add-schema/SKILL.md +0 -277
  233. package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
  234. package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
  235. package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
  236. package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
  237. package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
  238. package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
  239. package/templates/apps/web/app/[locale]/cancellation/page.tsx +0 -44
  240. package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
  241. package/templates/apps/web/public/logo-dark.svg +0 -21
  242. package/templates/apps/web/public/logo-light.svg +0 -21
  243. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
  244. package/templates/content/en/LandingPage/blog.mdx +0 -83
  245. package/templates/content/en/LandingPage/features.mdx +0 -103
  246. package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
  247. package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
  248. package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
@@ -0,0 +1,34 @@
1
+ export interface LicenseSetRunResult {
2
+ /** Exit status; null when the process never ran or was killed. */
3
+ status: number | null;
4
+ /** Set when the process could not be spawned (e.g. `ENOENT`). */
5
+ error?: NodeJS.ErrnoException;
6
+ }
7
+ /**
8
+ * Runs `vercel <args>`. The command itself is fixed — a runner is never
9
+ * handed one to run, per the package's hard-coded-command invariant
10
+ * (security.test.ts).
11
+ * `input`, when given, is written to the child's stdin.
12
+ */
13
+ export type LicenseSetRunner = (args: readonly string[], options: {
14
+ cwd: string;
15
+ input?: string;
16
+ }) => LicenseSetRunResult;
17
+ export interface LicenseSetIO {
18
+ /** Resolves a relative or absent `[dir]`. */
19
+ cwd: string;
20
+ stdout: (text: string) => void;
21
+ stderr: (text: string) => void;
22
+ /** Reads all of stdin — used only by the `-` form. */
23
+ readStdin: () => string;
24
+ /** Defaults to `spawnSync` with `shell: false` (see `defaultRunner`). */
25
+ runner?: LicenseSetRunner;
26
+ }
27
+ export declare const LICENSE_SET_HELP = "create-ailk license set \u2014 put your licence key into an existing site.\n\nUsage:\n pnpm create ailk@latest license set <key> [<target-dir>]\n pnpm create ailk@latest license set - [<target-dir>] < key.txt\n\nArguments:\n <key> Your licence key, or `-` to read it from stdin\n (keeps it out of your shell history).\n <target-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n\nWrites the key to ailk.key and apps/api/.env (owner-only, gitignored). When the\nsite (or apps/api, or apps/web) is linked to Vercel, it also replaces the\nproduction AILK_LICENSE_KEY variable and redeploys.\n";
28
+ /** `spawnSync`, never through a shell; stdin piped only when there is input. */
29
+ export declare const defaultRunner: LicenseSetRunner;
30
+ /**
31
+ * Run `license set` with `argv` = everything after the `license set` words.
32
+ * Returns the process exit code; never throws for an expected failure.
33
+ */
34
+ export declare function runLicenseSet(argv: string[], io: LicenseSetIO): number;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * `create-ailk license set <key|-> [dir]` — put a licence key into an existing
3
+ * scaffold and, when it is Vercel-linked, into production (#6683 § 3 D9).
4
+ *
5
+ * The one-liner a buyer runs is `pnpm create ailk@latest license set <key>`:
6
+ * `create-ailk` is the public CLI they already have (§ 1.8).
7
+ *
8
+ * 1. The target (default: cwd) must contain `architecture.yaml` — the
9
+ * `sync-routes` precondition — else exit 1 with nothing written.
10
+ * 2. `writeLicenseKeyFiles` — `ailk.key` + `apps/api/.env`, both 0600 and
11
+ * gitignored; the same writer `--license` uses.
12
+ * 3. For each of `<dir>`, `<dir>/apps/api`, `<dir>/apps/web` holding
13
+ * `.vercel/project.json`, with that directory as `cwd`:
14
+ * a. print the linked `projectId` about to change (SEC-13);
15
+ * b. list `ailk.key` and `.env` in its `.vercelignore` — a CLI deploy
16
+ * uploads the local tree, which `.gitignore` does not govern (SEC-10);
17
+ * c. `vercel env rm AILK_LICENSE_KEY production --yes` (failure
18
+ * tolerated: the variable may not exist yet);
19
+ * d. `vercel env add AILK_LICENSE_KEY production`, the key on STDIN;
20
+ * e. `vercel deploy --prod --yes`.
21
+ *
22
+ * The key is never an argv element of a spawned process and never appears in
23
+ * an error (SEC-11). `vercel` missing → exit 1 naming what was written and the
24
+ * exact manual commands; a failed `env add` after `env rm` → exit 1 saying the
25
+ * production variable is now UNSET, with the command that finishes the job.
26
+ * No Vercel link → exit 0 after the local write.
27
+ *
28
+ * Pure module (no `import.meta.url`): every effect on the outside world —
29
+ * output, stdin, the process runner — is injected, so jest drives it directly.
30
+ */
31
+ import { spawnSync } from "node:child_process";
32
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
33
+ import { join, relative, resolve } from "node:path";
34
+ import { LICENSE_ENV_FILE, LICENSE_ENV_VAR, LICENSE_KEY_FILE, assertLicenseKeyShape, writeLicenseKeyFiles, } from "./lib/license-key-files.js";
35
+ export const LICENSE_SET_HELP = `create-ailk license set — put your licence key into an existing site.
36
+
37
+ Usage:
38
+ pnpm create ailk@latest license set <key> [<target-dir>]
39
+ pnpm create ailk@latest license set - [<target-dir>] < key.txt
40
+
41
+ Arguments:
42
+ <key> Your licence key, or \`-\` to read it from stdin
43
+ (keeps it out of your shell history).
44
+ <target-dir> An already-scaffolded repo (contains
45
+ architecture.yaml). Defaults to the current dir.
46
+
47
+ Writes the key to ailk.key and apps/api/.env (owner-only, gitignored). When the
48
+ site (or apps/api, or apps/web) is linked to Vercel, it also replaces the
49
+ production AILK_LICENSE_KEY variable and redeploys.
50
+ `;
51
+ const LINK_DIRS = [".", join("apps", "api"), join("apps", "web")];
52
+ const VERCELIGNORE_LINES = [LICENSE_KEY_FILE, ".env"];
53
+ const ENV_RM = ["env", "rm", LICENSE_ENV_VAR, "production", "--yes"];
54
+ const ENV_ADD = ["env", "add", LICENSE_ENV_VAR, "production"];
55
+ const DEPLOY = ["deploy", "--prod", "--yes"];
56
+ /** `spawnSync`, never through a shell; stdin piped only when there is input. */
57
+ export const defaultRunner = (args, options) => {
58
+ const res = spawnSync("vercel", [...args], {
59
+ cwd: options.cwd,
60
+ shell: false,
61
+ stdio: [options.input !== undefined ? "pipe" : "inherit", "inherit", "inherit"],
62
+ ...(options.input !== undefined ? { input: options.input } : {}),
63
+ });
64
+ return {
65
+ status: res.status,
66
+ ...(res.error !== undefined
67
+ ? { error: res.error }
68
+ : {}),
69
+ };
70
+ };
71
+ class LicenseSetError extends Error {
72
+ }
73
+ /**
74
+ * Hand-parsed rather than through `node:util` `parseArgs`: its errors quote
75
+ * the offending token, and the token here may be the key.
76
+ */
77
+ function parseLicenseSetArgs(argv) {
78
+ if (argv.includes("--help") || argv.includes("-h")) {
79
+ return { help: true, key: undefined, target: undefined };
80
+ }
81
+ if (argv.length > 2) {
82
+ throw new LicenseSetError(`expected <key|-> and at most one target directory, got ${argv.length} arguments`);
83
+ }
84
+ return { help: false, key: argv[0], target: argv[1] };
85
+ }
86
+ /** Append whichever of `lines` the file does not already list. */
87
+ function ensureVercelIgnore(dir) {
88
+ const path = join(dir, ".vercelignore");
89
+ const text = existsSync(path) ? readFileSync(path, "utf8") : "";
90
+ const present = new Set(text.split("\n").map((l) => l.trim()));
91
+ const missing = VERCELIGNORE_LINES.filter((l) => !present.has(l) && !present.has(`/${l}`));
92
+ if (missing.length === 0)
93
+ return;
94
+ const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n";
95
+ writeFileSync(path, `${text}${sep}${missing.join("\n")}\n`);
96
+ }
97
+ function readProjectId(dir) {
98
+ try {
99
+ const raw = JSON.parse(readFileSync(join(dir, ".vercel", "project.json"), "utf8"));
100
+ if (typeof raw.projectId === "string" && raw.projectId.length > 0) {
101
+ return raw.projectId;
102
+ }
103
+ }
104
+ catch {
105
+ // fall through
106
+ }
107
+ throw new LicenseSetError(`${join(dir, ".vercel", "project.json")} has no readable projectId; run \`vercel link\` there, then re-run.`);
108
+ }
109
+ /** The exact commands that finish the push by hand, from `dir`. */
110
+ function manualCommands(dir, keyFile, from) {
111
+ const keyRel = relative(dir, keyFile) || LICENSE_KEY_FILE;
112
+ const lines = [` cd ${dir}`];
113
+ if (from === "rm")
114
+ lines.push(` vercel ${ENV_RM.join(" ")}`);
115
+ lines.push(` tr -d '\\n' < ${keyRel} | vercel ${ENV_ADD.join(" ")}`);
116
+ lines.push(` vercel ${DEPLOY.join(" ")}`);
117
+ return lines.join("\n");
118
+ }
119
+ /**
120
+ * Run `license set` with `argv` = everything after the `license set` words.
121
+ * Returns the process exit code; never throws for an expected failure.
122
+ */
123
+ export function runLicenseSet(argv, io) {
124
+ const runner = io.runner ?? defaultRunner;
125
+ const fail = (message) => {
126
+ io.stderr(`[create-ailk] license set: ${message}\n`);
127
+ return 1;
128
+ };
129
+ let args;
130
+ try {
131
+ args = parseLicenseSetArgs(argv);
132
+ }
133
+ catch (err) {
134
+ return fail(`${err.message}\n\n${LICENSE_SET_HELP}`);
135
+ }
136
+ if (args.help) {
137
+ io.stdout(LICENSE_SET_HELP);
138
+ return 0;
139
+ }
140
+ if (args.key === undefined) {
141
+ return fail(`missing <key>.\n\n${LICENSE_SET_HELP}`);
142
+ }
143
+ const key = args.key === "-" ? io.readStdin().trim() : args.key;
144
+ try {
145
+ assertLicenseKeyShape(key);
146
+ }
147
+ catch (err) {
148
+ return fail(err.message);
149
+ }
150
+ const target = resolve(io.cwd, args.target ?? ".");
151
+ if (!existsSync(join(target, "architecture.yaml"))) {
152
+ return fail(`not a scaffolded repo: no architecture.yaml at ${target}. Run it from your site's root, or pass the site's directory.`);
153
+ }
154
+ writeLicenseKeyFiles(target, key);
155
+ const keyFile = join(target, LICENSE_KEY_FILE);
156
+ const written = `Wrote ${LICENSE_KEY_FILE} and ${LICENSE_ENV_FILE.split("\\").join("/")} in ${target}`;
157
+ io.stdout(`[create-ailk] ${written} (owner-only, gitignored).\n`);
158
+ const linked = LINK_DIRS.map((d) => join(target, d)).filter((d) => existsSync(join(d, ".vercel", "project.json")));
159
+ if (linked.length === 0) {
160
+ io.stdout("[create-ailk] No Vercel link found (.vercel/project.json); nothing pushed. Set AILK_LICENSE_KEY on your host yourself if you deploy elsewhere.\n");
161
+ return 0;
162
+ }
163
+ for (const dir of linked) {
164
+ let projectId;
165
+ try {
166
+ projectId = readProjectId(dir);
167
+ }
168
+ catch (err) {
169
+ return fail(err.message);
170
+ }
171
+ io.stdout(`[create-ailk] Updating ${LICENSE_ENV_VAR} (production) on Vercel project ${projectId}, linked at ${dir}\n`);
172
+ ensureVercelIgnore(dir);
173
+ const rm = runner(ENV_RM, { cwd: dir });
174
+ if (rm.error?.code === "ENOENT") {
175
+ return fail(`the vercel CLI was not found. ${written}; nothing was pushed. Install it (npm i -g vercel), or finish by hand:\n${manualCommands(dir, keyFile, "rm")}`);
176
+ }
177
+ // `rm` may fail because the variable never existed — tolerated — but then
178
+ // it removed nothing, and the message below must not say it did.
179
+ const removed = rm.error === undefined && rm.status === 0;
180
+ const add = runner(ENV_ADD, { cwd: dir, input: key });
181
+ if (add.error !== undefined || add.status !== 0) {
182
+ const state = removed
183
+ ? `The production variable ${LICENSE_ENV_VAR} is now UNSET there (the old value was removed).`
184
+ : `The production variable ${LICENSE_ENV_VAR} was not changed (removing the old value also failed).`;
185
+ return fail(`\`vercel ${ENV_ADD.join(" ")}\` failed on project ${projectId}. ${state} Finish with:\n${manualCommands(dir, keyFile, removed ? "add" : "rm")}`);
186
+ }
187
+ const deploy = runner(DEPLOY, { cwd: dir });
188
+ if (deploy.error !== undefined || deploy.status !== 0) {
189
+ return fail(`\`vercel ${DEPLOY.join(" ")}\` failed on project ${projectId}. ${LICENSE_ENV_VAR} is set in production; redeploy with:\n cd ${dir}\n vercel ${DEPLOY.join(" ")}`);
190
+ }
191
+ }
192
+ io.stdout("[create-ailk] Licence key set and production redeployed.\n");
193
+ return 0;
194
+ }
@@ -61,6 +61,14 @@ export interface PageRosterRow {
61
61
  * (`/meet` renders from the scheduling API, not from an MDX file).
62
62
  */
63
63
  content?: readonly string[];
64
+ /**
65
+ * Scaffold-relative seed DIRECTORY a dynamic detail row serves, e.g.
66
+ * `content/en/Service` under `/services/[slug]` (#6646 disposition (a)).
67
+ * `content` names files; a detail route whose seeds are a whole directory
68
+ * declares the directory instead. Every `.mdx` under it is a declared page,
69
+ * so it installs; a seed no row names does not.
70
+ */
71
+ contentDir?: string;
64
72
  }
65
73
  /**
66
74
  * The architecture fragment one module contributes.
@@ -144,6 +152,8 @@ export interface ModuleSlice {
144
152
  * `ownedFiles` names which files in THIS monorepo physically belong to the
145
153
  * module; `pageContributions` names which PAGES the module brings to a
146
154
  * template's roster. `template-manifest.ts`'s `mergeRoster` reads it.
155
+ * A row's `contentDir` names seeds a TEMPLATE supplies, not the module:
156
+ * the module's bundle stages only `content` files (#6646).
147
157
  *
148
158
  * A contribution never carries route CODE. How the route code reaches a
149
159
  * scaffold is a property of the module, not of the contribution (parent
@@ -399,3 +409,13 @@ export declare function normalizeModules(modules: string[]): ModuleName[];
399
409
  export declare function coOwnedModuleDestinations(): ReadonlySet<string>;
400
410
  export declare const EXCLUDE_TEMPLATE_DIRS: readonly string[];
401
411
  export declare function paidModuleTemplateExclusions(snapshotRoots: readonly string[], repoRoot: string): string[];
412
+ /** The scaffold's architecture linter, relative to the site root (#6649). */
413
+ export declare const ARCHITECTURE_LINT_PATH = "tools/lint-architecture.ts";
414
+ /**
415
+ * What a scaffold needs so its `architecture.yaml` is enforced rather than
416
+ * declared (#6649): the linter, the `architecture` + `preflight` root scripts
417
+ * that run it, and a `.claude/settings.json` PostToolUse hook on Edit|Write
418
+ * that runs it in-session. Returns one line per missing piece — empty means
419
+ * all present. `verify-scaffold.mjs` fails on a non-empty result.
420
+ */
421
+ export declare function missingArchitectureEnforcement(siteDir: string): string[];
@@ -21,7 +21,8 @@
21
21
  * `architecture.yaml` template files — a fixed template IS the drift this
22
22
  * removes.
23
23
  */
24
- import { sep } from "node:path";
24
+ import { existsSync, readFileSync } from "node:fs";
25
+ import { join, sep } from "node:path";
25
26
  import { importClosure } from "./lib/import-graph.js";
26
27
  import { PAID_MODULES } from "./lib/module-tier.js";
27
28
  import { RECOGNIZED_MODULES } from "./parse-args.js";
@@ -149,6 +150,15 @@ const ASSET_STORE_FILES = [
149
150
  "apps/api/src/routes/assets/manage.ts",
150
151
  "apps/api/src/lib/asset-store.ts",
151
152
  ];
153
+ /**
154
+ * The auth-adapter exception (#587 in the AILK repo's own architecture.yaml):
155
+ * the base apps/web hands Better Auth its Prisma client from
156
+ * `lib/prisma-for-auth.ts`, whatever modules are selected. Every slice that
157
+ * contributes `webImports` — and so makes apps/web's allowlist non-empty —
158
+ * names it, so no module set yields an allowlist the scaffold's own
159
+ * tools/lint-architecture.ts fails on (#6649).
160
+ */
161
+ const WEB_AUTH_ADAPTER_IMPORT = "database";
152
162
  /**
153
163
  * The per-module slices. Authored for every `RECOGNIZED_MODULES` value; only
154
164
  * `marketing` (the default) is validated end-to-end against a golden.
@@ -157,6 +167,7 @@ export const MODULE_SLICES = {
157
167
  // ── marketing — thin presentation module (the default) ───────────────────
158
168
  marketing: {
159
169
  webImports: [
170
+ WEB_AUTH_ADAPTER_IMPORT,
160
171
  "packages/content-adapters",
161
172
  "packages/jsonld",
162
173
  "packages/metadata",
@@ -236,7 +247,7 @@ export const MODULE_SLICES = {
236
247
  },
237
248
  // ── docs-blog — Pro module: both fumadocs surfaces (D-46) ────────────────
238
249
  "docs-blog": {
239
- webImports: ["packages/fumadocs-schema"],
250
+ webImports: [WEB_AUTH_ADAPTER_IMPORT, "packages/fumadocs-schema"],
240
251
  packages: {
241
252
  "fumadocs-schema": {
242
253
  description: "Fumadocs ↔ schema.org bridge — BlogList / DocsPage JSON-LD from Fumadocs MDX",
@@ -376,7 +387,10 @@ export const MODULE_SLICES = {
376
387
  path: ["scripts"],
377
388
  entries: {
378
389
  prebuild: "fumadocs-mdx",
379
- pretypecheck: "fumadocs-mdx",
390
+ // `pretype-check`, not `pretypecheck`: pnpm runs `pre<name>`
391
+ // only before a script named `<name>`, and the scaffold's is
392
+ // `type-check` (#6680).
393
+ "pretype-check": "fumadocs-mdx",
380
394
  pretest: "fumadocs-mdx",
381
395
  },
382
396
  },
@@ -795,7 +809,7 @@ export const MODULE_SLICES = {
795
809
  // dependency under `node_modules/`, never as a `packages/admin-ui`
796
810
  // source sibling; the `patches` entry below is what puts it in the
797
811
  // buyer's apps/web/package.json.
798
- webImports: ["packages/admin-ui"],
812
+ webImports: [WEB_AUTH_ADAPTER_IMPORT, "packages/admin-ui"],
799
813
  packages: {
800
814
  "admin-ui": {
801
815
  description: "Shared Admin-settings components — the settings shell plus the General / Site / Team panels, presentational only (data + save callbacks as props)",
@@ -892,12 +906,9 @@ export const MODULE_SLICES = {
892
906
  // all, and no scheduling value is ever published to the browser as a
893
907
  // `NEXT_PUBLIC_` variable.
894
908
  //
895
- // NOTE — a scaffolded site cannot run these yet. It receives
896
- // `architecture.yaml` and a `package.json` whose `architecture` script names
897
- // `tools/lint-architecture.ts`, but `build-templates.mjs` snapshots only
898
- // `apps/` and `database/`, so `tools/` is absent. The rules are declarative
899
- // there until the scaffold ships its linter; they ARE enforced in the AILK
900
- // repo, whose own architecture.yaml carries the same four.
909
+ // A scaffolded site enforces these with its own `tools/lint-architecture.ts`
910
+ // (template-overlay/, #6649) via `pnpm architecture`; the AILK repo's own
911
+ // architecture.yaml carries the same four.
901
912
  //
902
913
  // The `api` block is a VERBATIM copy of multi-tenant's, deliberately. Two
903
914
  // reasons: `deriveArchitecture` merges app blocks with `Object.assign`, so
@@ -1434,8 +1445,10 @@ export const MODULE_SLICES = {
1434
1445
  { route: "/flows/[slug]/results/[payload]" },
1435
1446
  // #5562 — the report reader. Unlike the three rows above, this row's
1436
1447
  // code IS this module's own `ownedFiles` (see the note beside it), so
1437
- // the contribution and the delivery shape carrying it agree here.
1438
- { route: "/reports/[slug]" },
1448
+ // the contribution and the delivery shape carrying it agree here. Its
1449
+ // seeds are a directory a template supplies (minimalism-site's four
1450
+ // reports), so the row declares that directory (#6646).
1451
+ { route: "/reports/[slug]", contentDir: "content/en/ReportPage" },
1439
1452
  ],
1440
1453
  // installMap (#5618 D6.3) — the module's ten sample files, moved here out
1441
1454
  // of `site-templates/marketing-site/` so the module OWNS its samples
@@ -1451,13 +1464,13 @@ export const MODULE_SLICES = {
1451
1464
  //
1452
1465
  // Four entries are real renames (the flow and waitlist seeds the API
1453
1466
  // reads, and the report PDF under apps/web/public/); the six MDX entries
1454
- // are self-addressed because no `pageContributions` row declares
1455
- // `/projects`, `/projects/[slug]` or `/thanks/[slug]` yet (spec § 7
1456
- // finding 7 — declaring them changes the enabled-surface set and so the
1457
- // Pro scaffold's file set, a separate decision). Once those rows exist
1458
- // the six move to `pageContributions[].content` with no further
1459
- // migration. `generate-sample-report.py` keeps its root-level
1460
- // destination (finding 1 — AC7 forbids changing the Pro file set).
1467
+ // are self-addressed. Their `/projects`, `/projects/[slug]` and
1468
+ // `/thanks/[slug]` rows are declared by the templates that install them
1469
+ // (`template-manifest.ts`'s signal-collectors sample rows, #6646), not
1470
+ // here — a module contribution would change the enabled-surface set and
1471
+ // so the Pro scaffold's file set (spec 5618 § 7 finding 7).
1472
+ // `generate-sample-report.py` keeps its root-level destination
1473
+ // (finding 1 — AC7 forbids changing the Pro file set).
1461
1474
  installMap: {
1462
1475
  "packages/create-ailk/module-templates/signal-collectors/content/en/ProjectsList/projects.mdx": "content/en/ProjectsList/projects.mdx",
1463
1476
  "packages/create-ailk/module-templates/signal-collectors/content/en/Project/fit-check-funnel.mdx": "content/en/Project/fit-check-funnel.mdx",
@@ -1801,3 +1814,48 @@ export function paidModuleTemplateExclusions(snapshotRoots, repoRoot) {
1801
1814
  // question with the same resolution this answer was computed with (#5701).
1802
1815
  return [...importClosure(repoRoot, [...roots], seeds)].sort();
1803
1816
  }
1817
+ /** The scaffold's architecture linter, relative to the site root (#6649). */
1818
+ export const ARCHITECTURE_LINT_PATH = "tools/lint-architecture.ts";
1819
+ /**
1820
+ * What a scaffold needs so its `architecture.yaml` is enforced rather than
1821
+ * declared (#6649): the linter, the `architecture` + `preflight` root scripts
1822
+ * that run it, and a `.claude/settings.json` PostToolUse hook on Edit|Write
1823
+ * that runs it in-session. Returns one line per missing piece — empty means
1824
+ * all present. `verify-scaffold.mjs` fails on a non-empty result.
1825
+ */
1826
+ export function missingArchitectureEnforcement(siteDir) {
1827
+ const missing = [];
1828
+ if (!existsSync(join(siteDir, ARCHITECTURE_LINT_PATH))) {
1829
+ missing.push(ARCHITECTURE_LINT_PATH);
1830
+ }
1831
+ const readJson = (rel) => {
1832
+ const p = join(siteDir, rel);
1833
+ if (!existsSync(p))
1834
+ return undefined;
1835
+ try {
1836
+ return JSON.parse(readFileSync(p, "utf8"));
1837
+ }
1838
+ catch {
1839
+ return undefined;
1840
+ }
1841
+ };
1842
+ const scripts = readJson("package.json")
1843
+ ?.scripts ?? {};
1844
+ if (!scripts.architecture?.includes(ARCHITECTURE_LINT_PATH)) {
1845
+ missing.push(`package.json scripts.architecture (runs ${ARCHITECTURE_LINT_PATH})`);
1846
+ }
1847
+ if (!scripts.preflight?.includes("pnpm architecture")) {
1848
+ missing.push("package.json scripts.preflight (runs pnpm architecture)");
1849
+ }
1850
+ const postToolUse = readJson(".claude/settings.json")?.hooks?.PostToolUse ?? [];
1851
+ const hooked = postToolUse.some((entry) => {
1852
+ const tools = (entry.matcher ?? "").split("|");
1853
+ return (tools.includes("Edit") &&
1854
+ tools.includes("Write") &&
1855
+ (entry.hooks ?? []).some((h) => /\bpnpm\b.*\barchitecture\b/.test(h.command ?? "")));
1856
+ });
1857
+ if (!hooked) {
1858
+ missing.push(".claude/settings.json PostToolUse hook on Edit|Write (runs pnpm architecture)");
1859
+ }
1860
+ return missing;
1861
+ }
@@ -47,6 +47,13 @@ export interface CliOptions {
47
47
  * value is validated through `parseModules` at parse time; cli.ts re-parses
48
48
  * it to the module list. Undefined when the flag is absent (scaffold mode). */
49
49
  syncArchitecture: string | undefined;
50
+ /**
51
+ * The licence key (`--license <key>`, #6683 D9), shape-checked at parse
52
+ * time. When set, `scaffold()` fetches paid bundles with it and writes it
53
+ * into the new scaffold (`ailk.key` + `apps/api/.env`). Absent when the
54
+ * flag is.
55
+ */
56
+ license?: string;
50
57
  }
51
58
  export declare class ParseError extends Error {
52
59
  constructor(message: string);
@@ -8,6 +8,7 @@
8
8
  * top-level catch turns this into a stderr write + non-zero exit.
9
9
  */
10
10
  import { parseArgs } from "node:util";
11
+ import { assertLicenseKeyShape } from "./lib/license-key-files.js";
11
12
  import { RECOGNIZED_SURFACES, parseSurfaces } from "./surfaces.js";
12
13
  import { RECOGNIZED_TEMPLATES } from "./template-names.js";
13
14
  /**
@@ -73,7 +74,7 @@ Options:
73
74
  Surfaces the declared architecture does not carry are
74
75
  NOT scaffolded — their routes are absent, so a fresh
75
76
  site never serves a stock page with placeholder
76
- content. Add one later with the \`scaffold-page\`
77
+ content. Add one later with the \`add-page\`
77
78
  skill, or re-run with the surface named here.
78
79
  --sync-architecture <csv> Regenerate architecture.yaml in place in an
79
80
  already-scaffolded repo from the given module set
@@ -93,6 +94,9 @@ Options:
93
94
  file, at the target dir (positional arg, else cwd).
94
95
  Bypasses the scaffold path — copies no template, runs
95
96
  no install. Re-run after upgrading create-ailk.
97
+ --license <key> Your licence key: fetch paid modules with it and
98
+ write it into the scaffold (apps/api/.env and a
99
+ gitignored ailk.key, both owner-only).
96
100
  --install After scaffolding, run \`pnpm install\` in the
97
101
  target directory. Off by default.
98
102
  --help, -h Print this message.
@@ -119,6 +123,7 @@ export function parseCliArgs(argv) {
119
123
  options: {
120
124
  modules: { type: "string" },
121
125
  template: { type: "string" },
126
+ license: { type: "string" },
122
127
  surfaces: { type: "string" },
123
128
  "sync-architecture": { type: "string" },
124
129
  "sync-catalog": { type: "boolean", default: false },
@@ -141,6 +146,7 @@ export function parseCliArgs(argv) {
141
146
  const syncArchitecture = parsed.values["sync-architecture"];
142
147
  const syncCatalog = parsed.values["sync-catalog"] === true;
143
148
  const withCatalog = parsed.values["with-catalog"] === true;
149
+ const license = parseLicense(parsed.values.license);
144
150
  // The two sync modes each exit after rewriting their one file, so asking for
145
151
  // both in one invocation has no coherent meaning.
146
152
  if (syncCatalog && syncArchitecture !== undefined) {
@@ -202,8 +208,24 @@ export function parseCliArgs(argv) {
202
208
  syncCatalog,
203
209
  withCatalog,
204
210
  syncArchitecture: undefined,
211
+ ...(license !== undefined ? { license } : {}),
205
212
  };
206
213
  }
214
+ /**
215
+ * Shape-check the `--license` value through the one validator `license set`
216
+ * also uses, re-thrown as a ParseError. The message never repeats the value.
217
+ */
218
+ function parseLicense(raw) {
219
+ if (raw === undefined)
220
+ return undefined;
221
+ try {
222
+ assertLicenseKeyShape(raw);
223
+ }
224
+ catch (err) {
225
+ throw new ParseError(`--license: ${err.message}`);
226
+ }
227
+ return raw;
228
+ }
207
229
  /**
208
230
  * Validate the `--template` value against the declared template set
209
231
  * (spec 5481 § 3 DD1).
@@ -15,10 +15,12 @@
15
15
  * import { scaffoldSync } from './programmatic.js';
16
16
  * const result = scaffoldSync({ targetDir: '/tmp/my-site', projectName: 'my-site' });
17
17
  */
18
+ import { type AddModuleResult } from "./add-module.js";
18
19
  import { CATALOG_TARGET_PATH } from "./component-catalog.js";
19
20
  import { type ModuleName } from "./parse-args.js";
20
21
  import { type TemplateLayout } from "./site-chrome.js";
21
22
  import { type SyncRoutesResult } from "./sync-routes.js";
23
+ import { type StarterDesign } from "./template-manifest.js";
22
24
  export interface ScaffoldOptions {
23
25
  /** Absolute or relative path to the target directory. Must not exist or be empty. */
24
26
  targetDir: string;
@@ -84,6 +86,14 @@ export interface ScaffoldOptions {
84
86
  * bare/all-Free scaffold reads neither).
85
87
  */
86
88
  licenseKey?: string;
89
+ /**
90
+ * The buyer's licence key (`--license`, #6683 D9). Takes precedence over
91
+ * `licenseKey` and `AILK_LICENSE_KEY` for bundle fetches, and — unlike
92
+ * them — is PERSISTED: after a successful scaffold it is written to the
93
+ * target's `ailk.key` and `apps/api/.env` (`writeLicenseKeyFiles`). Unset,
94
+ * the scaffold is byte-identical to one made before the option existed.
95
+ */
96
+ license?: string;
87
97
  /** Overrides `AILK_MODULE_REGISTRY_URL` / the built-in module-distribution default. */
88
98
  moduleRegistryUrl?: string;
89
99
  /** Overrides `AILK_TEMPLATE_REGISTRY_URL` / the built-in template-distribution default. */
@@ -99,6 +109,15 @@ export interface ScaffoldOptions {
99
109
  * caller) writes the same defaults `site-chrome.ts` always has.
100
110
  */
101
111
  layout?: TemplateLayout;
112
+ /**
113
+ * The design whose layout knobs fill what `layout` leaves unset (#6638,
114
+ * `DESIGN_LAYOUTS`). `scaffold()` sets it from the resolved template's
115
+ * `design`; a caller may name one to OVERRIDE the template's — the CI-only
116
+ * shape × design pairing `verify-scaffold.mjs --design` renders (#6638
117
+ * DD-4). No CLI flag sets it. Unset (every existing `scaffoldSync` caller)
118
+ * resolves exactly as before.
119
+ */
120
+ design?: StarterDesign;
102
121
  }
103
122
  export interface ScaffoldResult {
104
123
  /** Resolved absolute path to the scaffolded directory. */
@@ -233,6 +252,33 @@ export interface SyncRoutesSyncOptions {
233
252
  export declare function syncRoutesSync(opts: SyncRoutesSyncOptions): SyncRoutesResult;
234
253
  /** Re-exported beside the other sync results so consumers see one surface. */
235
254
  export type { RouteReport, SyncRoutesResult } from "./sync-routes.js";
255
+ export interface AddModuleOptions {
256
+ /** The already-scaffolded site (absolute or relative). */
257
+ targetDir: string;
258
+ /** The module to add (validated upstream by `parseAddModuleArgs`). */
259
+ moduleName: ModuleName;
260
+ /** Defaults to `process.env["AILK_LICENSE_KEY"]`, as for `scaffold()`. */
261
+ licenseKey?: string;
262
+ /** Defaults to `AILK_MODULE_REGISTRY_URL` / the built-in default. */
263
+ moduleRegistryUrl?: string;
264
+ /** Defaults to `globalThis.fetch`. Never a live call in tests. */
265
+ fetchImpl?: typeof fetch;
266
+ }
267
+ /**
268
+ * Apply a purchased module to an already-scaffolded site in place
269
+ * (`create-ailk add-module`, #6653). The thin package-rooted wrapper around
270
+ * `add-module.ts`'s `applyModuleToScaffold`, the `syncRoutesSync` split:
271
+ *
272
+ * 1. the target must be an existing scaffold — the same check as the sync
273
+ * subcommands above;
274
+ * 2. a free module is refused before any key read or network call — it
275
+ * ships with every scaffold, there is no bundle to add;
276
+ * 3. `resolvePaidModules` — the SAME gate `scaffold()` runs, so a missing
277
+ * key or a key that does not cover the module is refused with the
278
+ * scaffold-time message, before anything is written;
279
+ * 4. the fetched bundle is merged under the plus-config policy.
280
+ */
281
+ export declare function addModule(opts: AddModuleOptions): Promise<AddModuleResult>;
236
282
  /** Re-exported so the CLI can name the file it wrote without duplicating it. */
237
283
  export { CATALOG_TARGET_PATH };
238
284
  /**