create-ailk 0.6.1 → 0.8.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 (324) hide show
  1. package/README.md +87 -14
  2. package/component-catalog.json +735 -95
  3. package/component-catalog.md +13 -7
  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 +89 -29
  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 +170 -14
  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/module-license-gate.d.ts +79 -0
  18. package/dist/lib/module-license-gate.js +160 -12
  19. package/dist/lib/module-tier.d.ts +45 -16
  20. package/dist/lib/module-tier.js +76 -0
  21. package/dist/lib/pro-callout.d.ts +16 -10
  22. package/dist/lib/pro-callout.js +34 -3
  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.1.tar.gz +0 -0
  28. package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
  29. package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
  30. package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
  31. package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
  32. package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
  33. package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
  34. package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
  35. package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
  36. package/dist/modules/workspace/0.6.1.tar.gz +0 -0
  37. package/dist/parse-args.d.ts +85 -10
  38. package/dist/parse-args.js +227 -15
  39. package/dist/programmatic.d.ts +81 -5
  40. package/dist/programmatic.js +291 -32
  41. package/dist/site-chrome.d.ts +18 -7
  42. package/dist/site-chrome.js +32 -14
  43. package/dist/site-roster.d.ts +127 -0
  44. package/dist/site-roster.js +499 -0
  45. package/dist/starter-grid.d.ts +216 -0
  46. package/dist/starter-grid.js +449 -0
  47. package/dist/surfaces.d.ts +20 -7
  48. package/dist/surfaces.js +204 -24
  49. package/dist/sync-routes.d.ts +9 -0
  50. package/dist/sync-routes.js +2 -2
  51. package/dist/template-manifest.d.ts +94 -19
  52. package/dist/template-manifest.js +170 -289
  53. package/dist/template-names.d.ts +14 -12
  54. package/dist/template-names.js +13 -17
  55. package/package.json +6 -4
  56. package/site-templates/_base/content/en/BookCall/book-call.mdx +52 -0
  57. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  58. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  59. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  60. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  61. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +65 -0
  62. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +67 -0
  63. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  64. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  65. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  66. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  67. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  68. package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
  69. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +17 -3
  70. package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
  71. package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
  72. package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
  73. package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
  74. package/site-templates/designs/minimalism/DESIGN.md +162 -0
  75. package/site-templates/designs/standard/DESIGN.md +167 -0
  76. package/site-templates/marketing-site/DESIGN.md +59 -40
  77. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +376 -57
  78. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +230 -90
  79. package/site-templates/marketing-site/ailk-template.json +1 -0
  80. package/site-templates/marketing-site-free/DESIGN.md +59 -40
  81. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +323 -63
  82. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +323 -113
  83. package/site-templates/marketing-site-free/ailk-template.json +3 -2
  84. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  85. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  86. package/site-templates/minimalism-site/DESIGN.md +54 -40
  87. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
  88. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +173 -51
  89. package/site-templates/minimalism-site/ailk-template.json +3 -2
  90. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +52 -0
  91. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
  92. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  93. package/site-templates/saas-product/DESIGN.md +59 -40
  94. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  95. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +369 -0
  96. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
  97. package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
  98. package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
  99. package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
  100. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  101. package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
  102. package/site-templates/shapes/local-business-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  103. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
  104. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
  105. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
  106. package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
  107. package/site-templates/shapes/local-business-service/provenance.json +4 -0
  108. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
  109. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
  110. package/site-templates/shapes/organization-service/ailk-template.json +10 -0
  111. package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
  112. package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
  113. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  114. package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
  115. package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  116. package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
  117. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
  118. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
  119. package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
  120. package/site-templates/shapes/organization-service/provenance.json +4 -0
  121. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
  122. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
  123. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  124. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  125. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  126. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  127. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  128. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
  129. package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  130. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  131. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  132. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  133. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  134. package/site-templates/shapes/person-service/provenance.json +4 -0
  135. package/templates/.claude/settings.json +15 -0
  136. package/templates/.claude/skills/README.md +10 -9
  137. package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
  138. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
  139. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
  140. package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
  141. package/templates/.claude/skills/add-page/references/page-types.md +115 -0
  142. package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
  143. package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
  144. package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
  145. package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
  146. package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
  147. package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
  148. package/templates/.claude/skills/fill-page/SKILL.md +180 -0
  149. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
  150. package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
  151. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  152. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  153. package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +73 -41
  154. package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +21 -30
  155. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +204 -0
  156. package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
  157. package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
  158. package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
  159. package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
  160. package/templates/.github/workflows/ci.yml +5 -0
  161. package/templates/CLAUDE.md +8 -2
  162. package/templates/DESIGN.md +56 -44
  163. package/templates/README.md +116 -46
  164. package/templates/apps/api/.env.example +2 -1
  165. package/templates/apps/api/CLAUDE.md +3 -2
  166. package/templates/apps/api/package.json +1 -1
  167. package/templates/apps/api/src/config/__tests__/index.test.ts +146 -10
  168. package/templates/apps/api/src/config/index.ts +46 -14
  169. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  170. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  171. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  172. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  173. package/templates/apps/api/src/lib/license-key-store.ts +45 -0
  174. package/templates/apps/api/src/lib/site.ts +5 -3
  175. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
  176. package/templates/apps/api/src/middleware/auth.ts +9 -0
  177. package/templates/apps/api/src/openapi/spec.ts +63 -12
  178. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
  179. package/templates/apps/api/src/routes/content/get-page.ts +86 -2
  180. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  181. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  182. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  183. package/templates/apps/api/src/server.ts +13 -0
  184. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  185. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  186. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  187. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  188. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
  189. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
  190. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  191. package/templates/apps/mcp/package.json +2 -1
  192. package/templates/apps/mcp/src/index.ts +7 -0
  193. package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
  194. package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
  195. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  196. package/templates/apps/web/__tests__/setup.ts +18 -0
  197. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
  198. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
  199. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
  200. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
  201. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
  202. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
  203. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
  204. package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
  205. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
  206. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  207. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  208. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
  209. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +67 -29
  210. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +94 -11
  211. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  212. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  213. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +228 -86
  214. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  215. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  216. package/templates/apps/web/app/[locale]/contact/page.tsx +115 -7
  217. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  218. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  219. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
  220. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
  221. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  222. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  223. package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
  224. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  225. package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
  226. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  227. package/templates/apps/web/app/[locale]/layout.tsx +146 -52
  228. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  229. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  230. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  231. package/templates/apps/web/app/[locale]/newsletter/page.tsx +142 -0
  232. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  233. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  234. package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
  235. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  236. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
  237. package/templates/apps/web/app/[locale]/{cancellation → purchase-policy}/page.tsx +15 -10
  238. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
  239. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  240. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
  241. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  242. package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
  243. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  244. package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
  245. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
  246. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +63 -0
  247. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
  248. package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
  249. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  250. package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
  251. package/templates/apps/web/app/error-fallback.tsx +17 -45
  252. package/templates/apps/web/app/not-found.tsx +14 -38
  253. package/templates/apps/web/app/og/route.tsx +6 -0
  254. package/templates/apps/web/eslint.config.mjs +3 -0
  255. package/templates/apps/web/instrumentation.ts +14 -0
  256. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
  257. package/templates/apps/web/lib/license-key-store.ts +52 -0
  258. package/templates/apps/web/lib/nav-config.ts +82 -18
  259. package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
  260. package/templates/apps/web/lib/site-brand.tsx +1 -1
  261. package/templates/apps/web/lib/site-chrome.ts +3 -3
  262. package/templates/apps/web/lib/site-identity.ts +148 -0
  263. package/templates/apps/web/lib/site-theme.ts +11 -0
  264. package/templates/apps/web/messages/en.json +78 -37
  265. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  266. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  267. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  268. package/templates/apps/web/public/favicon-16x16.png +0 -0
  269. package/templates/apps/web/public/favicon-32x32.png +0 -0
  270. package/templates/apps/web/public/favicon.ico +0 -0
  271. package/templates/apps/web/public/favicon.svg +3 -3
  272. package/templates/apps/web/public/logomark.svg +6 -3
  273. package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
  274. package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
  275. package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
  276. package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
  277. package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
  278. package/templates/apps/web/public/logos/salessmyth.svg +4 -0
  279. package/templates/apps/web/public/site.webmanifest +4 -4
  280. package/templates/apps/web/public/wordmark-light.svg +4 -0
  281. package/templates/apps/web/public/wordmark.svg +3 -3
  282. package/templates/apps/web/security-headers.config.ts +5 -1
  283. package/templates/content/_site.mdx +25 -0
  284. package/templates/content/en/AboutPage/about.mdx +150 -144
  285. package/templates/content/en/BlogList/blog.mdx +126 -0
  286. package/templates/content/en/ContactPage/contact.mdx +21 -92
  287. package/templates/content/en/FAQPage/faq.mdx +86 -57
  288. package/templates/content/en/HomePage/home.mdx +268 -217
  289. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +181 -0
  290. package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
  291. package/templates/database/CHANGELOG.md +14 -0
  292. package/templates/database/package.json +1 -1
  293. package/templates/package.json +3 -1
  294. package/templates/tools/lint-architecture.ts +315 -0
  295. package/templates/tools/tsconfig.json +14 -0
  296. package/versions.json +33 -33
  297. package/site-templates/simple-service/DESIGN.md +0 -148
  298. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +0 -240
  299. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -278
  300. package/site-templates/simple-service/ailk-template.json +0 -10
  301. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -67
  302. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  303. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  304. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  305. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  306. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  307. package/site-templates/simple-service/provenance.json +0 -4
  308. package/templates/.claude/skills/add-schema/SKILL.md +0 -277
  309. package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
  310. package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
  311. package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
  312. package/templates/.claude/skills/provision-config/references/runbook-template.md +0 -248
  313. package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
  314. package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
  315. package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
  316. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -150
  317. package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
  318. package/templates/apps/web/app/[locale]/shipping/page.tsx +0 -44
  319. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
  320. package/templates/content/en/LandingPage/blog.mdx +0 -83
  321. package/templates/content/en/LandingPage/features.mdx +0 -103
  322. package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
  323. package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
  324. package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
