forgeos 0.1.0-alpha.0 → 0.1.0-alpha.2

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 (283) hide show
  1. package/.npmignore +9 -1
  2. package/AGENTS.md +6 -1
  3. package/CHANGELOG.md +30 -0
  4. package/CONTRIBUTING.md +22 -1
  5. package/README.md +30 -3
  6. package/bin/forge.mjs +4 -3
  7. package/package.json +3 -1
  8. package/packages/eslint-plugin-forge/index.ts +15 -15
  9. package/packages/eslint-plugin-forge/package.json +10 -10
  10. package/packages/eslint-plugin-forge/src/check-source.ts +95 -95
  11. package/packages/eslint-plugin-forge/src/load-artifacts.ts +24 -24
  12. package/packages/eslint-plugin-forge/src/rule-no-forge-guard-violation.ts +93 -93
  13. package/src/forge/_generated/actionSubscriptions.json +2 -2
  14. package/src/forge/_generated/actionSubscriptions.ts +3 -3
  15. package/src/forge/_generated/agentAdapterManifest.json +2 -2
  16. package/src/forge/_generated/agentAdapterManifest.ts +3 -3
  17. package/src/forge/_generated/agentContract.json +2 -2
  18. package/src/forge/_generated/agentContract.ts +6786 -2
  19. package/src/forge/_generated/agentQuickstart.md +1 -1
  20. package/src/forge/_generated/aiContext.ts +1 -1
  21. package/src/forge/_generated/aiModels.json +1 -1
  22. package/src/forge/_generated/aiModels.ts +1 -1
  23. package/src/forge/_generated/aiProviders.json +1 -1
  24. package/src/forge/_generated/aiProviders.ts +1 -1
  25. package/src/forge/_generated/aiRegistry.json +2 -2
  26. package/src/forge/_generated/aiRegistry.ts +3 -3
  27. package/src/forge/_generated/api.json +2 -2
  28. package/src/forge/_generated/api.ts +1 -1
  29. package/src/forge/_generated/appGraph.json +2 -2
  30. package/src/forge/_generated/appGraph.ts +1297 -1141
  31. package/src/forge/_generated/appMap.md +1 -1
  32. package/src/forge/_generated/artifactManifest.json +2 -2
  33. package/src/forge/_generated/artifactManifest.ts +2 -2
  34. package/src/forge/_generated/authClaims.json +1 -1
  35. package/src/forge/_generated/authClaims.ts +1 -1
  36. package/src/forge/_generated/authConfig.json +1 -1
  37. package/src/forge/_generated/authConfig.ts +1 -1
  38. package/src/forge/_generated/authContext.ts +1 -1
  39. package/src/forge/_generated/authRegistry.json +1 -1
  40. package/src/forge/_generated/authRegistry.ts +1 -1
  41. package/src/forge/_generated/buildInfo.json +2 -2
  42. package/src/forge/_generated/buildInfo.ts +4 -4
  43. package/src/forge/_generated/capabilityMap.json +2 -2
  44. package/src/forge/_generated/capabilityMap.md +1 -1
  45. package/src/forge/_generated/capabilityMap.ts +2 -2
  46. package/src/forge/_generated/client.ts +1 -1
  47. package/src/forge/_generated/clientApi.ts +1 -1
  48. package/src/forge/_generated/clientManifest.json +2 -2
  49. package/src/forge/_generated/clientManifest.ts +3 -3
  50. package/src/forge/_generated/clientTypes.ts +1 -1
  51. package/src/forge/_generated/configRegistry.json +1 -1
  52. package/src/forge/_generated/configRegistry.ts +1 -1
  53. package/src/forge/_generated/dataGraph.json +2 -2
  54. package/src/forge/_generated/dataGraph.ts +3 -3
  55. package/src/forge/_generated/db.json +1 -1
  56. package/src/forge/_generated/db.ts +1 -1
  57. package/src/forge/_generated/dbSecurityManifest.json +1 -1
  58. package/src/forge/_generated/dbSecurityManifest.ts +1 -1
  59. package/src/forge/_generated/dbSessionContext.json +1 -1
  60. package/src/forge/_generated/dbSessionContext.ts +1 -1
  61. package/src/forge/_generated/deployManifest.json +2 -2
  62. package/src/forge/_generated/deployManifest.ts +7 -7
  63. package/src/forge/_generated/devManifest.json +2 -2
  64. package/src/forge/_generated/devManifest.ts +3 -3
  65. package/src/forge/_generated/envSchema.json +1 -1
  66. package/src/forge/_generated/envSchema.ts +1 -1
  67. package/src/forge/_generated/frontendGraph.json +1 -1
  68. package/src/forge/_generated/frontendGraph.ts +1 -1
  69. package/src/forge/_generated/importGuards.json +2 -2
  70. package/src/forge/_generated/importGuards.ts +35 -1
  71. package/src/forge/_generated/index.ts +1 -1
  72. package/src/forge/_generated/liveProductionManifest.json +1 -1
  73. package/src/forge/_generated/liveProductionManifest.ts +1 -1
  74. package/src/forge/_generated/liveProtocol.json +1 -1
  75. package/src/forge/_generated/liveProtocol.ts +1 -1
  76. package/src/forge/_generated/liveQueryRegistry.json +2 -2
  77. package/src/forge/_generated/liveQueryRegistry.ts +3 -3
  78. package/src/forge/_generated/liveTransportConfig.json +1 -1
  79. package/src/forge/_generated/liveTransportConfig.ts +1 -1
  80. package/src/forge/_generated/makeRegistry.json +2 -2
  81. package/src/forge/_generated/makeRegistry.ts +2 -2
  82. package/src/forge/_generated/makeTemplates.json +1 -1
  83. package/src/forge/_generated/makeTemplates.ts +1 -1
  84. package/src/forge/_generated/mockMap.json +1 -1
  85. package/src/forge/_generated/mockMap.ts +1 -1
  86. package/src/forge/_generated/operationPlaybooks.md +7 -5
  87. package/src/forge/_generated/packageGraph.json +2 -2
  88. package/src/forge/_generated/packageGraph.ts +90964 -14284
  89. package/src/forge/_generated/packageUpgradeRegistry.json +2 -2
  90. package/src/forge/_generated/packageUpgradeRegistry.ts +2 -2
  91. package/src/forge/_generated/permissionMatrix.json +2 -2
  92. package/src/forge/_generated/permissionMatrix.ts +3 -3
  93. package/src/forge/_generated/policyRegistry.json +2 -2
  94. package/src/forge/_generated/policyRegistry.ts +3 -3
  95. package/src/forge/_generated/queryRegistry.json +2 -2
  96. package/src/forge/_generated/queryRegistry.ts +3 -3
  97. package/src/forge/_generated/react.d.ts +1 -1
  98. package/src/forge/_generated/react.ts +1 -1
  99. package/src/forge/_generated/reactManifest.json +2 -2
  100. package/src/forge/_generated/reactManifest.ts +3 -3
  101. package/src/forge/_generated/releaseManifest.json +2 -2
  102. package/src/forge/_generated/releaseManifest.ts +3 -3
  103. package/src/forge/_generated/rlsPolicies.json +1 -1
  104. package/src/forge/_generated/rlsPolicies.sql +1 -1
  105. package/src/forge/_generated/rlsPolicies.ts +1 -1
  106. package/src/forge/_generated/runtimeGraph.json +2 -2
  107. package/src/forge/_generated/runtimeGraph.ts +3 -3
  108. package/src/forge/_generated/runtimeMatrix.json +2 -2
  109. package/src/forge/_generated/runtimeMatrix.ts +106177 -7917
  110. package/src/forge/_generated/runtimeRegistry.ts +1 -1
  111. package/src/forge/_generated/runtimeRules.md +1 -1
  112. package/src/forge/_generated/secretRegistry.json +1 -1
  113. package/src/forge/_generated/secretRegistry.ts +1 -1
  114. package/src/forge/_generated/secretsContext.ts +1 -1
  115. package/src/forge/_generated/serverApi.ts +1 -1
  116. package/src/forge/_generated/sourceMapManifest.json +2 -2
  117. package/src/forge/_generated/sourceMapManifest.ts +2 -2
  118. package/src/forge/_generated/sqlPlan.json +1 -1
  119. package/src/forge/_generated/sqlPlan.ts +1 -1
  120. package/src/forge/_generated/subscriptionManifest.json +2 -2
  121. package/src/forge/_generated/subscriptionManifest.ts +3 -3
  122. package/src/forge/_generated/symbolicationManifest.json +2 -2
  123. package/src/forge/_generated/symbolicationManifest.ts +2 -2
  124. package/src/forge/_generated/telemetryRegistry.json +2 -2
  125. package/src/forge/_generated/telemetryRegistry.ts +3 -3
  126. package/src/forge/_generated/telemetrySinks.json +2 -2
  127. package/src/forge/_generated/telemetrySinks.ts +2 -2
  128. package/src/forge/_generated/tenantScope.json +2 -2
  129. package/src/forge/_generated/tenantScope.ts +3 -3
  130. package/src/forge/_generated/testGraph.json +2 -2
  131. package/src/forge/_generated/testGraph.ts +129 -75
  132. package/src/forge/_generated/testPlanRegistry.json +2 -2
  133. package/src/forge/_generated/testPlanRegistry.ts +2 -2
  134. package/src/forge/_generated/uiRoutes.json +1 -1
  135. package/src/forge/_generated/uiRoutes.ts +1 -1
  136. package/src/forge/_generated/uiScenarios.json +1 -1
  137. package/src/forge/_generated/uiScenarios.ts +1 -1
  138. package/src/forge/_generated/uiTestManifest.json +2 -2
  139. package/src/forge/_generated/uiTestManifest.ts +2 -2
  140. package/src/forge/_generated/workflowRegistry.json +2 -2
  141. package/src/forge/_generated/workflowRegistry.ts +3 -3
  142. package/src/forge/_generated/workflowSubscriptions.json +2 -2
  143. package/src/forge/_generated/workflowSubscriptions.ts +3 -3
  144. package/src/forge/cli/commands.ts +861 -861
  145. package/src/forge/cli/deps.ts +178 -11
  146. package/src/forge/cli/dev.ts +32 -5
  147. package/src/forge/cli/index.ts +7 -7
  148. package/src/forge/cli/main.ts +54 -54
  149. package/src/forge/cli/new.ts +29 -1
  150. package/src/forge/cli/output.ts +97 -97
  151. package/src/forge/cli/parse.ts +679 -673
  152. package/src/forge/cli/version.ts +1 -1
  153. package/src/forge/compiler/agent-contract/build.ts +28 -0
  154. package/src/forge/compiler/agent-contract/types.ts +16 -0
  155. package/src/forge/compiler/app-graph/build.ts +112 -112
  156. package/src/forge/compiler/app-graph/classify.ts +10 -10
  157. package/src/forge/compiler/app-graph/dup-symbol.ts +29 -29
  158. package/src/forge/compiler/app-graph/extract.ts +123 -123
  159. package/src/forge/compiler/app-graph/forge-apis.ts +29 -29
  160. package/src/forge/compiler/app-graph/index.ts +11 -11
  161. package/src/forge/compiler/app-graph/module-graph.ts +316 -316
  162. package/src/forge/compiler/app-graph/parser.ts +119 -119
  163. package/src/forge/compiler/app-graph/symbols.ts +48 -48
  164. package/src/forge/compiler/app-graph/tsconfig-hash.ts +62 -62
  165. package/src/forge/compiler/app-graph/types.ts +43 -43
  166. package/src/forge/compiler/app-graph/versions.ts +14 -14
  167. package/src/forge/compiler/cache/index.ts +17 -17
  168. package/src/forge/compiler/cache/key.ts +46 -46
  169. package/src/forge/compiler/cache/scheduler.ts +72 -72
  170. package/src/forge/compiler/cache/store.ts +78 -78
  171. package/src/forge/compiler/classifier/capabilities.ts +78 -78
  172. package/src/forge/compiler/classifier/classify.ts +113 -113
  173. package/src/forge/compiler/classifier/contexts.ts +188 -188
  174. package/src/forge/compiler/classifier/index.ts +18 -18
  175. package/src/forge/compiler/classifier/runtime-matrix.ts +45 -45
  176. package/src/forge/compiler/classifier/secrets.ts +41 -41
  177. package/src/forge/compiler/classifier/signals.ts +129 -129
  178. package/src/forge/compiler/diagnostics/codes.ts +125 -120
  179. package/src/forge/compiler/diagnostics/create.ts +87 -87
  180. package/src/forge/compiler/diagnostics/index.ts +41 -41
  181. package/src/forge/compiler/emitter/artifact-kind.ts +14 -14
  182. package/src/forge/compiler/emitter/barrel.ts +38 -38
  183. package/src/forge/compiler/emitter/constants.ts +7 -7
  184. package/src/forge/compiler/emitter/emit.ts +234 -237
  185. package/src/forge/compiler/emitter/index.ts +24 -24
  186. package/src/forge/compiler/emitter/lock.ts +61 -61
  187. package/src/forge/compiler/emitter/render.ts +73 -73
  188. package/src/forge/compiler/guards/artifacts.ts +96 -96
  189. package/src/forge/compiler/guards/check-import-guards.ts +106 -106
  190. package/src/forge/compiler/guards/index.ts +11 -11
  191. package/src/forge/compiler/guards/propagate-contexts.ts +57 -57
  192. package/src/forge/compiler/index.ts +17 -17
  193. package/src/forge/compiler/integration/add.ts +493 -493
  194. package/src/forge/compiler/integration/index.ts +17 -17
  195. package/src/forge/compiler/integration/plan.ts +279 -279
  196. package/src/forge/compiler/integration/render.ts +189 -189
  197. package/src/forge/compiler/integration/snapshot.ts +52 -52
  198. package/src/forge/compiler/orchestrator/discover.ts +214 -214
  199. package/src/forge/compiler/orchestrator/guards.ts +5 -5
  200. package/src/forge/compiler/orchestrator/index.ts +27 -27
  201. package/src/forge/compiler/orchestrator/manifest.ts +69 -69
  202. package/src/forge/compiler/orchestrator/orphans.ts +51 -51
  203. package/src/forge/compiler/orchestrator/plan.ts +804 -804
  204. package/src/forge/compiler/orchestrator/run.ts +178 -178
  205. package/src/forge/compiler/orchestrator/serialize.ts +859 -859
  206. package/src/forge/compiler/orchestrator/types.ts +23 -23
  207. package/src/forge/compiler/orchestrator/verify.ts +35 -35
  208. package/src/forge/compiler/package-graph/capabilities-stub.ts +33 -33
  209. package/src/forge/compiler/package-graph/checksum.ts +107 -97
  210. package/src/forge/compiler/package-graph/compiler.ts +444 -363
  211. package/src/forge/compiler/package-graph/constants.ts +4 -4
  212. package/src/forge/compiler/package-graph/exports-discovery.ts +91 -84
  213. package/src/forge/compiler/package-graph/extract-dts.ts +32 -32
  214. package/src/forge/compiler/package-graph/index.ts +24 -24
  215. package/src/forge/compiler/package-graph/jsdoc.ts +50 -50
  216. package/src/forge/compiler/package-graph/oracle.ts +326 -0
  217. package/src/forge/compiler/package-graph/read-file.ts +21 -21
  218. package/src/forge/compiler/package-graph/resolve.ts +131 -127
  219. package/src/forge/compiler/package-manager/adapter.ts +232 -232
  220. package/src/forge/compiler/package-manager/commands.ts +47 -47
  221. package/src/forge/compiler/package-manager/detect.ts +65 -65
  222. package/src/forge/compiler/package-manager/executor.ts +29 -29
  223. package/src/forge/compiler/package-manager/index.ts +22 -22
  224. package/src/forge/compiler/package-manager/parse-spec.ts +16 -16
  225. package/src/forge/compiler/package-manager/version.ts +20 -20
  226. package/src/forge/compiler/primitives/compare.ts +26 -26
  227. package/src/forge/compiler/primitives/hash.ts +42 -33
  228. package/src/forge/compiler/primitives/header.ts +43 -43
  229. package/src/forge/compiler/primitives/index.ts +45 -45
  230. package/src/forge/compiler/primitives/paths.ts +24 -24
  231. package/src/forge/compiler/primitives/serialize.ts +66 -66
  232. package/src/forge/compiler/primitives/sort.ts +87 -87
  233. package/src/forge/compiler/recipes/definitions.ts +269 -269
  234. package/src/forge/compiler/recipes/helpers.ts +37 -37
  235. package/src/forge/compiler/recipes/index.ts +21 -21
  236. package/src/forge/compiler/recipes/registry.ts +87 -87
  237. package/src/forge/compiler/sandbox/artifact-sanitize.ts +26 -26
  238. package/src/forge/compiler/sandbox/backends/child.ts +123 -123
  239. package/src/forge/compiler/sandbox/backends/docker.ts +173 -173
  240. package/src/forge/compiler/sandbox/index.ts +51 -51
  241. package/src/forge/compiler/sandbox/inspect.ts +143 -143
  242. package/src/forge/compiler/sandbox/inspector-entry.ts +115 -115
  243. package/src/forge/compiler/sandbox/limits.ts +31 -31
  244. package/src/forge/compiler/sandbox/scrub-env.ts +60 -60
  245. package/src/forge/compiler/sandbox/secret-scan.ts +54 -54
  246. package/src/forge/compiler/sandbox/serialize.ts +106 -106
  247. package/src/forge/compiler/sandbox/types.ts +7 -7
  248. package/src/forge/compiler/types/app-graph.ts +71 -71
  249. package/src/forge/compiler/types/capability.ts +29 -29
  250. package/src/forge/compiler/types/classification.ts +9 -9
  251. package/src/forge/compiler/types/cli.ts +85 -85
  252. package/src/forge/compiler/types/diagnostic.ts +2 -2
  253. package/src/forge/compiler/types/emit.ts +25 -25
  254. package/src/forge/compiler/types/import-guards.ts +19 -19
  255. package/src/forge/compiler/types/index.ts +98 -98
  256. package/src/forge/compiler/types/integration.ts +25 -25
  257. package/src/forge/compiler/types/json.ts +3 -3
  258. package/src/forge/compiler/types/lock.ts +37 -37
  259. package/src/forge/compiler/types/package-graph.ts +122 -77
  260. package/src/forge/compiler/types/runtime-matrix.ts +16 -16
  261. package/src/forge/compiler/types/runtime.ts +30 -30
  262. package/src/forge/compiler/types/sandbox.ts +24 -24
  263. package/src/forge/dev/server.ts +16 -2
  264. package/src/forge/refactor/index.ts +10 -2
  265. package/src/forge/refactor/runtime-rename.ts +598 -0
  266. package/src/forge/runtime/executor.ts +3 -2
  267. package/src/forge/runtime/live/live-query-runner.ts +2 -1
  268. package/src/forge/runtime/outbox/process.ts +2 -1
  269. package/src/forge/runtime/query/run-query.ts +2 -1
  270. package/src/forge/runtime/runner/run-entry.ts +2 -1
  271. package/src/forge/runtime/telemetry/sinks/posthog.ts +4 -5
  272. package/src/forge/runtime/telemetry/sinks/sentry.ts +4 -5
  273. package/src/forge/runtime/workflows/resolve-step.ts +2 -1
  274. package/src/forge/version.ts +3 -0
  275. package/templates/b2b-support-web/src/actions/captureTicketCreated.ts +7 -2
  276. package/templates/b2b-support-web/src/commands/closeTicket.ts +6 -1
  277. package/templates/b2b-support-web/src/commands/createTicket.ts +8 -2
  278. package/templates/b2b-support-web/src/queries/getTicket.ts +8 -1
  279. package/templates/b2b-support-web/web/components/CreateTicketForm.tsx +1 -2
  280. package/templates/b2b-support-web/web/components/PolicyDeniedDemo.tsx +1 -2
  281. package/templates/b2b-support-web/web/components/TicketList.tsx +1 -2
  282. package/templates/b2b-support-web/web/components/TraceDetails.tsx +1 -1
  283. package/templates/b2b-support-web/web/lib/forge.ts +1 -0
