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
@@ -1,5 +1,6 @@
1
+ import { type ModulePatch } from "../module-architecture.js";
1
2
  import { type TemplateLayout } from "../site-chrome.js";
2
- import type { WorkspaceVersionMap } from "../substitute.js";
3
+ import { type MarkerMap, type WorkspaceVersionMap } from "../substitute.js";
3
4
  /** Exported for `fetch-template.ts`/`fetch-module.ts`'s Content-Length guard (#5514) — one cap, not two literals. */
4
5
  export declare const MAX_TOTAL_SIZE_BYTES: number;
5
6
  /**
@@ -42,6 +43,12 @@ export interface ExtractModuleBundleOptions {
42
43
  templateExtends?: string;
43
44
  /** The call-scoped ownership registry (#5618 D4). See `InstallBundleTreeOptions`. */
44
45
  ownership?: LayerOwnership;
46
+ /** The scaffold's marker map (#6460). See `InstallBundleTreeOptions`. */
47
+ markers?: MarkerMap;
48
+ /** What to do with a destination that already exists (#6653 D3). See `InstallBundleTreeOptions`. */
49
+ existingFilePolicy?: ExistingFilePolicy;
50
+ /** A caller check on the manifest's `patches` (#6653 D3). See `InstallBundleTreeOptions`. */
51
+ checkPatches?: (patches: readonly ModulePatch[]) => void;
45
52
  }
46
53
  /**
47
54
  * Extract a fetched module bundle into `targetDir`, merging its files onto
@@ -89,8 +96,64 @@ export interface InstallBundleTreeOptions {
89
96
  ownership?: LayerOwnership;
90
97
  /** Canonical `@working-theory/*` versions for `workspace:*` substitution in patch targets. */
91
98
  versions?: WorkspaceVersionMap;
99
+ /**
100
+ * The scaffold's marker map (#6460) — `copy.ts`'s `buildScaffoldMarkers`,
101
+ * the SAME object `copyTemplate` substituted the base `templates/` copy
102
+ * with, threaded here so a `{{MARKER}}` resolves to the same value whichever
103
+ * layer authored the file it sits in. Applied by `copyTree` and the
104
+ * installMap loop below, to text files only (`copy.ts`'s own `isTextFile`),
105
+ * so the classifier is one definition rather than two. Absent ⟹ files land
106
+ * byte-for-byte as committed, the pre-#6460 behaviour.
107
+ *
108
+ * EVERY layer that lands on the snapshot gets it, modules included — not
109
+ * just the base layer and the site template. A bundle fetched by version
110
+ * from the paid registry may have been produced by an older create-ailk
111
+ * that never saw this pass, so a bundle author should know that the five
112
+ * mapped keys, and only those, now resolve in their `.md`/`.json`/`.ts`
113
+ * files; `substituteMarkers` leaves every other `{{KEY}}` alone.
114
+ */
115
+ markers?: MarkerMap;
116
+ /**
117
+ * What to do with a destination that already exists (#6653 D3). Default
118
+ * `"overwrite"` — the scaffold's layer-over-layer behaviour, unchanged.
119
+ * `"skip-existing"` — the `add-module` merge onto a tree a customer has
120
+ * lived in: an existing file is never written; when its bytes differ from
121
+ * what this install would write it is reported `skipped` with the change
122
+ * the bundle wanted, and when they are equal it is not reported at all.
123
+ * `patches` are NOT gated by it — they are the one mechanism designed to
124
+ * edit an existing file, and they are already idempotent.
125
+ */
126
+ existingFilePolicy?: ExistingFilePolicy;
127
+ /**
128
+ * Run against the manifest's `patches` after they validate and BEFORE any
129
+ * of them is computed or anything is written (#6653 D3) — a throw rejects
130
+ * the tree with `targetDir` untouched. `add-module` uses it to hold a
131
+ * bundle's patches to the plus-config allowlist.
132
+ */
133
+ checkPatches?: (patches: readonly ModulePatch[]) => void;
92
134
  }
