@loucompanion/forge-bridge 0.1.3-dev.bd0c67bbf352 → 0.1.3-dev.c24058d57ab3
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 +44 -0
- package/dist/bridge/bridge.base.d.ts +5 -1
- package/dist/bridge/bridge.service.d.ts +36 -2
- package/dist/bridge/bridge.service.js +588 -18
- package/dist/bridge/bridge.types.d.ts +114 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +183 -0
- package/dist/cli/cli.service.d.ts +12 -0
- package/dist/cli/cli.service.js +176 -184
- package/dist/cli/local-config.d.ts +2 -0
- package/dist/cli/local-config.js +6 -2
- 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 +36 -0
- package/dist/sandbox/preflight.js +239 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +124 -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 +83 -0
- package/dist/sandbox/runtime.d.ts +234 -0
- package/dist/sandbox/runtime.js +1767 -0
- package/dist/sandbox/sandbox.types.d.ts +83 -0
- package/dist/sandbox/sandbox.types.js +1 -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 +228 -0
- package/dist/sandbox/workspace.service.d.ts +12 -0
- package/dist/sandbox/workspace.service.js +49 -0
- package/dist/session/phase-completion.artifacts.d.ts +10 -0
- package/dist/session/phase-completion.artifacts.js +620 -0
- package/dist/session/phase-completion.d.ts +32 -44
- package/dist/session/phase-completion.helpers.d.ts +112 -0
- package/dist/session/phase-completion.helpers.js +660 -0
- package/dist/session/phase-completion.js +28 -3501
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +220 -0
- package/dist/session/phase-completion.runtime.d.ts +36 -0
- package/dist/session/phase-completion.runtime.js +609 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +707 -0
- package/dist/session/phase-completion.validation.d.ts +34 -0
- package/dist/session/phase-completion.validation.js +724 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +754 -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 +329 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +32 -0
- package/dist/session/provider-cli/provider-cli.process.js +287 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +44 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +528 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +65 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +40 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +9 -53
- package/dist/session/provider-cli/provider-cli.service.js +161 -2561
- package/dist/session/provider-cli/provider-cli.types.d.ts +1 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +20 -0
- package/dist/session/provider-cli/provider-output-state.js +445 -0
- package/dist/session/session.service.js +34 -24
- package/dist/session/session.types.d.ts +89 -1
- package/dist/session/session.types.js +209 -3
- 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 +4 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +11 -0
- package/dist/worktree/dependency-warmer.js +45 -1
- package/package.json +4 -2
package/dist/cli/cli.service.js
CHANGED
|
@@ -5,13 +5,19 @@ 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 { buildWindowsCmdLaunch } from "../shared/process/windows-launch.js";
|
|
9
|
+
import { supportsDraftImageAttachmentsFor } from "../shared/provider-capabilities.js";
|
|
8
10
|
import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
|
|
9
11
|
import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
|
|
10
12
|
import { forgeBridgeVersion } from "../version.js";
|
|
11
13
|
import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, writeLocalConfig } from "./local-config.js";
|
|
12
14
|
import { BridgeSessionService } from "../session/session.service.js";
|
|
15
|
+
import { validateDockerfilePolicy, validateSandboxContract } from "../sandbox/contract.js";
|
|
16
|
+
import { DockerPreflight } from "../sandbox/preflight.js";
|
|
17
|
+
import { DockerSandboxRuntime } from "../sandbox/runtime.js";
|
|
13
18
|
import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
|
|
14
19
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
20
|
+
import { commandHelpText, helpText } from "./cli.help.js";
|
|
15
21
|
const apiKeySecretGrantEnv = "FORGE_API_KEY_SECRET_GRANT";
|
|
16
22
|
const execFileAsync = promisify(execFile);
|
|
17
23
|
export class ForgeBridgeCliService {
|
|
@@ -86,6 +92,7 @@ export class ForgeBridgeCliService {
|
|
|
86
92
|
if (args.command === "pair") {
|
|
87
93
|
try {
|
|
88
94
|
const current = readLocalConfigSync();
|
|
95
|
+
io.stdout.write(`Pairing with server: ${args.serverUrl}\n`);
|
|
89
96
|
const code = await resolvePairingCode(args.code);
|
|
90
97
|
const repoRoot = await resolvePairRepoRoot(args.repoRoot, current.defaultRepoRoot);
|
|
91
98
|
await fs.mkdir(repoRoot, { recursive: true });
|
|
@@ -122,6 +129,15 @@ export class ForgeBridgeCliService {
|
|
|
122
129
|
if (args.command === "cleanup") {
|
|
123
130
|
return await cleanupRepos(args.config, args.mode, args.force, io);
|
|
124
131
|
}
|
|
132
|
+
if (args.command === "sandbox-init") {
|
|
133
|
+
return await initializeSandbox(args.repoPath, io);
|
|
134
|
+
}
|
|
135
|
+
if (args.command === "sandbox-doctor") {
|
|
136
|
+
return await diagnoseSandbox(args.repoPath, args.requirePinnedBase, io);
|
|
137
|
+
}
|
|
138
|
+
if (args.command === "sandbox-reap") {
|
|
139
|
+
return await reapSandboxes(io);
|
|
140
|
+
}
|
|
125
141
|
if (args.command === "mcp") {
|
|
126
142
|
const parsed = parseCompletionMcpArgs(args.argv);
|
|
127
143
|
if (!parsed.ok) {
|
|
@@ -250,10 +266,13 @@ export function parseArgs(argv) {
|
|
|
250
266
|
if (argv[0] === "mcp") {
|
|
251
267
|
return { ok: true, command: "mcp", argv };
|
|
252
268
|
}
|
|
269
|
+
if (argv[0] === "sandbox") {
|
|
270
|
+
return parseSandboxArgs(argv);
|
|
271
|
+
}
|
|
253
272
|
if (argv[0] !== "run-once") {
|
|
254
273
|
return {
|
|
255
274
|
ok: false,
|
|
256
|
-
error:
|
|
275
|
+
error: helpText()
|
|
257
276
|
};
|
|
258
277
|
}
|
|
259
278
|
let dispatchPath = null;
|
|
@@ -280,6 +299,151 @@ export function parseArgs(argv) {
|
|
|
280
299
|
resultPath: path.resolve(resultPath)
|
|
281
300
|
};
|
|
282
301
|
}
|
|
302
|
+
function parseSandboxArgs(argv) {
|
|
303
|
+
const action = argv[1];
|
|
304
|
+
let repoPath = process.cwd();
|
|
305
|
+
let requirePinnedBase = false;
|
|
306
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
307
|
+
const arg = argv[index];
|
|
308
|
+
if (arg === "--repo") {
|
|
309
|
+
repoPath = argv[++index] ?? "";
|
|
310
|
+
if (!repoPath)
|
|
311
|
+
return { ok: false, error: "--repo requires a path." };
|
|
312
|
+
}
|
|
313
|
+
else if (arg === "--require-pinned-base" && action === "doctor") {
|
|
314
|
+
requirePinnedBase = true;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
return { ok: false, error: `Unknown sandbox argument: ${arg}` };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (action === "init")
|
|
321
|
+
return { ok: true, command: "sandbox-init", repoPath: path.resolve(repoPath) };
|
|
322
|
+
if (action === "doctor")
|
|
323
|
+
return { ok: true, command: "sandbox-doctor", repoPath: path.resolve(repoPath), requirePinnedBase };
|
|
324
|
+
if (action === "reap" && argv.length === 2)
|
|
325
|
+
return { ok: true, command: "sandbox-reap" };
|
|
326
|
+
return { ok: false, error: "Usage: forge-bridge sandbox <init|doctor|reap> [--repo <path>] [--require-pinned-base]" };
|
|
327
|
+
}
|
|
328
|
+
const sandboxContractTemplate = `schemaVersion: 1
|
|
329
|
+
workingDirectory: /workspace
|
|
330
|
+
commands:
|
|
331
|
+
install: npm ci
|
|
332
|
+
test: npm test
|
|
333
|
+
caches:
|
|
334
|
+
- name: npm
|
|
335
|
+
mode: readonly
|
|
336
|
+
services: []
|
|
337
|
+
environment: []
|
|
338
|
+
artifacts: []
|
|
339
|
+
logs: []
|
|
340
|
+
browser: false
|
|
341
|
+
`;
|
|
342
|
+
const sandboxDockerfileTemplate = `# Pin this base by digest before enabling sandbox_required.
|
|
343
|
+
FROM node:22.17.0-bookworm-slim
|
|
344
|
+
WORKDIR /workspace
|
|
345
|
+
`;
|
|
346
|
+
async function initializeSandbox(repoPath, io) {
|
|
347
|
+
const forgePath = path.join(repoPath, ".forge");
|
|
348
|
+
const files = [
|
|
349
|
+
["forge.sandbox.yml", sandboxContractTemplate],
|
|
350
|
+
["Dockerfile", sandboxDockerfileTemplate],
|
|
351
|
+
["forge.env.example", "# List non-secret variable names in forge.sandbox.yml. Never commit secret values.\n"]
|
|
352
|
+
];
|
|
353
|
+
await fs.mkdir(forgePath, { recursive: true });
|
|
354
|
+
let created = 0;
|
|
355
|
+
for (const [name, contents] of files) {
|
|
356
|
+
const target = path.join(forgePath, name);
|
|
357
|
+
try {
|
|
358
|
+
const handle = await fs.open(target, "wx", 0o600);
|
|
359
|
+
await handle.writeFile(contents);
|
|
360
|
+
await handle.close();
|
|
361
|
+
io.stdout.write(`Created ${path.relative(repoPath, target)}\n`);
|
|
362
|
+
created += 1;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (error.code !== "EEXIST")
|
|
366
|
+
throw error;
|
|
367
|
+
io.stdout.write(`Kept existing ${path.relative(repoPath, target)}\n`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
io.stdout.write(created > 0 ? "Sandbox scaffold is ready. Run `forge-bridge sandbox doctor`.\n" : "Sandbox scaffold already exists.\n");
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
async function diagnoseSandbox(repoPath, requirePinnedBase, io) {
|
|
374
|
+
try {
|
|
375
|
+
const forgePath = path.join(repoPath, ".forge");
|
|
376
|
+
const contractSource = await fs.readFile(path.join(forgePath, "forge.sandbox.yml"), "utf8");
|
|
377
|
+
const dockerfile = await fs.readFile(path.join(forgePath, "Dockerfile"), "utf8");
|
|
378
|
+
const contract = validateSandboxContract(contractSource);
|
|
379
|
+
const warnings = validateDockerfilePolicy(dockerfile, requirePinnedBase);
|
|
380
|
+
io.stdout.write(`Contract: valid (${contract.digest})\n`);
|
|
381
|
+
for (const warning of warnings)
|
|
382
|
+
io.stdout.write(`Warning: ${warning}\n`);
|
|
383
|
+
const config = defaultBridgeServiceConfig();
|
|
384
|
+
const preflight = new DockerPreflight({
|
|
385
|
+
dockerExecutable: config.dockerExecutable,
|
|
386
|
+
dockerContext: config.dockerContext,
|
|
387
|
+
stateRoot: config.sandboxStateRoot,
|
|
388
|
+
maxConcurrentSandboxes: config.maxConcurrentSandboxes,
|
|
389
|
+
diskHighWatermarkPercent: config.diskHighWatermarkPercent,
|
|
390
|
+
diskLowWatermarkPercent: config.diskLowWatermarkPercent,
|
|
391
|
+
minimumWorkspaceBytes: config.minimumWorkspaceBytes,
|
|
392
|
+
requireRootless: config.requireRootlessDocker
|
|
393
|
+
});
|
|
394
|
+
try {
|
|
395
|
+
const result = await preflight.refresh();
|
|
396
|
+
if (!result.healthy) {
|
|
397
|
+
io.stderr.write(`Docker: unavailable (${result.unavailableCode ?? "preflight_failed"}): ${result.unavailableReason ?? "unknown reason"}\n`);
|
|
398
|
+
return 1;
|
|
399
|
+
}
|
|
400
|
+
io.stdout.write(`Docker: healthy (${result.runtimeVersion}; capacity ${result.maxConcurrentSandboxes})\n`);
|
|
401
|
+
return 0;
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
await preflight.release();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async function reapSandboxes(io) {
|
|
413
|
+
const config = defaultBridgeServiceConfig();
|
|
414
|
+
if (!/^[0-9a-f-]{32,36}$/iu.test(config.runnerId)) {
|
|
415
|
+
io.stderr.write("FORGE_RUNNER_ID must identify the runner whose exact-labeled resources may be reaped.\n");
|
|
416
|
+
return 2;
|
|
417
|
+
}
|
|
418
|
+
const preflight = new DockerPreflight({
|
|
419
|
+
dockerExecutable: config.dockerExecutable,
|
|
420
|
+
dockerContext: config.dockerContext,
|
|
421
|
+
stateRoot: config.sandboxStateRoot,
|
|
422
|
+
maxConcurrentSandboxes: config.maxConcurrentSandboxes,
|
|
423
|
+
diskHighWatermarkPercent: config.diskHighWatermarkPercent,
|
|
424
|
+
diskLowWatermarkPercent: config.diskLowWatermarkPercent,
|
|
425
|
+
minimumWorkspaceBytes: config.minimumWorkspaceBytes,
|
|
426
|
+
requireRootless: config.requireRootlessDocker
|
|
427
|
+
});
|
|
428
|
+
try {
|
|
429
|
+
const result = await preflight.refresh();
|
|
430
|
+
if (!result.healthy && result.unavailableCode !== "docker_disk_low") {
|
|
431
|
+
io.stderr.write(`Docker: unavailable (${result.unavailableCode ?? "preflight_failed"}): ${result.unavailableReason ?? "unknown reason"}\n`);
|
|
432
|
+
return 1;
|
|
433
|
+
}
|
|
434
|
+
const runtime = new DockerSandboxRuntime(config);
|
|
435
|
+
await runtime.reap({ diskLow: result.diskLow });
|
|
436
|
+
io.stdout.write(`Sandbox reaper completed for runner ${config.runnerId}.\n`);
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
441
|
+
return 1;
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
await preflight.release();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
283
447
|
function parseConnectArgs(argv) {
|
|
284
448
|
let serverUrl = null;
|
|
285
449
|
let runnerId = null;
|
|
@@ -468,7 +632,7 @@ async function claimBridgeRunner(args) {
|
|
|
468
632
|
})
|
|
469
633
|
});
|
|
470
634
|
if (!response.ok) {
|
|
471
|
-
throw new Error(
|
|
635
|
+
throw new Error(formatPairingFailure(response.status, args.serverUrl, await response.text()));
|
|
472
636
|
}
|
|
473
637
|
const value = await response.json();
|
|
474
638
|
if (!value.runnerId || !value.deviceToken) {
|
|
@@ -497,6 +661,8 @@ function pairedConfig(current, args, claim) {
|
|
|
497
661
|
supportsManagedForgeArtifacts: current.supportsManagedForgeArtifacts ?? true,
|
|
498
662
|
supportsRunnerAuthoredPhaseArtifacts: current.supportsRunnerAuthoredPhaseArtifacts ?? true,
|
|
499
663
|
supportsDraftRunOnce: current.supportsDraftRunOnce ?? true,
|
|
664
|
+
supportsDraftAttachments: current.supportsDraftAttachments ?? true,
|
|
665
|
+
supportsDraftImageAttachments: supportsDraftImageAttachmentsFor(args.cliProviders),
|
|
500
666
|
repoMappings: current.repoMappings ?? []
|
|
501
667
|
};
|
|
502
668
|
}
|
|
@@ -541,181 +707,15 @@ async function resolvePairRepoRoot(repoRoot, configuredRoot) {
|
|
|
541
707
|
function normalizePairingCode(code) {
|
|
542
708
|
return code?.replace(/[\s-]/g, "").trim() ?? "";
|
|
543
709
|
}
|
|
710
|
+
function formatPairingFailure(status, serverUrl, body) {
|
|
711
|
+
const hint = body.includes("runner.invalid_pairing_code")
|
|
712
|
+
? " Make sure the code was generated by the same Forge server shown above."
|
|
713
|
+
: "";
|
|
714
|
+
return `Pairing failed (${status}) against ${serverUrl}: ${body}${hint}`;
|
|
715
|
+
}
|
|
544
716
|
function normalizeServerUrl(serverUrl) {
|
|
545
717
|
return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
546
718
|
}
|
|
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
719
|
function connectStartupText(config) {
|
|
720
720
|
const runner = config.runnerId
|
|
721
721
|
? `${config.runnerId}${config.deviceName ? ` (${config.deviceName})` : ""}`
|
|
@@ -862,17 +862,9 @@ async function execVersionCommand(probe, timeout = 2000) {
|
|
|
862
862
|
if (process.platform !== "win32") {
|
|
863
863
|
throw error;
|
|
864
864
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
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;
|
|
865
|
+
const launch = buildWindowsCmdLaunch(probe.command, probe.args);
|
|
866
|
+
return await execFileAsync(launch.command, [...launch.args], { timeout });
|
|
874
867
|
}
|
|
875
|
-
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
|
|
876
868
|
}
|
|
877
869
|
function versionLine(output) {
|
|
878
870
|
const lines = output
|
|
@@ -17,6 +17,8 @@ export interface BridgeLocalConfig {
|
|
|
17
17
|
readonly supportsManagedForgeArtifacts?: boolean;
|
|
18
18
|
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean;
|
|
19
19
|
readonly supportsDraftRunOnce?: boolean;
|
|
20
|
+
readonly supportsDraftAttachments?: boolean;
|
|
21
|
+
readonly supportsDraftImageAttachments?: boolean;
|
|
20
22
|
}
|
|
21
23
|
export interface LocalRepoMapping extends BridgeRepoMapping {
|
|
22
24
|
readonly label: string;
|
package/dist/cli/local-config.js
CHANGED
|
@@ -54,7 +54,9 @@ export function applyLocalConfigDefaults(config, local) {
|
|
|
54
54
|
supportsManagedWorktrees: local.supportsManagedWorktrees ?? config.supportsManagedWorktrees,
|
|
55
55
|
supportsManagedForgeArtifacts: local.supportsManagedForgeArtifacts ?? config.supportsManagedForgeArtifacts,
|
|
56
56
|
supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts,
|
|
57
|
-
supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce
|
|
57
|
+
supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce,
|
|
58
|
+
supportsDraftAttachments: local.supportsDraftAttachments ?? config.supportsDraftAttachments,
|
|
59
|
+
supportsDraftImageAttachments: local.supportsDraftImageAttachments ?? config.supportsDraftImageAttachments
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
export async function addRepoMapping(label, repoPath) {
|
|
@@ -113,7 +115,9 @@ function normalizeLocalConfig(config) {
|
|
|
113
115
|
supportsManagedWorktrees: config.supportsManagedWorktrees,
|
|
114
116
|
supportsManagedForgeArtifacts: config.supportsManagedForgeArtifacts,
|
|
115
117
|
supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts,
|
|
116
|
-
supportsDraftRunOnce: config.supportsDraftRunOnce
|
|
118
|
+
supportsDraftRunOnce: config.supportsDraftRunOnce,
|
|
119
|
+
supportsDraftAttachments: config.supportsDraftAttachments,
|
|
120
|
+
supportsDraftImageAttachments: config.supportsDraftImageAttachments
|
|
117
121
|
};
|
|
118
122
|
}
|
|
119
123
|
function emptyConfig() {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export class BoundedCommandRunner {
|
|
3
|
+
async run(command, args, options = {}) {
|
|
4
|
+
const maxOutputBytes = options.maxOutputBytes ?? 1024 * 1024;
|
|
5
|
+
return await new Promise((resolve, reject) => {
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const abort = () => controller.abort(options.signal?.reason ?? "command cancelled");
|
|
8
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
9
|
+
const timeout = options.timeoutMs
|
|
10
|
+
? setTimeout(() => controller.abort(`command exceeded ${options.timeoutMs}ms`), options.timeoutMs)
|
|
11
|
+
: null;
|
|
12
|
+
timeout?.unref?.();
|
|
13
|
+
const child = spawn(command, [...args], {
|
|
14
|
+
cwd: options.cwd,
|
|
15
|
+
env: options.env ? { ...options.env } : undefined,
|
|
16
|
+
stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
});
|
|
20
|
+
let stdout = Buffer.alloc(0);
|
|
21
|
+
let stderr = Buffer.alloc(0);
|
|
22
|
+
const append = (current, chunk) => {
|
|
23
|
+
if (current.length >= maxOutputBytes)
|
|
24
|
+
return current;
|
|
25
|
+
return Buffer.concat([current, chunk.subarray(0, maxOutputBytes - current.length)]);
|
|
26
|
+
};
|
|
27
|
+
child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
|
|
28
|
+
child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
|
|
29
|
+
child.on("error", (error) => {
|
|
30
|
+
cleanup();
|
|
31
|
+
reject(error);
|
|
32
|
+
});
|
|
33
|
+
child.on("close", (code) => {
|
|
34
|
+
cleanup();
|
|
35
|
+
resolve({ exitCode: code ?? 1, stdout: stdout.toString("utf8"), stderr: stderr.toString("utf8") });
|
|
36
|
+
});
|
|
37
|
+
if (options.stdin !== undefined) {
|
|
38
|
+
child.stdin.end(options.stdin);
|
|
39
|
+
}
|
|
40
|
+
function cleanup() {
|
|
41
|
+
if (timeout)
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
options.signal?.removeEventListener("abort", abort);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ValidatedSandboxContract } from "./sandbox.types.js";
|
|
2
|
+
export declare function validateSandboxContract(source: string): ValidatedSandboxContract;
|
|
3
|
+
export declare function validateDockerfilePolicy(source: string, requirePinnedBase: boolean): readonly string[];
|
|
4
|
+
export declare function dockerfileBaseImages(source: string): readonly string[];
|
|
5
|
+
export declare function dockerfileUpstreamImages(source: string): readonly string[];
|