@loucompanion/forge-bridge 0.1.3-dev.b3bce6ba65a6 → 0.1.3-dev.ba293ccf876b

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 (189) 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 +98 -3
  4. package/dist/bridge/bridge.service.js +1425 -45
  5. package/dist/bridge/bridge.types.d.ts +205 -1
  6. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  7. package/dist/bridge/readiness-result-outbox.js +54 -0
  8. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  9. package/dist/bridge/readiness-result-worker.js +100 -0
  10. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  11. package/dist/bridge/runner-host-capabilities.js +93 -0
  12. package/dist/cleanup/cleanup.service.d.ts +14 -2
  13. package/dist/cleanup/cleanup.service.js +496 -3
  14. package/dist/cleanup/cleanup.types.d.ts +10 -0
  15. package/dist/cleanup/index.d.ts +1 -1
  16. package/dist/cleanup/index.js +1 -1
  17. package/dist/cleanup/internals.d.ts +2 -2
  18. package/dist/cleanup/internals.js +1 -1
  19. package/dist/cli/cli.help.js +47 -1
  20. package/dist/cli/cli.service.d.ts +45 -2
  21. package/dist/cli/cli.service.js +377 -27
  22. package/dist/cli/local-config.d.ts +5 -0
  23. package/dist/cli/local-config.js +45 -1
  24. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  25. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  26. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  27. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  28. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  29. package/dist/runtime/attempt-path-operation.js +94 -0
  30. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  31. package/dist/runtime/attempt-private-directory.js +54 -0
  32. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  33. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  34. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  35. package/dist/runtime/attempt-process-supervisor.js +386 -0
  36. package/dist/runtime/canonical-source-file.d.ts +3 -0
  37. package/dist/runtime/canonical-source-file.js +54 -0
  38. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  39. package/dist/runtime/docker-engine-probe.js +300 -0
  40. package/dist/runtime/generated-output-reset.d.ts +10 -0
  41. package/dist/runtime/generated-output-reset.js +103 -0
  42. package/dist/runtime/host-diagnostics.d.ts +4 -0
  43. package/dist/runtime/host-diagnostics.js +95 -0
  44. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  45. package/dist/runtime/host-listener-inspector.js +349 -0
  46. package/dist/runtime/host-port-broker.d.ts +76 -0
  47. package/dist/runtime/host-port-broker.js +333 -0
  48. package/dist/runtime/host-runtime-contract.d.ts +56 -0
  49. package/dist/runtime/host-runtime-contract.js +709 -0
  50. package/dist/runtime/host-runtime-lease.d.ts +201 -0
  51. package/dist/runtime/host-runtime-lease.js +2314 -0
  52. package/dist/runtime/host-runtime-promotion.d.ts +32 -0
  53. package/dist/runtime/host-runtime-promotion.js +206 -0
  54. package/dist/runtime/memory-capacity.d.ts +68 -0
  55. package/dist/runtime/memory-capacity.js +121 -0
  56. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  57. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  58. package/dist/runtime/pinned-directory-process.d.ts +4 -0
  59. package/dist/runtime/pinned-directory-process.js +20 -0
  60. package/dist/runtime/pinned-source-directory.d.ts +9 -0
  61. package/dist/runtime/pinned-source-directory.js +56 -0
  62. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  63. package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
  64. package/dist/runtime/prepared-runtime-identity.d.ts +3 -0
  65. package/dist/runtime/prepared-runtime-identity.js +191 -0
  66. package/dist/runtime/prepared-runtime-readiness.d.ts +22 -0
  67. package/dist/runtime/prepared-runtime-readiness.js +191 -0
  68. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  69. package/dist/runtime/runtime-isolation-lease.js +158 -0
  70. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  71. package/dist/runtime/workspace-resource-estimator.js +174 -0
  72. package/dist/sandbox/command-runner.d.ts +4 -0
  73. package/dist/sandbox/command-runner.js +47 -0
  74. package/dist/sandbox/contract.d.ts +5 -0
  75. package/dist/sandbox/contract.js +276 -0
  76. package/dist/sandbox/event-budget.d.ts +11 -0
  77. package/dist/sandbox/event-budget.js +27 -0
  78. package/dist/sandbox/image-policy.d.ts +35 -0
  79. package/dist/sandbox/image-policy.js +247 -0
  80. package/dist/sandbox/index.d.ts +5 -0
  81. package/dist/sandbox/index.js +5 -0
  82. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  83. package/dist/sandbox/lease-watchdog.js +67 -0
  84. package/dist/sandbox/preflight.d.ts +84 -0
  85. package/dist/sandbox/preflight.js +521 -0
  86. package/dist/sandbox/process.d.ts +20 -0
  87. package/dist/sandbox/process.js +209 -0
  88. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  89. package/dist/sandbox/provider-auth/codex.js +212 -0
  90. package/dist/sandbox/repository-lock.d.ts +5 -0
  91. package/dist/sandbox/repository-lock.js +121 -0
  92. package/dist/sandbox/runtime.d.ts +297 -0
  93. package/dist/sandbox/runtime.js +2338 -0
  94. package/dist/sandbox/sandbox.types.d.ts +93 -0
  95. package/dist/sandbox/sandbox.types.js +1 -0
  96. package/dist/sandbox/smoke.d.ts +76 -0
  97. package/dist/sandbox/smoke.js +380 -0
  98. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  99. package/dist/sandbox/worker-supervisor.js +36 -0
  100. package/dist/sandbox/worker.d.ts +1 -0
  101. package/dist/sandbox/worker.js +225 -0
  102. package/dist/sandbox/workspace.service.d.ts +13 -0
  103. package/dist/sandbox/workspace.service.js +72 -0
  104. package/dist/session/managed-attempt-tools.d.ts +21 -0
  105. package/dist/session/managed-attempt-tools.js +912 -0
  106. package/dist/session/managed-build-coordinator.d.ts +5 -0
  107. package/dist/session/managed-build-coordinator.js +105 -0
  108. package/dist/session/managed-build-receipt.d.ts +21 -0
  109. package/dist/session/managed-build-receipt.js +335 -0
  110. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  111. package/dist/session/phase-completion.artifacts.js +256 -26
  112. package/dist/session/phase-completion.d.ts +12 -2
  113. package/dist/session/phase-completion.helpers.d.ts +5 -0
  114. package/dist/session/phase-completion.helpers.js +51 -10
  115. package/dist/session/phase-completion.js +14 -3
  116. package/dist/session/phase-completion.prompts.d.ts +3 -3
  117. package/dist/session/phase-completion.prompts.js +24 -8
  118. package/dist/session/phase-completion.runtime.d.ts +13 -1
  119. package/dist/session/phase-completion.runtime.js +189 -8
  120. package/dist/session/phase-completion.schemas.js +117 -4
  121. package/dist/session/phase-completion.validation.d.ts +4 -1
  122. package/dist/session/phase-completion.validation.js +153 -22
  123. package/dist/session/pre-publication-validation.d.ts +47 -0
  124. package/dist/session/pre-publication-validation.js +249 -0
  125. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  126. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  127. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  128. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  129. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  130. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  131. package/dist/session/provider-cli/provider-cli.deterministic.js +68 -9
  132. package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
  133. package/dist/session/provider-cli/provider-cli.process.js +474 -23
  134. package/dist/session/provider-cli/provider-cli.prompts.d.ts +14 -2
  135. package/dist/session/provider-cli/provider-cli.prompts.js +147 -25
  136. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  137. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  138. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  139. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  140. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  141. package/dist/session/provider-cli/provider-cli.service.d.ts +25 -2
  142. package/dist/session/provider-cli/provider-cli.service.js +636 -84
  143. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  144. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  145. package/dist/session/provider-cli/provider-output-state.js +91 -16
  146. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  147. package/dist/session/provider-cli/provider-usage.js +28 -5
  148. package/dist/session/session-redaction.d.ts +3 -0
  149. package/dist/session/session-redaction.js +38 -0
  150. package/dist/session/session.base.d.ts +6 -1
  151. package/dist/session/session.service.d.ts +8 -2
  152. package/dist/session/session.service.js +531 -42
  153. package/dist/session/session.types.d.ts +115 -1
  154. package/dist/session/session.types.js +194 -3
  155. package/dist/shared/format/format-bytes.d.ts +1 -0
  156. package/dist/shared/format/format-bytes.js +9 -0
  157. package/dist/shared/git/git.js +2 -1
  158. package/dist/shared/path/path.d.ts +1 -0
  159. package/dist/shared/path/path.js +21 -1
  160. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  161. package/dist/shared/process/dotnet-toolchain.js +179 -0
  162. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  163. package/dist/shared/process/host-process-recovery.js +78 -0
  164. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  165. package/dist/shared/process/host-process-supervisor.js +330 -0
  166. package/dist/shared/process/process.d.ts +2 -0
  167. package/dist/shared/process/process.js +11 -6
  168. package/dist/shared/provider-capabilities.d.ts +1 -0
  169. package/dist/shared/provider-capabilities.js +3 -0
  170. package/dist/shared/redaction/redaction.js +12 -2
  171. package/dist/version.d.ts +1 -1
  172. package/dist/version.js +1 -1
  173. package/dist/worktree/artifact-injector.d.ts +3 -2
  174. package/dist/worktree/artifact-injector.js +43 -4
  175. package/dist/worktree/dependency-warmer.d.ts +28 -1
  176. package/dist/worktree/dependency-warmer.js +308 -9
  177. package/dist/worktree/index.d.ts +2 -1
  178. package/dist/worktree/index.js +1 -0
  179. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  180. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  181. package/dist/worktree/worktree-paths.d.ts +4 -0
  182. package/dist/worktree/worktree-paths.js +30 -0
  183. package/dist/worktree/worktree.base.d.ts +3 -0
  184. package/dist/worktree/worktree.service.d.ts +14 -2
  185. package/dist/worktree/worktree.service.js +296 -47
  186. package/dist/worktree/worktree.types.d.ts +5 -1
  187. package/package.json +8 -4
  188. package/runtime/windows-job-supervisor.ps1 +444 -0
  189. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -45,6 +45,15 @@ export function applyLocalConfigDefaults(config, local) {
45
45
  deviceName: process.env.FORGE_DEVICE_NAME ?? local.deviceName ?? config.deviceName,
46
46
  defaultRepoRoot: process.env.FORGE_REPO_ROOT ?? local.defaultRepoRoot ?? config.defaultRepoRoot,
47
47
  cleanupWorktrees: local.cleanupWorktrees ?? config.cleanupWorktrees,
48
+ dockerEnabled: process.env.FORGE_DOCKER_SANDBOX === undefined
49
+ ? local.dockerEnabled ?? config.dockerEnabled
50
+ : config.dockerEnabled,
51
+ dockerDesktopRecoveryEnabled: process.env.FORGE_DOCKER_DESKTOP_AUTO_RECOVERY === undefined
52
+ ? local.dockerDesktopRecoveryEnabled ?? config.dockerDesktopRecoveryEnabled
53
+ : config.dockerDesktopRecoveryEnabled,
54
+ allowOpenPublicSandbox: process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX === undefined
55
+ ? local.allowOpenPublicSandbox ?? config.allowOpenPublicSandbox
56
+ : config.allowOpenPublicSandbox,
48
57
  cliProviders,
49
58
  providerStatuses: normalizeProviderStatuses(local.providerStatuses) ?? providerStatusesFor(cliProviders),
50
59
  projectLabels: local.projectLabels ?? config.projectLabels,
@@ -88,6 +97,19 @@ export async function removeRepoMapping(label) {
88
97
  await writeLocalConfig(next);
89
98
  return { config: next, removed: after.length !== before.length };
90
99
  }
100
+ export async function setServerUrl(serverUrl) {
101
+ const normalized = normalizeServerUrl(serverUrl);
102
+ const current = await readLocalConfig();
103
+ const next = normalizeLocalConfig({ ...current, serverUrl: normalized });
104
+ await writeLocalConfig(next);
105
+ return next;
106
+ }
107
+ export async function setSandboxPolicy(dockerEnabled, allowOpenPublicSandbox) {
108
+ const current = await readLocalConfig();
109
+ const next = normalizeLocalConfig({ ...current, dockerEnabled, allowOpenPublicSandbox });
110
+ await writeLocalConfig(next);
111
+ return next;
112
+ }
91
113
  export function providerStatusesFor(providers) {
92
114
  return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
93
115
  }
@@ -106,6 +128,9 @@ function normalizeLocalConfig(config) {
106
128
  deviceName: cleanString(config.deviceName),
107
129
  defaultRepoRoot: cleanString(config.defaultRepoRoot),
108
130
  cleanupWorktrees: config.cleanupWorktrees,
131
+ dockerEnabled: config.dockerEnabled,
132
+ dockerDesktopRecoveryEnabled: config.dockerDesktopRecoveryEnabled,
133
+ allowOpenPublicSandbox: config.allowOpenPublicSandbox,
109
134
  cliProviders: cleanList(config.cliProviders),
110
135
  providerStatuses: normalizeProviderStatuses(config.providerStatuses),
111
136
  projectLabels: cleanList(config.projectLabels),
@@ -200,10 +225,11 @@ function normalizeQaAppLaunchTarget(value) {
200
225
  command: cleanString(value.command),
201
226
  working_directory: cleanString(value.working_directory),
202
227
  ready_url: cleanString(value.ready_url),
228
+ managed_runtime_port: cleanString(value.managed_runtime_port),
203
229
  routes,
204
230
  notes: cleanString(value.notes)
205
231
  };
206
- return normalized.url || normalized.command || normalized.ready_url || routes?.length
232
+ return normalized.url || normalized.command || normalized.ready_url || normalized.managed_runtime_port
207
233
  ? normalized
208
234
  : undefined;
209
235
  }
@@ -211,6 +237,24 @@ function cleanString(value) {
211
237
  const clean = value?.trim();
212
238
  return clean ? clean : undefined;
213
239
  }
240
+ function normalizeServerUrl(value) {
241
+ const clean = value.trim();
242
+ let parsed;
243
+ try {
244
+ parsed = new URL(clean);
245
+ }
246
+ catch {
247
+ throw new Error(`Invalid Forge server URL: ${value}`);
248
+ }
249
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
250
+ throw new Error("Forge server URL must use http or https.");
251
+ }
252
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
253
+ throw new Error("Forge server URL cannot include credentials, a query string, or a fragment.");
254
+ }
255
+ parsed.pathname = parsed.pathname.replace(/\/+$/u, "") || "/";
256
+ return parsed.toString().replace(/\/$/u, "");
257
+ }
214
258
  function cleanList(values) {
215
259
  const clean = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
216
260
  return clean.length > 0 ? clean : undefined;
@@ -0,0 +1,36 @@
1
+ export interface AspNetRuntimePrebuildPlan {
2
+ readonly fingerprint: string;
3
+ readonly receiptPath: string;
4
+ readonly command: string;
5
+ readonly args: readonly string[];
6
+ readonly cwd: string;
7
+ readonly environment: Readonly<Record<string, string>>;
8
+ readonly projectPath: string;
9
+ readonly worktreePath: string;
10
+ readonly inputCommitSha: string;
11
+ }
12
+ export interface AspNetRuntimePrebuildResult {
13
+ readonly cacheHit: boolean;
14
+ readonly receiptPath: string;
15
+ readonly fingerprint: string;
16
+ }
17
+ export interface AspNetRuntimeBuildExecutionResult {
18
+ readonly exitCode: number;
19
+ readonly stdoutTail: string;
20
+ readonly stderrTail: string;
21
+ readonly stdoutLogPath: string;
22
+ readonly stderrLogPath: string;
23
+ }
24
+ export type AspNetRuntimeBuildExecutor = (plan: AspNetRuntimePrebuildPlan, signal?: AbortSignal) => Promise<AspNetRuntimeBuildExecutionResult>;
25
+ export declare function createAspNetRuntimePrebuildPlan(options: {
26
+ readonly command: string;
27
+ readonly args: readonly string[];
28
+ readonly cwd: string;
29
+ readonly worktreePath: string;
30
+ readonly environment?: Readonly<Record<string, string>>;
31
+ readonly inputCommitSha: string;
32
+ readonly runtimeContractFingerprint: string;
33
+ }): AspNetRuntimePrebuildPlan | null;
34
+ export declare function ensureAspNetRuntimePrebuilt(plan: AspNetRuntimePrebuildPlan, execute: AspNetRuntimeBuildExecutor, signal?: AbortSignal): Promise<AspNetRuntimePrebuildResult>;
35
+ export declare function withAspNetPrebuiltRunArguments(command: string, args: readonly string[]): readonly string[];
36
+ export declare function resolveWorktreeCommitSha(worktreePath: string, supplied?: string | null): Promise<string>;
@@ -0,0 +1,340 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ const inFlightPrebuilds = new Map();
6
+ export function createAspNetRuntimePrebuildPlan(options) {
7
+ if (!isDotnetRun(options.command, options.args))
8
+ return null;
9
+ const projectArgument = readOptionValue(options.args, "--project");
10
+ const projectPath = path.resolve(options.cwd, projectArgument ?? ".");
11
+ const normalizedWorktree = path.resolve(options.worktreePath);
12
+ const relativeProject = path.relative(normalizedWorktree, projectPath);
13
+ if (relativeProject.startsWith("..") || path.isAbsolute(relativeProject)) {
14
+ throw new Error("host_runtime_build_invalid: the ASP.NET project must remain inside the managed worktree.");
15
+ }
16
+ const projectDirectory = projectArgument && path.extname(projectPath).toLowerCase() === ".csproj"
17
+ ? path.dirname(projectPath)
18
+ : projectPath;
19
+ const buildArgs = [
20
+ "build",
21
+ ...(projectArgument ? [projectArgument] : []),
22
+ "--nologo",
23
+ "--no-restore",
24
+ "--disable-build-servers",
25
+ "-p:UseSharedCompilation=false",
26
+ "-p:DefaultItemExcludesInProjectFolder=TestResults/**",
27
+ ...forwardBuildOptions(options.args),
28
+ ];
29
+ const fingerprint = `sha256:${createHash("sha256").update(JSON.stringify({
30
+ inputCommitSha: options.inputCommitSha,
31
+ runtimeContractFingerprint: options.runtimeContractFingerprint,
32
+ project: relativeProject.replaceAll("\\", "/"),
33
+ args: buildArgs,
34
+ environment: buildEnvironmentContract(options.environment ?? {}),
35
+ })).digest("hex")}`;
36
+ return {
37
+ fingerprint,
38
+ receiptPath: path.join(projectDirectory, "obj", ".forge-runtime-build.json"),
39
+ command: options.command,
40
+ args: buildArgs,
41
+ cwd: options.cwd,
42
+ environment: options.environment ?? {},
43
+ projectPath,
44
+ worktreePath: normalizedWorktree,
45
+ inputCommitSha: options.inputCommitSha,
46
+ };
47
+ }
48
+ export async function ensureAspNetRuntimePrebuilt(plan, execute, signal) {
49
+ const pending = inFlightPrebuilds.get(plan.receiptPath);
50
+ if (pending)
51
+ return await pending;
52
+ const started = ensureAspNetRuntimePrebuiltCore(plan, execute, signal);
53
+ inFlightPrebuilds.set(plan.receiptPath, started);
54
+ try {
55
+ return await started;
56
+ }
57
+ finally {
58
+ if (inFlightPrebuilds.get(plan.receiptPath) === started)
59
+ inFlightPrebuilds.delete(plan.receiptPath);
60
+ }
61
+ }
62
+ async function ensureAspNetRuntimePrebuiltCore(plan, execute, signal) {
63
+ const inputs = await aspNetBuildInputs(plan);
64
+ const fingerprint = `sha256:${createHash("sha256").update(JSON.stringify({
65
+ planFingerprint: plan.fingerprint,
66
+ ...inputs,
67
+ })).digest("hex")}`;
68
+ const receipt = await readReceipt(plan.receiptPath);
69
+ if (receipt?.fingerprint === fingerprint
70
+ && receipt.planFingerprint === plan.fingerprint
71
+ && receipt.inputCommitSha === plan.inputCommitSha
72
+ && await aspNetOutputsMatch(plan, receipt.outputs)) {
73
+ return { cacheHit: true, receiptPath: plan.receiptPath, fingerprint };
74
+ }
75
+ const result = await execute(plan, signal);
76
+ if (result.exitCode !== 0) {
77
+ const evidence = [
78
+ diagnostic("stderr", result.stderrTail),
79
+ diagnostic("stdout", result.stdoutTail),
80
+ `stdout log: ${result.stdoutLogPath}`,
81
+ `stderr log: ${result.stderrLogPath}`,
82
+ ].filter(Boolean).join("; ");
83
+ throw new Error(`host_runtime_build_failed: dotnet build exited with code ${result.exitCode}.${evidence ? ` ${evidence}` : ""}`);
84
+ }
85
+ await fs.mkdir(path.dirname(plan.receiptPath), { recursive: true });
86
+ const outputs = await collectAspNetBuildOutputs(plan);
87
+ if (outputs.length === 0) {
88
+ throw new Error("host_runtime_build_output_missing: dotnet build succeeded without producing a verifiable project output.");
89
+ }
90
+ const temporaryReceipt = `${plan.receiptPath}.${randomUUID()}.tmp`;
91
+ const nextReceipt = {
92
+ schemaVersion: 2,
93
+ fingerprint,
94
+ planFingerprint: plan.fingerprint,
95
+ inputCommitSha: plan.inputCommitSha,
96
+ projectPath: plan.projectPath,
97
+ ...inputs,
98
+ outputs,
99
+ completedAt: new Date().toISOString(),
100
+ };
101
+ await fs.writeFile(temporaryReceipt, `${JSON.stringify(nextReceipt)}\n`, { encoding: "utf8", mode: 0o600 });
102
+ try {
103
+ await fs.rename(temporaryReceipt, plan.receiptPath);
104
+ }
105
+ finally {
106
+ await fs.rm(temporaryReceipt, { force: true }).catch(() => undefined);
107
+ }
108
+ return { cacheHit: false, receiptPath: plan.receiptPath, fingerprint };
109
+ }
110
+ export function withAspNetPrebuiltRunArguments(command, args) {
111
+ if (!isDotnetRun(command, args)) {
112
+ throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires a dotnet run command.");
113
+ }
114
+ const applicationArguments = args.indexOf("--");
115
+ const insertionIndex = applicationArguments < 0 ? args.length : applicationArguments;
116
+ const beforeApplication = args.slice(0, insertionIndex);
117
+ return [
118
+ ...beforeApplication,
119
+ ...(!beforeApplication.includes("--no-build") ? ["--no-build"] : []),
120
+ ...(!beforeApplication.includes("--no-restore") ? ["--no-restore"] : []),
121
+ ...args.slice(insertionIndex),
122
+ ];
123
+ }
124
+ export async function resolveWorktreeCommitSha(worktreePath, supplied) {
125
+ if (supplied && /^[0-9a-f]{40,64}$/iu.test(supplied))
126
+ return supplied.toLowerCase();
127
+ return await new Promise((resolve, reject) => {
128
+ const child = spawn("git", ["rev-parse", "--verify", "HEAD^{commit}"], {
129
+ cwd: worktreePath,
130
+ windowsHide: true,
131
+ stdio: ["ignore", "pipe", "pipe"],
132
+ });
133
+ let stdout = "";
134
+ let stderr = "";
135
+ child.stdout.setEncoding("utf8");
136
+ child.stderr.setEncoding("utf8");
137
+ child.stdout.on("data", chunk => { stdout = `${stdout}${String(chunk)}`.slice(-256); });
138
+ child.stderr.on("data", chunk => { stderr = `${stderr}${String(chunk)}`.slice(-1_000); });
139
+ child.once("error", reject);
140
+ child.once("close", code => {
141
+ const commit = stdout.trim();
142
+ if (code === 0 && /^[0-9a-f]{40,64}$/iu.test(commit))
143
+ resolve(commit.toLowerCase());
144
+ else
145
+ reject(new Error(`host_runtime_build_commit_unavailable: git rev-parse failed (${code ?? "unknown"}): ${stderr.trim()}`));
146
+ });
147
+ });
148
+ }
149
+ function isDotnetRun(command, args) {
150
+ const executable = path.basename(command).toLowerCase();
151
+ return (executable === "dotnet" || executable === "dotnet.exe") && args[0]?.toLowerCase() === "run";
152
+ }
153
+ function readOptionValue(args, option) {
154
+ const index = args.findIndex(argument => argument === option || argument.startsWith(`${option}=`));
155
+ if (index < 0)
156
+ return null;
157
+ const argument = args[index];
158
+ return argument.startsWith(`${option}=`) ? argument.slice(option.length + 1) : args[index + 1] ?? null;
159
+ }
160
+ function forwardBuildOptions(args) {
161
+ const forwarded = [];
162
+ const optionsWithValues = new Set(["-c", "--configuration", "-f", "--framework", "-r", "--runtime", "--arch", "--os"]);
163
+ for (let index = 1; index < args.length; index += 1) {
164
+ const argument = args[index];
165
+ if (argument === "--")
166
+ break;
167
+ if (argument === "--project") {
168
+ index += 1;
169
+ continue;
170
+ }
171
+ if (argument.startsWith("--project="))
172
+ continue;
173
+ if (optionsWithValues.has(argument)) {
174
+ const value = args[index + 1];
175
+ if (value !== undefined) {
176
+ forwarded.push(argument, value);
177
+ index += 1;
178
+ }
179
+ continue;
180
+ }
181
+ if ([...optionsWithValues].some(option => argument.startsWith(`${option}=`))
182
+ || argument.startsWith("-p:")
183
+ || argument.startsWith("/p:")) {
184
+ forwarded.push(argument);
185
+ }
186
+ }
187
+ return forwarded;
188
+ }
189
+ async function readReceipt(receiptPath) {
190
+ try {
191
+ const parsed = JSON.parse(await fs.readFile(receiptPath, "utf8"));
192
+ return parsed.schemaVersion === 2
193
+ && typeof parsed.fingerprint === "string"
194
+ && typeof parsed.planFingerprint === "string"
195
+ && typeof parsed.inputCommitSha === "string"
196
+ && typeof parsed.sourceDigest === "string"
197
+ && typeof parsed.dependencyDigest === "string"
198
+ && typeof parsed.sdkVersion === "string"
199
+ && Array.isArray(parsed.outputs)
200
+ ? parsed
201
+ : null;
202
+ }
203
+ catch (error) {
204
+ if (error.code === "ENOENT" || error instanceof SyntaxError)
205
+ return null;
206
+ throw error;
207
+ }
208
+ }
209
+ async function aspNetBuildInputs(plan) {
210
+ const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
211
+ ? path.dirname(plan.projectPath)
212
+ : plan.projectPath;
213
+ const sourceDigest = await directoryContentDigest(projectDirectory);
214
+ const dependencyReceipt = path.join(projectDirectory, "obj", ".forge-dependency-preparation.json");
215
+ const dependencyContent = await fs.readFile(dependencyReceipt).catch(() => null);
216
+ const globalJson = await findAncestorFile(projectDirectory, plan.worktreePath, "global.json");
217
+ const globalJsonContent = globalJson ? await fs.readFile(globalJson) : null;
218
+ return {
219
+ sourceDigest,
220
+ dependencyDigest: `sha256:${createHash("sha256")
221
+ .update(dependencyContent ?? Buffer.alloc(0))
222
+ .update(globalJsonContent ?? Buffer.alloc(0))
223
+ .digest("hex")}`,
224
+ sdkVersion: await dotnetSdkVersion(plan.cwd),
225
+ };
226
+ }
227
+ async function directoryContentDigest(directory) {
228
+ const files = [];
229
+ await collectInputFiles(directory, directory, files);
230
+ const hash = createHash("sha256");
231
+ for (const file of files.sort((left, right) => left.localeCompare(right))) {
232
+ hash.update(path.relative(directory, file).replaceAll("\\", "/"));
233
+ hash.update("\0");
234
+ hash.update(await fs.readFile(file));
235
+ hash.update("\0");
236
+ }
237
+ return `sha256:${hash.digest("hex")}`;
238
+ }
239
+ async function collectInputFiles(root, directory, files) {
240
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
241
+ for (const entry of entries) {
242
+ const absolute = path.join(directory, entry.name);
243
+ if (entry.isDirectory()) {
244
+ if (!["bin", "obj", "node_modules", ".git", "TestResults"].includes(entry.name)) {
245
+ await collectInputFiles(root, absolute, files);
246
+ }
247
+ }
248
+ else if (entry.isFile()) {
249
+ files.push(absolute);
250
+ }
251
+ }
252
+ }
253
+ async function collectAspNetBuildOutputs(plan) {
254
+ const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
255
+ ? path.dirname(plan.projectPath)
256
+ : plan.projectPath;
257
+ const outputRoot = path.join(projectDirectory, "bin");
258
+ const outputs = [];
259
+ await collectOutputFiles(outputRoot, projectDirectory, outputs);
260
+ return outputs.sort((left, right) => left.path.localeCompare(right.path));
261
+ }
262
+ async function collectOutputFiles(directory, projectDirectory, outputs) {
263
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
264
+ for (const entry of entries) {
265
+ const absolute = path.join(directory, entry.name);
266
+ if (entry.isDirectory())
267
+ await collectOutputFiles(absolute, projectDirectory, outputs);
268
+ else if (entry.isFile()) {
269
+ const stat = await fs.stat(absolute);
270
+ outputs.push({
271
+ path: path.relative(projectDirectory, absolute).replaceAll("\\", "/"),
272
+ size: stat.size,
273
+ digest: createHash("sha256").update(await fs.readFile(absolute)).digest("hex"),
274
+ });
275
+ }
276
+ }
277
+ }
278
+ async function aspNetOutputsMatch(plan, outputs) {
279
+ if (outputs.length === 0)
280
+ return false;
281
+ const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
282
+ ? path.dirname(plan.projectPath)
283
+ : plan.projectPath;
284
+ for (const output of outputs) {
285
+ const absolute = path.resolve(projectDirectory, output.path);
286
+ const relative = path.relative(projectDirectory, absolute);
287
+ if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
288
+ return false;
289
+ const stat = await fs.stat(absolute).catch(() => null);
290
+ if (!stat?.isFile() || stat.size !== output.size)
291
+ return false;
292
+ const digest = createHash("sha256").update(await fs.readFile(absolute)).digest("hex");
293
+ if (digest !== output.digest)
294
+ return false;
295
+ }
296
+ return true;
297
+ }
298
+ async function findAncestorFile(directory, root, name) {
299
+ let current = path.resolve(directory);
300
+ const resolvedRoot = path.resolve(root);
301
+ while (true) {
302
+ const candidate = path.join(current, name);
303
+ if (await fs.stat(candidate).then(stat => stat.isFile()).catch(() => false))
304
+ return candidate;
305
+ if (current === resolvedRoot)
306
+ return null;
307
+ const parent = path.dirname(current);
308
+ if (parent === current || path.relative(resolvedRoot, parent).startsWith(".."))
309
+ return null;
310
+ current = parent;
311
+ }
312
+ }
313
+ async function dotnetSdkVersion(cwd) {
314
+ return await new Promise(resolve => {
315
+ const child = spawn("dotnet", ["--version"], { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
316
+ let output = "";
317
+ child.stdout.setEncoding("utf8");
318
+ child.stderr.setEncoding("utf8");
319
+ child.stdout.on("data", chunk => { output += String(chunk); });
320
+ child.stderr.on("data", chunk => { output += String(chunk); });
321
+ child.once("error", () => resolve("unavailable"));
322
+ child.once("close", code => resolve(code === 0 ? output.trim().slice(0, 256) : `unavailable:${code ?? "unknown"}`));
323
+ });
324
+ }
325
+ function buildEnvironmentContract(environment) {
326
+ return {
327
+ DOTNET_ROOT: environment.DOTNET_ROOT ?? process.env.DOTNET_ROOT ?? null,
328
+ DOTNET_ROLL_FORWARD: environment.DOTNET_ROLL_FORWARD ?? process.env.DOTNET_ROLL_FORWARD ?? null,
329
+ DOTNET_MULTILEVEL_LOOKUP: environment.DOTNET_MULTILEVEL_LOOKUP ?? process.env.DOTNET_MULTILEVEL_LOOKUP ?? null,
330
+ MSBuildSDKsPath: environment.MSBuildSDKsPath ?? process.env.MSBuildSDKsPath ?? null,
331
+ NUGET_PACKAGES: environment.NUGET_PACKAGES ?? process.env.NUGET_PACKAGES ?? null,
332
+ FORGE_BUILD_CACHE_EPOCH: createHash("sha256")
333
+ .update(environment.FORGE_BUILD_CACHE_EPOCH ?? process.env.FORGE_BUILD_CACHE_EPOCH ?? "default")
334
+ .digest("hex"),
335
+ };
336
+ }
337
+ function diagnostic(label, value) {
338
+ const normalized = value.replaceAll(/[\r\n\t]+/gu, " ").trim();
339
+ return normalized ? `${label} tail: ${normalized.slice(-2_000)}` : "";
340
+ }
@@ -0,0 +1,17 @@
1
+ import type { AttemptProcessChildHandle, AttemptProcessSupervisor } from "./attempt-process-supervisor.js";
2
+ import type { HostPortLeaseOwner } from "./host-port-broker.js";
3
+ import { HostListenerInspector } from "./host-listener-inspector.js";
4
+ import type { HostListenerOwnership, NodeRuntimeCommandLauncher, NodeRuntimeLaunchRequest, NodeRuntimeReadinessDiagnostics } from "./node-host-runtime-adapter.js";
5
+ /** Launches one explicitly configured HTTP project beneath the attempt coordinator. */
6
+ export declare class AttemptNodeRuntimeLauncher implements NodeRuntimeCommandLauncher<AttemptProcessChildHandle> {
7
+ private readonly supervisor;
8
+ private readonly owner;
9
+ private readonly logDirectory;
10
+ private readonly listenerInspector;
11
+ private readonly diagnosticsByChild;
12
+ constructor(supervisor: AttemptProcessSupervisor, owner: HostPortLeaseOwner, logDirectory: string, listenerInspector?: HostListenerInspector);
13
+ launch(request: NodeRuntimeLaunchRequest, signal?: AbortSignal): Promise<AttemptProcessChildHandle>;
14
+ inspectListener(child: AttemptProcessChildHandle, request: NodeRuntimeLaunchRequest): Promise<HostListenerOwnership>;
15
+ stop(child: AttemptProcessChildHandle): Promise<void>;
16
+ readinessDiagnostics(child: AttemptProcessChildHandle): Promise<NodeRuntimeReadinessDiagnostics>;
17
+ }
@@ -0,0 +1,105 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { HostListenerInspector } from "./host-listener-inspector.js";
5
+ const diagnosticTailLength = 4_000;
6
+ /** Launches one explicitly configured HTTP project beneath the attempt coordinator. */
7
+ export class AttemptNodeRuntimeLauncher {
8
+ supervisor;
9
+ owner;
10
+ logDirectory;
11
+ listenerInspector;
12
+ diagnosticsByChild = new WeakMap();
13
+ constructor(supervisor, owner, logDirectory, listenerInspector = new HostListenerInspector()) {
14
+ this.supervisor = supervisor;
15
+ this.owner = owner;
16
+ this.logDirectory = logDirectory;
17
+ this.listenerInspector = listenerInspector;
18
+ }
19
+ async launch(request, signal) {
20
+ const childName = projectChildName(request.port.name);
21
+ const stdoutPath = path.join(this.logDirectory, `${childName}.stdout.log`);
22
+ const stderrPath = path.join(this.logDirectory, `${childName}.stderr.log`);
23
+ const diagnostics = { exited: false, stdoutTail: "", stderrTail: "", outputRevision: 0 };
24
+ let child;
25
+ try {
26
+ child = await this.supervisor.startChildReady(childName, {
27
+ command: request.command,
28
+ args: request.args,
29
+ cwd: request.cwd,
30
+ env: request.environment,
31
+ captureOutput: false,
32
+ }, {
33
+ onStdoutLine: async (line) => {
34
+ diagnostics.stdoutTail = appendTail(diagnostics.stdoutTail, line);
35
+ diagnostics.outputRevision += 1;
36
+ await appendLine(stdoutPath, line);
37
+ },
38
+ onStderrLine: async (line) => {
39
+ diagnostics.stderrTail = appendTail(diagnostics.stderrTail, line);
40
+ diagnostics.outputRevision += 1;
41
+ await appendLine(stderrPath, line);
42
+ },
43
+ }, signal, { maxStartAttempts: 3, retryDelayMs: 250 });
44
+ }
45
+ catch (error) {
46
+ throw enrichLaunchFailure(error, diagnostics);
47
+ }
48
+ this.diagnosticsByChild.set(child, diagnostics);
49
+ void child.result.then(result => {
50
+ diagnostics.exited = true;
51
+ diagnostics.exitCode = result.exitCode;
52
+ diagnostics.stdoutTail = appendTail(diagnostics.stdoutTail, result.stdout);
53
+ diagnostics.stderrTail = appendTail(diagnostics.stderrTail, result.stderr);
54
+ diagnostics.outputRevision += 1;
55
+ }, error => {
56
+ diagnostics.exited = true;
57
+ diagnostics.stderrTail = appendTail(diagnostics.stderrTail, error instanceof Error ? error.message : String(error));
58
+ diagnostics.outputRevision += 1;
59
+ });
60
+ return child;
61
+ }
62
+ async inspectListener(child, request) {
63
+ return await this.listenerInspector.inspect(child, request.port, this.owner);
64
+ }
65
+ async stop(child) {
66
+ await child.stop(this.owner.fencingToken);
67
+ }
68
+ async readinessDiagnostics(child) {
69
+ // Yield once so an already-settled result can update the diagnostic record.
70
+ await Promise.resolve();
71
+ const diagnostics = this.diagnosticsByChild.get(child);
72
+ return diagnostics ?? { exited: false };
73
+ }
74
+ }
75
+ function projectChildName(portName) {
76
+ const candidate = `project-${portName}`;
77
+ if (/^[a-z][a-z0-9_-]{0,31}$/u.test(candidate))
78
+ return candidate;
79
+ const digest = createHash("sha256").update(portName).digest("hex").slice(0, 16);
80
+ return `project-${digest}`;
81
+ }
82
+ async function appendLine(target, line) {
83
+ await fs.appendFile(target, `${line}\n`, { encoding: "utf8", mode: 0o600 });
84
+ }
85
+ function appendTail(current, value) {
86
+ if (!value)
87
+ return current;
88
+ const combined = current ? `${current}\n${value}` : value;
89
+ return combined.slice(-diagnosticTailLength);
90
+ }
91
+ function enrichLaunchFailure(error, diagnostics) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ const evidence = [
94
+ diagnostics.stderrTail ? `stderr tail: ${boundedDiagnostic(diagnostics.stderrTail)}` : "",
95
+ diagnostics.stdoutTail ? `stdout tail: ${boundedDiagnostic(diagnostics.stdoutTail)}` : "",
96
+ ].filter(Boolean);
97
+ return evidence.length > 0
98
+ ? new Error(`${message} ${evidence.join("; ")}`, { cause: error })
99
+ : error instanceof Error
100
+ ? error
101
+ : new Error(message);
102
+ }
103
+ function boundedDiagnostic(value) {
104
+ return value.replaceAll(/[\r\n\t]+/gu, " ").trim().slice(-1_000);
105
+ }
@@ -0,0 +1,21 @@
1
+ import type { BigIntStats } from "node:fs";
2
+ /** A separate process pins cwd without changing the multithreaded Bridge's cwd.
3
+ * All writes use single relative components, never a re-resolved ancestor path.
4
+ */
5
+ export declare function runAttemptPathOperation(parent: string, identity: BigIntStats, operation: {
6
+ kind: "mkdir";
7
+ segments: string[];
8
+ } | {
9
+ kind: "private-directory-mode";
10
+ segments: string[];
11
+ } | {
12
+ kind: "overlay";
13
+ name: string;
14
+ content: string;
15
+ }): Promise<void>;
16
+ /** Archive copies may be large or numerous: never block Bridge heartbeats while a pinned worker writes. */
17
+ export declare function runAttemptArchiveOperation(parent: string, identity: BigIntStats, operation: {
18
+ name: string;
19
+ bytes: Buffer;
20
+ sha256: string;
21
+ }): Promise<void>;