create-ailk 0.2.0 → 0.4.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 (292) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/cli.js +19 -9
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +51 -3
  6. package/dist/lib/extract-module-bundle.js +327 -28
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/fetch-template.d.ts +20 -0
  10. package/dist/lib/fetch-template.js +129 -0
  11. package/dist/lib/module-license-gate.d.ts +10 -0
  12. package/dist/lib/module-license-gate.js +13 -4
  13. package/dist/lib/paid-paths.d.ts +16 -0
  14. package/dist/lib/paid-paths.js +90 -0
  15. package/dist/module-architecture.d.ts +197 -23
  16. package/dist/module-architecture.js +820 -25
  17. package/dist/parse-args.d.ts +9 -2
  18. package/dist/parse-args.js +46 -1
  19. package/dist/programmatic.d.ts +28 -2
  20. package/dist/programmatic.js +115 -8
  21. package/dist/surfaces.d.ts +12 -36
  22. package/dist/surfaces.js +77 -5
  23. package/dist/sync-routes.js +31 -1
  24. package/dist/template-manifest.d.ts +92 -0
  25. package/dist/template-manifest.js +168 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +44 -0
  28. package/package.json +3 -2
  29. package/site-templates/marketing-site-free/ailk-template.json +8 -0
  30. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +103 -0
  31. package/site-templates/marketing-site-free/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/marketing-site-free/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site-free/provenance.json +4 -0
  34. package/templates/.claude/agents/web.md +2 -3
  35. package/templates/.claude/rules/architecture.md +5 -5
  36. package/templates/.claude/skills/README.md +2 -1
  37. package/templates/.env.example +21 -9
  38. package/templates/CONVENTIONS.md +6 -7
  39. package/templates/apps/api/.env.example +9 -0
  40. package/templates/apps/api/CLAUDE.md +33 -5
  41. package/templates/apps/api/package.json +0 -1
  42. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  43. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  44. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  45. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +166 -0
  46. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  47. package/templates/apps/api/src/config/index.ts +19 -0
  48. package/templates/apps/api/src/config/modules.ts +12 -14
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  50. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  51. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  52. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  53. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  54. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  55. package/templates/apps/api/src/openapi/index.ts +9 -3
  56. package/templates/apps/api/src/openapi/spec.ts +505 -65
  57. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  58. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  59. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  60. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  61. package/templates/apps/api/src/routes/content/index.ts +38 -11
  62. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  63. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  64. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  65. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  66. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  67. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  68. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  69. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  70. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  71. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  72. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  73. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  74. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  75. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  76. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  77. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  78. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  79. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  80. package/templates/apps/api/src/server.ts +504 -137
  81. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  82. package/templates/apps/api/src/services/listing-config.ts +58 -0
  83. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  84. package/templates/apps/api/src/services/project-listings.ts +229 -4
  85. package/templates/apps/api/src/vercel-handler.ts +60 -26
  86. package/templates/apps/mcp/.env.example +8 -0
  87. package/templates/apps/mcp/CLAUDE.md +2 -2
  88. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  89. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  90. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  91. package/templates/apps/mcp/src/config/modules.ts +14 -13
  92. package/templates/apps/mcp/src/server.ts +8 -4
  93. package/templates/apps/mcp/src/tools/index.ts +7 -22
  94. package/templates/apps/web/.env.example +15 -0
  95. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  96. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  97. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  98. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  99. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  100. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  101. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  102. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  103. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  104. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  105. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  106. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  107. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  108. package/templates/apps/web/jest.config.cjs +19 -13
  109. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  110. package/templates/apps/web/lib/server-api.ts +28 -12
  111. package/templates/apps/web/messages/en.json +58 -0
  112. package/templates/apps/web/next.config.mjs +1 -3
  113. package/templates/apps/web/package.json +0 -6
  114. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  115. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  116. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  117. package/templates/apps/web/public/favicon-16x16.png +0 -0
  118. package/templates/apps/web/public/favicon-32x32.png +0 -0
  119. package/templates/apps/web/public/favicon.ico +0 -0
  120. package/templates/apps/web/public/favicon.svg +6 -3
  121. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  122. package/templates/apps/web/public/logomark.svg +4 -0
  123. package/templates/apps/web/public/site.webmanifest +2 -2
  124. package/templates/apps/web/public/wordmark.svg +4 -0
  125. package/templates/database/CHANGELOG.md +47 -0
  126. package/templates/database/inbox/schema.prisma +67 -0
  127. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  128. package/templates/database/package.json +1 -1
  129. package/templates/package.json +1 -1
  130. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  131. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  132. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  133. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  134. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  135. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  136. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  137. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  138. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  139. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  140. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  141. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  142. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  143. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  144. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  145. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  146. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  147. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  148. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  149. package/templates/apps/api/src/lib/stripe.ts +0 -52
  150. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  151. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  152. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  153. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  154. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  155. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  156. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  157. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  158. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  159. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  160. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  161. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  162. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  163. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  164. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  165. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  166. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  167. package/templates/apps/api/src/routes/flows/README.md +0 -147
  168. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  169. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  170. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  171. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  172. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  173. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  174. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  175. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  176. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  177. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  178. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  179. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  180. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  181. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  182. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  183. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  184. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  185. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  186. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  187. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  188. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  189. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  190. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  191. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  192. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  193. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  194. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  195. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  196. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  197. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  198. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  199. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  200. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  201. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  202. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  203. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  204. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  205. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  206. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  207. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  208. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  209. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  210. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  211. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  212. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  213. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  214. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  215. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  216. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  217. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  218. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  219. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  220. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  221. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  222. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  223. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  224. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  225. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  226. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  227. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  228. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  229. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  230. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  231. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  232. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  233. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  234. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  235. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  236. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  237. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  238. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  239. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  240. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  241. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  242. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  243. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  244. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  245. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  246. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  247. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  248. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  249. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  250. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  258. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  259. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  260. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  261. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  262. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  263. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  264. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  265. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  266. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  267. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  268. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  269. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  270. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  271. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  272. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  273. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  274. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  275. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  276. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  277. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  278. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  279. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  280. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  281. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  282. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  283. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  284. package/templates/apps/web/content/en/blog/README.txt +0 -25
  285. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  286. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  287. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  288. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  289. package/templates/apps/web/lib/source.ts +0 -34
  290. package/templates/apps/web/public/logomark-dark.svg +0 -4
  291. package/templates/apps/web/public/logomark-light.svg +0 -4
  292. package/templates/apps/web/source.config.ts +0 -78
