@loucompanion/forge-bridge 0.1.3-dev.d1ca67076aa9 → 0.1.3-dev.e7b62d8fdd33
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 +127 -0
- package/dist/bridge/bridge.base.d.ts +8 -1
- package/dist/bridge/bridge.service.d.ts +93 -3
- package/dist/bridge/bridge.service.js +1358 -47
- package/dist/bridge/bridge.types.d.ts +202 -1
- 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.js +47 -1
- package/dist/cli/cli.service.d.ts +45 -2
- package/dist/cli/cli.service.js +378 -26
- 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 +340 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +105 -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 +386 -0
- package/dist/runtime/docker-engine-probe.d.ts +83 -0
- package/dist/runtime/docker-engine-probe.js +300 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +46 -0
- package/dist/runtime/host-listener-inspector.js +349 -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 +47 -0
- package/dist/runtime/host-runtime-contract.js +700 -0
- package/dist/runtime/host-runtime-lease.d.ts +197 -0
- package/dist/runtime/host-runtime-lease.js +2203 -0
- package/dist/runtime/host-runtime-promotion.d.ts +23 -0
- package/dist/runtime/host-runtime-promotion.js +159 -0
- package/dist/runtime/memory-capacity.d.ts +68 -0
- package/dist/runtime/memory-capacity.js +121 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +284 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -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 +5 -0
- package/dist/sandbox/repository-lock.js +121 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -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 +72 -0
- package/dist/session/managed-attempt-tools.d.ts +21 -0
- package/dist/session/managed-attempt-tools.js +912 -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 +335 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +256 -26
- package/dist/session/phase-completion.d.ts +12 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +51 -10
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +24 -8
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +189 -8
- package/dist/session/phase-completion.schemas.js +117 -4
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +153 -22
- package/dist/session/pre-publication-validation.d.ts +46 -0
- package/dist/session/pre-publication-validation.js +208 -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 +3 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +68 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
- package/dist/session/provider-cli/provider-cli.process.js +474 -23
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +16 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +224 -24
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -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 +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +26 -3
- package/dist/session/provider-cli/provider-cli.service.js +650 -108
- package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +91 -16
- 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 +38 -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 +549 -58
- package/dist/session/session.types.d.ts +124 -1
- package/dist/session/session.types.js +231 -4
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- 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 +14 -0
- package/dist/shared/process/dotnet-toolchain.js +179 -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 +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/shared/process/windows-launch.d.ts +1 -1
- package/dist/shared/process/windows-launch.js +6 -3
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +16 -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 +43 -4
- package/dist/worktree/dependency-warmer.d.ts +39 -1
- package/dist/worktree/dependency-warmer.js +351 -8
- 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/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 +13 -2
- package/dist/worktree/worktree.service.js +238 -47
- package/dist/worktree/worktree.types.d.ts +5 -1
- package/package.json +8 -4
- package/runtime/windows-job-supervisor.ps1 +444 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import { type DotNetToolchainSelection } from "../shared/process/dotnet-toolchain.js";
|
|
1
2
|
import type { BridgeService } from "../bridge/bridge.base.js";
|
|
2
3
|
import type { BridgeServiceConfig } from "../bridge/bridge.types.js";
|
|
4
|
+
import type { CleanupMode } from "../cleanup/cleanup.types.js";
|
|
5
|
+
import type { CleanupService } from "../cleanup/cleanup.base.js";
|
|
3
6
|
import type { SessionService } from "../session/session.base.js";
|
|
4
7
|
import type { CliIo, CliService } from "./cli.base.js";
|
|
8
|
+
type DotNetToolchainConfigurator = (repoRoots: readonly string[]) => Promise<DotNetToolchainSelection | null>;
|
|
5
9
|
export declare class ForgeBridgeCliService implements CliService {
|
|
6
10
|
private readonly sessions;
|
|
7
11
|
private readonly bridgeFactory;
|
|
8
12
|
private readonly pairRunner;
|
|
9
|
-
|
|
13
|
+
private readonly cleanupFactory;
|
|
14
|
+
private readonly dotNetToolchainConfigurator;
|
|
15
|
+
constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService, pairRunner?: PairRunnerClient, cleanupFactory?: CleanupServiceFactory, dotNetToolchainConfigurator?: DotNetToolchainConfigurator);
|
|
10
16
|
run(argv: readonly string[], io?: CliIo): Promise<number>;
|
|
11
17
|
}
|
|
12
18
|
export declare function parseArgs(argv: readonly string[]): {
|
|
@@ -34,6 +40,14 @@ export declare function parseArgs(argv: readonly string[]): {
|
|
|
34
40
|
readonly ok: true;
|
|
35
41
|
readonly command: "remove-repo";
|
|
36
42
|
readonly label: string;
|
|
43
|
+
} | {
|
|
44
|
+
readonly ok: true;
|
|
45
|
+
readonly command: "server-show";
|
|
46
|
+
readonly config: BridgeServiceConfig;
|
|
47
|
+
} | {
|
|
48
|
+
readonly ok: true;
|
|
49
|
+
readonly command: "server-set";
|
|
50
|
+
readonly serverUrl: string;
|
|
37
51
|
} | {
|
|
38
52
|
readonly ok: true;
|
|
39
53
|
readonly command: "status";
|
|
@@ -45,6 +59,35 @@ export declare function parseArgs(argv: readonly string[]): {
|
|
|
45
59
|
readonly config: BridgeServiceConfig;
|
|
46
60
|
readonly mode: CleanupMode;
|
|
47
61
|
readonly force: boolean;
|
|
62
|
+
} | {
|
|
63
|
+
readonly ok: true;
|
|
64
|
+
readonly command: "sandbox-init";
|
|
65
|
+
readonly repoPath: string;
|
|
66
|
+
} | {
|
|
67
|
+
readonly ok: true;
|
|
68
|
+
readonly command: "sandbox-doctor";
|
|
69
|
+
readonly repoPath: string;
|
|
70
|
+
readonly requirePinnedBase: boolean;
|
|
71
|
+
} | {
|
|
72
|
+
readonly ok: true;
|
|
73
|
+
readonly command: "sandbox-smoke";
|
|
74
|
+
readonly repoPath: string;
|
|
75
|
+
readonly reportPath?: string;
|
|
76
|
+
readonly requirePinnedBase: boolean;
|
|
77
|
+
readonly probeLoopbackPort: boolean;
|
|
78
|
+
} | {
|
|
79
|
+
readonly ok: true;
|
|
80
|
+
readonly command: "sandbox-reap";
|
|
81
|
+
} | {
|
|
82
|
+
readonly ok: true;
|
|
83
|
+
readonly command: "sandbox-on";
|
|
84
|
+
readonly allowOpenPublic: boolean;
|
|
85
|
+
} | {
|
|
86
|
+
readonly ok: true;
|
|
87
|
+
readonly command: "sandbox-off";
|
|
88
|
+
} | {
|
|
89
|
+
readonly ok: true;
|
|
90
|
+
readonly command: "sandbox-status";
|
|
48
91
|
} | {
|
|
49
92
|
readonly ok: false;
|
|
50
93
|
readonly error: string;
|
|
@@ -66,5 +109,5 @@ interface PairRunnerClaim {
|
|
|
66
109
|
type PairRunnerClient = (args: PairArgs) => Promise<PairRunnerClaim>;
|
|
67
110
|
export declare function parseCodexModelCatalog(raw: string): string[];
|
|
68
111
|
export declare function parseCursorModelList(raw: string): string[];
|
|
69
|
-
type
|
|
112
|
+
type CleanupServiceFactory = (stateRoot: string) => CleanupService;
|
|
70
113
|
export {};
|
package/dist/cli/cli.service.js
CHANGED
|
@@ -6,11 +6,19 @@ import { createInterface } from "node:readline/promises";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { resolveCursorAgentDirectLaunch } from "../shared/cursor-agent/cursor-agent.launch.js";
|
|
8
8
|
import { buildWindowsCmdLaunch } from "../shared/process/windows-launch.js";
|
|
9
|
-
import {
|
|
9
|
+
import { configureDotNetToolchain } from "../shared/process/dotnet-toolchain.js";
|
|
10
|
+
import { supportsDraftImageAttachmentsFor } from "../shared/provider-capabilities.js";
|
|
11
|
+
import { redactText } from "../shared/redaction/redaction.js";
|
|
10
12
|
import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
|
|
11
13
|
import { forgeBridgeVersion } from "../version.js";
|
|
12
|
-
import {
|
|
14
|
+
import { ManagedWorktreeCleanupService } from "../cleanup/cleanup.service.js";
|
|
15
|
+
import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, setSandboxPolicy, setServerUrl, writeLocalConfig } from "./local-config.js";
|
|
13
16
|
import { BridgeSessionService } from "../session/session.service.js";
|
|
17
|
+
import { redactRunnerDispatchEvent, redactRunOnceResultPacket } from "../session/session-redaction.js";
|
|
18
|
+
import { validateDockerfilePolicy, validateSandboxContract } from "../sandbox/contract.js";
|
|
19
|
+
import { DockerPreflight } from "../sandbox/preflight.js";
|
|
20
|
+
import { DockerSandboxRuntime } from "../sandbox/runtime.js";
|
|
21
|
+
import { DockerSandboxSmoke } from "../sandbox/smoke.js";
|
|
14
22
|
import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
|
|
15
23
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
16
24
|
import { commandHelpText, helpText } from "./cli.help.js";
|
|
@@ -20,10 +28,14 @@ export class ForgeBridgeCliService {
|
|
|
20
28
|
sessions;
|
|
21
29
|
bridgeFactory;
|
|
22
30
|
pairRunner;
|
|
23
|
-
|
|
31
|
+
cleanupFactory;
|
|
32
|
+
dotNetToolchainConfigurator;
|
|
33
|
+
constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner, cleanupFactory = stateRoot => new ManagedWorktreeCleanupService(stateRoot), dotNetToolchainConfigurator = configureDotNetToolchain) {
|
|
24
34
|
this.sessions = sessions;
|
|
25
35
|
this.bridgeFactory = bridgeFactory;
|
|
26
36
|
this.pairRunner = pairRunner;
|
|
37
|
+
this.cleanupFactory = cleanupFactory;
|
|
38
|
+
this.dotNetToolchainConfigurator = dotNetToolchainConfigurator;
|
|
27
39
|
}
|
|
28
40
|
async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
29
41
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
@@ -58,11 +70,29 @@ export class ForgeBridgeCliService {
|
|
|
58
70
|
io.stderr.write(`forge-bridge connect requires ${missing[0]}.\n`);
|
|
59
71
|
return 2;
|
|
60
72
|
}
|
|
73
|
+
let dotNetToolchain;
|
|
74
|
+
try {
|
|
75
|
+
dotNetToolchain = await this.dotNetToolchainConfigurator([
|
|
76
|
+
...config.repoMappings.map((mapping) => mapping.localPath?.trim()),
|
|
77
|
+
config.defaultRepoRoot.trim(),
|
|
78
|
+
].filter((repoRoot) => Boolean(repoRoot)));
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
if (dotNetToolchain) {
|
|
85
|
+
io.stdout.write(`Using .NET SDK ${dotNetToolchain.sdkVersion}: ${dotNetToolchain.executablePath}\n`);
|
|
86
|
+
}
|
|
61
87
|
const controller = new AbortController();
|
|
62
88
|
const stop = () => controller.abort("signal received");
|
|
63
89
|
process.once("SIGINT", stop);
|
|
64
90
|
process.once("SIGTERM", stop);
|
|
65
91
|
try {
|
|
92
|
+
if (config.cleanupWorktrees && config.repoMappings.some(mapping => mapping.localPath?.trim())) {
|
|
93
|
+
io.stdout.write("Cleaning stale managed worktrees before accepting dispatches.\n");
|
|
94
|
+
await cleanupRepos(config, "stale", true, this.cleanupFactory(config.sandboxStateRoot), io);
|
|
95
|
+
}
|
|
66
96
|
io.stdout.write(connectStartupText(config));
|
|
67
97
|
await this.bridgeFactory(config).run(controller.signal);
|
|
68
98
|
return 0;
|
|
@@ -88,6 +118,7 @@ export class ForgeBridgeCliService {
|
|
|
88
118
|
if (args.command === "pair") {
|
|
89
119
|
try {
|
|
90
120
|
const current = readLocalConfigSync();
|
|
121
|
+
io.stdout.write(`Pairing with server: ${args.serverUrl}\n`);
|
|
91
122
|
const code = await resolvePairingCode(args.code);
|
|
92
123
|
const repoRoot = await resolvePairRepoRoot(args.repoRoot, current.defaultRepoRoot);
|
|
93
124
|
await fs.mkdir(repoRoot, { recursive: true });
|
|
@@ -117,12 +148,66 @@ export class ForgeBridgeCliService {
|
|
|
117
148
|
return 2;
|
|
118
149
|
}
|
|
119
150
|
}
|
|
151
|
+
if (args.command === "server-show") {
|
|
152
|
+
const saved = readLocalConfigSync().serverUrl ?? "not configured";
|
|
153
|
+
io.stdout.write(`Saved server: ${saved}\n`);
|
|
154
|
+
if (process.env.FORGE_URL?.trim()) {
|
|
155
|
+
io.stdout.write(`Effective server: ${process.env.FORGE_URL.trim()} (FORGE_URL override)\n`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
io.stdout.write(`Effective server: ${args.config.serverUrl || "not configured"}\n`);
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
if (args.command === "server-set") {
|
|
163
|
+
try {
|
|
164
|
+
const config = await setServerUrl(args.serverUrl);
|
|
165
|
+
io.stdout.write(`Saved Forge server: ${config.serverUrl}\n`);
|
|
166
|
+
io.stdout.write("Existing runner credentials were retained. Re-run `forge-bridge pair --server <url>` if this is a different Forge deployment.\n");
|
|
167
|
+
if (process.env.FORGE_URL?.trim()) {
|
|
168
|
+
io.stdout.write(`Warning: FORGE_URL is set to ${process.env.FORGE_URL.trim()} and overrides the saved server.\n`);
|
|
169
|
+
}
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
174
|
+
return 2;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
120
177
|
if (args.command === "status") {
|
|
121
178
|
await writeStatus(args.config, args.checkUpdates, io);
|
|
122
179
|
return 0;
|
|
123
180
|
}
|
|
124
181
|
if (args.command === "cleanup") {
|
|
125
|
-
return await cleanupRepos(args.config, args.mode, args.force, io);
|
|
182
|
+
return await cleanupRepos(args.config, args.mode, args.force, this.cleanupFactory(args.config.sandboxStateRoot), io);
|
|
183
|
+
}
|
|
184
|
+
if (args.command === "sandbox-init") {
|
|
185
|
+
return await initializeSandbox(args.repoPath, io);
|
|
186
|
+
}
|
|
187
|
+
if (args.command === "sandbox-doctor") {
|
|
188
|
+
return await diagnoseSandbox(args.repoPath, args.requirePinnedBase, io);
|
|
189
|
+
}
|
|
190
|
+
if (args.command === "sandbox-smoke") {
|
|
191
|
+
return await smokeSandbox(args.repoPath, args.reportPath, args.requirePinnedBase, args.probeLoopbackPort, io);
|
|
192
|
+
}
|
|
193
|
+
if (args.command === "sandbox-reap") {
|
|
194
|
+
return await reapSandboxes(io);
|
|
195
|
+
}
|
|
196
|
+
if (args.command === "sandbox-on") {
|
|
197
|
+
const saved = await setSandboxPolicy(true, args.allowOpenPublic);
|
|
198
|
+
io.stdout.write("Saved local Docker sandbox policy. Restart the bridge to apply it.\n");
|
|
199
|
+
writeSandboxPolicyStatus(saved, io);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
if (args.command === "sandbox-off") {
|
|
203
|
+
const saved = await setSandboxPolicy(false, false);
|
|
204
|
+
io.stdout.write("Disabled local Docker sandbox placement. Restart the bridge to apply it.\n");
|
|
205
|
+
writeSandboxPolicyStatus(saved, io);
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
if (args.command === "sandbox-status") {
|
|
209
|
+
writeSandboxPolicyStatus(readLocalConfigSync(), io);
|
|
210
|
+
return 0;
|
|
126
211
|
}
|
|
127
212
|
if (args.command === "mcp") {
|
|
128
213
|
const parsed = parseCompletionMcpArgs(args.argv);
|
|
@@ -142,12 +227,12 @@ export class ForgeBridgeCliService {
|
|
|
142
227
|
emit: async (event) => {
|
|
143
228
|
const line = {
|
|
144
229
|
type: "runner_event",
|
|
145
|
-
event:
|
|
230
|
+
event: redactRunnerDispatchEvent(event, redactionSecrets)
|
|
146
231
|
};
|
|
147
232
|
io.stdout.write(`${JSON.stringify(line)}\n`);
|
|
148
233
|
}
|
|
149
234
|
});
|
|
150
|
-
await writeJsonAtomic(args.resultPath,
|
|
235
|
+
await writeJsonAtomic(args.resultPath, redactRunOnceResultPacket(result, redactionSecrets));
|
|
151
236
|
return result.exitCode;
|
|
152
237
|
}
|
|
153
238
|
catch (error) {
|
|
@@ -233,6 +318,15 @@ export function parseArgs(argv) {
|
|
|
233
318
|
}
|
|
234
319
|
return { ok: true, command: "remove-repo", label: argv[1] };
|
|
235
320
|
}
|
|
321
|
+
if (argv[0] === "server") {
|
|
322
|
+
if (argv.length === 1 || (argv[1] === "show" && argv.length === 2)) {
|
|
323
|
+
return { ok: true, command: "server-show", config: configuredBridgeServiceConfig() };
|
|
324
|
+
}
|
|
325
|
+
if (argv[1] === "set" && argv[2] && argv.length === 3) {
|
|
326
|
+
return { ok: true, command: "server-set", serverUrl: argv[2] };
|
|
327
|
+
}
|
|
328
|
+
return { ok: false, error: "Usage: forge-bridge server <show|set <url>>" };
|
|
329
|
+
}
|
|
236
330
|
if (argv[0] === "status") {
|
|
237
331
|
let checkUpdates = false;
|
|
238
332
|
for (let index = 1; index < argv.length; index += 1) {
|
|
@@ -252,6 +346,9 @@ export function parseArgs(argv) {
|
|
|
252
346
|
if (argv[0] === "mcp") {
|
|
253
347
|
return { ok: true, command: "mcp", argv };
|
|
254
348
|
}
|
|
349
|
+
if (argv[0] === "sandbox") {
|
|
350
|
+
return parseSandboxArgs(argv);
|
|
351
|
+
}
|
|
255
352
|
if (argv[0] !== "run-once") {
|
|
256
353
|
return {
|
|
257
354
|
ok: false,
|
|
@@ -282,6 +379,230 @@ export function parseArgs(argv) {
|
|
|
282
379
|
resultPath: path.resolve(resultPath)
|
|
283
380
|
};
|
|
284
381
|
}
|
|
382
|
+
function parseSandboxArgs(argv) {
|
|
383
|
+
const action = argv[1];
|
|
384
|
+
if (action === "on") {
|
|
385
|
+
let allowOpenPublic = false;
|
|
386
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
387
|
+
if (argv[index] === "--allow-open-public") {
|
|
388
|
+
allowOpenPublic = true;
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
return { ok: false, error: `Unknown sandbox argument: ${argv[index]}` };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { ok: true, command: "sandbox-on", allowOpenPublic };
|
|
395
|
+
}
|
|
396
|
+
if (action === "off" && argv.length === 2)
|
|
397
|
+
return { ok: true, command: "sandbox-off" };
|
|
398
|
+
if (action === "status" && argv.length === 2)
|
|
399
|
+
return { ok: true, command: "sandbox-status" };
|
|
400
|
+
let repoPath = process.cwd();
|
|
401
|
+
let reportPath;
|
|
402
|
+
let requirePinnedBase = false;
|
|
403
|
+
let probeLoopbackPort = false;
|
|
404
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
405
|
+
const arg = argv[index];
|
|
406
|
+
if (arg === "--repo") {
|
|
407
|
+
repoPath = argv[++index] ?? "";
|
|
408
|
+
if (!repoPath)
|
|
409
|
+
return { ok: false, error: "--repo requires a path." };
|
|
410
|
+
}
|
|
411
|
+
else if (arg === "--report" && action === "smoke") {
|
|
412
|
+
reportPath = argv[++index];
|
|
413
|
+
if (!reportPath)
|
|
414
|
+
return { ok: false, error: "--report requires a path." };
|
|
415
|
+
}
|
|
416
|
+
else if (arg === "--require-pinned-base" && (action === "doctor" || action === "smoke")) {
|
|
417
|
+
requirePinnedBase = true;
|
|
418
|
+
}
|
|
419
|
+
else if (arg === "--probe-loopback-port" && action === "smoke") {
|
|
420
|
+
probeLoopbackPort = true;
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
return { ok: false, error: `Unknown sandbox argument: ${arg}` };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (action === "init")
|
|
427
|
+
return { ok: true, command: "sandbox-init", repoPath: path.resolve(repoPath) };
|
|
428
|
+
if (action === "doctor")
|
|
429
|
+
return { ok: true, command: "sandbox-doctor", repoPath: path.resolve(repoPath), requirePinnedBase };
|
|
430
|
+
if (action === "smoke")
|
|
431
|
+
return {
|
|
432
|
+
ok: true,
|
|
433
|
+
command: "sandbox-smoke",
|
|
434
|
+
repoPath: path.resolve(repoPath),
|
|
435
|
+
reportPath: reportPath ? path.resolve(reportPath) : undefined,
|
|
436
|
+
requirePinnedBase,
|
|
437
|
+
probeLoopbackPort,
|
|
438
|
+
};
|
|
439
|
+
if (action === "reap" && argv.length === 2)
|
|
440
|
+
return { ok: true, command: "sandbox-reap" };
|
|
441
|
+
return { ok: false, error: "Usage: forge-bridge sandbox <on|off|status|init|doctor|smoke|reap> [--allow-open-public] [--repo <path>] [--report <path>] [--require-pinned-base] [--probe-loopback-port]" };
|
|
442
|
+
}
|
|
443
|
+
const sandboxContractTemplate = `schemaVersion: 1
|
|
444
|
+
workingDirectory: /workspace
|
|
445
|
+
commands:
|
|
446
|
+
install: npm ci
|
|
447
|
+
test: npm test
|
|
448
|
+
caches:
|
|
449
|
+
- name: npm
|
|
450
|
+
mode: readonly
|
|
451
|
+
services: []
|
|
452
|
+
environment: []
|
|
453
|
+
artifacts: []
|
|
454
|
+
logs: []
|
|
455
|
+
browser: false
|
|
456
|
+
`;
|
|
457
|
+
const sandboxDockerfileTemplate = `# Pin this base by digest before enabling sandbox_required.
|
|
458
|
+
FROM node:22.17.0-bookworm-slim
|
|
459
|
+
WORKDIR /workspace
|
|
460
|
+
`;
|
|
461
|
+
async function initializeSandbox(repoPath, io) {
|
|
462
|
+
const forgePath = path.join(repoPath, ".forge");
|
|
463
|
+
const files = [
|
|
464
|
+
["forge.sandbox.yml", sandboxContractTemplate],
|
|
465
|
+
["Dockerfile", sandboxDockerfileTemplate],
|
|
466
|
+
["forge.env.example", "# List non-secret variable names in forge.sandbox.yml. Never commit secret values.\n"]
|
|
467
|
+
];
|
|
468
|
+
await fs.mkdir(forgePath, { recursive: true });
|
|
469
|
+
let created = 0;
|
|
470
|
+
for (const [name, contents] of files) {
|
|
471
|
+
const target = path.join(forgePath, name);
|
|
472
|
+
try {
|
|
473
|
+
const handle = await fs.open(target, "wx", 0o600);
|
|
474
|
+
await handle.writeFile(contents);
|
|
475
|
+
await handle.close();
|
|
476
|
+
io.stdout.write(`Created ${path.relative(repoPath, target)}\n`);
|
|
477
|
+
created += 1;
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
if (error.code !== "EEXIST")
|
|
481
|
+
throw error;
|
|
482
|
+
io.stdout.write(`Kept existing ${path.relative(repoPath, target)}\n`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
io.stdout.write(created > 0 ? "Sandbox scaffold is ready. Run `forge-bridge sandbox doctor`.\n" : "Sandbox scaffold already exists.\n");
|
|
486
|
+
return 0;
|
|
487
|
+
}
|
|
488
|
+
async function diagnoseSandbox(repoPath, requirePinnedBase, io) {
|
|
489
|
+
try {
|
|
490
|
+
const forgePath = path.join(repoPath, ".forge");
|
|
491
|
+
const contractSource = await fs.readFile(path.join(forgePath, "forge.sandbox.yml"), "utf8");
|
|
492
|
+
const dockerfile = await fs.readFile(path.join(forgePath, "Dockerfile"), "utf8");
|
|
493
|
+
const contract = validateSandboxContract(contractSource);
|
|
494
|
+
const warnings = validateDockerfilePolicy(dockerfile, requirePinnedBase);
|
|
495
|
+
io.stdout.write(`Contract: valid (${contract.digest})\n`);
|
|
496
|
+
for (const warning of warnings)
|
|
497
|
+
io.stdout.write(`Warning: ${warning}\n`);
|
|
498
|
+
const config = defaultBridgeServiceConfig();
|
|
499
|
+
const preflight = new DockerPreflight({
|
|
500
|
+
dockerExecutable: config.dockerExecutable,
|
|
501
|
+
dockerContext: config.dockerContext,
|
|
502
|
+
stateRoot: config.sandboxStateRoot,
|
|
503
|
+
maxConcurrentSandboxes: config.maxConcurrentSandboxes,
|
|
504
|
+
diskHighWatermarkPercent: config.diskHighWatermarkPercent,
|
|
505
|
+
diskLowWatermarkPercent: config.diskLowWatermarkPercent,
|
|
506
|
+
minimumWorkspaceBytes: config.minimumWorkspaceBytes,
|
|
507
|
+
hostMemoryReserveBytes: config.sandboxHostMemoryReserveBytes,
|
|
508
|
+
defaultWorkerMemoryLimitBytes: config.defaultSandboxWorkerMemoryLimitBytes,
|
|
509
|
+
defaultServiceMemoryLimitBytes: config.defaultSandboxServiceMemoryLimitBytes,
|
|
510
|
+
runtimeOverheadBytes: config.sandboxRuntimeOverheadBytes,
|
|
511
|
+
requireRootless: config.requireRootlessDocker
|
|
512
|
+
});
|
|
513
|
+
try {
|
|
514
|
+
const result = await preflight.refresh();
|
|
515
|
+
if (!result.healthy) {
|
|
516
|
+
io.stderr.write(`Docker: unavailable (${result.unavailableCode ?? "preflight_failed"}): ${result.unavailableReason ?? "unknown reason"}\n`);
|
|
517
|
+
return 1;
|
|
518
|
+
}
|
|
519
|
+
io.stdout.write(`Docker: healthy (${result.runtimeVersion}; capacity ${result.maxConcurrentSandboxes})\n`);
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
await preflight.release();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
528
|
+
return 1;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function smokeSandbox(repoPath, reportPath, requirePinnedBase, probeLoopbackPort, io) {
|
|
532
|
+
try {
|
|
533
|
+
const result = await new DockerSandboxSmoke(defaultBridgeServiceConfig()).run({
|
|
534
|
+
repoPath,
|
|
535
|
+
reportPath,
|
|
536
|
+
requirePinnedBase,
|
|
537
|
+
probeLoopbackPort,
|
|
538
|
+
onProgress: (stage) => io.stdout.write(`Smoke: ${stage}...\n`),
|
|
539
|
+
});
|
|
540
|
+
const { report } = result;
|
|
541
|
+
if (report.repository.contractDigest)
|
|
542
|
+
io.stdout.write(`Contract: valid (${report.repository.contractDigest})\n`);
|
|
543
|
+
for (const warning of report.repository.policyWarnings)
|
|
544
|
+
io.stdout.write(`Warning: ${warning}\n`);
|
|
545
|
+
if (report.docker.runtimeVersion) {
|
|
546
|
+
io.stdout.write(`Docker: healthy (${report.docker.runtimeVersion}; capacity ${report.docker.capacity})\n`);
|
|
547
|
+
}
|
|
548
|
+
io.stdout.write(`Attempt: ${report.dispatchAttemptId}\n`);
|
|
549
|
+
io.stdout.write(`Resource lease: ${report.resourceLeaseId}\n`);
|
|
550
|
+
if (report.publishedPortProbe) {
|
|
551
|
+
io.stdout.write(`Loopback port: ${report.publishedPortProbe.hostIp}:${report.publishedPortProbe.hostPort} -> ${report.publishedPortProbe.containerPort}/tcp\n`);
|
|
552
|
+
}
|
|
553
|
+
io.stdout.write(`Cleanup: ${report.cleanup.verified ? "verified (zero exact-label resources)" : "failed"}\n`);
|
|
554
|
+
io.stdout.write(`Report: ${result.reportPath}\n`);
|
|
555
|
+
if (report.status === "passed") {
|
|
556
|
+
io.stdout.write("Sandbox smoke: PASS\n");
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
io.stderr.write(`Sandbox smoke: FAIL (${report.failure?.code ?? "sandbox_smoke_failed"} at ${report.failure?.stage ?? "unknown"}): ${report.failure?.message ?? "unknown failure"}\n`);
|
|
560
|
+
return 1;
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
io.stderr.write(`Sandbox smoke could not write its report: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
564
|
+
return 1;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function reapSandboxes(io) {
|
|
568
|
+
const config = defaultBridgeServiceConfig();
|
|
569
|
+
if (!/^[0-9a-f-]{32,36}$/iu.test(config.runnerId)) {
|
|
570
|
+
io.stderr.write("FORGE_RUNNER_ID must identify the runner whose exact-labeled resources may be reaped.\n");
|
|
571
|
+
return 2;
|
|
572
|
+
}
|
|
573
|
+
const preflight = new DockerPreflight({
|
|
574
|
+
dockerExecutable: config.dockerExecutable,
|
|
575
|
+
dockerContext: config.dockerContext,
|
|
576
|
+
stateRoot: config.sandboxStateRoot,
|
|
577
|
+
maxConcurrentSandboxes: config.maxConcurrentSandboxes,
|
|
578
|
+
diskHighWatermarkPercent: config.diskHighWatermarkPercent,
|
|
579
|
+
diskLowWatermarkPercent: config.diskLowWatermarkPercent,
|
|
580
|
+
minimumWorkspaceBytes: config.minimumWorkspaceBytes,
|
|
581
|
+
hostMemoryReserveBytes: config.sandboxHostMemoryReserveBytes,
|
|
582
|
+
defaultWorkerMemoryLimitBytes: config.defaultSandboxWorkerMemoryLimitBytes,
|
|
583
|
+
defaultServiceMemoryLimitBytes: config.defaultSandboxServiceMemoryLimitBytes,
|
|
584
|
+
runtimeOverheadBytes: config.sandboxRuntimeOverheadBytes,
|
|
585
|
+
requireRootless: config.requireRootlessDocker
|
|
586
|
+
});
|
|
587
|
+
try {
|
|
588
|
+
const result = await preflight.refresh();
|
|
589
|
+
if (!result.healthy && result.unavailableCode !== "docker_disk_low") {
|
|
590
|
+
io.stderr.write(`Docker: unavailable (${result.unavailableCode ?? "preflight_failed"}): ${result.unavailableReason ?? "unknown reason"}\n`);
|
|
591
|
+
return 1;
|
|
592
|
+
}
|
|
593
|
+
const runtime = new DockerSandboxRuntime(config);
|
|
594
|
+
await runtime.reap({ diskLow: result.diskLow });
|
|
595
|
+
io.stdout.write(`Sandbox reaper completed for runner ${config.runnerId}.\n`);
|
|
596
|
+
return 0;
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
600
|
+
return 1;
|
|
601
|
+
}
|
|
602
|
+
finally {
|
|
603
|
+
await preflight.release();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
285
606
|
function parseConnectArgs(argv) {
|
|
286
607
|
let serverUrl = null;
|
|
287
608
|
let runnerId = null;
|
|
@@ -414,7 +735,7 @@ function parsePairArgs(argv) {
|
|
|
414
735
|
name: name?.trim() || defaultName,
|
|
415
736
|
fingerprint: fingerprint?.trim() || `local-bridge:${os.hostname()}:${user}`,
|
|
416
737
|
repoRoot: repoRoot?.trim() ? path.resolve(repoRoot) : undefined,
|
|
417
|
-
cleanupWorktrees: cleanupWorktrees ??
|
|
738
|
+
cleanupWorktrees: cleanupWorktrees ?? defaultConfig.cleanupWorktrees,
|
|
418
739
|
cliProviders: cliProviders.length > 0 ? cliProviders : localConfig.cliProviders && localConfig.cliProviders.length > 0 ? [...localConfig.cliProviders] : ["codex"],
|
|
419
740
|
projectLabels: projectLabels.length > 0 ? projectLabels : localConfig.projectLabels ? [...localConfig.projectLabels] : []
|
|
420
741
|
};
|
|
@@ -470,7 +791,7 @@ async function claimBridgeRunner(args) {
|
|
|
470
791
|
})
|
|
471
792
|
});
|
|
472
793
|
if (!response.ok) {
|
|
473
|
-
throw new Error(
|
|
794
|
+
throw new Error(formatPairingFailure(response.status, args.serverUrl, await response.text()));
|
|
474
795
|
}
|
|
475
796
|
const value = await response.json();
|
|
476
797
|
if (!value.runnerId || !value.deviceToken) {
|
|
@@ -499,6 +820,8 @@ function pairedConfig(current, args, claim) {
|
|
|
499
820
|
supportsManagedForgeArtifacts: current.supportsManagedForgeArtifacts ?? true,
|
|
500
821
|
supportsRunnerAuthoredPhaseArtifacts: current.supportsRunnerAuthoredPhaseArtifacts ?? true,
|
|
501
822
|
supportsDraftRunOnce: current.supportsDraftRunOnce ?? true,
|
|
823
|
+
supportsDraftAttachments: current.supportsDraftAttachments ?? true,
|
|
824
|
+
supportsDraftImageAttachments: supportsDraftImageAttachmentsFor(args.cliProviders),
|
|
502
825
|
repoMappings: current.repoMappings ?? []
|
|
503
826
|
};
|
|
504
827
|
}
|
|
@@ -543,6 +866,12 @@ async function resolvePairRepoRoot(repoRoot, configuredRoot) {
|
|
|
543
866
|
function normalizePairingCode(code) {
|
|
544
867
|
return code?.replace(/[\s-]/g, "").trim() ?? "";
|
|
545
868
|
}
|
|
869
|
+
function formatPairingFailure(status, serverUrl, body) {
|
|
870
|
+
const hint = body.includes("runner.invalid_pairing_code")
|
|
871
|
+
? " Make sure the code was generated by the same Forge server shown above."
|
|
872
|
+
: "";
|
|
873
|
+
return `Pairing failed (${status}) against ${serverUrl}: ${body}${hint}`;
|
|
874
|
+
}
|
|
546
875
|
function normalizeServerUrl(serverUrl) {
|
|
547
876
|
return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
548
877
|
}
|
|
@@ -561,11 +890,28 @@ function connectStartupText(config) {
|
|
|
561
890
|
` repo folder: ${config.defaultRepoRoot || "not configured"}`,
|
|
562
891
|
` providers: ${formatList(config.cliProviders)}`,
|
|
563
892
|
` cleanup worktrees: ${config.cleanupWorktrees ? "enabled" : "disabled"}`,
|
|
893
|
+
` Docker Desktop auto-recovery: ${config.dockerDesktopRecoveryEnabled ? "enabled" : "disabled"}`,
|
|
894
|
+
` open public sandbox networking: ${config.allowOpenPublicSandbox ? "enabled" : "disabled"}`,
|
|
564
895
|
"",
|
|
565
896
|
"Press Ctrl+C to stop.",
|
|
566
897
|
"",
|
|
567
898
|
].join("\n");
|
|
568
899
|
}
|
|
900
|
+
function writeSandboxPolicyStatus(local, io) {
|
|
901
|
+
const effective = configuredBridgeServiceConfig({}, local);
|
|
902
|
+
io.stdout.write(`Config: ${resolveLocalConfigPath()}\n`);
|
|
903
|
+
io.stdout.write(`Docker sandbox placement: ${effective.dockerEnabled ? "on" : "off"}\n`);
|
|
904
|
+
io.stdout.write(`Open public sandbox networking: ${effective.allowOpenPublicSandbox ? "allowed" : "denied"}\n`);
|
|
905
|
+
if (process.env.FORGE_DOCKER_SANDBOX !== undefined) {
|
|
906
|
+
io.stdout.write("Docker setting source: FORGE_DOCKER_SANDBOX override\n");
|
|
907
|
+
}
|
|
908
|
+
if (process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX !== undefined) {
|
|
909
|
+
io.stdout.write("Open public setting source: FORGE_ALLOW_OPEN_PUBLIC_SANDBOX override\n");
|
|
910
|
+
}
|
|
911
|
+
if (effective.allowOpenPublicSandbox) {
|
|
912
|
+
io.stdout.write("Warning: open_public permits unrestricted outbound network access from developer sandboxes.\n");
|
|
913
|
+
}
|
|
914
|
+
}
|
|
569
915
|
function formatList(values) {
|
|
570
916
|
return values.length > 0 ? values.join(", ") : "none";
|
|
571
917
|
}
|
|
@@ -602,7 +948,7 @@ async function writeStatus(config, checkUpdates, io) {
|
|
|
602
948
|
io.stdout.write("Update check: no update channel configured.\n");
|
|
603
949
|
}
|
|
604
950
|
}
|
|
605
|
-
async function cleanupRepos(config, mode, force, io) {
|
|
951
|
+
async function cleanupRepos(config, mode, force, cleanup, io) {
|
|
606
952
|
const repos = config.repoMappings.filter((mapping) => mapping.localPath?.trim());
|
|
607
953
|
if (repos.length === 0) {
|
|
608
954
|
io.stdout.write("No repos configured.\n");
|
|
@@ -611,20 +957,28 @@ async function cleanupRepos(config, mode, force, io) {
|
|
|
611
957
|
let failures = 0;
|
|
612
958
|
for (const repo of repos) {
|
|
613
959
|
const repoPath = path.resolve(repo.localPath);
|
|
614
|
-
const worktreeRoot = path.join(path.dirname(repoPath), ".forge-worktrees", path.basename(repoPath));
|
|
615
|
-
if (!force) {
|
|
616
|
-
const action = mode === "all" ? "remove" : "prune";
|
|
617
|
-
io.stdout.write(`[dry-run] Would ${action} ${worktreeRoot}\n`);
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
960
|
try {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
961
|
+
const result = await cleanup.run({
|
|
962
|
+
repoRoot: repoPath,
|
|
963
|
+
allowedRepoRoot: config.defaultRepoRoot || undefined,
|
|
964
|
+
mode,
|
|
965
|
+
force,
|
|
966
|
+
});
|
|
967
|
+
if (!force) {
|
|
968
|
+
const action = mode === "all"
|
|
969
|
+
? "remove"
|
|
970
|
+
: mode === "stale"
|
|
971
|
+
? `remove ${result.candidateWorktrees} stale managed ${result.candidateWorktrees === 1 ? "worktree" : "worktrees"} from`
|
|
972
|
+
: "prune";
|
|
973
|
+
io.stdout.write(`[dry-run] Would ${action} ${result.worktreeRoot}\n`);
|
|
974
|
+
}
|
|
975
|
+
else if (mode === "all") {
|
|
976
|
+
io.stdout.write(`Removed ${result.worktreeRoot}\n`);
|
|
977
|
+
}
|
|
978
|
+
else if (mode === "stale") {
|
|
979
|
+
io.stdout.write(`Removed ${result.removedWorktrees} stale managed ${result.removedWorktrees === 1 ? "worktree" : "worktrees"} from ${result.worktreeRoot}\n`);
|
|
625
980
|
}
|
|
626
981
|
else {
|
|
627
|
-
await runGit(repoPath, ["worktree", "prune"]);
|
|
628
982
|
io.stdout.write(`Pruned stale worktree metadata for ${repoPath}\n`);
|
|
629
983
|
}
|
|
630
984
|
}
|
|
@@ -773,8 +1127,8 @@ function uniqueModels(models) {
|
|
|
773
1127
|
function isRecord(value) {
|
|
774
1128
|
return typeof value === "object" && value !== null;
|
|
775
1129
|
}
|
|
776
|
-
async function withDetectedCliProviders(config) {
|
|
777
|
-
const detected = await detectCliProviders(
|
|
1130
|
+
async function withDetectedCliProviders(config, candidates = config.cliProviders) {
|
|
1131
|
+
const detected = await detectCliProviders(uniqueProviders(candidates));
|
|
778
1132
|
const found = detected.filter((result) => result.found);
|
|
779
1133
|
if (found.length === 0) {
|
|
780
1134
|
return config;
|
|
@@ -798,6 +1152,7 @@ async function withDetectedCliProviders(config) {
|
|
|
798
1152
|
};
|
|
799
1153
|
}
|
|
800
1154
|
async function withDetectedPairCliProviders(args) {
|
|
1155
|
+
const configured = configuredProviderCandidates(args.cliProviders);
|
|
801
1156
|
const detectedConfig = await withDetectedCliProviders({
|
|
802
1157
|
...defaultBridgeServiceConfig({
|
|
803
1158
|
serverUrl: args.serverUrl,
|
|
@@ -807,7 +1162,7 @@ async function withDetectedPairCliProviders(args) {
|
|
|
807
1162
|
cleanupWorktrees: args.cleanupWorktrees,
|
|
808
1163
|
defaultRepoRoot: args.repoRoot ?? defaultBridgeServiceConfig().defaultRepoRoot
|
|
809
1164
|
})
|
|
810
|
-
});
|
|
1165
|
+
}, configured);
|
|
811
1166
|
return {
|
|
812
1167
|
...args,
|
|
813
1168
|
cliProviders: detectedConfig.cliProviders
|
|
@@ -822,6 +1177,3 @@ function uniqueProviders(providers) {
|
|
|
822
1177
|
.filter((provider) => provider.length > 0)
|
|
823
1178
|
.filter((provider, index, all) => all.findIndex((candidate) => candidate.toLowerCase() === provider.toLowerCase()) === index);
|
|
824
1179
|
}
|
|
825
|
-
async function runGit(cwd, args) {
|
|
826
|
-
await execFileAsync("git", args, { cwd, timeout: 10_000 });
|
|
827
|
-
}
|
|
@@ -7,6 +7,9 @@ export interface BridgeLocalConfig {
|
|
|
7
7
|
readonly deviceName?: string;
|
|
8
8
|
readonly defaultRepoRoot?: string;
|
|
9
9
|
readonly cleanupWorktrees?: boolean;
|
|
10
|
+
readonly dockerEnabled?: boolean;
|
|
11
|
+
readonly dockerDesktopRecoveryEnabled?: boolean;
|
|
12
|
+
readonly allowOpenPublicSandbox?: boolean;
|
|
10
13
|
readonly cliProviders?: readonly string[];
|
|
11
14
|
readonly providerStatuses?: Readonly<Record<string, BridgeProviderStatus>>;
|
|
12
15
|
readonly projectLabels?: readonly string[];
|
|
@@ -17,6 +20,8 @@ export interface BridgeLocalConfig {
|
|
|
17
20
|
readonly supportsManagedForgeArtifacts?: boolean;
|
|
18
21
|
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean;
|
|
19
22
|
readonly supportsDraftRunOnce?: boolean;
|
|
23
|
+
readonly supportsDraftAttachments?: boolean;
|
|
24
|
+
readonly supportsDraftImageAttachments?: boolean;
|
|
20
25
|
}
|
|
21
26
|
export interface LocalRepoMapping extends BridgeRepoMapping {
|
|
22
27
|
readonly label: string;
|
|
@@ -32,4 +37,6 @@ export declare function removeRepoMapping(label: string): Promise<{
|
|
|
32
37
|
readonly config: BridgeLocalConfig;
|
|
33
38
|
readonly removed: boolean;
|
|
34
39
|
}>;
|
|
40
|
+
export declare function setServerUrl(serverUrl: string): Promise<BridgeLocalConfig>;
|
|
41
|
+
export declare function setSandboxPolicy(dockerEnabled: boolean, allowOpenPublicSandbox: boolean): Promise<BridgeLocalConfig>;
|
|
35
42
|
export declare function providerStatusesFor(providers: readonly string[]): Readonly<Record<string, BridgeProviderStatus>>;
|