@@ -1,3 +1,4 @@
1
+ import { type ModulePatch } from "../module-architecture.js";
1
2
  import { type TemplateLayout } from "../site-chrome.js";
2
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. */
@@ -44,6 +45,10 @@ export interface ExtractModuleBundleOptions {
44
45
  ownership?: LayerOwnership;
45
46
  /** The scaffold's marker map (#6460). See `InstallBundleTreeOptions`. */
46
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;
47
52
  }
48
53
  /**
49
54
  * Extract a fetched module bundle into `targetDir`, merging its files onto
@@ -108,8 +113,47 @@ export interface InstallBundleTreeOptions {
108
113
  * files; `substituteMarkers` leaves every other `{{KEY}}` alone.
109
114
  */
110
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;
111
134
  }
112
- /** `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. */
113
157
  export interface InstallBundleTreeResult {
114
158
  /**
115
159
  * The installed template's validated `layout` block (#5612 D1), unresolved
@@ -117,6 +161,8 @@ export interface InstallBundleTreeResult {
117
161
  * module install, or a template whose manifest declares no `layout`.
118
162
  */
119
163
  layout?: TemplateLayout;
164
+ /** One row per file written, patched or skipped, in install order (#6653 D3). */
165
+ report: InstallReportRow[];
120
166
  }
