@loucompanion/forge-bridge 0.1.3-dev.b6be5e7f9668 → 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.
- package/README.md +127 -0
- package/dist/bridge/bridge.base.d.ts +8 -1
- package/dist/bridge/bridge.service.d.ts +98 -3
- package/dist/bridge/bridge.service.js +1445 -48
- package/dist/bridge/bridge.types.d.ts +209 -1
- 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 +100 -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 +45 -2
- package/dist/cli/cli.service.js +383 -209
- 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-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 +386 -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 +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-diagnostics.d.ts +4 -0
- package/dist/runtime/host-diagnostics.js +95 -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 +56 -0
- package/dist/runtime/host-runtime-contract.js +709 -0
- package/dist/runtime/host-runtime-lease.d.ts +201 -0
- package/dist/runtime/host-runtime-lease.js +2314 -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 +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/pinned-directory-process.d.ts +4 -0
- package/dist/runtime/pinned-directory-process.js +20 -0
- package/dist/runtime/pinned-source-directory.d.ts +9 -0
- package/dist/runtime/pinned-source-directory.js +56 -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 +3 -0
- package/dist/runtime/prepared-runtime-identity.js +191 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +22 -0
- package/dist/runtime/prepared-runtime-readiness.js +191 -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 +27 -0
- package/dist/session/phase-completion.artifacts.js +829 -0
- package/dist/session/phase-completion.d.ts +44 -46
- package/dist/session/phase-completion.helpers.d.ts +117 -0
- package/dist/session/phase-completion.helpers.js +701 -0
- package/dist/session/phase-completion.js +42 -3488
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +236 -0
- package/dist/session/phase-completion.runtime.d.ts +48 -0
- package/dist/session/phase-completion.runtime.js +790 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +820 -0
- package/dist/session/phase-completion.validation.d.ts +37 -0
- package/dist/session/phase-completion.validation.js +855 -0
- package/dist/session/pre-publication-validation.d.ts +47 -0
- package/dist/session/pre-publication-validation.js +249 -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 +94 -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 +377 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +57 -0
- package/dist/session/provider-cli/provider-cli.process.js +716 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +47 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +611 -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 +9 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +43 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +29 -53
- package/dist/session/provider-cli/provider-cli.service.js +640 -2581
- package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +22 -0
- package/dist/session/provider-cli/provider-output-state.js +520 -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 +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 +550 -58
- package/dist/session/session.types.d.ts +180 -1
- package/dist/session/session.types.js +323 -4
- 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 +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 +2 -0
- package/dist/shared/process/process.js +11 -6
- 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 +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 +82 -10
- 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 +16 -3
- package/dist/worktree/worktree.service.js +323 -54
- 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
package/dist/cli/cli.service.js
CHANGED
|
@@ -5,23 +5,37 @@ import path from "node:path";
|
|
|
5
5
|
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
|
-
import {
|
|
8
|
+
import { buildWindowsCmdLaunch } from "../shared/process/windows-launch.js";
|
|
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";
|
|
9
12
|
import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
|
|
10
13
|
import { forgeBridgeVersion } from "../version.js";
|
|
11
|
-
import {
|
|
14
|
+
import { ManagedWorktreeCleanupService } from "../cleanup/cleanup.service.js";
|
|
15
|
+
import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, setSandboxPolicy, setServerUrl, writeLocalConfig } from "./local-config.js";
|
|
12
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";
|
|
13
22
|
import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
|
|
14
23
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
24
|
+
import { commandHelpText, helpText } from "./cli.help.js";
|
|
15
25
|
const apiKeySecretGrantEnv = "FORGE_API_KEY_SECRET_GRANT";
|
|
16
26
|
const execFileAsync = promisify(execFile);
|
|
17
27
|
export class ForgeBridgeCliService {
|
|
18
28
|
sessions;
|
|
19
29
|
bridgeFactory;
|
|
20
30
|
pairRunner;
|
|
21
|
-
|
|
31
|
+
cleanupFactory;
|
|
32
|
+
dotNetToolchainConfigurator;
|
|
33
|
+
constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner, cleanupFactory = stateRoot => new ManagedWorktreeCleanupService(stateRoot), dotNetToolchainConfigurator = configureDotNetToolchain) {
|
|
22
34
|
this.sessions = sessions;
|
|
23
35
|
this.bridgeFactory = bridgeFactory;
|
|
24
36
|
this.pairRunner = pairRunner;
|
|
37
|
+
this.cleanupFactory = cleanupFactory;
|
|
38
|
+
this.dotNetToolchainConfigurator = dotNetToolchainConfigurator;
|
|
25
39
|
}
|
|
26
40
|
async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
27
41
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
@@ -56,11 +70,29 @@ export class ForgeBridgeCliService {
|
|
|
56
70
|
io.stderr.write(`forge-bridge connect requires ${missing[0]}.\n`);
|
|
57
71
|
return 2;
|
|
58
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
|
+
}
|
|
59
87
|
const controller = new AbortController();
|
|
60
88
|
const stop = () => controller.abort("signal received");
|
|
61
89
|
process.once("SIGINT", stop);
|
|
62
90
|
process.once("SIGTERM", stop);
|
|
63
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
|
+
}
|
|
64
96
|
io.stdout.write(connectStartupText(config));
|
|
65
97
|
await this.bridgeFactory(config).run(controller.signal);
|
|
66
98
|
return 0;
|
|
@@ -86,6 +118,7 @@ export class ForgeBridgeCliService {
|
|
|
86
118
|
if (args.command === "pair") {
|
|
87
119
|
try {
|
|
88
120
|
const current = readLocalConfigSync();
|
|
121
|
+
io.stdout.write(`Pairing with server: ${args.serverUrl}\n`);
|
|
89
122
|
const code = await resolvePairingCode(args.code);
|
|
90
123
|
const repoRoot = await resolvePairRepoRoot(args.repoRoot, current.defaultRepoRoot);
|
|
91
124
|
await fs.mkdir(repoRoot, { recursive: true });
|
|
@@ -115,12 +148,66 @@ export class ForgeBridgeCliService {
|
|
|
115
148
|
return 2;
|
|
116
149
|
}
|
|
117
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
|
+
}
|
|
118
177
|
if (args.command === "status") {
|
|
119
178
|
await writeStatus(args.config, args.checkUpdates, io);
|
|
120
179
|
return 0;
|
|
121
180
|
}
|
|
122
181
|
if (args.command === "cleanup") {
|
|
123
|
-
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;
|
|
124
211
|
}
|
|
125
212
|
if (args.command === "mcp") {
|
|
126
213
|
const parsed = parseCompletionMcpArgs(args.argv);
|
|
@@ -140,12 +227,12 @@ export class ForgeBridgeCliService {
|
|
|
140
227
|
emit: async (event) => {
|
|
141
228
|
const line = {
|
|
142
229
|
type: "runner_event",
|
|
143
|
-
event:
|
|
230
|
+
event: redactRunnerDispatchEvent(event, redactionSecrets)
|
|
144
231
|
};
|
|
145
232
|
io.stdout.write(`${JSON.stringify(line)}\n`);
|
|
146
233
|
}
|
|
147
234
|
});
|
|
148
|
-
await writeJsonAtomic(args.resultPath,
|
|
235
|
+
await writeJsonAtomic(args.resultPath, redactRunOnceResultPacket(result, redactionSecrets));
|
|
149
236
|
return result.exitCode;
|
|
150
237
|
}
|
|
151
238
|
catch (error) {
|
|
@@ -231,6 +318,15 @@ export function parseArgs(argv) {
|
|
|
231
318
|
}
|
|
232
319
|
return { ok: true, command: "remove-repo", label: argv[1] };
|
|
233
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
|
+
}
|
|
234
330
|
if (argv[0] === "status") {
|
|
235
331
|
let checkUpdates = false;
|
|
236
332
|
for (let index = 1; index < argv.length; index += 1) {
|
|
@@ -250,10 +346,13 @@ export function parseArgs(argv) {
|
|
|
250
346
|
if (argv[0] === "mcp") {
|
|
251
347
|
return { ok: true, command: "mcp", argv };
|
|
252
348
|
}
|
|
349
|
+
if (argv[0] === "sandbox") {
|
|
350
|
+
return parseSandboxArgs(argv);
|
|
351
|
+
}
|
|
253
352
|
if (argv[0] !== "run-once") {
|
|
254
353
|
return {
|
|
255
354
|
ok: false,
|
|
256
|
-
error:
|
|
355
|
+
error: helpText()
|
|
257
356
|
};
|
|
258
357
|
}
|
|
259
358
|
let dispatchPath = null;
|
|
@@ -280,6 +379,230 @@ export function parseArgs(argv) {
|
|
|
280
379
|
resultPath: path.resolve(resultPath)
|
|
281
380
|
};
|
|
282
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
|
+
}
|
|
283
606
|
function parseConnectArgs(argv) {
|
|
284
607
|
let serverUrl = null;
|
|
285
608
|
let runnerId = null;
|
|
@@ -412,7 +735,7 @@ function parsePairArgs(argv) {
|
|
|
412
735
|
name: name?.trim() || defaultName,
|
|
413
736
|
fingerprint: fingerprint?.trim() || `local-bridge:${os.hostname()}:${user}`,
|
|
414
737
|
repoRoot: repoRoot?.trim() ? path.resolve(repoRoot) : undefined,
|
|
415
|
-
cleanupWorktrees: cleanupWorktrees ??
|
|
738
|
+
cleanupWorktrees: cleanupWorktrees ?? defaultConfig.cleanupWorktrees,
|
|
416
739
|
cliProviders: cliProviders.length > 0 ? cliProviders : localConfig.cliProviders && localConfig.cliProviders.length > 0 ? [...localConfig.cliProviders] : ["codex"],
|
|
417
740
|
projectLabels: projectLabels.length > 0 ? projectLabels : localConfig.projectLabels ? [...localConfig.projectLabels] : []
|
|
418
741
|
};
|
|
@@ -468,7 +791,7 @@ async function claimBridgeRunner(args) {
|
|
|
468
791
|
})
|
|
469
792
|
});
|
|
470
793
|
if (!response.ok) {
|
|
471
|
-
throw new Error(
|
|
794
|
+
throw new Error(formatPairingFailure(response.status, args.serverUrl, await response.text()));
|
|
472
795
|
}
|
|
473
796
|
const value = await response.json();
|
|
474
797
|
if (!value.runnerId || !value.deviceToken) {
|
|
@@ -497,6 +820,8 @@ function pairedConfig(current, args, claim) {
|
|
|
497
820
|
supportsManagedForgeArtifacts: current.supportsManagedForgeArtifacts ?? true,
|
|
498
821
|
supportsRunnerAuthoredPhaseArtifacts: current.supportsRunnerAuthoredPhaseArtifacts ?? true,
|
|
499
822
|
supportsDraftRunOnce: current.supportsDraftRunOnce ?? true,
|
|
823
|
+
supportsDraftAttachments: current.supportsDraftAttachments ?? true,
|
|
824
|
+
supportsDraftImageAttachments: supportsDraftImageAttachmentsFor(args.cliProviders),
|
|
500
825
|
repoMappings: current.repoMappings ?? []
|
|
501
826
|
};
|
|
502
827
|
}
|
|
@@ -541,181 +866,15 @@ async function resolvePairRepoRoot(repoRoot, configuredRoot) {
|
|
|
541
866
|
function normalizePairingCode(code) {
|
|
542
867
|
return code?.replace(/[\s-]/g, "").trim() ?? "";
|
|
543
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
|
+
}
|
|
544
875
|
function normalizeServerUrl(serverUrl) {
|
|
545
876
|
return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
546
877
|
}
|
|
547
|
-
function usage() {
|
|
548
|
-
return helpText();
|
|
549
|
-
}
|
|
550
|
-
function helpText() {
|
|
551
|
-
const version = defaultBridgeServiceConfig().version ?? forgeBridgeVersion;
|
|
552
|
-
return [
|
|
553
|
-
`🛠 Forge Bridge ${version} — Local provider runners for Forge.`,
|
|
554
|
-
"",
|
|
555
|
-
"Usage: forge-bridge [options] [command]",
|
|
556
|
-
"",
|
|
557
|
-
"Options:",
|
|
558
|
-
" -h, --help Display help for command",
|
|
559
|
-
" -v, --version Output the version number",
|
|
560
|
-
"",
|
|
561
|
-
"Commands:",
|
|
562
|
-
" Hint: commands suffixed with * have detailed help. Run <command> --help for details.",
|
|
563
|
-
" connect * Start the runner bridge and wait for Forge dispatches",
|
|
564
|
-
" pair * Pair this machine with Forge and save runner credentials",
|
|
565
|
-
" status * Show config, runner identity, repos, provider CLIs, and updates",
|
|
566
|
-
" add-repo * Map a Forge repo label/name to a local checkout path",
|
|
567
|
-
" remove-repo * Remove a saved repo mapping",
|
|
568
|
-
" cleanup * Prune managed bridge worktrees",
|
|
569
|
-
" version * Print the installed bridge package version",
|
|
570
|
-
" help * Display help for command",
|
|
571
|
-
" run-once * Internal: run one dispatch packet from JSON",
|
|
572
|
-
" mcp * Internal: run the phase-completion MCP server",
|
|
573
|
-
"",
|
|
574
|
-
"Examples:",
|
|
575
|
-
" npm install -g @loucompanion/forge-bridge@next",
|
|
576
|
-
" forge-bridge pair 1234-5678 --server https://srv1493013.tail66073d.ts.net:8443 --repo-root \"%USERPROFILE%\\ForgeRepos\"",
|
|
577
|
-
" forge-bridge connect",
|
|
578
|
-
" forge-bridge status --check-updates",
|
|
579
|
-
" forge-bridge add-repo Forge D:\\Programming\\repos\\Forge",
|
|
580
|
-
" forge-bridge cleanup --stale",
|
|
581
|
-
"",
|
|
582
|
-
"Troubleshooting:",
|
|
583
|
-
" If Forge says the bridge is old: npm install -g @loucompanion/forge-bridge@next",
|
|
584
|
-
" If connect shows 502/500: leave it open; it reconnects with backoff while dev restarts.",
|
|
585
|
-
" If a provider is missing: run forge-bridge status --check-updates and verify CLI login.",
|
|
586
|
-
"",
|
|
587
|
-
"Docs:",
|
|
588
|
-
" https://github.com/coleman399/Forge/tree/develop/tools/forge-bridge#readme",
|
|
589
|
-
"",
|
|
590
|
-
].join("\n");
|
|
591
|
-
}
|
|
592
|
-
function commandHelpText(command) {
|
|
593
|
-
switch (command) {
|
|
594
|
-
case "pair":
|
|
595
|
-
return [
|
|
596
|
-
"Forge Bridge — pair",
|
|
597
|
-
"Pair this machine with Forge and save runner credentials.",
|
|
598
|
-
"",
|
|
599
|
-
"Usage:",
|
|
600
|
-
" forge-bridge pair [code] [options]",
|
|
601
|
-
"",
|
|
602
|
-
"Options:",
|
|
603
|
-
" --server <url> Forge server URL.",
|
|
604
|
-
" --repo-root <path> Folder where bridge-managed repos/worktrees live.",
|
|
605
|
-
" --name <name> Display name for this runner.",
|
|
606
|
-
" --fingerprint <value> Stable device fingerprint override.",
|
|
607
|
-
" --cli-provider <list> Comma-separated providers, e.g. codex,cursor.",
|
|
608
|
-
" --cleanup-worktrees Enable managed worktree cleanup.",
|
|
609
|
-
" --no-cleanup-worktrees Disable managed worktree cleanup.",
|
|
610
|
-
"",
|
|
611
|
-
"Example:",
|
|
612
|
-
" forge-bridge pair 1234-5678 --server https://forge.example.com --repo-root \"%USERPROFILE%\\ForgeRepos\"",
|
|
613
|
-
"",
|
|
614
|
-
].join("\n");
|
|
615
|
-
case "connect":
|
|
616
|
-
return [
|
|
617
|
-
"Forge Bridge — connect",
|
|
618
|
-
"Start the runner bridge and keep it connected for Forge dispatches.",
|
|
619
|
-
"",
|
|
620
|
-
"Usage:",
|
|
621
|
-
" forge-bridge connect [options]",
|
|
622
|
-
"",
|
|
623
|
-
"Options:",
|
|
624
|
-
" --server <url> Forge server URL; saved config is used when omitted.",
|
|
625
|
-
" --runner-id <id> Runner id; saved config is used when omitted.",
|
|
626
|
-
" --device-token <token> Device token; saved config is used when omitted.",
|
|
627
|
-
" --repo-root <path> Override repo/worktree folder for this run.",
|
|
628
|
-
" --cli-provider <list> Comma-separated providers, e.g. codex,cursor.",
|
|
629
|
-
" --heartbeat-interval-ms <n> Heartbeat interval in milliseconds.",
|
|
630
|
-
" --cleanup-worktrees Enable managed worktree cleanup for this run.",
|
|
631
|
-
" --no-cleanup-worktrees Disable managed worktree cleanup for this run.",
|
|
632
|
-
"",
|
|
633
|
-
"Example:",
|
|
634
|
-
" forge-bridge connect",
|
|
635
|
-
"",
|
|
636
|
-
].join("\n");
|
|
637
|
-
case "status":
|
|
638
|
-
return [
|
|
639
|
-
"Forge Bridge — status",
|
|
640
|
-
"Show saved configuration, paired runner identity, repo mappings, detected provider CLIs, and optional update info.",
|
|
641
|
-
"",
|
|
642
|
-
"Usage:",
|
|
643
|
-
" forge-bridge status [--check-updates]",
|
|
644
|
-
"",
|
|
645
|
-
"Example:",
|
|
646
|
-
" forge-bridge status --check-updates",
|
|
647
|
-
"",
|
|
648
|
-
].join("\n");
|
|
649
|
-
case "add-repo":
|
|
650
|
-
return [
|
|
651
|
-
"Forge Bridge — add-repo",
|
|
652
|
-
"Map a Forge repo label/name to an existing local checkout.",
|
|
653
|
-
"",
|
|
654
|
-
"Usage:",
|
|
655
|
-
" forge-bridge add-repo <label> <path>",
|
|
656
|
-
"",
|
|
657
|
-
"Example:",
|
|
658
|
-
" forge-bridge add-repo Forge D:\\Programming\\repos\\Forge",
|
|
659
|
-
"",
|
|
660
|
-
].join("\n");
|
|
661
|
-
case "remove-repo":
|
|
662
|
-
return [
|
|
663
|
-
"Forge Bridge — remove-repo",
|
|
664
|
-
"Remove a saved local repo mapping.",
|
|
665
|
-
"",
|
|
666
|
-
"Usage:",
|
|
667
|
-
" forge-bridge remove-repo <label>",
|
|
668
|
-
"",
|
|
669
|
-
].join("\n");
|
|
670
|
-
case "cleanup":
|
|
671
|
-
return [
|
|
672
|
-
"Forge Bridge — cleanup",
|
|
673
|
-
"Prune managed bridge worktrees under the configured repo folder.",
|
|
674
|
-
"",
|
|
675
|
-
"Usage:",
|
|
676
|
-
" forge-bridge cleanup [--stale|--merged|--all] [--force]",
|
|
677
|
-
"",
|
|
678
|
-
"Examples:",
|
|
679
|
-
" forge-bridge cleanup --stale",
|
|
680
|
-
" forge-bridge cleanup --all --force",
|
|
681
|
-
"",
|
|
682
|
-
].join("\n");
|
|
683
|
-
case "run-once":
|
|
684
|
-
return [
|
|
685
|
-
"Forge Bridge — run-once",
|
|
686
|
-
"Internal command used by Forge to run a single dispatch packet.",
|
|
687
|
-
"",
|
|
688
|
-
"Usage:",
|
|
689
|
-
" forge-bridge run-once --dispatch <dispatch-json-path> --result <result-json-path>",
|
|
690
|
-
"",
|
|
691
|
-
].join("\n");
|
|
692
|
-
case "mcp":
|
|
693
|
-
return [
|
|
694
|
-
"Forge Bridge — mcp",
|
|
695
|
-
"Internal MCP server used by provider CLIs to report Forge phase artifacts.",
|
|
696
|
-
"",
|
|
697
|
-
"Usage:",
|
|
698
|
-
" forge-bridge mcp --worktree <path> --work-item-id <id> --phase <phase>",
|
|
699
|
-
"",
|
|
700
|
-
].join("\n");
|
|
701
|
-
case "version":
|
|
702
|
-
return [
|
|
703
|
-
"Forge Bridge — version",
|
|
704
|
-
"Print the installed bridge package version.",
|
|
705
|
-
"",
|
|
706
|
-
"Usage:",
|
|
707
|
-
" forge-bridge version",
|
|
708
|
-
"",
|
|
709
|
-
].join("\n");
|
|
710
|
-
default:
|
|
711
|
-
return [
|
|
712
|
-
`Unknown forge-bridge command: ${command}`,
|
|
713
|
-
"",
|
|
714
|
-
"Run `forge-bridge help` to see available commands.",
|
|
715
|
-
"",
|
|
716
|
-
].join("\n");
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
878
|
function connectStartupText(config) {
|
|
720
879
|
const runner = config.runnerId
|
|
721
880
|
? `${config.runnerId}${config.deviceName ? ` (${config.deviceName})` : ""}`
|
|
@@ -731,11 +890,28 @@ function connectStartupText(config) {
|
|
|
731
890
|
` repo folder: ${config.defaultRepoRoot || "not configured"}`,
|
|
732
891
|
` providers: ${formatList(config.cliProviders)}`,
|
|
733
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"}`,
|
|
734
895
|
"",
|
|
735
896
|
"Press Ctrl+C to stop.",
|
|
736
897
|
"",
|
|
737
898
|
].join("\n");
|
|
738
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
|
+
}
|
|
739
915
|
function formatList(values) {
|
|
740
916
|
return values.length > 0 ? values.join(", ") : "none";
|
|
741
917
|
}
|
|
@@ -772,7 +948,7 @@ async function writeStatus(config, checkUpdates, io) {
|
|
|
772
948
|
io.stdout.write("Update check: no update channel configured.\n");
|
|
773
949
|
}
|
|
774
950
|
}
|
|
775
|
-
async function cleanupRepos(config, mode, force, io) {
|
|
951
|
+
async function cleanupRepos(config, mode, force, cleanup, io) {
|
|
776
952
|
const repos = config.repoMappings.filter((mapping) => mapping.localPath?.trim());
|
|
777
953
|
if (repos.length === 0) {
|
|
778
954
|
io.stdout.write("No repos configured.\n");
|
|
@@ -781,20 +957,28 @@ async function cleanupRepos(config, mode, force, io) {
|
|
|
781
957
|
let failures = 0;
|
|
782
958
|
for (const repo of repos) {
|
|
783
959
|
const repoPath = path.resolve(repo.localPath);
|
|
784
|
-
const worktreeRoot = path.join(path.dirname(repoPath), ".forge-worktrees", path.basename(repoPath));
|
|
785
|
-
if (!force) {
|
|
786
|
-
const action = mode === "all" ? "remove" : "prune";
|
|
787
|
-
io.stdout.write(`[dry-run] Would ${action} ${worktreeRoot}\n`);
|
|
788
|
-
continue;
|
|
789
|
-
}
|
|
790
960
|
try {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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`);
|
|
795
980
|
}
|
|
796
981
|
else {
|
|
797
|
-
await runGit(repoPath, ["worktree", "prune"]);
|
|
798
982
|
io.stdout.write(`Pruned stale worktree metadata for ${repoPath}\n`);
|
|
799
983
|
}
|
|
800
984
|
}
|
|
@@ -862,18 +1046,10 @@ async function execVersionCommand(probe, timeout = 2000) {
|
|
|
862
1046
|
if (process.platform !== "win32") {
|
|
863
1047
|
throw error;
|
|
864
1048
|
}
|
|
865
|
-
|
|
1049
|
+
const launch = buildWindowsCmdLaunch(probe.command, probe.args);
|
|
1050
|
+
return await execFileAsync(launch.command, [...launch.args], { timeout });
|
|
866
1051
|
}
|
|
867
1052
|
}
|
|
868
|
-
function windowsCommandLine(probe) {
|
|
869
|
-
return [probe.command, ...probe.args].map(quoteWindowsCommandLineArg).join(" ");
|
|
870
|
-
}
|
|
871
|
-
function quoteWindowsCommandLineArg(value) {
|
|
872
|
-
if (value.length > 0 && !/[\s"]/.test(value)) {
|
|
873
|
-
return value;
|
|
874
|
-
}
|
|
875
|
-
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
|
|
876
|
-
}
|
|
877
1053
|
function versionLine(output) {
|
|
878
1054
|
const lines = output
|
|
879
1055
|
.trim()
|
|
@@ -951,8 +1127,8 @@ function uniqueModels(models) {
|
|
|
951
1127
|
function isRecord(value) {
|
|
952
1128
|
return typeof value === "object" && value !== null;
|
|
953
1129
|
}
|
|
954
|
-
async function withDetectedCliProviders(config) {
|
|
955
|
-
const detected = await detectCliProviders(
|
|
1130
|
+
async function withDetectedCliProviders(config, candidates = config.cliProviders) {
|
|
1131
|
+
const detected = await detectCliProviders(uniqueProviders(candidates));
|
|
956
1132
|
const found = detected.filter((result) => result.found);
|
|
957
1133
|
if (found.length === 0) {
|
|
958
1134
|
return config;
|
|
@@ -976,6 +1152,7 @@ async function withDetectedCliProviders(config) {
|
|
|
976
1152
|
};
|
|
977
1153
|
}
|
|
978
1154
|
async function withDetectedPairCliProviders(args) {
|
|
1155
|
+
const configured = configuredProviderCandidates(args.cliProviders);
|
|
979
1156
|
const detectedConfig = await withDetectedCliProviders({
|
|
980
1157
|
...defaultBridgeServiceConfig({
|
|
981
1158
|
serverUrl: args.serverUrl,
|
|
@@ -985,7 +1162,7 @@ async function withDetectedPairCliProviders(args) {
|
|
|
985
1162
|
cleanupWorktrees: args.cleanupWorktrees,
|
|
986
1163
|
defaultRepoRoot: args.repoRoot ?? defaultBridgeServiceConfig().defaultRepoRoot
|
|
987
1164
|
})
|
|
988
|
-
});
|
|
1165
|
+
}, configured);
|
|
989
1166
|
return {
|
|
990
1167
|
...args,
|
|
991
1168
|
cliProviders: detectedConfig.cliProviders
|
|
@@ -1000,6 +1177,3 @@ function uniqueProviders(providers) {
|
|
|
1000
1177
|
.filter((provider) => provider.length > 0)
|
|
1001
1178
|
.filter((provider, index, all) => all.findIndex((candidate) => candidate.toLowerCase() === provider.toLowerCase()) === index);
|
|
1002
1179
|
}
|
|
1003
|
-
async function runGit(cwd, args) {
|
|
1004
|
-
await execFileAsync("git", args, { cwd, timeout: 10_000 });
|
|
1005
|
-
}
|