93
- /** `installBundleTree`'s result — presently only the template-only `layout` knobs. */
135
+ /** See `InstallBundleTreeOptions.existingFilePolicy`. */
136
+ export type ExistingFilePolicy = "overwrite" | "skip-existing";
137
+ /**
138
+ * One file this install landed, patched, or declined to land (#6653 D3). A
139
+ * destination whose bytes already equal the install's produces no row.
140
+ * `change` / `wanted` is a unified diff (`diffTwoStrings`) — for `skipped`,
141
+ * the exact edit the bundle wanted, so the row is a manual-review item and
142
+ * never a bare path.
143
+ */
144
+ export type InstallReportRow = {
145
+ path: string;
146
+ action: "written";
147
+ } | {
148
+ path: string;
149
+ action: "patched";
150
+ change: string;
151
+ } | {
152
+ path: string;
153
+ action: "skipped";
154
+ wanted: string;
155
+ };
156
+ /** `installBundleTree`'s result. */
94
157
  export interface InstallBundleTreeResult {
95
158
  /**
96
159
  * The installed template's validated `layout` block (#5612 D1), unresolved
@@ -98,6 +161,8 @@ export interface InstallBundleTreeResult {
98
161
  * module install, or a template whose manifest declares no `layout`.
99
162
  */
100
163
  layout?: TemplateLayout;
164
+ /** One row per file written, patched or skipped, in install order (#6653 D3). */
165
+ report: InstallReportRow[];
101
166
  }
102
167
  /**
103
168
  * Apply one resolved manifest's `installMap`, plain content and `patches`
@@ -110,3 +175,12 @@ export interface InstallBundleTreeResult {
110
175
  * rejected tree leaves `targetDir` exactly as it was.
111
176
  */
112
177
  export declare function installBundleTree(options: InstallBundleTreeOptions): InstallBundleTreeResult;
178
+ /**
179
+ * A unified diff of `before` → `after` for the report (#6653 D3) — the one
180
+ * helper behind both a `skipped` row's `wanted` and a `patched` row's
181
+ * `change`. Line-based LCS; the inputs are config and content files, small
182
+ * enough that the quadratic table is not a concern. `isText` false yields the
183
+ * binary summary instead, reading each string's length as its byte count
184
+ * (the caller decodes binary content as latin1).
185
+ */
186
+ export declare function diffTwoStrings(path: string, before: string, after: string, isText: boolean): string;
@@ -83,8 +83,10 @@ import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
83
83
  import { pipeline } from "node:stream/promises";
84
84
  import { createGunzip } from "node:zlib";
85
85
  import * as tar from "tar";
86
+ import { isTextFile } from "../copy.js";
86
87
  import { coOwnedModuleDestinations, } from "../module-architecture.js";
87
88
  import { validateTemplateLayout, } from "../site-chrome.js";
89
+ import { substituteMarkers, } from "../substitute.js";
88
90
  import { computeModulePatches, writeModulePatches } from "./apply-module-patches.js";
89
91
  /** Soft limits to prevent decompression-bomb / zip-bomb attacks. Same values as extractBundle. */
90
92
  const MAX_ENTRIES = 1000;
@@ -98,6 +100,8 @@ const AILK_MODULE_JSON = "ailk-module.json";
98
100
  * Never copied into the scaffold either.
99
101
  */
100
102
  const AILK_TEMPLATE_JSON = "ailk-template.json";
103
+ /** A template's hand-and-generated roster document — never installed (#6658 D7). */
104
+ const TEMPLATE_MANIFEST_MD = "TEMPLATE-MANIFEST.md";
101
105
  /** The op names valid for each patch `kind` (spec 5418 § 3 D1's ModulePatchOp union, partitioned). */
102
106
  const VALID_OPS_BY_KIND = {
103
107
  json: new Set(["merge-keys", "append-unique"]),
@@ -446,6 +450,13 @@ export async function extractModuleBundle(options) {
446
450
  : {}),
447
451
  ...(options.ownership !== undefined ? { ownership: options.ownership } : {}),
448
452
  ...(options.versions !== undefined ? { versions: options.versions } : {}),
453
+ ...(options.markers !== undefined ? { markers: options.markers } : {}),
454
+ ...(options.existingFilePolicy !== undefined
455
+ ? { existingFilePolicy: options.existingFilePolicy }
456
+ : {}),
457
+ ...(options.checkPatches !== undefined
458
+ ? { checkPatches: options.checkPatches }
459
+ : {}),
449
460
  });
450
461
  }
