@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.1ff37038366e
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.
- package/README.md +165 -1
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +9 -1
- package/dist/bridge/bridge.service.d.ts +129 -3
- package/dist/bridge/bridge.service.js +2427 -70
- package/dist/bridge/bridge.types.d.ts +218 -2
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +14 -2
- package/dist/cleanup/cleanup.service.js +496 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +55 -7
- package/dist/cli/cli.service.js +429 -219
- package/dist/cli/local-config.d.ts +7 -0
- package/dist/cli/local-config.js +51 -3
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +18 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
- package/dist/runtime/attempt-process-supervisor.js +397 -0
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +20 -0
- package/dist/runtime/generated-output-reset.js +157 -0
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +191 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +51 -0
- package/dist/runtime/host-listener-inspector.js +385 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +58 -0
- package/dist/runtime/host-runtime-contract.js +813 -0
- package/dist/runtime/host-runtime-lease.d.ts +217 -0
- package/dist/runtime/host-runtime-lease.js +2487 -0
- package/dist/runtime/host-runtime-promotion.d.ts +32 -0
- package/dist/runtime/host-runtime-promotion.js +206 -0
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +285 -0
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +161 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +84 -0
- package/dist/sandbox/preflight.js +521 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +6 -0
- package/dist/sandbox/repository-lock.js +128 -0
- package/dist/sandbox/runtime.d.ts +302 -0
- package/dist/sandbox/runtime.js +2367 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +74 -0
- package/dist/session/managed-attempt-tools.d.ts +28 -0
- package/dist/session/managed-attempt-tools.js +1015 -0
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +27 -0
- package/dist/session/phase-completion.artifacts.js +829 -0
- package/dist/session/phase-completion.d.ts +53 -42
- package/dist/session/phase-completion.helpers.d.ts +117 -0
- package/dist/session/phase-completion.helpers.js +707 -0
- package/dist/session/phase-completion.js +125 -3007
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +233 -0
- package/dist/session/phase-completion.runtime.d.ts +49 -0
- package/dist/session/phase-completion.runtime.js +840 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +859 -0
- package/dist/session/phase-completion.validation.d.ts +38 -0
- package/dist/session/phase-completion.validation.js +918 -0
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +661 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
- package/dist/session/provider-cli/provider-cli.process.js +874 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +16 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +81 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
- package/dist/session/provider-cli/provider-cli.service.js +1257 -2497
- package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
- package/dist/session/provider-cli/provider-output-state.js +817 -0
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +448 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +586 -59
- package/dist/session/session.types.d.ts +324 -3
- package/dist/session/session.types.js +939 -10
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +21 -1
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +59 -0
- package/dist/shared/process/host-process-supervisor.js +330 -0
- package/dist/shared/process/process.d.ts +3 -0
- package/dist/shared/process/process.js +13 -7
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +4 -0
- package/dist/shared/provider-capabilities.js +88 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +47 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +82 -10
- package/dist/worktree/dependency-warmer.d.ts +53 -1
- package/dist/worktree/dependency-warmer.js +619 -11
- package/dist/worktree/index.d.ts +2 -1
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +16 -3
- package/dist/worktree/worktree.service.js +334 -54
- package/dist/worktree/worktree.types.d.ts +9 -1
- package/package.json +12 -7
- package/runtime/windows-job-supervisor.ps1 +722 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type GitRunner = (args: readonly string[]) => Promise<{
|
|
2
|
+
exitCode: number;
|
|
3
|
+
stdout?: string;
|
|
4
|
+
}>;
|
|
5
|
+
export type UnreferencedExport = {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly testOnly: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function isTestPath(file: string): boolean;
|
|
11
|
+
export declare function addedExports(diff: string): Map<string, string>;
|
|
12
|
+
export declare function findUnreferencedNewExports(git: GitRunner, worktreePath: string, baselineRevision: string | undefined): Promise<UnreferencedExport[]>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Finds exports added by this change that no production code uses. An export reached only from
|
|
2
|
+
// tests is code the current Story does not actually deliver: reviewers flag it and the Story
|
|
3
|
+
// cannot fix it without changing scope, so publication stops before the PR is opened.
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
const SourceFile = /\.(?:[cm]?[jt]sx?)$/u;
|
|
7
|
+
const TestFile = /(?:^|\/)(?:test|tests|__tests__|e2e)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/u;
|
|
8
|
+
const ExportDeclaration = /^\+\s*export\s+(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\*?|const|let|var|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/u;
|
|
9
|
+
const Lines = /\r?\n/u;
|
|
10
|
+
const MaximumNames = 200;
|
|
11
|
+
export function isTestPath(file) {
|
|
12
|
+
return TestFile.test(file);
|
|
13
|
+
}
|
|
14
|
+
export function addedExports(diff) {
|
|
15
|
+
const added = new Map();
|
|
16
|
+
let file = null;
|
|
17
|
+
for (const line of diff.split(Lines)) {
|
|
18
|
+
if (line.startsWith("+++ ")) {
|
|
19
|
+
const target = line.slice(4).trim();
|
|
20
|
+
file = target === "/dev/null" ? null : target.replace(/^b\//u, "");
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!file || !SourceFile.test(file) || isTestPath(file) || file.endsWith(".d.ts"))
|
|
24
|
+
continue;
|
|
25
|
+
const match = ExportDeclaration.exec(line);
|
|
26
|
+
if (match && added.size < MaximumNames)
|
|
27
|
+
added.set(match[1], file);
|
|
28
|
+
}
|
|
29
|
+
return added;
|
|
30
|
+
}
|
|
31
|
+
export async function findUnreferencedNewExports(git, worktreePath, baselineRevision) {
|
|
32
|
+
const diff = await git(["diff", "--no-color", "--no-ext-diff", "-U0", baselineRevision?.trim() || "HEAD", "--"]);
|
|
33
|
+
if (diff.exitCode !== 0)
|
|
34
|
+
return [];
|
|
35
|
+
// New files are untracked until Bridge commits them, so git diff alone would miss their exports.
|
|
36
|
+
const untracked = await git(["ls-files", "--others", "--exclude-standard"]);
|
|
37
|
+
const sections = [diff.stdout ?? ""];
|
|
38
|
+
for (const file of (untracked.exitCode === 0 ? untracked.stdout ?? "" : "").split(Lines).filter(row => SourceFile.test(row) && !isTestPath(row))) {
|
|
39
|
+
const content = await fs.readFile(path.join(worktreePath, file), "utf8").catch(() => "");
|
|
40
|
+
sections.push(`+++ b/${file}`, ...content.split(Lines).map(line => `+${line}`));
|
|
41
|
+
}
|
|
42
|
+
const found = [];
|
|
43
|
+
for (const [name, file] of addedExports(sections.join("\n"))) {
|
|
44
|
+
const grep = await git(["grep", "--untracked", "-I", "-c", "-w", "-F", "-e", name, "--"]);
|
|
45
|
+
// git grep exits 1 for no match; any other failure skips the name rather than guessing.
|
|
46
|
+
if (grep.exitCode !== 0 && grep.exitCode !== 1)
|
|
47
|
+
continue;
|
|
48
|
+
const counts = (grep.stdout ?? "").split(Lines).filter(Boolean).map(row => {
|
|
49
|
+
const separator = row.lastIndexOf(":");
|
|
50
|
+
return { file: row.slice(0, separator).replaceAll("\\", "/"), count: Number(row.slice(separator + 1)) };
|
|
51
|
+
});
|
|
52
|
+
const ownUses = counts.find(row => row.file === file)?.count ?? 0;
|
|
53
|
+
const elsewhere = counts.filter(row => row.file !== file);
|
|
54
|
+
if (ownUses > 1 || elsewhere.some(row => !isTestPath(row.file)))
|
|
55
|
+
continue;
|
|
56
|
+
found.push({ name, path: file, testOnly: elsewhere.length > 0 });
|
|
57
|
+
}
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatBytes(bytes: number): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function formatBytes(bytes) {
|
|
2
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
3
|
+
return "0 B";
|
|
4
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
5
|
+
const power = Math.max(0, Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024))));
|
|
6
|
+
const value = bytes / 1024 ** power;
|
|
7
|
+
const precision = power === 0 || value >= 100 ? 0 : 1;
|
|
8
|
+
return `${value.toFixed(precision)} ${units[power]}`;
|
|
9
|
+
}
|
package/dist/shared/git/git.js
CHANGED
|
@@ -8,7 +8,8 @@ export class GitClient {
|
|
|
8
8
|
const result = await this.processRunner.run({
|
|
9
9
|
command: "git",
|
|
10
10
|
args,
|
|
11
|
-
cwd
|
|
11
|
+
cwd,
|
|
12
|
+
...(args.includes("-z") ? { rawOutput: true } : {}),
|
|
12
13
|
});
|
|
13
14
|
if (result.exitCode !== 0) {
|
|
14
15
|
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare function isSubPath(root: string, candidate: string): boolean;
|
|
2
2
|
export declare function resolveExistingDirectoryPath(inputPath: string): Promise<string>;
|
|
3
|
+
export declare function resolvePathIdentity(inputPath: string): Promise<string>;
|
|
3
4
|
export declare function ensureDirectory(inputPath: string): Promise<string>;
|
|
4
5
|
export declare function safeSegment(value: string, fallback: string): string;
|
package/dist/shared/path/path.js
CHANGED
|
@@ -4,7 +4,9 @@ export function isSubPath(root, candidate) {
|
|
|
4
4
|
const normalizedRoot = path.resolve(root);
|
|
5
5
|
const normalizedCandidate = path.resolve(candidate);
|
|
6
6
|
const relative = path.relative(normalizedRoot, normalizedCandidate);
|
|
7
|
-
return relative === "" || (
|
|
7
|
+
return relative === "" || (relative !== ".."
|
|
8
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
9
|
+
&& !path.isAbsolute(relative));
|
|
8
10
|
}
|
|
9
11
|
export async function resolveExistingDirectoryPath(inputPath) {
|
|
10
12
|
const stat = await fs.stat(inputPath);
|
|
@@ -13,6 +15,24 @@ export async function resolveExistingDirectoryPath(inputPath) {
|
|
|
13
15
|
}
|
|
14
16
|
return await fs.realpath(inputPath);
|
|
15
17
|
}
|
|
18
|
+
export async function resolvePathIdentity(inputPath) {
|
|
19
|
+
let existingAncestor = path.resolve(inputPath);
|
|
20
|
+
const missingSegments = [];
|
|
21
|
+
while (true) {
|
|
22
|
+
try {
|
|
23
|
+
return path.join(await fs.realpath(existingAncestor), ...missingSegments);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== "ENOENT")
|
|
27
|
+
throw error;
|
|
28
|
+
const parent = path.dirname(existingAncestor);
|
|
29
|
+
if (parent === existingAncestor)
|
|
30
|
+
throw error;
|
|
31
|
+
missingSegments.unshift(path.basename(existingAncestor));
|
|
32
|
+
existingAncestor = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
16
36
|
export async function ensureDirectory(inputPath) {
|
|
17
37
|
await fs.mkdir(inputPath, { recursive: true });
|
|
18
38
|
return await resolveExistingDirectoryPath(inputPath);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface DotNetToolchainSelection {
|
|
2
|
+
readonly executablePath: string;
|
|
3
|
+
readonly rootPath: string;
|
|
4
|
+
readonly sdkVersion: string;
|
|
5
|
+
readonly requiredMajor: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DotNetToolchainOptions {
|
|
8
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
9
|
+
readonly platform?: NodeJS.Platform;
|
|
10
|
+
readonly homeDirectory?: string;
|
|
11
|
+
readonly fileExists?: (candidate: string) => Promise<boolean>;
|
|
12
|
+
readonly listSdks?: (candidate: string) => Promise<string>;
|
|
13
|
+
readonly resolveSdk?: (candidate: string, repositoryRoot: string) => Promise<string>;
|
|
14
|
+
/** Supplied roots that hold many repositories, such as the bridge clone folder. */
|
|
15
|
+
readonly cloneFolders?: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function configureDotNetToolchain(repoRoots: readonly string[], options?: DotNetToolchainOptions): Promise<DotNetToolchainSelection | null>;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const skippedDirectories = new Set([".git", "bin", "node_modules", "obj"]);
|
|
8
|
+
const maximumDiscoveryDepth = 32;
|
|
9
|
+
const maximumDiscoveryEntries = 20_000;
|
|
10
|
+
const maximumProjectFiles = 256;
|
|
11
|
+
const maximumInvocationContexts = 256;
|
|
12
|
+
export async function configureDotNetToolchain(repoRoots, options = {}) {
|
|
13
|
+
const environment = options.environment ?? process.env;
|
|
14
|
+
const platform = options.platform ?? process.platform;
|
|
15
|
+
const requirements = await discoverSdkRequirements(repoRoots, options.cloneFolders ?? []);
|
|
16
|
+
if (requirements.length === 0)
|
|
17
|
+
return null;
|
|
18
|
+
const requiredMajor = Math.max(...requirements.map((requirement) => requirement.minimumMajor));
|
|
19
|
+
const policyGroups = groupPolicyRequirements(requirements, platform);
|
|
20
|
+
const explicit = environmentValue(environment, "FORGE_DOTNET_PATH");
|
|
21
|
+
if (explicit && !path.isAbsolute(explicit)) {
|
|
22
|
+
throw new Error("dotnet_toolchain_invalid: FORGE_DOTNET_PATH must be an absolute executable path.");
|
|
23
|
+
}
|
|
24
|
+
const candidates = dotNetCandidates(environment, platform, options.homeDirectory ?? os.homedir(), requiredMajor);
|
|
25
|
+
const fileExists = options.fileExists ?? isFile;
|
|
26
|
+
const listSdks = options.listSdks ?? probeSdks;
|
|
27
|
+
const resolveSdk = options.resolveSdk ?? probeSelectedSdk;
|
|
28
|
+
const probed = [];
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
if (!await fileExists(candidate))
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
const output = await listSdks(candidate);
|
|
34
|
+
probed.push(`${candidate} (${sdkVersions(output).join(", ") || "no SDKs"})`);
|
|
35
|
+
// Project, solution, and repository-root commands can resolve different
|
|
36
|
+
// SDKs. Preserve all invocation contexts as well as nested project policies;
|
|
37
|
+
// only identical policy identities may share a probe.
|
|
38
|
+
const resolvedVersions = [];
|
|
39
|
+
for (const requirement of policyGroups) {
|
|
40
|
+
const resolved = (await resolveSdk(candidate, requirement.directory)).trim();
|
|
41
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(resolved)) {
|
|
42
|
+
throw new Error(`No valid SDK selected for ${requirement.directory}`);
|
|
43
|
+
}
|
|
44
|
+
if ((majorVersion(resolved) ?? 0) < requirement.minimumMajor) {
|
|
45
|
+
throw new Error(`SDK ${resolved} cannot satisfy .NET ${requirement.minimumMajor} in ${requirement.directory}`);
|
|
46
|
+
}
|
|
47
|
+
resolvedVersions.push(resolved);
|
|
48
|
+
}
|
|
49
|
+
// Detect a policy added, removed, or changed during discovery/probing.
|
|
50
|
+
// In particular, do not reuse a parent's probe after a nested override appears.
|
|
51
|
+
for (const requirement of requirements) {
|
|
52
|
+
const policy = await nearestSdkPolicy(requirement.directory);
|
|
53
|
+
if (policy?.path !== requirement.policy?.path || policy?.contents !== requirement.policy?.contents) {
|
|
54
|
+
throw new Error(`SDK policy changed during resolution for ${requirement.directory}; retry after repository changes finish`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const selectedVersion = resolvedVersions.sort((a, b) => a.localeCompare(b, "en", { numeric: true })).at(-1);
|
|
58
|
+
const rootPath = path.dirname(candidate);
|
|
59
|
+
prependPath(environment, rootPath, platform);
|
|
60
|
+
setEnvironmentValue(environment, "DOTNET_ROOT", rootPath);
|
|
61
|
+
if (platform === "win32")
|
|
62
|
+
setEnvironmentValue(environment, "DOTNET_ROOT_X64", rootPath);
|
|
63
|
+
setEnvironmentValue(environment, "FORGE_DOTNET_PATH", candidate);
|
|
64
|
+
return { executablePath: candidate, rootPath, sdkVersion: selectedVersion, requiredMajor: requiredMajor || majorVersion(selectedVersion) };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
probed.push(`${candidate} (${error instanceof Error ? error.message : String(error)})`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const detail = probed.length > 0 ? ` Probed: ${probed.join("; ")}.` : "";
|
|
71
|
+
throw new Error(`dotnet_toolchain_unavailable: repositories require ${requiredMajor ? `.NET SDK ${requiredMajor}.x` : "a .NET SDK"}, but no compatible dotnet executable was found.`
|
|
72
|
+
+ ` Set FORGE_DOTNET_PATH to the absolute executable path.${detail}`);
|
|
73
|
+
}
|
|
74
|
+
async function discoverSdkRequirements(repoRoots, cloneFolders) {
|
|
75
|
+
const { projects, invocationDirectories } = await findDotNetInputs(repoRoots, cloneFolders);
|
|
76
|
+
const requirements = [];
|
|
77
|
+
for (const project of projects) {
|
|
78
|
+
const source = await fs.readFile(project, "utf8");
|
|
79
|
+
const directory = path.dirname(project);
|
|
80
|
+
const policy = await nearestSdkPolicy(directory);
|
|
81
|
+
const majors = [policy?.major ?? 0];
|
|
82
|
+
for (const match of source.matchAll(/<TargetFrameworks?>\s*([^<]+)\s*<\/TargetFrameworks?>/giu)) {
|
|
83
|
+
for (const framework of match[1]?.split(";") ?? []) {
|
|
84
|
+
const target = /^net(\d+)\./iu.exec(framework.trim());
|
|
85
|
+
if (target?.[1])
|
|
86
|
+
majors.push(Number.parseInt(target[1], 10));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
requirements.push({ directory, policy, minimumMajor: Math.max(...majors) });
|
|
90
|
+
}
|
|
91
|
+
for (const directory of invocationDirectories) {
|
|
92
|
+
const policy = await nearestSdkPolicy(directory);
|
|
93
|
+
requirements.push({ directory, policy, minimumMajor: policy?.major ?? 0 });
|
|
94
|
+
}
|
|
95
|
+
return requirements;
|
|
96
|
+
}
|
|
97
|
+
async function nearestSdkPolicy(projectDirectory) {
|
|
98
|
+
for (let directory = projectDirectory;; directory = path.dirname(directory)) {
|
|
99
|
+
const policyPath = path.join(directory, "global.json");
|
|
100
|
+
try {
|
|
101
|
+
const contents = await fs.readFile(policyPath, "utf8");
|
|
102
|
+
let major = null;
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(contents);
|
|
105
|
+
major = typeof parsed.sdk?.version === "string" ? majorVersion(parsed.sdk.version) : null;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// The actual SDK resolver diagnoses malformed policies; never bypass one.
|
|
109
|
+
}
|
|
110
|
+
return { path: policyPath, contents, major };
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error.code !== "ENOENT")
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
if (path.dirname(directory) === directory)
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function groupPolicyRequirements(requirements, platform) {
|
|
121
|
+
const groups = new Map();
|
|
122
|
+
for (const requirement of requirements) {
|
|
123
|
+
const key = requirement.policy ? canonicalPath(requirement.policy.path, platform) : "no-global-json";
|
|
124
|
+
const previous = groups.get(key);
|
|
125
|
+
if (previous && previous.policy?.contents !== requirement.policy?.contents) {
|
|
126
|
+
throw new Error(`dotnet_toolchain_changed: SDK policy changed during discovery: ${requirement.policy?.path}`);
|
|
127
|
+
}
|
|
128
|
+
groups.set(key, previous
|
|
129
|
+
? { ...previous, minimumMajor: Math.max(previous.minimumMajor, requirement.minimumMajor) }
|
|
130
|
+
: requirement);
|
|
131
|
+
}
|
|
132
|
+
return [...groups.values()];
|
|
133
|
+
}
|
|
134
|
+
async function findDotNetInputs(repoRoots, cloneFolders) {
|
|
135
|
+
const projects = [];
|
|
136
|
+
const invocationDirectories = new Map();
|
|
137
|
+
const visitedDirectories = new Set();
|
|
138
|
+
const roots = [...new Set(repoRoots.map((candidate) => path.resolve(candidate)))];
|
|
139
|
+
const explicitRoots = new Set(roots.map((root) => canonicalPath(root, process.platform)));
|
|
140
|
+
const cloneFolderRoots = new Set(cloneFolders.map((folder) => canonicalPath(path.resolve(folder), process.platform)));
|
|
141
|
+
// The entry bound applies per repository. A clone folder is not one: each of
|
|
142
|
+
// its child directories is bounded as its own repository root.
|
|
143
|
+
let entryCount = 0;
|
|
144
|
+
async function visitRoot(root) {
|
|
145
|
+
entryCount = 0;
|
|
146
|
+
await visit(root, 0, root, new Set());
|
|
147
|
+
}
|
|
148
|
+
function addInvocationDirectory(directory, root) {
|
|
149
|
+
const identity = canonicalPath(directory, process.platform);
|
|
150
|
+
if (invocationDirectories.has(identity))
|
|
151
|
+
return;
|
|
152
|
+
if (invocationDirectories.size >= maximumInvocationContexts)
|
|
153
|
+
throw discoveryLimit("invocation contexts", maximumInvocationContexts, root);
|
|
154
|
+
invocationDirectories.set(identity, directory);
|
|
155
|
+
}
|
|
156
|
+
async function visit(directory, depth, root, ancestors) {
|
|
157
|
+
const directoryIdentity = canonicalPath(directory, process.platform);
|
|
158
|
+
if (visitedDirectories.has(directoryIdentity))
|
|
159
|
+
return;
|
|
160
|
+
if (depth > maximumDiscoveryDepth)
|
|
161
|
+
throw discoveryLimit("depth", maximumDiscoveryDepth, root);
|
|
162
|
+
const physicalDirectory = await fs.realpath(directory);
|
|
163
|
+
if (ancestors.has(physicalDirectory))
|
|
164
|
+
throw new Error(`dotnet_toolchain_discovery: directory cycle at ${directory}`);
|
|
165
|
+
const nextAncestors = new Set([...ancestors, physicalDirectory]);
|
|
166
|
+
const cloneFolder = depth === 0 && cloneFolderRoots.has(directoryIdentity);
|
|
167
|
+
visitedDirectories.add(directoryIdentity);
|
|
168
|
+
const entries = [];
|
|
169
|
+
for await (const entry of await fs.opendir(directory)) {
|
|
170
|
+
if (++entryCount > maximumDiscoveryEntries)
|
|
171
|
+
throw discoveryLimit("entries", maximumDiscoveryEntries, root);
|
|
172
|
+
entries.push(entry);
|
|
173
|
+
}
|
|
174
|
+
// Stable traversal also makes the representative policy probe deterministic.
|
|
175
|
+
entries.sort((a, b) => a.name.localeCompare(b.name, "en"));
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
// Hidden folders (.git, .vs, .forge-worktrees, ...) are tool state, never
|
|
178
|
+
// repository sources; an explicitly supplied hidden root is still visited.
|
|
179
|
+
if (skippedDirectories.has(entry.name) || entry.name.startsWith(".") && !entry.isFile())
|
|
180
|
+
continue;
|
|
181
|
+
const entryPath = path.join(directory, entry.name);
|
|
182
|
+
const linked = entry.isSymbolicLink() ? await fs.stat(entryPath) : null;
|
|
183
|
+
if (linked?.isDirectory()) {
|
|
184
|
+
if (cloneFolder || explicitRoots.has(canonicalPath(entryPath, process.platform))) {
|
|
185
|
+
await visitRoot(entryPath);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`dotnet_toolchain_discovery: symbolic/reparse directory ${entryPath} must be supplied as an explicit repository root, not traversed from ${root}`);
|
|
189
|
+
}
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
if (cloneFolder)
|
|
192
|
+
await visitRoot(entryPath);
|
|
193
|
+
else
|
|
194
|
+
await visit(entryPath, depth + 1, root, nextAncestors);
|
|
195
|
+
}
|
|
196
|
+
else if (entry.isFile() || linked?.isFile()) {
|
|
197
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
198
|
+
if (extension === ".csproj") {
|
|
199
|
+
if (projects.length >= maximumProjectFiles)
|
|
200
|
+
throw discoveryLimit("projects", maximumProjectFiles, root);
|
|
201
|
+
projects.push(entryPath);
|
|
202
|
+
}
|
|
203
|
+
// A solution's SDK resolver starts at its directory. A global.json also
|
|
204
|
+
// defines a CLI invocation context even when every child project overrides
|
|
205
|
+
// it (or the repository contains only SDK-based tools).
|
|
206
|
+
if ([".sln", ".slnx", ".slnf"].includes(extension)
|
|
207
|
+
|| canonicalPath(entry.name, process.platform) === "global.json") {
|
|
208
|
+
addInvocationDirectory(directory, root);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const root of roots) {
|
|
214
|
+
await visitRoot(root);
|
|
215
|
+
}
|
|
216
|
+
// Supplied roots are also supported command working directories. A parent's
|
|
217
|
+
// policy cannot be dropped merely because every project/solution below it has
|
|
218
|
+
// its own override. Ignore unrelated roots without any discovered .NET inputs.
|
|
219
|
+
const inputDirectories = [...projects.map((project) => path.dirname(project)), ...invocationDirectories.values()];
|
|
220
|
+
for (const root of roots) {
|
|
221
|
+
if (inputDirectories.some((directory) => isWithinDirectory(root, directory))) {
|
|
222
|
+
addInvocationDirectory(root, root);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { projects, invocationDirectories: [...invocationDirectories.values()] };
|
|
226
|
+
}
|
|
227
|
+
function isWithinDirectory(root, directory) {
|
|
228
|
+
const relative = path.relative(root, directory);
|
|
229
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
230
|
+
}
|
|
231
|
+
function discoveryLimit(bound, maximum, root) {
|
|
232
|
+
return new Error(`dotnet_toolchain_discovery: ${bound} limit ${maximum} exceeded under ${root}; configure narrower explicit repository roots`);
|
|
233
|
+
}
|
|
234
|
+
function dotNetCandidates(environment, platform, homeDirectory, requiredMajor) {
|
|
235
|
+
const executable = platform === "win32" ? "dotnet.exe" : "dotnet";
|
|
236
|
+
const explicit = environmentValue(environment, "FORGE_DOTNET_PATH");
|
|
237
|
+
if (explicit)
|
|
238
|
+
return [explicit];
|
|
239
|
+
const dotNetRoot = environmentValue(environment, "DOTNET_ROOT");
|
|
240
|
+
const pathValue = environmentValue(environment, "PATH") ?? "";
|
|
241
|
+
const programFiles = environmentValue(environment, "ProgramFiles");
|
|
242
|
+
const candidates = [
|
|
243
|
+
dotNetRoot ? path.join(dotNetRoot, executable) : null,
|
|
244
|
+
path.join(homeDirectory, ".dotnet", executable),
|
|
245
|
+
requiredMajor > 0 ? path.join(homeDirectory, `.dotnet${requiredMajor}`, executable) : null,
|
|
246
|
+
...(platform === "win32" && programFiles ? [path.join(programFiles, "dotnet", executable)] : []),
|
|
247
|
+
...pathValue.split(path.delimiter).filter(Boolean).map((directory) => path.join(directory, executable)),
|
|
248
|
+
].filter((candidate) => Boolean(candidate));
|
|
249
|
+
return [...new Map(candidates.map((candidate) => [canonicalPath(candidate, platform), path.resolve(candidate)])).values()];
|
|
250
|
+
}
|
|
251
|
+
async function probeSdks(candidate) {
|
|
252
|
+
const { stdout } = await execFileAsync(candidate, ["--list-sdks"], {
|
|
253
|
+
encoding: "utf8",
|
|
254
|
+
timeout: 10_000,
|
|
255
|
+
windowsHide: true,
|
|
256
|
+
});
|
|
257
|
+
return stdout;
|
|
258
|
+
}
|
|
259
|
+
async function probeSelectedSdk(candidate, repositoryRoot) {
|
|
260
|
+
const { stdout } = await execFileAsync(candidate, ["--version"], {
|
|
261
|
+
cwd: repositoryRoot,
|
|
262
|
+
encoding: "utf8",
|
|
263
|
+
timeout: 10_000,
|
|
264
|
+
windowsHide: true,
|
|
265
|
+
});
|
|
266
|
+
return stdout;
|
|
267
|
+
}
|
|
268
|
+
function sdkVersions(output) {
|
|
269
|
+
return output.split(/\r?\n/gu)
|
|
270
|
+
.map((line) => /^(\d+\.\d+\.\d+(?:[-+][^\s]+)?)/u.exec(line.trim())?.[1] ?? null)
|
|
271
|
+
.filter((version) => version !== null);
|
|
272
|
+
}
|
|
273
|
+
function majorVersion(version) {
|
|
274
|
+
const match = /^(\d+)\./u.exec(version.trim());
|
|
275
|
+
return match?.[1] ? Number.parseInt(match[1], 10) : null;
|
|
276
|
+
}
|
|
277
|
+
async function isFile(candidate) {
|
|
278
|
+
try {
|
|
279
|
+
return (await fs.stat(candidate)).isFile();
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function prependPath(environment, directory, platform) {
|
|
286
|
+
const current = environmentValue(environment, "PATH") ?? "";
|
|
287
|
+
const normalizedDirectory = canonicalPath(directory, platform);
|
|
288
|
+
const remaining = current.split(path.delimiter)
|
|
289
|
+
.filter(Boolean)
|
|
290
|
+
.filter((entry) => canonicalPath(entry, platform) !== normalizedDirectory);
|
|
291
|
+
setEnvironmentValue(environment, "PATH", [directory, ...remaining].join(path.delimiter));
|
|
292
|
+
}
|
|
293
|
+
function environmentValue(environment, name) {
|
|
294
|
+
const key = Object.keys(environment).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
295
|
+
return key ? environment[key] : undefined;
|
|
296
|
+
}
|
|
297
|
+
function setEnvironmentValue(environment, name, value) {
|
|
298
|
+
const keys = Object.keys(environment).filter((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
299
|
+
const target = keys[0] ?? name;
|
|
300
|
+
for (const duplicate of keys.slice(1))
|
|
301
|
+
delete environment[duplicate];
|
|
302
|
+
environment[target] = value;
|
|
303
|
+
}
|
|
304
|
+
function canonicalPath(candidate, platform) {
|
|
305
|
+
const normalized = path.normalize(candidate);
|
|
306
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
307
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HostProcessSupervisorMetadata } from "./host-process-supervisor.js";
|
|
2
|
+
export type HostProcessRecoveryOutcome = "absent" | "terminated" | "pid_reused" | "unverifiable";
|
|
3
|
+
export interface HostProcessRecovery {
|
|
4
|
+
recover(metadata: HostProcessSupervisorMetadata): Promise<HostProcessRecoveryOutcome>;
|
|
5
|
+
}
|
|
6
|
+
/** Exact, birth-marker-fenced recovery for a process tree left by an old bridge. */
|
|
7
|
+
export declare class ExactHostProcessRecovery implements HostProcessRecovery {
|
|
8
|
+
private readonly platform;
|
|
9
|
+
private readonly graceMs;
|
|
10
|
+
constructor(platform?: NodeJS.Platform, graceMs?: number);
|
|
11
|
+
recover(metadata: HostProcessSupervisorMetadata): Promise<HostProcessRecoveryOutcome>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { captureHostProcessIdentity } from "./host-process-supervisor.js";
|
|
2
|
+
/** Exact, birth-marker-fenced recovery for a process tree left by an old bridge. */
|
|
3
|
+
export class ExactHostProcessRecovery {
|
|
4
|
+
platform;
|
|
5
|
+
graceMs;
|
|
6
|
+
constructor(platform = process.platform, graceMs = 2_000) {
|
|
7
|
+
this.platform = platform;
|
|
8
|
+
this.graceMs = graceMs;
|
|
9
|
+
}
|
|
10
|
+
async recover(metadata) {
|
|
11
|
+
if (!processExists(metadata.rootPid))
|
|
12
|
+
return "absent";
|
|
13
|
+
if (!metadata.rootIdentity)
|
|
14
|
+
return "unverifiable";
|
|
15
|
+
const currentIdentity = captureHostProcessIdentity(metadata.rootPid, this.platform);
|
|
16
|
+
if (!currentIdentity)
|
|
17
|
+
return processExists(metadata.rootPid) ? "unverifiable" : "absent";
|
|
18
|
+
if (currentIdentity !== metadata.rootIdentity)
|
|
19
|
+
return "pid_reused";
|
|
20
|
+
if (this.platform === "win32") {
|
|
21
|
+
signalExactProcess(metadata.rootPid, "SIGTERM");
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
signalExactProcessGroup(metadata.rootPid, "SIGTERM");
|
|
25
|
+
}
|
|
26
|
+
await waitForExit(metadata.rootPid, this.graceMs);
|
|
27
|
+
if (processExists(metadata.rootPid)) {
|
|
28
|
+
if (captureHostProcessIdentity(metadata.rootPid, this.platform) !== metadata.rootIdentity) {
|
|
29
|
+
return "pid_reused";
|
|
30
|
+
}
|
|
31
|
+
if (this.platform === "win32") {
|
|
32
|
+
signalExactProcess(metadata.rootPid, "SIGKILL");
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
signalExactProcessGroup(metadata.rootPid, "SIGKILL");
|
|
36
|
+
}
|
|
37
|
+
await waitForExit(metadata.rootPid, this.graceMs);
|
|
38
|
+
}
|
|
39
|
+
if (processExists(metadata.rootPid)
|
|
40
|
+
&& captureHostProcessIdentity(metadata.rootPid, this.platform) === metadata.rootIdentity) {
|
|
41
|
+
throw new Error("host_process_recovery_degraded: exact supervised process tree did not stop.");
|
|
42
|
+
}
|
|
43
|
+
return "terminated";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function processExists(pid) {
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return error.code === "EPERM";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function signalExactProcess(pid, signal) {
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, signal);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code !== "ESRCH")
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function signalExactProcessGroup(pid, signal) {
|
|
65
|
+
try {
|
|
66
|
+
process.kill(-pid, signal);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code !== "ESRCH")
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function waitForExit(pid, timeoutMs) {
|
|
74
|
+
const deadline = Date.now() + timeoutMs;
|
|
75
|
+
while (Date.now() < deadline && processExists(pid)) {
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
export type HostProcessSupervisorStrategy = "windows_job_object" | "unix_process_group";
|
|
3
|
+
export interface HostProcessSupervisionRequest {
|
|
4
|
+
readonly command: string;
|
|
5
|
+
readonly args: readonly string[];
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
8
|
+
}
|
|
9
|
+
export interface HostProcessSupervisedLaunch {
|
|
10
|
+
readonly command: string;
|
|
11
|
+
readonly args: readonly string[];
|
|
12
|
+
readonly detached: boolean;
|
|
13
|
+
readonly supervisorId: string;
|
|
14
|
+
readonly strategy: HostProcessSupervisorStrategy;
|
|
15
|
+
readonly metadataPath: string;
|
|
16
|
+
readonly memoryTelemetryPath?: string;
|
|
17
|
+
readonly payloadPath?: string;
|
|
18
|
+
readonly payloadSha256?: string;
|
|
19
|
+
readonly readyPath?: string;
|
|
20
|
+
readonly startGrantPath?: string;
|
|
21
|
+
readonly commandDigest: string;
|
|
22
|
+
}
|
|
23
|
+
export interface HostProcessProviderReady {
|
|
24
|
+
readonly schemaVersion: 1;
|
|
25
|
+
readonly supervisorId: string;
|
|
26
|
+
readonly rootPid?: number;
|
|
27
|
+
readonly rootIdentity?: string | null;
|
|
28
|
+
readonly providerPid: number;
|
|
29
|
+
readonly providerCreatedAt: string;
|
|
30
|
+
readonly providerIdentity?: string | null;
|
|
31
|
+
readonly startAttempt: number;
|
|
32
|
+
}
|
|
33
|
+
export interface HostProcessSupervisorMetadata {
|
|
34
|
+
readonly schemaVersion: 1;
|
|
35
|
+
readonly supervisorId: string;
|
|
36
|
+
readonly strategy: HostProcessSupervisorStrategy;
|
|
37
|
+
readonly rootPid: number;
|
|
38
|
+
readonly rootCreatedAt: string;
|
|
39
|
+
readonly rootIdentity?: string | null;
|
|
40
|
+
readonly commandDigest: string;
|
|
41
|
+
readonly worktreePath: string;
|
|
42
|
+
readonly runtimeLeaseId: string;
|
|
43
|
+
readonly dispatchAttemptId: string;
|
|
44
|
+
readonly fencingToken: string;
|
|
45
|
+
readonly bridgePid: number;
|
|
46
|
+
readonly providerPid?: number;
|
|
47
|
+
readonly providerCreatedAt?: string;
|
|
48
|
+
readonly providerIdentity?: string | null;
|
|
49
|
+
readonly providerStartAttempt?: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function resolveHostProcessSupervisedLaunch(request: HostProcessSupervisionRequest, resolvedCommand: string, resolvedArgs: readonly string[], platform?: NodeJS.Platform): HostProcessSupervisedLaunch | null;
|
|
52
|
+
export declare function persistHostProcessSupervisorMetadata(launch: HostProcessSupervisedLaunch, request: HostProcessSupervisionRequest, rootPid: number, rootCreatedAt: string, providerReady?: HostProcessProviderReady): HostProcessSupervisorMetadata;
|
|
53
|
+
/**
|
|
54
|
+
* Captures an OS-issued process birth marker. Recovery compares this marker
|
|
55
|
+
* before signalling a persisted PID so PID reuse can never redirect cleanup
|
|
56
|
+
* at an unrelated process.
|
|
57
|
+
*/
|
|
58
|
+
export declare function captureHostProcessIdentity(pid: number, platform?: NodeJS.Platform): string | null;
|
|
59
|
+
export declare function terminateHostProcessTree(child: ChildProcess, launch: HostProcessSupervisedLaunch | null, graceMs?: number): Promise<void>;
|