create-ailk 0.21.0 → 0.23.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 (342) hide show
  1. package/component-catalog.json +186 -8
  2. package/component-catalog.md +9 -7
  3. package/dist/add-module.js +20 -14
  4. package/dist/add-route.d.ts +55 -1
  5. package/dist/add-route.js +294 -12
  6. package/dist/cli.js +27 -10
  7. package/dist/derive-architecture.d.ts +12 -3
  8. package/dist/derive-architecture.js +20 -8
  9. package/dist/design-package.js +7 -2
  10. package/dist/lib/design-checks.js +4 -2
  11. package/dist/lib/env-catalog.js +5 -5
  12. package/dist/lib/npm-auth.d.ts +9 -0
  13. package/dist/lib/npm-auth.js +48 -0
  14. package/dist/lib/scaffold-render-sweep.d.ts +3 -2
  15. package/dist/lib/scaffold-render-sweep.js +5 -8
  16. package/dist/module-architecture.js +99 -9
  17. package/dist/paid-sections.d.ts +21 -2
  18. package/dist/paid-sections.js +69 -7
  19. package/dist/programmatic.js +19 -5
  20. package/dist/seed-flows.d.ts +4 -1
  21. package/dist/seed-flows.js +50 -4
  22. package/dist/site-chrome.d.ts +8 -0
  23. package/dist/site-chrome.js +17 -1
  24. package/dist/spoke-checkout.d.ts +65 -41
  25. package/dist/spoke-checkout.js +208 -430
  26. package/dist/starter-grid.d.ts +0 -3
  27. package/dist/starter-grid.js +24 -14
  28. package/dist/surfaces.js +52 -26
  29. package/dist/sync-routes.d.ts +38 -1
  30. package/dist/sync-routes.js +187 -13
  31. package/dist/template-record.d.ts +4 -3
  32. package/dist/template-record.js +8 -4
  33. package/dist/topology.js +25 -1
  34. package/dist/update.js +15 -0
  35. package/package.json +3 -3
  36. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
  37. package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
  38. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  39. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
  40. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
  41. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
  42. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
  43. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
  44. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
  45. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
  46. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  47. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
  48. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
  49. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  50. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
  51. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
  52. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  53. package/templates/.env.example +9 -7
  54. package/templates/.github/workflows/ci.yml +13 -0
  55. package/templates/README.md +1 -0
  56. package/templates/_gitignore +4 -0
  57. package/templates/apps/api/.env.example +27 -0
  58. package/templates/apps/api/CLAUDE.md +87 -17
  59. package/templates/apps/api/package.json +1 -0
  60. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
  61. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
  62. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
  63. package/templates/apps/api/src/config/index.ts +109 -0
  64. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  65. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
  66. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
  67. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
  68. package/templates/apps/api/src/lib/consent-store.ts +28 -0
  69. package/templates/apps/api/src/lib/error-serializer.ts +74 -7
  70. package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
  71. package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
  72. package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
  73. package/templates/apps/api/src/lib/stripe.ts +74 -4
  74. package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
  75. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
  77. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
  78. package/templates/apps/api/src/middleware/auth.ts +13 -6
  79. package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
  80. package/templates/apps/api/src/openapi/spec.ts +33 -3
  81. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
  82. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
  83. package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
  84. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
  85. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
  86. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
  87. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
  88. package/templates/apps/api/src/routes/checkout/session.ts +57 -25
  89. package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
  90. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
  91. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
  92. package/templates/apps/api/src/routes/consent/account.ts +268 -0
  93. package/templates/apps/api/src/routes/consent/index.ts +15 -7
  94. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
  95. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
  96. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
  97. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
  98. package/templates/apps/api/src/routes/content/get-page.ts +4 -0
  99. package/templates/apps/api/src/routes/content/index.ts +16 -2
  100. package/templates/apps/api/src/routes/content/offerings.ts +184 -0
  101. package/templates/apps/api/src/routes/content/pricing.ts +176 -0
  102. package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
  103. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
  104. package/templates/apps/api/src/routes/leads/index.ts +164 -7
  105. package/templates/apps/api/src/routes/webhooks/README.md +66 -0
  106. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
  107. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
  108. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
  109. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
  110. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
  111. package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
  112. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
  113. package/templates/apps/api/src/routes/webhooks/stripe.ts +245 -36
  114. package/templates/apps/api/src/server.ts +163 -46
  115. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
  116. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
  117. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
  118. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1383 -133
  119. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
  120. package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
  121. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
  122. package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
  123. package/templates/apps/api/src/services/consent.ts +95 -0
  124. package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
  125. package/templates/apps/api/src/services/entitlements.ts +157 -1
  126. package/templates/apps/api/src/services/lead-routing.ts +431 -47
  127. package/templates/apps/api/src/services/magnet-completion.ts +242 -0
  128. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
  129. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
  130. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
  131. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
  132. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
  133. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
  134. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
  135. package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
  136. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
  137. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
  138. package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
  139. package/templates/apps/api/src/services/offerings/index.ts +614 -0
  140. package/templates/apps/api/src/services/offerings/source.ts +140 -0
  141. package/templates/apps/api/src/services/offerings/store.ts +150 -0
  142. package/templates/apps/api/src/services/person.ts +164 -17
  143. package/templates/apps/api/src/services/plan-offerings.ts +125 -0
  144. package/templates/apps/api/src/services/public-site-resolver.ts +27 -15
  145. package/templates/apps/mcp/CLAUDE.md +22 -14
  146. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
  147. package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
  148. package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
  149. package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
  150. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
  151. package/templates/apps/mcp/__tests__/server.test.ts +13 -11
  152. package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
  153. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
  154. package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
  155. package/templates/apps/mcp/src/server.ts +5 -0
  156. package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
  157. package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
  158. package/templates/apps/mcp/src/tools/purchase.ts +30 -2
  159. package/templates/apps/mcp/src/workspace-server.ts +15 -0
  160. package/templates/apps/web/.env.example +24 -31
  161. package/templates/apps/web/CLAUDE.md +393 -9
  162. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
  163. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
  164. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
  165. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
  166. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
  167. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
  168. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
  169. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
  170. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
  171. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
  172. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
  173. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
  174. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
  175. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
  176. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
  177. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
  178. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
  179. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
  180. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
  181. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
  182. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
  183. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
  184. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
  185. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
  186. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
  187. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
  188. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
  189. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
  190. package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
  191. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
  192. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
  193. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
  194. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
  195. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
  196. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +295 -0
  197. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +120 -0
  198. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +23 -0
  199. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +319 -0
  200. package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
  201. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
  202. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +141 -0
  203. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +424 -0
  204. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +72 -11
  205. package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
  206. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  207. package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
  208. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
  209. package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
  210. package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
  211. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  212. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
  213. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
  214. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +160 -13
  215. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +466 -36
  216. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
  217. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +172 -57
  218. package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
  219. package/templates/apps/web/app/[locale]/contact/page.tsx +99 -60
  220. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  221. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
  222. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +341 -4
  223. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
  224. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
  225. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
  226. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
  227. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
  228. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
  229. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
  230. package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
  231. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
  232. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
  233. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
  234. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
  235. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  236. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  237. package/templates/apps/web/app/[locale]/layout.tsx +55 -4
  238. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
  239. package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
  240. package/templates/apps/web/app/[locale]/page.tsx +13 -1
  241. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  242. package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
  243. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
  244. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
  245. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
  246. package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
  247. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  248. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
  249. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  250. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  251. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
  252. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  253. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  254. package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
  255. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
  256. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
  257. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
  258. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
  259. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
  260. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
  261. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
  262. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  263. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
  264. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
  265. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
  266. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
  267. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
  268. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
  269. package/templates/apps/web/auth.ts +28 -2
  270. package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
  271. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
  272. package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
  273. package/templates/apps/web/lib/__tests__/deck-page.test.ts +7 -1
  274. package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
  275. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
  276. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
  277. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
  278. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +101 -25
  279. package/templates/apps/web/lib/account-invite.ts +37 -0
  280. package/templates/apps/web/lib/auth-routes.ts +36 -2
  281. package/templates/apps/web/lib/checkout-config.ts +16 -0
  282. package/templates/apps/web/lib/consent-policies.ts +22 -0
  283. package/templates/apps/web/lib/deck-page.ts +7 -0
  284. package/templates/apps/web/lib/deliverable-grant.ts +18 -0
  285. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
  286. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
  287. package/templates/apps/web/lib/in-page-magnet.ts +416 -0
  288. package/templates/apps/web/lib/magnet-routes.ts +42 -0
  289. package/templates/apps/web/lib/modules.generated.ts +9 -0
  290. package/templates/apps/web/lib/newsletter.actions.ts +202 -0
  291. package/templates/apps/web/lib/offerings-pricing.ts +326 -0
  292. package/templates/apps/web/lib/posting-title.ts +17 -0
  293. package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
  294. package/templates/apps/web/lib/sign-up-agreements.ts +449 -0
  295. package/templates/apps/web/lib/site-chrome.ts +2 -0
  296. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  297. package/templates/apps/web/messages/en.json +71 -20
  298. package/templates/apps/web/middleware.ts +12 -2
  299. package/templates/apps/web/package.json +1 -0
  300. package/templates/apps/web/security-headers.config.ts +17 -0
  301. package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
  302. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
  303. package/templates/apps/web/src/lib/lead-write.ts +90 -0
  304. package/templates/apps/web/src/lib/rate-limit.ts +79 -0
  305. package/templates/content/en/BlogList/blog.mdx +4 -71
  306. package/templates/content/en/BookCall/book-a-call.mdx +1 -41
  307. package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
  308. package/templates/content/en/BookCall/onsite-service.mdx +1 -17
  309. package/templates/content/en/BookCall/start-project.mdx +1 -17
  310. package/templates/content/en/ContactPage/contact.mdx +6 -63
  311. package/templates/content/en/HomePage/home.mdx +3 -21
  312. package/templates/database/CHANGELOG.md +69 -0
  313. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
  314. package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
  315. package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
  316. package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
  317. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
  318. package/templates/database/__tests__/offerings-migration.test.ts +197 -0
  319. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
  320. package/templates/database/__tests__/offerings.db.test.ts +303 -0
  321. package/templates/database/__tests__/seed.test.ts +64 -1
  322. package/templates/database/auth/schema.prisma +4 -0
  323. package/templates/database/config/schema.prisma +91 -0
  324. package/templates/database/inbox/schema.prisma +17 -2
  325. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
  326. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
  327. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
  328. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
  329. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
  330. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
  331. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
  332. package/templates/database/package.json +1 -1
  333. package/templates/database/seed.ts +62 -3
  334. package/versions.json +35 -35
  335. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
  336. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
  337. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
  338. package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
  339. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
  340. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
  341. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
  342. package/templates/apps/web/lib/meeting-slugs.ts +0 -63