451
462
  finally {
@@ -463,7 +474,7 @@ export async function extractModuleBundle(options) {
463
474
  * rejected tree leaves `targetDir` exactly as it was.
464
475
  */
465
476
  export function installBundleTree(options) {
466
- const { sourceRoot, name, templateRequires, templateExtends, ownership } = options;
477
+ const { sourceRoot, name, templateRequires, templateExtends, ownership, markers } = options;
467
478
  const targetAbs = resolve(options.targetDir);
468
479
  const isTemplate = templateRequires !== undefined;
469
480
  const subject = isTemplate ? `template "${name}"` : `module "${name}"`;
@@ -491,6 +502,7 @@ export function installBundleTree(options) {
491
502
  throw new Error(`${subject}: a template must not carry patches — content, never code (spec 5462 D9)`);
492
503
  }
493
504
  validatePatches(patches, targetAbs, subject);
505
+ options.checkPatches?.(patches);
494
506
  // The exclusion set copyTree/the content-only guard both use — built from
495
507
  // NORMALIZED installMap keys (#5514) so a "./x" key excludes the same
496
508
  // string `rel`'s traversal below computes, rather than colliding with it
@@ -503,6 +515,11 @@ export function installBundleTree(options) {
503
515
  ...Object.keys(installMap).map((key) => normalize(key).split(sep).join("/")),
504
516
  ]);
505
517
  if (isTemplate) {
518
+ // #6658 D7 — a template's roster of record lands as JSON, through its
519
+ // `installMap` (`TEMPLATE-MANIFEST.json` → `.ailk-site-manifest.json`).
520
+ // The Markdown is the template's own documentation: bundles still carry
521
+ // it, but it never reaches a scaffold's root.
522
+ excluded.add(TEMPLATE_MANIFEST_MD);
506
523
  validateTemplateContentOnly(sourceRoot, excluded, subject);
507
524
  }
508
525
  // #5618 D4 — the additive-overlap check, at the same seam as the
@@ -526,6 +543,17 @@ export function installBundleTree(options) {
526
543
  // installMap destination) is read from the STAGED tree here, since
527
544
  // targetDir would not have it yet at this point.
528
545
  const installMapDestinationToSource = new Map(Object.entries(installMap).map(([source, destination]) => [destination, source]));
546
+ // #6460 — a patch target is read RAW from the staged tree here, while
547
+ // `copyTree` below writes the same path SUBSTITUTED, and
548
+ // `writeModulePatches` runs after `copyTree`. So a file a bundle both
549
+ // delivers as plain content AND patches would end up carrying unsubstituted
550
+ // markers. Unreachable today and asserted as such rather than guarded: the
551
+ // only patch targets any slice declares are `apps/web/package.json`,
552
+ // `next.config.mjs` and `jest.config.cjs`, all three delivered by the base
553
+ // `templates/` snapshot rather than by a bundle — so this reader falls
554
+ // through to the `targetAbs` branch and reads already-substituted bytes —
555
+ // and a template is refused `patches` outright above. A bundle that starts
556
+ // delivering one of its own patch targets must substitute here too.
529
557
  const readPatchTarget = (target) => {
530
558
  const mappedSource = installMapDestinationToSource.get(target);
531
559
  if (mappedSource !== undefined) {
@@ -539,6 +567,20 @@ export function installBundleTree(options) {
539
567
  const targetAbsPath = resolve(targetAbs, target);
540
568
  return existsSync(targetAbsPath) ? readFileSync(targetAbsPath, "utf8") : undefined;
541
569
  };
570
+ // #6653 — under skip-existing, a patch target the tree ALSO delivers would be
571
+ // computed from the bundle's copy and written over the site's file, which
572
+ // the policy exists to leave alone. No slice does this today; refuse it.
573
+ if (options.existingFilePolicy === "skip-existing") {
574
+ for (const patch of patches) {
575
+ const target = toPosix(patch.target);
576
+ const staged = resolve(sourceRoot, patch.target);
577
+ const delivered = [...installMapDestinationToSource.keys()].some((d) => toPosix(d) === target) ||
578
+ (!excluded.has(target) && existsSync(staged) && statSync(staged).isFile());
579
+ if (delivered) {
580
+ throw new Error(`${subject}: patch target ${target} is also delivered by the bundle — refused, because it would replace the site's own file`);
581
+ }
582
+ }
583
+ }
542
584
  const computedPatches = computeModulePatches(patches, {
543
585
  versions: options.versions ?? {},
544
586
  readTarget: readPatchTarget,
@@ -550,18 +592,38 @@ export function installBundleTree(options) {
550
592
  // every installMap source are excluded from the whole-tree copy — the
551
593
  // manifest never lands in the scaffold, and a mapped source lands ONLY
552
594
  // at its mapped destination, never also at its original path.
553
- copyTree(sourceRoot, targetAbs, excluded);
595
+ const report = [];
596
+ const policy = options.existingFilePolicy ?? "overwrite";
597
+ copyTree(sourceRoot, targetAbs, excluded, markers, policy, report);
554
598
  for (const [source, destination] of Object.entries(installMap)) {
555
599
  const destAbs = resolve(targetAbs, destination);
556
600
  mkdirSync(join(destAbs, ".."), { recursive: true });
557
- copyFileSync(resolve(sourceRoot, source), destAbs);
601
+ // BOTH ends decide text-ness (#6460), because an installMap entry is free
602
+ // to rename across the boundary and the two mismatches are not
603
+ // symmetric: a text source landing at a binary name merely misses a
604
+ // substitution, while a BINARY source landing at a text name would be
605
+ // read as utf8 and rewritten, turning every invalid byte sequence into
606
+ // U+FFFD — silent, unrecoverable corruption. Requiring both makes the
607
+ // safe direction structural rather than a property of today's manifests.
608
+ const row = writeOneFile(resolve(sourceRoot, source), destAbs, toPosix(destination), [source, destination], markers, policy);
609
+ if (row !== undefined)
610
+ report.push(row);
558
611
  }
559
612
  // patches write AFTER every file the tree brought is in place (D5) —
560
613
  // after installMap, before postInstall — so a patch can never race a
561
614
  // file its own ownedFiles/installMap just delivered, and (since it was
562
615
  // COMPUTED above, before either ran) overwrites rather than conflicts
563
616
  // with content copyTree/installMap just placed at the same path.
617
+ // computeModulePatches already dropped every already-applied target, so an
618
+ // idempotent replay produces no `patched` row. Diffed before the write, while
619
+ // `readPatchTarget` still returns the pre-patch bytes.
620
+ const patchedRows = computedPatches.map(({ target, content }) => ({
621
+ path: toPosix(target),
622
+ action: "patched",
623
+ change: diffTwoStrings(toPosix(target), readPatchTarget(target) ?? "", content, true),
624
+ }));
564
625
  writeModulePatches(computedPatches, targetAbs);
626
+ report.push(...patchedRows);
565
627
  // #5618 D4 — record this layer's destinations only now that every one of
566
628
  // them is in place (a later layer's overwrite is judged against the layer
567
629
  // that actually wrote the bytes). Patch targets are NOT recorded: a patch
@@ -577,9 +639,12 @@ export function installBundleTree(options) {
577
639
  // validateTemplateLayout); returned as-is (defaults unfilled) for the
578
640
  // caller to resolve alongside whatever the code registry's own
579
641
  // `TemplateManifest.layout` says.
580
- return isTemplate && manifest?.layout !== undefined
581
- ? { layout: manifest.layout }
582
- : {};
642
+ return {
643
+ report,
644
+ ...(isTemplate && manifest?.layout !== undefined
645
+ ? { layout: manifest.layout }
646
+ : {}),
647
+ };
583
648
  }
584
649
  /**
585
650
  * Recursively copy every file from `srcRoot` into `destRoot`, creating parent
@@ -589,8 +654,11 @@ export function installBundleTree(options) {
589
654
  * `safeExtractTarGz` has validated every entry, so no additional
590
655
  * path-safety check is needed here — `srcRoot` is our own staging tree, not
591
656
  * attacker-controlled input.
657
+ *
658
+ * `markers`, when given, is substituted into every TEXT file on the way
659
+ * through (#6460) — see `InstallBundleTreeOptions.markers`.
592
660
  */
593
- function copyTree(srcRoot, destRoot, excluded = new Set()) {
661
+ function copyTree(srcRoot, destRoot, excluded, markers, policy, report) {
594
662
  const stack = [srcRoot];
595
663
  while (stack.length > 0) {
596
664
  const dir = stack.pop();
@@ -608,12 +676,155 @@ function copyTree(srcRoot, destRoot, excluded = new Set()) {
608
676
  if (excluded.has(rel.split(sep).join("/")))
609
677
  continue;
610
678
  mkdirSync(join(destPath, ".."), { recursive: true });
611
- copyFileSync(srcPath, destPath);
679
+ const row = writeOneFile(srcPath, destPath, toPosix(rel), [rel], markers, policy);
680
+ if (row !== undefined)
681
+ report.push(row);
612
682
  }
613
683
  // symlinks: never present — safeExtractTarGz rejects them outright.
614
684
  }
615
685
  }
616
686
  }
687
+ /**
688
+ * Land one file at `destAbs`, substituting `markers` into it when EVERY path
689
+ * in `relsForType` is text by `copy.ts`'s own `isTextFile` (#6460) — the
690
+ * source's relative path alone for a plain-content copy, source AND
691
+ * destination for an installMap entry, which may rename across the
692
+ * text/binary boundary. Unanimity rather than either-or, so binary content is
693
+ * never round-tripped through a utf8 string by construction; see the
694
+ * installMap loop for why the two mismatch directions are not symmetric.
695
+ *
696
+ * With no `markers`, or with any non-text path, this is exactly the
697
+ * `copyFileSync` it replaced.
698
+ *
699
+ * `isTextFile` is an allowlist of extensions, NOT "anything that parses as
700
+ * text": `.css`, `.html`, `.svg`, `.sh`, `.toml` and `.sql` are outside it, so
701
+ * a marker authored in one of those survives verbatim. That is pre-existing
702
+ * `copyTemplate` behaviour, deliberately shared rather than forked here.
703
+ *
704
+ * The source's mode rides along, because `copyFileSync` preserves it and
705
+ * `writeFileSync` does not: no tree this installs carries an executable file
706
+ * today, but a module slice is free to ship one, and a substituted `.mjs`
707
+ * silently losing its `+x` surfaces as "the scaffold builds but the hook never
708
+ * runs". Masked to the permission bits — `statSync().mode` carries the
709
+ * `S_IFREG` type bits too, which `open(2)` ignores but which have no business
710
+ * being passed. Like `copyFileSync`, this only sets the mode when the file is
711
+ * CREATED: an overwrite (the layer-over-layer case) keeps the existing one.
712
+ *
713
+ * `policy` (#6653 D3) is checked here, once, so `copyTree` and the installMap
714
+ * loop cannot diverge. Under `"skip-existing"` an existing destination is
715
+ * compared against the bytes this call WOULD write (post-substitution): equal
716
+ * → no row, nothing written; different → a `skipped` row carrying the diff.
717
+ * `path` is the scaffold-relative name the returned row carries.
718
+ */
719
+ function writeOneFile(srcAbs, destAbs, path, relsForType, markers, policy) {
720
+ const isText = relsForType.every(isTextFile);
721
+ const substitute = markers !== undefined && isText;
722
+ if (policy === "skip-existing" && existsSync(destAbs)) {
723
+ const wanted = substitute
724
+ ? Buffer.from(substituteMarkers(readFileSync(srcAbs, "utf8"), markers))
725
+ : readFileSync(srcAbs);
726
+ const existing = readFileSync(destAbs);
727
+ if (existing.equals(wanted))
728
+ return undefined;
729
+ // latin1 keeps one char per byte, so a binary pair's lengths stay byte counts.
730
+ const encoding = isText ? "utf8" : "latin1";
731
+ return {
732
+ path,
733
+ action: "skipped",
734
+ wanted: diffTwoStrings(path, existing.toString(encoding), wanted.toString(encoding), isText),
735
+ };
736
+ }
737
+ if (!substitute) {
738
+ copyFileSync(srcAbs, destAbs);
739
+ }
740
+ else {
741
+ writeFileSync(destAbs, substituteMarkers(readFileSync(srcAbs, "utf8"), markers), {
742
+ mode: statSync(srcAbs).mode & 0o777,
743
+ });
744
+ }
745
+ return { path, action: "written" };
746
+ }
747
+ function toPosix(rel) {
748
+ return normalize(rel).split(sep).join("/");
749
+ }
750
+ /** Lines of context around each change in `diffTwoStrings`'s hunks. */
751
+ const DIFF_CONTEXT = 3;
752
+ /**
753
+ * A unified diff of `before` → `after` for the report (#6653 D3) — the one
754
+ * helper behind both a `skipped` row's `wanted` and a `patched` row's
755
+ * `change`. Line-based LCS; the inputs are config and content files, small
756
+ * enough that the quadratic table is not a concern. `isText` false yields the
757
+ * binary summary instead, reading each string's length as its byte count
758
+ * (the caller decodes binary content as latin1).
759
+ */
760
+ export function diffTwoStrings(path, before, after, isText) {
761
+ if (!isText) {
762
+ return `binary content differs (${before.length} bytes existing vs ${after.length} bytes bundle)`;
763
+ }
764
+ // A final newline terminates the last line; it is not an empty line after it.
765
+ const lines = (text) => (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n");
766
+ const a = before === "" ? [] : lines(before);
767
+ const b = after === "" ? [] : lines(after);
768
+ // lcs[i][j] = LCS length of a[i..] and b[j..].
769
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
770
+ for (let i = a.length - 1; i >= 0; i--) {
771
+ for (let j = b.length - 1; j >= 0; j--) {
772
+ lcs[i][j] =
773
+ a[i] === b[j]
774
+ ? lcs[i + 1][j + 1] + 1
775
+ : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
776
+ }
777
+ }
778
+ const ops = [];
779
+ let i = 0;
780
+ let j = 0;
781
+ while (i < a.length || j < b.length) {
782
+ if (i < a.length && j < b.length && a[i] === b[j]) {
783
+ ops.push({ kind: " ", line: a[i], ai: i + 1, bi: j + 1 });
784
+ i++;
785
+ j++;
786
+ }
787
+ else if (i < a.length && (j === b.length || lcs[i + 1][j] >= lcs[i][j + 1])) {
788
+ ops.push({ kind: "-", line: a[i], ai: i + 1, bi: j });
789
+ i++;
790
+ }
791
+ else {
792
+ ops.push({ kind: "+", line: b[j], ai: i, bi: j + 1 });
793
+ j++;
794
+ }
795
+ }
796
+ const out = [`--- a/${path}`, `+++ b/${path}`];
797
+ if (ops.every((op) => op.kind === " ")) {
798
+ out.push("(the files differ only in their final newline)");
799
+ return out.join("\n");
800
+ }
801
+ let k = 0;
802
+ while (k < ops.length) {
803
+ if (ops[k].kind === " ") {
804
+ k++;
805
+ continue;
806
+ }
807
+ // Grow a hunk from this change until DIFF_CONTEXT*2 unchanged lines separate it from the next.
808
+ const start = Math.max(0, k - DIFF_CONTEXT);
809
+ let end = k;
810
+ let unchanged = 0;
811
+ while (end < ops.length && unchanged <= DIFF_CONTEXT * 2) {
812
+ unchanged = ops[end].kind === " " ? unchanged + 1 : 0;
813
+ end++;
814
+ }
815
+ end = Math.min(ops.length, end - Math.max(0, unchanged - DIFF_CONTEXT));
816
+ const hunk = ops.slice(start, end);
817
+ const oldLines = hunk.filter((op) => op.kind !== "+").length;
818
+ const newLines = hunk.filter((op) => op.kind !== "-").length;
819
+ const oldStart = hunk.find((op) => op.kind !== "+")?.ai ?? hunk[0].ai;
820
+ const newStart = hunk.find((op) => op.kind !== "-")?.bi ?? hunk[0].bi;
821
+ out.push(`@@ -${oldStart},${oldLines} +${newStart},${newLines} @@`);
822
+ for (const op of hunk)
823
+ out.push(`${op.kind}${op.line}`);
824
+ k = end;
825
+ }
826
+ return out.join("\n");
827
+ }
617
828
  /**
618
829
  * `extractBundle`'s validating-filter tar extraction, ported verbatim (see
619
830
  * file doc for why this is a port and not an import). Extracts `tarGzPath`
@@ -0,0 +1,15 @@
1
+ export declare const LICENSE_KEY_FILE = "ailk.key";
2
+ export declare const LICENSE_ENV_FILE: string;
3
+ export declare const LICENSE_ENV_VAR = "AILK_LICENSE_KEY";
4
+ /**
5
+ * Throws unless `key` is 1–4096 characters of `[A-Za-z0-9._-]`. The message
6
+ * describes the rule, never the value.
7
+ */
8
+ export declare function assertLicenseKeyShape(key: string): void;
9
+ /**
10
+ * Persist `key` into the scaffold at `dir`: `ailk.key` (0600), the
11
+ * `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
12
+ * `ailk.key` when no existing line covers it. Idempotent — a second call with
13
+ * the same key changes no byte.
14
+ */
15
+ export declare function writeLicenseKeyFiles(dir: string, key: string): void;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Where a scaffold keeps its licence key (#6683 § 3 D9).
3
+ *
4
+ * Two copies, both owner-only and both gitignored:
5
+ *
6
+ * - `ailk.key` at the scaffold root — the key on its own, for a person or a
7
+ * tool that needs it again (`license set` re-pushes it to Vercel);
8
+ * - `apps/api/.env` — one `AILK_LICENSE_KEY=<key>` line, because `apps/api`
9
+ * is the scaffold's runtime reader and loads `.env` from its own directory.
10
+ *
11
+ * `assertLicenseKeyShape` is the ONE validator every entry point calls
12
+ * (`--license`, `license set <key>`, `license set -`): a compact JWS is
13
+ * base64url segments joined by dots, so anything else — whitespace, a newline
14
+ * that would inject a second `.env` line — is refused, and the refusal never
15
+ * repeats the value (it may be a real key in a shell history or a CI log).
16
+ *
17
+ * Pure module: no `import.meta.url`, so jest imports it directly.
18
+ */
19
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
20
+ import { dirname, join } from "node:path";
21
+ export const LICENSE_KEY_FILE = "ailk.key";
22
+ export const LICENSE_ENV_FILE = join("apps", "api", ".env");
23
+ export const LICENSE_ENV_VAR = "AILK_LICENSE_KEY";
24
+ const KEY_SHAPE = /^[A-Za-z0-9._-]{1,4096}$/;
25
+ /** `.gitignore` lines that already ignore a root `ailk.key`. */
26
+ const KEY_IGNORE_LINES = new Set([
27
+ "ailk.key",
28
+ "/ailk.key",
29
+ "**/ailk.key",
30
+ "*.key",
31
+ "/*.key",
32
+ "**/*.key",
33
+ ]);
34
+ /** A line assigning the variable, with or without `export`. */
35
+ const ENV_LINE = new RegExp(`^\\s*(?:export\\s+)?${LICENSE_ENV_VAR}\\s*=`);
36
+ /**
37
+ * Throws unless `key` is 1–4096 characters of `[A-Za-z0-9._-]`. The message
38
+ * describes the rule, never the value.
39
+ */
40
+ export function assertLicenseKeyShape(key) {
41
+ if (!KEY_SHAPE.test(key)) {
42
+ throw new Error("the licence key is malformed: expected 1-4096 characters of A-Z a-z 0-9 . _ - (a compact JWS, no whitespace or newline). Copy it again from your welcome page.");
43
+ }
44
+ }
45
+ /**
46
+ * Write `content` only when it differs, with mode 0600 either way. An existing
47
+ * file is tightened BEFORE the write — `mode` applies only on creation, so a
48
+ * looser pre-existing `.env` would otherwise hold the key for a moment at its
49
+ * old permissions.
50
+ */
51
+ function writeOwnerOnly(path, content) {
52
+ mkdirSync(dirname(path), { recursive: true });
53
+ if (existsSync(path))
54
+ chmodSync(path, 0o600);
55
+ if (!existsSync(path) || readFileSync(path, "utf8") !== content) {
56
+ writeFileSync(path, content, { mode: 0o600 });
57
+ }
58
+ chmodSync(path, 0o600);
59
+ }
60
+ /** `text` plus `line`, starting on a fresh line. */
61
+ function appendLine(text, line) {
62
+ const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n";
63
+ return `${text}${sep}${line}\n`;
64
+ }
65
+ /** Replace the first assignment line, drop any later duplicate, else append. */
66
+ function upsertEnvLine(existing, key) {
67
+ const line = `${LICENSE_ENV_VAR}=${key}`;
68
+ const lines = existing.split("\n");
69
+ let seen = false;
70
+ const out = [];
71
+ for (const l of lines) {
72
+ if (ENV_LINE.test(l)) {
73
+ if (!seen)
74
+ out.push(line);
75
+ seen = true;
76
+ }
77
+ else {
78
+ out.push(l);
79
+ }
80
+ }
81
+ return seen ? out.join("\n") : appendLine(existing, line);
82
+ }
83
+ /**
84
+ * Persist `key` into the scaffold at `dir`: `ailk.key` (0600), the
85
+ * `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
86
+ * `ailk.key` when no existing line covers it. Idempotent — a second call with
87
+ * the same key changes no byte.
88
+ */
89
+ export function writeLicenseKeyFiles(dir, key) {
90
+ assertLicenseKeyShape(key);
91
+ writeOwnerOnly(join(dir, LICENSE_KEY_FILE), `${key}\n`);
92
+ const envPath = join(dir, LICENSE_ENV_FILE);
93
+ const env = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
94
+ writeOwnerOnly(envPath, upsertEnvLine(env, key));
95
+ const ignorePath = join(dir, ".gitignore");
96
+ const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
97
+ const covered = ignore
98
+ .split("\n")
99
+ .some((l) => KEY_IGNORE_LINES.has(l.trim()));
100
+ if (!covered)
101
+ writeFileSync(ignorePath, appendLine(ignore, LICENSE_KEY_FILE));
102
+ }
@@ -18,7 +18,12 @@
18
18
  * gate (K1's regression floor).
19
19
  */
20
20
  import type { ModuleName } from "../parse-args.js";
21
- export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pro";
21
+ /**
22
+ * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
23
+ * `/pro` route and there will not be one (#6439, operator decision OD-2) —
24
+ * pricing is where every level and the commerce add-on are actually listed.
25
+ */
26
+ export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
22
27
  /**
23
28
  * Thrown for both honest-denial states (no key, and an invalid/lapsed key).
24
29
  * The CLI does not distinguish them in its exit code — both are "this run
@@ -1,5 +1,10 @@
1
1
  import { FetchModuleError, fetchModule } from "./fetch-module.js";
2
- export const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pro";
2
+ /**
3
+ * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
4
+ * `/pro` route and there will not be one (#6439, operator decision OD-2) —
5
+ * pricing is where every level and the commerce add-on are actually listed.
6
+ */
7
+ export const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
3
8
  /**
4
9
  * Thrown for both honest-denial states (no key, and an invalid/lapsed key).
5
10
  * The CLI does not distinguish them in its exit code — both are "this run
@@ -1,12 +1,18 @@
1
+ export declare function proCallout(): string;
2
+ /** The closing prerequisites note on a free install (#6440 AC4). */
3
+ export declare const FREE_PREREQUISITES_NOTE = "No database, no licence key and no vendor API key are needed to run or build this site.";
1
4
  /**
2
- * `proCallout()` — the AI-guided setup conversion message appended to
3
- * `pnpm create ailk-app`'s closing summary.
4
- *
5
- * Wording is locked in `docs/internal/specs/ailk-onboard-site-spec.md` §2.1 and
6
- * `docs/internal/specs/ailk-onboard-site-spec.md` §5.1 (Sam-verifiable AC).
7
- *
8
- * The message is always shown — no flags, no env gating. Free-tier users
9
- * see it as a discovery surface for Pro; Pro users see it as a reminder
10
- * of the `/onboard-site` skill they can install.
5
+ * The same note on a licensed install (#6480): it drops the licence-key claim,
6
+ * which would contradict the key the customer just installed with.
11
7
  */
12
- export declare function proCallout(): string;
8
+ export declare const LICENSED_PREREQUISITES_NOTE = "No database and no vendor API key are needed to run or build this site.";
9
+ /**
10
+ * Whether an install delivered paid content — any paid module in the effective
11
+ * module set, `union(template.requires, --modules)`, the same union
12
+ * `scaffold()` gates on. A paid template is covered by that union too:
13
+ * `templateDelivery` derives "paid" from `requires` alone.
14
+ */
15
+ export declare function isLicensedInstall(opts: {
16
+ template?: string;
17
+ modules?: string[];
18
+ }): boolean;
@@ -5,16 +5,39 @@
5
5
  * Wording is locked in `docs/internal/specs/ailk-onboard-site-spec.md` §2.1 and
6
6
  * `docs/internal/specs/ailk-onboard-site-spec.md` §5.1 (Sam-verifiable AC).
7
7
  *
8
- * The message is always shown — no flags, no env gating. Free-tier users
9
- * see it as a discovery surface for Pro; Pro users see it as a reminder
10
- * of the `/onboard-site` skill they can install.
8
+ * Shown on a free install only (#6480): it is a discovery surface for Pro,
9
+ * and a licensed install already owns the tier it pitches.
11
10
  */
11
+ import { normalizeModules } from "../module-architecture.js";
12
+ import { resolveTemplate } from "../template-manifest.js";
13
+ import { classifyModules } from "./module-tier.js";
12
14
  export function proCallout() {
13
15
  return [
14
16
  "Want AI-guided setup? Pro's `/onboard-site` interviews you about",
15
17
  "your business, generates a vertical-aware page plan, configures",
16
18
  "Stripe / auth / lead routing / analytics, and sets up your",
17
19
  "GitHub→Vercel deploy — all in one orchestrated flow. Or invoke the",
18
- "free scaffolders one at a time → ailaunchkit.ai/pro",
20
+ "free scaffolders one at a time → ailaunchkit.ai/pricing",
19
21
  ].join("\n");
20
22
  }
23
+ /** The closing prerequisites note on a free install (#6440 AC4). */
24
+ export const FREE_PREREQUISITES_NOTE = "No database, no licence key and no vendor API key are needed to run or build this site.";
25
+ /**
26
+ * The same note on a licensed install (#6480): it drops the licence-key claim,
27
+ * which would contradict the key the customer just installed with.
28
+ */
29
+ export const LICENSED_PREREQUISITES_NOTE = "No database and no vendor API key are needed to run or build this site.";
30
+ /**
31
+ * Whether an install delivered paid content — any paid module in the effective
32
+ * module set, `union(template.requires, --modules)`, the same union
33
+ * `scaffold()` gates on. A paid template is covered by that union too:
34
+ * `templateDelivery` derives "paid" from `requires` alone.
35
+ */
36
+ export function isLicensedInstall(opts) {
37
+ const requires = opts.template !== undefined ? resolveTemplate(opts.template).requires : [];
38
+ const modules = normalizeModules([
39
+ ...requires,
40
+ ...(opts.modules ?? ["marketing"]),
41
+ ]);
42
+ return classifyModules(modules).paid.length > 0;
43
+ }