forgeos 0.1.0-alpha.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 (540) hide show
  1. package/.npmignore +1 -0
  2. package/AGENTS.md +277 -0
  3. package/CHANGELOG.md +8 -0
  4. package/CONTRIBUTING.md +58 -0
  5. package/README.md +377 -0
  6. package/bin/forge-bun.mjs +110 -0
  7. package/bin/forge.mjs +19 -0
  8. package/package.json +96 -0
  9. package/packages/eslint-plugin-forge/index.ts +15 -0
  10. package/packages/eslint-plugin-forge/package.json +10 -0
  11. package/packages/eslint-plugin-forge/src/check-source.ts +95 -0
  12. package/packages/eslint-plugin-forge/src/load-artifacts.ts +24 -0
  13. package/packages/eslint-plugin-forge/src/rule-no-forge-guard-violation.ts +93 -0
  14. package/src/forge/_generated/actionSubscriptions.json +2 -0
  15. package/src/forge/_generated/actionSubscriptions.ts +10 -0
  16. package/src/forge/_generated/agentAdapterManifest.json +2 -0
  17. package/src/forge/_generated/agentAdapterManifest.ts +73 -0
  18. package/src/forge/_generated/agentContract.json +2 -0
  19. package/src/forge/_generated/agentContract.ts +912 -0
  20. package/src/forge/_generated/agentQuickstart.md +32 -0
  21. package/src/forge/_generated/aiContext.ts +59 -0
  22. package/src/forge/_generated/aiModels.json +2 -0
  23. package/src/forge/_generated/aiModels.ts +35 -0
  24. package/src/forge/_generated/aiProviders.json +2 -0
  25. package/src/forge/_generated/aiProviders.ts +23 -0
  26. package/src/forge/_generated/aiRegistry.json +2 -0
  27. package/src/forge/_generated/aiRegistry.ts +29 -0
  28. package/src/forge/_generated/api.json +2 -0
  29. package/src/forge/_generated/api.ts +8 -0
  30. package/src/forge/_generated/appGraph.json +2 -0
  31. package/src/forge/_generated/appGraph.ts +14511 -0
  32. package/src/forge/_generated/appMap.md +35 -0
  33. package/src/forge/_generated/artifactManifest.json +2 -0
  34. package/src/forge/_generated/artifactManifest.ts +7 -0
  35. package/src/forge/_generated/authClaims.json +2 -0
  36. package/src/forge/_generated/authClaims.ts +13 -0
  37. package/src/forge/_generated/authConfig.json +2 -0
  38. package/src/forge/_generated/authConfig.ts +17 -0
  39. package/src/forge/_generated/authContext.ts +23 -0
  40. package/src/forge/_generated/authRegistry.json +2 -0
  41. package/src/forge/_generated/authRegistry.ts +25 -0
  42. package/src/forge/_generated/buildInfo.json +2 -0
  43. package/src/forge/_generated/buildInfo.ts +9 -0
  44. package/src/forge/_generated/capabilityMap.json +2 -0
  45. package/src/forge/_generated/capabilityMap.md +15 -0
  46. package/src/forge/_generated/capabilityMap.ts +17 -0
  47. package/src/forge/_generated/client.ts +282 -0
  48. package/src/forge/_generated/clientApi.ts +9 -0
  49. package/src/forge/_generated/clientManifest.json +2 -0
  50. package/src/forge/_generated/clientManifest.ts +39 -0
  51. package/src/forge/_generated/clientTypes.ts +78 -0
  52. package/src/forge/_generated/configRegistry.json +2 -0
  53. package/src/forge/_generated/configRegistry.ts +4 -0
  54. package/src/forge/_generated/dataGraph.json +2 -0
  55. package/src/forge/_generated/dataGraph.ts +8 -0
  56. package/src/forge/_generated/db.json +2 -0
  57. package/src/forge/_generated/db.ts +2 -0
  58. package/src/forge/_generated/dbSecurityManifest.json +2 -0
  59. package/src/forge/_generated/dbSecurityManifest.ts +15 -0
  60. package/src/forge/_generated/dbSessionContext.json +2 -0
  61. package/src/forge/_generated/dbSessionContext.ts +39 -0
  62. package/src/forge/_generated/deployManifest.json +2 -0
  63. package/src/forge/_generated/deployManifest.ts +14 -0
  64. package/src/forge/_generated/devManifest.json +2 -0
  65. package/src/forge/_generated/devManifest.ts +47 -0
  66. package/src/forge/_generated/envSchema.json +2 -0
  67. package/src/forge/_generated/envSchema.ts +59 -0
  68. package/src/forge/_generated/frontendGraph.json +2 -0
  69. package/src/forge/_generated/frontendGraph.ts +27 -0
  70. package/src/forge/_generated/importGuards.json +2 -0
  71. package/src/forge/_generated/importGuards.ts +652 -0
  72. package/src/forge/_generated/index.ts +67 -0
  73. package/src/forge/_generated/liveProductionManifest.json +2 -0
  74. package/src/forge/_generated/liveProductionManifest.ts +23 -0
  75. package/src/forge/_generated/liveProtocol.json +2 -0
  76. package/src/forge/_generated/liveProtocol.ts +21 -0
  77. package/src/forge/_generated/liveQueryRegistry.json +2 -0
  78. package/src/forge/_generated/liveQueryRegistry.ts +9 -0
  79. package/src/forge/_generated/liveTransportConfig.json +2 -0
  80. package/src/forge/_generated/liveTransportConfig.ts +19 -0
  81. package/src/forge/_generated/makeRegistry.json +2 -0
  82. package/src/forge/_generated/makeRegistry.ts +163 -0
  83. package/src/forge/_generated/makeTemplates.json +2 -0
  84. package/src/forge/_generated/makeTemplates.ts +61 -0
  85. package/src/forge/_generated/mockMap.json +2 -0
  86. package/src/forge/_generated/mockMap.ts +7 -0
  87. package/src/forge/_generated/operationPlaybooks.md +145 -0
  88. package/src/forge/_generated/packageGraph.json +2 -0
  89. package/src/forge/_generated/packageGraph.ts +168569 -0
  90. package/src/forge/_generated/packageUpgradeRegistry.json +2 -0
  91. package/src/forge/_generated/packageUpgradeRegistry.ts +15 -0
  92. package/src/forge/_generated/permissionMatrix.json +2 -0
  93. package/src/forge/_generated/permissionMatrix.ts +7 -0
  94. package/src/forge/_generated/policyRegistry.json +2 -0
  95. package/src/forge/_generated/policyRegistry.ts +11 -0
  96. package/src/forge/_generated/queryRegistry.json +2 -0
  97. package/src/forge/_generated/queryRegistry.ts +9 -0
  98. package/src/forge/_generated/react.d.ts +22 -0
  99. package/src/forge/_generated/react.ts +29 -0
  100. package/src/forge/_generated/reactManifest.json +2 -0
  101. package/src/forge/_generated/reactManifest.ts +19 -0
  102. package/src/forge/_generated/releaseManifest.json +2 -0
  103. package/src/forge/_generated/releaseManifest.ts +25 -0
  104. package/src/forge/_generated/rlsPolicies.json +2 -0
  105. package/src/forge/_generated/rlsPolicies.sql +34 -0
  106. package/src/forge/_generated/rlsPolicies.ts +6 -0
  107. package/src/forge/_generated/runtimeGraph.json +2 -0
  108. package/src/forge/_generated/runtimeGraph.ts +8 -0
  109. package/src/forge/_generated/runtimeMatrix.json +2 -0
  110. package/src/forge/_generated/runtimeMatrix.ts +229125 -0
  111. package/src/forge/_generated/runtimeRegistry.ts +2 -0
  112. package/src/forge/_generated/runtimeRules.md +79 -0
  113. package/src/forge/_generated/secretRegistry.json +2 -0
  114. package/src/forge/_generated/secretRegistry.ts +50 -0
  115. package/src/forge/_generated/secretsContext.ts +11 -0
  116. package/src/forge/_generated/serverApi.ts +10 -0
  117. package/src/forge/_generated/sourceMapManifest.json +2 -0
  118. package/src/forge/_generated/sourceMapManifest.ts +7 -0
  119. package/src/forge/_generated/sqlPlan.json +2 -0
  120. package/src/forge/_generated/sqlPlan.ts +88 -0
  121. package/src/forge/_generated/subscriptionManifest.json +2 -0
  122. package/src/forge/_generated/subscriptionManifest.ts +7 -0
  123. package/src/forge/_generated/symbolicationManifest.json +2 -0
  124. package/src/forge/_generated/symbolicationManifest.ts +17 -0
  125. package/src/forge/_generated/telemetryRegistry.json +2 -0
  126. package/src/forge/_generated/telemetryRegistry.ts +9 -0
  127. package/src/forge/_generated/telemetrySinks.json +2 -0
  128. package/src/forge/_generated/telemetrySinks.ts +11 -0
  129. package/src/forge/_generated/tenantScope.json +2 -0
  130. package/src/forge/_generated/tenantScope.ts +8 -0
  131. package/src/forge/_generated/testGraph.json +2 -0
  132. package/src/forge/_generated/testGraph.ts +3054 -0
  133. package/src/forge/_generated/testPlanRegistry.json +2 -0
  134. package/src/forge/_generated/testPlanRegistry.ts +33 -0
  135. package/src/forge/_generated/uiRoutes.json +2 -0
  136. package/src/forge/_generated/uiRoutes.ts +16 -0
  137. package/src/forge/_generated/uiScenarios.json +2 -0
  138. package/src/forge/_generated/uiScenarios.ts +30 -0
  139. package/src/forge/_generated/uiTestManifest.json +2 -0
  140. package/src/forge/_generated/uiTestManifest.ts +27 -0
  141. package/src/forge/_generated/workflowRegistry.json +2 -0
  142. package/src/forge/_generated/workflowRegistry.ts +9 -0
  143. package/src/forge/_generated/workflowSubscriptions.json +2 -0
  144. package/src/forge/_generated/workflowSubscriptions.ts +10 -0
  145. package/src/forge/agent-adapters/index.ts +1002 -0
  146. package/src/forge/agent-adapters/types.ts +135 -0
  147. package/src/forge/cli/agent-contract.ts +50 -0
  148. package/src/forge/cli/ai.ts +148 -0
  149. package/src/forge/cli/auth.ts +198 -0
  150. package/src/forge/cli/build.ts +105 -0
  151. package/src/forge/cli/bun-exec.ts +4 -0
  152. package/src/forge/cli/commands.ts +1130 -0
  153. package/src/forge/cli/db.ts +316 -0
  154. package/src/forge/cli/deps.ts +277 -0
  155. package/src/forge/cli/dev.ts +529 -0
  156. package/src/forge/cli/doctor.ts +209 -0
  157. package/src/forge/cli/feature.ts +485 -0
  158. package/src/forge/cli/index.ts +25 -0
  159. package/src/forge/cli/lint-forge.ts +119 -0
  160. package/src/forge/cli/live.ts +179 -0
  161. package/src/forge/cli/main.ts +92 -0
  162. package/src/forge/cli/make.ts +133 -0
  163. package/src/forge/cli/new.ts +505 -0
  164. package/src/forge/cli/outbox.ts +297 -0
  165. package/src/forge/cli/output.ts +114 -0
  166. package/src/forge/cli/parse.ts +2211 -0
  167. package/src/forge/cli/policy.ts +204 -0
  168. package/src/forge/cli/query.ts +91 -0
  169. package/src/forge/cli/refactor.ts +221 -0
  170. package/src/forge/cli/release.ts +285 -0
  171. package/src/forge/cli/rls.ts +322 -0
  172. package/src/forge/cli/run.ts +76 -0
  173. package/src/forge/cli/secrets.ts +274 -0
  174. package/src/forge/cli/self-host.ts +468 -0
  175. package/src/forge/cli/serve.ts +93 -0
  176. package/src/forge/cli/telemetry.ts +219 -0
  177. package/src/forge/cli/verify.ts +587 -0
  178. package/src/forge/cli/version.ts +1 -0
  179. package/src/forge/cli/windows.ts +413 -0
  180. package/src/forge/cli/worker.ts +87 -0
  181. package/src/forge/cli/workflow.ts +424 -0
  182. package/src/forge/compiler/action-subscriptions/build.ts +116 -0
  183. package/src/forge/compiler/action-subscriptions/constants.ts +2 -0
  184. package/src/forge/compiler/action-subscriptions/index.ts +6 -0
  185. package/src/forge/compiler/action-subscriptions/parse.ts +6 -0
  186. package/src/forge/compiler/agent-contract/build.ts +1651 -0
  187. package/src/forge/compiler/agent-contract/types.ts +326 -0
  188. package/src/forge/compiler/ai-registry/build.ts +165 -0
  189. package/src/forge/compiler/ai-registry/constants.ts +2 -0
  190. package/src/forge/compiler/ai-registry/parse.ts +56 -0
  191. package/src/forge/compiler/api-surface/build.ts +107 -0
  192. package/src/forge/compiler/app-graph/build.ts +121 -0
  193. package/src/forge/compiler/app-graph/classify.ts +10 -0
  194. package/src/forge/compiler/app-graph/dup-symbol.ts +29 -0
  195. package/src/forge/compiler/app-graph/extract.ts +124 -0
  196. package/src/forge/compiler/app-graph/forge-apis.ts +29 -0
  197. package/src/forge/compiler/app-graph/index.ts +15 -0
  198. package/src/forge/compiler/app-graph/module-graph.ts +320 -0
  199. package/src/forge/compiler/app-graph/parser.ts +119 -0
  200. package/src/forge/compiler/app-graph/symbols.ts +48 -0
  201. package/src/forge/compiler/app-graph/tsconfig-hash.ts +62 -0
  202. package/src/forge/compiler/app-graph/types.ts +43 -0
  203. package/src/forge/compiler/app-graph/versions.ts +14 -0
  204. package/src/forge/compiler/cache/index.ts +17 -0
  205. package/src/forge/compiler/cache/key.ts +46 -0
  206. package/src/forge/compiler/cache/scheduler.ts +72 -0
  207. package/src/forge/compiler/cache/store.ts +78 -0
  208. package/src/forge/compiler/classifier/capabilities.ts +78 -0
  209. package/src/forge/compiler/classifier/classify.ts +113 -0
  210. package/src/forge/compiler/classifier/contexts.ts +188 -0
  211. package/src/forge/compiler/classifier/index.ts +18 -0
  212. package/src/forge/compiler/classifier/runtime-matrix.ts +45 -0
  213. package/src/forge/compiler/classifier/secrets.ts +41 -0
  214. package/src/forge/compiler/classifier/signals.ts +129 -0
  215. package/src/forge/compiler/client-sdk/build-manifest.ts +151 -0
  216. package/src/forge/compiler/client-sdk/render-client.ts +432 -0
  217. package/src/forge/compiler/data-graph/build.ts +131 -0
  218. package/src/forge/compiler/data-graph/constants.ts +5 -0
  219. package/src/forge/compiler/data-graph/index.ts +6 -0
  220. package/src/forge/compiler/data-graph/parse.ts +176 -0
  221. package/src/forge/compiler/data-graph/rls/build.ts +222 -0
  222. package/src/forge/compiler/data-graph/rls/types.ts +62 -0
  223. package/src/forge/compiler/data-graph/sql/ddl.ts +390 -0
  224. package/src/forge/compiler/data-graph/sql/naming.ts +10 -0
  225. package/src/forge/compiler/data-graph/sql/serialize.ts +85 -0
  226. package/src/forge/compiler/data-graph/sql/types.ts +37 -0
  227. package/src/forge/compiler/dev-manifest/build.ts +170 -0
  228. package/src/forge/compiler/dev-manifest/constants.ts +5 -0
  229. package/src/forge/compiler/diagnostics/codes.ts +611 -0
  230. package/src/forge/compiler/diagnostics/create.ts +245 -0
  231. package/src/forge/compiler/diagnostics/index.ts +55 -0
  232. package/src/forge/compiler/emitter/artifact-kind.ts +14 -0
  233. package/src/forge/compiler/emitter/barrel.ts +44 -0
  234. package/src/forge/compiler/emitter/constants.ts +7 -0
  235. package/src/forge/compiler/emitter/emit.ts +237 -0
  236. package/src/forge/compiler/emitter/index.ts +24 -0
  237. package/src/forge/compiler/emitter/lock.ts +62 -0
  238. package/src/forge/compiler/emitter/render.ts +73 -0
  239. package/src/forge/compiler/emitter/write.ts +35 -0
  240. package/src/forge/compiler/frontend-graph/build.ts +495 -0
  241. package/src/forge/compiler/fs/index.ts +23 -0
  242. package/src/forge/compiler/fs/memory.ts +233 -0
  243. package/src/forge/compiler/fs/node.ts +139 -0
  244. package/src/forge/compiler/fs/profile.ts +108 -0
  245. package/src/forge/compiler/fs/types.ts +52 -0
  246. package/src/forge/compiler/guards/artifacts.ts +96 -0
  247. package/src/forge/compiler/guards/check-ai-usage.ts +98 -0
  248. package/src/forge/compiler/guards/check-import-guards.ts +106 -0
  249. package/src/forge/compiler/guards/check-process-env.ts +98 -0
  250. package/src/forge/compiler/guards/check-query-usage.ts +76 -0
  251. package/src/forge/compiler/guards/index.ts +11 -0
  252. package/src/forge/compiler/guards/propagate-contexts.ts +57 -0
  253. package/src/forge/compiler/index.ts +17 -0
  254. package/src/forge/compiler/integration/add.ts +496 -0
  255. package/src/forge/compiler/integration/index.ts +17 -0
  256. package/src/forge/compiler/integration/plan.ts +283 -0
  257. package/src/forge/compiler/integration/render.ts +189 -0
  258. package/src/forge/compiler/integration/snapshot.ts +52 -0
  259. package/src/forge/compiler/integration/templates/ai.ts +131 -0
  260. package/src/forge/compiler/integration/templates/index.ts +8 -0
  261. package/src/forge/compiler/integration/templates/posthog.ts +145 -0
  262. package/src/forge/compiler/integration/templates/render.ts +113 -0
  263. package/src/forge/compiler/integration/templates/sentry.ts +151 -0
  264. package/src/forge/compiler/integration/templates/stripe.ts +109 -0
  265. package/src/forge/compiler/integration/templates/types.ts +14 -0
  266. package/src/forge/compiler/integration/templates/zod.ts +55 -0
  267. package/src/forge/compiler/live-production/types.ts +122 -0
  268. package/src/forge/compiler/live-query-registry/build.ts +150 -0
  269. package/src/forge/compiler/live-query-registry/constants.ts +2 -0
  270. package/src/forge/compiler/make-registry/build.ts +179 -0
  271. package/src/forge/compiler/orchestrator/discover.ts +214 -0
  272. package/src/forge/compiler/orchestrator/fast-check.ts +117 -0
  273. package/src/forge/compiler/orchestrator/generate-lock.ts +138 -0
  274. package/src/forge/compiler/orchestrator/guards.ts +5 -0
  275. package/src/forge/compiler/orchestrator/index.ts +27 -0
  276. package/src/forge/compiler/orchestrator/manifest-hashes.ts +21 -0
  277. package/src/forge/compiler/orchestrator/manifest.ts +92 -0
  278. package/src/forge/compiler/orchestrator/orphans.ts +51 -0
  279. package/src/forge/compiler/orchestrator/plan.ts +876 -0
  280. package/src/forge/compiler/orchestrator/profile.ts +36 -0
  281. package/src/forge/compiler/orchestrator/run.ts +277 -0
  282. package/src/forge/compiler/orchestrator/serialize.ts +886 -0
  283. package/src/forge/compiler/orchestrator/session.ts +96 -0
  284. package/src/forge/compiler/orchestrator/types.ts +31 -0
  285. package/src/forge/compiler/orchestrator/verify.ts +38 -0
  286. package/src/forge/compiler/orchestrator/workspace-index.ts +154 -0
  287. package/src/forge/compiler/package-graph/capabilities-stub.ts +33 -0
  288. package/src/forge/compiler/package-graph/checksum.ts +97 -0
  289. package/src/forge/compiler/package-graph/compiler.ts +392 -0
  290. package/src/forge/compiler/package-graph/constants.ts +4 -0
  291. package/src/forge/compiler/package-graph/dts-extractor.ts +142 -0
  292. package/src/forge/compiler/package-graph/exports-discovery.ts +84 -0
  293. package/src/forge/compiler/package-graph/extract-dts.ts +32 -0
  294. package/src/forge/compiler/package-graph/index.ts +33 -0
  295. package/src/forge/compiler/package-graph/jsdoc.ts +62 -0
  296. package/src/forge/compiler/package-graph/read-file.ts +21 -0
  297. package/src/forge/compiler/package-graph/resolve.ts +127 -0
  298. package/src/forge/compiler/package-manager/adapter.ts +237 -0
  299. package/src/forge/compiler/package-manager/bun-executable.ts +92 -0
  300. package/src/forge/compiler/package-manager/commands.ts +47 -0
  301. package/src/forge/compiler/package-manager/detect.ts +79 -0
  302. package/src/forge/compiler/package-manager/executor.ts +117 -0
  303. package/src/forge/compiler/package-manager/index.ts +22 -0
  304. package/src/forge/compiler/package-manager/parse-spec.ts +16 -0
  305. package/src/forge/compiler/package-manager/version.ts +27 -0
  306. package/src/forge/compiler/package-upgrades/apply.ts +195 -0
  307. package/src/forge/compiler/package-upgrades/comparator.ts +181 -0
  308. package/src/forge/compiler/package-upgrades/impact.ts +139 -0
  309. package/src/forge/compiler/package-upgrades/markdown.ts +97 -0
  310. package/src/forge/compiler/package-upgrades/planner.ts +532 -0
  311. package/src/forge/compiler/package-upgrades/risk.ts +208 -0
  312. package/src/forge/compiler/package-upgrades/types.ts +174 -0
  313. package/src/forge/compiler/policy-registry/build.ts +266 -0
  314. package/src/forge/compiler/policy-registry/constants.ts +2 -0
  315. package/src/forge/compiler/policy-registry/parse.ts +81 -0
  316. package/src/forge/compiler/primitives/compare.ts +26 -0
  317. package/src/forge/compiler/primitives/hash.ts +40 -0
  318. package/src/forge/compiler/primitives/header.ts +45 -0
  319. package/src/forge/compiler/primitives/index.ts +45 -0
  320. package/src/forge/compiler/primitives/paths.ts +24 -0
  321. package/src/forge/compiler/primitives/result.ts +164 -0
  322. package/src/forge/compiler/primitives/serialize.ts +66 -0
  323. package/src/forge/compiler/primitives/sort.ts +87 -0
  324. package/src/forge/compiler/query-registry/build.ts +114 -0
  325. package/src/forge/compiler/query-registry/constants.ts +2 -0
  326. package/src/forge/compiler/recipes/definitions.ts +289 -0
  327. package/src/forge/compiler/recipes/helpers.ts +37 -0
  328. package/src/forge/compiler/recipes/index.ts +21 -0
  329. package/src/forge/compiler/recipes/registry.ts +102 -0
  330. package/src/forge/compiler/release/build.ts +100 -0
  331. package/src/forge/compiler/release/types.ts +119 -0
  332. package/src/forge/compiler/runtime-graph/build.ts +137 -0
  333. package/src/forge/compiler/runtime-graph/constants.ts +5 -0
  334. package/src/forge/compiler/runtime-graph/index.ts +5 -0
  335. package/src/forge/compiler/sandbox/artifact-sanitize.ts +26 -0
  336. package/src/forge/compiler/sandbox/backends/child.ts +123 -0
  337. package/src/forge/compiler/sandbox/backends/docker.ts +173 -0
  338. package/src/forge/compiler/sandbox/index.ts +51 -0
  339. package/src/forge/compiler/sandbox/inspect.ts +143 -0
  340. package/src/forge/compiler/sandbox/inspector-entry.ts +115 -0
  341. package/src/forge/compiler/sandbox/limits.ts +31 -0
  342. package/src/forge/compiler/sandbox/scrub-env.ts +60 -0
  343. package/src/forge/compiler/sandbox/secret-scan.ts +54 -0
  344. package/src/forge/compiler/sandbox/serialize.ts +106 -0
  345. package/src/forge/compiler/sandbox/types.ts +7 -0
  346. package/src/forge/compiler/secret-registry/build.ts +123 -0
  347. package/src/forge/compiler/telemetry-registry/build.ts +89 -0
  348. package/src/forge/compiler/telemetry-registry/constants.ts +2 -0
  349. package/src/forge/compiler/telemetry-registry/parse.ts +13 -0
  350. package/src/forge/compiler/test-graph/build.ts +277 -0
  351. package/src/forge/compiler/types/action-subscriptions.ts +19 -0
  352. package/src/forge/compiler/types/ai-registry.ts +33 -0
  353. package/src/forge/compiler/types/app-graph.ts +80 -0
  354. package/src/forge/compiler/types/capability.ts +29 -0
  355. package/src/forge/compiler/types/classification.ts +9 -0
  356. package/src/forge/compiler/types/cli.ts +159 -0
  357. package/src/forge/compiler/types/data-graph.ts +24 -0
  358. package/src/forge/compiler/types/dev-manifest.ts +41 -0
  359. package/src/forge/compiler/types/diagnostic.ts +12 -0
  360. package/src/forge/compiler/types/emit.ts +25 -0
  361. package/src/forge/compiler/types/frontend-graph.ts +81 -0
  362. package/src/forge/compiler/types/import-guards.ts +19 -0
  363. package/src/forge/compiler/types/index.ts +98 -0
  364. package/src/forge/compiler/types/integration.ts +25 -0
  365. package/src/forge/compiler/types/json.ts +3 -0
  366. package/src/forge/compiler/types/live-query-registry.ts +32 -0
  367. package/src/forge/compiler/types/lock.ts +37 -0
  368. package/src/forge/compiler/types/package-graph.ts +84 -0
  369. package/src/forge/compiler/types/policy-registry.ts +69 -0
  370. package/src/forge/compiler/types/query-registry.ts +18 -0
  371. package/src/forge/compiler/types/runtime-graph.ts +30 -0
  372. package/src/forge/compiler/types/runtime-matrix.ts +16 -0
  373. package/src/forge/compiler/types/runtime.ts +30 -0
  374. package/src/forge/compiler/types/sandbox.ts +24 -0
  375. package/src/forge/compiler/types/secret-registry.ts +38 -0
  376. package/src/forge/compiler/types/telemetry-registry.ts +26 -0
  377. package/src/forge/compiler/types/test-graph.ts +45 -0
  378. package/src/forge/compiler/types/workflow-registry.ts +42 -0
  379. package/src/forge/compiler/workflow-registry/build.ts +180 -0
  380. package/src/forge/compiler/workflow-registry/constants.ts +2 -0
  381. package/src/forge/compiler/workflow-registry/index.ts +5 -0
  382. package/src/forge/compiler/workflow-registry/parse.ts +19 -0
  383. package/src/forge/dev/server.ts +1379 -0
  384. package/src/forge/dev/types.ts +49 -0
  385. package/src/forge/dev/watch.ts +109 -0
  386. package/src/forge/dev-console/cycle.ts +652 -0
  387. package/src/forge/dev-console/types.ts +99 -0
  388. package/src/forge/feature/compiler.ts +656 -0
  389. package/src/forge/feature/examples.ts +125 -0
  390. package/src/forge/feature/types.ts +177 -0
  391. package/src/forge/impact/index.ts +1160 -0
  392. package/src/forge/impact/types.ts +151 -0
  393. package/src/forge/intent/index.ts +490 -0
  394. package/src/forge/intent/types.ts +73 -0
  395. package/src/forge/make/fields.ts +146 -0
  396. package/src/forge/make/index.ts +1101 -0
  397. package/src/forge/make/naming.ts +42 -0
  398. package/src/forge/make/templates.ts +525 -0
  399. package/src/forge/make/types.ts +151 -0
  400. package/src/forge/platform/module.ts +20 -0
  401. package/src/forge/policy.ts +1 -0
  402. package/src/forge/react/index.ts +418 -0
  403. package/src/forge/refactor/index.ts +1936 -0
  404. package/src/forge/refactor/text-utils.ts +34 -0
  405. package/src/forge/refactor/types.ts +191 -0
  406. package/src/forge/refactor/workspace-fs.ts +171 -0
  407. package/src/forge/repair/index.ts +656 -0
  408. package/src/forge/repair/rules/index.ts +476 -0
  409. package/src/forge/repair/types.ts +175 -0
  410. package/src/forge/review/index.ts +992 -0
  411. package/src/forge/review/types.ts +196 -0
  412. package/src/forge/runtime/ai/check.ts +86 -0
  413. package/src/forge/runtime/ai/context.ts +394 -0
  414. package/src/forge/runtime/ai/cost-estimator.ts +41 -0
  415. package/src/forge/runtime/ai/mock.ts +49 -0
  416. package/src/forge/runtime/ai/providers.ts +78 -0
  417. package/src/forge/runtime/ai/state.ts +17 -0
  418. package/src/forge/runtime/ai/types.ts +67 -0
  419. package/src/forge/runtime/auth/authenticate.ts +58 -0
  420. package/src/forge/runtime/auth/claims.ts +119 -0
  421. package/src/forge/runtime/auth/config.ts +148 -0
  422. package/src/forge/runtime/auth/errors.ts +45 -0
  423. package/src/forge/runtime/auth/evaluate.ts +126 -0
  424. package/src/forge/runtime/auth/resolve.ts +74 -0
  425. package/src/forge/runtime/auth/types.ts +87 -0
  426. package/src/forge/runtime/auth/verifier.ts +138 -0
  427. package/src/forge/runtime/context/create-context.ts +204 -0
  428. package/src/forge/runtime/context/create-query-context.ts +34 -0
  429. package/src/forge/runtime/db/adapter.ts +31 -0
  430. package/src/forge/runtime/db/factory.ts +83 -0
  431. package/src/forge/runtime/db/generated-client.ts +294 -0
  432. package/src/forge/runtime/db/memory-adapter.ts +706 -0
  433. package/src/forge/runtime/db/migrate.ts +132 -0
  434. package/src/forge/runtime/db/outbox.ts +54 -0
  435. package/src/forge/runtime/db/pglite-adapter.ts +51 -0
  436. package/src/forge/runtime/db/postgres-adapter.ts +112 -0
  437. package/src/forge/runtime/db/read-only-client.ts +97 -0
  438. package/src/forge/runtime/db/session-context.ts +62 -0
  439. package/src/forge/runtime/executor.ts +446 -0
  440. package/src/forge/runtime/live/dependency-tracker.ts +57 -0
  441. package/src/forge/runtime/live/invalidation-log.ts +189 -0
  442. package/src/forge/runtime/live/live-query-runner.ts +267 -0
  443. package/src/forge/runtime/live/registry.ts +28 -0
  444. package/src/forge/runtime/live/sse.ts +75 -0
  445. package/src/forge/runtime/live/subscription-manager.ts +443 -0
  446. package/src/forge/runtime/live/types.ts +143 -0
  447. package/src/forge/runtime/outbox/claim.ts +153 -0
  448. package/src/forge/runtime/outbox/process.ts +298 -0
  449. package/src/forge/runtime/outbox/retry.ts +8 -0
  450. package/src/forge/runtime/outbox/subscriptions.ts +33 -0
  451. package/src/forge/runtime/outbox/types.ts +69 -0
  452. package/src/forge/runtime/policy/check.ts +157 -0
  453. package/src/forge/runtime/policy/load.ts +55 -0
  454. package/src/forge/runtime/query/registry.ts +19 -0
  455. package/src/forge/runtime/query/run-query.ts +347 -0
  456. package/src/forge/runtime/release/runtime.ts +322 -0
  457. package/src/forge/runtime/release/symbolicate.ts +175 -0
  458. package/src/forge/runtime/runner/command-transaction.ts +193 -0
  459. package/src/forge/runtime/runner/run-entry.ts +226 -0
  460. package/src/forge/runtime/secrets/check.ts +78 -0
  461. package/src/forge/runtime/secrets/create-context.ts +138 -0
  462. package/src/forge/runtime/secrets/env-loader.ts +94 -0
  463. package/src/forge/runtime/secrets/runtime-bundle.ts +47 -0
  464. package/src/forge/runtime/secrets/types.ts +31 -0
  465. package/src/forge/runtime/telemetry/buffer.ts +87 -0
  466. package/src/forge/runtime/telemetry/context.ts +192 -0
  467. package/src/forge/runtime/telemetry/correlation.ts +13 -0
  468. package/src/forge/runtime/telemetry/flush.ts +190 -0
  469. package/src/forge/runtime/telemetry/process.ts +20 -0
  470. package/src/forge/runtime/telemetry/scrubber.ts +115 -0
  471. package/src/forge/runtime/telemetry/sinks/local-jsonl.ts +39 -0
  472. package/src/forge/runtime/telemetry/sinks/posthog.ts +64 -0
  473. package/src/forge/runtime/telemetry/sinks/sentry.ts +60 -0
  474. package/src/forge/runtime/telemetry/spans.ts +58 -0
  475. package/src/forge/runtime/telemetry/types.ts +64 -0
  476. package/src/forge/runtime/workflows/cancel.ts +26 -0
  477. package/src/forge/runtime/workflows/create-run.ts +98 -0
  478. package/src/forge/runtime/workflows/process-run.ts +182 -0
  479. package/src/forge/runtime/workflows/process-step.ts +190 -0
  480. package/src/forge/runtime/workflows/process.ts +260 -0
  481. package/src/forge/runtime/workflows/registry.ts +51 -0
  482. package/src/forge/runtime/workflows/resolve-step.ts +46 -0
  483. package/src/forge/runtime/workflows/retry-run.ts +44 -0
  484. package/src/forge/runtime/workflows/retry.ts +8 -0
  485. package/src/forge/runtime/workflows/sanitize.ts +19 -0
  486. package/src/forge/runtime/workflows/start-from-outbox.ts +71 -0
  487. package/src/forge/runtime/workflows/types.ts +77 -0
  488. package/src/forge/server.ts +96 -0
  489. package/src/forge/ui/index.ts +770 -0
  490. package/src/forge/ui/types.ts +191 -0
  491. package/templates/b2b-support-web/.env.example +22 -0
  492. package/templates/b2b-support-web/.vscode/settings.json +14 -0
  493. package/templates/b2b-support-web/AGENTS.md +108 -0
  494. package/templates/b2b-support-web/README.md +48 -0
  495. package/templates/b2b-support-web/forge.config.ts +3 -0
  496. package/templates/b2b-support-web/package.json +34 -0
  497. package/templates/b2b-support-web/src/actions/captureTicketCreated.ts +14 -0
  498. package/templates/b2b-support-web/src/commands/closeTicket.ts +20 -0
  499. package/templates/b2b-support-web/src/commands/createTicket.ts +47 -0
  500. package/templates/b2b-support-web/src/commands/manageBilling.ts +9 -0
  501. package/templates/b2b-support-web/src/forge/schema.ts +35 -0
  502. package/templates/b2b-support-web/src/policies.ts +9 -0
  503. package/templates/b2b-support-web/src/queries/getTicket.ts +6 -0
  504. package/templates/b2b-support-web/src/queries/listTickets.ts +6 -0
  505. package/templates/b2b-support-web/src/queries/liveTickets.ts +9 -0
  506. package/templates/b2b-support-web/src/workflows/triageTicketWorkflow.ts +64 -0
  507. package/templates/b2b-support-web/tsconfig.json +14 -0
  508. package/templates/b2b-support-web/web/app/globals.css +77 -0
  509. package/templates/b2b-support-web/web/app/layout.tsx +13 -0
  510. package/templates/b2b-support-web/web/app/page.tsx +13 -0
  511. package/templates/b2b-support-web/web/app/providers.tsx +21 -0
  512. package/templates/b2b-support-web/web/app/tickets/page.tsx +21 -0
  513. package/templates/b2b-support-web/web/components/CreateTicketForm.tsx +43 -0
  514. package/templates/b2b-support-web/web/components/PolicyDeniedDemo.tsx +31 -0
  515. package/templates/b2b-support-web/web/components/TicketList.tsx +52 -0
  516. package/templates/b2b-support-web/web/components/TraceDetails.tsx +18 -0
  517. package/templates/b2b-support-web/web/components/TriageStatus.tsx +13 -0
  518. package/templates/b2b-support-web/web/lib/forge.ts +13 -0
  519. package/templates/b2b-support-web/web/next-env.d.ts +5 -0
  520. package/templates/b2b-support-web/web/next.config.ts +8 -0
  521. package/templates/b2b-support-web/web/package.json +21 -0
  522. package/templates/b2b-support-web/web/tsconfig.json +30 -0
  523. package/templates/minimal-web/.vscode/settings.json +14 -0
  524. package/templates/minimal-web/README.md +21 -0
  525. package/templates/minimal-web/forge.config.ts +3 -0
  526. package/templates/minimal-web/package.json +32 -0
  527. package/templates/minimal-web/src/actions/logNoteCreated.ts +11 -0
  528. package/templates/minimal-web/src/commands/createNote.ts +26 -0
  529. package/templates/minimal-web/src/forge/schema.ts +12 -0
  530. package/templates/minimal-web/src/policies.ts +6 -0
  531. package/templates/minimal-web/src/queries/listNotes.ts +8 -0
  532. package/templates/minimal-web/src/queries/liveNotes.ts +8 -0
  533. package/templates/minimal-web/tsconfig.json +15 -0
  534. package/templates/minimal-web/web/index.html +12 -0
  535. package/templates/minimal-web/web/package.json +21 -0
  536. package/templates/minimal-web/web/src/App.tsx +89 -0
  537. package/templates/minimal-web/web/src/lib/forge.ts +13 -0
  538. package/templates/minimal-web/web/src/main.tsx +13 -0
  539. package/templates/minimal-web/web/src/styles.css +156 -0
  540. package/templates/minimal-web/web/tsconfig.json +18 -0