@@ -1,496 +1,496 @@
1
1
  import { join } from "node:path";
2
2
  import { nodeFileSystem } from "../fs/index.ts";
3
3
  import type { AddOptions } from "../types/cli.ts";
4
- import type { Diagnostic } from "../types/diagnostic.ts";
5
- import type { Dependency } from "../types/package-graph.ts";
6
- import type { ClassifiedPackage } from "../classifier/runtime-matrix.ts";
7
- import { buildAppGraph } from "../app-graph/build.ts";
8
- import { classify } from "../classifier/classify.ts";
9
- import { createDiagnostic } from "../diagnostics/create.ts";
10
- import { emit } from "../emitter/emit.ts";
11
- import {
12
- FORGE_LOCK_SCHEMA_VERSION,
13
- GENERATOR_VERSION,
14
- } from "../emitter/constants.ts";
15
- import { PACKAGE_ANALYZER_VERSION } from "../package-graph/constants.ts";
16
- import { renderBody } from "../emitter/render.ts";
17
- import { hashStable } from "../primitives/hash.ts";
18
- import { PackageGraphCompiler } from "../package-graph/compiler.ts";
19
- import {
20
- detectAndCreatePackageManagerAdapter,
21
- dryRunRecipeFallbackMessage,
22
- type PackageManagerAdapter,
23
- } from "../package-manager/adapter.ts";
24
- import { PackageManagerCommandError } from "../package-manager/executor.ts";
25
- import {
26
- isReferenceAlias,
27
- resolveByPackageName,
28
- resolveRecipe,
29
- } from "../recipes/registry.ts";
30
- import { discover } from "../orchestrator/discover.ts";
31
- import {
32
- loadManifest,
33
- saveManifest,
34
- updateManifestAfterWrite,
35
- } from "../orchestrator/manifest.ts";
36
- import { verifyLockIntegrity } from "../orchestrator/verify.ts";
37
- import {
38
- buildIntegrationEmitPlan,
39
- loadExistingForgeLock,
40
- } from "./plan.ts";
41
- import {
42
- restoreVersionControlledSnapshot,
43
- snapshotVersionControlled,
44
- } from "./snapshot.ts";
45
-
46
- export interface ForgeAddOptions extends AddOptions {
47
- workspaceRoot: string;
48
- pmAdapter?: PackageManagerAdapter;
49
- }
50
-
51
- export interface ForgeAddResult {
52
- alias: string;
53
- changed: string[];
54
- unchanged: string[];
55
- warnings: Diagnostic[];
56
- errors: Diagnostic[];
57
- exitCode: 0 | 1;
58
- failureKind?: string;
59
- }
60
-
61
- function failureKind(errors: Diagnostic[]): string | undefined {
62
- if (errors.length === 0) {
63
- return undefined;
64
- }
65
- const first = errors[0];
66
- if (first?.code === "FORGE_UNKNOWN_ALIAS") {
67
- return "unknown_alias";
68
- }
69
- if (first?.code === "FORGE_ADD_INSTALL_FAILED") {
70
- return "install_failed";
71
- }
72
- if (errors.some((error) => error.code === "FORGE_WRITE_ERROR")) {
73
- return "write_failed";
74
- }
75
- if (errors.some((error) => error.code === "FORGE_LOCK_INTEGRITY")) {
76
- return "lock_integrity";
77
- }
78
- return "error";
79
- }
80
-
81
- function collectAllClassified(
82
- ctx: ReturnType<typeof discover>,
83
- cacheDir: string,
84
- runtimeInspect: boolean,
85
- sandboxBackend: ForgeAddOptions["sandboxBackend"],
86
- ): Promise<ClassifiedPackage[]> {
87
- const compiler = new PackageGraphCompiler();
88
- return Promise.all(
89
- ctx.dependencies.map(async (dep) => {
90
- const recipe = resolveByPackageName(dep.name) ?? undefined;
91
- const api = await compiler.analyze(dep, {
92
- runtimeInspect,
93
- sandboxBackend,
94
- resolutionMode: "nodenext",
95
- cacheDir,
96
- recipeVersion: recipe?.recipeVersion,
97
- });
98
- return {
99
- api,
100
- classification: classify(api, recipe),
101
- recipe,
102
- };
103
- }),
104
- );
105
- }
106
-
107
- function dependencyFromInstall(
108
- packageName: string,
109
- version: string,
110
- workspaceRoot: string,
111
- packageManager: Dependency["packageManager"],
112
- installRoot = workspaceRoot,
113
- ): Dependency {
114
- return {
115
- name: packageName,
116
- version,
117
- packageManager,
118
- installPath: join(installRoot, "node_modules", ...packageName.split("/")),
119
- };
120
- }
121
-
122
- async function analyzeRecipePackages(
123
- recipe: NonNullable<ReturnType<typeof resolveRecipe>>,
124
- ctx: ReturnType<typeof discover>,
125
- installRoot: string,
126
- options: ForgeAddOptions,
127
- ): Promise<{ classified: ClassifiedPackage[]; diagnostics: Diagnostic[] }> {
128
- const compiler = new PackageGraphCompiler();
129
- const classified: ClassifiedPackage[] = [];
130
- const diagnostics: Diagnostic[] = [];
131
-
132
- for (const pkg of recipe.packages) {
133
- const dep = dependencyFromInstall(
134
- pkg.packageName,
135
- "0.0.0",
136
- ctx.workspaceRoot,
137
- ctx.packageManager,
138
- installRoot,
139
- );
140
-
141
- const pkgJsonPath = join(dep.installPath, "package.json");
142
- if (nodeFileSystem.exists(pkgJsonPath)) {
143
- const installed = JSON.parse((nodeFileSystem.readText(pkgJsonPath) ?? "")) as {
144
- version?: string;
145
- };
146
- if (installed.version) {
147
- dep.version = installed.version;
148
- }
149
- }
150
-
151
- try {
152
- const result = await compiler.analyze(dep, {
153
- runtimeInspect: options.runtimeInspect,
154
- sandboxBackend: options.sandboxBackend,
155
- resolutionMode: "nodenext",
156
- cacheDir: ctx.cacheDir,
157
- recipeVersion: recipe.recipeVersion,
158
- });
159
- classified.push({
160
- api: result,
161
- classification: classify(result, recipe),
162
- recipe,
163
- });
164
- } catch (error) {
165
- diagnostics.push(
166
- createDiagnostic({
167
- severity: "error",
168
- code: "FORGE_ADD_ANALYZE_FAILED",
169
- message: `failed to analyze ${pkg.packageName}: ${error instanceof Error ? error.message : String(error)}`,
170
- }),
171
- );
172
- }
173
- }
174
-
175
- return { classified, diagnostics };
176
- }
177
-
178
- async function buildAddPlan(
179
- alias: string,
180
- recipe: NonNullable<ReturnType<typeof resolveRecipe>>,
181
- ctx: ReturnType<typeof discover>,
182
- installRoot: string,
183
- options: ForgeAddOptions,
184
- ): Promise<{
185
- emitPlan: ReturnType<typeof buildIntegrationEmitPlan>;
186
- warnings: Diagnostic[];
187
- errors: Diagnostic[];
188
- }> {
189
- const manifest = loadManifest(ctx.cacheDir);
190
- const appGraph = await buildAppGraph({
191
- workspaceRoot: ctx.workspaceRoot,
192
- sources: ctx.sources,
193
- prior: manifest.priorAppGraph,
194
- tsconfigPath: ctx.tsconfigPath ?? undefined,
195
- });
196
-
197
- const { classified, diagnostics } = await analyzeRecipePackages(
198
- recipe,
199
- ctx,
200
- installRoot,
201
- options,
202
- );
203
-
204
- const errors = diagnostics.filter((item) => item.severity === "error");
205
- const warnings = [
206
- ...appGraph.diagnostics.filter((item) => item.severity === "warning"),
207
- ...diagnostics.filter((item) => item.severity === "warning"),
208
- ];
209
-
210
- if (classified.length === 0) {
211
- errors.push(
212
- createDiagnostic({
213
- severity: "error",
214
- code: "FORGE_ADD_ANALYZE_FAILED",
215
- message: `no packages analyzed for alias '${alias}'`,
216
- }),
217
- );
218
- return {
219
- emitPlan: {
220
- files: [],
221
- orphanedFiles: [],
222
- lock: loadExistingForgeLock(options.workspaceRoot) ?? {
223
- schemaVersion: FORGE_LOCK_SCHEMA_VERSION,
224
- generatorVersion: GENERATOR_VERSION,
225
- analyzerVersion: PACKAGE_ANALYZER_VERSION,
226
- inputHash: ctx.inputFingerprint,
227
- lockfileHash: ctx.lockfileHash,
228
- packageManager: ctx.packageManager,
229
- packages: [],
230
- },
231
- },
232
- warnings,
233
- errors,
234
- };
235
- }
236
-
237
- const allClassified = await collectAllClassified(
238
- discover({ workspaceRoot: options.workspaceRoot }),
239
- ctx.cacheDir,
240
- false,
241
- "none",
242
- );
243
-
244
- const emitPlan = buildIntegrationEmitPlan({
245
- alias,
246
- recipe,
247
- classified,
248
- allClassified,
249
- appGraph,
250
- ctx,
251
- existingLock: loadExistingForgeLock(options.workspaceRoot),
252
- });
253
-
254
- return { emitPlan, warnings, errors };
255
- }
256
-
257
- export async function forgeAdd(
258
- alias: string,
259
- options: ForgeAddOptions,
260
- ): Promise<ForgeAddResult> {
261
- const normalized = alias.trim().toLowerCase();
262
- const recipe = resolveRecipe(normalized);
263
-
264
- if (!isReferenceAlias(normalized) || recipe === null) {
265
- const error = createDiagnostic({
266
- severity: "error",
267
- code: "FORGE_UNKNOWN_ALIAS",
268
- message: `unknown integration alias '${alias}'; supported: stripe, posthog, sentry, zod, ai`,
269
- });
270
- return {
271
- alias: normalized,
272
- changed: [],
273
- unchanged: [],
274
- warnings: [],
275
- errors: [error],
276
- exitCode: 1,
277
- failureKind: "unknown_alias",
278
- };
279
- }
280
-
281
- const pm =
282
- options.pmAdapter ??
283
- detectAndCreatePackageManagerAdapter(options.workspaceRoot);
284
-
285
- if (options.dryRun) {
286
- const ctx = discover({ workspaceRoot: options.workspaceRoot });
287
- let installRoot = options.workspaceRoot;
288
-
289
- try {
290
- const dryRun = await pm.dryRunAddWithPath(
291
- recipe.packages.map((pkg) => pkg.packageName).join(" "),
292
- {
293
- cwd: options.workspaceRoot,
294
- ignoreScripts: !options.allowScripts,
295
- },
296
- );
297
- installRoot = dryRun.installPath;
298
- } catch {
299
- const fallback = dryRunRecipeFallbackMessage(normalized);
300
- const { emitPlan, warnings, errors } = await buildAddPlan(
301
- normalized,
302
- recipe,
303
- ctx,
304
- installRoot,
305
- options,
306
- );
307
- warnings.push(
308
- createDiagnostic({
309
- severity: "warning",
310
- code: "FORGE_DRY_RUN_FALLBACK",
311
- message: fallback,
312
- }),
313
- );
314
-
315
- return {
316
- alias: normalized,
317
- changed: [...emitPlan.files.map((file) => file.path), "forge.lock"],
318
- unchanged: [],
319
- warnings,
320
- errors,
321
- exitCode: errors.length > 0 ? 1 : 0,
322
- failureKind: failureKind(errors),
323
- };
324
- }
325
-
326
- const { emitPlan, warnings, errors } = await buildAddPlan(
327
- normalized,
328
- recipe,
329
- ctx,
330
- installRoot,
331
- options,
332
- );
333
-
334
- return {
335
- alias: normalized,
336
- changed: [...emitPlan.files.map((file) => file.path), "forge.lock"],
337
- unchanged: [],
338
- warnings,
339
- errors,
340
- exitCode: errors.length > 0 ? 1 : 0,
341
- failureKind: failureKind(errors),
342
- };
343
- }
344
-
345
- const snapshot = snapshotVersionControlled(options.workspaceRoot);
346
-
347
- try {
348
- for (const pkg of recipe.packages) {
349
- await pm.add(pkg.packageName, {
350
- cwd: options.workspaceRoot,
351
- ignoreScripts: !options.allowScripts,
352
- });
353
- }
354
-
355
- const ctx = discover({ workspaceRoot: options.workspaceRoot });
356
- const { emitPlan, warnings, errors: analyzeErrors } = await buildAddPlan(
357
- normalized,
358
- recipe,
359
- ctx,
360
- options.workspaceRoot,
361
- options,
362
- );
363
-
364
- if (analyzeErrors.length > 0) {
365
- restoreVersionControlledSnapshot(options.workspaceRoot, snapshot);
366
- return {
367
- alias: normalized,
368
- changed: [],
369
- unchanged: [],
370
- warnings,
371
- errors: analyzeErrors,
372
- exitCode: 1,
373
- failureKind: failureKind(analyzeErrors),
374
- };
375
- }
376
-
377
- const emitResult = await emit(emitPlan, {
378
- workspaceRoot: options.workspaceRoot,
379
- mode: "write",
380
- });
381
-
382
- const warningsCombined = [...warnings, ...emitResult.warnings];
383
- const errors = [...analyzeErrors, ...emitResult.errors];
384
-
385
- if (errors.length > 0) {
386
- restoreVersionControlledSnapshot(options.workspaceRoot, snapshot);
387
- return {
388
- alias: normalized,
389
- changed: [],
390
- unchanged: [],
391
- warnings: warningsCombined,
392
- errors,
393
- exitCode: 1,
394
- failureKind: failureKind(errors),
395
- };
396
- }
397
-
398
- const integrityErrors = verifyLockIntegrity(
399
- options.workspaceRoot,
400
- emitPlan.lock,
401
- );
402
- if (integrityErrors.length > 0) {
403
- restoreVersionControlledSnapshot(options.workspaceRoot, snapshot);
404
- return {
405
- alias: normalized,
406
- changed: [],
407
- unchanged: [],
408
- warnings: warningsCombined,
409
- errors: integrityErrors,
410
- exitCode: 1,
411
- failureKind: "lock_integrity",
412
- };
413
- }
414
-
415
- const manifest = loadManifest(ctx.cacheDir);
416
- const appGraph = await buildAppGraph({
417
- workspaceRoot: ctx.workspaceRoot,
418
- sources: ctx.sources,
419
- prior: manifest.priorAppGraph,
420
- tsconfigPath: ctx.tsconfigPath ?? undefined,
421
- });
422
-
423
- saveManifest(
424
- ctx.cacheDir,
425
- updateManifestAfterWrite(
426
- manifest,
427
- Object.fromEntries(
428
- emitPlan.files.map((file) => [
429
- file.path,
430
- hashStable(renderBody(file)),
431
- ]),
432
- ),
433
- appGraph,
434
- ctx.inputFingerprint,
435
- ),
436
- );
437
-
438
- return {
439
- alias: normalized,
440
- changed: emitResult.changed,
441
- unchanged: emitResult.unchanged,
442
- warnings: warningsCombined,
443
- errors: [],
444
- exitCode: 0,
445
- };
446
- } catch (error) {
447
- restoreVersionControlledSnapshot(options.workspaceRoot, snapshot);
448
-
449
- const message =
450
- error instanceof PackageManagerCommandError
451
- ? error.message
452
- : error instanceof Error
453
- ? error.message
454
- : String(error);
455
-
456
- const diagnostic = createDiagnostic({
457
- severity: "error",
458
- code: "FORGE_ADD_INSTALL_FAILED",
459
- message: `forge add failed: ${message}`,
460
- });
461
-
462
- return {
463
- alias: normalized,
464
- changed: [],
465
- unchanged: [],
466
- warnings: [],
467
- errors: [diagnostic],
468
- exitCode: 1,
469
- failureKind: "install_failed",
470
- };
471
- }
472
- }
473
-
474
- /** Test helper: seed fixture packages into node_modules and update package.json. */
475
- export function seedWorkspacePackage(
476
- workspaceRoot: string,
477
- packageName: string,
478
- fixtureRoot: string,
479
- ): void {
480
- const segments = packageName.startsWith("@")
481
- ? packageName.slice(1).split("/")
482
- : [packageName];
483
- const target = join(workspaceRoot, "node_modules", ...segments);
484
- nodeFileSystem.mkdirp(target);
485
- nodeFileSystem.copy(join(fixtureRoot, ...segments), target);
486
-
487
- const pkgJsonPath = join(workspaceRoot, "package.json");
488
- const pkg = JSON.parse((nodeFileSystem.readText(pkgJsonPath) ?? "")) as {
489
- dependencies?: Record<string, string>;
490
- };
491
- pkg.dependencies = {
492
- ...pkg.dependencies,
493
- [packageName]: "1.0.0",
494
- };
495
- nodeFileSystem.writeText(pkgJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
496
- }
4
+ import type { Diagnostic } from "../types/diagnostic.ts";
5
+ import type { Dependency } from "../types/package-graph.ts";
6
+ import type { ClassifiedPackage } from "../classifier/runtime-matrix.ts";
7
+ import { buildAppGraph } from "../app-graph/build.ts";
8
+ import { classify } from "../classifier/classify.ts";
9
+ import { createDiagnostic } from "../diagnostics/create.ts";
10
+ import { emit } from "../emitter/emit.ts";
11
+ import {
12
+ FORGE_LOCK_SCHEMA_VERSION,
13
+ GENERATOR_VERSION,
14
+ } from "../emitter/constants.ts";
15
+ import { PACKAGE_ANALYZER_VERSION } from "../package-graph/constants.ts";
16
+ import { renderBody } from "../emitter/render.ts";
17
+ import { hashStable } from "../primitives/hash.ts";
18
+ import { PackageGraphCompiler } from "../package-graph/compiler.ts";
19
+ import {
20
+ detectAndCreatePackageManagerAdapter,
21
+ dryRunRecipeFallbackMessage,
22
+ type PackageManagerAdapter,
23
+ } from "../package-manager/adapter.ts";
24
+ import { PackageManagerCommandError } from "../package-manager/executor.ts";
25
+ import {
26
+ isReferenceAlias,
27
+ resolveByPackageName,
28
+ resolveRecipe,
29
+ } from "../recipes/registry.ts";
30
+ import { discover } from "../orchestrator/discover.ts";
31
+ import {
32
+ loadManifest,
33
+ saveManifest,
34
+ updateManifestAfterWrite,
35
+ } from "../orchestrator/manifest.ts";
36
+ import { verifyLockIntegrity } from "../orchestrator/verify.ts";
37
+ import {
38
+ buildIntegrationEmitPlan,
39
+ loadExistingForgeLock,
40
+ } from "./plan.ts";
41
+ import {
42
+ restoreVersionControlledSnapshot,
43
+ snapshotVersionControlled,
44
+ } from "./snapshot.ts";
45
+
46
+ export interface ForgeAddOptions extends AddOptions {
47
+ workspaceRoot: string;
48
+ pmAdapter?: PackageManagerAdapter;
49
+ }
50
+
51
+ export interface ForgeAddResult {
52
+ alias: string;
53
+ changed: string[];
54
+ unchanged: string[];
55
+ warnings: Diagnostic[];
56
+ errors: Diagnostic[];
57
+ exitCode: 0 | 1;
58
+ failureKind?: string;
59
+ }
60
+
61
+ function failureKind(errors: Diagnostic[]): string | undefined {
62
+ if (errors.length === 0) {
63
+ return undefined;
64
+ }
65
+ const first = errors[0];
66
+ if (first?.code === "FORGE_UNKNOWN_ALIAS") {
67
+ return "unknown_alias";
68
+ }
69
+ if (first?.code === "FORGE_ADD_INSTALL_FAILED") {
70
+ return "install_failed";
71
+ }
72
+ if (errors.some((error) => error.code === "FORGE_WRITE_ERROR")) {
73
+ return "write_failed";
74
+ }
75
+ if (errors.some((error) => error.code === "FORGE_LOCK_INTEGRITY")) {
76
+ return "lock_integrity";
77
+ }
78
+ return "error";
79
+ }
80
+
81
+ function collectAllClassified(
82
+ ctx: ReturnType<typeof discover>,
83
+ cacheDir: string,
84
+ runtimeInspect: boolean,
85
+ sandboxBackend: ForgeAddOptions["sandboxBackend"],
86
+ ): Promise<ClassifiedPackage[]> {
87
+ const compiler = new PackageGraphCompiler();
88
+ return Promise.all(
89
+ ctx.dependencies.map(async (dep) => {
90
+ const recipe = resolveByPackageName(dep.name) ?? undefined;
91
+ const api = await compiler.analyze(dep, {
92
+ runtimeInspect,
93
+ sandboxBackend,
94
+ resolutionMode: "nodenext",
95
+ cacheDir,
96
+ recipeVersion: recipe?.recipeVersion,
97
+ });
98
+ return {
99
+ api,
100
+ classification: classify(api, recipe),
101
+ recipe,
102
+ };
103
+ }),
104
+ );
105
+ }
106
+
107
+ function dependencyFromInstall(
108
+ packageName: string,
109
+ version: string,
110
+ workspaceRoot: string,
111
+ packageManager: Dependency["packageManager"],
112
+ installRoot = workspaceRoot,
113
+ ): Dependency {
114
+ return {
115
+ name: packageName,
116
+ version,
117
+ packageManager,
118
+ installPath: join(installRoot, "node_modules", ...packageName.split("/")),
119
+ };
120
+ }
121
+
122
+ async function analyzeRecipePackages(
123
+ recipe: NonNullable<ReturnType<typeof resolveRecipe>>,
124
+ ctx: ReturnType<typeof discover>,
125
+ installRoot: string,
126
+ options: ForgeAddOptions,
127
+ ): Promise<{ classified: ClassifiedPackage[]; diagnostics: Diagnostic[] }> {
128
+ const compiler = new PackageGraphCompiler();
129
+ const classified: ClassifiedPackage[] = [];
130
+ const diagnostics: Diagnostic[] = [];
131
+
132
+ for (const pkg of recipe.packages) {
133
+ const dep = dependencyFromInstall(
134
+ pkg.packageName,
135
+ "0.0.0",
136
+ ctx.workspaceRoot,
137
+ ctx.packageManager,
138
+ installRoot,
139
+ );
140
+
141
+ const pkgJsonPath = join(dep.installPath, "package.json");
142
+ if (nodeFileSystem.exists(pkgJsonPath)) {
143
+ const installed = JSON.parse((nodeFileSystem.readText(pkgJsonPath) ?? "")) as {
144
+ version?: string;
145
+ };
146
+ if (installed.version) {
147
+ dep.version = installed.version;
148
+ }
149
+ }
150
+
151
+ try {
152
+ const result = await compiler.analyze(dep, {
153
+ runtimeInspect: options.runtimeInspect,
154
+ sandboxBackend: options.sandboxBackend,
155
+ resolutionMode: "nodenext",
156
+ cacheDir: ctx.cacheDir,
157
+ recipeVersion: recipe.recipeVersion,
158
+ });
159
+ classified.push({
160
+ api: result,
161
+ classification: classify(result, recipe),
162
+ recipe,
163
+ });
164
+ } catch (error) {
165
+ diagnostics.push(
166
+ createDiagnostic({
167
+ severity: "error",
168
+ code: "FORGE_ADD_ANALYZE_FAILED",
169
+ message: `failed to analyze ${pkg.packageName}: ${error instanceof Error ? error.message : String(error)}`,
170
+ }),
171
+ );
172
+ }
173
+ }
174
+
175
+ return { classified, diagnostics };
176
+ }
177
+
178
+ async function buildAddPlan(
179
+ alias: string,
180
+ recipe: NonNullable<ReturnType<typeof resolveRecipe>>,
181
+ ctx: ReturnType<typeof discover>,
182
+ installRoot: string,
183
+ options: ForgeAddOptions,
184
+ ): Promise<{
185
+ emitPlan: ReturnType<typeof buildIntegrationEmitPlan>;
186
+ warnings: Diagnostic[];
187
+ errors: Diagnostic[];
188
+ }> {
189
+ const manifest = loadManifest(ctx.cacheDir);
190
+ const appGraph = await buildAppGraph({
191
+ workspaceRoot: ctx.workspaceRoot,
192
+ sources: ctx.sources,
193
+ prior: manifest.priorAppGraph,
194
+ tsconfigPath: ctx.tsconfigPath ?? undefined,
195
+ });
196
+
197
+ const { classified, diagnostics } = await analyzeRecipePackages(
198
+ recipe,
199
+ ctx,
200
+ installRoot,
201
+ options,
202
+ );
203
+
204
+ const errors = diagnostics.filter((item) => item.severity === "error");
205
+ const warnings = [
206
+ ...appGraph.diagnostics.filter((item) => item.severity === "warning"),
207
+ ...diagnostics.filter((item) => item.severity === "warning"),
208
+ ];
209
+
210
+ if (classified.length === 0) {
211
+ errors.push(
212
+ createDiagnostic({
213
+ severity: "error",
214
+ code: "FORGE_ADD_ANALYZE_FAILED",
215
+ message: `no packages analyzed for alias '${alias}'`,
216
+ }),
217
+ );
218
+ return {
219
+ emitPlan: {
220
+ files: [],
221
+ orphanedFiles: [],
222
+ lock: loadExistingForgeLock(options.workspaceRoot) ?? {
223
+ schemaVersion: FORGE_LOCK_SCHEMA_VERSION,
224
+ generatorVersion: GENERATOR_VERSION,
225
+ analyzerVersion: PACKAGE_ANALYZER_VERSION,
226
+ inputHash: ctx.inputFingerprint,
227
+ lockfileHash: ctx.lockfileHash,
228
+ packageManager: ctx.packageManager,
229
+ packages: [],
230
+ },
231
+ },
232
+ warnings,
233
+ errors,
234
+ };
235
+ }
236
+
237
+ const allClassified = await collectAllClassified(
238
+ discover({ workspaceRoot: options.workspaceRoot }),
239
+ ctx.cacheDir,
240
+ false,
241
+ "none",
242
+ );
243
+
244
+ const emitPlan = buildIntegrationEmitPlan({
245
+ alias,
246
+ recipe,
247
+ classified,
248
+ allClassified,
249
+ appGraph,
250
+ ctx,
251
+ existingLock: loadExistingForgeLock(options.workspaceRoot),
252
+ });
253
+
254
+ return { emitPlan, warnings, errors };
255
+ }
256
+
257
+ export async function forgeAdd(
258
+ alias: string,
259
+ options: ForgeAddOptions,
260
+ ): Promise<ForgeAddResult> {
261
+ const normalized = alias.trim().toLowerCase();
262
+ const recipe = resolveRecipe(normalized);
263
+
264
+ if (!isReferenceAlias(normalized) || recipe === null) {
265
+ const error = createDiagnostic({
266
+ severity: "error",
267
+ code: "FORGE_UNKNOWN_ALIAS",
268
+ message: `unknown integration alias '${alias}'; supported: stripe, posthog, sentry, zod, ai`,
269
+ });
270
+ return {
271
+ alias: normalized,
272
+ changed: [],
273
+ unchanged: [],
274
+ warnings: [],
275
+ errors: [error],
276
+ exitCode: 1,
277
+ failureKind: "unknown_alias",
278
+ };
279
+ }
280
+
281
+ const pm =
282
+ options.pmAdapter ??
283
+ detectAndCreatePackageManagerAdapter(options.workspaceRoot);
284
+
285
+ if (options.dryRun) {
286
+ const ctx = discover({ workspaceRoot: options.workspaceRoot });
287
+ let installRoot = options.workspaceRoot;
288
+
289
+ try {
290
+ const dryRun = await pm.dryRunAddWithPath(
291
+ recipe.packages.map((pkg) => pkg.packageName).join(" "),
292
+ {
293
+ cwd: options.workspaceRoot,
294
+ ignoreScripts: !options.allowScripts,
295
+ },
296
+ );
297
+ installRoot = dryRun.installPath;
298
+ } catch {
299
+ const fallback = dryRunRecipeFallbackMessage(normalized);
300
+ const { emitPlan, warnings, errors } = await buildAddPlan(
301
+ normalized,
302
+ recipe,
303
+ ctx,
304
+ installRoot,
305
+ options,
306
+ );
307
+ warnings.push(
308
+ createDiagnostic({
309
+ severity: "warning",
310
+ code: "FORGE_DRY_RUN_FALLBACK",
311
+ message: fallback,
312
+ }),
313
+ );
314
+
315
+ return {
316
+ alias: normalized,
317
+ changed: [...emitPlan.files.map((file) => file.path), "forge.lock"],
318
+ unchanged: [],
319
+ warnings,
320
+ errors,
321
+ exitCode: errors.length > 0 ? 1 : 0,
322
+ failureKind: failureKind(errors),
323
+ };
324
+ }
325
+
326
+ const { emitPlan, warnings, errors } = await buildAddPlan(
327
+ normalized,
328
+ recipe,
329
+ ctx,
330
+ installRoot,
331
+ options,
332
+ );
333
+
334
+ return {
335
+ alias: normalized,
336
+ changed: [...emitPlan.files.map((file) => file.path), "forge.lock"],
337
+ unchanged: [],
338
+ warnings,
339
+ errors,
340
+ exitCode: errors.length > 0 ? 1 : 0,
341
+ failureKind: failureKind(errors),
342
+ };
343
+ }
344
+
345
+ const snapshot = snapshotVersionControlled(options.workspaceRoot);
346
+
347
+ try {
348
+ for (const pkg of recipe.packages) {
349
+ await pm.add(pkg.packageName, {
350
+ cwd: options.workspaceRoot,
351
+ ignoreScripts: !options.allowScripts,
352
+ });
353
+ }
354
+
355
+ const ctx = discover({ workspaceRoot: options.workspaceRoot });
356
+ const { emitPlan, warnings, errors: analyzeErrors } = await buildAddPlan(
357
+ normalized,
358
+ recipe,
359
+ ctx,
360
+ options.workspaceRoot,
361
+ options,
362
+ );
363
+
364
+ if (analyzeErrors.length > 0) {
365
+ restoreVersionControlledSnapshot(options.workspaceRoot, snapshot);
366
+ return {
367
+ alias: normalized,
368
+ changed: [],
369
+ unchanged: [],
370
+ warnings,
371
+ errors: analyzeErrors,
372
+ exitCode: 1,
373
+ failureKind: failureKind(analyzeErrors),
374
+ };
375
+ }
376
+
377
+ const emitResult = await emit(emitPlan, {
378
+ workspaceRoot: options.workspaceRoot,
379
+ mode: "write",
380
+ });
381
+
382
+ const warningsCombined = [...warnings, ...emitResult.warnings];
383
+ const errors = [...analyzeErrors, ...emitResult.errors];
384
+
385
+ if (errors.length > 0) {
386
+ restoreVersionControlledSnapshot(options.workspaceRoot, snapshot);
387
+ return {
388
+ alias: normalized,
389
+ changed: [],
390
+ unchanged: [],
391
+ warnings: warningsCombined,
392
+ errors,
393
+ exitCode: 1,
394
+ failureKind: failureKind(errors),
395
+ };
396
+ }
397
+
398
+ const integrityErrors = verifyLockIntegrity(
399
+ options.workspaceRoot,
400
+ emitPlan.lock,
401
+ );
402
+ if (integrityErrors.length > 0) {
403
+ restoreVersionControlledSnapshot(options.workspaceRoot, snapshot);
404
+ return {
405
+ alias: normalized,
406
+ changed: [],
407
+ unchanged: [],
408
+ warnings: warningsCombined,
409
+ errors: integrityErrors,
410
+ exitCode: 1,
411
+ failureKind: "lock_integrity",
412
+ };
413
+ }
414
+
415
+ const manifest = loadManifest(ctx.cacheDir);
416
+ const appGraph = await buildAppGraph({
417
+ workspaceRoot: ctx.workspaceRoot,
418
+ sources: ctx.sources,
419
+ prior: manifest.priorAppGraph,
420
+ tsconfigPath: ctx.tsconfigPath ?? undefined,
421
+ });
422
+
423
+ saveManifest(
424
+ ctx.cacheDir,
425
+ updateManifestAfterWrite(
426
+ manifest,
427
+ Object.fromEntries(
428
+ emitPlan.files.map((file) => [
429
+ file.path,
430
+ hashStable(renderBody(file)),
431
+ ]),
432
+ ),
433
+ appGraph,
434
+ ctx.inputFingerprint,
435
+ ),
436
+ );
437
+
438
+ return {
439
+ alias: normalized,
440
+ changed: emitResult.changed,
441
+ unchanged: emitResult.unchanged,
442
+ warnings: warningsCombined,
443
+ errors: [],
444
+ exitCode: 0,
445
+ };
446
+ } catch (error) {
447
+ restoreVersionControlledSnapshot(options.workspaceRoot, snapshot);
448
+
449
+ const message =
450
+ error instanceof PackageManagerCommandError
451
+ ? error.message
452
+ : error instanceof Error
453
+ ? error.message
454
+ : String(error);
455
+
456
+ const diagnostic = createDiagnostic({
457
+ severity: "error",
458
+ code: "FORGE_ADD_INSTALL_FAILED",
459
+ message: `forge add failed: ${message}`,
460
+ });
461
+
462
+ return {
463
+ alias: normalized,
464
+ changed: [],
465
+ unchanged: [],
466
+ warnings: [],
467
+ errors: [diagnostic],
468
+ exitCode: 1,
469
+ failureKind: "install_failed",
470
+ };
471
+ }
472
+ }
473
+
474
+ /** Test helper: seed fixture packages into node_modules and update package.json. */
475
+ export function seedWorkspacePackage(
476
+ workspaceRoot: string,
477
+ packageName: string,
478
+ fixtureRoot: string,
479
+ ): void {
480
+ const segments = packageName.startsWith("@")
481
+ ? packageName.slice(1).split("/")
482
+ : [packageName];
483
+ const target = join(workspaceRoot, "node_modules", ...segments);
484
+ nodeFileSystem.mkdirp(target);
485
+ nodeFileSystem.copy(join(fixtureRoot, ...segments), target);
486
+
487
+ const pkgJsonPath = join(workspaceRoot, "package.json");
488
+ const pkg = JSON.parse((nodeFileSystem.readText(pkgJsonPath) ?? "")) as {
489
+ dependencies?: Record<string, string>;
490
+ };
491
+ pkg.dependencies = {
492
+ ...pkg.dependencies,
493
+ [packageName]: "1.0.0",
494
+ };
495
+ nodeFileSystem.writeText(pkgJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
496
+ }