@@ -109,9 +109,6 @@ export declare const ALIASES: Readonly<{
109
109
  readonly design: "standard";
110
110
  readonly modules: readonly ["marketing"];
111
111
  readonly categories: readonly ["accounting", "legal", "technology"];
112
- readonly layout: {
113
- readonly announcementBanner: true;
114
- };
115
112
  };
116
113
  /**
117
114
  * The Pro flagship. Its module list is OFFERINGS § 4 offering 3's tiers line
@@ -1,13 +1,17 @@
1
1
  /**
2
- * The two pages every starter carries whatever its shape (#6658 D5,
2
+ * The pages every starter carries whatever its shape (#6658 D5,
3
3
  * `docs/decisions/starter-roster-page-set.md` D4/D7): a glossary and a
4
- * newsletter signup. Each is a module-less `GATEABLE_SURFACES` entry the
5
- * roster enables, with its seed in `_base`.
4
+ * newsletter signup — each a module-less `GATEABLE_SURFACES` entry the
5
+ * roster enables, with its seed in `_base` — and the `/unsubscribe` landing
6
+ * every signed link calls (#8850 D1). That one is a code route with no
7
+ * `content`, like `/sign-in`: a `CORE_ROUTE_DIRS` entry every scaffold
8
+ * already serves, never gated (#8438 D17), declared here so the roster of
9
+ * record lists it beside the newsletter.
6
10
  *
7
- * The third — `/book-a-call` — was retired by #6897 D10: every scaffold's
8
- * former `/book-a-call` link now points at `/meet/book-a-call`, served by
9
- * `FREE_MEET_PLACEHOLDER_ROWS` below (a Free placeholder) or by the
10
- * scheduling module's own live `/meet/[slug]` page.
11
+ * A former third page — `/book-a-call` — was retired by #6897 D10: every
12
+ * scaffold's former `/book-a-call` link now points at `/meet/book-a-call`,
13
+ * served by `FREE_MEET_PLACEHOLDER_ROWS` below (a Free placeholder) or by
14
+ * the scheduling module's own live `/meet/[slug]` page.
11
15
  */