121
167
  /**
122
168
  * Apply one resolved manifest's `installMap`, plain content and `patches`
@@ -129,3 +175,12 @@ export interface InstallBundleTreeResult {
129
175
  * rejected tree leaves `targetDir` exactly as it was.
130
176
  */
131
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;
@@ -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
@@ -611,10 +655,15 @@ export function installBundleTree(options) {
611
655
  * path-safety check is needed here — `srcRoot` is our own staging tree, not
612
656
  * attacker-controlled input.
613
657
  *
658
+ * A directory is created only as the parent of a file actually copied, never
659
+ * for its own sake: a source directory whose every file is excluded (mapped
660
+ * away) leaves nothing behind at its original path (#6787 — an empty root
661
+ * `content/en/blog/` reads as an unregistered page type).
662
+ *
614
663
  * `markers`, when given, is substituted into every TEXT file on the way
615
664
  * through (#6460) — see `InstallBundleTreeOptions.markers`.
616
665
  */
617
- function copyTree(srcRoot, destRoot, excluded = new Set(), markers) {
666
+ function copyTree(srcRoot, destRoot, excluded, markers, policy, report) {
618
667
  const stack = [srcRoot];
619
668
  while (stack.length > 0) {
620
669
  const dir = stack.pop();
@@ -625,14 +674,15 @@ function copyTree(srcRoot, destRoot, excluded = new Set(), markers) {
625
674
  const rel = relative(srcRoot, srcPath);
626
675
  const destPath = join(destRoot, rel);
627
676
  if (entry.isDirectory()) {
628
- mkdirSync(destPath, { recursive: true });
629
677
  stack.push(srcPath);
630
678
  }
631
679
  else if (entry.isFile()) {
632
680
  if (excluded.has(rel.split(sep).join("/")))
633
681
  continue;
634
682
  mkdirSync(join(destPath, ".."), { recursive: true });
635
- writeOneFile(srcPath, destPath, [rel], markers);
683
+ const row = writeOneFile(srcPath, destPath, toPosix(rel), [rel], markers, policy);
684
+ if (row !== undefined)
685
+ report.push(row);
636
686
  }
637
687
  // symlinks: never present — safeExtractTarGz rejects them outright.
638
688
  }
@@ -663,15 +713,121 @@ function copyTree(srcRoot, destRoot, excluded = new Set(), markers) {
663
713
  * `S_IFREG` type bits too, which `open(2)` ignores but which have no business
664
714
  * being passed. Like `copyFileSync`, this only sets the mode when the file is
665
715
  * CREATED: an overwrite (the layer-over-layer case) keeps the existing one.
716
+ *
717
+ * `policy` (#6653 D3) is checked here, once, so `copyTree` and the installMap
718
+ * loop cannot diverge. Under `"skip-existing"` an existing destination is
719
+ * compared against the bytes this call WOULD write (post-substitution): equal
720
+ * → no row, nothing written; different → a `skipped` row carrying the diff.
721
+ * `path` is the scaffold-relative name the returned row carries.
666
722
  */
667
- function writeOneFile(srcAbs, destAbs, relsForType, markers) {
668
- if (markers === undefined || !relsForType.every(isTextFile)) {
723
+ function writeOneFile(srcAbs, destAbs, path, relsForType, markers, policy) {
724
+ const isText = relsForType.every(isTextFile);
725
+ const substitute = markers !== undefined && isText;
726
+ if (policy === "skip-existing" && existsSync(destAbs)) {
727
+ const wanted = substitute
728
+ ? Buffer.from(substituteMarkers(readFileSync(srcAbs, "utf8"), markers))
729
+ : readFileSync(srcAbs);
730
+ const existing = readFileSync(destAbs);
731
+ if (existing.equals(wanted))
732
+ return undefined;
733
+ // latin1 keeps one char per byte, so a binary pair's lengths stay byte counts.
734
+ const encoding = isText ? "utf8" : "latin1";
735
+ return {
736
+ path,
737
+ action: "skipped",
738
+ wanted: diffTwoStrings(path, existing.toString(encoding), wanted.toString(encoding), isText),
739
+ };
740
+ }
741
+ if (!substitute) {
669
742
  copyFileSync(srcAbs, destAbs);
670
- return;
671
743
  }
672
- writeFileSync(destAbs, substituteMarkers(readFileSync(srcAbs, "utf8"), markers), {
673
- mode: statSync(srcAbs).mode & 0o777,
674
- });
744
+ else {
745
+ writeFileSync(destAbs, substituteMarkers(readFileSync(srcAbs, "utf8"), markers), {
746
+ mode: statSync(srcAbs).mode & 0o777,
747
+ });
748
+ }
749
+ return { path, action: "written" };
750
+ }
751
+ function toPosix(rel) {
752
+ return normalize(rel).split(sep).join("/");
753
+ }
754
+ /** Lines of context around each change in `diffTwoStrings`'s hunks. */
755
+ const DIFF_CONTEXT = 3;
756
+ /**
757
+ * A unified diff of `before` → `after` for the report (#6653 D3) — the one
758
+ * helper behind both a `skipped` row's `wanted` and a `patched` row's
759
+ * `change`. Line-based LCS; the inputs are config and content files, small
760
+ * enough that the quadratic table is not a concern. `isText` false yields the
761
+ * binary summary instead, reading each string's length as its byte count
762
+ * (the caller decodes binary content as latin1).
763
+ */
764
+ export function diffTwoStrings(path, before, after, isText) {
765
+ if (!isText) {
766
+ return `binary content differs (${before.length} bytes existing vs ${after.length} bytes bundle)`;
767
+ }
768
+ // A final newline terminates the last line; it is not an empty line after it.
769
+ const lines = (text) => (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n");
770
+ const a = before === "" ? [] : lines(before);
771
+ const b = after === "" ? [] : lines(after);
772
+ // lcs[i][j] = LCS length of a[i..] and b[j..].
773
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
774
+ for (let i = a.length - 1; i >= 0; i--) {
775
+ for (let j = b.length - 1; j >= 0; j--) {
776
+ lcs[i][j] =
777
+ a[i] === b[j]
778
+ ? lcs[i + 1][j + 1] + 1
779
+ : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
780
+ }
781
+ }
782
+ const ops = [];
783
+ let i = 0;
784
+ let j = 0;
785
+ while (i < a.length || j < b.length) {
786
+ if (i < a.length && j < b.length && a[i] === b[j]) {
787
+ ops.push({ kind: " ", line: a[i], ai: i + 1, bi: j + 1 });
788
+ i++;
789
+ j++;
790
+ }
791
+ else if (i < a.length && (j === b.length || lcs[i + 1][j] >= lcs[i][j + 1])) {
792
+ ops.push({ kind: "-", line: a[i], ai: i + 1, bi: j });
793
+ i++;
794
+ }
795
+ else {
796
+ ops.push({ kind: "+", line: b[j], ai: i, bi: j + 1 });
797
+ j++;
798
+ }
799
+ }
800
+ const out = [`--- a/${path}`, `+++ b/${path}`];
801
+ if (ops.every((op) => op.kind === " ")) {
802
+ out.push("(the files differ only in their final newline)");
803
+ return out.join("\n");
804
+ }
805
+ let k = 0;
806
+ while (k < ops.length) {
807
+ if (ops[k].kind === " ") {
808
+ k++;
809
+ continue;
810
+ }
811
+ // Grow a hunk from this change until DIFF_CONTEXT*2 unchanged lines separate it from the next.
812
+ const start = Math.max(0, k - DIFF_CONTEXT);
813
+ let end = k;
814
+ let unchanged = 0;
815
+ while (end < ops.length && unchanged <= DIFF_CONTEXT * 2) {
816
+ unchanged = ops[end].kind === " " ? unchanged + 1 : 0;
817
+ end++;
818
+ }
819
+ end = Math.min(ops.length, end - Math.max(0, unchanged - DIFF_CONTEXT));
820
+ const hunk = ops.slice(start, end);
821
+ const oldLines = hunk.filter((op) => op.kind !== "+").length;
822
+ const newLines = hunk.filter((op) => op.kind !== "-").length;
823
+ const oldStart = hunk.find((op) => op.kind !== "+")?.ai ?? hunk[0].ai;
824
+ const newStart = hunk.find((op) => op.kind !== "-")?.bi ?? hunk[0].bi;
825
+ out.push(`@@ -${oldStart},${oldLines} +${newStart},${newLines} @@`);
826
+ for (const op of hunk)
827
+ out.push(`${op.kind}${op.line}`);
828
+ k = end;
829
+ }
830
+ return out.join("\n");
675
831
  }
676
832
  /**
677
833
  * `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
+ }
@@ -16,8 +16,14 @@
16
16
  * `resolvePaidModules` returns immediately, before touching `process.env` or
17
17
  * the network, so a bare/all-Free scaffold is provably unaffected by this
18
18
  * gate (K1's regression floor).
19
+ *
20
+ * Also carries the design gate (#6760): `assertDesignAllowed` refuses a
21
+ * non-`standard` design at the `free` level, naming the design and the
22
+ * run's level, before `scaffold()`'s first disk write.
19
23
  */
20
24
  import type { ModuleName } from "../parse-args.js";
25
+ import type { StarterDesign } from "../template-manifest.js";
26
+ import { type Level } from "./module-tier.js";
21
27
  /**
22
28
  * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
23
29
  * `/pro` route and there will not be one (#6439, operator decision OD-2) —
@@ -33,6 +39,18 @@ export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
33
39
  export declare class LicenseRequiredError extends Error {
34
40
  constructor(message: string);
35
41
  }
42
+ /** Designs every level renders (#6760 D1). Anything else in `STARTER_DESIGNS` is Pro and up. */
43
+ export declare const FREE_DESIGNS: readonly StarterDesign[];
44
+ /** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
45
+ export declare const DESIGN_LEVELS: readonly Level[];
46
+ /**
47
+ * Refuse a non-`standard` design at a level below Pro (#6760 D1, D6). Pure
48
+ * and synchronous — takes the level `deriveLevel`/`resolveModuleSet` already
49
+ * resolved; never reads a key or derives a level itself (D2, fail-closed:
50
+ * both checks below are allow-lists, so an unrecognized `level` refuses
51
+ * rather than granting it).
52
+ */
53
+ export declare function assertDesignAllowed(design: StarterDesign | undefined, level: Level): void;
36
54
  export interface ResolvePaidModulesOptions {
37
55
  /** The requested module set's paid subset (see `module-tier.ts`). */
38
56
  paidModules: ModuleName[];
@@ -52,7 +70,20 @@ export interface ResolvePaidModulesOptions {
52
70
  * to do. Absent ⟹ a plain `--modules` run, whose messages are unchanged.
53
71
  */
54
72
  templateName?: string;
73
+ /**
74
+ * The key's derived level (#6759 D5), when one was derived on this call's
75
+ * path. Absent on the `--modules`-keep branch, where no level is derived
76
+ * (NFR-2) — the refusal then names only the module and its own level,
77
+ * never the caller's.
78
+ */
79
+ level?: Level;
55
80
  }
81
+ /**
82
+ * The message phrase for one module (D5): what level includes it, or that it
83
+ * is an add-on. Pure — derived from `levelOf`, never from the current run's
84
+ * own derived level.
85
+ */
86
+ export declare function levelPhrase(m: ModuleName): string;
56
87
  /**
57
88
  * Resolve every requested paid module to its fetched bundle bytes, or throw
58
89
  * `LicenseRequiredError` naming exactly why the run cannot proceed. Never
@@ -61,3 +92,51 @@ export interface ResolvePaidModulesOptions {
61
92
  * BEFORE the scaffold copy — see `programmatic.ts`'s `scaffold()`).
62
93
  */
63
94
  export declare function resolvePaidModules(options: ResolvePaidModulesOptions): Promise<Map<ModuleName, Uint8Array>>;
95
+ /** Inputs shared by `deriveLevel` and `resolveModuleSet` (D4/D5). */
96
+ export interface GateOptions {
97
+ /** Defaults to `process.env["AILK_LICENSE_KEY"]`. Injectable for tests. */
98
+ licenseKey?: string | undefined;
99
+ /** Forwarded to `fetchModule`; defaults to `AILK_MODULE_REGISTRY_URL` / the built-in default. */
100
+ registryUrl?: string;
101
+ /** Forwarded to `fetchModule`; defaults to this package's own version. */
102
+ version?: string;
103
+ /** Forwarded to `fetchModule`; defaults to `globalThis.fetch`. Never a live call in tests. */
104
+ fetchImpl?: typeof fetch;
105
+ }
106
+ /**
107
+ * Derive the key's level from what the registry actually answers (D4) — the
108
+ * level only chooses which modules to ASK FOR; every byte is still
109
+ * authorized by the registry per module. Nothing client-side is an
110
+ * entitlement claim: the JWT is never parsed here.
111
+ *
112
+ * No key ⟹ `"free"`, zero network. A present key is shape-checked first,
113
+ * whichever source it came from (SEC-2). Then: probe every paid member of
114
+ * `LEVEL_MODULES.pro`, in order — a licence error on any of them is refused
115
+ * (a key that does not unlock all of Pro is not silently downgraded); any
116
+ * other failure (404, 5xx, unreachable) is wrapped so the caller sees it
117
+ * could not determine the level, and is told the escape hatch (`--modules`).
118
+ * Then probe `multi-tenant` (what Pro Plus adds beyond Pro): a licence error
119
+ * there is the expected "this key is Pro, not Pro Plus" answer, not a
120
+ * refusal.
121
+ */
122
+ export declare function deriveLevel(options: GateOptions): Promise<{
123
+ level: Level;
124
+ bundles: Map<ModuleName, Uint8Array>;
125
+ }>;
126
+ /**
127
+ * The one entry point that turns `--modules` + the key into the effective
128
+ * module set and its resolved bundles (D5) — `programmatic.ts`'s `scaffold()`
129
+ * calls this for every run except a named `--template` alias (NFR-4).
130
+ *
131
+ * A named list with a non-addition (the KEEP branch, D2) resolves it exactly
132
+ * as today, with no level derived and no extra request (NFR-2). Otherwise the
133
+ * level is derived first, the set composed from it, and any named addition
134
+ * not already fetched during the level probe is resolved on top.
135
+ */
136
+ export declare function resolveModuleSet(options: GateOptions & {
137
+ named?: readonly ModuleName[] | undefined;
138
+ }): Promise<{
139
+ modules: ModuleName[];
140
+ bundles: Map<ModuleName, Uint8Array>;
141
+ level?: Level;
142
+ }>;