@@ -7,16 +7,23 @@
7
7
  * composition is downstream work, so the default (`marketing`) reproduces
8
8
  * today's fixed-tree scaffold exactly.
9
9
  */
10
- export declare const RECOGNIZED_MODULES: readonly ["marketing", "docs", "multi-tenant", "scheduling"];
10
+ export declare const RECOGNIZED_MODULES: readonly ["marketing", "docs-blog", "multi-tenant", "workspace", "scheduling", "signal-collectors", "commerce"];
11
11
  export type ModuleName = (typeof RECOGNIZED_MODULES)[number];
12
12
  export interface CliOptions {
13
13
  /** Positional target directory (the dir to scaffold into). Optional only
14
14
  * when `help` or `version` is set. */
15
15
  target: string | undefined;
16
+ /**
17
+ * The site template to install (`--template`), validated against
18
+ * `RECOGNIZED_TEMPLATES` at parse time. Undefined when the flag is absent —
19
+ * and a run naming no template scaffolds exactly what it did before the
20
+ * flag existed (spec 5481 § 3 DD3).
21
+ */
22
+ template: string | undefined;
16
23
  install: boolean;
17
24
  help: boolean;
18
25
  version: boolean;
19
- /** Selected modules (recognized: marketing, docs, multi-tenant, scheduling).
26
+ /** Selected modules (recognized: marketing, docs-blog, multi-tenant, scheduling).
20
27
  * Defaults to ['marketing'] when the flag is absent. */
21
28
  modules: string[];
22
29
  /** Explicitly requested route surfaces (`--surfaces`). Empty when the flag is
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { parseArgs } from "node:util";
11
11
  import { RECOGNIZED_SURFACES, parseSurfaces } from "./surfaces.js";
12
+ import { RECOGNIZED_TEMPLATES } from "./template-names.js";
12
13
  /**
13
14
  * The recognized module vocabulary for the `--modules` selector.
14
15
  *
@@ -20,9 +21,12 @@ import { RECOGNIZED_SURFACES, parseSurfaces } from "./surfaces.js";
20
21
  */
21
22
  export const RECOGNIZED_MODULES = [
22
23
  "marketing",
23
- "docs",
24
+ "docs-blog",
24
25
  "multi-tenant",
26
+ "workspace",
25
27
  "scheduling",
28
+ "signal-collectors",
29
+ "commerce",
26
30
  ];
27
31
  /** The default module selection when `--modules` is absent (reproduces today's scaffold). */
28
32
  const DEFAULT_MODULES = ["marketing"];
@@ -49,6 +53,16 @@ Options:
49
53
  Defaults to \`marketing\`, which reproduces today's
50
54
  scaffold. A module is a composable unit across the
51
55
  three tiers (data · API · presentation).
56
+ --template <name> The site template to install (recognized:
57
+ ${RECOGNIZED_TEMPLATES.join(", ")}).
58
+ A template is a page roster plus seed content and
59
+ config — never code. Its required modules are ADDED
60
+ to whatever \`--modules\` names, so the two compose.
61
+ A template needing only free modules installs from
62
+ the tree this package ships, with no key and no
63
+ network call; one needing a paid module needs
64
+ AILK_LICENSE_KEY to cover every module it requires,
65
+ and refuses naming whichever it does not.
52
66
  --surfaces <csv> Comma-separated route surfaces to scaffold IN
53
67
  ADDITION to the core set (/, /about, /contact, /faq,
54
68
  /thanks). Recognized: ${RECOGNIZED_SURFACES.join(", ")}.
@@ -100,6 +114,7 @@ export function parseCliArgs(argv) {
100
114
  allowPositionals: true,
101
115
  options: {
102
116
  modules: { type: "string" },
117
+ template: { type: "string" },
103
118
  surfaces: { type: "string" },
104
119
  "sync-architecture": { type: "string" },
105
120
  "sync-catalog": { type: "boolean", default: false },
@@ -117,6 +132,7 @@ export function parseCliArgs(argv) {
117
132
  const version = parsed.values.version === true;
118
133
  const install = parsed.values.install === true;
119
134
  const modules = parseModules(parsed.values.modules);
135
+ const template = parseTemplate(parsed.values.template);
120
136
  const surfaces = parseSurfaceList(parsed.values.surfaces);
121
137
  const syncArchitecture = parsed.values["sync-architecture"];
122
138
  const syncCatalog = parsed.values["sync-catalog"] === true;
@@ -136,6 +152,7 @@ export function parseCliArgs(argv) {
136
152
  if (help || version) {
137
153
  return {
138
154
  target: undefined,
155
+ template: undefined,
139
156
  install,
140
157
  help,
141
158
  version,
@@ -153,6 +170,7 @@ export function parseCliArgs(argv) {
153
170
  }
154
171
  return {
155
172
  target: parsed.positionals[0],
173
+ template,
156
174
  install,
157
175
  help,
158
176
  version,
@@ -171,6 +189,7 @@ export function parseCliArgs(argv) {
171
189
  }
172
190
  return {
173
191
  target: parsed.positionals[0],
192
+ template,
174
193
  install,
175
194
  help,
176
195
  version,
@@ -181,6 +200,32 @@ export function parseCliArgs(argv) {
181
200
  syncArchitecture: undefined,
182
201
  };
183
202
  }
203
+ /**
204
+ * Validate the `--template` value against the declared template set
205
+ * (spec 5481 § 3 DD1).
206
+ *
207
+ * Validated HERE, in the parser, so an unrecognized template fails at parse
208
+ * time as a `ParseError` exactly like an unrecognized module or surface does.
209
+ * Splitting it out to `programmatic.ts` would make `--template` the one flag
210
+ * in this CLI whose bad value surfaces somewhere else.
211
+ *
212
+ * The names come from `template-names.ts` rather than from
213
+ * `TEMPLATE_MANIFESTS` itself, and that file's doc comment records the
214
+ * measured reason: importing the registry here closes a runtime cycle
215
+ * through `lib/module-tier.ts`. `TEMPLATE_MANIFESTS` remains the registry;
216
+ * the two sets are asserted equal by the suite.
217
+ *
218
+ * @throws ParseError on an undeclared template name, naming the declared set.
219
+ */
220
+ function parseTemplate(raw) {
221
+ if (raw === undefined)
222
+ return undefined;
223
+ const name = raw.trim();
224
+ if (!RECOGNIZED_TEMPLATES.includes(name)) {
225
+ throw new ParseError(`unknown template "${raw}". Recognized: ${RECOGNIZED_TEMPLATES.join(", ")}`);
226
+ }
227
+ return name;
228
+ }
184
229
  /**
185
230
  * Parse + validate the `--surfaces` CSV, re-throwing an unrecognized-surface
186
231
  * failure as a ParseError so every CLI parse failure reaches the orchestrator's
@@ -29,7 +29,23 @@ export interface ScaffoldOptions {
29
29
  */
30
30
  preset?: string;
31
31
  /**
32
- * Selected modules (recognized: marketing, docs, multi-tenant). Defaults to
32
+ * The site template to install (`--template`), a `TEMPLATE_MANIFESTS` key.
33
+ *
34
+ * Consumed ONLY by the async `scaffold()` below. `scaffoldSync` stays
35
+ * template-oblivious for exactly the reason it stays licensing-oblivious
36
+ * (spec 5481 § 3 DD3): a call naming no template takes a code path with no
37
+ * new statement in it, which is a stronger byte-identity guarantee than a
38
+ * test asserting the output happens to match.
39
+ *
40
+ * What a template DOES change is `modules` — `scaffold()` unions the
41
+ * template's `requires` with whatever `--modules` named and passes THAT
42
+ * list to `scaffoldSync` (DD2), so the resolved surfaces, the emitted
43
+ * `architecture.yaml` and the generated nav are all functions of the union.
44
+ */
45
+ template?: string;
46
+ /**
47
+ * Selected modules (recognized: marketing, docs-blog, multi-tenant,
48
+ * scheduling). Defaults to
33
49
  * ['marketing']. The scaffolder does NOT yet vary the copied template tree by
34
50
  * module (per-module template composition is downstream work), so the copied
35
51
  * files are today's fixed-tree scaffold — BUT the derived `architecture.yaml`
@@ -69,6 +85,8 @@ export interface ScaffoldOptions {
69
85
  licenseKey?: string;
70
86
  /** Overrides `AILK_MODULE_REGISTRY_URL` / the built-in module-distribution default. */
71
87
  moduleRegistryUrl?: string;
88
+ /** Overrides `AILK_TEMPLATE_REGISTRY_URL` / the built-in template-distribution default. */
89
+ templateRegistryUrl?: string;
72
90
  /** Injected fetch for tests. Never a live network call in CI or in this repo's evidence runs. */
73
91
  fetchImpl?: typeof fetch;
74
92
  }
@@ -97,9 +115,17 @@ export declare function scaffoldSync(opts: ScaffoldOptions): ScaffoldResult;
97
115
  * read and no network call (the regression floor). A paid module with
98
116
  * no key, or a licence the module distribution rejects, throws
99
117
  * `LicenseRequiredError` HERE — before anything is written to disk.
100
- * 3. `scaffoldSync` — unchanged, today's local `templates/` copy.
118
+ * 3. `scaffoldSync` — unchanged, today's local `templates/` copy, over the
119
+ * EFFECTIVE module set (see 0 below).
101
120
  * 4. Merge each resolved paid module's fetched bundle into the freshly
102
121
  * scaffolded tree (`extractModuleBundle`).
122
+ * 5. Install the named site template LAST (#5481; parent record D9), from
123
+ * its fetched bundle or — when it requires only free modules — from the
124
+ * `site-templates/` tree this package ships.
125
+ *
126
+ * And, ahead of all five when `opts.template` is set (0): resolve the
127
+ * template and union its `requires` with `opts.modules`, so every step above
128
+ * runs over the EFFECTIVE module set rather than the requested one.
103
129
  *
104
130
  * `scaffoldSync` itself stays licensing-oblivious on purpose: every existing
105
131
  * synchronous caller (this package's own test suite, overwhelmingly) is
@@ -21,15 +21,26 @@ import { fileURLToPath } from "node:url";
21
21
  import { CATALOG_TARGET_PATH, writeComponentCatalog, } from "./component-catalog.js";
22
22
  import { copyTemplate } from "./copy.js";
23
23
  import { deriveArchitecture } from "./derive-architecture.js";
24
- import { extractModuleBundle } from "./lib/extract-module-bundle.js";
24
+ import { extractModuleBundle, installBundleTree, } from "./lib/extract-module-bundle.js";
25
+ import { fetchTemplate } from "./lib/fetch-template.js";
25
26
  import { resolvePaidModules } from "./lib/module-license-gate.js";
26
27
  import { classifyModules } from "./lib/module-tier.js";
27
28
  import { normalizeModules } from "./module-architecture.js";
28
29
  import { NAV_CONFIG_PATH, excludedRoutePaths, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
29
30
  import { syncRoutes } from "./sync-routes.js";
31
+ import { resolveTemplate, templateDelivery } from "./template-manifest.js";
30
32
  import { validateTarget } from "./validate.js";
31
33
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
32
34
  const TEMPLATES_DIR = resolve(PKG_ROOT, "templates");
35
+ /**
36
+ * Where a Free site template's files are read from on the keyless path — and
37
+ * why it is BESIDE `templates/` rather than inside it (parent record D14):
38
+ * `copyTemplate` copies the whole `templates/` tree into EVERY scaffold, so a
39
+ * per-template subtree there would land in scaffolds that asked for a
40
+ * different template, or for none. It reaches the published tarball through
41
+ * `package.json` `files` instead.
42
+ */
43
+ const SITE_TEMPLATES_DIR = resolve(PKG_ROOT, "site-templates");
33
44
  const VERSIONS_FILE = resolve(PKG_ROOT, "versions.json");
34
45
  function readVersionMap() {
35
46
  if (!existsSync(VERSIONS_FILE)) {
@@ -114,9 +125,17 @@ export function scaffoldSync(opts) {
114
125
  * read and no network call (the regression floor). A paid module with
115
126
  * no key, or a licence the module distribution rejects, throws
116
127
  * `LicenseRequiredError` HERE — before anything is written to disk.
117
- * 3. `scaffoldSync` — unchanged, today's local `templates/` copy.
128
+ * 3. `scaffoldSync` — unchanged, today's local `templates/` copy, over the
129
+ * EFFECTIVE module set (see 0 below).
118
130
  * 4. Merge each resolved paid module's fetched bundle into the freshly
119
131
  * scaffolded tree (`extractModuleBundle`).
132
+ * 5. Install the named site template LAST (#5481; parent record D9), from
133
+ * its fetched bundle or — when it requires only free modules — from the
134
+ * `site-templates/` tree this package ships.
135
+ *
136
+ * And, ahead of all five when `opts.template` is set (0): resolve the
137
+ * template and union its `requires` with `opts.modules`, so every step above
138
+ * runs over the EFFECTIVE module set rather than the requested one.
120
139
  *
121
140
  * `scaffoldSync` itself stays licensing-oblivious on purpose: every existing
122
141
  * synchronous caller (this package's own test suite, overwhelmingly) is
@@ -124,23 +143,106 @@ export function scaffoldSync(opts) {
124
143
  * with a paid module in `opts.modules` exercises the gate at all.
125
144
  */
126
145
  export async function scaffold(opts) {
127
- const { paid } = classifyModules(normalizeModules(opts.modules ?? ["marketing"]));
146
+ // Resolved first, and by the registry rather than re-validated here: the
147
+ // name already passed `parse-args.ts`, and `resolveTemplate` throws a
148
+ // `TemplateError` for a programmatic caller that skipped the parser.
149
+ const template = opts.template !== undefined ? resolveTemplate(opts.template) : undefined;
150
+ // The effective module set: union(template.requires, parsed --modules),
151
+ // normalized through `normalizeModules` — the single ordering authority
152
+ // (parent record D7). Order-independence falls out rather than being
153
+ // engineered: it returns `RECOGNIZED_MODULES.filter(...)`, declaration-
154
+ // ordered and deduped, so the same SET in any argument order is the same
155
+ // list.
156
+ const modules = normalizeModules([
157
+ ...(template?.requires ?? []),
158
+ ...(opts.modules ?? ["marketing"]),
159
+ ]);
160
+ const { paid } = classifyModules(modules);
128
161
  const bundles = await resolvePaidModules({
129
162
  paidModules: paid,
130
163
  licenseKey: opts.licenseKey,
164
+ ...(template !== undefined ? { templateName: template.name } : {}),
131
165
  ...(opts.moduleRegistryUrl !== undefined
132
166
  ? { registryUrl: opts.moduleRegistryUrl }
133
167
  : {}),
134
168
  ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
135
169
  });
136
- const result = scaffoldSync(opts);
170
+ // The template's OWN bundle, fetched here — before `scaffoldSync` — so the
171
+ // fourth refusal (the template's bundle unpublished, parent record D17 /
172
+ // spec 5481 § 3 DD5 row 4) throws with the target still non-existent, like
173
+ // the three module refusals above it (DD6). It is EXTRACTED last, below.
174
+ //
175
+ // The keyless path never reaches this at all: a template requiring only
176
+ // free modules classifies to an empty paid set, so `resolvePaidModules`
177
+ // returned at its first branch without reading `process.env` or the
178
+ // network, and `templateDelivery` sends us past this too (DD7). That is
179
+ // what makes record D11's no-network guarantee structural rather than
180
+ // merely likely.
181
+ const templateBundle = template !== undefined && templateDelivery(template) === "paid"
182
+ ? await fetchTemplate({
183
+ templateName: template.name,
184
+ token: opts.licenseKey ?? process.env["AILK_LICENSE_KEY"] ?? "",
185
+ ...(opts.templateRegistryUrl !== undefined
186
+ ? { registryUrl: opts.templateRegistryUrl }
187
+ : {}),
188
+ ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
189
+ })
190
+ : undefined;
191
+ // `modules` — the union — is what `scaffoldSync` receives, not `opts`
192
+ // (spec 5481 § 3 DD2). `resolveSurfaces`, `deriveArchitecture` and
193
+ // `renderNavConfig` are all computed INSIDE `scaffoldSync` from
194
+ // `opts.modules`, so a union computed for the licence classification alone
195
+ // would gate correctly and emit the wrong surfaces, the wrong
196
+ // `architecture.yaml` and the wrong nav — a silent wrong result, not a
197
+ // failure.
198
+ const result = scaffoldSync({ ...opts, modules });
199
+ // Threaded to extractModuleBundle for `package.json` patch targets'
200
+ // `workspace:*` substitution (spec 5418 § 3 D2) — the same version map
201
+ // `scaffoldSync` already reads for `copyTemplate`, re-read here since
202
+ // `scaffold()` doesn't share `scaffoldSync`'s call frame.
203
+ const versions = readVersionMap();
137
204
  for (const [moduleName, bytes] of bundles) {
138
205
  await extractModuleBundle({
139
206
  bytes,
140
207
  targetDir: result.targetDir,
141
208
  moduleName,
209
+ versions,
142
210
  });
143
211
  }
212
+ // The template lands LAST — after `scaffoldSync` and after every module
213
+ // bundle (parent record D9) — so its content and config files sit on top of
214
+ // everything derived. It shapes the derived artifacts by DECLARING (its
215
+ // `requires` are in the union above; its roster rows are what the nav is
216
+ // generated from), never by landing a code file: the nav is already written
217
+ // by `scaffoldSync` from `renderNavConfig(enabledSurfaces)`, and record
218
+ // §5.2 forbids a `.ts` in any `site-templates/` `installMap` destination.
219
+ //
220
+ // One install step, two source roots (DD7): an extracted staging dir on the
221
+ // paid path, `site-templates/<name>/` on disk on the Free one.
222
+ if (template !== undefined) {
223
+ if (templateBundle !== undefined) {
224
+ await extractModuleBundle({
225
+ bytes: templateBundle,
226
+ targetDir: result.targetDir,
227
+ moduleName: template.name,
228
+ templateRequires: template.requires,
229
+ versions,
230
+ });
231
+ }
232
+ else {
233
+ const sourceRoot = join(SITE_TEMPLATES_DIR, template.name);
234
+ if (!existsSync(sourceRoot)) {
235
+ throw new Error(`template "${template.name}": ${sourceRoot} is missing — the published package must carry site-templates/ in its "files" list`);
236
+ }
237
+ installBundleTree({
238
+ sourceRoot,
239
+ targetDir: result.targetDir,
240
+ name: template.name,
241
+ templateRequires: template.requires,
242
+ versions,
243
+ });
244
+ }
245
+ }
144
246
  return result;
145
247
  }
146
248
  // Re-exported so a consumer of the async `scaffold()` above can type its own
@@ -194,10 +296,15 @@ export function syncArchitectureSync(opts) {
194
296
  * stats paths and returns strings, and never writes or deletes.
195
297
  *
196
298
  * Routes only, deliberately. A surface's content directories are informational
197
- * (`Surface.content`), not drift: `apps/web/content/<locale>/{blog,docs}` must
198
- * exist in EVERY scaffold — `source.config.ts` resolves those paths at build
199
- * time whether or not the route is mounted — so sweeping content here would
200
- * tell every operator to delete directories the build requires.
299
+ * (`Surface.content`), not drift, so sweeping content here would tell an
300
+ * operator to delete directories that merely hold their own copy.
301
+ *
302
+ * This used to rest on a stronger claim — that `apps/web/content/<locale>/
303
+ * {blog,docs}` must exist in EVERY scaffold, because `source.config.ts`
304
+ * resolved those paths at build time whether or not the route mounted. D-46
305
+ * retired it: `source.config.ts` leaves with the docs + blog module (#5365 D3),
306
+ * so a Free scaffold defines no Fumadocs collection and carries neither
307
+ * directory.
201
308
  */
202
309
  function findUndeclaredSurfacePaths(targetAbs, enabled) {
203
310
  return excludedRoutePaths(enabled)
@@ -1,36 +1,3 @@
1
- /**
2
- * Route-surface registry — which public routes a scaffolded site carries.
3
- *
4
- * A SURFACE is one public route surface of the presentation tier (see
5
- * architecture.yaml `composition_model`): a hub route plus the detail route and
6
- * content directories that feed it. The registry is the single source of the
7
- * surface vocabulary, in the same spirit as `module-architecture.ts` is the
8
- * single source of the module→architecture-slice mapping — a code registry, not
9
- * N hand-maintained template trees.
10
- *
11
- * Two tiers:
12
- *
13
- * - CORE_SURFACE_PATHS — the routes every AILK site has regardless of what
14
- * the business is (`/`, `/about`, `/contact`, `/faq`, `/thanks/{slug}`).
15
- * Always scaffolded, never gateable.
16
- * - GATEABLE_SURFACES — everything else. A surface is scaffolded ONLY when
17
- * the declared architecture carries it; otherwise its route directories are
18
- * skipped by `copyTemplate` and the routes are ABSENT from the scaffold,
19
- * not present-and-placeholdering (spec #4154 D1).
20
- *
21
- * The default (`--modules marketing`) enables NO gateable surface: every one of
22
- * them is a claim about the business that the scaffolder cannot make. The path
23
- * from there is the `suggest-site-pages` → `scaffold-page` skill pair that
24
- * already ships in `templates/.claude/skills/`.
25
- *
26
- * Zero runtime dependencies — create-ailk is a published, standalone CLI.
27
- *
28
- * Deliberately imports nothing from `parse-args.ts`: that module imports THIS
29
- * one (for the `--surfaces` vocabulary in its help text), so a back-import
30
- * would close a cycle. The module vocabulary is re-stated here as the keys of
31
- * `MODULE_SURFACES`, and `__tests__/surface-gating.test.ts` asserts those keys
32
- * equal `RECOGNIZED_MODULES` so the two cannot drift.
33
- */
34
1
  /**
35
2
  * Route paths (site-relative, for the record written into `architecture.yaml`)
36
3
  * that every scaffold carries.
@@ -88,9 +55,18 @@ export declare const RECOGNIZED_SURFACES: readonly string[];
88
55
  * Surfaces each recognized module contributes.
89
56
  *
90
57
  * `marketing` (the default) contributes NONE — a default scaffold is core-only.
91
- * `docs` mounts the Fumadocs presentation surface; `multi-tenant` brings the
92
- * flow-stepper surface with its API-backed engine; `scheduling` brings the
93
- * booking page and the confirmation page the module owns.
58
+ * `docs-blog` mounts BOTH Fumadocs presentation surfaces: D-46 made docs and
59
+ * blog one Pro module, so `/blog` is no longer a Free surface that any scaffold
60
+ * can turn on. `multi-tenant` contributes no surface of its own — it USED to
61
+ * carry `flows`, but D-48 reassigns that surface to `signal-collectors`
62
+ * (#5366), which now brings the flow-stepper surface with its API-backed
63
+ * engine; `scheduling` brings the booking page and the confirmation page the
64
+ * module owns.
65
+ *
66
+ * A surface named here is MODULE-GATED: `resolveSurfaces` refuses it as an
67
+ * explicit `--surfaces` value unless the module that owns it was selected
68
+ * (#5365 FR5). Without that, `--surfaces blog` on a keyless scaffold would
69
+ * declare a surface whose route files the starter no longer contains.
94
70
  */
95
71
  export declare const MODULE_SURFACES: Record<string, readonly string[]>;
96
72
  /** Thrown on an unrecognized surface id. Mirrors parse-args' ParseError shape. */
package/dist/surfaces.js CHANGED
@@ -164,6 +164,19 @@ export const GATEABLE_SURFACES = [
164
164
  routes: [`${ROUTE_ROOT}/partners`],
165
165
  content: ["content/<locale>/PartnersList", "content/<locale>/Partner"],
166
166
  },
167
+ {
168
+ // Module-LESS by construction (#5479 D16): no MODULE_SURFACES value names
169
+ // it, so a site template may enable it by roster alone. Same for
170
+ // `privacy` and `terms` below.
171
+ id: "pricing",
172
+ routes: [`${ROUTE_ROOT}/pricing`],
173
+ content: ["content/<locale>/Pricing"],
174
+ },
175
+ {
176
+ id: "privacy",
177
+ routes: [`${ROUTE_ROOT}/privacy`],
178
+ content: ["content/<locale>/PrivacyPage"],
179
+ },
167
180
  {
168
181
  id: "products",
169
182
  routes: [`${ROUTE_ROOT}/products`],
@@ -208,6 +221,11 @@ export const GATEABLE_SURFACES = [
208
221
  routes: [`${ROUTE_ROOT}/tech-articles`],
209
222
  content: ["content/<locale>/TechArticle"],
210
223
  },
224
+ {
225
+ id: "terms",
226
+ routes: [`${ROUTE_ROOT}/terms`],
227
+ content: ["content/<locale>/TermsPage"],
228
+ },
211
229
  {
212
230
  id: "testimonials",
213
231
  routes: [`${ROUTE_ROOT}/testimonials`],
@@ -230,15 +248,32 @@ export const RECOGNIZED_SURFACES = GATEABLE_SURFACES.map((s) => s.id);
230
248
  * Surfaces each recognized module contributes.
231
249
  *
232
250
  * `marketing` (the default) contributes NONE — a default scaffold is core-only.
233
- * `docs` mounts the Fumadocs presentation surface; `multi-tenant` brings the
234
- * flow-stepper surface with its API-backed engine; `scheduling` brings the
235
- * booking page and the confirmation page the module owns.
251
+ * `docs-blog` mounts BOTH Fumadocs presentation surfaces: D-46 made docs and
252
+ * blog one Pro module, so `/blog` is no longer a Free surface that any scaffold
253
+ * can turn on. `multi-tenant` contributes no surface of its own — it USED to
254
+ * carry `flows`, but D-48 reassigns that surface to `signal-collectors`
255
+ * (#5366), which now brings the flow-stepper surface with its API-backed
256
+ * engine; `scheduling` brings the booking page and the confirmation page the
257
+ * module owns.
258
+ *
259
+ * A surface named here is MODULE-GATED: `resolveSurfaces` refuses it as an
260
+ * explicit `--surfaces` value unless the module that owns it was selected
261
+ * (#5365 FR5). Without that, `--surfaces blog` on a keyless scaffold would
262
+ * declare a surface whose route files the starter no longer contains.
236
263
  */
237
264
  export const MODULE_SURFACES = {
238
265
  marketing: [],
239
- docs: ["docs"],
240
- "multi-tenant": ["flows"],
266
+ "docs-blog": ["docs", "blog"],
267
+ "multi-tenant": [],
268
+ // `workspace` contributes zero gateable surfaces (#5371 D4): every route it
269
+ // owns lives in the `(authed)` segment, never in the public marketing route
270
+ // tree a template's roster composes — the same reason `multi-tenant` above
271
+ // carries an empty list. The entry itself is still required: surface-gating's
272
+ // vocabulary assertion covers exactly the recognized module set.
273
+ workspace: [],
241
274
  scheduling: ["schedule"],
275
+ "signal-collectors": ["flows"],
276
+ commerce: [],
242
277
  };
243
278
  /** Thrown on an unrecognized surface id. Mirrors parse-args' ParseError shape. */
244
279
  export class SurfaceError extends Error {
@@ -290,14 +325,33 @@ export function resolveSurfaces(modules = ["marketing"], explicit = []) {
290
325
  for (const s of contributed)
291
326
  set.add(s);
292
327
  }
328
+ const selected = new Set(modules.map((m) => m.trim().toLowerCase()));
293
329
  for (const s of explicit) {
294
330
  if (!RECOGNIZED_SURFACES.includes(s)) {
295
331
  throw new SurfaceError(`unknown surface "${s}". Recognized: ${RECOGNIZED_SURFACES.join(", ")}`);
296
332
  }
333
+ // A surface a module OWNS cannot be declared without that module (#5365
334
+ // FR5/D12). The starter no longer carries the route files, so a bare
335
+ // `--surfaces blog` would declare a surface that serves 404 with no error
336
+ // anywhere — the silent-wrong-result this refusal closes. Keyed on
337
+ // MODULE_SURFACES, never on a surface id, so every module inherits it.
338
+ const owner = moduleOwningSurface(s);
339
+ if (owner !== undefined && !selected.has(owner)) {
340
+ throw new SurfaceError(`surface "${s}" is delivered by the "${owner}" module — add it with ` +
341
+ `--modules ${owner} (a paid module also needs AILK_LICENSE_KEY).`);
342
+ }
297
343
  set.add(s);
298
344
  }
299
345
  return [...set].sort();
300
346
  }
347
+ /** The module that owns a gateable surface, or undefined if no module does. */
348
+ function moduleOwningSurface(surface) {
349
+ for (const [moduleName, contributed] of Object.entries(MODULE_SURFACES)) {
350
+ if (contributed.includes(surface))
351
+ return moduleName;
352
+ }
353
+ return undefined;
354
+ }
301
355
  /**
302
356
  * The template-relative route directories to SKIP when copying the template —
303
357
  * every gateable surface not in `enabled`.
@@ -379,6 +433,11 @@ export const ROUTE_SINCE = {
379
433
  "launch-service": "0.0.0",
380
434
  partners: "0.0.0",
381
435
  "partners/[slug]": "0.0.0",
436
+ // #5479 — three genuinely new route directories, so they attach at today's
437
+ // fixed-group version rather than the "predates the manifest" 0.0.0 above,
438
+ // the same rule #5048 ("projects/apply") and #5014 ("(authed)") applied.
439
+ pricing: "0.22.0",
440
+ privacy: "0.22.0",
382
441
  products: "0.0.0",
383
442
  "products/[slug]": "0.0.0",
384
443
  projects: "0.0.0",
@@ -398,6 +457,7 @@ export const ROUTE_SINCE = {
398
457
  "software/[slug]": "0.0.0",
399
458
  "tech-articles": "0.0.0",
400
459
  "tech-articles/[slug]": "0.0.0",
460
+ terms: "0.22.0",
401
461
  testimonials: "0.0.0",
402
462
  "thanks/[slug]": "0.14.0",
403
463
  "use-cases": "0.0.0",
@@ -414,6 +474,12 @@ export const ROUTE_SINCE = {
414
474
  // choice #5048's "projects/apply" entry already made for the same reason.
415
475
  "(authed)": "0.18.0",
416
476
  "(authed)/account": "0.18.0",
477
+ // #5371 — the workspace module's three settings routes. Genuinely new route
478
+ // directories, so they attach at today's fixed-group version rather than the
479
+ // "predates the manifest" 0.0.0 above, the same rule #5479 and #5048 applied.
480
+ "(authed)/workspace": "0.22.1",
481
+ "(authed)/workspace/site": "0.22.1",
482
+ "(authed)/workspace/team": "0.22.1",
417
483
  "(authed)/waitlist": "0.18.0",
418
484
  "(authed)/waitlist/[experimentId]": "0.18.0",
419
485
  "(authed)/waitlist/[experimentId]/signups/[leadId]": "0.18.0",
@@ -460,6 +526,9 @@ export const ROUTE_DEPENDENCIES = {
460
526
  `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
461
527
  `${ROUTE_ROOT}/(authed)/waitlist/format.ts`,
462
528
  ],
