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
@@ -100,6 +100,8 @@ const AILK_MODULE_JSON = "ailk-module.json";
100
100
  * Never copied into the scaffold either.
101
101
  */
102
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";
103
105
  /** The op names valid for each patch `kind` (spec 5418 § 3 D1's ModulePatchOp union, partitioned). */
104
106
  const VALID_OPS_BY_KIND = {
105
107
  json: new Set(["merge-keys", "append-unique"]),
@@ -449,6 +451,12 @@ export async function extractModuleBundle(options) {
449
451
  ...(options.ownership !== undefined ? { ownership: options.ownership } : {}),
450
452
  ...(options.versions !== undefined ? { versions: options.versions } : {}),
451
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
+ : {}),
452
460
  });
453
461
  }
454
462
  finally {
@@ -494,6 +502,7 @@ export function installBundleTree(options) {
494
502
  throw new Error(`${subject}: a template must not carry patches — content, never code (spec 5462 D9)`);
495
503
  }
496
504
  validatePatches(patches, targetAbs, subject);
505
+ options.checkPatches?.(patches);
497
506
  // The exclusion set copyTree/the content-only guard both use — built from
498
507
  // NORMALIZED installMap keys (#5514) so a "./x" key excludes the same
499
508
  // string `rel`'s traversal below computes, rather than colliding with it
@@ -506,6 +515,11 @@ export function installBundleTree(options) {
506
515
  ...Object.keys(installMap).map((key) => normalize(key).split(sep).join("/")),
507
516
  ]);
508
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);
509
523
  validateTemplateContentOnly(sourceRoot, excluded, subject);
510
524
  }
511
525
  // #5618 D4 — the additive-overlap check, at the same seam as the
@@ -553,6 +567,20 @@ export function installBundleTree(options) {
553
567
  const targetAbsPath = resolve(targetAbs, target);
554
568
  return existsSync(targetAbsPath) ? readFileSync(targetAbsPath, "utf8") : undefined;
555
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
+ }
556
584
  const computedPatches = computeModulePatches(patches, {
557
585
  versions: options.versions ?? {},
558
586
  readTarget: readPatchTarget,
@@ -564,7 +592,9 @@ export function installBundleTree(options) {
564
592
  // every installMap source are excluded from the whole-tree copy — the
565
593
  // manifest never lands in the scaffold, and a mapped source lands ONLY
566
594
  // at its mapped destination, never also at its original path.
567
- copyTree(sourceRoot, targetAbs, excluded, markers);
595
+ const report = [];
596
+ const policy = options.existingFilePolicy ?? "overwrite";
597
+ copyTree(sourceRoot, targetAbs, excluded, markers, policy, report);
568
598
  for (const [source, destination] of Object.entries(installMap)) {
569
599
  const destAbs = resolve(targetAbs, destination);
570
600
  mkdirSync(join(destAbs, ".."), { recursive: true });
@@ -575,14 +605,25 @@ export function installBundleTree(options) {
575
605
  // read as utf8 and rewritten, turning every invalid byte sequence into
576
606
  // U+FFFD — silent, unrecoverable corruption. Requiring both makes the
577
607
  // safe direction structural rather than a property of today's manifests.
578
- writeOneFile(resolve(sourceRoot, source), destAbs, [source, destination], markers);
608
+ const row = writeOneFile(resolve(sourceRoot, source), destAbs, toPosix(destination), [source, destination], markers, policy);
609
+ if (row !== undefined)
610
+ report.push(row);
579
611
  }
580
612
  // patches write AFTER every file the tree brought is in place (D5) —
581
613
  // after installMap, before postInstall — so a patch can never race a
582
614
  // file its own ownedFiles/installMap just delivered, and (since it was
583
615
  // COMPUTED above, before either ran) overwrites rather than conflicts
584
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
+ }));
585
625
  writeModulePatches(computedPatches, targetAbs);
626
+ report.push(...patchedRows);
586
627
  // #5618 D4 — record this layer's destinations only now that every one of
587
628
  // them is in place (a later layer's overwrite is judged against the layer
588
629
  // that actually wrote the bytes). Patch targets are NOT recorded: a patch
@@ -598,9 +639,12 @@ export function installBundleTree(options) {
598
639
  // validateTemplateLayout); returned as-is (defaults unfilled) for the
599
640
  // caller to resolve alongside whatever the code registry's own
600
641
  // `TemplateManifest.layout` says.
601
- return isTemplate && manifest?.layout !== undefined
602
- ? { layout: manifest.layout }
603
- : {};
642
+ return {
643
+ report,
644
+ ...(isTemplate && manifest?.layout !== undefined
645
+ ? { layout: manifest.layout }
646
+ : {}),
647
+ };
604
648
  }
605
649
  /**
606
650
  * Recursively copy every file from `srcRoot` into `destRoot`, creating parent
@@ -614,7 +658,7 @@ export function installBundleTree(options) {
614
658
  * `markers`, when given, is substituted into every TEXT file on the way
615
659
  * through (#6460) — see `InstallBundleTreeOptions.markers`.
616
660
  */
617
- function copyTree(srcRoot, destRoot, excluded = new Set(), markers) {
661
+ function copyTree(srcRoot, destRoot, excluded, markers, policy, report) {
618
662
  const stack = [srcRoot];
619
663
  while (stack.length > 0) {
620
664
  const dir = stack.pop();
@@ -632,7 +676,9 @@ function copyTree(srcRoot, destRoot, excluded = new Set(), markers) {
632
676
  if (excluded.has(rel.split(sep).join("/")))
633
677
  continue;
634
678
  mkdirSync(join(destPath, ".."), { recursive: true });
635
- writeOneFile(srcPath, destPath, [rel], markers);
679
+ const row = writeOneFile(srcPath, destPath, toPosix(rel), [rel], markers, policy);
680
+ if (row !== undefined)
681
+ report.push(row);
636
682
  }
637
683
  // symlinks: never present — safeExtractTarGz rejects them outright.
638
684
  }
@@ -663,15 +709,121 @@ function copyTree(srcRoot, destRoot, excluded = new Set(), markers) {
663
709
  * `S_IFREG` type bits too, which `open(2)` ignores but which have no business
664
710
  * being passed. Like `copyFileSync`, this only sets the mode when the file is
665
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.
666
718
  */
667
- function writeOneFile(srcAbs, destAbs, relsForType, markers) {
668
- if (markers === undefined || !relsForType.every(isTextFile)) {
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) {
669
738
  copyFileSync(srcAbs, destAbs);
670
- return;
671
739
  }
672
- writeFileSync(destAbs, substituteMarkers(readFileSync(srcAbs, "utf8"), markers), {
673
- mode: statSync(srcAbs).mode & 0o777,
674
- });
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");
675
827
  }
676
828
  /**
677
829
  * `extractBundle`'s validating-filter tar extraction, ported verbatim (see
@@ -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
+ }
@@ -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,10 +5,12 @@
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",
@@ -18,3 +20,24 @@ export function proCallout() {
18
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
+ }
@@ -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;