12
16
  const SHAPE_INDEPENDENT_ROWS = [
13
17
  { route: "/glossary", content: ["content/en/GlossaryList/glossary.mdx"] },
@@ -15,6 +19,7 @@ const SHAPE_INDEPENDENT_ROWS = [
15
19
  route: "/newsletter",
16
20
  content: ["content/en/NewsletterSignup/newsletter.mdx"],
17
21
  },
22
+ { route: "/unsubscribe" },
18
23
  ];
19
24
  /**
20
25
  * The Free `/meet` placeholder pages (#6897 D5) — a meeting-types hub
@@ -30,10 +35,12 @@ const SHAPE_INDEPENDENT_ROWS = [
30
35
  * Every alias that takes the `scheduling` module drops `"/meet"` and
31
36
  * `"/meet/[slug]"` (D5) — otherwise `mergeRoster` refuses the duplicate
32
37
  * route the module's own rows also contribute. The ROWS go; the SEEDS stay
33
- * (#6942, operator 2026-09-28, partly reversing D5): until
34
- * `SCHEDULING_CAL_LINK` is configured the module's pages hand off to the Free
35
- * placeholder routes, which serve these seeds. `withMeetPlaceholderSeeds`
36
- * (`site-roster.ts`) folds them onto the module's rows.
38
+ * (#6942, operator 2026-09-28, partly reversing D5): the module's meeting
39
+ * page reads a `BookCall/<slug>` seed for its URL, its JSON-LD and its
40
+ * fallback title, never its sections (#8843 C5). `withMeetPlaceholderSeeds`
41
+ * (`site-roster.ts`) folds them onto the module's rows. The hub-driven
42
+ * `/private` index has no row here: it reaches those aliases through the
43
+ * module's own `pageContributions` (#8843 D8, #8850 D2).
37
44
  */
