@loucompanion/forge-bridge 0.1.1-dev.f599637d804e → 0.1.3-dev.0b2f81d660c0
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/dist/bridge/bridge.service.js +112 -12
- package/dist/bridge/bridge.types.d.ts +6 -0
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +171 -0
- package/dist/cli/cli.service.js +49 -13
- package/dist/cli/local-config.d.ts +3 -0
- package/dist/cli/local-config.js +22 -3
- package/dist/config.js +1 -1
- 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 +47 -37
- 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 +144 -2586
- 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 +2 -1
- 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 +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +318 -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 +42 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +497 -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 -44
- package/dist/session/provider-cli/provider-cli.service.js +269 -2109
- package/dist/session/provider-cli/provider-cli.types.d.ts +6 -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 +20 -0
- package/dist/session/session.types.d.ts +91 -0
- package/dist/session/session.types.js +175 -1
- 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/artifact-injector.js +72 -0
- package/dist/worktree/dependency-warmer.d.ts +11 -0
- package/dist/worktree/dependency-warmer.js +45 -1
- package/dist/worktree/worktree.service.d.ts +2 -1
- package/dist/worktree/worktree.service.js +32 -12
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr";
|
|
1
|
+
import { HubConnectionBuilder, HttpTransportType, LogLevel } from "@microsoft/signalr";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { defaultForgeBridgeConfig } from "../config.js";
|
|
5
|
+
import { supportsDraftImageAttachmentsFor } from "../shared/provider-capabilities.js";
|
|
4
6
|
import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
|
|
5
7
|
import { BridgeSessionService } from "../session/session.service.js";
|
|
6
8
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
@@ -48,15 +50,22 @@ export class ForgeBridgeService {
|
|
|
48
50
|
while (!this.stopped && !signal?.aborted) {
|
|
49
51
|
try {
|
|
50
52
|
await this.connectOnce();
|
|
53
|
+
if (attempt > 0) {
|
|
54
|
+
logBridgeStatus("Connected to Forge. Waiting for dispatches...");
|
|
55
|
+
}
|
|
51
56
|
attempt = 0;
|
|
52
57
|
await this.waitForClose(signal);
|
|
58
|
+
if (!this.stopped && !signal?.aborted) {
|
|
59
|
+
logBridgeStatus("Connection closed. Reconnecting...");
|
|
60
|
+
}
|
|
53
61
|
}
|
|
54
|
-
catch {
|
|
62
|
+
catch (error) {
|
|
55
63
|
if (this.stopped || signal?.aborted) {
|
|
56
64
|
break;
|
|
57
65
|
}
|
|
58
66
|
const backoff = this.config.reconnectBackoffMs[Math.min(attempt, this.config.reconnectBackoffMs.length - 1)] ?? 5000;
|
|
59
67
|
attempt += 1;
|
|
68
|
+
logBridgeStatus(`Connect attempt ${attempt} failed: ${errorMessage(error)}. Retrying in ${formatDelay(backoff)}...`);
|
|
60
69
|
await delay(backoff, signal);
|
|
61
70
|
}
|
|
62
71
|
}
|
|
@@ -154,7 +163,7 @@ export class ForgeBridgeService {
|
|
|
154
163
|
this.activeSessions.clear();
|
|
155
164
|
}
|
|
156
165
|
normalizeDispatch(dispatch) {
|
|
157
|
-
const mapping = findRepoMapping(dispatch, this.config
|
|
166
|
+
const mapping = findRepoMapping(dispatch, repoMappingsFor(this.config));
|
|
158
167
|
const localPath = mapping?.localPath?.trim();
|
|
159
168
|
const repoRoot = localPath ? path.dirname(path.resolve(localPath)) : this.config.defaultRepoRoot;
|
|
160
169
|
const qaLaunchTarget = normalizeQaAppLaunchTarget(mapping?.qaAppLaunchTarget) ?? dispatch.qaBrowser?.launchTarget ?? null;
|
|
@@ -222,13 +231,16 @@ export class ForgeBridgeService {
|
|
|
222
231
|
cliProviders: this.config.cliProviders,
|
|
223
232
|
providerStatuses: this.config.providerStatuses,
|
|
224
233
|
projectLabels: this.config.projectLabels,
|
|
225
|
-
repoMappings: this.config
|
|
234
|
+
repoMappings: repoMappingsFor(this.config),
|
|
226
235
|
os: this.config.os,
|
|
227
236
|
sandboxProfiles: this.config.sandboxProfiles,
|
|
228
237
|
supportsApiSecretGrant: this.config.supportsApiSecretGrant,
|
|
229
238
|
supportsManagedWorktrees: this.config.supportsManagedWorktrees,
|
|
230
239
|
supportsManagedForgeArtifacts: this.config.supportsManagedForgeArtifacts,
|
|
231
|
-
supportsRunnerAuthoredPhaseArtifacts: this.config.supportsRunnerAuthoredPhaseArtifacts
|
|
240
|
+
supportsRunnerAuthoredPhaseArtifacts: this.config.supportsRunnerAuthoredPhaseArtifacts,
|
|
241
|
+
supportsDraftRunOnce: this.config.supportsDraftRunOnce,
|
|
242
|
+
supportsDraftAttachments: this.config.supportsDraftAttachments,
|
|
243
|
+
supportsDraftImageAttachments: this.config.supportsDraftImageAttachments
|
|
232
244
|
};
|
|
233
245
|
}
|
|
234
246
|
async invokeAck(methodName, payload) {
|
|
@@ -284,7 +296,13 @@ export class SignalRBridgeConnection {
|
|
|
284
296
|
url.searchParams.set("runnerId", config.runnerId);
|
|
285
297
|
this.connection = new HubConnectionBuilder()
|
|
286
298
|
.withUrl(url.toString(), {
|
|
287
|
-
|
|
299
|
+
// Query-string access tokens are intentionally rejected by the API. The bridge is
|
|
300
|
+
// a Node CLI, so use long polling where Authorization headers are carried on each
|
|
301
|
+
// HTTP request instead of relying on browser/WebSocket token fallbacks.
|
|
302
|
+
transport: HttpTransportType.LongPolling,
|
|
303
|
+
headers: {
|
|
304
|
+
Authorization: `Bearer ${config.deviceToken}`
|
|
305
|
+
}
|
|
288
306
|
})
|
|
289
307
|
.configureLogging(LogLevel.Warning)
|
|
290
308
|
.build();
|
|
@@ -332,6 +350,9 @@ export function defaultBridgeServiceConfig(overrides = {}) {
|
|
|
332
350
|
supportsManagedWorktrees: true,
|
|
333
351
|
supportsManagedForgeArtifacts: true,
|
|
334
352
|
supportsRunnerAuthoredPhaseArtifacts: true,
|
|
353
|
+
supportsDraftRunOnce: true,
|
|
354
|
+
supportsDraftAttachments: true,
|
|
355
|
+
supportsDraftImageAttachments: supportsDraftImageAttachmentsFor(cliProviders),
|
|
335
356
|
version: process.env.FORGE_BRIDGE_VERSION ?? forgeBridgeVersion,
|
|
336
357
|
protocolVersion: 1,
|
|
337
358
|
minServerVersion: null,
|
|
@@ -341,7 +362,9 @@ export function defaultBridgeServiceConfig(overrides = {}) {
|
|
|
341
362
|
};
|
|
342
363
|
return {
|
|
343
364
|
...config,
|
|
365
|
+
providerStatuses: overrides.providerStatuses ?? providerStatusesFor(config.cliProviders),
|
|
344
366
|
sandboxProfiles: config.sandboxProfiles ?? defaultSandboxProfiles(config.cliProviders),
|
|
367
|
+
supportsDraftImageAttachments: (overrides.supportsDraftImageAttachments ?? true) && supportsDraftImageAttachmentsFor(config.cliProviders),
|
|
345
368
|
heartbeatIntervalMs: positiveFiniteNumber(config.heartbeatIntervalMs, 30_000)
|
|
346
369
|
};
|
|
347
370
|
}
|
|
@@ -360,10 +383,13 @@ function positiveFiniteNumber(value, fallback) {
|
|
|
360
383
|
function providerStatusesFor(providers) {
|
|
361
384
|
return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
|
|
362
385
|
}
|
|
363
|
-
function defaultSandboxProfiles(providers) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
386
|
+
function defaultSandboxProfiles(providers, platform = process.platform) {
|
|
387
|
+
if (!providers.some((provider) => provider.toLowerCase() === "codex")) {
|
|
388
|
+
return ["none"];
|
|
389
|
+
}
|
|
390
|
+
return platform === "win32"
|
|
391
|
+
? ["danger-full-access"]
|
|
392
|
+
: ["provider-workspace-write"];
|
|
367
393
|
}
|
|
368
394
|
function configuredModelsFor(provider) {
|
|
369
395
|
const envName = `FORGE_${provider.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}_MODELS`;
|
|
@@ -375,6 +401,62 @@ function splitCsv(value) {
|
|
|
375
401
|
.map((entry) => entry.trim())
|
|
376
402
|
.filter((entry) => entry.length > 0);
|
|
377
403
|
}
|
|
404
|
+
function repoMappingsFor(config) {
|
|
405
|
+
const explicit = [...config.repoMappings];
|
|
406
|
+
const discovered = discoverRepoRootMappings(config.defaultRepoRoot);
|
|
407
|
+
if (explicit.length === 0) {
|
|
408
|
+
return discovered;
|
|
409
|
+
}
|
|
410
|
+
const explicitKeys = new Set(explicit.flatMap(mappingKeys));
|
|
411
|
+
return [
|
|
412
|
+
...explicit,
|
|
413
|
+
...discovered.filter((mapping) => !mappingKeys(mapping).some((key) => explicitKeys.has(key)))
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
function discoverRepoRootMappings(defaultRepoRoot) {
|
|
417
|
+
const root = defaultRepoRoot.trim();
|
|
418
|
+
if (!root) {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
const resolvedRoot = path.resolve(root);
|
|
422
|
+
const mappings = [];
|
|
423
|
+
if (isGitCheckout(resolvedRoot)) {
|
|
424
|
+
mappings.push({ name: path.basename(resolvedRoot), localPath: resolvedRoot });
|
|
425
|
+
}
|
|
426
|
+
let entries;
|
|
427
|
+
try {
|
|
428
|
+
entries = fs.readdirSync(resolvedRoot, { withFileTypes: true });
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return mappings;
|
|
432
|
+
}
|
|
433
|
+
for (const entry of entries) {
|
|
434
|
+
if (!entry.isDirectory()) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const localPath = path.join(resolvedRoot, entry.name);
|
|
438
|
+
if (isGitCheckout(localPath)) {
|
|
439
|
+
mappings.push({ name: entry.name, localPath });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return mappings.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
443
|
+
}
|
|
444
|
+
function isGitCheckout(localPath) {
|
|
445
|
+
try {
|
|
446
|
+
return fs.existsSync(path.join(localPath, ".git"));
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function mappingKeys(mapping) {
|
|
453
|
+
return [
|
|
454
|
+
mapping.repoId ? `id:${mapping.repoId.toLowerCase()}` : null,
|
|
455
|
+
mapping.name ? `name:${mapping.name.trim().toLowerCase()}` : null,
|
|
456
|
+
mapping.remoteUrl ? `remote:${mapping.remoteUrl.trim().toLowerCase()}` : null,
|
|
457
|
+
mapping.localPath ? `path:${path.resolve(mapping.localPath).toLowerCase()}` : null
|
|
458
|
+
].filter((key) => Boolean(key));
|
|
459
|
+
}
|
|
378
460
|
function findRepoMapping(dispatch, mappings) {
|
|
379
461
|
return mappings.find((mapping) => {
|
|
380
462
|
if (!mapping.localPath?.trim()) {
|
|
@@ -431,13 +513,24 @@ function invalidDispatchSummary(error) {
|
|
|
431
513
|
const detail = error instanceof Error ? error.message : String(error);
|
|
432
514
|
return `Invalid bridge dispatch packet: ${detail}`;
|
|
433
515
|
}
|
|
516
|
+
function errorMessage(error) {
|
|
517
|
+
return error instanceof Error ? error.message : String(error);
|
|
518
|
+
}
|
|
519
|
+
function formatDelay(ms) {
|
|
520
|
+
if (ms >= 1000 && ms % 1000 === 0) {
|
|
521
|
+
return `${ms / 1000}s`;
|
|
522
|
+
}
|
|
523
|
+
return `${ms}ms`;
|
|
524
|
+
}
|
|
525
|
+
function logBridgeStatus(message) {
|
|
526
|
+
process.stderr.write(`[${new Date().toISOString()}] ${message}\n`);
|
|
527
|
+
}
|
|
434
528
|
function delay(ms, signal) {
|
|
435
529
|
if (signal?.aborted) {
|
|
436
530
|
return Promise.resolve();
|
|
437
531
|
}
|
|
438
532
|
return new Promise((resolve) => {
|
|
439
533
|
const timeout = setTimeout(resolve, ms);
|
|
440
|
-
timeout.unref?.();
|
|
441
534
|
signal?.addEventListener("abort", () => {
|
|
442
535
|
clearTimeout(timeout);
|
|
443
536
|
resolve();
|
|
@@ -453,9 +546,16 @@ function abortPromise(signal) {
|
|
|
453
546
|
}
|
|
454
547
|
return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
455
548
|
}
|
|
549
|
+
const maxAttachmentExactSecretLength = 8 * 1024;
|
|
456
550
|
function exactSecrets(dispatch, config) {
|
|
457
551
|
return [
|
|
458
552
|
dispatch.apiKeySecretGrant?.secret,
|
|
459
|
-
config.deviceToken
|
|
553
|
+
config.deviceToken,
|
|
554
|
+
...boundedAttachmentExactSecrets(dispatch)
|
|
460
555
|
].filter((secret) => typeof secret === "string" && secret.length > 0);
|
|
461
556
|
}
|
|
557
|
+
function boundedAttachmentExactSecrets(dispatch) {
|
|
558
|
+
return (dispatch.draft?.attachments ?? [])
|
|
559
|
+
.flatMap((attachment) => [attachment.textContent, attachment.contentBase64])
|
|
560
|
+
.filter((secret) => typeof secret === "string" && secret.length > 0 && secret.length <= maxAttachmentExactSecretLength);
|
|
561
|
+
}
|
|
@@ -16,6 +16,9 @@ export interface BridgeServiceConfig {
|
|
|
16
16
|
readonly supportsManagedWorktrees: boolean;
|
|
17
17
|
readonly supportsManagedForgeArtifacts: boolean;
|
|
18
18
|
readonly supportsRunnerAuthoredPhaseArtifacts: boolean;
|
|
19
|
+
readonly supportsDraftRunOnce: boolean;
|
|
20
|
+
readonly supportsDraftAttachments: boolean;
|
|
21
|
+
readonly supportsDraftImageAttachments: boolean;
|
|
19
22
|
readonly version: string;
|
|
20
23
|
readonly protocolVersion: number;
|
|
21
24
|
readonly minServerVersion?: string | null;
|
|
@@ -55,6 +58,9 @@ export interface BridgeCapabilities {
|
|
|
55
58
|
readonly supportsManagedWorktrees?: boolean | null;
|
|
56
59
|
readonly supportsManagedForgeArtifacts?: boolean | null;
|
|
57
60
|
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean | null;
|
|
61
|
+
readonly supportsDraftRunOnce?: boolean | null;
|
|
62
|
+
readonly supportsDraftAttachments?: boolean | null;
|
|
63
|
+
readonly supportsDraftImageAttachments?: boolean | null;
|
|
58
64
|
}
|
|
59
65
|
export interface BridgeHelloRequest {
|
|
60
66
|
readonly version: string;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { defaultBridgeServiceConfig } from "../bridge/bridge.service.js";
|
|
2
|
+
import { forgeBridgeVersion } from "../version.js";
|
|
3
|
+
export function helpText() {
|
|
4
|
+
const version = defaultBridgeServiceConfig().version ?? forgeBridgeVersion;
|
|
5
|
+
return [
|
|
6
|
+
`🛠 Forge Bridge ${version} — Local provider runners for Forge.`,
|
|
7
|
+
"",
|
|
8
|
+
"Usage: forge-bridge [options] [command]",
|
|
9
|
+
"",
|
|
10
|
+
"Options:",
|
|
11
|
+
" -h, --help Display help for command",
|
|
12
|
+
" -v, --version Output the version number",
|
|
13
|
+
"",
|
|
14
|
+
"Commands:",
|
|
15
|
+
" Hint: commands suffixed with * have detailed help. Run <command> --help for details.",
|
|
16
|
+
" connect * Start the runner bridge and wait for Forge dispatches",
|
|
17
|
+
" pair * Pair this machine with Forge and save runner credentials",
|
|
18
|
+
" status * Show config, runner identity, repos, provider CLIs, and updates",
|
|
19
|
+
" add-repo * Map a Forge repo label/name to a local checkout path",
|
|
20
|
+
" remove-repo * Remove a saved repo mapping",
|
|
21
|
+
" cleanup * Prune managed bridge worktrees",
|
|
22
|
+
" version * Print the installed bridge package version",
|
|
23
|
+
" help * Display help for command",
|
|
24
|
+
" run-once * Internal: run one dispatch packet from JSON",
|
|
25
|
+
" mcp * Internal: run the phase-completion MCP server",
|
|
26
|
+
"",
|
|
27
|
+
"Examples:",
|
|
28
|
+
" npm install -g @loucompanion/forge-bridge@next",
|
|
29
|
+
" forge-bridge pair 1234-5678 --server https://srv1493013.tail66073d.ts.net:8443 --repo-root \"%USERPROFILE%\\ForgeRepos\"",
|
|
30
|
+
" forge-bridge connect",
|
|
31
|
+
" forge-bridge status --check-updates",
|
|
32
|
+
" forge-bridge add-repo Forge D:\\Programming\\repos\\Forge",
|
|
33
|
+
" forge-bridge cleanup --stale",
|
|
34
|
+
"",
|
|
35
|
+
"Troubleshooting:",
|
|
36
|
+
" If Forge says the bridge is old: npm install -g @loucompanion/forge-bridge@next",
|
|
37
|
+
" If connect shows 502/500: leave it open; it reconnects with backoff while dev restarts.",
|
|
38
|
+
" If a provider is missing: run forge-bridge status --check-updates and verify CLI login.",
|
|
39
|
+
"",
|
|
40
|
+
"Docs:",
|
|
41
|
+
" https://github.com/coleman399/Forge/tree/develop/tools/forge-bridge#readme",
|
|
42
|
+
"",
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
export function commandHelpText(command) {
|
|
46
|
+
switch (command) {
|
|
47
|
+
case "pair":
|
|
48
|
+
return [
|
|
49
|
+
"Forge Bridge — pair",
|
|
50
|
+
"Pair this machine with Forge and save runner credentials.",
|
|
51
|
+
"",
|
|
52
|
+
"Usage:",
|
|
53
|
+
" forge-bridge pair [code] [options]",
|
|
54
|
+
"",
|
|
55
|
+
"Options:",
|
|
56
|
+
" --server <url> Forge server URL.",
|
|
57
|
+
" --repo-root <path> Folder where bridge-managed repos/worktrees live.",
|
|
58
|
+
" --name <name> Display name for this runner.",
|
|
59
|
+
" --fingerprint <value> Stable device fingerprint override.",
|
|
60
|
+
" --cli-provider <list> Comma-separated providers, e.g. codex,cursor.",
|
|
61
|
+
" --cleanup-worktrees Enable managed worktree cleanup.",
|
|
62
|
+
" --no-cleanup-worktrees Disable managed worktree cleanup.",
|
|
63
|
+
"",
|
|
64
|
+
"Example:",
|
|
65
|
+
" forge-bridge pair 1234-5678 --server https://forge.example.com --repo-root \"%USERPROFILE%\\ForgeRepos\"",
|
|
66
|
+
"",
|
|
67
|
+
].join("\n");
|
|
68
|
+
case "connect":
|
|
69
|
+
return [
|
|
70
|
+
"Forge Bridge — connect",
|
|
71
|
+
"Start the runner bridge and keep it connected for Forge dispatches.",
|
|
72
|
+
"",
|
|
73
|
+
"Usage:",
|
|
74
|
+
" forge-bridge connect [options]",
|
|
75
|
+
"",
|
|
76
|
+
"Options:",
|
|
77
|
+
" --server <url> Forge server URL; saved config is used when omitted.",
|
|
78
|
+
" --runner-id <id> Runner id; saved config is used when omitted.",
|
|
79
|
+
" --device-token <token> Device token; saved config is used when omitted.",
|
|
80
|
+
" --repo-root <path> Override repo/worktree folder for this run.",
|
|
81
|
+
" --cli-provider <list> Comma-separated providers, e.g. codex,cursor.",
|
|
82
|
+
" --heartbeat-interval-ms <n> Heartbeat interval in milliseconds.",
|
|
83
|
+
" --cleanup-worktrees Enable managed worktree cleanup for this run.",
|
|
84
|
+
" --no-cleanup-worktrees Disable managed worktree cleanup for this run.",
|
|
85
|
+
"",
|
|
86
|
+
"Example:",
|
|
87
|
+
" forge-bridge connect",
|
|
88
|
+
"",
|
|
89
|
+
].join("\n");
|
|
90
|
+
case "status":
|
|
91
|
+
return [
|
|
92
|
+
"Forge Bridge — status",
|
|
93
|
+
"Show saved configuration, paired runner identity, repo mappings, detected provider CLIs, and optional update info.",
|
|
94
|
+
"",
|
|
95
|
+
"Usage:",
|
|
96
|
+
" forge-bridge status [--check-updates]",
|
|
97
|
+
"",
|
|
98
|
+
"Example:",
|
|
99
|
+
" forge-bridge status --check-updates",
|
|
100
|
+
"",
|
|
101
|
+
].join("\n");
|
|
102
|
+
case "add-repo":
|
|
103
|
+
return [
|
|
104
|
+
"Forge Bridge — add-repo",
|
|
105
|
+
"Map a Forge repo label/name to an existing local checkout.",
|
|
106
|
+
"",
|
|
107
|
+
"Usage:",
|
|
108
|
+
" forge-bridge add-repo <label> <path>",
|
|
109
|
+
"",
|
|
110
|
+
"Example:",
|
|
111
|
+
" forge-bridge add-repo Forge D:\\Programming\\repos\\Forge",
|
|
112
|
+
"",
|
|
113
|
+
].join("\n");
|
|
114
|
+
case "remove-repo":
|
|
115
|
+
return [
|
|
116
|
+
"Forge Bridge — remove-repo",
|
|
117
|
+
"Remove a saved local repo mapping.",
|
|
118
|
+
"",
|
|
119
|
+
"Usage:",
|
|
120
|
+
" forge-bridge remove-repo <label>",
|
|
121
|
+
"",
|
|
122
|
+
].join("\n");
|
|
123
|
+
case "cleanup":
|
|
124
|
+
return [
|
|
125
|
+
"Forge Bridge — cleanup",
|
|
126
|
+
"Prune managed bridge worktrees under the configured repo folder.",
|
|
127
|
+
"",
|
|
128
|
+
"Usage:",
|
|
129
|
+
" forge-bridge cleanup [--stale|--merged|--all] [--force]",
|
|
130
|
+
"",
|
|
131
|
+
"Examples:",
|
|
132
|
+
" forge-bridge cleanup --stale",
|
|
133
|
+
" forge-bridge cleanup --all --force",
|
|
134
|
+
"",
|
|
135
|
+
].join("\n");
|
|
136
|
+
case "run-once":
|
|
137
|
+
return [
|
|
138
|
+
"Forge Bridge — run-once",
|
|
139
|
+
"Internal command used by Forge to run a single dispatch packet.",
|
|
140
|
+
"",
|
|
141
|
+
"Usage:",
|
|
142
|
+
" forge-bridge run-once --dispatch <dispatch-json-path> --result <result-json-path>",
|
|
143
|
+
"",
|
|
144
|
+
].join("\n");
|
|
145
|
+
case "mcp":
|
|
146
|
+
return [
|
|
147
|
+
"Forge Bridge — mcp",
|
|
148
|
+
"Internal MCP server used by provider CLIs to report Forge phase artifacts.",
|
|
149
|
+
"",
|
|
150
|
+
"Usage:",
|
|
151
|
+
" forge-bridge mcp --worktree <path> --work-item-id <id> --phase <phase>",
|
|
152
|
+
"",
|
|
153
|
+
].join("\n");
|
|
154
|
+
case "version":
|
|
155
|
+
return [
|
|
156
|
+
"Forge Bridge — version",
|
|
157
|
+
"Print the installed bridge package version.",
|
|
158
|
+
"",
|
|
159
|
+
"Usage:",
|
|
160
|
+
" forge-bridge version",
|
|
161
|
+
"",
|
|
162
|
+
].join("\n");
|
|
163
|
+
default:
|
|
164
|
+
return [
|
|
165
|
+
`Unknown forge-bridge command: ${command}`,
|
|
166
|
+
"",
|
|
167
|
+
"Run `forge-bridge help` to see available commands.",
|
|
168
|
+
"",
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
|
171
|
+
}
|
package/dist/cli/cli.service.js
CHANGED
|
@@ -5,12 +5,16 @@ 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";
|
|
12
|
+
import { forgeBridgeVersion } from "../version.js";
|
|
10
13
|
import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, writeLocalConfig } from "./local-config.js";
|
|
11
14
|
import { BridgeSessionService } from "../session/session.service.js";
|
|
12
15
|
import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
|
|
13
16
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
17
|
+
import { commandHelpText, helpText } from "./cli.help.js";
|
|
14
18
|
const apiKeySecretGrantEnv = "FORGE_API_KEY_SECRET_GRANT";
|
|
15
19
|
const execFileAsync = promisify(execFile);
|
|
16
20
|
export class ForgeBridgeCliService {
|
|
@@ -23,6 +27,22 @@ export class ForgeBridgeCliService {
|
|
|
23
27
|
this.pairRunner = pairRunner;
|
|
24
28
|
}
|
|
25
29
|
async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
30
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
31
|
+
io.stdout.write(helpText());
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
if (argv[0] === "help") {
|
|
35
|
+
io.stdout.write(argv[1] ? commandHelpText(argv[1]) : helpText());
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
if (argv[1] === "--help" || argv[1] === "-h") {
|
|
39
|
+
io.stdout.write(commandHelpText(argv[0]));
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
if (argv[0] === "version" || argv[0] === "--version" || argv[0] === "-v") {
|
|
43
|
+
io.stdout.write(`forge-bridge ${defaultBridgeServiceConfig().version ?? forgeBridgeVersion}\n`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
26
46
|
const args = parseArgs(argv);
|
|
27
47
|
if (!args.ok) {
|
|
28
48
|
io.stderr.write(`${args.error}\n`);
|
|
@@ -44,6 +64,7 @@ export class ForgeBridgeCliService {
|
|
|
44
64
|
process.once("SIGINT", stop);
|
|
45
65
|
process.once("SIGTERM", stop);
|
|
46
66
|
try {
|
|
67
|
+
io.stdout.write(connectStartupText(config));
|
|
47
68
|
await this.bridgeFactory(config).run(controller.signal);
|
|
48
69
|
return 0;
|
|
49
70
|
}
|
|
@@ -235,7 +256,7 @@ export function parseArgs(argv) {
|
|
|
235
256
|
if (argv[0] !== "run-once") {
|
|
236
257
|
return {
|
|
237
258
|
ok: false,
|
|
238
|
-
error:
|
|
259
|
+
error: helpText()
|
|
239
260
|
};
|
|
240
261
|
}
|
|
241
262
|
let dispatchPath = null;
|
|
@@ -478,6 +499,9 @@ function pairedConfig(current, args, claim) {
|
|
|
478
499
|
supportsManagedWorktrees: current.supportsManagedWorktrees ?? true,
|
|
479
500
|
supportsManagedForgeArtifacts: current.supportsManagedForgeArtifacts ?? true,
|
|
480
501
|
supportsRunnerAuthoredPhaseArtifacts: current.supportsRunnerAuthoredPhaseArtifacts ?? true,
|
|
502
|
+
supportsDraftRunOnce: current.supportsDraftRunOnce ?? true,
|
|
503
|
+
supportsDraftAttachments: current.supportsDraftAttachments ?? true,
|
|
504
|
+
supportsDraftImageAttachments: supportsDraftImageAttachmentsFor(args.cliProviders),
|
|
481
505
|
repoMappings: current.repoMappings ?? []
|
|
482
506
|
};
|
|
483
507
|
}
|
|
@@ -525,8 +549,28 @@ function normalizePairingCode(code) {
|
|
|
525
549
|
function normalizeServerUrl(serverUrl) {
|
|
526
550
|
return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
527
551
|
}
|
|
528
|
-
function
|
|
529
|
-
|
|
552
|
+
function connectStartupText(config) {
|
|
553
|
+
const runner = config.runnerId
|
|
554
|
+
? `${config.runnerId}${config.deviceName ? ` (${config.deviceName})` : ""}`
|
|
555
|
+
: "not configured";
|
|
556
|
+
return [
|
|
557
|
+
`Forge Bridge ${config.version}`,
|
|
558
|
+
"Starting runner bridge...",
|
|
559
|
+
"",
|
|
560
|
+
"Startup:",
|
|
561
|
+
` server: ${config.serverUrl || "not configured"}`,
|
|
562
|
+
` runner: ${runner}`,
|
|
563
|
+
` config: ${resolveLocalConfigPath()}`,
|
|
564
|
+
` repo folder: ${config.defaultRepoRoot || "not configured"}`,
|
|
565
|
+
` providers: ${formatList(config.cliProviders)}`,
|
|
566
|
+
` cleanup worktrees: ${config.cleanupWorktrees ? "enabled" : "disabled"}`,
|
|
567
|
+
"",
|
|
568
|
+
"Press Ctrl+C to stop.",
|
|
569
|
+
"",
|
|
570
|
+
].join("\n");
|
|
571
|
+
}
|
|
572
|
+
function formatList(values) {
|
|
573
|
+
return values.length > 0 ? values.join(", ") : "none";
|
|
530
574
|
}
|
|
531
575
|
async function writeJsonAtomic(outputPath, value) {
|
|
532
576
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
@@ -651,17 +695,9 @@ async function execVersionCommand(probe, timeout = 2000) {
|
|
|
651
695
|
if (process.platform !== "win32") {
|
|
652
696
|
throw error;
|
|
653
697
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
function windowsCommandLine(probe) {
|
|
658
|
-
return [probe.command, ...probe.args].map(quoteWindowsCommandLineArg).join(" ");
|
|
659
|
-
}
|
|
660
|
-
function quoteWindowsCommandLineArg(value) {
|
|
661
|
-
if (value.length > 0 && !/[\s"]/.test(value)) {
|
|
662
|
-
return value;
|
|
698
|
+
const launch = buildWindowsCmdLaunch(probe.command, probe.args);
|
|
699
|
+
return await execFileAsync(launch.command, [...launch.args], { timeout });
|
|
663
700
|
}
|
|
664
|
-
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
|
|
665
701
|
}
|
|
666
702
|
function versionLine(output) {
|
|
667
703
|
const lines = output
|
|
@@ -16,6 +16,9 @@ export interface BridgeLocalConfig {
|
|
|
16
16
|
readonly supportsManagedWorktrees?: boolean;
|
|
17
17
|
readonly supportsManagedForgeArtifacts?: boolean;
|
|
18
18
|
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean;
|
|
19
|
+
readonly supportsDraftRunOnce?: boolean;
|
|
20
|
+
readonly supportsDraftAttachments?: boolean;
|
|
21
|
+
readonly supportsDraftImageAttachments?: boolean;
|
|
19
22
|
}
|
|
20
23
|
export interface LocalRepoMapping extends BridgeRepoMapping {
|
|
21
24
|
readonly label: string;
|
package/dist/cli/local-config.js
CHANGED
|
@@ -49,11 +49,14 @@ export function applyLocalConfigDefaults(config, local) {
|
|
|
49
49
|
providerStatuses: normalizeProviderStatuses(local.providerStatuses) ?? providerStatusesFor(cliProviders),
|
|
50
50
|
projectLabels: local.projectLabels ?? config.projectLabels,
|
|
51
51
|
repoMappings: local.repoMappings ?? config.repoMappings,
|
|
52
|
-
sandboxProfiles: local.sandboxProfiles ?? config.sandboxProfiles,
|
|
52
|
+
sandboxProfiles: normalizeSandboxProfiles(local.sandboxProfiles ?? config.sandboxProfiles, cliProviders) ?? config.sandboxProfiles,
|
|
53
53
|
supportsApiSecretGrant: local.supportsApiSecretGrant ?? config.supportsApiSecretGrant,
|
|
54
54
|
supportsManagedWorktrees: local.supportsManagedWorktrees ?? config.supportsManagedWorktrees,
|
|
55
55
|
supportsManagedForgeArtifacts: local.supportsManagedForgeArtifacts ?? config.supportsManagedForgeArtifacts,
|
|
56
|
-
supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts
|
|
56
|
+
supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts,
|
|
57
|
+
supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce,
|
|
58
|
+
supportsDraftAttachments: local.supportsDraftAttachments ?? config.supportsDraftAttachments,
|
|
59
|
+
supportsDraftImageAttachments: local.supportsDraftImageAttachments ?? config.supportsDraftImageAttachments
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
export async function addRepoMapping(label, repoPath) {
|
|
@@ -111,7 +114,10 @@ function normalizeLocalConfig(config) {
|
|
|
111
114
|
supportsApiSecretGrant: config.supportsApiSecretGrant,
|
|
112
115
|
supportsManagedWorktrees: config.supportsManagedWorktrees,
|
|
113
116
|
supportsManagedForgeArtifacts: config.supportsManagedForgeArtifacts,
|
|
114
|
-
supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts
|
|
117
|
+
supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts,
|
|
118
|
+
supportsDraftRunOnce: config.supportsDraftRunOnce,
|
|
119
|
+
supportsDraftAttachments: config.supportsDraftAttachments,
|
|
120
|
+
supportsDraftImageAttachments: config.supportsDraftImageAttachments
|
|
115
121
|
};
|
|
116
122
|
}
|
|
117
123
|
function emptyConfig() {
|
|
@@ -149,6 +155,19 @@ function splitCsv(value) {
|
|
|
149
155
|
.map((entry) => entry.trim())
|
|
150
156
|
.filter((entry) => entry.length > 0);
|
|
151
157
|
}
|
|
158
|
+
function normalizeSandboxProfiles(profiles, cliProviders) {
|
|
159
|
+
const normalized = cleanList(profiles);
|
|
160
|
+
if (process.platform === "win32"
|
|
161
|
+
&& cliProviders.some((provider) => provider.toLowerCase() === "codex")
|
|
162
|
+
&& normalized?.some((profile) => profile.toLowerCase() === "provider-workspace-write")
|
|
163
|
+
&& !normalized.some((profile) => profile.toLowerCase() === "danger-full-access")) {
|
|
164
|
+
return [
|
|
165
|
+
...normalized.filter((profile) => profile.toLowerCase() !== "provider-workspace-write"),
|
|
166
|
+
"danger-full-access"
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
return normalized;
|
|
170
|
+
}
|
|
152
171
|
function normalizeRepoMappings(values) {
|
|
153
172
|
return (values ?? [])
|
|
154
173
|
.filter((mapping) => !!mapping.label?.trim() && !!mapping.localPath?.trim())
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import os from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
export function defaultForgeBridgeConfig() {
|
|
4
4
|
return {
|
|
5
|
-
defaultRepoRoot: path.join(os.tmpdir(), "forge-
|
|
5
|
+
defaultRepoRoot: path.join(os.tmpdir(), "forge-repos"),
|
|
6
6
|
cleanupWorktrees: true
|
|
7
7
|
};
|
|
8
8
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CompletionArtifactReference, type CompletionPhase, type CompletionToolServerConfig, type DecomposeCompletionGate, type DraftCompletionGate, type QaCompletionGate } from "./phase-completion.js";
|
|
2
|
+
import type { RunnerDispatchArtifact } from "./session.types.js";
|
|
3
|
+
export declare function writeDecomposeGateCompletionArtifact(config: CompletionToolServerConfig, gate: DecomposeCompletionGate, toolName: string, input: unknown): Promise<CompletionArtifactReference>;
|
|
4
|
+
export declare function writeDraftGateCompletionArtifact(config: CompletionToolServerConfig, gate: DraftCompletionGate, toolName: string, input: unknown): Promise<CompletionArtifactReference>;
|
|
5
|
+
export declare function writeCompletionArtifact(config: CompletionToolServerConfig, toolName: string, input: unknown): Promise<CompletionArtifactReference>;
|
|
6
|
+
export declare function writeArchitectCompletionArtifact(config: CompletionToolServerConfig, payload: Record<string, unknown>, status: string, producedBy: string): Promise<CompletionArtifactReference>;
|
|
7
|
+
export declare function writeQaGateCompletionArtifact(config: CompletionToolServerConfig, gate: QaCompletionGate, input: unknown): Promise<CompletionArtifactReference>;
|
|
8
|
+
export declare function writeQaCompletionArtifact(config: CompletionToolServerConfig, payload: Record<string, unknown>, status: string, producedBy: string): Promise<CompletionArtifactReference>;
|
|
9
|
+
export declare function readQaGateArtifact(config: CompletionToolServerConfig, gate: QaCompletionGate): Promise<Record<string, unknown>>;
|
|
10
|
+
export declare function readCompletionArtifact(worktreePath: string, workItemId: string, phase: CompletionPhase, ownerType?: string | null, ownerId?: string | null): Promise<RunnerDispatchArtifact | null>;
|