529
+ "(authed)/workspace": ["apps/web/lib/server-api.ts"],
530
+ "(authed)/workspace/site": ["apps/web/lib/server-api.ts"],
531
+ "(authed)/workspace/team": ["apps/web/lib/server-api.ts"],
463
532
  "(authed)/waitlist": ["apps/web/lib/server-api.ts"],
464
533
  };
465
534
  /**
@@ -474,6 +543,9 @@ export const ROUTE_DEPENDENCIES = {
474
543
  export const ROUTE_MESSAGE_NAMESPACES = {
475
544
  "sign-in": ["signIn"],
476
545
  "(authed)/account": ["account"],
546
+ "(authed)/workspace": ["workspace"],
547
+ "(authed)/workspace/site": ["workspace"],
548
+ "(authed)/workspace/team": ["workspace"],
477
549
  "(authed)/waitlist": ["waitlistDashboard.gate"],
478
550
  };
479
551
  /** Template-relative path of the generated site-nav registry. */
@@ -35,6 +35,18 @@ import { substituteMarkers } from "./substitute.js";
35
35
  import { GATEABLE_SURFACES, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
36
36
  /** Template-relative prefix every `apps/web` route directory sits under. */
37
37
  const ROUTE_ROOT = "apps/web/app/[locale]";
38
+ /**
39
+ * `<surface id>/<entry name>` pairs that live directly under a GATEABLE
40
+ * surface's top-level directory but belong to a DIFFERENT module (#5366).
41
+ * `projects/apply` (#5048) is signal-collectors' own owned file
42
+ * (`ListingApplicationClient.tsx`, D-48) — its module bundle restores it
43
+ * whenever signal-collectors is selected, independent of the unrelated
44
+ * "projects" content surface's own selection. See the doc comment on
45
+ * `syncRoutes`'s `carried` computation.
46
+ */
47
+ const NESTED_FOREIGN_MODULE_ENTRIES = new Set([
48
+ "projects/apply",
49
+ ]);
38
50
  /**
39
51
  * The template's route directories, ROUTE_ROOT-relative and sorted.
40
52
  *
@@ -120,12 +132,30 @@ export function syncRoutes(opts) {
120
132
  throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
121
133
  }
122
134
  // Presence of any of a surface's route dirs means the site carries it.
135
+ // A surface's directory containing ONLY entries in
136
+ // `NESTED_FOREIGN_MODULE_ENTRIES` does not count as present (#5366): a
137
+ // DIFFERENT module can own a companion sub-route nested under an unrelated
138
+ // content surface's URL segment — `projects/apply` (#5048) is
139
+ // signal-collectors' own owned file (D-48), and its module bundle restores
140
+ // it whenever signal-collectors is selected, regardless of whether the
141
+ // unrelated "projects" CONTENT surface is separately requested. Without
142
+ // this exclusion, a signal-collectors-only scaffold would falsely read as
143
+ // "carries projects" the moment its bundle lands that one file.
123
144
  const carried = new Set();
124
145
  const skippedSurfaces = [];
125
146
  for (const surface of GATEABLE_SURFACES) {
126
147
  const present = surface.routes.some((r) => {
127
148
  const p = join(targetAbs, r);
128
- return existsSync(p) && statSync(p).isDirectory();
149
+ if (!existsSync(p) || !statSync(p).isDirectory())
150
+ return false;
151
+ const entries = readdirSync(p);
152
+ // An ancestor dir left empty by a consumer-side deletion (AC-2's
153
+ // "ancestor also missing" case) still counts as carried — only a
154
+ // NON-empty directory whose entries are entirely foreign (the
155
+ // `projects`/`apply` case above) does not.
156
+ if (entries.length === 0)
157
+ return true;
158
+ return entries.some((name) => !NESTED_FOREIGN_MODULE_ENTRIES.has(`${surface.id}/${name}`));
129
159
  });
130
160
  if (present)
131
161
  carried.add(surface.id);