@@ -0,0 +1,1101 @@
1
+ import { dirname, normalize, relative, resolve } from "node:path";
2
+ import { nodeFileSystem } from "../compiler/fs/index.ts";
3
+ import type { FileSystem } from "../compiler/fs/index.ts";
4
+ import { createDiagnostic } from "../compiler/diagnostics/create.ts";
5
+ import { GENERATOR_VERSION } from "../compiler/emitter/constants.ts";
6
+ import { hashStable } from "../compiler/primitives/hash.ts";
7
+ import { canonicalJson, serializeCanonical } from "../compiler/primitives/serialize.ts";
8
+ import type { Diagnostic } from "../compiler/types/diagnostic.ts";
9
+ import { parseFieldSpec, parseFields, splitTopLevel } from "./fields.ts";
10
+ import { camelCase, kebabCase, pascalCase, singularize, titleCase } from "./naming.ts";
11
+ import {
12
+ renderAction,
13
+ renderCreateCommand,
14
+ renderCreateForm,
15
+ renderDeleteCommand,
16
+ renderGetQuery,
17
+ renderListComponent,
18
+ renderListQuery,
19
+ renderLiveQuery,
20
+ renderPage,
21
+ renderPlaceholderTest,
22
+ renderPolicyFile,
23
+ renderSchemaTable,
24
+ renderUpdateCommand,
25
+ renderViteApp,
26
+ renderViteIndex,
27
+ renderViteMain,
28
+ renderVitePackage,
29
+ renderViteStyles,
30
+ renderViteTsconfig,
31
+ renderWebBridge,
32
+ renderWebRootBridge,
33
+ renderWorkflow,
34
+ } from "./templates.ts";
35
+ import type {
36
+ MakeCommandOptions,
37
+ MakeFieldSpec,
38
+ MakeIntent,
39
+ MakePlan,
40
+ MakePrimitive,
41
+ MakeResult,
42
+ PlannedFile,
43
+ PlannedPatch,
44
+ } from "./types.ts";
45
+
46
+ export const MAKE_PRIMITIVES: MakePrimitive[] = [
47
+ "list",
48
+ "explain",
49
+ "table",
50
+ "field",
51
+ "policy",
52
+ "command",
53
+ "query",
54
+ "livequery",
55
+ "action",
56
+ "workflow",
57
+ "component",
58
+ "page",
59
+ "ui",
60
+ "resource",
61
+ "apply",
62
+ "rollback",
63
+ ];
64
+
65
+ const AUTHORING_PRIMITIVES = MAKE_PRIMITIVES.filter(
66
+ (primitive) => !["list", "explain", "apply", "rollback"].includes(primitive),
67
+ ) as Array<MakeIntent["kind"]>;
68
+
69
+ const PLAN_DIR = ".forge/make-plans";
70
+
71
+ interface SnapshotFile {
72
+ file: string;
73
+ existed: boolean;
74
+ content?: string;
75
+ }
76
+
77
+ interface MakeSnapshot {
78
+ schemaVersion: "0.1.0";
79
+ planId: string;
80
+ files: SnapshotFile[];
81
+ }
82
+
83
+ const EXPLANATIONS: Record<MakeIntent["kind"], string> = {
84
+ table:
85
+ "Adds a Forge schema table in src/forge/schema.ts. Use --fields for user fields and --tenant-scoped for tenant isolation.",
86
+ field:
87
+ "Adds a field to an existing table. Use table.field as the name or pass --table and --type.",
88
+ policy:
89
+ "Adds a named policy in src/policies.ts using canRole(...roles).",
90
+ command:
91
+ "Adds a transactional command under src/commands with explicit auth and optional ctx.emit.",
92
+ query:
93
+ "Adds a read-only query under src/queries with explicit auth.",
94
+ livequery:
95
+ "Adds a read-only liveQuery under src/queries for reactive clients.",
96
+ action:
97
+ "Adds an after-commit action under src/actions subscribed to an event.",
98
+ workflow:
99
+ "Adds a durable workflow under src/workflows triggered by an event.",
100
+ component:
101
+ "Adds a React client component under web/components wired to generated Forge hooks.",
102
+ page:
103
+ "Adds a minimal app page under web/app/<route>/page.tsx.",
104
+ ui:
105
+ "Adds a minimal Vite React web app with ForgeProvider devAuth and a generated client bridge.",
106
+ resource:
107
+ "Creates a full resource slice: table, policies, CRUD commands, queries, liveQuery, action, optional React, and tests.",
108
+ };
109
+
110
+ function diagnostic(
111
+ severity: Diagnostic["severity"],
112
+ code: string,
113
+ message: string,
114
+ file?: string,
115
+ ): Diagnostic {
116
+ return createDiagnostic({
117
+ severity,
118
+ code,
119
+ message,
120
+ ...(file ? { file } : {}),
121
+ });
122
+ }
123
+
124
+ function okResult(partial: Omit<MakeResult, "ok" | "diagnostics" | "exitCode">): MakeResult {
125
+ return { ok: true, diagnostics: [], exitCode: 0, ...partial };
126
+ }
127
+
128
+ function failResult(diagnostics: Diagnostic[], plan?: MakePlan): MakeResult {
129
+ return { ok: false, plan, diagnostics, exitCode: 1 };
130
+ }
131
+
132
+ function normalizeRel(file: string): string {
133
+ return file.replace(/\\/g, "/").replace(/^\/+/, "");
134
+ }
135
+
136
+ function absPath(workspaceRoot: string, file: string): string {
137
+ const root = resolve(workspaceRoot);
138
+ const absolute = resolve(root, normalize(file));
139
+ const rel = relative(root, absolute);
140
+ if (rel.startsWith("..") || resolve(rel) === rel) {
141
+ throw new Error(`refusing to write outside workspace: ${file}`);
142
+ }
143
+ return absolute;
144
+ }
145
+
146
+ function readIfExists(
147
+ workspaceRoot: string,
148
+ file: string,
149
+ fs: FileSystem = nodeFileSystem,
150
+ ): string | null {
151
+ return fs.readText(absPath(workspaceRoot, file));
152
+ }
153
+
154
+ function writeText(
155
+ workspaceRoot: string,
156
+ file: string,
157
+ content: string,
158
+ fs: FileSystem = nodeFileSystem,
159
+ ): void {
160
+ fs.writeText(absPath(workspaceRoot, file), content);
161
+ }
162
+
163
+ function fileExists(
164
+ workspaceRoot: string,
165
+ file: string,
166
+ fs: FileSystem = nodeFileSystem,
167
+ ): boolean {
168
+ return fs.exists(absPath(workspaceRoot, file));
169
+ }
170
+
171
+ function removeFile(
172
+ workspaceRoot: string,
173
+ file: string,
174
+ fs: FileSystem = nodeFileSystem,
175
+ ): void {
176
+ fs.remove(absPath(workspaceRoot, file));
177
+ }
178
+
179
+ function chooseSchemaFile(workspaceRoot: string): string {
180
+ if (fileExists(workspaceRoot, "src/forge/schema.ts")) {
181
+ return "src/forge/schema.ts";
182
+ }
183
+ if (fileExists(workspaceRoot, "src/schema.ts")) {
184
+ return "src/schema.ts";
185
+ }
186
+ return "src/forge/schema.ts";
187
+ }
188
+
189
+ function choosePolicyFile(workspaceRoot: string): string {
190
+ if (fileExists(workspaceRoot, "src/policies.ts")) {
191
+ return "src/policies.ts";
192
+ }
193
+ if (fileExists(workspaceRoot, "src/forge/policies.ts")) {
194
+ return "src/forge/policies.ts";
195
+ }
196
+ return "src/policies.ts";
197
+ }
198
+
199
+ function ensureSchemaImport(content: string): string {
200
+ if (content.includes("defineTable")) {
201
+ return content;
202
+ }
203
+ return `import { defineTable } from "forge/server";\n\n${content}`;
204
+ }
205
+
206
+ function appendSchemaTable(
207
+ workspaceRoot: string,
208
+ tableName: string,
209
+ fields: MakeFieldSpec[],
210
+ tenantScoped: boolean,
211
+ ): { patch?: PlannedPatch; diagnostics: Diagnostic[] } {
212
+ const file = chooseSchemaFile(workspaceRoot);
213
+ const existing = readIfExists(workspaceRoot, file);
214
+ const rendered = renderSchemaTable(tableName, fields, tenantScoped);
215
+ const diagnostics: Diagnostic[] = [];
216
+ const base = existing ?? "";
217
+
218
+ if (
219
+ new RegExp(`name:\\s*["']${tableName}["']`).test(base) ||
220
+ new RegExp(`export\\s+const\\s+${camelCase(tableName)}\\b`).test(base)
221
+ ) {
222
+ diagnostics.push(
223
+ diagnostic("error", "FORGE_MAKE_TABLE_EXISTS", `table '${tableName}' already exists`, file),
224
+ );
225
+ return { diagnostics };
226
+ }
227
+
228
+ const next = `${ensureSchemaImport(base).trimEnd()}\n\n${rendered}`;
229
+ return {
230
+ diagnostics,
231
+ patch: {
232
+ file,
233
+ kind: existing ? "append-section" : "create-if-missing",
234
+ description: `Add table '${tableName}'`,
235
+ beforeHash: existing ? hashStable(existing) : undefined,
236
+ afterPreview: next,
237
+ },
238
+ };
239
+ }
240
+
241
+ function fieldLine(field: MakeFieldSpec): string {
242
+ const type =
243
+ field.type === "enum"
244
+ ? `enum:${field.enumValues?.join(",") ?? ""}`
245
+ : field.type === "ref"
246
+ ? `ref:${field.refTable ?? field.name.replace(/Id$/, "s")}`
247
+ : field.type;
248
+ return ` ${field.name}: "${type}",`;
249
+ }
250
+
251
+ function addFieldToSchema(
252
+ workspaceRoot: string,
253
+ tableName: string,
254
+ field: MakeFieldSpec,
255
+ ): { patch?: PlannedPatch; diagnostics: Diagnostic[] } {
256
+ const file = chooseSchemaFile(workspaceRoot);
257
+ const existing = readIfExists(workspaceRoot, file);
258
+ const diagnostics: Diagnostic[] = [];
259
+ if (!existing) {
260
+ return {
261
+ diagnostics: [
262
+ diagnostic(
263
+ "error",
264
+ "FORGE_MAKE_FILE_MISSING",
265
+ `schema file '${file}' does not exist`,
266
+ file,
267
+ ),
268
+ ],
269
+ };
270
+ }
271
+
272
+ const tableExport = new RegExp(`export\\s+const\\s+${camelCase(tableName)}\\s*=\\s*defineTable`);
273
+ const tableIndex = existing.search(tableExport);
274
+ const nameIndex = existing.search(new RegExp(`name:\\s*["']${tableName}["']`));
275
+ const start = tableIndex >= 0 ? tableIndex : nameIndex;
276
+ if (start < 0) {
277
+ return {
278
+ diagnostics: [
279
+ diagnostic(
280
+ "error",
281
+ "FORGE_MAKE_COMMAND_TABLE_MISSING",
282
+ `table '${tableName}' was not found in schema`,
283
+ file,
284
+ ),
285
+ ],
286
+ };
287
+ }
288
+
289
+ const fieldsIndex = existing.indexOf("fields: {", start);
290
+ const closeIndex = fieldsIndex >= 0 ? existing.indexOf("\n },", fieldsIndex) : -1;
291
+ if (fieldsIndex < 0 || closeIndex < 0) {
292
+ return {
293
+ diagnostics: [
294
+ diagnostic(
295
+ "error",
296
+ "FORGE_MAKE_SCHEMA_UNSUPPORTED_SHAPE",
297
+ `could not safely patch fields for table '${tableName}'`,
298
+ file,
299
+ ),
300
+ ],
301
+ };
302
+ }
303
+
304
+ const fieldsBlock = existing.slice(fieldsIndex, closeIndex);
305
+ if (new RegExp(`\\b${field.name}\\s*:`).test(fieldsBlock)) {
306
+ diagnostics.push(
307
+ diagnostic(
308
+ "error",
309
+ "FORGE_MAKE_FIELD_EXISTS",
310
+ `field '${field.name}' already exists on '${tableName}'`,
311
+ file,
312
+ ),
313
+ );
314
+ return { diagnostics };
315
+ }
316
+
317
+ const next = `${existing.slice(0, closeIndex)}\n${fieldLine(field)}${existing.slice(closeIndex)}`;
318
+ return {
319
+ diagnostics,
320
+ patch: {
321
+ file,
322
+ kind: "replace-section",
323
+ description: `Add field '${field.name}' to '${tableName}'`,
324
+ beforeHash: hashStable(existing),
325
+ afterPreview: next,
326
+ },
327
+ };
328
+ }
329
+
330
+ function parseRoles(raw: string | undefined): string[] {
331
+ return (raw ?? "owner,admin,member")
332
+ .split(",")
333
+ .map((role) => role.trim())
334
+ .filter(Boolean)
335
+ .sort();
336
+ }
337
+
338
+ function buildPolicyContentFrom(
339
+ file: string,
340
+ existing: string | null,
341
+ entries: Record<string, string[]>,
342
+ ): { patch?: PlannedPatch; diagnostics: Diagnostic[] } {
343
+ const diagnostics: Diagnostic[] = [];
344
+ const names = Object.keys(entries).sort();
345
+
346
+ for (const name of names) {
347
+ if (!/^[a-zA-Z0-9_.:-]+$/.test(name)) {
348
+ diagnostics.push(
349
+ diagnostic("error", "FORGE_MAKE_POLICY_NAME_INVALID", `invalid policy name '${name}'`, file),
350
+ );
351
+ }
352
+ if ((entries[name] ?? []).length === 0) {
353
+ diagnostics.push(
354
+ diagnostic("error", "FORGE_MAKE_POLICY_EMPTY_ROLES", `policy '${name}' has no roles`, file),
355
+ );
356
+ }
357
+ if (existing?.includes(`${JSON.stringify(name)}:`) || existing?.includes(`'${name}':`)) {
358
+ diagnostics.push(
359
+ diagnostic("error", "FORGE_MAKE_POLICY_EXISTS", `policy '${name}' already exists`, file),
360
+ );
361
+ }
362
+ }
363
+ if (diagnostics.length > 0) {
364
+ return { diagnostics };
365
+ }
366
+
367
+ if (!existing) {
368
+ return {
369
+ diagnostics,
370
+ patch: {
371
+ file,
372
+ kind: "create-if-missing",
373
+ description: `Create policy file with ${names.length} policy entries`,
374
+ afterPreview: renderPolicyFile(entries),
375
+ },
376
+ };
377
+ }
378
+
379
+ const closeIndex = existing.lastIndexOf("});");
380
+ if (closeIndex < 0 || !existing.includes("definePolicies")) {
381
+ return {
382
+ diagnostics: [
383
+ diagnostic(
384
+ "error",
385
+ "FORGE_MAKE_POLICY_UNSUPPORTED_SHAPE",
386
+ `could not safely patch policy file '${file}'`,
387
+ file,
388
+ ),
389
+ ],
390
+ };
391
+ }
392
+
393
+ const additions = names
394
+ .map(
395
+ (name) =>
396
+ ` ${JSON.stringify(name)}: canRole(${entries[name].map((role) => JSON.stringify(role)).join(", ")}),`,
397
+ )
398
+ .join("\n");
399
+ const next = `${existing.slice(0, closeIndex).trimEnd()}\n${additions}\n${existing.slice(closeIndex)}`;
400
+ return {
401
+ diagnostics,
402
+ patch: {
403
+ file,
404
+ kind: "append-section",
405
+ description: `Add ${names.length} policy entries`,
406
+ beforeHash: hashStable(existing),
407
+ afterPreview: next,
408
+ },
409
+ };
410
+ }
411
+
412
+ function buildPolicyContent(
413
+ workspaceRoot: string,
414
+ entries: Record<string, string[]>,
415
+ ): { patch?: PlannedPatch; diagnostics: Diagnostic[] } {
416
+ const file = choosePolicyFile(workspaceRoot);
417
+ return buildPolicyContentFrom(file, readIfExists(workspaceRoot, file), entries);
418
+ }
419
+
420
+ function createFile(
421
+ workspaceRoot: string,
422
+ file: string,
423
+ description: string,
424
+ content: string,
425
+ ): PlannedFile {
426
+ return {
427
+ file,
428
+ description,
429
+ content,
430
+ exists: fileExists(workspaceRoot, file),
431
+ };
432
+ }
433
+
434
+ function defaultFields(kind: MakeIntent["kind"]): MakeFieldSpec[] {
435
+ if (kind === "resource" || kind === "table") {
436
+ return [
437
+ { name: "title", type: "text", required: true, optional: false },
438
+ {
439
+ name: "status",
440
+ type: "enum",
441
+ required: true,
442
+ optional: false,
443
+ enumValues: ["open", "closed"],
444
+ default: "open",
445
+ indexed: true,
446
+ },
447
+ ];
448
+ }
449
+ return [];
450
+ }
451
+
452
+ function parseFieldOptions(options: MakeCommandOptions): {
453
+ fields: MakeFieldSpec[];
454
+ field?: MakeFieldSpec;
455
+ diagnostics: Diagnostic[];
456
+ } {
457
+ const rawFields = [
458
+ ...(options.fieldsRaw ? [options.fieldsRaw] : []),
459
+ ...options.fieldSpecs,
460
+ ];
461
+ const parsed = parseFields(rawFields);
462
+ const diagnostics = [...parsed.diagnostics];
463
+
464
+ if (options.primitive === "field") {
465
+ const name = options.name?.includes(".")
466
+ ? options.name.split(".").pop()
467
+ : options.name;
468
+ const type = options.type ?? "text";
469
+ const rawType =
470
+ type === "enum"
471
+ ? `enum(${options.values ?? "open,closed"})`
472
+ : type === "ref"
473
+ ? `ref(${options.table ?? ""})`
474
+ : type;
475
+ const raw = [
476
+ `${name ?? "field"}:${rawType}`,
477
+ options.index ? "index" : "",
478
+ options.defaultValue ? `default=${options.defaultValue}` : "",
479
+ ]
480
+ .filter(Boolean)
481
+ .join(":");
482
+ const field = parseFieldSpec(raw);
483
+ diagnostics.push(...field.diagnostics);
484
+ return { fields: parsed.fields, field: field.field, diagnostics };
485
+ }
486
+
487
+ return { fields: parsed.fields, diagnostics };
488
+ }
489
+
490
+ function defaultPolicyFor(tableName: string, action: string): string {
491
+ return `${tableName}.${action}`;
492
+ }
493
+
494
+ function buildIntent(options: MakeCommandOptions): {
495
+ intent?: MakeIntent;
496
+ diagnostics: Diagnostic[];
497
+ } {
498
+ if (!AUTHORING_PRIMITIVES.includes(options.primitive as MakeIntent["kind"])) {
499
+ return {
500
+ diagnostics: [
501
+ diagnostic("error", "FORGE_MAKE_PATCH_UNSAFE", `unsupported make primitive '${options.primitive}'`),
502
+ ],
503
+ };
504
+ }
505
+
506
+ const kind = options.primitive as MakeIntent["kind"];
507
+ const fieldOptions = parseFieldOptions(options);
508
+ const diagnostics = [...fieldOptions.diagnostics];
509
+ if (options.framework && !["vite", "next"].includes(options.framework)) {
510
+ diagnostics.push(
511
+ diagnostic("error", "FORGE_MAKE_PATCH_UNSAFE", `unsupported frontend framework '${options.framework}'`),
512
+ );
513
+ }
514
+ if (kind === "ui" && options.framework === "next") {
515
+ diagnostics.push(
516
+ diagnostic(
517
+ "warning",
518
+ "FORGE_MAKE_UI_FRAMEWORK_EXPERIMENTAL",
519
+ "forge make ui currently generates the Vite React bridge; Next support should use the b2b-support-web template",
520
+ ),
521
+ );
522
+ }
523
+ const name = options.name ?? (kind === "field" ? "" : undefined);
524
+ if (!name && !["component", "page", "ui"].includes(kind)) {
525
+ diagnostics.push(
526
+ diagnostic("error", "FORGE_MAKE_PATCH_UNSAFE", `forge make ${kind} requires a name`),
527
+ );
528
+ }
529
+
530
+ const tableFromName = name?.includes(".") ? name.split(".")[0] : undefined;
531
+ const table = options.table ?? tableFromName ?? (kind === "resource" || kind === "table" ? name : undefined);
532
+ const fields =
533
+ fieldOptions.fields.length > 0 ? fieldOptions.fields : defaultFields(kind);
534
+ const singular = singularize(table ?? name ?? "item");
535
+ const actionName = name?.includes(".") ? name.split(".").pop() ?? name : name;
536
+ const policyAction =
537
+ kind === "command"
538
+ ? actionName?.replace(/^(create|update|delete)/, "") || "create"
539
+ : "read";
540
+
541
+ return {
542
+ diagnostics,
543
+ intent: {
544
+ kind,
545
+ name: name ?? options.component ?? table ?? "component",
546
+ table,
547
+ field: fieldOptions.field,
548
+ fields,
549
+ tenantScoped: options.tenantScoped || kind === "resource",
550
+ crud: options.withCrud || kind === "resource",
551
+ liveQuery: options.withLiveQuery || kind === "resource" || kind === "livequery",
552
+ react:
553
+ options.withReact ||
554
+ options.withUi ||
555
+ kind === "resource" ||
556
+ kind === "component" ||
557
+ kind === "page" ||
558
+ kind === "ui",
559
+ tests: options.withTests || kind === "resource",
560
+ policy:
561
+ options.policy ??
562
+ (table ? defaultPolicyFor(table, kind === "command" ? policyAction.toLowerCase() : "read") : undefined),
563
+ roles: parseRoles(options.roles),
564
+ emit: options.emit,
565
+ event: options.event ?? options.emit ?? (table ? `${singular}.created` : undefined),
566
+ trigger: options.trigger ?? options.event ?? (table ? `${singular}.created` : undefined),
567
+ component: options.component,
568
+ route: options.name ? kebabCase(options.name) : undefined,
569
+ withAi: options.withAi,
570
+ withCreateForm: options.withCreateForm || kind === "resource",
571
+ },
572
+ };
573
+ }
574
+
575
+ function addPatch(plan: MakePlan, result: { patch?: PlannedPatch; diagnostics: Diagnostic[] }): void {
576
+ plan.diagnostics.push(...result.diagnostics);
577
+ if (result.patch) {
578
+ const existing = plan.filesToModify.find((patch) => patch.file === result.patch?.file);
579
+ if (existing) {
580
+ existing.afterPreview = result.patch.afterPreview;
581
+ existing.description = `${existing.description}; ${result.patch.description}`;
582
+ return;
583
+ }
584
+ plan.filesToModify.push(result.patch);
585
+ }
586
+ }
587
+
588
+ function addPolicies(plan: MakePlan, workspaceRoot: string, entries: Record<string, string[]>): void {
589
+ const existingPatch = plan.filesToModify.find((patch) => patch.file === choosePolicyFile(workspaceRoot));
590
+ if (!existingPatch) {
591
+ addPatch(plan, buildPolicyContent(workspaceRoot, entries));
592
+ return;
593
+ }
594
+
595
+ const next = buildPolicyContentFrom(existingPatch.file, existingPatch.afterPreview, entries);
596
+ if (next.patch) {
597
+ existingPatch.afterPreview = next.patch.afterPreview;
598
+ existingPatch.description = `${existingPatch.description}; ${next.patch.description}`;
599
+ }
600
+ plan.diagnostics.push(...next.diagnostics);
601
+ }
602
+
603
+ function addRuntimeFiles(plan: MakePlan, workspaceRoot: string, intent: MakeIntent): void {
604
+ const table = intent.table ?? intent.name;
605
+ const singular = singularize(table);
606
+ const pascal = pascalCase(singular);
607
+ const pascalPlural = pascalCase(table);
608
+ const readPolicy = intent.policy ?? defaultPolicyFor(table, "read");
609
+ const event = intent.event ?? `${singular}.created`;
610
+
611
+ if (intent.kind === "command") {
612
+ const action = intent.name.includes(".") ? intent.name.split(".").pop() ?? "create" : intent.name;
613
+ const file = `src/commands/${camelCase(intent.name.replace(/\./g, "-"))}.ts`;
614
+ const content =
615
+ action.startsWith("update")
616
+ ? renderUpdateCommand(table, intent.fields, intent.policy ?? defaultPolicyFor(table, "update"), intent.emit ?? `${singular}.updated`)
617
+ : action.startsWith("delete")
618
+ ? renderDeleteCommand(table, intent.policy ?? defaultPolicyFor(table, "delete"), intent.emit ?? `${singular}.deleted`)
619
+ : renderCreateCommand(table, intent.fields, intent.policy ?? defaultPolicyFor(table, "create"), intent.emit ?? event);
620
+ plan.filesToCreate.push(createFile(workspaceRoot, file, `Add command '${intent.name}'`, content));
621
+ return;
622
+ }
623
+
624
+ if (intent.kind === "query") {
625
+ const fileName = intent.name.includes("get") ? `get${pascal}` : `list${pascalPlural}`;
626
+ const content = intent.name.includes("get")
627
+ ? renderGetQuery(table, readPolicy)
628
+ : renderListQuery(table, readPolicy);
629
+ plan.filesToCreate.push(createFile(workspaceRoot, `src/queries/${fileName}.ts`, `Add query '${intent.name}'`, content));
630
+ return;
631
+ }
632
+
633
+ if (intent.kind === "livequery") {
634
+ plan.filesToCreate.push(
635
+ createFile(
636
+ workspaceRoot,
637
+ `src/queries/live${pascalPlural}.ts`,
638
+ `Add liveQuery '${intent.name}'`,
639
+ renderLiveQuery(table, readPolicy),
640
+ ),
641
+ );
642
+ return;
643
+ }
644
+
645
+ if (intent.kind === "action") {
646
+ plan.filesToCreate.push(
647
+ createFile(
648
+ workspaceRoot,
649
+ `src/actions/capture${pascal}Created.ts`,
650
+ `Add action '${intent.name}'`,
651
+ renderAction(table, intent.event ?? event),
652
+ ),
653
+ );
654
+ return;
655
+ }
656
+
657
+ if (intent.kind === "workflow") {
658
+ plan.filesToCreate.push(
659
+ createFile(
660
+ workspaceRoot,
661
+ `src/workflows/${camelCase(intent.name)}.ts`,
662
+ `Add workflow '${intent.name}'`,
663
+ renderWorkflow(table, intent.trigger ?? event, intent.withAi),
664
+ ),
665
+ );
666
+ }
667
+ }
668
+
669
+ function addFrontendFiles(plan: MakePlan, workspaceRoot: string, intent: MakeIntent): void {
670
+ const table = intent.table ?? intent.name;
671
+ const singular = singularize(table);
672
+ const pascal = pascalCase(singular);
673
+ if (intent.kind === "ui") {
674
+ plan.filesToCreate.push(
675
+ createFile(workspaceRoot, "web/package.json", "Add Vite React web package", renderVitePackage(intent.name)),
676
+ createFile(workspaceRoot, "web/tsconfig.json", "Add web TypeScript config", renderViteTsconfig()),
677
+ createFile(workspaceRoot, "web/index.html", "Add web HTML entry", renderViteIndex("ForgeOS App")),
678
+ createFile(workspaceRoot, "web/src/lib/forge.ts", "Add Forge client bridge", renderWebBridge()),
679
+ createFile(workspaceRoot, "web/src/main.tsx", "Add React entrypoint", renderViteMain()),
680
+ createFile(workspaceRoot, "web/src/App.tsx", "Add starter app", renderViteApp()),
681
+ createFile(workspaceRoot, "web/src/styles.css", "Add starter styles", renderViteStyles()),
682
+ );
683
+ return;
684
+ }
685
+ if (intent.kind === "component") {
686
+ const component = intent.component ?? `${pascal}List`;
687
+ plan.filesToCreate.push(
688
+ createFile(
689
+ workspaceRoot,
690
+ `web/components/${component}.tsx`,
691
+ `Add component '${component}'`,
692
+ component.startsWith("Create")
693
+ ? renderCreateForm(table, intent.fields)
694
+ : renderListComponent(table),
695
+ ),
696
+ );
697
+ return;
698
+ }
699
+ if (intent.kind === "page") {
700
+ const route = intent.route ?? kebabCase(intent.name);
701
+ plan.filesToCreate.push(
702
+ createFile(
703
+ workspaceRoot,
704
+ `web/app/${route}/page.tsx`,
705
+ `Add page '${route}'`,
706
+ renderPage(table, intent.withCreateForm),
707
+ ),
708
+ );
709
+ return;
710
+ }
711
+
712
+ if (intent.react) {
713
+ if (!fileExists(workspaceRoot, "web/lib/forge.ts")) {
714
+ plan.filesToCreate.push(
715
+ createFile(
716
+ workspaceRoot,
717
+ "web/lib/forge.ts",
718
+ "Add Forge client bridge",
719
+ renderWebRootBridge(),
720
+ ),
721
+ );
722
+ }
723
+ plan.filesToCreate.push(
724
+ createFile(
725
+ workspaceRoot,
726
+ `web/components/${pascal}List.tsx`,
727
+ `Add ${titleCase(table)} list component`,
728
+ renderListComponent(table),
729
+ ),
730
+ );
731
+ if (intent.withCreateForm) {
732
+ plan.filesToCreate.push(
733
+ createFile(
734
+ workspaceRoot,
735
+ `web/components/Create${pascal}Form.tsx`,
736
+ `Add ${titleCase(table)} create form`,
737
+ renderCreateForm(table, intent.fields),
738
+ ),
739
+ );
740
+ }
741
+ plan.filesToCreate.push(
742
+ createFile(
743
+ workspaceRoot,
744
+ `web/app/${kebabCase(table)}/page.tsx`,
745
+ `Add ${titleCase(table)} page`,
746
+ renderPage(table, intent.withCreateForm),
747
+ ),
748
+ );
749
+ }
750
+ }
751
+
752
+ function buildPlan(options: MakeCommandOptions): MakePlan {
753
+ const built = buildIntent(options);
754
+ const intent = built.intent ?? {
755
+ kind: "resource" as const,
756
+ name: options.name ?? "resource",
757
+ fields: [],
758
+ tenantScoped: true,
759
+ crud: true,
760
+ liveQuery: true,
761
+ react: true,
762
+ tests: true,
763
+ roles: [],
764
+ withAi: false,
765
+ withCreateForm: true,
766
+ };
767
+ const id = `make_${hashStable(canonicalJson(intent)).slice(0, 12)}`;
768
+ const plan: MakePlan = {
769
+ schemaVersion: "0.1.0",
770
+ makeVersion: GENERATOR_VERSION,
771
+ id,
772
+ intent,
773
+ summary: `Plan forge make ${intent.kind} ${intent.name}`,
774
+ filesToCreate: [],
775
+ filesToModify: [],
776
+ filesToDelete: [],
777
+ generatedAfterApply: !options.noGenerate,
778
+ commandsToRun: [
779
+ ...(!options.noGenerate ? ["forge generate"] : []),
780
+ ...(!options.noVerify ? ["forge verify --strict"] : []),
781
+ ],
782
+ diagnostics: [...built.diagnostics],
783
+ risk: { level: "low", reasons: [] },
784
+ rollback: {
785
+ trackedFiles: [],
786
+ instructions: [`forge make rollback ${id}`],
787
+ },
788
+ };
789
+
790
+ const table = intent.table ?? intent.name;
791
+ if (intent.kind === "table" || intent.kind === "resource") {
792
+ addPatch(plan, appendSchemaTable(options.workspaceRoot, table, intent.fields, intent.tenantScoped));
793
+ if (intent.tenantScoped) {
794
+ const schema = readIfExists(options.workspaceRoot, chooseSchemaFile(options.workspaceRoot)) ?? "";
795
+ if (!schema.includes('name: "tenants"') && !schema.includes("name: 'tenants'")) {
796
+ plan.diagnostics.push(
797
+ diagnostic(
798
+ "warning",
799
+ "FORGE_MAKE_TENANTS_TABLE_MISSING",
800
+ "tenant-scoped table references tenants but no tenants table was found",
801
+ chooseSchemaFile(options.workspaceRoot),
802
+ ),
803
+ );
804
+ }
805
+ }
806
+ }
807
+
808
+ if (intent.kind === "field" && intent.field) {
809
+ const targetTable = intent.table ?? intent.name.split(".")[0];
810
+ addPatch(plan, addFieldToSchema(options.workspaceRoot, targetTable, intent.field));
811
+ }
812
+
813
+ if (intent.kind === "policy") {
814
+ addPolicies(plan, options.workspaceRoot, { [intent.name]: intent.roles });
815
+ }
816
+
817
+ if (intent.kind === "resource") {
818
+ addPolicies(plan, options.workspaceRoot, {
819
+ [defaultPolicyFor(table, "create")]: intent.roles,
820
+ [defaultPolicyFor(table, "delete")]: intent.roles.filter((role) => role !== "member"),
821
+ [defaultPolicyFor(table, "read")]: intent.roles,
822
+ [defaultPolicyFor(table, "update")]: intent.roles,
823
+ });
824
+ plan.filesToCreate.push(
825
+ createFile(
826
+ options.workspaceRoot,
827
+ `src/commands/create${pascalCase(singularize(table))}.ts`,
828
+ `Add create command for '${table}'`,
829
+ renderCreateCommand(table, intent.fields, defaultPolicyFor(table, "create"), `${singularize(table)}.created`),
830
+ ),
831
+ createFile(
832
+ options.workspaceRoot,
833
+ `src/commands/update${pascalCase(singularize(table))}.ts`,
834
+ `Add update command for '${table}'`,
835
+ renderUpdateCommand(table, intent.fields, defaultPolicyFor(table, "update"), `${singularize(table)}.updated`),
836
+ ),
837
+ createFile(
838
+ options.workspaceRoot,
839
+ `src/commands/delete${pascalCase(singularize(table))}.ts`,
840
+ `Add delete command for '${table}'`,
841
+ renderDeleteCommand(table, defaultPolicyFor(table, "delete"), `${singularize(table)}.deleted`),
842
+ ),
843
+ createFile(
844
+ options.workspaceRoot,
845
+ `src/queries/list${pascalCase(table)}.ts`,
846
+ `Add list query for '${table}'`,
847
+ renderListQuery(table, defaultPolicyFor(table, "read")),
848
+ ),
849
+ createFile(
850
+ options.workspaceRoot,
851
+ `src/queries/get${pascalCase(singularize(table))}.ts`,
852
+ `Add get query for '${table}'`,
853
+ renderGetQuery(table, defaultPolicyFor(table, "read")),
854
+ ),
855
+ );
856
+ if (intent.liveQuery) {
857
+ plan.filesToCreate.push(
858
+ createFile(
859
+ options.workspaceRoot,
860
+ `src/queries/live${pascalCase(table)}.ts`,
861
+ `Add liveQuery for '${table}'`,
862
+ renderLiveQuery(table, defaultPolicyFor(table, "read")),
863
+ ),
864
+ );
865
+ }
866
+ plan.filesToCreate.push(
867
+ createFile(
868
+ options.workspaceRoot,
869
+ `src/actions/capture${pascalCase(singularize(table))}Created.ts`,
870
+ `Add created action for '${table}'`,
871
+ renderAction(table, `${singularize(table)}.created`),
872
+ ),
873
+ );
874
+ }
875
+
876
+ if (["command", "query", "livequery", "action", "workflow"].includes(intent.kind)) {
877
+ addRuntimeFiles(plan, options.workspaceRoot, intent);
878
+ }
879
+
880
+ if (["component", "page"].includes(intent.kind) || intent.react) {
881
+ addFrontendFiles(plan, options.workspaceRoot, intent);
882
+ }
883
+
884
+ if (intent.tests) {
885
+ plan.filesToCreate.push(
886
+ createFile(
887
+ options.workspaceRoot,
888
+ `tests/make-generated/${kebabCase(intent.name)}.test.ts`,
889
+ `Add smoke test for '${intent.name}'`,
890
+ renderPlaceholderTest(`forge make ${intent.kind} ${intent.name}`),
891
+ ),
892
+ );
893
+ }
894
+
895
+ for (const file of plan.filesToCreate) {
896
+ if (file.exists && !options.force) {
897
+ plan.diagnostics.push(
898
+ diagnostic("error", "FORGE_MAKE_FILE_EXISTS", `file already exists: ${file.file}`, file.file),
899
+ );
900
+ }
901
+ }
902
+
903
+ const schemaOrPolicy = plan.filesToModify.length > 0;
904
+ if (schemaOrPolicy) {
905
+ plan.risk.level = plan.diagnostics.some((diag) => diag.severity === "error") ? "high" : "medium";
906
+ plan.risk.reasons.push("schema or policy source changes require regeneration");
907
+ }
908
+ if (plan.filesToCreate.length > 5) {
909
+ plan.risk.reasons.push("resource generation creates multiple runtime files");
910
+ }
911
+ plan.rollback.trackedFiles = [
912
+ ...plan.filesToCreate.map((file) => file.file),
913
+ ...plan.filesToModify.map((patch) => patch.file),
914
+ ].sort();
915
+
916
+ return plan;
917
+ }
918
+
919
+ function planPath(_workspaceRoot: string, planId: string): string {
920
+ return `${PLAN_DIR}/${planId}/plan.json`;
921
+ }
922
+
923
+ function snapshotPath(_workspaceRoot: string, planId: string): string {
924
+ return `${PLAN_DIR}/${planId}/snapshot.json`;
925
+ }
926
+
927
+ function writePlanFiles(workspaceRoot: string, plan: MakePlan): string {
928
+ const dir = `${PLAN_DIR}/${plan.id}`;
929
+ const jsonPath = `${dir}/plan.json`;
930
+ writeText(workspaceRoot, jsonPath, serializeCanonical(plan));
931
+ writeText(
932
+ workspaceRoot,
933
+ `${dir}/plan.md`,
934
+ `# ${plan.summary}\n\nFiles to create:\n${plan.filesToCreate.map((file) => `- ${file.file}`).join("\n") || "- none"}\n\nFiles to modify:\n${plan.filesToModify.map((file) => `- ${file.file}`).join("\n") || "- none"}\n`,
935
+ );
936
+ return normalizeRel(jsonPath);
937
+ }
938
+
939
+ function readPlan(workspaceRoot: string, idOrPath: string): MakePlan | null {
940
+ const candidates = [
941
+ idOrPath,
942
+ planPath(workspaceRoot, idOrPath),
943
+ `${PLAN_DIR}/${idOrPath}/plan.json`,
944
+ ];
945
+ for (const candidate of candidates) {
946
+ const content = readIfExists(workspaceRoot, candidate);
947
+ if (content) {
948
+ return JSON.parse(content) as MakePlan;
949
+ }
950
+ }
951
+ return null;
952
+ }
953
+
954
+ function writeSnapshot(workspaceRoot: string, plan: MakePlan): void {
955
+ const seen = new Set<string>();
956
+ const files: SnapshotFile[] = [];
957
+ for (const file of plan.rollback.trackedFiles) {
958
+ if (seen.has(file)) {
959
+ continue;
960
+ }
961
+ seen.add(file);
962
+ const content = readIfExists(workspaceRoot, file);
963
+ files.push({
964
+ file,
965
+ existed: content !== null,
966
+ ...(content !== null ? { content } : {}),
967
+ });
968
+ }
969
+ const snapshot: MakeSnapshot = {
970
+ schemaVersion: "0.1.0",
971
+ planId: plan.id,
972
+ files,
973
+ };
974
+ writeText(workspaceRoot, snapshotPath(workspaceRoot, plan.id), serializeCanonical(snapshot));
975
+ }
976
+
977
+ function applyPlan(workspaceRoot: string, plan: MakePlan, force: boolean): Diagnostic[] {
978
+ const diagnostics: Diagnostic[] = [];
979
+ writeSnapshot(workspaceRoot, plan);
980
+
981
+ for (const file of plan.filesToCreate) {
982
+ const exists = fileExists(workspaceRoot, file.file);
983
+ if (exists && !force) {
984
+ diagnostics.push(
985
+ diagnostic("error", "FORGE_MAKE_FILE_EXISTS", `file already exists: ${file.file}`, file.file),
986
+ );
987
+ continue;
988
+ }
989
+ writeText(workspaceRoot, file.file, file.content);
990
+ }
991
+
992
+ for (const patch of plan.filesToModify) {
993
+ if (patch.beforeHash) {
994
+ const current = readIfExists(workspaceRoot, patch.file);
995
+ if (current !== null && hashStable(current) !== patch.beforeHash) {
996
+ diagnostics.push(
997
+ diagnostic(
998
+ "error",
999
+ "FORGE_MAKE_PATCH_UNSAFE",
1000
+ `file changed after plan was created: ${patch.file}`,
1001
+ patch.file,
1002
+ ),
1003
+ );
1004
+ continue;
1005
+ }
1006
+ }
1007
+ writeText(workspaceRoot, patch.file, patch.afterPreview);
1008
+ }
1009
+
1010
+ return diagnostics;
1011
+ }
1012
+
1013
+ export function rollbackMakePlan(workspaceRoot: string, idOrPath: string): MakeResult {
1014
+ const id = idOrPath.endsWith(".json") ? dirname(idOrPath).split(/[\\/]/).pop() ?? idOrPath : idOrPath;
1015
+ const snapshotRaw = readIfExists(workspaceRoot, snapshotPath(workspaceRoot, id));
1016
+ if (!snapshotRaw) {
1017
+ return failResult([
1018
+ diagnostic("error", "FORGE_MAKE_FILE_MISSING", `missing make snapshot for '${id}'`),
1019
+ ]);
1020
+ }
1021
+ const snapshot = JSON.parse(snapshotRaw) as MakeSnapshot;
1022
+ for (const file of snapshot.files) {
1023
+ if (file.existed) {
1024
+ writeText(workspaceRoot, file.file, file.content ?? "");
1025
+ } else {
1026
+ removeFile(workspaceRoot, file.file);
1027
+ }
1028
+ }
1029
+ return okResult({ applied: true, explanation: `rolled back ${snapshot.planId}` });
1030
+ }
1031
+
1032
+ export function planMakeCommand(options: MakeCommandOptions): MakeResult {
1033
+ if (options.primitive === "list") {
1034
+ return okResult({ primitives: MAKE_PRIMITIVES });
1035
+ }
1036
+
1037
+ if (options.primitive === "explain") {
1038
+ const primitive = options.explainPrimitive as MakeIntent["kind"] | undefined;
1039
+ if (!primitive || !(primitive in EXPLANATIONS)) {
1040
+ return failResult([
1041
+ diagnostic("error", "FORGE_MAKE_PATCH_UNSAFE", "forge make explain requires a known primitive"),
1042
+ ]);
1043
+ }
1044
+ return okResult({ explanation: EXPLANATIONS[primitive] });
1045
+ }
1046
+
1047
+ if (options.primitive === "rollback") {
1048
+ return rollbackMakePlan(options.workspaceRoot, options.name ?? "");
1049
+ }
1050
+
1051
+ if (options.primitive === "apply") {
1052
+ const plan = options.name ? readPlan(options.workspaceRoot, options.name) : null;
1053
+ if (!plan) {
1054
+ return failResult([
1055
+ diagnostic("error", "FORGE_MAKE_FILE_MISSING", `make plan not found: ${options.name ?? ""}`),
1056
+ ]);
1057
+ }
1058
+ const diagnostics = applyPlan(options.workspaceRoot, plan, options.force);
1059
+ return {
1060
+ ok: diagnostics.filter((diag) => diag.severity === "error").length === 0,
1061
+ plan,
1062
+ applied: diagnostics.filter((diag) => diag.severity === "error").length === 0,
1063
+ diagnostics,
1064
+ exitCode: diagnostics.some((diag) => diag.severity === "error") ? 1 : 0,
1065
+ };
1066
+ }
1067
+
1068
+ const plan = buildPlan(options);
1069
+ const errors = plan.diagnostics.filter((diag) => diag.severity === "error");
1070
+ const planPathWritten = options.plan ? writePlanFiles(options.workspaceRoot, plan) : undefined;
1071
+
1072
+ if (errors.length > 0) {
1073
+ return failResult(plan.diagnostics, plan);
1074
+ }
1075
+
1076
+ if (options.dryRun || !options.apply) {
1077
+ return {
1078
+ ok: true,
1079
+ plan,
1080
+ planPath: planPathWritten,
1081
+ diagnostics: plan.diagnostics,
1082
+ exitCode: 0,
1083
+ };
1084
+ }
1085
+
1086
+ const applyDiagnostics = applyPlan(options.workspaceRoot, plan, options.force);
1087
+ const diagnostics = [...plan.diagnostics, ...applyDiagnostics];
1088
+ const ok = !diagnostics.some((diag) => diag.severity === "error");
1089
+ return {
1090
+ ok,
1091
+ plan,
1092
+ applied: ok,
1093
+ planPath: planPathWritten,
1094
+ diagnostics,
1095
+ exitCode: ok ? 0 : 1,
1096
+ };
1097
+ }
1098
+
1099
+ export function parseMakeFields(raw: string | undefined): string[] {
1100
+ return raw ? splitTopLevel(raw) : [];
1101
+ }