create-ailk 0.1.1 → 0.3.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 (284) hide show
  1. package/component-catalog.md +210 -52
  2. package/dist/cli.js +0 -0
  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 +12 -3
  6. package/dist/lib/extract-module-bundle.js +236 -24
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/module-license-gate.d.ts +2 -0
  10. package/dist/lib/module-license-gate.js +1 -0
  11. package/dist/lib/paid-paths.d.ts +16 -0
  12. package/dist/lib/paid-paths.js +90 -0
  13. package/dist/module-architecture.d.ts +139 -23
  14. package/dist/module-architecture.js +668 -25
  15. package/dist/parse-args.d.ts +2 -2
  16. package/dist/parse-args.js +3 -1
  17. package/dist/programmatic.d.ts +2 -1
  18. package/dist/programmatic.js +15 -4
  19. package/dist/surfaces.d.ts +12 -36
  20. package/dist/surfaces.js +35 -5
  21. package/dist/sync-routes.js +31 -1
  22. package/package.json +14 -15
  23. package/templates/.claude/agents/web.md +2 -3
  24. package/templates/.claude/rules/architecture.md +5 -5
  25. package/templates/.claude/skills/README.md +2 -1
  26. package/templates/.env.example +21 -9
  27. package/templates/CONVENTIONS.md +6 -7
  28. package/templates/apps/api/.env.example +9 -0
  29. package/templates/apps/api/CLAUDE.md +35 -4
  30. package/templates/apps/api/package.json +0 -1
  31. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  32. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  33. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  34. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  35. package/templates/apps/api/src/config/index.ts +19 -0
  36. package/templates/apps/api/src/config/modules.ts +12 -14
  37. package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
  38. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  39. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  40. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  41. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  42. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  43. package/templates/apps/api/src/openapi/index.ts +9 -3
  44. package/templates/apps/api/src/openapi/spec.ts +505 -65
  45. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  46. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  49. package/templates/apps/api/src/routes/content/index.ts +38 -11
  50. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
  59. package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
  60. package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
  61. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  63. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  64. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  65. package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
  66. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  67. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  68. package/templates/apps/api/src/server.ts +471 -133
  69. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  70. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  71. package/templates/apps/api/src/services/consent.ts +236 -0
  72. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  73. package/templates/apps/api/src/services/listing-config.ts +58 -0
  74. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  75. package/templates/apps/api/src/services/project-listings.ts +602 -29
  76. package/templates/apps/api/src/vercel-handler.ts +60 -26
  77. package/templates/apps/mcp/.env.example +8 -0
  78. package/templates/apps/mcp/CLAUDE.md +2 -2
  79. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  80. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  81. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  82. package/templates/apps/mcp/src/config/modules.ts +14 -13
  83. package/templates/apps/mcp/src/server.ts +8 -4
  84. package/templates/apps/mcp/src/tools/index.ts +7 -22
  85. package/templates/apps/web/.env.example +15 -0
  86. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  87. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  88. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  90. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  93. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  95. package/templates/apps/web/app/[locale]/layout.tsx +13 -1
  96. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  97. package/templates/apps/web/jest.config.cjs +20 -13
  98. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  99. package/templates/apps/web/lib/site-brand.tsx +4 -1
  100. package/templates/apps/web/lib/site-theme.ts +74 -0
  101. package/templates/apps/web/next.config.mjs +1 -3
  102. package/templates/apps/web/package.json +1 -6
  103. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  104. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  105. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  106. package/templates/apps/web/public/favicon-16x16.png +0 -0
  107. package/templates/apps/web/public/favicon-32x32.png +0 -0
  108. package/templates/apps/web/public/favicon.ico +0 -0
  109. package/templates/apps/web/public/favicon.svg +6 -3
  110. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  111. package/templates/apps/web/public/logomark.svg +4 -0
  112. package/templates/apps/web/public/site.webmanifest +2 -2
  113. package/templates/apps/web/public/wordmark.svg +4 -0
  114. package/templates/content/_site.mdx +12 -0
  115. package/templates/database/CHANGELOG.md +94 -0
  116. package/templates/database/inbox/schema.prisma +165 -0
  117. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  118. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  119. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  120. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  121. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/db-generate-locked.sh +0 -0
  124. package/templates/package.json +1 -1
  125. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  126. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  127. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  128. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  129. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  130. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  131. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  132. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  133. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  134. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  135. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  136. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  137. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  138. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  139. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  140. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  141. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  142. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  143. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  144. package/templates/apps/api/src/lib/stripe.ts +0 -52
  145. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  146. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  147. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  148. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  149. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  150. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  151. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  152. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  153. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  154. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  155. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  156. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  157. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  158. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  159. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  160. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  161. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  162. package/templates/apps/api/src/routes/flows/README.md +0 -147
  163. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  164. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  165. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  166. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  167. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  168. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  169. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  170. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  171. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  172. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  173. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  174. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  175. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  176. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  177. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  178. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  179. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  180. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  181. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  182. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  183. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  184. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  185. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  186. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  187. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  188. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  189. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  190. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  191. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  192. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  193. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  194. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  195. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  196. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  197. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  198. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  199. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  200. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  201. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  202. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  203. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  204. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  205. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  206. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  207. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  208. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  209. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  210. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  211. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  212. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  213. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  214. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  215. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  216. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  217. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  218. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  219. package/templates/apps/api/src/services/flow-engine.ts +0 -1278
  220. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  221. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  222. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  223. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  224. package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
  225. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  226. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  227. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  228. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  229. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  230. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  231. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  232. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  233. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  234. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  235. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  236. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  237. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  238. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  239. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  240. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  241. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  242. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  243. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  246. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  247. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  248. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  249. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  250. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  251. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  258. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  259. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  260. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  261. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  262. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  263. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  264. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  265. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  266. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  267. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  268. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  269. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  270. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  271. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  272. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  273. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  274. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  275. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  276. package/templates/apps/web/content/en/blog/README.txt +0 -25
  277. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  278. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  279. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  280. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  281. package/templates/apps/web/lib/source.ts +0 -34
  282. package/templates/apps/web/public/logomark-dark.svg +0 -4
  283. package/templates/apps/web/public/logomark-light.svg +0 -4
  284. package/templates/apps/web/source.config.ts +0 -78
