@loucompanion/forge-bridge 0.1.3-dev.9bc5014a2270 → 0.1.3-dev.a58d7cd1092f

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 (168) hide show
  1. package/README.md +127 -0
  2. package/dist/bridge/bridge.base.d.ts +8 -1
  3. package/dist/bridge/bridge.service.d.ts +93 -3
  4. package/dist/bridge/bridge.service.js +1358 -47
  5. package/dist/bridge/bridge.types.d.ts +202 -1
  6. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  7. package/dist/bridge/runner-host-capabilities.js +93 -0
  8. package/dist/cleanup/cleanup.service.d.ts +14 -2
  9. package/dist/cleanup/cleanup.service.js +496 -3
  10. package/dist/cleanup/cleanup.types.d.ts +10 -0
  11. package/dist/cleanup/index.d.ts +1 -1
  12. package/dist/cleanup/index.js +1 -1
  13. package/dist/cleanup/internals.d.ts +2 -2
  14. package/dist/cleanup/internals.js +1 -1
  15. package/dist/cli/cli.help.js +47 -1
  16. package/dist/cli/cli.service.d.ts +45 -2
  17. package/dist/cli/cli.service.js +378 -26
  18. package/dist/cli/local-config.d.ts +7 -0
  19. package/dist/cli/local-config.js +51 -3
  20. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  21. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  22. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  23. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  24. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  25. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  26. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  27. package/dist/runtime/attempt-process-supervisor.js +386 -0
  28. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  29. package/dist/runtime/docker-engine-probe.js +300 -0
  30. package/dist/runtime/generated-output-reset.d.ts +10 -0
  31. package/dist/runtime/generated-output-reset.js +103 -0
  32. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  33. package/dist/runtime/host-listener-inspector.js +349 -0
  34. package/dist/runtime/host-port-broker.d.ts +76 -0
  35. package/dist/runtime/host-port-broker.js +333 -0
  36. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  37. package/dist/runtime/host-runtime-contract.js +700 -0
  38. package/dist/runtime/host-runtime-lease.d.ts +197 -0
  39. package/dist/runtime/host-runtime-lease.js +2203 -0
  40. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  41. package/dist/runtime/host-runtime-promotion.js +159 -0
  42. package/dist/runtime/memory-capacity.d.ts +68 -0
  43. package/dist/runtime/memory-capacity.js +121 -0
  44. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  45. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  46. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  47. package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
  48. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  49. package/dist/runtime/runtime-isolation-lease.js +158 -0
  50. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  51. package/dist/runtime/workspace-resource-estimator.js +174 -0
  52. package/dist/sandbox/command-runner.d.ts +4 -0
  53. package/dist/sandbox/command-runner.js +47 -0
  54. package/dist/sandbox/contract.d.ts +5 -0
  55. package/dist/sandbox/contract.js +276 -0
  56. package/dist/sandbox/event-budget.d.ts +11 -0
  57. package/dist/sandbox/event-budget.js +27 -0
  58. package/dist/sandbox/image-policy.d.ts +35 -0
  59. package/dist/sandbox/image-policy.js +247 -0
  60. package/dist/sandbox/index.d.ts +5 -0
  61. package/dist/sandbox/index.js +5 -0
  62. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  63. package/dist/sandbox/lease-watchdog.js +67 -0
  64. package/dist/sandbox/preflight.d.ts +84 -0
  65. package/dist/sandbox/preflight.js +521 -0
  66. package/dist/sandbox/process.d.ts +20 -0
  67. package/dist/sandbox/process.js +209 -0
  68. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  69. package/dist/sandbox/provider-auth/codex.js +212 -0
  70. package/dist/sandbox/repository-lock.d.ts +5 -0
  71. package/dist/sandbox/repository-lock.js +121 -0
  72. package/dist/sandbox/runtime.d.ts +297 -0
  73. package/dist/sandbox/runtime.js +2338 -0
  74. package/dist/sandbox/sandbox.types.d.ts +93 -0
  75. package/dist/sandbox/sandbox.types.js +1 -0
  76. package/dist/sandbox/smoke.d.ts +76 -0
  77. package/dist/sandbox/smoke.js +380 -0
  78. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  79. package/dist/sandbox/worker-supervisor.js +36 -0
  80. package/dist/sandbox/worker.d.ts +1 -0
  81. package/dist/sandbox/worker.js +225 -0
  82. package/dist/sandbox/workspace.service.d.ts +13 -0
  83. package/dist/sandbox/workspace.service.js +72 -0
  84. package/dist/session/managed-attempt-tools.d.ts +21 -0
  85. package/dist/session/managed-attempt-tools.js +912 -0
  86. package/dist/session/managed-build-coordinator.d.ts +5 -0
  87. package/dist/session/managed-build-coordinator.js +105 -0
  88. package/dist/session/managed-build-receipt.d.ts +21 -0
  89. package/dist/session/managed-build-receipt.js +335 -0
  90. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  91. package/dist/session/phase-completion.artifacts.js +256 -26
  92. package/dist/session/phase-completion.d.ts +12 -2
  93. package/dist/session/phase-completion.helpers.d.ts +5 -0
  94. package/dist/session/phase-completion.helpers.js +51 -10
  95. package/dist/session/phase-completion.js +14 -3
  96. package/dist/session/phase-completion.prompts.d.ts +3 -3
  97. package/dist/session/phase-completion.prompts.js +24 -8
  98. package/dist/session/phase-completion.runtime.d.ts +13 -1
  99. package/dist/session/phase-completion.runtime.js +189 -8
  100. package/dist/session/phase-completion.schemas.js +117 -4
  101. package/dist/session/phase-completion.validation.d.ts +4 -1
  102. package/dist/session/phase-completion.validation.js +153 -22
  103. package/dist/session/pre-publication-validation.d.ts +46 -0
  104. package/dist/session/pre-publication-validation.js +208 -0
  105. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  106. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  107. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  108. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  109. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  110. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  111. package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
  112. package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
  113. package/dist/session/provider-cli/provider-cli.process.js +474 -23
  114. package/dist/session/provider-cli/provider-cli.prompts.d.ts +16 -2
  115. package/dist/session/provider-cli/provider-cli.prompts.js +224 -24
  116. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  117. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  118. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  119. package/dist/session/provider-cli/provider-cli.service.d.ts +26 -3
  120. package/dist/session/provider-cli/provider-cli.service.js +646 -90
  121. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  122. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  123. package/dist/session/provider-cli/provider-output-state.js +91 -16
  124. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  125. package/dist/session/provider-cli/provider-usage.js +28 -5
  126. package/dist/session/session-redaction.d.ts +3 -0
  127. package/dist/session/session-redaction.js +38 -0
  128. package/dist/session/session.base.d.ts +6 -1
  129. package/dist/session/session.service.d.ts +8 -2
  130. package/dist/session/session.service.js +530 -42
  131. package/dist/session/session.types.d.ts +124 -1
  132. package/dist/session/session.types.js +231 -4
  133. package/dist/shared/format/format-bytes.d.ts +1 -0
  134. package/dist/shared/format/format-bytes.js +9 -0
  135. package/dist/shared/path/path.d.ts +1 -0
  136. package/dist/shared/path/path.js +21 -1
  137. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  138. package/dist/shared/process/dotnet-toolchain.js +179 -0
  139. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  140. package/dist/shared/process/host-process-recovery.js +78 -0
  141. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  142. package/dist/shared/process/host-process-supervisor.js +330 -0
  143. package/dist/shared/process/process.d.ts +1 -0
  144. package/dist/shared/process/process.js +1 -0
  145. package/dist/shared/process/windows-launch.d.ts +1 -1
  146. package/dist/shared/process/windows-launch.js +6 -3
  147. package/dist/shared/provider-capabilities.d.ts +1 -0
  148. package/dist/shared/provider-capabilities.js +3 -0
  149. package/dist/shared/redaction/redaction.js +16 -4
  150. package/dist/version.d.ts +1 -1
  151. package/dist/version.js +1 -1
  152. package/dist/worktree/artifact-injector.d.ts +3 -2
  153. package/dist/worktree/artifact-injector.js +43 -4
  154. package/dist/worktree/dependency-warmer.d.ts +36 -2
  155. package/dist/worktree/dependency-warmer.js +347 -11
  156. package/dist/worktree/index.d.ts +2 -1
  157. package/dist/worktree/index.js +1 -0
  158. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  159. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  160. package/dist/worktree/worktree-paths.d.ts +4 -0
  161. package/dist/worktree/worktree-paths.js +30 -0
  162. package/dist/worktree/worktree.base.d.ts +3 -0
  163. package/dist/worktree/worktree.service.d.ts +13 -2
  164. package/dist/worktree/worktree.service.js +238 -47
  165. package/dist/worktree/worktree.types.d.ts +5 -1
  166. package/package.json +8 -4
  167. package/runtime/windows-job-supervisor.ps1 +444 -0
  168. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -1,8 +1,11 @@
