@loucompanion/forge-bridge 0.1.3-dev.8777774c67bc → 0.1.3-dev.88e93f53db5b
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 +51 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +50 -3
- package/dist/bridge/bridge.service.js +730 -87
- package/dist/bridge/bridge.types.d.ts +63 -1
- 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 +37 -3
- package/dist/cli/cli.service.d.ts +30 -2
- package/dist/cli/cli.service.js +191 -27
- package/dist/cli/local-config.d.ts +4 -0
- package/dist/cli/local-config.js +41 -1
- 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 +384 -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 +194 -0
- package/dist/runtime/host-runtime-lease.js +2138 -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 +225 -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/image-policy.d.ts +3 -1
- package/dist/sandbox/image-policy.js +4 -2
- package/dist/sandbox/preflight.d.ts +49 -1
- package/dist/sandbox/preflight.js +313 -31
- package/dist/sandbox/process.js +125 -40
- package/dist/sandbox/repository-lock.js +41 -3
- package/dist/sandbox/runtime.d.ts +85 -4
- package/dist/sandbox/runtime.js +806 -126
- package/dist/sandbox/sandbox.types.d.ts +10 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.js +1 -1
- package/dist/sandbox/worker.js +2 -5
- package/dist/sandbox/workspace.service.d.ts +1 -0
- package/dist/sandbox/workspace.service.js +28 -5
- package/dist/session/managed-attempt-tools.d.ts +18 -0
- package/dist/session/managed-attempt-tools.js +854 -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 +233 -24
- package/dist/session/phase-completion.d.ts +11 -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 +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +21 -6
- 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 +108 -3
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +151 -21
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +14 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +105 -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.deterministic.js +47 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +23 -0
- package/dist/session/provider-cli/provider-cli.process.js +420 -32
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
- package/dist/session/provider-cli/provider-cli.prompts.js +99 -19
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -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 +20 -2
- package/dist/session/provider-cli/provider-cli.service.js +464 -102
- package/dist/session/provider-cli/provider-cli.types.d.ts +5 -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 +522 -41
- package/dist/session/session.types.d.ts +70 -0
- package/dist/session/session.types.js +76 -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/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/redaction/redaction.js +12 -2
- 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 +28 -1
- package/dist/worktree/dependency-warmer.js +308 -9
- 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 +9 -2
- package/dist/worktree/worktree.service.js +217 -48
- package/dist/worktree/worktree.types.d.ts +5 -1
- package/package.json +5 -3
- package/runtime/windows-job-supervisor.ps1 +444 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
package/dist/cli/cli.help.js
CHANGED
|
@@ -16,10 +16,11 @@ export function helpText() {
|
|
|
16
16
|
" connect * Start the runner bridge and wait for Forge dispatches",
|
|
17
17
|
" pair * Pair this machine with Forge and save runner credentials",
|
|
18
18
|
" status * Show config, runner identity, repos, provider CLIs, and updates",
|
|
19
|
+
" server * Show or change the saved Forge server URL",
|
|
19
20
|
" add-repo * Map a Forge repo label/name to a local checkout path",
|
|
20
21
|
" remove-repo * Remove a saved repo mapping",
|
|
21
22
|
" cleanup * Prune managed bridge worktrees",
|
|
22
|
-
" sandbox * Initialize, diagnose, or reap Docker sandboxes",
|
|
23
|
+
" sandbox * Initialize, diagnose, smoke-test, or reap Docker sandboxes",
|
|
23
24
|
" version * Print the installed bridge package version",
|
|
24
25
|
" help * Display help for command",
|
|
25
26
|
" run-once * Internal: run one dispatch packet from JSON",
|
|
@@ -30,6 +31,7 @@ export function helpText() {
|
|
|
30
31
|
" forge-bridge pair 1234-5678 --server https://srv1493013.tail66073d.ts.net:8443 --repo-root \"%USERPROFILE%\\ForgeRepos\"",
|
|
31
32
|
" forge-bridge connect",
|
|
32
33
|
" forge-bridge status --check-updates",
|
|
34
|
+
" forge-bridge server set http://localhost:5208",
|
|
33
35
|
" forge-bridge add-repo Forge D:\\Programming\\repos\\Forge",
|
|
34
36
|
" forge-bridge cleanup --stale",
|
|
35
37
|
"",
|
|
@@ -100,6 +102,23 @@ export function commandHelpText(command) {
|
|
|
100
102
|
" forge-bridge status --check-updates",
|
|
101
103
|
"",
|
|
102
104
|
].join("\n");
|
|
105
|
+
case "server":
|
|
106
|
+
return [
|
|
107
|
+
"Forge Bridge — server",
|
|
108
|
+
"Show or change the saved Forge server URL.",
|
|
109
|
+
"",
|
|
110
|
+
"Usage:",
|
|
111
|
+
" forge-bridge server show",
|
|
112
|
+
" forge-bridge server set <url>",
|
|
113
|
+
"",
|
|
114
|
+
"The saved runner credentials are retained. Pair again when switching to a different Forge deployment.",
|
|
115
|
+
"FORGE_URL overrides the saved server when that environment variable is set.",
|
|
116
|
+
"",
|
|
117
|
+
"Examples:",
|
|
118
|
+
" forge-bridge server set http://localhost:5208",
|
|
119
|
+
" forge-bridge server set https://srv1493013.tail66073d.ts.net:8443",
|
|
120
|
+
"",
|
|
121
|
+
].join("\n");
|
|
103
122
|
case "add-repo":
|
|
104
123
|
return [
|
|
105
124
|
"Forge Bridge — add-repo",
|
|
@@ -137,13 +156,28 @@ export function commandHelpText(command) {
|
|
|
137
156
|
case "sandbox":
|
|
138
157
|
return [
|
|
139
158
|
"Forge Bridge — sandbox",
|
|
140
|
-
"
|
|
159
|
+
"Configure local Docker sandbox placement or inspect a repository-owned sandbox contract.",
|
|
141
160
|
"",
|
|
142
161
|
"Usage:",
|
|
162
|
+
" forge-bridge sandbox on [--allow-open-public]",
|
|
163
|
+
" forge-bridge sandbox off",
|
|
164
|
+
" forge-bridge sandbox status",
|
|
143
165
|
" forge-bridge sandbox init [--repo <path>]",
|
|
144
166
|
" forge-bridge sandbox doctor [--repo <path>] [--require-pinned-base]",
|
|
167
|
+
" forge-bridge sandbox smoke [--repo <path>] [--report <path>] [--require-pinned-base] [--probe-loopback-port]",
|
|
145
168
|
" forge-bridge sandbox reap",
|
|
146
169
|
"",
|
|
170
|
+
"`sandbox on` keeps unrestricted public networking denied unless the explicit",
|
|
171
|
+
"developer-only --allow-open-public option is supplied. The local policy is",
|
|
172
|
+
"saved in bridge.json and takes effect after the connected bridge restarts.",
|
|
173
|
+
"Environment variables remain higher-priority operator overrides.",
|
|
174
|
+
"",
|
|
175
|
+
"The smoke command builds the repository Dockerfile, runs a disposable hardened",
|
|
176
|
+
"container, verifies exact-label teardown, and writes a redacted JSON report.",
|
|
177
|
+
"The optional port probe publishes 127.0.0.1::8080 only for the disposable fixture",
|
|
178
|
+
"and verifies Docker-assigned host-port discovery; normal sandboxes publish no ports.",
|
|
179
|
+
"Stop the connected bridge first so the Docker ownership lock can be acquired.",
|
|
180
|
+
"",
|
|
147
181
|
].join("\n");
|
|
148
182
|
case "run-once":
|
|
149
183
|
return [
|
|
@@ -160,7 +194,7 @@ export function commandHelpText(command) {
|
|
|
160
194
|
"Internal MCP server used by provider CLIs to report Forge phase artifacts.",
|
|
161
195
|
"",
|
|
162
196
|
"Usage:",
|
|
163
|
-
" forge-bridge mcp --worktree <path> --work-item-id <id> --phase <phase>",
|
|
197
|
+
" forge-bridge mcp --worktree <path> --work-item-id <id> --phase <phase> # includes ci-repair",
|
|
164
198
|
"",
|
|
165
199
|
].join("\n");
|
|
166
200
|
case "version":
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { BridgeService } from "../bridge/bridge.base.js";
|
|
2
2
|
import type { BridgeServiceConfig } from "../bridge/bridge.types.js";
|
|
3
|
+
import type { CleanupMode } from "../cleanup/cleanup.types.js";
|
|
4
|
+
import type { CleanupService } from "../cleanup/cleanup.base.js";
|
|
3
5
|
import type { SessionService } from "../session/session.base.js";
|
|
4
6
|
import type { CliIo, CliService } from "./cli.base.js";
|
|
5
7
|
export declare class ForgeBridgeCliService implements CliService {
|
|
6
8
|
private readonly sessions;
|
|
7
9
|
private readonly bridgeFactory;
|
|
8
10
|
private readonly pairRunner;
|
|
9
|
-
|
|
11
|
+
private readonly cleanupFactory;
|
|
12
|
+
constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService, pairRunner?: PairRunnerClient, cleanupFactory?: CleanupServiceFactory);
|
|
10
13
|
run(argv: readonly string[], io?: CliIo): Promise<number>;
|
|
11
14
|
}
|
|
12
15
|
export declare function parseArgs(argv: readonly string[]): {
|
|
@@ -34,6 +37,14 @@ export declare function parseArgs(argv: readonly string[]): {
|
|
|
34
37
|
readonly ok: true;
|
|
35
38
|
readonly command: "remove-repo";
|
|
36
39
|
readonly label: string;
|
|
40
|
+
} | {
|
|
41
|
+
readonly ok: true;
|
|
42
|
+
readonly command: "server-show";
|
|
43
|
+
readonly config: BridgeServiceConfig;
|
|
44
|
+
} | {
|
|
45
|
+
readonly ok: true;
|
|
46
|
+
readonly command: "server-set";
|
|
47
|
+
readonly serverUrl: string;
|
|
37
48
|
} | {
|
|
38
49
|
readonly ok: true;
|
|
39
50
|
readonly command: "status";
|
|
@@ -54,9 +65,26 @@ export declare function parseArgs(argv: readonly string[]): {
|
|
|
54
65
|
readonly command: "sandbox-doctor";
|
|
55
66
|
readonly repoPath: string;
|
|
56
67
|
readonly requirePinnedBase: boolean;
|
|
68
|
+
} | {
|
|
69
|
+
readonly ok: true;
|
|
70
|
+
readonly command: "sandbox-smoke";
|
|
71
|
+
readonly repoPath: string;
|
|
72
|
+
readonly reportPath?: string;
|
|
73
|
+
readonly requirePinnedBase: boolean;
|
|
74
|
+
readonly probeLoopbackPort: boolean;
|
|
57
75
|
} | {
|
|
58
76
|
readonly ok: true;
|
|
59
77
|
readonly command: "sandbox-reap";
|
|
78
|
+
} | {
|
|
79
|
+
readonly ok: true;
|
|
80
|
+
readonly command: "sandbox-on";
|
|
81
|
+
readonly allowOpenPublic: boolean;
|
|
82
|
+
} | {
|
|
83
|
+
readonly ok: true;
|
|
84
|
+
readonly command: "sandbox-off";
|
|
85
|
+
} | {
|
|
86
|
+
readonly ok: true;
|
|
87
|
+
readonly command: "sandbox-status";
|
|
60
88
|
} | {
|
|
61
89
|
readonly ok: false;
|
|
62
90
|
readonly error: string;
|
|
@@ -78,5 +106,5 @@ interface PairRunnerClaim {
|
|
|
78
106
|
type PairRunnerClient = (args: PairArgs) => Promise<PairRunnerClaim>;
|
|
79
107
|
export declare function parseCodexModelCatalog(raw: string): string[];
|
|
80
108
|
export declare function parseCursorModelList(raw: string): string[];
|
|
81
|
-
type
|
|
109
|
+
type CleanupServiceFactory = (stateRoot: string) => CleanupService;
|
|
82
110
|
export {};
|
package/dist/cli/cli.service.js
CHANGED
|
@@ -7,14 +7,17 @@ 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
9
|
import { supportsDraftImageAttachmentsFor } from "../shared/provider-capabilities.js";
|
|
10
|
-
import {
|
|
10
|
+
import { redactText } from "../shared/redaction/redaction.js";
|
|
11
11
|
import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
|
|
12
12
|
import { forgeBridgeVersion } from "../version.js";
|
|
13
|
-
import {
|
|
13
|
+
import { ManagedWorktreeCleanupService } from "../cleanup/cleanup.service.js";
|
|
14
|
+
import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, setSandboxPolicy, setServerUrl, writeLocalConfig } from "./local-config.js";
|
|
14
15
|
import { BridgeSessionService } from "../session/session.service.js";
|
|
16
|
+
import { redactRunnerDispatchEvent, redactRunOnceResultPacket } from "../session/session-redaction.js";
|
|
15
17
|
import { validateDockerfilePolicy, validateSandboxContract } from "../sandbox/contract.js";
|
|
16
18
|
import { DockerPreflight } from "../sandbox/preflight.js";
|
|
17
19
|
import { DockerSandboxRuntime } from "../sandbox/runtime.js";
|
|
20
|
+
import { DockerSandboxSmoke } from "../sandbox/smoke.js";
|
|
18
21
|
import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
|
|
19
22
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
20
23
|
import { commandHelpText, helpText } from "./cli.help.js";
|
|
@@ -24,10 +27,12 @@ export class ForgeBridgeCliService {
|
|
|
24
27
|
sessions;
|
|
25
28
|
bridgeFactory;
|
|
26
29
|
pairRunner;
|
|
27
|
-
|
|
30
|
+
cleanupFactory;
|
|
31
|
+
constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner, cleanupFactory = stateRoot => new ManagedWorktreeCleanupService(stateRoot)) {
|
|
28
32
|
this.sessions = sessions;
|
|
29
33
|
this.bridgeFactory = bridgeFactory;
|
|
30
34
|
this.pairRunner = pairRunner;
|
|
35
|
+
this.cleanupFactory = cleanupFactory;
|
|
31
36
|
}
|
|
32
37
|
async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
33
38
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
@@ -67,6 +72,10 @@ export class ForgeBridgeCliService {
|
|
|
67
72
|
process.once("SIGINT", stop);
|
|
68
73
|
process.once("SIGTERM", stop);
|
|
69
74
|
try {
|
|
75
|
+
if (config.cleanupWorktrees && config.repoMappings.some(mapping => mapping.localPath?.trim())) {
|
|
76
|
+
io.stdout.write("Cleaning stale managed worktrees before accepting dispatches.\n");
|
|
77
|
+
await cleanupRepos(config, "stale", true, this.cleanupFactory(config.sandboxStateRoot), io);
|
|
78
|
+
}
|
|
70
79
|
io.stdout.write(connectStartupText(config));
|
|
71
80
|
await this.bridgeFactory(config).run(controller.signal);
|
|
72
81
|
return 0;
|
|
@@ -122,12 +131,38 @@ export class ForgeBridgeCliService {
|
|
|
122
131
|
return 2;
|
|
123
132
|
}
|
|
124
133
|
}
|
|
134
|
+
if (args.command === "server-show") {
|
|
135
|
+
const saved = readLocalConfigSync().serverUrl ?? "not configured";
|
|
136
|
+
io.stdout.write(`Saved server: ${saved}\n`);
|
|
137
|
+
if (process.env.FORGE_URL?.trim()) {
|
|
138
|
+
io.stdout.write(`Effective server: ${process.env.FORGE_URL.trim()} (FORGE_URL override)\n`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
io.stdout.write(`Effective server: ${args.config.serverUrl || "not configured"}\n`);
|
|
142
|
+
}
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
if (args.command === "server-set") {
|
|
146
|
+
try {
|
|
147
|
+
const config = await setServerUrl(args.serverUrl);
|
|
148
|
+
io.stdout.write(`Saved Forge server: ${config.serverUrl}\n`);
|
|
149
|
+
io.stdout.write("Existing runner credentials were retained. Re-run `forge-bridge pair --server <url>` if this is a different Forge deployment.\n");
|
|
150
|
+
if (process.env.FORGE_URL?.trim()) {
|
|
151
|
+
io.stdout.write(`Warning: FORGE_URL is set to ${process.env.FORGE_URL.trim()} and overrides the saved server.\n`);
|
|
152
|
+
}
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
157
|
+
return 2;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
125
160
|
if (args.command === "status") {
|
|
126
161
|
await writeStatus(args.config, args.checkUpdates, io);
|
|
127
162
|
return 0;
|
|
128
163
|
}
|
|
129
164
|
if (args.command === "cleanup") {
|
|
130
|
-
return await cleanupRepos(args.config, args.mode, args.force, io);
|
|
165
|
+
return await cleanupRepos(args.config, args.mode, args.force, this.cleanupFactory(args.config.sandboxStateRoot), io);
|
|
131
166
|
}
|
|
132
167
|
if (args.command === "sandbox-init") {
|
|
133
168
|
return await initializeSandbox(args.repoPath, io);
|
|
@@ -135,9 +170,28 @@ export class ForgeBridgeCliService {
|
|
|
135
170
|
if (args.command === "sandbox-doctor") {
|
|
136
171
|
return await diagnoseSandbox(args.repoPath, args.requirePinnedBase, io);
|
|
137
172
|
}
|
|
173
|
+
if (args.command === "sandbox-smoke") {
|
|
174
|
+
return await smokeSandbox(args.repoPath, args.reportPath, args.requirePinnedBase, args.probeLoopbackPort, io);
|
|
175
|
+
}
|
|
138
176
|
if (args.command === "sandbox-reap") {
|
|
139
177
|
return await reapSandboxes(io);
|
|
140
178
|
}
|
|
179
|
+
if (args.command === "sandbox-on") {
|
|
180
|
+
const saved = await setSandboxPolicy(true, args.allowOpenPublic);
|
|
181
|
+
io.stdout.write("Saved local Docker sandbox policy. Restart the bridge to apply it.\n");
|
|
182
|
+
writeSandboxPolicyStatus(saved, io);
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
if (args.command === "sandbox-off") {
|
|
186
|
+
const saved = await setSandboxPolicy(false, false);
|
|
187
|
+
io.stdout.write("Disabled local Docker sandbox placement. Restart the bridge to apply it.\n");
|
|
188
|
+
writeSandboxPolicyStatus(saved, io);
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
if (args.command === "sandbox-status") {
|
|
192
|
+
writeSandboxPolicyStatus(readLocalConfigSync(), io);
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
141
195
|
if (args.command === "mcp") {
|
|
142
196
|
const parsed = parseCompletionMcpArgs(args.argv);
|
|
143
197
|
if (!parsed.ok) {
|
|
@@ -156,12 +210,12 @@ export class ForgeBridgeCliService {
|
|
|
156
210
|
emit: async (event) => {
|
|
157
211
|
const line = {
|
|
158
212
|
type: "runner_event",
|
|
159
|
-
event:
|
|
213
|
+
event: redactRunnerDispatchEvent(event, redactionSecrets)
|
|
160
214
|
};
|
|
161
215
|
io.stdout.write(`${JSON.stringify(line)}\n`);
|
|
162
216
|
}
|
|
163
217
|
});
|
|
164
|
-
await writeJsonAtomic(args.resultPath,
|
|
218
|
+
await writeJsonAtomic(args.resultPath, redactRunOnceResultPacket(result, redactionSecrets));
|
|
165
219
|
return result.exitCode;
|
|
166
220
|
}
|
|
167
221
|
catch (error) {
|
|
@@ -247,6 +301,15 @@ export function parseArgs(argv) {
|
|
|
247
301
|
}
|
|
248
302
|
return { ok: true, command: "remove-repo", label: argv[1] };
|
|
249
303
|
}
|
|
304
|
+
if (argv[0] === "server") {
|
|
305
|
+
if (argv.length === 1 || (argv[1] === "show" && argv.length === 2)) {
|
|
306
|
+
return { ok: true, command: "server-show", config: configuredBridgeServiceConfig() };
|
|
307
|
+
}
|
|
308
|
+
if (argv[1] === "set" && argv[2] && argv.length === 3) {
|
|
309
|
+
return { ok: true, command: "server-set", serverUrl: argv[2] };
|
|
310
|
+
}
|
|
311
|
+
return { ok: false, error: "Usage: forge-bridge server <show|set <url>>" };
|
|
312
|
+
}
|
|
250
313
|
if (argv[0] === "status") {
|
|
251
314
|
let checkUpdates = false;
|
|
252
315
|
for (let index = 1; index < argv.length; index += 1) {
|
|
@@ -301,8 +364,26 @@ export function parseArgs(argv) {
|
|
|
301
364
|
}
|
|
302
365
|
function parseSandboxArgs(argv) {
|
|
303
366
|
const action = argv[1];
|
|
367
|
+
if (action === "on") {
|
|
368
|
+
let allowOpenPublic = false;
|
|
369
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
370
|
+
if (argv[index] === "--allow-open-public") {
|
|
371
|
+
allowOpenPublic = true;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
return { ok: false, error: `Unknown sandbox argument: ${argv[index]}` };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return { ok: true, command: "sandbox-on", allowOpenPublic };
|
|
378
|
+
}
|
|
379
|
+
if (action === "off" && argv.length === 2)
|
|
380
|
+
return { ok: true, command: "sandbox-off" };
|
|
381
|
+
if (action === "status" && argv.length === 2)
|
|
382
|
+
return { ok: true, command: "sandbox-status" };
|
|
304
383
|
let repoPath = process.cwd();
|
|
384
|
+
let reportPath;
|
|
305
385
|
let requirePinnedBase = false;
|
|
386
|
+
let probeLoopbackPort = false;
|
|
306
387
|
for (let index = 2; index < argv.length; index += 1) {
|
|
307
388
|
const arg = argv[index];
|
|
308
389
|
if (arg === "--repo") {
|
|
@@ -310,9 +391,17 @@ function parseSandboxArgs(argv) {
|
|
|
310
391
|
if (!repoPath)
|
|
311
392
|
return { ok: false, error: "--repo requires a path." };
|
|
312
393
|
}
|
|
313
|
-
else if (arg === "--
|
|
394
|
+
else if (arg === "--report" && action === "smoke") {
|
|
395
|
+
reportPath = argv[++index];
|
|
396
|
+
if (!reportPath)
|
|
397
|
+
return { ok: false, error: "--report requires a path." };
|
|
398
|
+
}
|
|
399
|
+
else if (arg === "--require-pinned-base" && (action === "doctor" || action === "smoke")) {
|
|
314
400
|
requirePinnedBase = true;
|
|
315
401
|
}
|
|
402
|
+
else if (arg === "--probe-loopback-port" && action === "smoke") {
|
|
403
|
+
probeLoopbackPort = true;
|
|
404
|
+
}
|
|
316
405
|
else {
|
|
317
406
|
return { ok: false, error: `Unknown sandbox argument: ${arg}` };
|
|
318
407
|
}
|
|
@@ -321,9 +410,18 @@ function parseSandboxArgs(argv) {
|
|
|
321
410
|
return { ok: true, command: "sandbox-init", repoPath: path.resolve(repoPath) };
|
|
322
411
|
if (action === "doctor")
|
|
323
412
|
return { ok: true, command: "sandbox-doctor", repoPath: path.resolve(repoPath), requirePinnedBase };
|
|
413
|
+
if (action === "smoke")
|
|
414
|
+
return {
|
|
415
|
+
ok: true,
|
|
416
|
+
command: "sandbox-smoke",
|
|
417
|
+
repoPath: path.resolve(repoPath),
|
|
418
|
+
reportPath: reportPath ? path.resolve(reportPath) : undefined,
|
|
419
|
+
requirePinnedBase,
|
|
420
|
+
probeLoopbackPort,
|
|
421
|
+
};
|
|
324
422
|
if (action === "reap" && argv.length === 2)
|
|
325
423
|
return { ok: true, command: "sandbox-reap" };
|
|
326
|
-
return { ok: false, error: "Usage: forge-bridge sandbox <init|doctor|reap> [--repo <path>] [--require-pinned-base]" };
|
|
424
|
+
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]" };
|
|
327
425
|
}
|
|
328
426
|
const sandboxContractTemplate = `schemaVersion: 1
|
|
329
427
|
workingDirectory: /workspace
|
|
@@ -389,6 +487,10 @@ async function diagnoseSandbox(repoPath, requirePinnedBase, io) {
|
|
|
389
487
|
diskHighWatermarkPercent: config.diskHighWatermarkPercent,
|
|
390
488
|
diskLowWatermarkPercent: config.diskLowWatermarkPercent,
|
|
391
489
|
minimumWorkspaceBytes: config.minimumWorkspaceBytes,
|
|
490
|
+
hostMemoryReserveBytes: config.sandboxHostMemoryReserveBytes,
|
|
491
|
+
defaultWorkerMemoryLimitBytes: config.defaultSandboxWorkerMemoryLimitBytes,
|
|
492
|
+
defaultServiceMemoryLimitBytes: config.defaultSandboxServiceMemoryLimitBytes,
|
|
493
|
+
runtimeOverheadBytes: config.sandboxRuntimeOverheadBytes,
|
|
392
494
|
requireRootless: config.requireRootlessDocker
|
|
393
495
|
});
|
|
394
496
|
try {
|
|
@@ -409,6 +511,42 @@ async function diagnoseSandbox(repoPath, requirePinnedBase, io) {
|
|
|
409
511
|
return 1;
|
|
410
512
|
}
|
|
411
513
|
}
|
|
514
|
+
async function smokeSandbox(repoPath, reportPath, requirePinnedBase, probeLoopbackPort, io) {
|
|
515
|
+
try {
|
|
516
|
+
const result = await new DockerSandboxSmoke(defaultBridgeServiceConfig()).run({
|
|
517
|
+
repoPath,
|
|
518
|
+
reportPath,
|
|
519
|
+
requirePinnedBase,
|
|
520
|
+
probeLoopbackPort,
|
|
521
|
+
onProgress: (stage) => io.stdout.write(`Smoke: ${stage}...\n`),
|
|
522
|
+
});
|
|
523
|
+
const { report } = result;
|
|
524
|
+
if (report.repository.contractDigest)
|
|
525
|
+
io.stdout.write(`Contract: valid (${report.repository.contractDigest})\n`);
|
|
526
|
+
for (const warning of report.repository.policyWarnings)
|
|
527
|
+
io.stdout.write(`Warning: ${warning}\n`);
|
|
528
|
+
if (report.docker.runtimeVersion) {
|
|
529
|
+
io.stdout.write(`Docker: healthy (${report.docker.runtimeVersion}; capacity ${report.docker.capacity})\n`);
|
|
530
|
+
}
|
|
531
|
+
io.stdout.write(`Attempt: ${report.dispatchAttemptId}\n`);
|
|
532
|
+
io.stdout.write(`Resource lease: ${report.resourceLeaseId}\n`);
|
|
533
|
+
if (report.publishedPortProbe) {
|
|
534
|
+
io.stdout.write(`Loopback port: ${report.publishedPortProbe.hostIp}:${report.publishedPortProbe.hostPort} -> ${report.publishedPortProbe.containerPort}/tcp\n`);
|
|
535
|
+
}
|
|
536
|
+
io.stdout.write(`Cleanup: ${report.cleanup.verified ? "verified (zero exact-label resources)" : "failed"}\n`);
|
|
537
|
+
io.stdout.write(`Report: ${result.reportPath}\n`);
|
|
538
|
+
if (report.status === "passed") {
|
|
539
|
+
io.stdout.write("Sandbox smoke: PASS\n");
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
io.stderr.write(`Sandbox smoke: FAIL (${report.failure?.code ?? "sandbox_smoke_failed"} at ${report.failure?.stage ?? "unknown"}): ${report.failure?.message ?? "unknown failure"}\n`);
|
|
543
|
+
return 1;
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
io.stderr.write(`Sandbox smoke could not write its report: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
547
|
+
return 1;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
412
550
|
async function reapSandboxes(io) {
|
|
413
551
|
const config = defaultBridgeServiceConfig();
|
|
414
552
|
if (!/^[0-9a-f-]{32,36}$/iu.test(config.runnerId)) {
|
|
@@ -423,6 +561,10 @@ async function reapSandboxes(io) {
|
|
|
423
561
|
diskHighWatermarkPercent: config.diskHighWatermarkPercent,
|
|
424
562
|
diskLowWatermarkPercent: config.diskLowWatermarkPercent,
|
|
425
563
|
minimumWorkspaceBytes: config.minimumWorkspaceBytes,
|
|
564
|
+
hostMemoryReserveBytes: config.sandboxHostMemoryReserveBytes,
|
|
565
|
+
defaultWorkerMemoryLimitBytes: config.defaultSandboxWorkerMemoryLimitBytes,
|
|
566
|
+
defaultServiceMemoryLimitBytes: config.defaultSandboxServiceMemoryLimitBytes,
|
|
567
|
+
runtimeOverheadBytes: config.sandboxRuntimeOverheadBytes,
|
|
426
568
|
requireRootless: config.requireRootlessDocker
|
|
427
569
|
});
|
|
428
570
|
try {
|
|
@@ -576,7 +718,7 @@ function parsePairArgs(argv) {
|
|
|
576
718
|
name: name?.trim() || defaultName,
|
|
577
719
|
fingerprint: fingerprint?.trim() || `local-bridge:${os.hostname()}:${user}`,
|
|
578
720
|
repoRoot: repoRoot?.trim() ? path.resolve(repoRoot) : undefined,
|
|
579
|
-
cleanupWorktrees: cleanupWorktrees ??
|
|
721
|
+
cleanupWorktrees: cleanupWorktrees ?? defaultConfig.cleanupWorktrees,
|
|
580
722
|
cliProviders: cliProviders.length > 0 ? cliProviders : localConfig.cliProviders && localConfig.cliProviders.length > 0 ? [...localConfig.cliProviders] : ["codex"],
|
|
581
723
|
projectLabels: projectLabels.length > 0 ? projectLabels : localConfig.projectLabels ? [...localConfig.projectLabels] : []
|
|
582
724
|
};
|
|
@@ -731,11 +873,27 @@ function connectStartupText(config) {
|
|
|
731
873
|
` repo folder: ${config.defaultRepoRoot || "not configured"}`,
|
|
732
874
|
` providers: ${formatList(config.cliProviders)}`,
|
|
733
875
|
` cleanup worktrees: ${config.cleanupWorktrees ? "enabled" : "disabled"}`,
|
|
876
|
+
` open public sandbox networking: ${config.allowOpenPublicSandbox ? "enabled" : "disabled"}`,
|
|
734
877
|
"",
|
|
735
878
|
"Press Ctrl+C to stop.",
|
|
736
879
|
"",
|
|
737
880
|
].join("\n");
|
|
738
881
|
}
|
|
882
|
+
function writeSandboxPolicyStatus(local, io) {
|
|
883
|
+
const effective = configuredBridgeServiceConfig({}, local);
|
|
884
|
+
io.stdout.write(`Config: ${resolveLocalConfigPath()}\n`);
|
|
885
|
+
io.stdout.write(`Docker sandbox placement: ${effective.dockerEnabled ? "on" : "off"}\n`);
|
|
886
|
+
io.stdout.write(`Open public sandbox networking: ${effective.allowOpenPublicSandbox ? "allowed" : "denied"}\n`);
|
|
887
|
+
if (process.env.FORGE_DOCKER_SANDBOX !== undefined) {
|
|
888
|
+
io.stdout.write("Docker setting source: FORGE_DOCKER_SANDBOX override\n");
|
|
889
|
+
}
|
|
890
|
+
if (process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX !== undefined) {
|
|
891
|
+
io.stdout.write("Open public setting source: FORGE_ALLOW_OPEN_PUBLIC_SANDBOX override\n");
|
|
892
|
+
}
|
|
893
|
+
if (effective.allowOpenPublicSandbox) {
|
|
894
|
+
io.stdout.write("Warning: open_public permits unrestricted outbound network access from developer sandboxes.\n");
|
|
895
|
+
}
|
|
896
|
+
}
|
|
739
897
|
function formatList(values) {
|
|
740
898
|
return values.length > 0 ? values.join(", ") : "none";
|
|
741
899
|
}
|
|
@@ -772,7 +930,7 @@ async function writeStatus(config, checkUpdates, io) {
|
|
|
772
930
|
io.stdout.write("Update check: no update channel configured.\n");
|
|
773
931
|
}
|
|
774
932
|
}
|
|
775
|
-
async function cleanupRepos(config, mode, force, io) {
|
|
933
|
+
async function cleanupRepos(config, mode, force, cleanup, io) {
|
|
776
934
|
const repos = config.repoMappings.filter((mapping) => mapping.localPath?.trim());
|
|
777
935
|
if (repos.length === 0) {
|
|
778
936
|
io.stdout.write("No repos configured.\n");
|
|
@@ -781,20 +939,28 @@ async function cleanupRepos(config, mode, force, io) {
|
|
|
781
939
|
let failures = 0;
|
|
782
940
|
for (const repo of repos) {
|
|
783
941
|
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
942
|
try {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
943
|
+
const result = await cleanup.run({
|
|
944
|
+
repoRoot: repoPath,
|
|
945
|
+
allowedRepoRoot: config.defaultRepoRoot || undefined,
|
|
946
|
+
mode,
|
|
947
|
+
force,
|
|
948
|
+
});
|
|
949
|
+
if (!force) {
|
|
950
|
+
const action = mode === "all"
|
|
951
|
+
? "remove"
|
|
952
|
+
: mode === "stale"
|
|
953
|
+
? `remove ${result.candidateWorktrees} stale managed ${result.candidateWorktrees === 1 ? "worktree" : "worktrees"} from`
|
|
954
|
+
: "prune";
|
|
955
|
+
io.stdout.write(`[dry-run] Would ${action} ${result.worktreeRoot}\n`);
|
|
956
|
+
}
|
|
957
|
+
else if (mode === "all") {
|
|
958
|
+
io.stdout.write(`Removed ${result.worktreeRoot}\n`);
|
|
959
|
+
}
|
|
960
|
+
else if (mode === "stale") {
|
|
961
|
+
io.stdout.write(`Removed ${result.removedWorktrees} stale managed ${result.removedWorktrees === 1 ? "worktree" : "worktrees"} from ${result.worktreeRoot}\n`);
|
|
795
962
|
}
|
|
796
963
|
else {
|
|
797
|
-
await runGit(repoPath, ["worktree", "prune"]);
|
|
798
964
|
io.stdout.write(`Pruned stale worktree metadata for ${repoPath}\n`);
|
|
799
965
|
}
|
|
800
966
|
}
|
|
@@ -943,8 +1109,8 @@ function uniqueModels(models) {
|
|
|
943
1109
|
function isRecord(value) {
|
|
944
1110
|
return typeof value === "object" && value !== null;
|
|
945
1111
|
}
|
|
946
|
-
async function withDetectedCliProviders(config) {
|
|
947
|
-
const detected = await detectCliProviders(
|
|
1112
|
+
async function withDetectedCliProviders(config, candidates = config.cliProviders) {
|
|
1113
|
+
const detected = await detectCliProviders(uniqueProviders(candidates));
|
|
948
1114
|
const found = detected.filter((result) => result.found);
|
|
949
1115
|
if (found.length === 0) {
|
|
950
1116
|
return config;
|
|
@@ -968,6 +1134,7 @@ async function withDetectedCliProviders(config) {
|
|
|
968
1134
|
};
|
|
969
1135
|
}
|
|
970
1136
|
async function withDetectedPairCliProviders(args) {
|
|
1137
|
+
const configured = configuredProviderCandidates(args.cliProviders);
|
|
971
1138
|
const detectedConfig = await withDetectedCliProviders({
|
|
972
1139
|
...defaultBridgeServiceConfig({
|
|
973
1140
|
serverUrl: args.serverUrl,
|
|
@@ -977,7 +1144,7 @@ async function withDetectedPairCliProviders(args) {
|
|
|
977
1144
|
cleanupWorktrees: args.cleanupWorktrees,
|
|
978
1145
|
defaultRepoRoot: args.repoRoot ?? defaultBridgeServiceConfig().defaultRepoRoot
|
|
979
1146
|
})
|
|
980
|
-
});
|
|
1147
|
+
}, configured);
|
|
981
1148
|
return {
|
|
982
1149
|
...args,
|
|
983
1150
|
cliProviders: detectedConfig.cliProviders
|
|
@@ -992,6 +1159,3 @@ function uniqueProviders(providers) {
|
|
|
992
1159
|
.filter((provider) => provider.length > 0)
|
|
993
1160
|
.filter((provider, index, all) => all.findIndex((candidate) => candidate.toLowerCase() === provider.toLowerCase()) === index);
|
|
994
1161
|
}
|
|
995
|
-
async function runGit(cwd, args) {
|
|
996
|
-
await execFileAsync("git", args, { cwd, timeout: 10_000 });
|
|
997
|
-
}
|
|
@@ -7,6 +7,8 @@ export interface BridgeLocalConfig {
|
|
|
7
7
|
readonly deviceName?: string;
|
|
8
8
|
readonly defaultRepoRoot?: string;
|
|
9
9
|
readonly cleanupWorktrees?: boolean;
|
|
10
|
+
readonly dockerEnabled?: boolean;
|
|
11
|
+
readonly allowOpenPublicSandbox?: boolean;
|
|
10
12
|
readonly cliProviders?: readonly string[];
|
|
11
13
|
readonly providerStatuses?: Readonly<Record<string, BridgeProviderStatus>>;
|
|
12
14
|
readonly projectLabels?: readonly string[];
|
|
@@ -34,4 +36,6 @@ export declare function removeRepoMapping(label: string): Promise<{
|
|
|
34
36
|
readonly config: BridgeLocalConfig;
|
|
35
37
|
readonly removed: boolean;
|
|
36
38
|
}>;
|
|
39
|
+
export declare function setServerUrl(serverUrl: string): Promise<BridgeLocalConfig>;
|
|
40
|
+
export declare function setSandboxPolicy(dockerEnabled: boolean, allowOpenPublicSandbox: boolean): Promise<BridgeLocalConfig>;
|
|
37
41
|
export declare function providerStatusesFor(providers: readonly string[]): Readonly<Record<string, BridgeProviderStatus>>;
|
package/dist/cli/local-config.js
CHANGED
|
@@ -45,6 +45,12 @@ export function applyLocalConfigDefaults(config, local) {
|
|
|
45
45
|
deviceName: process.env.FORGE_DEVICE_NAME ?? local.deviceName ?? config.deviceName,
|
|
46
46
|
defaultRepoRoot: process.env.FORGE_REPO_ROOT ?? local.defaultRepoRoot ?? config.defaultRepoRoot,
|
|
47
47
|
cleanupWorktrees: local.cleanupWorktrees ?? config.cleanupWorktrees,
|
|
48
|
+
dockerEnabled: process.env.FORGE_DOCKER_SANDBOX === undefined
|
|
49
|
+
? local.dockerEnabled ?? config.dockerEnabled
|
|
50
|
+
: config.dockerEnabled,
|
|
51
|
+
allowOpenPublicSandbox: process.env.FORGE_ALLOW_OPEN_PUBLIC_SANDBOX === undefined
|
|
52
|
+
? local.allowOpenPublicSandbox ?? config.allowOpenPublicSandbox
|
|
53
|
+
: config.allowOpenPublicSandbox,
|
|
48
54
|
cliProviders,
|
|
49
55
|
providerStatuses: normalizeProviderStatuses(local.providerStatuses) ?? providerStatusesFor(cliProviders),
|
|
50
56
|
projectLabels: local.projectLabels ?? config.projectLabels,
|
|
@@ -88,6 +94,19 @@ export async function removeRepoMapping(label) {
|
|
|
88
94
|
await writeLocalConfig(next);
|
|
89
95
|
return { config: next, removed: after.length !== before.length };
|
|
90
96
|
}
|
|
97
|
+
export async function setServerUrl(serverUrl) {
|
|
98
|
+
const normalized = normalizeServerUrl(serverUrl);
|
|
99
|
+
const current = await readLocalConfig();
|
|
100
|
+
const next = normalizeLocalConfig({ ...current, serverUrl: normalized });
|
|
101
|
+
await writeLocalConfig(next);
|
|
102
|
+
return next;
|
|
103
|
+
}
|
|
104
|
+
export async function setSandboxPolicy(dockerEnabled, allowOpenPublicSandbox) {
|
|
105
|
+
const current = await readLocalConfig();
|
|
106
|
+
const next = normalizeLocalConfig({ ...current, dockerEnabled, allowOpenPublicSandbox });
|
|
107
|
+
await writeLocalConfig(next);
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
91
110
|
export function providerStatusesFor(providers) {
|
|
92
111
|
return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
|
|
93
112
|
}
|
|
@@ -106,6 +125,8 @@ function normalizeLocalConfig(config) {
|
|
|
106
125
|
deviceName: cleanString(config.deviceName),
|
|
107
126
|
defaultRepoRoot: cleanString(config.defaultRepoRoot),
|
|
108
127
|
cleanupWorktrees: config.cleanupWorktrees,
|
|
128
|
+
dockerEnabled: config.dockerEnabled,
|
|
129
|
+
allowOpenPublicSandbox: config.allowOpenPublicSandbox,
|
|
109
130
|
cliProviders: cleanList(config.cliProviders),
|
|
110
131
|
providerStatuses: normalizeProviderStatuses(config.providerStatuses),
|
|
111
132
|
projectLabels: cleanList(config.projectLabels),
|
|
@@ -200,10 +221,11 @@ function normalizeQaAppLaunchTarget(value) {
|
|
|
200
221
|
command: cleanString(value.command),
|
|
201
222
|
working_directory: cleanString(value.working_directory),
|
|
202
223
|
ready_url: cleanString(value.ready_url),
|
|
224
|
+
managed_runtime_port: cleanString(value.managed_runtime_port),
|
|
203
225
|
routes,
|
|
204
226
|
notes: cleanString(value.notes)
|
|
205
227
|
};
|
|
206
|
-
return normalized.url || normalized.command || normalized.ready_url ||
|
|
228
|
+
return normalized.url || normalized.command || normalized.ready_url || normalized.managed_runtime_port
|
|
207
229
|
? normalized
|
|
208
230
|
: undefined;
|
|
209
231
|
}
|
|
@@ -211,6 +233,24 @@ function cleanString(value) {
|
|
|
211
233
|
const clean = value?.trim();
|
|
212
234
|
return clean ? clean : undefined;
|
|
213
235
|
}
|
|
236
|
+
function normalizeServerUrl(value) {
|
|
237
|
+
const clean = value.trim();
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = new URL(clean);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
throw new Error(`Invalid Forge server URL: ${value}`);
|
|
244
|
+
}
|
|
245
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
246
|
+
throw new Error("Forge server URL must use http or https.");
|
|
247
|
+
}
|
|
248
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
249
|
+
throw new Error("Forge server URL cannot include credentials, a query string, or a fragment.");
|
|
250
|
+
}
|
|
251
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/u, "") || "/";
|
|
252
|
+
return parsed.toString().replace(/\/$/u, "");
|
|
253
|
+
}
|
|
214
254
|
function cleanList(values) {
|
|
215
255
|
const clean = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
|
|
216
256
|
return clean.length > 0 ? clean : undefined;
|