38
45
  const FREE_MEET_PLACEHOLDER_ROWS = [
39
46
  { route: "/meet", content: ["content/en/LandingPage/meet.mdx"] },
@@ -391,9 +398,12 @@ export const ALIASES = Object.freeze({
391
398
  design: "standard",
392
399
  modules: ["marketing"],
393
400
  categories: ["accounting", "legal", "technology"],
394
- // The demo site carries the "Get started with this template" banner
395
- // (operator ruling 2026-09-23); every other layout knob keeps its default.
396
- layout: { announcementBanner: true },
401
+ // No `layout`: every knob keeps its default. The "Get started for free or
402
+ // upgrade to Pro" banner this alias carried for the demo site (operator
403
+ // ruling 2026-09-23) reached every keyless `organization-product`
404
+ // scaffold too, so Epic #8845's "no starter page ships our own brands or
405
+ // upsells" retires it (#8850, spec C6); a demo deployment that wants it
406
+ // sets it in its own config.
397
407
  },
398
408
  /**
399
409
  * The Pro flagship. Its module list is OFFERINGS § 4 offering 3's tiers line
package/dist/surfaces.js CHANGED
@@ -229,15 +229,15 @@ export const GATEABLE_SURFACES = [
229
229
  content: [],
230
230
  },
231
231
  {
232
- // The `scheduling` MODULE's presentation tier — the booking page, the
233
- // per-booking-type page and the confirmation page the module OWNS
234
- // (`/meet`, `/meet/:slug`, `/meet/confirmed`, one directory, so one entry
235
- // gates all three; renamed from the schedule/ directory by #5563).
236
- // API-backed like `flows`: the booker reads its slots from apps/api's
237
- // `/v1/schedule/*` routes and the event name from `SCHEDULING_CAL_LINK`,
238
- // so there is no MDX of its own.
232
+ // The `scheduling` MODULE's presentation tier — the meeting index, the
233
+ // meeting page and the confirmation page the module OWNS (`/meet`,
234
+ // `/meet/:slug`, `/meet/confirmed`, one directory; renamed from the
235
+ // schedule/ directory by #5563), and the private index `/private`
236
+ // (#8843 D10), a second directory gated with it. API-backed like
237
+ // `flows`: every meeting fact comes from apps/api's
238
+ // `GET /v1/schedule/meetings`, so there is no MDX of its own.
239
239
  id: "meet",
240
- routes: [`${ROUTE_ROOT}/meet`],
240
+ routes: [`${ROUTE_ROOT}/meet`, `${ROUTE_ROOT}/private`],
241
241
  content: [],
242
242
  // `navLabelKey` names COPY, not a route — D1 R2 (#5563): renaming it would
243
243
  // orphan an operator's customised `messages/<locale>.json` for zero
@@ -395,7 +395,9 @@ export const MODULE_SURFACES = {
395
395
  // vocabulary assertion covers exactly the recognized module set.
396
396
  workspace: [],
397
397
  scheduling: ["meet"],
398
- "signal-collectors": ["flows"],
398
+ // #8848 D12 — `/downloads` is now a redirect into `/reports`, both this
399
+ // module's, so `--surfaces downloads` without it is refused as `flows` is.
400
+ "signal-collectors": ["flows", "downloads"],
399
401
  commerce: [],
400
402
  };
401
403
  /** Thrown on an unrecognized surface id. Mirrors parse-args' ParseError shape. */
@@ -563,6 +565,13 @@ export const ROUTE_SINCE = {
563
565
  "blog/[[...slug]]": "0.0.0",
564
566
  careers: "0.0.0",
565
567
  "careers/[slug]": "0.0.0",
568
+ // #8840 D11 — the Job application magnet's two walks, signal-collectors
569
+ // slice files. Genuinely new, so they attach at the fixed group's next
570
+ // release line, the same rule #8661 D10 applied.
571
+ "careers/apply": "0.44.0",
572
+ "careers/[slug]/apply": "0.44.0",
573
+ // #8849 D7 — the Free apply page, a core file of the careers surface.
574
+ "careers/[slug]/[action]": "0.44.0",
566
575
  "case-studies": "0.0.0",
567
576
  "case-studies/[slug]": "0.0.0",
568
577
  clients: "0.0.0",
@@ -572,6 +581,9 @@ export const ROUTE_SINCE = {
572
581
  "dev/purchase": "0.0.0",
573
582
  "docs/[[...slug]]": "0.0.0",
574
583
  downloads: "0.0.0",
584
+ // #8848 D12 — the per-slug redirect into `/reports/<slug>`, a
585
+ // signal-collectors slice file at the fixed group's next release line.
586
+ "downloads/[slug]": "0.44.0",
575
587
  events: "0.0.0",
576
588
  "events/[slug]": "0.0.0",
577
589
  // #6721 — the feature pages, a new route pair at today's line.
@@ -751,6 +763,9 @@ export const ROUTE_SINCE = {
751
763
  // new route directory, so it attaches at today's fixed-group version, the
752
764
  // same rule applied above.
753
765
  "[slug]/[detail]": "0.28.0",
766
+ // #8843 D8 — the private meeting index, a new route directory gated with
767
+ // `meet`, attached at today's fixed-group version by the same rule.
768
+ private: "0.43.0",
754
769
  };
755
770
  /**
756
771
  * Surface id of the registry that owns `routeDir` (ROUTE_ROOT-relative): the
@@ -798,38 +813,42 @@ export const ROUTE_DEPENDENCIES = {
798
813
  "apps/web/lib/auth-routes.ts",
799
814
  ],
800
815
  "(auth)/sign-in": ["apps/web/lib/auth-env.ts"],
801
- // #8438 D13/D21 — the sign-up card's agreements gate also carries the
802
- // carried-cookie mint, `sign-up.actions.ts`, beside it in this route's own
803
- // directory.
816
+ // #8765 D8/D9 — the sign-up page links its agreements through the one
817
+ // localized policy map; the carried-cookie mint (`sign-up.actions.ts`) is
818
+ // deleted, since the server now refuses and records the agreements.
804
819
  "(auth)/sign-up": [
805
820
  "apps/web/lib/auth-env.ts",
806
- `${ROUTE_ROOT}/(auth)/sign-up/sign-up.actions.ts`,
821
+ "apps/web/lib/consent-policies.ts",
807
822
  ],
808
823
  "(auth)/forgot-password": ["apps/web/lib/auth-env.ts"],
809
824
  "(auth)/reset-password": ["apps/web/lib/auth-env.ts"],
810
825
  // #7430 — the group's layout reads `isAccountPath()` to give the account
811
- // area its member-facing signed-out gate.
826
+ // area its member-facing signed-out gate. #8765 D3/D4 — `auth.ts` imports
827
+ // the sign-up agreements plugin and hooks, so that file travels beside it.
812
828
  "(authed)": [
813
829
  "apps/web/auth.ts",
814
830
  "apps/web/lib/auth-env.ts",
815
831
  "apps/web/lib/auth-routes.ts",
832
+ "apps/web/lib/sign-up-agreements.ts",
816
833
  ],
817
834
  "(authed)/account": [
818
835
  "apps/web/lib/account-config.ts",
836
+ "apps/web/lib/consent-policies.ts",
819
837
  "apps/web/lib/server-api.ts",
820
838
  "apps/web/lib/site-brand.tsx",
821
839
  "apps/web/lib/site-logo.tsx",
822
- // #8438 D13/D21 — two brand-new files in this already-existing route
823
- // directory; a scaffold whose `(authed)/account` predates this unit
824
- // needs them added, which the directory-level sync alone would not do
825
- // (sync never overwrites or adds to a directory it already has).
826
- // `acceptCarriedAgreementsAction` (the carried-cookie CONSUME side) lives
827
- // in this directory's own `agreements-actions.ts`, never in
828
- // `(auth)/sign-up` — see that file's header: `(auth)/**` is a sign-in
829
- // seed a sign-in-less spoke drops, so a cross-directory reach from here
830
- // into it would dangle.
840
+ // #8438 D13/D21, #8765 D7 — the agreements interstitial the layout shows
841
+ // a member with no current grant (`agreements-prompt.tsx`), its accept
842
+ // action (`agreements-actions.ts`) and the read the layout and the Inbox
843
+ // share (`agreements-gate.ts`). Listed because a scaffold
844
+ // whose `(authed)/account` predates #8438 needs them added, which the
845
+ // directory-level sync alone would not do (sync never overwrites or adds
846
+ // to a directory it already has). Both live in this directory, never in
847
+ // `(auth)/**` — a sign-in seed a sign-in-less spoke drops, so a
848
+ // cross-directory reach from here into it would dangle.
831
849
  `${ROUTE_ROOT}/(authed)/account/agreements-prompt.tsx`,
832
850
  `${ROUTE_ROOT}/(authed)/account/agreements-actions.ts`,
851
+ `${ROUTE_ROOT}/(authed)/account/agreements-gate.ts`,
833
852
  ],
834
853
  // #7430 — the three child routes also import files in their PARENT's
835
854
  // directory. A scaffold whose `(authed)/account` predates #7430 keeps its
@@ -934,6 +953,7 @@ export const ROUTE_DEPENDENCIES = {
934
953
  // D11 applied to Notifications' cookie-settings link.
935
954
  "(authed)/account/privacy": [
936
955
  "apps/web/lib/account-config.ts",
956
+ "apps/web/lib/consent-policies.ts",
937
957
  "apps/web/lib/server-api.ts",
938
958
  `${ROUTE_ROOT}/(authed)/account/account-data.ts`,
939
959
  `${ROUTE_ROOT}/(authed)/account/settings-frame.tsx`,
@@ -1039,9 +1059,13 @@ export const ROUTE_DEPENDENCIES = {
1039
1059
  `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
1040
1060
  ],
1041
1061
  "(authed)/waitlist": ["apps/web/lib/server-api.ts"],
1042
- // #6183 S9b D5 — both /meet pages call the shared enabled-slug read.
1043
- meet: ["apps/web/lib/meeting-slugs.ts"],
1044
- "meet/[slug]": ["apps/web/lib/meeting-slugs.ts"],
1062
+ // #8843 D6 — the booking pages share one builder over the meetings read;
1063
+ // `/private` returns the index frame that lives under `meet/`.
1064
+ meet: ["apps/web/lib/meetings.tsx"],
1065
+ "meet/[slug]": ["apps/web/lib/meetings.tsx"],
1066
+ private: ["apps/web/lib/meetings.tsx", `${ROUTE_ROOT}/meet/meeting-index.tsx`],
1067
+ // #8927 D8 — /pricing maps the Offerings shown set through the mapper.
1068
+ pricing: ["apps/web/lib/offerings-pricing.ts"],
1045
1069
  };
1046
1070
  /**
1047
1071
  * i18n message namespaces (dot-path into `apps/web/messages/<locale>.json`) a
@@ -1088,6 +1112,8 @@ export const ROUTE_MESSAGE_NAMESPACES = {
1088
1112
  team: ["team"],
1089
1113
  // #8438 D21 — the unsubscribe landing's own namespace.
1090
1114
  unsubscribe: ["unsubscribe"],
1115
+ // #8927 D6 — the Offerings cards' CTA, recommendation and period phrases.
1116
+ pricing: ["pricing", "pricingOfferings"],
1091
1117
  };
1092
1118
  /** Template-relative path of the generated site-nav registry. */
1093
1119
  export const NAV_CONFIG_PATH = "apps/web/lib/nav-config.ts";
@@ -78,6 +78,23 @@ export interface SyncRoutesResult {
78
78
  routes: string[];
79
79
  entry: string;
80
80
  }>;
81
+ /**
82
+ * The retired legacy spoke checkout (#9142), on a spoke only — empty lists
83
+ * on a hub. See `retireLegacySpokeCheckout`.
84
+ */
85
+ retiredCheckout: RetiredCheckoutReport;
86
+ }
87
+ /** What `retireLegacySpokeCheckout` found, and did when `apply` was set. */
88
+ export interface RetiredCheckoutReport {
89
+ /** Files deleted (without `apply`: to delete), scaffold-relative, sorted. */
90
+ retired: string[];
91
+ /** Retired files left in place, with why — the site deletes them by hand. Sorted by path. */
92
+ kept: Array<{
93
+ path: string;
94
+ reason: string;
95
+ }>;
96
+ /** Generated files rewritten (without `apply`: to rewrite) — the billing action. */
97
+ rewritten: string[];
81
98
  }
82
99
  /** One `NO_STRUCTURED_DATA_ROUTES` element: its source text, dedented, and the routes it exempts. */
83
100
  interface StructuredDataExemption {
@@ -102,6 +119,26 @@ export declare function parseStructuredDataExemptions(source: string): Structure
102
119
  * the registry recognizes (`surfaceForRouteDir`).
103
120
  */
104
121
  export declare function listTemplateRouteDirs(templatesDir: string): string[];
122
+ /**
123
+ * Retire the legacy env-key spoke checkout from a spoke (#9142): its buy
124
+ * route, Stripe webhook route, `lib/checkout.ts`, their tests
125
+ * (`RETIRED_SPOKE_CHECKOUT_PATHS`) and the generated checkout e2e workflow.
126
+ *
127
+ * - A copied file is deleted only when the template record lists it and its
128
+ * bytes still hash to the recorded copy — the rule `create-ailk update`
129
+ * uses for a file the template dropped; its record row goes with it. An
130
+ * edited file, or one the record does not list, is reported in `kept`.
131
+ * - The workflow, never recorded, is deleted only when its first line is
132
+ * still create-ailk's generated marker.
133
+ * - The billing action (a spoke that signs in through its hub) imported
134
+ * `lib/checkout.ts`; when its first line is still the generated marker and
135
+ * it differs from the current render, it is rewritten to `BILLING_ACTION`.
136
+ * An edited one that still imports `lib/checkout` is reported in `kept`.
137
+ *
138
+ * Without `apply` nothing is written: the lists say what `--apply` would do.
139
+ * Directories the deletes leave empty are removed, up to `RETIRE_STOP_DIRS`.
140
+ */
141
+ export declare function retireLegacySpokeCheckout(targetAbs: string, apply: boolean): RetiredCheckoutReport;
105
142
  /**
106
143
  * Compare an already-scaffolded repo's route tree with the template's and,
107
144
  * when `apply` is set, copy in the route dirs it is missing.
@@ -120,7 +157,7 @@ export interface SyncRoutesArgs {
120
157
  apply: boolean;
121
158
  help: boolean;
122
159
  }
123
- export declare const SYNC_ROUTES_HELP = "create-ailk sync-routes \u2014 copy template routes an existing site is missing.\n\nUsage:\n create-ailk sync-routes [<target-dir>] [--apply]\n\nArguments:\n <target-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n\nOptions:\n --apply Copy the missing route directories into the target.\n Without it the command only reports: exit 1 when a\n route is missing, 0 when in sync.\n --help, -h Print this message.\n\nChecks every core route plus the routes of each surface the site carries; a\nsurface gated off at scaffold time is skipped, not drift. A route the site has\nchanged locally is reported and never overwritten. Nothing is ever deleted.\n";
160
+ export declare const SYNC_ROUTES_HELP = "create-ailk sync-routes \u2014 copy template routes an existing site is missing.\n\nUsage:\n create-ailk sync-routes [<target-dir>] [--apply]\n\nArguments:\n <target-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n\nOptions:\n --apply Copy the missing route directories into the target.\n Without it the command only reports: exit 1 when a\n route is missing, 0 when in sync.\n --help, -h Print this message.\n\nChecks every core route plus the routes of each surface the site carries; a\nsurface gated off at scaffold time is skipped, not drift. A route the site has\nchanged locally is reported and never overwritten. Nothing is deleted except,\non a spoke, the retired legacy checkout's files that still match create-ailk's\ncopy: --apply removes them and rewrites the generated billing action (#9142).\n";
124
161
  /**
125
162
  * Parse the argv AFTER the `sync-routes` word. Strict `node:util` parseArgs,
126
163
  * like `parseCliArgs`; at most one positional.
@@ -19,26 +19,33 @@
19
19
  * - Expected bytes are what `copyTemplate` would write. The copier's
20
20
  * text/binary + marker rules are NOT re-implemented here: the template's
21
21
  * route root is copied once into a scratch dir and compared from there.
22
- * - Never overwrite, never delete. A route dir the site has changed is
23
- * reported as modified and left alone; `--apply` only creates directories
24
- * that do not exist, so a second run is a no-op. When the site has edited
25
- * its structured-data test, a route `--apply` adds that the template
26
- * exempts there is reported with the exact `NO_STRUCTURED_DATA_ROUTES`
27
- * entry to add — the test itself is never written (#8704).
22
+ * - Never overwrite, never delete — with one named exception. A route dir
23
+ * the site has changed is reported as modified and left alone; `--apply`
24
+ * only creates directories that do not exist, so a second run is a no-op.
25
+ * When the site has edited its structured-data test, a route `--apply`
26
+ * adds that the template exempts there is reported with the exact
27
+ * `NO_STRUCTURED_DATA_ROUTES` entry to add — the test itself is never
28
+ * written (#8704). The exception is the retired legacy spoke checkout
29
+ * (#9142): on a spoke, `--apply` deletes its files that are still
30
+ * byte-identical to the template record and rewrites the generated
31
+ * billing action off them (`retireLegacySpokeCheckout`); an edited or
32
+ * unrecorded file is reported and left alone.
28
33
  *
29
34
  * Pure module: no `import.meta.url`, so jest can import it directly. The
30
35
  * package's own `templates/` dir is supplied by programmatic.ts.
31
36
  */
32
- import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
37
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
33
38
  import { tmpdir } from "node:os";
34
39
  import { basename, dirname, join, resolve, sep } from "node:path";
35
40
  import { parseArgs } from "node:util";
36
41
  import { copyTemplate } from "./copy.js";
42
+ import { SPOKE_BILLING_ACTION_PATH } from "./derive-architecture.js";
37
43
  import { appendEnvRows } from "./lib/declared-env.js";
38
44
  import { ENV_CATALOG } from "./lib/env-catalog.js";
45
+ import { BILLING_ACTION, BILLING_ACTION_MARKER, RETIRED_CHECKOUT_E2E_MARKER, RETIRED_CHECKOUT_E2E_WORKFLOW_PATH, RETIRED_SPOKE_CHECKOUT_PATHS, } from "./spoke-checkout.js";
39
46
  import { substituteMarkers } from "./substitute.js";
40
47
  import { GATEABLE_SURFACES, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
41
- import { TEMPLATE_RECORD_PATH, parseTemplateRecord, sha256Hex } from "./template-record.js";
48
+ import { TEMPLATE_RECORD_PATH, parseTemplateRecord, serializeTemplateRecord, sha256Hex, } from "./template-record.js";
42
49
  import { SPOKE_SEAM_FILES, isSpokeExcluded, readSpokeShape, } from "./topology.js";
43
50
  /** Template-relative prefix every `apps/web` route directory sits under. */
44
51
  const ROUTE_ROOT = "apps/web/app/[locale]";
@@ -50,11 +57,18 @@ export const STRUCTURED_DATA_TEST_PATH = "apps/web/lib/__tests__/structured-data
50
57
  * `projects/apply` (#5048) is signal-collectors' own owned file
51
58
  * (`ListingApplicationClient.tsx`, D-48) — its module bundle restores it
52
59
  * whenever signal-collectors is selected, independent of the unrelated
53
- * "projects" content surface's own selection. See the doc comment on
54
- * `syncRoutes`'s `carried` computation.
60
+ * "projects" content surface's own selection. `careers/apply` and
61
+ * `careers/[slug]` (#8840 D13) are the same case: signal-collectors' bundle
62
+ * lands the Job application walks (`careers/apply/page.tsx`,
63
+ * `careers/[slug]/apply/**`) whenever the module is selected, so a site that
64
+ * turned the careers pages off must not read as carrying "careers" — a site
65
+ * that carries them keeps `careers/page.tsx`, which still counts. See the doc
66
+ * comment on `syncRoutes`'s `carried` computation.
55
67
  */
56
68
  const NESTED_FOREIGN_MODULE_ENTRIES = new Set([
57
69
  "projects/apply",
70
+ "careers/apply",
71
+ "careers/[slug]",
58
72
  ]);
59
73
  /**
60
74
  * The elements of `source`'s `NO_STRUCTURED_DATA_ROUTES` array literal, by a
@@ -122,8 +136,9 @@ export function parseStructuredDataExemptions(source) {
122
136
  /**
123
137
  * The first exemption covering `route` by the structured-data test's route
124
138
  * rule (`route` itself, or under a `subtree` entry). An entry's `requires`
125
- * strings are not checked: they key only the generated spoke checkout pages,
126
- * which no template route dir — so no `--apply` — ever adds.
139
+ * strings are not checked: they key only the standard spoke checkout pages
140
+ * (#8748), which `surfaceForRouteDir` leaves unclassified, so no `--apply`
141
+ * ever adds them.
127
142
  */
128
143
  function exemptionFor(exemptions, route) {
129
144
  return exemptions.find((e) => route === e.route || (e.subtree && route.startsWith(`${e.route}/`)));
@@ -228,6 +243,141 @@ function isModified(expectedDir, targetDir, skip = () => false) {
228
243
  }
229
244
  return false;
230
245
  }
246
+ /** Directories a retirement never removes, even when it leaves them empty. */
247
+ const RETIRE_STOP_DIRS = new Set([
248
+ "apps/web/app/api",
249
+ "apps/web/lib",
250
+ "apps/web/lib/__tests__",
251
+ ".github/workflows",
252
+ ]);
253
+ /** Every regular file at or under `rel` in `targetAbs`, `/`-separated. A symlink is skipped, never followed. */
254
+ function regularFilesUnder(targetAbs, rel) {
255
+ const abs = join(targetAbs, ...rel.split("/"));
256
+ let stat;
257
+ try {
258
+ stat = lstatSync(abs);
259
+ }
260
+ catch {
261
+ return [];
262
+ }
263
+ if (stat.isFile())
264
+ return [rel];
265
+ if (!stat.isDirectory())
266
+ return [];
267
+ return readdirSync(abs)
268
+ .sort()
269
+ .flatMap((name) => regularFilesUnder(targetAbs, `${rel}/${name}`));
270
+ }
271
+ /** The first line of a text file. */
272
+ function firstLine(text) {
273
+ const eol = text.indexOf("\n");
274
+ return eol === -1 ? text : text.slice(0, eol);
275
+ }
276
+ /**
277
+ * Retire the legacy env-key spoke checkout from a spoke (#9142): its buy
278
+ * route, Stripe webhook route, `lib/checkout.ts`, their tests
279
+ * (`RETIRED_SPOKE_CHECKOUT_PATHS`) and the generated checkout e2e workflow.
280
+ *
281
+ * - A copied file is deleted only when the template record lists it and its
282
+ * bytes still hash to the recorded copy — the rule `create-ailk update`
283
+ * uses for a file the template dropped; its record row goes with it. An
284
+ * edited file, or one the record does not list, is reported in `kept`.
285
+ * - The workflow, never recorded, is deleted only when its first line is
286
+ * still create-ailk's generated marker.
287
+ * - The billing action (a spoke that signs in through its hub) imported
288
+ * `lib/checkout.ts`; when its first line is still the generated marker and
289
+ * it differs from the current render, it is rewritten to `BILLING_ACTION`.
290
+ * An edited one that still imports `lib/checkout` is reported in `kept`.
291
+ *
292
+ * Without `apply` nothing is written: the lists say what `--apply` would do.
293
+ * Directories the deletes leave empty are removed, up to `RETIRE_STOP_DIRS`.
294
+ */
295
+ export function retireLegacySpokeCheckout(targetAbs, apply) {
296
+ const retired = [];
297
+ const kept = [];
298
+ const rewritten = [];
299
+ const recordPath = join(targetAbs, TEMPLATE_RECORD_PATH);
300
+ let record;
301
+ if (existsSync(recordPath)) {
302
+ try {
303
+ record = parseTemplateRecord(readFileSync(recordPath, "utf8"));
304
+ }
305
+ catch {
306
+ record = undefined;
307
+ }
308
+ }
309
+ for (const path of RETIRED_SPOKE_CHECKOUT_PATHS.flatMap((rel) => regularFilesUnder(targetAbs, rel))) {
310
+ const recorded = record?.files[path]?.sha256;
311
+ if (recorded === undefined) {
312
+ kept.push({
313
+ path,
314
+ reason: record === undefined
315
+ ? "no readable template record, so an edit cannot be told apart"
316
+ : "not in the template record",
317
+ });
318
+ }
319
+ else if (sha256Hex(readFileSync(join(targetAbs, ...path.split("/")))) !== recorded) {
320
+ kept.push({ path, reason: "edited here" });
321
+ }
322
+ else {
323
+ retired.push(path);
324
+ }
325
+ }
326
+ const workflow = regularFilesUnder(targetAbs, RETIRED_CHECKOUT_E2E_WORKFLOW_PATH);
327
+ for (const path of workflow) {
328
+ const text = readFileSync(join(targetAbs, ...path.split("/")), "utf8");
329
+ if (firstLine(text) === RETIRED_CHECKOUT_E2E_MARKER)
330
+ retired.push(path);
331
+ else
332
+ kept.push({ path, reason: "edited here" });
333
+ }
334
+ const billingAbs = join(targetAbs, ...SPOKE_BILLING_ACTION_PATH.split("/"));
335
+ if (regularFilesUnder(targetAbs, SPOKE_BILLING_ACTION_PATH).length === 1) {
336
+ const text = readFileSync(billingAbs, "utf8");
337
+ if (text !== BILLING_ACTION) {
338
+ if (firstLine(text) === BILLING_ACTION_MARKER) {
339
+ rewritten.push(SPOKE_BILLING_ACTION_PATH);
340
+ }
341
+ else if (/lib\/checkout["'`]/.test(text)) {
342
+ kept.push({
343
+ path: SPOKE_BILLING_ACTION_PATH,
344
+ reason: "edited here, and it imports the retired lib/checkout",
345
+ });
346
+ }
347
+ }
348
+ }
349
+ if (apply) {
350
+ for (const path of retired) {
351
+ const abs = resolve(join(targetAbs, ...path.split("/")));
352
+ if (!abs.startsWith(targetAbs + sep)) {
353
+ throw new Error(`refusing to delete outside target dir: ${abs}`);
354
+ }
355
+ rmSync(abs);
356
+ let dir = dirname(path);
357
+ while (dir !== "." && !RETIRE_STOP_DIRS.has(dir)) {
358
+ const dirAbs = join(targetAbs, ...dir.split("/"));
359
+ if (!existsSync(dirAbs) || readdirSync(dirAbs).length > 0)
360
+ break;
361
+ rmdirSync(dirAbs);
362
+ dir = dirname(dir);
363
+ }
364
+ }
365
+ if (rewritten.length > 0)
366
+ writeFileSync(billingAbs, BILLING_ACTION);
367
+ if (record !== undefined && retired.some((path) => path in record.files)) {
368
+ const files = { ...record.files };
369
+ for (const path of retired)
370
+ delete files[path];
371
+ writeFileSync(recordPath, serializeTemplateRecord({ ...record, files }));
372
+ }
373
+ }
374
+ const byPath = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
375
+ return {
376
+ retired: retired.sort(byPath),
377
+ kept: kept.sort((a, b) => byPath(a.path, b.path)),
378
+ rewritten,
379
+ };
380
+ }
231
381
  /**
232
382
  * Compare an already-scaffolded repo's route tree with the template's and,
233
383
  * when `apply` is set, copy in the route dirs it is missing.
@@ -391,6 +541,9 @@ export function syncRoutes(opts) {
391
541
  missingMessageNamespaces,
392
542
  skippedSurfaces,
393
543
  structuredDataEntries: structuredDataEntriesFor(targetAbs, opts.templatesDir, applied),
544
+ retiredCheckout: spoke !== undefined
545
+ ? retireLegacySpokeCheckout(targetAbs, opts.apply === true)
546
+ : { retired: [], kept: [], rewritten: [] },
394
547
  };
395
548
  }
396
549
  /**
@@ -477,6 +630,25 @@ export function formatSyncRoutesReport(result, apply) {
477
630
  lines.push(...entry.split("\n").map((line) => ` ${line}`));
478
631
  }
479
632
  }
633
+ // #9142 — the retired legacy spoke checkout, named so the operator sees
634
+ // exactly what left and what stays for them to delete.
635
+ const retiredCheckout = result.retiredCheckout;
636
+ if (retiredCheckout.retired.length > 0) {
637
+ lines.push(apply
638
+ ? `[create-ailk] Removed ${retiredCheckout.retired.length} file(s) of the retired legacy spoke checkout (a spoke sells through its hub's Offerings buy route):`
639
+ : `[create-ailk] ${retiredCheckout.retired.length} file(s) of the retired legacy spoke checkout to remove with --apply (a spoke sells through its hub's Offerings buy route):`);
640
+ lines.push(...retiredCheckout.retired.map((path) => ` ${path}`));
641
+ }
642
+ if (retiredCheckout.rewritten.length > 0) {
643
+ lines.push(apply
644
+ ? "[create-ailk] Rewrote the generated billing action off the retired lib/checkout:"
645
+ : "[create-ailk] The generated billing action imports the retired lib/checkout; --apply rewrites it:");
646
+ lines.push(...retiredCheckout.rewritten.map((path) => ` ${path}`));
647
+ }
648
+ if (retiredCheckout.kept.length > 0) {
649
+ lines.push(`[create-ailk] ${retiredCheckout.kept.length} file(s) of the retired legacy spoke checkout left in place — delete or rewrite by hand:`);
650
+ lines.push(...retiredCheckout.kept.map(({ path, reason }) => ` ${path} (${reason})`));
651
+ }
480
652
  if (result.missing.length > 0 && !apply) {
481
653
  lines.push("Run `create-ailk sync-routes <target> --apply` to copy the missing routes.");
482
654
  }
@@ -499,7 +671,9 @@ Options:
499
671
 
500
672
  Checks every core route plus the routes of each surface the site carries; a
501
673
  surface gated off at scaffold time is skipped, not drift. A route the site has
502
- changed locally is reported and never overwritten. Nothing is ever deleted.
674
+ changed locally is reported and never overwritten. Nothing is deleted except,
675
+ on a spoke, the retired legacy checkout's files that still match create-ailk's
676
+ copy: --apply removes them and rewrites the generated billing action (#9142).
503
677
  `;
504
678
  /**
505
679
  * Parse the argv AFTER the `sync-routes` word. Strict `node:util` parseArgs,
@@ -3,9 +3,10 @@ export declare const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
3
3
  /**
4
4
  * The app paths a generator rewrites after `copyTemplate` lands its copy
5
5
  * (§1): the nav, the site chrome, the site fonts, the paid-sections
6
- * registry, and the four spoke seams. Composed from each writer's own
7
- * exported path constant, so a renamed constant carries its path along
8
- * here too (#8522 D2).
6
+ * registry and its module-presence flags (#8843 D10), the four spoke seams, and a commerce spoke's checkout config,
7
+ * which the checkout writer stamps and the site owns from then on (#8748
8
+ * D2). Composed from each writer's own exported path constant, so a renamed
9
+ * constant carries its path along here too (#8522 D2).
9
10
  */
10
11
  export declare const REGENERATED_APP_PATHS: readonly string[];
11
12
  /**
@@ -18,9 +18,10 @@
18
18
  import { createHash } from "node:crypto";
19
19
  import { existsSync, readFileSync } from "node:fs";
20
20
  import { join } from "node:path";
21
- import { PAID_SECTIONS_PATH } from "./paid-sections.js";
21
+ import { MODULE_FLAGS_PATH, PAID_SECTIONS_PATH } from "./paid-sections.js";
22
22
  import { SITE_CHROME_PATH } from "./site-chrome.js";
23
23
  import { SITE_FONTS_PATH } from "./site-fonts.js";
24
+ import { SPOKE_CHECKOUT_CONFIG_PATH } from "./spoke-checkout.js";
24
25
  import { NAV_CONFIG_PATH } from "./surfaces.js";
25
26
  import { SPOKE_SEAM_FILES } from "./topology.js";
26
27
  /** Where the per-file record lives, beside `.ailk-site-manifest.json`. */
@@ -28,16 +29,19 @@ export const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
28
29
  /**
29
30
  * The app paths a generator rewrites after `copyTemplate` lands its copy
30
31
  * (§1): the nav, the site chrome, the site fonts, the paid-sections
31
- * registry, and the four spoke seams. Composed from each writer's own
32
- * exported path constant, so a renamed constant carries its path along
33
- * here too (#8522 D2).
32
+ * registry and its module-presence flags (#8843 D10), the four spoke seams, and a commerce spoke's checkout config,
33
+ * which the checkout writer stamps and the site owns from then on (#8748
34
+ * D2). Composed from each writer's own exported path constant, so a renamed
35
+ * constant carries its path along here too (#8522 D2).
34
36
  */
35
37
  export const REGENERATED_APP_PATHS = [
36
38
  NAV_CONFIG_PATH,
37
39
  SITE_CHROME_PATH,
38
40
  SITE_FONTS_PATH,
39
41
  PAID_SECTIONS_PATH,
42
+ MODULE_FLAGS_PATH,
40
43
  ...SPOKE_SEAM_FILES,
44
+ SPOKE_CHECKOUT_CONFIG_PATH,
41
45
  ];
42
46
  const REGENERATED_SET = new Set(REGENERATED_APP_PATHS);
43
47
  const RECORDABLE_APP_ROOT = /^apps\/(web|api|mcp)\//;