1
1
  import path from "node:path";
2
- export function buildWindowsCmdLaunch(command, args) {
2
+ export function buildWindowsCmdLaunch(command, args, comSpec = process.env.ComSpec) {
3
+ const resolvedComSpec = comSpec?.trim();
4
+ const commandLine = windowsCommandLine(command, args);
5
+ const cmdPayload = commandLine.startsWith('"') ? `"${commandLine}"` : commandLine;
3
6
  return {
4
- command: process.env.ComSpec ?? "cmd.exe",
5
- args: ["/d", "/s", "/v:off", "/c", `"${windowsCommandLine(command, args)}"`]
7
+ command: resolvedComSpec || "cmd.exe",
8
+ args: ["/d", "/s", "/v:off", "/c", cmdPayload]
6
9
  };
7
10
  }
8
11
  export function buildWindowsLaunchCommand(commandPath, args) {
@@ -0,0 +1 @@
1
+ export declare function supportsDraftImageAttachmentsFor(cliProviders: readonly string[]): boolean;
@@ -0,0 +1,3 @@
1
+ export function supportsDraftImageAttachmentsFor(cliProviders) {
2
+ return cliProviders.some((provider) => provider.trim().toLowerCase() === "codex");
3
+ }
@@ -1,17 +1,29 @@
1
1
  const secretKeyPattern = /(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)/i;
2
+ const attachmentContentKeyPattern = /^(textContent|contentBase64)$/i;
3
+ const serializedAttachmentContentPattern = /("(?:textContent|contentBase64)"\s*:\s*")(?:(?:\\.)|[^"\\])*(")/gi;
4
+ const authorizationCredentialPattern = /(\bauthorization\s*[:=]\s*(?:bearer|basic)?\s*)([^"',\s]+)/gi;
5
+ const bearerCredentialPattern = /(\bbearer\s+)([^"',\s]+)/gi;
6
+ const secretAssignmentPattern = /(api[_-]?key|client[_-]?secret|password|secret|token)(\s*["']?\s*[:=]\s*["']?\s*)([^"',\s]+)/gi;
7
+ // Keep this boundary in parity with Forge.Api's SecretRedactor. Without it, the
8
+ // `sk-` substring in a structural identifier such as `task-skill-contract`
9
+ // is mistaken for a provider credential and corrupts the artifact after it has
10
+ // already passed schema validation.
11
+ const providerKeyPattern = /(?<![a-zA-Z0-9])sk-[a-zA-Z0-9_-]{12,}/g;
2
12
  export function redactSecrets(value, exactSecrets = []) {
3
13
  return redactValue(value, exactSecrets);
4
14
  }
5
15
  export function redactText(value, exactSecrets = []) {
6
- let redacted = value;
16
+ let redacted = value.replace(serializedAttachmentContentPattern, "$1[REDACTED]$2");
7
17
  for (const secret of exactSecrets) {
8
18
  if (secret.length > 0) {
9
19
  redacted = redacted.split(secret).join("[REDACTED]");
10
20
  }
11
21
  }
12
22
  return redacted
13
- .replace(/(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)(["':=\s]+)([^"',\s]+)/gi, "$1$2[REDACTED]")
14
- .replace(/(sk-[a-zA-Z0-9_-]{12,})/g, "[REDACTED]");
23
+ .replace(authorizationCredentialPattern, "$1[REDACTED]")
24
+ .replace(bearerCredentialPattern, "$1[REDACTED]")
25
+ .replace(secretAssignmentPattern, "$1$2[REDACTED]")
26
+ .replace(providerKeyPattern, "[REDACTED]");
15
27
  }
16
28
  function redactValue(value, exactSecrets) {
17
29
  if (typeof value === "string") {
@@ -25,6 +37,6 @@ function redactValue(value, exactSecrets) {
25
37
  }
26
38
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [
27
39
  key,
28
- secretKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
40
+ secretKeyPattern.test(key) || attachmentContentKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
29
41
  ]));
30
42
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const forgeBridgeVersion = "0.1.3-dev.9bc5014a2270";
1
+ export declare const forgeBridgeVersion = "0.1.3-dev.a58d7cd1092f";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const forgeBridgeVersion = "0.1.3-dev.9bc5014a2270";
1
+ export const forgeBridgeVersion = "0.1.3-dev.a58d7cd1092f";
@@ -1,9 +1,10 @@
1
1
  import type { DispatchResolvedArtifact } from "../session/session.types.js";
2
- export declare function injectWorktreeArtifacts(worktreePath: string, artifacts: readonly DispatchResolvedArtifact[]): Promise<void>;
2
+ export declare function injectWorktreeArtifacts(worktreePath: string, artifacts: readonly DispatchResolvedArtifact[], protectedRelativePaths?: ReadonlySet<string>): Promise<void>;
3
3
  export declare function filterArtifactsForLane(artifacts: readonly DispatchResolvedArtifact[], lane: string, phase: string): readonly DispatchResolvedArtifact[];
4
- export declare function writeArtifact(worktreePath: string, artifact: DispatchResolvedArtifact): Promise<void>;
4
+ export declare function writeArtifact(worktreePath: string, artifact: DispatchResolvedArtifact, protectedRelativePaths?: ReadonlySet<string>): Promise<void>;
5
5
  export declare function selectLaneCommand(artifacts: readonly DispatchResolvedArtifact[], lane: string, phase: string): DispatchResolvedArtifact | undefined;
6
6
  export declare function resolveCommandSlug(lane: string, phase: string): string;
7
+ export declare function artifactExecutionBody(artifact: DispatchResolvedArtifact): string;
7
8
  export declare function buildSpecBody(laneCommand: DispatchResolvedArtifact, workItem: {
8
9
  readonly id: string;
9
10
  readonly title: string;
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { spawn } from "node:child_process";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  const DEBUG_OPERATIONAL_SKILLS = new Set(["debug-repro", "debug-root-cause", "debug-fix", "debug-regression"]);
@@ -59,9 +60,9 @@ const DRAFT_WORK_ITEM_OPERATIONAL_HOOKS = new Set([
59
60
  const DRAFT_WORK_ORDER_OPERATIONAL_HOOKS = new Set([
60
61
  "draft-work-order-plan"
61
62
  ]);
62
- export async function injectWorktreeArtifacts(worktreePath, artifacts) {
63
+ export async function injectWorktreeArtifacts(worktreePath, artifacts, protectedRelativePaths = new Set()) {
63
64
  for (const artifact of artifacts) {
64
- await writeArtifact(worktreePath, artifact);
65
+ await writeArtifact(worktreePath, artifact, protectedRelativePaths);
65
66
  }
66
67
  }
67
68
  export function filterArtifactsForLane(artifacts, lane, phase) {
@@ -78,7 +79,7 @@ export function filterArtifactsForLane(artifacts, lane, phase) {
78
79
  }
79
80
  return artifacts;
80
81
  }
81
- export async function writeArtifact(worktreePath, artifact) {
82
+ export async function writeArtifact(worktreePath, artifact, protectedRelativePaths = new Set()) {
82
83
  const writtenHash = sha256(artifact.body);
83
84
  if (writtenHash !== artifact.contentHash.toLowerCase()) {
84
85
  throw new Error(`Injected artifact '${artifact.slug}' content_hash mismatch (expected ${artifact.contentHash}, got ${writtenHash}).`);
@@ -93,6 +94,11 @@ export async function writeArtifact(worktreePath, artifact) {
93
94
  ? path.resolve(scopeDir, slug, "SKILL.md")
94
95
  : path.resolve(scopeDir, `${slug}.md`);
95
96
  assertInside(forgeRoot, filePath, "Artifact file");
97
+ const relativePath = path.relative(worktreePath, filePath).replaceAll(path.sep, "/");
98
+ if (protectedRelativePaths.has(relativePath)) {
99
+ await restoreTrackedArtifactIfItMatchesManagedBody(worktreePath, filePath, relativePath, artifact);
100
+ return;
101
+ }
96
102
  await fs.mkdir(path.dirname(filePath), { recursive: true });
97
103
  await fs.writeFile(filePath, artifact.body, "utf8");
98
104
  }
@@ -120,6 +126,39 @@ export function resolveCommandSlug(lane, phase) {
120
126
  return "dev-prompt";
121
127
  }
122
128
  }
129
+ async function restoreTrackedArtifactIfItMatchesManagedBody(worktreePath, filePath, relativePath, artifact) {
130
+ const current = await fs.readFile(filePath, "utf8").catch(() => null);
131
+ if (current === null
132
+ || (current !== artifact.body && current !== artifactExecutionBody(artifact)))
133
+ return;
134
+ const result = await new Promise((resolve) => {
135
+ const child = spawn("git", ["show", `HEAD:${relativePath}`], {
136
+ cwd: worktreePath,
137
+ windowsHide: true,
138
+ stdio: ["ignore", "pipe", "ignore"],
139
+ });
140
+ let stdout = "";
141
+ child.stdout?.setEncoding("utf8");
142
+ child.stdout?.on("data", chunk => { stdout += chunk; });
143
+ child.on("error", () => resolve({ exitCode: 1, stdout: "" }));
144
+ child.on("close", code => resolve({ exitCode: code ?? 1, stdout }));
145
+ });
146
+ if (result.exitCode === 0)
147
+ await fs.writeFile(filePath, result.stdout, "utf8");
148
+ }
149
+ export function artifactExecutionBody(artifact) {
150
+ const content = artifact.body;
151
+ if (!content.startsWith("---"))
152
+ return content;
153
+ const end = content.indexOf("\n---", 3);
154
+ if (end < 3)
155
+ return content;
156
+ let bodyStart = end + 4;
157
+ while (bodyStart < content.length && (content[bodyStart] === "\n" || content[bodyStart] === "\r")) {
158
+ bodyStart += 1;
159
+ }
160
+ return content.slice(bodyStart);
161
+ }
123
162
  function shouldInjectDebugArtifact(artifact, commandSlug) {
124
163
  if (artifact.scope.trim().toLowerCase() !== "operational") {
125
164
  return false;
@@ -192,7 +231,7 @@ function normalizeDraftPhase(phase) {
192
231
  return phase.trim().toLowerCase().replaceAll("-", "_");
193
232
  }
194
233
  export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
195
- const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
234
+ const prompt = artifactExecutionBody(laneCommand).replaceAll("{workItemId}", workItem.id);
196
235
  return `---
197
236
  work_item_id: ${workItem.id}
198
237
  status: approved
@@ -9,6 +9,13 @@ export interface WarmDependenciesRequest {
9
9
  readonly worktreePath: string;
10
10
  readonly allowedRoot: string;
11
11
  readonly warmPathOverrides?: readonly string[];
12
+ readonly preparationPolicy?: DependencyPreparationPolicy;
13
+ }
14
+ export interface DependencyPreparationPolicy {
15
+ readonly isolation?: string;
16
+ readonly trustDomain?: string;
17
+ readonly invalidationEpoch?: string;
18
+ readonly toolchainVersions?: Readonly<Partial<Record<DependencyEcosystem | "npm" | "pnpm", string>>>;
12
19
  }
13
20
  export interface InstallFallbackResult {
14
21
  readonly ecosystem: DependencyEcosystem;
@@ -20,9 +27,23 @@ export interface WarmDependenciesResult {
20
27
  readonly linked: readonly string[];
21
28
  readonly skipped: readonly string[];
22
29
  readonly fallbacks: readonly InstallFallbackResult[];
30
+ readonly receipts: readonly DependencyPreparationReceiptResult[];
31
+ }
32
+ export interface DependencyPreparationReceiptResult {
33
+ readonly ecosystem: DependencyEcosystem;
34
+ readonly packageRoot: string;
35
+ readonly command: string;
36
+ readonly cacheHit: boolean;
37
+ readonly fingerprint: string;
38
+ readonly receiptPath: string;
39
+ /** End-to-end receipt lookup/preparation time, including coordinator wait. */
40
+ readonly durationMs: number;
23
41
  }
24
42
  export interface CommandExecutor {
25
- run(cwd: string, command: string, args: readonly string[]): Promise<void>;
43
+ run(cwd: string, command: string, args: readonly string[], options?: CommandExecutionOptions): Promise<void>;
44
+ }
45
+ export interface CommandExecutionOptions {
46
+ readonly env?: NodeJS.ProcessEnv;
26
47
  }
27
48
  export declare function detectWarmPaths(repoRoot: string, warmPathOverrides?: readonly string[]): Promise<WarmPathEntry[]>;
28
49
  export declare function warmWorktreeDependencies(request: WarmDependenciesRequest, executor?: CommandExecutor): Promise<WarmDependenciesResult>;
@@ -30,7 +51,20 @@ export declare function createDirectoryLink(sourcePath: string, targetPath: stri
30
51
  export declare function lockfilesDiffer(baseCheckoutPath: string, worktreePath: string, packageRootRelative: string): Promise<boolean>;
31
52
  export declare function requiresInstallFallback(entry: WarmPathEntry, baseCheckoutPath: string, worktreePath: string, sourceExists: boolean): Promise<string | null>;
32
53
  export declare function declaresNativeModules(packageRootPath: string): Promise<boolean>;
33
- export declare function resolveNpmLaunch(args: readonly string[]): {
54
+ export interface NpmLaunchOptions {
55
+ readonly env?: NodeJS.ProcessEnv;
56
+ readonly execPath?: string;
57
+ readonly platform?: NodeJS.Platform;
58
+ readonly homeDir?: string;
59
+ readonly candidates?: readonly string[];
60
+ }
61
+ export declare function resolveNpmLaunch(args: readonly string[], options?: NpmLaunchOptions): {
62
+ command: string;
63
+ args: readonly string[];
64
+ };
65
+ export declare function resolvePnpmLaunch(args: readonly string[]): {
34
66
  command: string;
35
67
  args: readonly string[];
36
68
  };
69
+ export declare function formatCommandFailureOutput(stdout: string, stderr: string): string;
70
+ export declare function unattendedPackageManagerEnvironment(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
@@ -1,7 +1,13 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { existsSync, realpathSync } from "node:fs";
1
3
  import fs from "node:fs/promises";
4
+ import os from "node:os";
2
5
  import path from "node:path";
6
+ import { RepositoryCoordinatorLock } from "../sandbox/repository-lock.js";
3
7
  import { NodeProcessRunner } from "../shared/process/process.js";
8
+ import { buildWindowsCmdLaunch } from "../shared/process/windows-launch.js";
4
9
  import { isSubPath } from "../shared/path/path.js";
10
+ import { defaultWorktreeCoordinatorStateRoot } from "./worktree-paths.js";
5
11
  const SKIP_DIR_NAMES = new Set([
6
12
  ".git",
7
13
  "node_modules",
@@ -20,6 +26,10 @@ const NATIVE_MODULE_HINTS = [
20
26
  "better-sqlite3",
21
27
  "@swc/core"
22
28
  ];
29
+ const MAX_COMMAND_FAILURE_OUTPUT_LENGTH = 8_000;
30
+ const DEPENDENCY_RECEIPT_SCHEMA_VERSION = 1;
31
+ const DEPENDENCY_PREPARATION_CONTRACT = "managed-worktree-isolated-v1";
32
+ const dependencyPreparationCoordinator = new RepositoryCoordinatorLock(defaultWorktreeCoordinatorStateRoot());
23
33
  export async function detectWarmPaths(repoRoot, warmPathOverrides = []) {
24
34
  const entries = new Map();
25
35
  await collectDetectedPaths(repoRoot, repoRoot, entries, 0);
@@ -47,15 +57,51 @@ export async function warmWorktreeDependencies(request, executor = defaultComman
47
57
  const linked = [];
48
58
  const skipped = [];
49
59
  const fallbacks = [];
60
+ const receipts = [];
61
+ const restoredDotNetRoots = new Set();
62
+ const installedNodeRoots = new Set();
50
63
  for (const entry of warmPaths) {
51
64
  const sourcePath = path.join(baseCheckoutPath, entry.relativePath);
52
65
  const targetPath = path.join(worktreePath, entry.relativePath);
53
66
  assertWithinRoot(allowedRoot, targetPath, "Warm target");
67
+ if (entry.ecosystem === "node" && path.basename(entry.relativePath) === "node_modules") {
68
+ await removeLegacyDirectoryLink(targetPath, allowedRoot);
69
+ const installPackageRoot = await resolveNodeInstallPackageRoot(worktreePath, entry.packageRoot);
70
+ const packageRoot = path.resolve(worktreePath, installPackageRoot === "." ? "" : installPackageRoot);
71
+ const packageKey = process.platform === "win32" ? packageRoot.toLowerCase() : packageRoot;
72
+ if (installedNodeRoots.has(packageKey)) {
73
+ skipped.push(entry.relativePath);
74
+ continue;
75
+ }
76
+ installedNodeRoots.add(packageKey);
77
+ const preparation = await ensureInstallFallback({ ...entry, packageRoot: installPackageRoot }, worktreePath, "Managed worktrees require isolated Node dependencies.", executor, request.preparationPolicy);
78
+ receipts.push(preparation.receipt);
79
+ if (preparation.install)
80
+ fallbacks.push(preparation.install);
81
+ continue;
82
+ }
83
+ if (entry.ecosystem === "dotnet") {
84
+ await removeLegacyDirectoryLink(targetPath, allowedRoot);
85
+ const packageRoot = path.resolve(worktreePath, entry.packageRoot === "." ? "" : entry.packageRoot);
86
+ const packageKey = process.platform === "win32" ? packageRoot.toLowerCase() : packageRoot;
87
+ if (restoredDotNetRoots.has(packageKey)) {
88
+ skipped.push(entry.relativePath);
89
+ continue;
90
+ }
91
+ restoredDotNetRoots.add(packageKey);
92
+ const preparation = await ensureInstallFallback(entry, worktreePath, "Managed worktrees require isolated .NET build outputs.", executor, request.preparationPolicy);
93
+ receipts.push(preparation.receipt);
94
+ if (preparation.install)
95
+ fallbacks.push(preparation.install);
96
+ continue;
97
+ }
54
98
  const sourceExists = await directoryExists(sourcePath);
55
99
  const fallbackReason = await resolveFallbackReason(entry, baseCheckoutPath, worktreePath, sourceExists);
56
100
  if (fallbackReason) {
57
- const install = await runInstallFallback(entry, worktreePath, fallbackReason, executor);
58
- fallbacks.push(install);
101
+ const preparation = await ensureInstallFallback(entry, worktreePath, fallbackReason, executor, request.preparationPolicy);
102
+ receipts.push(preparation.receipt);
103
+ if (preparation.install)
104
+ fallbacks.push(preparation.install);
59
105
  continue;
60
106
  }
61
107
  if (!sourceExists) {
@@ -65,7 +111,17 @@ export async function warmWorktreeDependencies(request, executor = defaultComman
65
111
  await createDirectoryLink(sourcePath, targetPath, allowedRoot);
66
112
  linked.push(entry.relativePath);
67
113
  }
68
- return { linked, skipped, fallbacks };
114
+ return { linked, skipped, fallbacks, receipts };
115
+ }
116
+ async function removeLegacyDirectoryLink(targetPath, allowedRoot) {
117
+ const resolvedTarget = path.resolve(targetPath);
118
+ const resolvedRoot = path.resolve(allowedRoot);
119
+ assertWithinRoot(resolvedRoot, resolvedTarget, "Legacy warm target");
120
+ const stat = await fs.lstat(resolvedTarget).catch(() => null);
121
+ if (!stat?.isSymbolicLink()) {
122
+ return;
123
+ }
124
+ await fs.rm(resolvedTarget, { force: true });
69
125
  }
70
126
  export async function createDirectoryLink(sourcePath, targetPath, allowedRoot) {
71
127
  const resolvedSource = path.resolve(sourcePath);
@@ -78,7 +134,10 @@ export async function createDirectoryLink(sourcePath, targetPath, allowedRoot) {
78
134
  if (!stat?.isDirectory()) {
79
135
  throw new Error(`Link source must be an existing directory: ${resolvedSource}`);
80
136
  }
81
- await fs.rm(resolvedTarget, { recursive: true, force: true });
137
+ const existingTarget = await fs.lstat(resolvedTarget).catch(() => null);
138
+ await fs.rm(resolvedTarget, existingTarget?.isSymbolicLink()
139
+ ? { force: true }
140
+ : { recursive: true, force: true });
82
141
  await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
83
142
  if (process.platform === "win32") {
84
143
  await fs.symlink(resolvedSource, resolvedTarget, "junction");
@@ -179,13 +238,217 @@ export async function lockfilesDiffer(baseCheckoutPath, worktreePath, packageRoo
179
238
  }
180
239
  return false;
181
240
  }
241
+ async function ensureInstallFallback(entry, worktreePath, reason, executor, policy) {
242
+ const startedAt = performance.now();
243
+ const cwd = path.resolve(worktreePath, entry.packageRoot === "." ? "" : entry.packageRoot);
244
+ assertWithinRoot(path.resolve(worktreePath), cwd, "Dependency package root");
245
+ const command = await installFallbackCommand(entry, cwd);
246
+ const outputMarker = dependencyOutputMarker(entry.ecosystem, cwd);
247
+ const receiptPath = path.join(outputMarker, ".forge-dependency-preparation.json");
248
+ const fingerprint = await dependencyPreparationFingerprint(entry, worktreePath, cwd, command, policy);
249
+ return await dependencyPreparationCoordinator.withLock(receiptPath, async () => {
250
+ const existing = await readDependencyPreparationReceipt(receiptPath);
251
+ if (existing?.fingerprint === fingerprint
252
+ && existing.ecosystem === entry.ecosystem
253
+ && existing.packageRoot === entry.packageRoot
254
+ && existing.command === command
255
+ && await directoryExists(outputMarker)) {
256
+ return {
257
+ install: null,
258
+ receipt: {
259
+ ecosystem: entry.ecosystem,
260
+ packageRoot: entry.packageRoot,
261
+ command,
262
+ cacheHit: true,
263
+ fingerprint,
264
+ receiptPath,
265
+ durationMs: elapsedMilliseconds(startedAt),
266
+ },
267
+ };
268
+ }
269
+ const install = await runInstallFallback(entry, worktreePath, reason, executor);
270
+ // npm is allowed to omit node_modules when the package graph is empty. Keep
271
+ // an isolated, ignored output directory so the durable receipt still has a
272
+ // concrete marker whose removal invalidates reuse.
273
+ if (entry.ecosystem === "node") {
274
+ await fs.mkdir(outputMarker, { recursive: true });
275
+ }
276
+ if (!(await directoryExists(outputMarker))) {
277
+ throw new Error(`dependency_preparation_output_missing: ${install.command} succeeded without creating ${outputMarker}.`);
278
+ }
279
+ const receipt = {
280
+ schemaVersion: DEPENDENCY_RECEIPT_SCHEMA_VERSION,
281
+ fingerprint,
282
+ ecosystem: entry.ecosystem,
283
+ packageRoot: entry.packageRoot,
284
+ command,
285
+ outputMarker: path.relative(cwd, outputMarker).replaceAll("\\", "/"),
286
+ completedAt: new Date().toISOString(),
287
+ };
288
+ await persistDependencyPreparationReceipt(receiptPath, receipt);
289
+ return {
290
+ install,
291
+ receipt: {
292
+ ecosystem: entry.ecosystem,
293
+ packageRoot: entry.packageRoot,
294
+ command,
295
+ cacheHit: false,
296
+ fingerprint,
297
+ receiptPath,
298
+ durationMs: elapsedMilliseconds(startedAt),
299
+ },
300
+ };
301
+ });
302
+ }
303
+ function elapsedMilliseconds(startedAt) {
304
+ return Math.max(0, Math.round((performance.now() - startedAt) * 1000) / 1000);
305
+ }
306
+ async function installFallbackCommand(entry, cwd) {
307
+ if (entry.ecosystem === "node") {
308
+ if (await pathExists(path.join(cwd, "pnpm-lock.yaml")))
309
+ return "pnpm install --frozen-lockfile";
310
+ return await pathExists(path.join(cwd, "package-lock.json")) ? "npm ci" : "npm install";
311
+ }
312
+ if (entry.ecosystem === "python") {
313
+ return await pathExists(path.join(cwd, "requirements.txt"))
314
+ ? "python -m venv .venv && pip install -r requirements.txt"
315
+ : "python -m venv .venv";
316
+ }
317
+ return "dotnet restore";
318
+ }
319
+ function dependencyOutputMarker(ecosystem, cwd) {
320
+ if (ecosystem === "node")
321
+ return path.join(cwd, "node_modules");
322
+ if (ecosystem === "python")
323
+ return path.join(cwd, ".venv");
324
+ return path.join(cwd, "obj");
325
+ }
326
+ async function dependencyPreparationFingerprint(entry, worktreePath, cwd, command, policy) {
327
+ const inputFiles = await dependencyInputFiles(entry.ecosystem, worktreePath, cwd);
328
+ const inputDigests = await Promise.all(inputFiles.map(async (file) => ({
329
+ path: path.relative(worktreePath, file).replaceAll("\\", "/"),
330
+ digest: await sha256File(file),
331
+ })));
332
+ const toolchainVersion = await dependencyToolchainVersion(entry.ecosystem, cwd, policy);
333
+ const trustDomain = policy?.trustDomain ?? process.env.FORGE_TRUST_DOMAIN_ID ?? "default";
334
+ const invalidationEpoch = policy?.invalidationEpoch ?? process.env.FORGE_DEPENDENCY_CACHE_EPOCH ?? "default";
335
+ const contract = {
336
+ schemaVersion: DEPENDENCY_RECEIPT_SCHEMA_VERSION,
337
+ preparationContract: DEPENDENCY_PREPARATION_CONTRACT,
338
+ ecosystem: entry.ecosystem,
339
+ packageRoot: entry.packageRoot.replaceAll("\\", "/"),
340
+ command,
341
+ inputDigests,
342
+ toolchainVersion,
343
+ nodeRuntime: process.version,
344
+ platform: process.platform,
345
+ architecture: process.arch,
346
+ osRelease: os.release(),
347
+ isolation: policy?.isolation ?? process.env.FORGE_RUNTIME_MODE ?? "managed_host_process",
348
+ trustDomainFingerprint: sha256Text(trustDomain),
349
+ invalidationEpochFingerprint: sha256Text(invalidationEpoch),
350
+ };
351
+ return `sha256:${sha256Text(JSON.stringify(contract))}`;
352
+ }
353
+ async function dependencyInputFiles(ecosystem, worktreePath, cwd) {
354
+ const result = new Set();
355
+ const directNames = ecosystem === "node"
356
+ ? ["package.json", "package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "yarn.lock", ".npmrc", ".yarnrc", ".yarnrc.yml", ".nvmrc", ".node-version"]
357
+ : ecosystem === "python"
358
+ ? ["pyproject.toml", "requirements.txt", "requirements-dev.txt", "poetry.lock", "Pipfile", "Pipfile.lock", "uv.lock", "pip.conf", "pyvenv.cfg"]
359
+ : ["global.json", "NuGet.Config", "nuget.config", "Directory.Build.props", "Directory.Build.targets", "Directory.Packages.props", "packages.lock.json"];
360
+ let current = cwd;
361
+ const root = path.resolve(worktreePath);
362
+ while (isSubPath(root, current)) {
363
+ for (const name of directNames) {
364
+ const candidate = path.join(current, name);
365
+ if (await pathExists(candidate))
366
+ result.add(candidate);
367
+ }
368
+ if (ecosystem === "dotnet" && current === cwd) {
369
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
370
+ for (const item of entries) {
371
+ if (item.isFile() && (/\.(?:csproj|fsproj|vbproj|sln|slnx)$/iu.test(item.name))) {
372
+ result.add(path.join(current, item.name));
373
+ }
374
+ }
375
+ }
376
+ if (current === root)
377
+ break;
378
+ current = path.dirname(current);
379
+ }
380
+ return [...result].sort((left, right) => left.localeCompare(right));
381
+ }
382
+ async function dependencyToolchainVersion(ecosystem, cwd, policy) {
383
+ if (ecosystem === "node") {
384
+ const manager = await pathExists(path.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : "npm";
385
+ const override = policy?.toolchainVersions?.[manager];
386
+ if (override)
387
+ return `${manager}@${override};node@${process.version}`;
388
+ const launch = manager === "pnpm" ? resolvePnpmLaunch(["--version"]) : resolveNpmLaunch(["--version"]);
389
+ return `${manager}@${await readToolVersion(cwd, launch.command, launch.args)};node@${process.version}`;
390
+ }
391
+ const override = policy?.toolchainVersions?.[ecosystem];
392
+ if (override)
393
+ return `${ecosystem}@${override}`;
394
+ const launch = ecosystem === "dotnet"
395
+ ? { command: "dotnet", args: ["--version"] }
396
+ : { command: "python", args: ["--version"] };
397
+ return `${ecosystem}@${await readToolVersion(cwd, launch.command, launch.args)}`;
398
+ }
399
+ async function readToolVersion(cwd, command, args) {
400
+ return await defaultProcessRunner.run({ command, cwd, args })
401
+ .then(result => result.exitCode === 0
402
+ ? (result.stdout.trim() || result.stderr.trim() || "unknown").slice(0, 256)
403
+ : `unavailable:${result.exitCode}`)
404
+ .catch(() => "unavailable");
405
+ }
406
+ async function readDependencyPreparationReceipt(receiptPath) {
407
+ try {
408
+ const parsed = JSON.parse(await fs.readFile(receiptPath, "utf8"));
409
+ return parsed.schemaVersion === DEPENDENCY_RECEIPT_SCHEMA_VERSION
410
+ && typeof parsed.fingerprint === "string"
411
+ && typeof parsed.ecosystem === "string"
412
+ && typeof parsed.packageRoot === "string"
413
+ && typeof parsed.command === "string"
414
+ && typeof parsed.outputMarker === "string"
415
+ ? parsed
416
+ : null;
417
+ }
418
+ catch (error) {
419
+ if (error.code === "ENOENT" || error instanceof SyntaxError)
420
+ return null;
421
+ throw error;
422
+ }
423
+ }
424
+ async function persistDependencyPreparationReceipt(receiptPath, receipt) {
425
+ await fs.mkdir(path.dirname(receiptPath), { recursive: true });
426
+ const temporaryPath = `${receiptPath}.${randomUUID()}.tmp`;
427
+ await fs.writeFile(temporaryPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
428
+ try {
429
+ await fs.rename(temporaryPath, receiptPath);
430
+ }
431
+ finally {
432
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
433
+ }
434
+ }
435
+ function sha256Text(value) {
436
+ return createHash("sha256").update(value).digest("hex");
437
+ }
182
438
  async function runInstallFallback(entry, worktreePath, reason, executor) {
183
439
  const cwd = path.join(worktreePath, entry.packageRoot === "." ? "" : entry.packageRoot);
184
440
  if (entry.ecosystem === "node") {
441
+ const pnpmLockPath = path.join(cwd, "pnpm-lock.yaml");
442
+ if (await pathExists(pnpmLockPath)) {
443
+ const args = ["install", "--frozen-lockfile"];
444
+ const launch = resolvePnpmLaunch(args);
445
+ await executor.run(cwd, launch.command, launch.args, { env: unattendedPackageManagerEnvironment() });
446
+ return { ecosystem: entry.ecosystem, packageRoot: entry.packageRoot, reason, command: `pnpm ${args.join(" ")}` };
447
+ }
185
448
  const lockPath = path.join(cwd, "package-lock.json");
186
449
  const args = (await pathExists(lockPath)) ? ["ci"] : ["install"];
187
450
  const launch = resolveNpmLaunch(args);
188
- await executor.run(cwd, launch.command, launch.args);
451
+ await executor.run(cwd, launch.command, launch.args, { env: unattendedPackageManagerEnvironment() });
189
452
  return { ecosystem: entry.ecosystem, packageRoot: entry.packageRoot, reason, command: `npm ${args.join(" ")}` };
190
453
  }
191
454
  if (entry.ecosystem === "python") {
@@ -216,6 +479,23 @@ async function runInstallFallback(entry, worktreePath, reason, executor) {
216
479
  command: "dotnet restore"
217
480
  };
218
481
  }
482
+ async function resolveNodeInstallPackageRoot(worktreePath, packageRoot) {
483
+ const resolvedWorktree = path.resolve(worktreePath);
484
+ let current = path.resolve(resolvedWorktree, packageRoot === "." ? "" : packageRoot);
485
+ assertWithinRoot(resolvedWorktree, current, "Node package root");
486
+ while (isSubPath(resolvedWorktree, current)) {
487
+ if (await pathExists(path.join(current, "pnpm-lock.yaml"))
488
+ || await pathExists(path.join(current, "pnpm-workspace.yaml"))
489
+ || await pathExists(path.join(current, "package-lock.json"))) {
490
+ const relative = normalizeRelativePath(path.relative(resolvedWorktree, current));
491
+ return relative || ".";
492
+ }
493
+ if (current === resolvedWorktree)
494
+ break;
495
+ current = path.dirname(current);
496
+ }
497
+ return packageRoot;
498
+ }
219
499
  export async function requiresInstallFallback(entry, baseCheckoutPath, worktreePath, sourceExists) {
220
500
  return resolveFallbackReason(entry, baseCheckoutPath, worktreePath, sourceExists);
221
501
  }
@@ -287,17 +567,73 @@ async function sha256File(filePath) {
287
567
  const content = await fs.readFile(filePath);
288
568
  return crypto.createHash("sha256").update(content).digest("hex");
289
569
  }
290
- export function resolveNpmLaunch(args) {
291
- if (process.env.npm_execpath) {
292
- return { command: process.execPath, args: [process.env.npm_execpath, ...args] };
570
+ export function resolveNpmLaunch(args, options = {}) {
571
+ const env = options.env ?? process.env;
572
+ const execPath = options.execPath ?? process.execPath;
573
+ const npmExecPath = env.npm_execpath;
574
+ if (npmExecPath && path.basename(npmExecPath).toLowerCase() === "npm-cli.js") {
575
+ return { command: execPath, args: [npmExecPath, ...args] };
576
+ }
577
+ const platform = options.platform ?? process.platform;
578
+ if (platform === "win32") {
579
+ const comSpec = options.env ? env.ComSpec ?? "cmd.exe" : env.ComSpec;
580
+ return buildWindowsCmdLaunch("npm.cmd", args, comSpec);
581
+ }
582
+ for (const candidate of options.candidates ?? defaultNpmCliCandidates(execPath, options.homeDir ?? os.homedir())) {
583
+ const resolvedCandidate = tryResolveNpmCandidate(candidate);
584
+ if (resolvedCandidate) {
585
+ return { command: execPath, args: [resolvedCandidate, ...args] };
586
+ }
293
587
  }
294
588
  return { command: "npm", args };
295
589
  }
590
+ export function resolvePnpmLaunch(args) {
591
+ if (process.platform === "win32") {
592
+ return buildWindowsCmdLaunch("pnpm.cmd", args, process.env.ComSpec);
593
+ }
594
+ return { command: "pnpm", args };
595
+ }
596
+ export function formatCommandFailureOutput(stdout, stderr) {
597
+ const sections = [
598
+ stderr.trim().length > 0 ? `stderr:\n${stderr.trim()}` : null,
599
+ stdout.trim().length > 0 ? `stdout:\n${stdout.trim()}` : null,
600
+ ].filter((section) => section !== null);
601
+ const output = sections.join("\n");
602
+ if (output.length <= MAX_COMMAND_FAILURE_OUTPUT_LENGTH) {
603
+ return output;
604
+ }
605
+ return `[last ${MAX_COMMAND_FAILURE_OUTPUT_LENGTH} characters]\n${output.slice(-MAX_COMMAND_FAILURE_OUTPUT_LENGTH)}`;
606
+ }
607
+ export function unattendedPackageManagerEnvironment(env = process.env) {
608
+ return { ...env, CI: "true" };
609
+ }
610
+ function tryResolveNpmCandidate(candidate) {
611
+ try {
612
+ if (!existsSync(candidate)) {
613
+ return null;
614
+ }
615
+ return realpathSync(candidate);
616
+ }
617
+ catch {
618
+ return null;
619
+ }
620
+ }
621
+ function defaultNpmCliCandidates(execPath, homeDir) {
622
+ const candidates = [];
623
+ const execDir = path.dirname(execPath);
624
+ if (path.isAbsolute(execDir)) {
625
+ candidates.push(path.join(execDir, "npm"), path.join(execDir, "npm-cli.js"), path.join(execDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
626
+ }
627
+ candidates.push("/usr/bin/npm", "/usr/local/bin/npm", path.join(homeDir, ".npm-global", "bin", "npm"), path.join(homeDir, ".npm-global", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
628
+ return candidates;
629
+ }
296
630
  const defaultCommandExecutor = {
297
- async run(cwd, command, args) {
298
- const result = await defaultProcessRunner.run({ command, cwd, args });
631
+ async run(cwd, command, args, options) {
632
+ const result = await defaultProcessRunner.run({ command, cwd, args, env: options?.env });
299
633
  if (result.exitCode !== 0) {
300
- throw new Error(`${command} ${args.join(" ")} failed (${result.exitCode}): ${result.stderr.trim()}`);
634
+ const output = formatCommandFailureOutput(result.stdout, result.stderr);
635
+ const details = output.length > 0 ? `\n${output}` : " no output was captured";
636
+ throw new Error(`${command} ${args.join(" ")} failed (${result.exitCode}) in ${cwd}:${details}`);
301
637
  }
302
638
  }
303
639
  };