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
@@ -0,0 +1,334 @@
1
+ /**
2
+ * `create-ailk add-module <module>` (alias `upgrade`) — apply a purchased
3
+ * module to a site that was scaffolded without it, in place (#6653).
4
+ *
5
+ * A Free customer who buys Pro already has a site. `scaffold()` merges a
6
+ * module bundle only onto a fresh tree; this merges one onto a tree someone
7
+ * has lived in, under the plus-config policy (operator-disposed 2026-09-24):
8
+ *
9
+ * - a file the tree lacks is written;
10
+ * - a file the tree has is never overwritten — when its bytes differ from
11
+ * the bundle's it is reported `skipped` with the exact change the bundle
12
+ * wanted (a manual-review item), and when they match it is not reported;
13
+ * - the bundle's `patches` are the only edits to an existing file, and only
14
+ * to the config allowlist: `package.json` dependency/script keys,
15
+ * `next.config.*`, `jest.config.*`.
16
+ *
17
+ * Then the module's route surfaces are synced in (`syncRoutes` with the
18
+ * module's surfaces forced carried), `architecture.yaml` is regenerated from
19
+ * the site's current module set plus this one, and the generated nav follows
20
+ * the new surfaces unless the site has edited it. Run twice, the second run
21
+ * changes nothing and reports nothing.
22
+ *
23
+ * Pure module: no `import.meta.url`, so jest can import it directly — the
24
+ * package-rooted inputs (`templatesDir`, `versions`) and the fetched bundle
25
+ * bytes are supplied by `programmatic.ts`'s `addModule`, the same split as
26
+ * `sync-routes.ts` / `syncRoutesSync`.
27
+ */
28
+ import { existsSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync, } from "node:fs";
29
+ import { basename, dirname, join, resolve } from "node:path";
30
+ import { parseArgs } from "node:util";
31
+ import { AddModuleRefusedError, readArchitectureHeader, } from "./add-module-core.js";
32
+ import { deriveArchitecture } from "./derive-architecture.js";
33
+ import { diffTwoStrings, extractModuleBundle, } from "./lib/extract-module-bundle.js";
34
+ import { normalizeModules } from "./module-architecture.js";
35
+ import { ParseError, parseModules } from "./parse-args.js";
36
+ import { SITE_ROSTER_PATH, moduleRosterRows, planRosterAppend, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
37
+ import { MODULE_SURFACES, NAV_CONFIG_PATH, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
38
+ import { syncRoutes } from "./sync-routes.js";
39
+ export { AddModuleRefusedError, readArchitectureHeader };
40
+ /** `package.json` paths a `json` patch may write under the plus-config policy. */
41
+ const PACKAGE_JSON_PATCH_KEYS = new Set([
42
+ "dependencies",
43
+ "devDependencies",
44
+ "scripts",
45
+ ]);
46
+ /**
47
+ * Refuse a bundle whose `patches` reach past the plus-config allowlist
48
+ * (#6653 D3), matched on the target's basename so any depth qualifies:
49
+ * `package.json` (only `merge-keys`/`append-unique` on a dependency or script
50
+ * key), `next.config.*`, `jest.config.*`. Runs inside `installBundleTree`
51
+ * after the patches validate and before any is computed or written.
52
+ */
53
+ export function checkPlusConfigPatches(patches) {
54
+ for (const patch of patches) {
55
+ const name = basename(patch.target);
56
+ if (name === "package.json") {
57
+ for (const op of patch.ops) {
58
+ const allowed = (op.op === "merge-keys" || op.op === "append-unique") &&
59
+ op.path.length > 0 &&
60
+ PACKAGE_JSON_PATCH_KEYS.has(op.path[0]);
61
+ if (!allowed) {
62
+ throw new AddModuleRefusedError(`the module bundle patches ${patch.target} outside its dependency and script keys — add-module only patches dependencies, devDependencies and scripts there`);
63
+ }
64
+ }
65
+ continue;
66
+ }
67
+ if (/^next\.config\.[^/]+$/.test(name) ||
68
+ /^jest\.config\.[^/]+$/.test(name)) {
69
+ continue;
70
+ }
71
+ throw new AddModuleRefusedError(`the module bundle patches ${patch.target}, which is outside the config files add-module may edit (package.json, next.config.*, jest.config.*)`);
72
+ }
73
+ }
74
+ /**
75
+ * Merge one module's bundle onto an existing site, sync its routes,
76
+ * regenerate `architecture.yaml` and the nav, then append the module's pages
77
+ * to the site's roster. Every refusal — an unreadable header or roster, a
78
+ * module row no shipped manifest carries, a patch outside the allowlist, a
79
+ * patch conflict, an unsafe bundle — throws before the first write (the header
80
+ * and roster are read first; `extractModuleBundle` validates and computes
81
+ * everything before it copies).
82
+ */
83
+ export async function applyModuleToScaffold(opts) {
84
+ const targetAbs = resolve(opts.targetDir);
85
+ const archPath = join(targetAbs, "architecture.yaml");
86
+ const current = readArchitectureHeader(readFileSync(archPath, "utf8"));
87
+ const modules = normalizeModules([...current.modules, opts.moduleName]);
88
+ // Resolved before the first write: a header surface that is unknown, or
89
+ // belongs to a module the site does not carry, is a refusal with the tree
90
+ // untouched — not a failure after the bundle has landed.
91
+ let surfaces;
92
+ try {
93
+ surfaces = resolveSurfaces(modules, current.surfaces);
94
+ }
95
+ catch (err) {
96
+ throw new AddModuleRefusedError(`architecture.yaml's \`# Surfaces:\` header cannot be carried forward: ${err.message}`);
97
+ }
98
+ // The roster and every module row it may need, likewise before the first
99
+ // write. A template-less site has no roster and needs none.
100
+ const siteRoster = readSiteRoster(targetAbs);
101
+ const references = siteRoster === undefined
102
+ ? []
103
+ : modules.flatMap((m) => moduleRosterRows(m, opts.siteTemplatesDir));
104
+ const { report } = await extractModuleBundle({
105
+ bytes: opts.bundleBytes,
106
+ targetDir: targetAbs,
107
+ moduleName: opts.moduleName,
108
+ versions: opts.versions,
109
+ markers: opts.markers,
110
+ existingFilePolicy: "skip-existing",
111
+ checkPatches: checkPlusConfigPatches,
112
+ });
113
+ const routes = syncRoutes({
114
+ targetDir: targetAbs,
115
+ templatesDir: opts.templatesDir,
116
+ versions: opts.versions,
117
+ apply: true,
118
+ forceSurfaces: MODULE_SURFACES[opts.moduleName] ?? [],
119
+ });
120
+ // The same composition `syncArchitectureSync` writes — a pure function of
121
+ // the module set, so an unchanged set rewrites identical bytes.
122
+ writeFileSync(archPath, deriveArchitecture(modules, surfaces));
123
+ const rows = [...report];
124
+ const navRow = syncNavConfig(targetAbs, current.surfaces, surfaces);
125
+ if (navRow !== undefined)
126
+ rows.push(navRow);
127
+ const roster = {
128
+ state: "none",
129
+ appended: [],
130
+ removed: [],
131
+ problems: [],
132
+ path: siteRoster?.path ?? SITE_ROSTER_PATH,
133
+ };
134
+ if (siteRoster !== undefined) {
135
+ // After the bundle has landed: a `contentDir` row's form follows the
136
+ // seeds now on disk.
137
+ const plan = planRosterAppend({
138
+ targetDir: targetAbs,
139
+ roster: siteRoster,
140
+ modules,
141
+ references,
142
+ siteTemplatesDir: opts.siteTemplatesDir,
143
+ templatesDir: opts.templatesDir,
144
+ markers: opts.markers,
145
+ });
146
+ for (const seed of plan.seeds) {
147
+ if (seed.remove) {
148
+ const abs = join(targetAbs, seed.path);
149
+ rmSync(abs);
150
+ if (readdirSync(dirname(abs)).length === 0)
151
+ rmdirSync(dirname(abs));
152
+ rows.push({ path: seed.path, action: "removed" });
153
+ roster.removed.push(seed.path);
154
+ }
155
+ else {
156
+ rows.push({ path: seed.path, action: "skipped", wanted: seed.wanted });
157
+ }
158
+ }
159
+ if (plan.next !== undefined) {
160
+ writeFileSync(join(targetAbs, siteRoster.path), plan.next);
161
+ rows.push({
162
+ path: siteRoster.path,
163
+ action: "patched",
164
+ change: diffTwoStrings(siteRoster.path, siteRoster.text, plan.next, true),
165
+ });
166
+ }
167
+ roster.state = plan.next === undefined ? "unchanged" : "appended";
168
+ roster.appended = plan.appended;
169
+ roster.problems = siteRosterProblems({
170
+ targetDir: targetAbs,
171
+ siteTemplatesDir: opts.siteTemplatesDir,
172
+ templatesDir: opts.templatesDir,
173
+ });
174
+ }
175
+ return { targetDir: targetAbs, modules, report: rows, routes, roster };
176
+ }
177
+ /**
178
+ * Regenerate the site nav (`scaffoldSync` writes it from the surface set) for
179
+ * the new surfaces — but only when the site's copy is still exactly what the
180
+ * OLD surface set generates. An edited nav is the customer's file: left as-is
181
+ * and reported `skipped` with the change, like any other.
182
+ */
183
+ function syncNavConfig(targetAbs, before, after) {
184
+ const navPath = join(targetAbs, NAV_CONFIG_PATH);
185
+ const wanted = renderNavConfig(after);
186
+ const existing = existsSync(navPath)
187
+ ? readFileSync(navPath, "utf8")
188
+ : undefined;
189
+ if (existing === wanted)
190
+ return undefined;
191
+ if (existing === undefined || existing === renderNavConfig(before)) {
192
+ writeFileSync(navPath, wanted);
193
+ return { path: NAV_CONFIG_PATH, action: "written" };
194
+ }
195
+ return {
196
+ path: NAV_CONFIG_PATH,
197
+ action: "skipped",
198
+ wanted: diffTwoStrings(NAV_CONFIG_PATH, existing, wanted, true),
199
+ };
200
+ }
201
+ /** Indent a multi-line diff under its report row. */
202
+ function indent(text) {
203
+ return text.split("\n").map((line) => ` ${line}`);
204
+ }
205
+ /** The report `add-module` prints. */
206
+ export function formatAddModuleReport(result, moduleName) {
207
+ const written = result.report.filter((row) => row.action === "written");
208
+ const patched = result.report.filter((row) => row.action === "patched");
209
+ const skipped = result.report.filter((row) => row.action === "skipped");
210
+ const removed = result.report.filter((row) => row.action === "removed");
211
+ const lines = [
212
+ `[create-ailk] add-module ${moduleName} → ${result.targetDir}`,
213
+ ];
214
+ if (result.report.length === 0 && result.routes.applied.length === 0) {
215
+ lines.push(`Nothing to add — the site already carries everything ${moduleName} brings.`);
216
+ }
217
+ if (written.length > 0) {
218
+ lines.push(`Written (${written.length}):`);
219
+ lines.push(...written.map((row) => ` ${row.path}`));
220
+ }
221
+ if (patched.length > 0) {
222
+ lines.push(`Patched (${patched.length}):`);
223
+ for (const row of patched) {
224
+ lines.push(` ${row.path}`);
225
+ if (row.action === "patched")
226
+ lines.push(...indent(row.change));
227
+ }
228
+ }
229
+ if (removed.length > 0) {
230
+ lines.push(`Removed (${removed.length}):`);
231
+ lines.push(...removed.map((row) => ` ${row.path}`));
232
+ }
233
+ if (skipped.length > 0) {
234
+ lines.push(`Needs manual review (${skipped.length}) — the site's copy differs from the module's and was left untouched. Apply the change by hand if you want it:`);
235
+ for (const row of skipped) {
236
+ lines.push(` ${row.path}`);
237
+ if (row.action === "skipped")
238
+ lines.push(...indent(row.wanted));
239
+ }
240
+ }
241
+ const { routes } = result;
242
+ if (routes.applied.length > 0) {
243
+ lines.push(`Routes copied (${routes.applied.length}):`);
244
+ lines.push(...routes.applied.map((route) => ` ${route}`));
245
+ }
246
+ if (routes.appliedDependencies.length > 0) {
247
+ lines.push(`Also copied ${routes.appliedDependencies.length} file(s) those routes import:`);
248
+ lines.push(...routes.appliedDependencies.map((dep) => ` ${dep}`));
249
+ }
250
+ if (routes.missingMessageNamespaces.length > 0) {
251
+ lines.push("Also required — message namespace(s) not found in apps/web/messages/en.json (add manually, not auto-merged):");
252
+ lines.push(...routes.missingMessageNamespaces.map((ns) => ` ${ns}`));
253
+ }
254
+ lines.push(`architecture.yaml modules: ${result.modules.join(", ")}`);
255
+ lines.push(...formatRoster(result.roster, moduleName));
256
+ if (written.length > 0 || patched.length > 0 || routes.applied.length > 0) {
257
+ lines.push("Next: pnpm install, then pnpm build.");
258
+ }
259
+ return lines.join("\n") + "\n";
260
+ }
261
+ /** The page-roster lines of the report (#6682 D7). */
262
+ function formatRoster(roster, moduleName) {
263
+ if (roster.state === "none") {
264
+ return [
265
+ `Page roster: none. This site was scaffolded without a template, so it has no ${SITE_ROSTER_PATH} to append to.`,
266
+ ];
267
+ }
268
+ const lines = [
269
+ roster.state === "appended" && roster.appended.length === 0
270
+ ? `Page roster: reordered ${roster.path}'s rows into template-then-module order`
271
+ : roster.state === "appended"
272
+ ? `Page roster: appended ${roster.appended.length} row(s) to ${roster.path}: ${roster.appended.join(", ")}`
273
+ : `Page roster: ${roster.path} already carries ${moduleName}'s rows`,
274
+ ];
275
+ if (roster.problems.length === 0) {
276
+ lines.push("Page roster check: clean");
277
+ }
278
+ else {
279
+ lines.push(`Page roster check: ${roster.problems.length} problem(s)`);
280
+ lines.push(...roster.problems.map((p) => ` ${p}`));
281
+ }
282
+ return lines;
283
+ }
284
+ export const ADD_MODULE_HELP = `create-ailk add-module — add a purchased module to an existing site.
285
+
286
+ Usage:
287
+ create-ailk add-module <module> [<target-dir>]
288
+ create-ailk upgrade <module> [<target-dir>]
289
+
290
+ Arguments:
291
+ <module> One paid module, e.g. docs-blog. Needs
292
+ AILK_LICENSE_KEY covering it.
293
+ <target-dir> An already-scaffolded repo (contains
294
+ architecture.yaml). Defaults to the current dir.
295
+
296
+ Options:
297
+ --help, -h Print this message.
298
+
299
+ Writes the module's files the site lacks and never overwrites one it has: a
300
+ file whose content differs is left as-is and listed for manual review with the
301
+ change the module wanted. Only package.json dependency/script keys,
302
+ next.config.* and jest.config.* are patched. Then copies the module's routes
303
+ and regenerates architecture.yaml. Running it again changes nothing.
304
+ `;
305
+ /**
306
+ * Parse the argv AFTER the `add-module` / `upgrade` word. Strict
307
+ * `node:util` parseArgs, like `parseSyncRoutesArgs`; the module is validated
308
+ * the same way one `--modules` entry is.
309
+ */
310
+ export function parseAddModuleArgs(argv) {
311
+ const parsed = parseArgs({
312
+ args: argv,
313
+ strict: true,
314
+ allowPositionals: true,
315
+ options: {
316
+ help: { type: "boolean", default: false, short: "h" },
317
+ },
318
+ });
319
+ const help = parsed.values.help === true;
320
+ const [raw, target, ...rest] = parsed.positionals;
321
+ if (rest.length > 0) {
322
+ throw new ParseError(`expected a module and at most one target directory, got ${parsed.positionals.length} arguments`);
323
+ }
324
+ if (raw === undefined) {
325
+ if (help)
326
+ return { module: undefined, target: undefined, help };
327
+ throw new ParseError("add-module needs a module name, e.g. `create-ailk add-module docs-blog`");
328
+ }
329
+ const modules = parseModules(raw);
330
+ if (modules.length !== 1) {
331
+ throw new ParseError(`add-module takes exactly one module, got "${raw}" — run it once per module`);
332
+ }
333
+ return { module: modules[0], target, help };
334
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The category profile (#6640; starter-template contract § D) — one YAML
3
+ * document per leaf category slug at `category-profiles/<slug>.yaml`, the data
4
+ * the `fill-page` skill reads to make one starter speak for one business type.
5
+ *
6
+ * Dependency-free by design: `create-ailk` ships `tar` as its only runtime
7
+ * dependency, so this is a hand-written validator in the style of
8
+ * `resolveTemplate`/`TemplateError`, not a Zod schema. The caller parses the
9
+ * YAML and injects the registries (page types, leaf categories); nothing here
10
+ * imports a workspace package at run time.
11
+ */
12
+ import type { StarterShape } from "./template-manifest.js";
13
+ /**
14
+ * The contract's shape table, as data: each shape's primary node, the
15
+ * secondaries it allows, and whether it is reserved. Keyed by the manifest's
16
+ * own `StarterShape`, so a shape added there without a row here fails
17
+ * type-check.
18
+ */
19
+ export declare const CATEGORY_PROFILE_SHAPES: {
20
+ readonly "organization-product": {
21
+ readonly primary: "Organization";
22
+ readonly secondaries: readonly ["SoftwareApplication"];
23
+ readonly reserved: false;
24
+ };
25
+ readonly "organization-service": {
26
+ readonly primary: "Organization";
27
+ readonly secondaries: readonly [];
28
+ readonly reserved: false;
29
+ };
30
+ readonly "local-business-service": {
31
+ readonly primary: "LocalBusiness";
32
+ readonly secondaries: readonly ["Person"];
33
+ readonly reserved: false;
34
+ };
35
+ readonly "local-business-product": {
36
+ readonly primary: "LocalBusiness";
37
+ readonly secondaries: readonly [];
38
+ readonly reserved: true;
39
+ };
40
+ readonly "person-service": {
41
+ readonly primary: "Person";
42
+ readonly secondaries: readonly ["Organization"];
43
+ readonly reserved: false;
44
+ };
45
+ };
46
+ /**
47
+ * schema.org's `LocalBusiness` subtree, `LocalBusiness` itself included — the
48
+ * closure of the `LocalBusiness` union in `schema-dts`. A literal so `src/`
49
+ * needs no type package at run time; `category-profile.test.ts` proves it
50
+ * set-equal to the `schema-dts` closure, so it cannot drift silently.
51
+ */
52
+ export declare const LOCAL_BUSINESS_TYPES: ReadonlySet<string>;
53
+ export declare const OFFERING_PRICING: readonly ["quote", "fixed", "hourly", "subscription"];
54
+ export declare const PROOF_KINDS: readonly ["credential", "reviews", "before-after", "response-time", "guarantee", "experience"];
55
+ export declare const CONTENT_POSTURES: readonly ["instructions", "sample"];
56
+ export interface CategoryOffering {
57
+ name: string;
58
+ job: string;
59
+ pricing: (typeof OFFERING_PRICING)[number];
60
+ }
61
+ export interface CategoryProof {
62
+ kind: (typeof PROOF_KINDS)[number];
63
+ label: string;
64
+ }
65
+ export type CategoryRosterDelta = {
66
+ op: "add";
67
+ route: string;
68
+ pageType: string;
69
+ reason: string;
70
+ } | {
71
+ op: "drop";
72
+ route: string;
73
+ reason: string;
74
+ };
75
+ export interface CategoryIntakeItem {
76
+ id: string;
77
+ ask: string;
78
+ /** The section types an answer to this question can fill. */
79
+ fills: string[];
80
+ }
81
+ export interface CategoryProfile {
82
+ slug: string;
83
+ shape: StarterShape;
84
+ /** The schema.org subtype of the primary node; only on `local-business-*`. */
85
+ subtype?: string;
86
+ secondary?: string;
87
+ offerings: CategoryOffering[];
88
+ proof: CategoryProof[];
89
+ vocabulary: {
90
+ problem: string[];
91
+ service: string[];
92
+ outcome: string[];
93
+ };
94
+ roster: CategoryRosterDelta[];
95
+ /** Nav label → replacement label. Declared, not yet applied to a scaffold. */
96
+ nav: Record<string, string>;
97
+ content_posture: (typeof CONTENT_POSTURES)[number];
98
+ intake: CategoryIntakeItem[];
99
+ }
100
+ /** A recorded tier-2 intake — `content/_intake.yaml`. */
101
+ export interface CategoryIntake {
102
+ answers: Record<string, string | string[] | null>;
103
+ }
104
+ export interface ParseCategoryProfileOptions {
105
+ /** The file's basename without `.yaml`; the profile's `slug` must equal it. */
106
+ fileSlug: string;
107
+ /** Registered page-type keys (the schema registry). */
108
+ pageTypes: ReadonlySet<string>;
109
+ /** Leaf category slugs (the category registry). */
110
+ categories: ReadonlySet<string>;
111
+ }
112
+ /** Thrown on an invalid profile or intake; the message names the failing field path. */
113
+ export declare class CategoryProfileError extends Error {
114
+ constructor(message: string);
115
+ }
116
+ /**
117
+ * Validate a parsed category profile.
118
+ * @throws CategoryProfileError naming the failing field path.
119
+ */
120
+ export declare function parseCategoryProfile(raw: unknown, options: ParseCategoryProfileOptions): CategoryProfile;
121
+ /**
122
+ * Validate a recorded intake (`content/_intake.yaml`) against its profile.
123
+ * Unknown ids fail; an id the file omits is returned as `null` (unanswered).
124
+ * @throws CategoryProfileError naming the failing field path.
125
+ */
126
+ export declare function parseIntake(raw: unknown, profile: CategoryProfile): CategoryIntake;