create-ailk 0.2.0 → 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 (273) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/lib/apply-module-patches.d.ts +47 -0
  3. package/dist/lib/apply-module-patches.js +370 -0
  4. package/dist/lib/extract-module-bundle.d.ts +12 -3
  5. package/dist/lib/extract-module-bundle.js +236 -24
  6. package/dist/lib/fetch-module.d.ts +3 -38
  7. package/dist/lib/fetch-module.js +26 -8
  8. package/dist/lib/module-license-gate.d.ts +2 -0
  9. package/dist/lib/module-license-gate.js +1 -0
  10. package/dist/lib/paid-paths.d.ts +16 -0
  11. package/dist/lib/paid-paths.js +90 -0
  12. package/dist/module-architecture.d.ts +139 -23
  13. package/dist/module-architecture.js +668 -25
  14. package/dist/parse-args.d.ts +2 -2
  15. package/dist/parse-args.js +3 -1
  16. package/dist/programmatic.d.ts +2 -1
  17. package/dist/programmatic.js +15 -4
  18. package/dist/surfaces.d.ts +12 -36
  19. package/dist/surfaces.js +35 -5
  20. package/dist/sync-routes.js +31 -1
  21. package/package.json +2 -2
  22. package/templates/.claude/agents/web.md +2 -3
  23. package/templates/.claude/rules/architecture.md +5 -5
  24. package/templates/.claude/skills/README.md +2 -1
  25. package/templates/.env.example +21 -9
  26. package/templates/CONVENTIONS.md +6 -7
  27. package/templates/apps/api/.env.example +9 -0
  28. package/templates/apps/api/CLAUDE.md +33 -5
  29. package/templates/apps/api/package.json +0 -1
  30. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  31. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  32. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  33. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  34. package/templates/apps/api/src/config/index.ts +19 -0
  35. package/templates/apps/api/src/config/modules.ts +12 -14
  36. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  37. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  38. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  39. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  40. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  41. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  42. package/templates/apps/api/src/openapi/index.ts +9 -3
  43. package/templates/apps/api/src/openapi/spec.ts +505 -65
  44. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  45. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  46. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/index.ts +38 -11
  49. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  50. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  58. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  59. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  60. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  61. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  63. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  64. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  65. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  66. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  67. package/templates/apps/api/src/server.ts +459 -135
  68. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  69. package/templates/apps/api/src/services/listing-config.ts +58 -0
  70. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  71. package/templates/apps/api/src/services/project-listings.ts +229 -4
  72. package/templates/apps/api/src/vercel-handler.ts +60 -26
  73. package/templates/apps/mcp/.env.example +8 -0
  74. package/templates/apps/mcp/CLAUDE.md +2 -2
  75. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  76. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  77. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  78. package/templates/apps/mcp/src/config/modules.ts +14 -13
  79. package/templates/apps/mcp/src/server.ts +8 -4
  80. package/templates/apps/mcp/src/tools/index.ts +7 -22
  81. package/templates/apps/web/.env.example +15 -0
  82. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  83. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  84. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  91. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  92. package/templates/apps/web/jest.config.cjs +14 -13
  93. package/templates/apps/web/next.config.mjs +1 -3
  94. package/templates/apps/web/package.json +0 -6
  95. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  96. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  97. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  98. package/templates/apps/web/public/favicon-16x16.png +0 -0
  99. package/templates/apps/web/public/favicon-32x32.png +0 -0
  100. package/templates/apps/web/public/favicon.ico +0 -0
  101. package/templates/apps/web/public/favicon.svg +6 -3
  102. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  103. package/templates/apps/web/public/logomark.svg +4 -0
  104. package/templates/apps/web/public/site.webmanifest +2 -2
  105. package/templates/apps/web/public/wordmark.svg +4 -0
  106. package/templates/database/CHANGELOG.md +33 -0
  107. package/templates/database/inbox/schema.prisma +67 -0
  108. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  109. package/templates/database/package.json +1 -1
  110. package/templates/package.json +1 -1
  111. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  112. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  113. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  114. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  115. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  116. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  117. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  118. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  119. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  120. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  121. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  122. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  123. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  124. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  125. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  126. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  127. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  128. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  129. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  130. package/templates/apps/api/src/lib/stripe.ts +0 -52
  131. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  132. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  133. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  134. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  135. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  136. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  137. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  138. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  139. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  140. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  141. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  142. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  143. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  144. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  145. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  146. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  147. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  148. package/templates/apps/api/src/routes/flows/README.md +0 -147
  149. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  150. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  151. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  152. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  153. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  154. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  155. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  156. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  157. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  158. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  159. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  160. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  161. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  162. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  163. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  164. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  165. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  166. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  167. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  168. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  169. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  170. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  171. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  172. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  173. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  174. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  175. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  176. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  177. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  178. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  179. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  180. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  181. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  182. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  183. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  184. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  185. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  186. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  187. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  188. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  189. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  190. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  191. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  192. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  193. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  194. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  195. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  196. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  197. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  198. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  199. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  200. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  201. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  202. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  203. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  204. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  205. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  206. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  207. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  208. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  209. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  210. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  211. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  212. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  213. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  214. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  215. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  216. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  217. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  218. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  219. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  220. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  221. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  222. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  223. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  224. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  225. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  226. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  227. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  228. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  229. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  230. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  231. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  232. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  233. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  234. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  235. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  236. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  237. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  238. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  239. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  240. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  241. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  242. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  243. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  246. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  247. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  248. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  249. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  250. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  251. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  252. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  253. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  254. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  255. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  256. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  257. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  258. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  259. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  260. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  261. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  262. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  263. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  264. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  265. package/templates/apps/web/content/en/blog/README.txt +0 -25
  266. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  267. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  268. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  269. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  270. package/templates/apps/web/lib/source.ts +0 -34
  271. package/templates/apps/web/public/logomark-dark.svg +0 -4
  272. package/templates/apps/web/public/logomark-light.svg +0 -4
  273. package/templates/apps/web/source.config.ts +0 -78
@@ -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>;