package/dist/cli.js CHANGED
File without changes
@@ -0,0 +1,47 @@
1
+ import type { ModulePatch } from "../module-architecture.js";
2
+ import { type WorkspaceVersionMap } from "../substitute.js";
3
+ export interface ApplyModulePatchesOptions {
4
+ versions: WorkspaceVersionMap;
5
+ }
6
+ /** One target whose content changed — the output of `computeModulePatches`. */
7
+ export interface ComputedModulePatch {
8
+ /** Target path, exactly as it appeared in the manifest (`ModulePatch.target`). */
9
+ target: string;
10
+ content: string;
11
+ }
12
+ export interface ComputeModulePatchesOptions extends ApplyModulePatchesOptions {
13
+ /**
14
+ * Returns the "before" bytes for `target`, or `undefined` if it does not
15
+ * exist (D4 class 2, missing target). The caller decides WHERE those bytes
16
+ * come from — an already-scaffolded `targetDir` file, or (when the same
17
+ * bundle also delivers that target via its plain content or `installMap`)
18
+ * the staged copy — so that this function never has to know about staging
19
+ * at all. Called at most once per distinct target.
20
+ */
21
+ readTarget: (target: string) => string | undefined;
22
+ }
23
+ /**
24
+ * Compute every patched target's new content in memory — throws on the
25
+ * first D4 class 1-3 failure encountered (deterministic order — targets in
26
+ * `patches` array order) and writes nothing anywhere; returns only the
27
+ * targets whose content actually changed. Pure with respect to the
28
+ * filesystem: reads go through `options.readTarget`, nothing is written.
29
+ * Pair with `writeModulePatches` for the second pass, deferred until every
30
+ * other part of the caller's own operation (e.g. `copyTree`/`installMap`)
31
+ * has ALSO succeeded, so a patch failure can never leave partial output
32
+ * from an earlier step in place either.
33
+ */
34
+ export declare function computeModulePatches(patches: readonly ModulePatch[], options: ComputeModulePatchesOptions): ComputedModulePatch[];
35
+ /** Write every `computeModulePatches` result to disk, resolved against `baseDir`, once. */
36
+ export declare function writeModulePatches(computed: readonly ComputedModulePatch[], baseDir: string): void;
37
+ /**
38
+ * Apply every patch in `patches` onto the scaffold at `targetDir` — reads
39
+ * every target directly from `targetDir` (no staging tree involved) and
40
+ * writes once everything computes cleanly. The convenience form for a
41
+ * patch set whose targets are ALL pre-existing scaffold files; a caller
42
+ * whose bundle also delivers some of those targets itself (via plain
43
+ * content or `installMap`) should use `computeModulePatches`/
44
+ * `writeModulePatches` directly so a patch failure can roll back the
45
+ * whole merge, not just the patch step.
46
+ */
47
+ export declare function applyModulePatches(patches: readonly ModulePatch[], targetDir: string, options: ApplyModulePatchesOptions): void;
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Applies a module bundle's declarative `patches` (spec 5418 § 3 D1) onto an
3
+ * already-scaffolded target — the only mechanism that edits a file the
4
+ * buyer's scaffold already has (`installMap`/`postInstall`, spec 5373 § D7,
5
+ * can only add a file or print a message). Computes every patched file in
6
+ * memory first; only after every target's in-memory pass succeeds does a
7
+ * second pass write the changed files back to disk, once (D4) — so a
8
+ * conflict, a missing target, or an unparseable target anywhere in the
9
+ * manifest's patch set leaves the scaffold exactly as `copyTree`/`installMap`
10
+ * left it, nothing partial.
11
+ *
12
+ * `kind` decides how a target is read and re-serialized:
13
+ * - "json" — `package.json` (object-shaped fields, `merge-keys`) and
14
+ * `tsconfig.json` (array-shaped, `append-unique`). Parsed with
15
+ * `JSON.parse`, mutated in place, re-serialized with
16
+ * `JSON.stringify(obj, null, 2) + "\n"` (D2). A `package.json` target
17
+ * then runs through `substituteWorkspaceVersions`, mirroring
18
+ * `copy.ts`'s `isPackageJson` branch for a freshly-copied file.
19
+ * - "next-config" / "jest-config" — treated as text, never parsed or
20
+ * evaluated (NFR1 — no AST dependency). Each op is one of five named,
21
+ * idempotent, anchored transforms (D3): checked "already applied" →
22
+ * "anchor found, apply" → "not found, unparseable" — never a
23
+ * best-effort splice.
24
+ *
25
+ * Every failure mode here is one of D4's classes 1-3 (conflict, missing
26
+ * target, unparseable target). Class 4 (unknown kind/op) is caught earlier,
27
+ * at manifest validation (D6, `extract-module-bundle.ts`), before this
28
+ * function is ever called — every op here is assumed to be a recognized
29
+ * vocabulary member for its patch's `kind`.
30
+ */
31
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
32
+ import { basename, resolve } from "node:path";
33
+ import { substituteWorkspaceVersions, } from "../substitute.js";
34
+ /** D4 class 1 — a merge-keys key, or a JS-config anchor's already-applied value, disagrees. */
35
+ function conflict(target, locator, existing, wanted) {
36
+ return new Error(`patch conflict in ${target}: "${locator}" is ${JSON.stringify(existing)}, module wants ${JSON.stringify(wanted)}`);
37
+ }
38
+ /** D4 class 2 — `resolve(targetDir, target)` does not exist on disk. */
39
+ function missingTarget(target) {
40
+ return new Error(`patch target missing: ${target} does not exist in this scaffold`);
41
+ }
42
+ /** D4 class 3 — a json target fails JSON.parse. */
43
+ function unparseableJson(target) {
44
+ return new Error(`patch target unparseable: ${target} is not valid JSON`);
45
+ }
46
+ /** D4 class 3 — a next-config/jest-config anchor (or a JSON `path`) cannot be located at all. */
47
+ function unparseableAnchor(target, anchor) {
48
+ return new Error(`patch target unparseable: ${target} — could not locate "${anchor}"`);
49
+ }
50
+ /**
51
+ * Navigate `path` into `root`, requiring every intermediate segment to
52
+ * already resolve to a plain object. Returns the immediate parent + final
53
+ * key, or `null` if any intermediate segment is missing / not an object —
54
+ * the caller decides what "the final key's own container is missing or the
55
+ * wrong shape" means for its own op (D2: never create structure the buyer
56
+ * never had; a missing/wrong-shape container is `unparseableAnchor`, the
57
+ * same disposition the "if under-specified" resolution gives `merge-keys`
58
+ * and, by symmetry, `append-unique`).
59
+ */
60
+ function resolvePathParent(root, path) {
61
+ let cur = root;
62
+ for (let i = 0; i < path.length - 1; i++) {
63
+ const seg = path[i];
64
+ if (typeof cur !== "object" ||
65
+ cur === null ||
66
+ Array.isArray(cur) ||
67
+ !Object.prototype.hasOwnProperty.call(cur, seg)) {
68
+ return null;
69
+ }
70
+ cur = cur[seg];
71
+ }
72
+ if (typeof cur !== "object" || cur === null || Array.isArray(cur))
73
+ return null;
74
+ return { parent: cur, key: path[path.length - 1] };
75
+ }
76
+ function deepEqual(a, b) {
77
+ return JSON.stringify(a) === JSON.stringify(b);
78
+ }
79
+ /**
80
+ * Resolve `workspace:*` to the concrete published semver BEFORE the
81
+ * merge-keys comparison/write, not only at final text-level serialization
82
+ * (D2 mirrors `copy.ts`'s `isPackageJson` branch, but that branch runs on a
83
+ * FRESHLY-COPIED file that never gets re-read; a patch target does — a
84
+ * second `applyModulePatches` run reads back the ALREADY-substituted `^x.y.z`
85
+ * value, so comparing it against a still-literal `"workspace:*"` `entries`
86
+ * value would misreport idempotent re-application as a conflict). Reuses
87
+ * `substituteWorkspaceVersions` itself (same regex, same throw-on-missing
88
+ * behavior) rather than re-deriving the caret-prefix rule.
89
+ */
90
+ function resolveEntryValue(key, value, versions) {
91
+ if (value !== "workspace:*" || !key.startsWith("@working-theory/"))
92
+ return value;
93
+ const substituted = substituteWorkspaceVersions(`"${key}": "workspace:*"`, versions);
94
+ const match = /:\s*"([^"]+)"$/.exec(substituted);
95
+ return match ? match[1] : value;
96
+ }
97
+ function applyJsonMergeKeys(target, root, op, versions, isPackageJson) {
98
+ const resolved = resolvePathParent(root, op.path);
99
+ const container = resolved ? resolved.parent[resolved.key] : undefined;
100
+ if (!resolved || typeof container !== "object" || container === null || Array.isArray(container)) {
101
+ throw unparseableAnchor(target, op.path.join("."));
102
+ }
103
+ const obj = container;
104
+ for (const [key, rawValue] of Object.entries(op.entries)) {
105
+ const value = isPackageJson ? resolveEntryValue(key, rawValue, versions) : rawValue;
106
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
107
+ obj[key] = value;
108
+ }
109
+ else if (!deepEqual(obj[key], value)) {
110
+ throw conflict(target, [...op.path, key].join("."), obj[key], value);
111
+ }
112
+ // Present with the identical value — no-op (idempotent).
113
+ }
114
+ }
115
+ function applyJsonAppendUnique(target, root, op) {
116
+ const resolved = resolvePathParent(root, op.path);
117
+ const container = resolved ? resolved.parent[resolved.key] : undefined;
118
+ if (!resolved || !Array.isArray(container)) {
119
+ throw unparseableAnchor(target, op.path.join("."));
120
+ }
121
+ for (const item of op.items) {
122
+ if (!container.some((existing) => deepEqual(existing, item))) {
123
+ container.push(item);
124
+ }
125
+ }
126
+ }
127
+ function applyJsonOps(target, text, ops, versions, isPackageJson) {
128
+ let root;
129
+ try {
130
+ root = JSON.parse(text);
131
+ }
132
+ catch {
133
+ throw unparseableJson(target);
134
+ }
135
+ for (const op of ops) {
136
+ if (op.op === "merge-keys")
137
+ applyJsonMergeKeys(target, root, op, versions, isPackageJson);
138
+ else if (op.op === "append-unique")
139
+ applyJsonAppendUnique(target, root, op);
140
+ else {
141
+ throw new Error(`invalid module manifest: unknown patch op "${op.op}" for target ${target}`);
142
+ }
143
+ }
144
+ return `${JSON.stringify(root, null, 2)}\n`;
145
+ }
146
+ /** The `[...]` span of the array literal opened right after `anchorLiteral`, or `null` if the anchor/array can't be located. */
147
+ function findArraySpan(text, anchorLiteral) {
148
+ const anchorIdx = text.indexOf(anchorLiteral);
149
+ if (anchorIdx === -1)
150
+ return null;
151
+ const openIdx = text.indexOf("[", anchorIdx);
152
+ if (openIdx === -1)
153
+ return null;
154
+ let depth = 0;
155
+ let closeIdx = -1;
156
+ for (let i = openIdx; i < text.length; i++) {
157
+ if (text[i] === "[")
158
+ depth++;
159
+ else if (text[i] === "]") {
160
+ depth--;
161
+ if (depth === 0) {
162
+ closeIdx = i;
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ if (closeIdx === -1)
168
+ return null;
169
+ return { openIdx, closeIdx };
170
+ }
171
+ /** Whether `elementLine` already occurs WITHIN the array literal opened after `anchorLiteral` (not merely somewhere in the file). */
172
+ function arrayContainsElement(text, anchorLiteral, elementLine) {
173
+ const span = findArraySpan(text, anchorLiteral);
174
+ if (span === null)
175
+ return false;
176
+ return text.slice(span.openIdx, span.closeIdx).includes(elementLine);
177
+ }
178
+ /** Insert `elementLine` (already including its own trailing "\n") as the last element of the array literal opened right after `anchorLiteral`. */
179
+ function insertAsLastArrayElement(text, anchorLiteral, elementLine) {
180
+ const span = findArraySpan(text, anchorLiteral);
181
+ if (span === null)
182
+ return null;
183
+ const { openIdx, closeIdx } = span;
184
+ const body = text.slice(openIdx + 1, closeIdx);
185
+ const trailingMatch = /\n[ \t]*$/.exec(body);
186
+ const trailingWs = trailingMatch ? trailingMatch[0] : "\n";
187
+ const bodyCore = trailingMatch ? body.slice(0, -trailingWs.length) : body;
188
+ const newBody = `${bodyCore}\n${elementLine.replace(/\n$/, "")}${trailingWs}`;
189
+ return text.slice(0, openIdx + 1) + newBody + text.slice(closeIdx);
190
+ }
191
+ /** Insert `entryText` (including its own trailing "\n") immediately after the line containing `anchorLiteral`. */
192
+ function insertAfterAnchorLine(text, anchorLiteral, entryText) {
193
+ const anchorIdx = text.indexOf(anchorLiteral);
194
+ if (anchorIdx === -1)
195
+ return null;
196
+ const lineEnd = text.indexOf("\n", anchorIdx);
197
+ if (lineEnd === -1)
198
+ return null;
199
+ return text.slice(0, lineEnd + 1) + entryText + text.slice(lineEnd + 1);
200
+ }
201
+ function applyAddTranspilePackage(target, text, op) {
202
+ const elementLine = ` "${op.package}",\n`;
203
+ if (arrayContainsElement(text, "transpilePackages: [", elementLine))
204
+ return text; // already applied — idempotent no-op.
205
+ const next = insertAsLastArrayElement(text, "transpilePackages: [", elementLine);
206
+ if (next === null)
207
+ throw unparseableAnchor(target, "transpilePackages: [");
208
+ return next;
209
+ }
210
+ function applyWrapExportWith(target, text, op) {
211
+ let next = text;
212
+ if (!next.includes(op.importLine)) {
213
+ const re = /^import\b.*\n/gm;
214
+ let lastEnd = -1;
215
+ let m;
216
+ while ((m = re.exec(next)))
217
+ lastEnd = m.index + m[0].length;
218
+ if (lastEnd === -1)
219
+ throw unparseableAnchor(target, op.importLine.trimEnd());
220
+ next = next.slice(0, lastEnd) + op.importLine + next.slice(lastEnd);
221
+ }
222
+ if (next.includes(op.replaceWith))
223
+ return next; // already applied — idempotent no-op.
224
+ if (!next.includes(op.find))
225
+ throw unparseableAnchor(target, op.find);
226
+ return next.replace(op.find, op.replaceWith);
227
+ }
228
+ function applyAddTransformIgnorePattern(target, text, op) {
229
+ if (text.includes(op.replaceWith))
230
+ return text; // already applied — idempotent no-op.
231
+ if (!text.includes(op.find))
232
+ throw unparseableAnchor(target, op.find);
233
+ return text.replace(op.find, op.replaceWith);
234
+ }
235
+ /**
236
+ * Shared by add-module-name-mapper / add-transform: a keyed entry inside an
237
+ * object literal. `renderedValue` is the exact FILE-TEXT form of the wanted
238
+ * value (quoted for a mapper's string value; verbatim for a transform's raw
239
+ * source text) — used only for the idempotence text comparison and the
240
+ * insertion. `plainWantedValue` is the LOGICAL value — used for the D4
241
+ * class-1 message, which JSON.stringifies it itself (D4's `<newValueJSON>`
242
+ * shape); passing the already-quoted `renderedValue` there would double-quote it.
243
+ * `unwrapQuotes` strips one layer of literal `"…"` off the captured
244
+ * existing-file text before it goes into the message, for the same reason —
245
+ * on for add-module-name-mapper (always a quoted string), off for
246
+ * add-transform (verbatim source, not necessarily quoted at all).
247
+ */
248
+ function applyKeyedEntry(target, text, anchorLiteral, locatorPrefix, pattern, renderedValue, plainWantedValue, unwrapQuotes, entryText) {
249
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
250
+ const keyRe = new RegExp(`"${escaped}":\\s*(.+?),`, "s");
251
+ const match = keyRe.exec(text);
252
+ if (match) {
253
+ const existingRaw = match[1].trim();
254
+ if (existingRaw === renderedValue.trim())
255
+ return text; // already applied — idempotent no-op.
256
+ const plainExisting = unwrapQuotes
257
+ ? existingRaw.replace(/^"([\s\S]*)"$/, "$1")
258
+ : existingRaw;
259
+ throw conflict(target, `${locatorPrefix}["${pattern}"]`, plainExisting, plainWantedValue);
260
+ }
261
+ const next = insertAfterAnchorLine(text, anchorLiteral, entryText);
262
+ if (next === null)
263
+ throw unparseableAnchor(target, anchorLiteral);
264
+ return next;
265
+ }
266
+ function applyAddModuleNameMapper(target, text, op) {
267
+ const renderedValue = `"${op.value}"`;
268
+ const entryText = ` "${op.pattern}":\n ${renderedValue},\n`;
269
+ return applyKeyedEntry(target, text, "moduleNameMapper: {", "moduleNameMapper", op.pattern, renderedValue, op.value, true, entryText);
270
+ }
271
+ function applyAddTransform(target, text, op) {
272
+ const entryText = ` "${op.pattern}": ${op.value},\n`;
273
+ return applyKeyedEntry(target, text, "transform: {", "transform", op.pattern, op.value, op.value, false, entryText);
274
+ }
275
+ function applyTextOps(target, text, ops) {
276
+ let next = text;
277
+ for (const op of ops) {
278
+ if (op.op === "add-transpile-package")
279
+ next = applyAddTranspilePackage(target, next, op);
280
+ else if (op.op === "wrap-export-with")
281
+ next = applyWrapExportWith(target, next, op);
282
+ else if (op.op === "add-transform-ignore-pattern")
283
+ next = applyAddTransformIgnorePattern(target, next, op);
284
+ else if (op.op === "add-module-name-mapper")
285
+ next = applyAddModuleNameMapper(target, next, op);
286
+ else if (op.op === "add-transform")
287
+ next = applyAddTransform(target, next, op);
288
+ else {
289
+ throw new Error(`invalid module manifest: unknown patch op "${op.op}" for target ${target}`);
290
+ }
291
+ }
292
+ return next;
293
+ }
294
+ /**
295
+ * Compute every patched target's new content in memory — throws on the
296
+ * first D4 class 1-3 failure encountered (deterministic order — targets in
297
+ * `patches` array order) and writes nothing anywhere; returns only the
298
+ * targets whose content actually changed. Pure with respect to the
299
+ * filesystem: reads go through `options.readTarget`, nothing is written.
300
+ * Pair with `writeModulePatches` for the second pass, deferred until every
301
+ * other part of the caller's own operation (e.g. `copyTree`/`installMap`)
302
+ * has ALSO succeeded, so a patch failure can never leave partial output
303
+ * from an earlier step in place either.
304
+ */
305
+ export function computeModulePatches(patches, options) {
306
+ if (patches.length === 0)
307
+ return [];
308
+ // Group by target — a manifest MAY list the same target across multiple
309
+ // ModulePatch entries; every op targeting a file applies, in array order,
310
+ // to that one file's in-memory buffer.
311
+ const byTarget = new Map();
312
+ for (const patch of patches) {
313
+ const existing = byTarget.get(patch.target);
314
+ if (existing)
315
+ existing.ops.push(...patch.ops);
316
+ else
317
+ byTarget.set(patch.target, { kind: patch.kind, ops: [...patch.ops] });
318
+ }
319
+ const computed = [];
320
+ for (const [target, { kind, ops }] of byTarget) {
321
+ const original = options.readTarget(target);
322
+ if (original === undefined)
323
+ throw missingTarget(target);
324
+ let content;
325
+ if (kind === "json") {
326
+ const isPackageJson = basename(target) === "package.json";
327
+ content = applyJsonOps(target, original, ops, options.versions, isPackageJson);
328
+ if (isPackageJson) {
329
+ content = substituteWorkspaceVersions(content, options.versions);
330
+ }
331
+ }
332
+ else if (kind === "next-config" || kind === "jest-config") {
333
+ content = applyTextOps(target, original, ops);
334
+ }
335
+ else {
336
+ throw new Error(`invalid module manifest: unknown patch kind "${kind}" for target ${target}`);
337
+ }
338
+ if (content !== original)
339
+ computed.push({ target, content });
340
+ }
341
+ return computed;
342
+ }
343
+ /** Write every `computeModulePatches` result to disk, resolved against `baseDir`, once. */
344
+ export function writeModulePatches(computed, baseDir) {
345
+ const baseAbs = resolve(baseDir);
346
+ for (const { target, content } of computed) {
347
+ writeFileSync(resolve(baseAbs, target), content);
348
+ }
349
+ }
350
+ /**
351
+ * Apply every patch in `patches` onto the scaffold at `targetDir` — reads
352
+ * every target directly from `targetDir` (no staging tree involved) and
353
+ * writes once everything computes cleanly. The convenience form for a
354
+ * patch set whose targets are ALL pre-existing scaffold files; a caller
355
+ * whose bundle also delivers some of those targets itself (via plain
356
+ * content or `installMap`) should use `computeModulePatches`/
357
+ * `writeModulePatches` directly so a patch failure can roll back the
358
+ * whole merge, not just the patch step.
359
+ */
360
+ export function applyModulePatches(patches, targetDir, options) {
361
+ const targetAbs = resolve(targetDir);
362
+ const readTarget = (target) => {
363
+ const absPath = resolve(targetAbs, target);
364
+ if (!existsSync(absPath))
365
+ return undefined;
366
+ return readFileSync(absPath, "utf8");
367
+ };
368
+ const computed = computeModulePatches(patches, { ...options, readTarget });
369
+ writeModulePatches(computed, targetAbs);
370
+ }
@@ -1,3 +1,4 @@
1
+ import type { WorkspaceVersionMap } from "../substitute.js";
1
2
  export interface ExtractModuleBundleOptions {
2
3
  /** The fetched bundle bytes (a gzipped tar archive). */
3
4
  bytes: Uint8Array;
@@ -5,12 +6,20 @@ export interface ExtractModuleBundleOptions {
5
6
  targetDir: string;
6
7
  /** The module name — used only for error messages and staging dir naming. */
7
8
  moduleName: string;
9
+ /**
10
+ * Canonical `@working-theory/*` package versions (spec 5418 § 3 D2), for
11
+ * `package.json` patch targets' `workspace:*` substitution — the same map
12
+ * `copyTemplate` threads to `substituteWorkspaceVersions`. Defaults to `{}`
13
+ * (a bundle with no `patches`, or none using `workspace:*`, needs nothing).
14
+ */
15
+ versions?: WorkspaceVersionMap;
8
16
  }
9
17
  /**
10
18
  * Extract a fetched module bundle into `targetDir`, merging its files onto
11
19
  * the tree `copyTemplate` already wrote. Throws on any unsafe entry,
12
- * decompress error, or limit overrun `targetDir` is left untouched on
13
- * failure (validation completes in a throwaway staging dir before any file
14
- * is copied into place).
20
+ * decompress error, limit overrun, invalid `installMap`, or `patches`
21
+ * failure (D4) `targetDir` is left untouched on failure in every one of
22
+ * those cases: validation AND patch computation both complete before any
23
+ * file is copied or written into place.
15
24
  */
16
25
  export declare function extractModuleBundle(options: ExtractModuleBundleOptions): Promise<void>;