@loucompanion/forge-bridge 0.1.1-dev.8dcbc6245e51 → 0.1.1-dev.a8025fe64c0a
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.d.ts +1 -0
- package/dist/bridge/bridge.service.js +219 -25
- package/dist/bridge/bridge.types.d.ts +19 -2
- package/dist/cli/cli.service.d.ts +27 -2
- package/dist/cli/cli.service.js +630 -25
- package/dist/cli/local-config.d.ts +4 -0
- package/dist/cli/local-config.js +42 -4
- package/dist/config.js +1 -1
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +1 -0
- package/dist/session/internals.d.ts +5 -3
- package/dist/session/internals.js +2 -1
- package/dist/session/phase-completion.d.ts +115 -0
- package/dist/session/phase-completion.js +3051 -0
- package/dist/session/provider-cli/index.d.ts +2 -2
- package/dist/session/provider-cli/index.js +1 -1
- package/dist/session/provider-cli/provider-cli.base.d.ts +5 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +64 -4
- package/dist/session/provider-cli/provider-cli.service.js +2642 -51
- package/dist/session/provider-cli/provider-cli.types.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.d.ts +25 -0
- package/dist/session/provider-cli/provider-usage.js +64 -0
- package/dist/session/qa-sidecar-validation.d.ts +3 -0
- package/dist/session/qa-sidecar-validation.js +44 -0
- package/dist/session/session.service.js +62 -11
- package/dist/session/session.types.d.ts +130 -0
- package/dist/session/session.types.js +261 -2
- package/dist/shared/cursor-agent/cursor-agent.launch.d.ts +9 -0
- package/dist/shared/cursor-agent/cursor-agent.launch.js +66 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +2 -1
- package/dist/worktree/artifact-injector.js +86 -9
- package/dist/worktree/worktree.service.d.ts +3 -0
- package/dist/worktree/worktree.service.js +239 -8
- package/dist/worktree/worktree.types.d.ts +8 -1
- package/package.json +8 -3
|
@@ -15,6 +15,7 @@ export declare class ForgeBridgeService implements BridgeService {
|
|
|
15
15
|
run(signal?: AbortSignal): Promise<void>;
|
|
16
16
|
stop(): Promise<void>;
|
|
17
17
|
private handleDispatch;
|
|
18
|
+
private parseDispatchOrReport;
|
|
18
19
|
private handleCancel;
|
|
19
20
|
private abortActiveSessions;
|
|
20
21
|
private normalizeDispatch;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { defaultForgeBridgeConfig } from "../config.js";
|
|
4
5
|
import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
|
|
@@ -48,8 +49,14 @@ export class ForgeBridgeService {
|
|
|
48
49
|
while (!this.stopped && !signal?.aborted) {
|
|
49
50
|
try {
|
|
50
51
|
await this.connectOnce();
|
|
52
|
+
if (attempt > 0) {
|
|
53
|
+
logBridgeStatus("Connected to Forge. Waiting for dispatches...");
|
|
54
|
+
}
|
|
51
55
|
attempt = 0;
|
|
52
56
|
await this.waitForClose(signal);
|
|
57
|
+
if (!this.stopped && !signal?.aborted) {
|
|
58
|
+
logBridgeStatus("Connection closed. Reconnecting...");
|
|
59
|
+
}
|
|
53
60
|
}
|
|
54
61
|
catch (error) {
|
|
55
62
|
if (this.stopped || signal?.aborted) {
|
|
@@ -57,6 +64,7 @@ export class ForgeBridgeService {
|
|
|
57
64
|
}
|
|
58
65
|
const backoff = this.config.reconnectBackoffMs[Math.min(attempt, this.config.reconnectBackoffMs.length - 1)] ?? 5000;
|
|
59
66
|
attempt += 1;
|
|
67
|
+
logBridgeStatus(`Connect attempt ${attempt} failed: ${errorMessage(error)}. Retrying in ${formatDelay(backoff)}...`);
|
|
60
68
|
await delay(backoff, signal);
|
|
61
69
|
}
|
|
62
70
|
}
|
|
@@ -73,10 +81,14 @@ export class ForgeBridgeService {
|
|
|
73
81
|
this.abortActiveSessions("bridge stopping");
|
|
74
82
|
await this.stopConnection();
|
|
75
83
|
}
|
|
76
|
-
async handleDispatch(
|
|
84
|
+
async handleDispatch(rawDispatch) {
|
|
85
|
+
const dispatch = this.parseDispatchOrReport(rawDispatch);
|
|
86
|
+
if (!dispatch) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
77
89
|
const leaseId = dispatch.lease.id;
|
|
78
90
|
const fencingToken = dispatch.lease.fencingToken;
|
|
79
|
-
const redactionSecrets = exactSecrets(dispatch);
|
|
91
|
+
const redactionSecrets = exactSecrets(dispatch, this.config);
|
|
80
92
|
if (this.activeSessions.has(leaseId)) {
|
|
81
93
|
await this.sendResult(leaseId, fencingToken, failureResult("Dispatch is already active on this bridge.")).catch(() => undefined);
|
|
82
94
|
return;
|
|
@@ -87,11 +99,20 @@ export class ForgeBridgeService {
|
|
|
87
99
|
const normalized = this.normalizeDispatch(dispatch);
|
|
88
100
|
const result = await this.sessions.runOnce(normalized, {
|
|
89
101
|
emit: async (event) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
try {
|
|
103
|
+
await this.invokeAck("DispatchEvent", {
|
|
104
|
+
leaseId,
|
|
105
|
+
fencingToken,
|
|
106
|
+
event: redactSecrets(event, redactionSecrets)
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (isDispatchNoLongerPending(error)) {
|
|
111
|
+
controller.abort(error instanceof Error ? error.message : "Dispatch is no longer pending.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
95
116
|
}
|
|
96
117
|
}, controller.signal);
|
|
97
118
|
await this.sendResult(leaseId, fencingToken, redactSecrets(result, redactionSecrets));
|
|
@@ -106,6 +127,27 @@ export class ForgeBridgeService {
|
|
|
106
127
|
this.activeSessions.delete(leaseId);
|
|
107
128
|
}
|
|
108
129
|
}
|
|
130
|
+
parseDispatchOrReport(rawDispatch) {
|
|
131
|
+
try {
|
|
132
|
+
return parseRunOnceDispatchPacket(rawDispatch);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const identifiers = readDispatchIdentifiers(rawDispatch);
|
|
136
|
+
if (!identifiers) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const summary = invalidDispatchSummary(error);
|
|
140
|
+
void this.sendResult(identifiers.leaseId, identifiers.fencingToken, failureResult(summary, [
|
|
141
|
+
{
|
|
142
|
+
role: "runner",
|
|
143
|
+
kind: "error",
|
|
144
|
+
content: summary,
|
|
145
|
+
eventType: "invalid_dispatch"
|
|
146
|
+
}
|
|
147
|
+
])).catch(() => undefined);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
109
151
|
async handleCancel(request) {
|
|
110
152
|
const active = this.activeSessions.get(request.leaseId);
|
|
111
153
|
if (!active || active.fencingToken !== request.fencingToken) {
|
|
@@ -120,9 +162,10 @@ export class ForgeBridgeService {
|
|
|
120
162
|
this.activeSessions.clear();
|
|
121
163
|
}
|
|
122
164
|
normalizeDispatch(dispatch) {
|
|
123
|
-
const mapping = findRepoMapping(dispatch, this.config
|
|
165
|
+
const mapping = findRepoMapping(dispatch, repoMappingsFor(this.config));
|
|
124
166
|
const localPath = mapping?.localPath?.trim();
|
|
125
167
|
const repoRoot = localPath ? path.dirname(path.resolve(localPath)) : this.config.defaultRepoRoot;
|
|
168
|
+
const qaLaunchTarget = normalizeQaAppLaunchTarget(mapping?.qaAppLaunchTarget) ?? dispatch.qaBrowser?.launchTarget ?? null;
|
|
126
169
|
return {
|
|
127
170
|
...dispatch,
|
|
128
171
|
repo: {
|
|
@@ -133,7 +176,13 @@ export class ForgeBridgeService {
|
|
|
133
176
|
serverLocal: {
|
|
134
177
|
repoRoot,
|
|
135
178
|
cleanupWorktrees: this.config.cleanupWorktrees
|
|
136
|
-
}
|
|
179
|
+
},
|
|
180
|
+
qaBrowser: dispatch.qaBrowser
|
|
181
|
+
? {
|
|
182
|
+
...dispatch.qaBrowser,
|
|
183
|
+
launchTarget: qaLaunchTarget
|
|
184
|
+
}
|
|
185
|
+
: dispatch.qaBrowser
|
|
137
186
|
};
|
|
138
187
|
}
|
|
139
188
|
async sendResult(leaseId, fencingToken, result) {
|
|
@@ -142,7 +191,15 @@ export class ForgeBridgeService {
|
|
|
142
191
|
fencingToken,
|
|
143
192
|
result
|
|
144
193
|
};
|
|
145
|
-
|
|
194
|
+
try {
|
|
195
|
+
await this.invokeAck("DispatchResult", payload);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (isDispatchNoLongerPending(error)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
146
203
|
}
|
|
147
204
|
startHeartbeat() {
|
|
148
205
|
this.stopHeartbeat();
|
|
@@ -173,10 +230,14 @@ export class ForgeBridgeService {
|
|
|
173
230
|
cliProviders: this.config.cliProviders,
|
|
174
231
|
providerStatuses: this.config.providerStatuses,
|
|
175
232
|
projectLabels: this.config.projectLabels,
|
|
176
|
-
repoMappings: this.config
|
|
233
|
+
repoMappings: repoMappingsFor(this.config),
|
|
177
234
|
os: this.config.os,
|
|
178
235
|
sandboxProfiles: this.config.sandboxProfiles,
|
|
179
|
-
supportsApiSecretGrant: this.config.supportsApiSecretGrant
|
|
236
|
+
supportsApiSecretGrant: this.config.supportsApiSecretGrant,
|
|
237
|
+
supportsManagedWorktrees: this.config.supportsManagedWorktrees,
|
|
238
|
+
supportsManagedForgeArtifacts: this.config.supportsManagedForgeArtifacts,
|
|
239
|
+
supportsRunnerAuthoredPhaseArtifacts: this.config.supportsRunnerAuthoredPhaseArtifacts,
|
|
240
|
+
supportsDraftRunOnce: this.config.supportsDraftRunOnce
|
|
180
241
|
};
|
|
181
242
|
}
|
|
182
243
|
async invokeAck(methodName, payload) {
|
|
@@ -186,7 +247,7 @@ export class ForgeBridgeService {
|
|
|
186
247
|
}
|
|
187
248
|
const ack = await connection.invoke(methodName, payload);
|
|
188
249
|
if (!ack.accepted) {
|
|
189
|
-
throw new
|
|
250
|
+
throw new BridgeCallbackRejectedError(ack.code ?? null, ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
|
|
190
251
|
}
|
|
191
252
|
return ack;
|
|
192
253
|
}
|
|
@@ -204,6 +265,20 @@ export class ForgeBridgeService {
|
|
|
204
265
|
}
|
|
205
266
|
}
|
|
206
267
|
}
|
|
268
|
+
class BridgeCallbackRejectedError extends Error {
|
|
269
|
+
code;
|
|
270
|
+
constructor(code, message) {
|
|
271
|
+
super(message);
|
|
272
|
+
this.code = code;
|
|
273
|
+
this.name = "BridgeCallbackRejectedError";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function isDispatchNoLongerPending(error) {
|
|
277
|
+
if (error instanceof BridgeCallbackRejectedError) {
|
|
278
|
+
return error.code === "bridge.dispatch_not_pending" || error.code === "bridge.stale_dispatch";
|
|
279
|
+
}
|
|
280
|
+
return error instanceof Error && error.message === "Dispatch is no longer pending.";
|
|
281
|
+
}
|
|
207
282
|
export class SignalRBridgeConnectionFactory {
|
|
208
283
|
create(config) {
|
|
209
284
|
return new SignalRBridgeConnection(config);
|
|
@@ -234,14 +309,7 @@ export class SignalRBridgeConnection {
|
|
|
234
309
|
}
|
|
235
310
|
onDispatch(handler) {
|
|
236
311
|
this.connection.on("Dispatch", (raw) => {
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
dispatch = parseRunOnceDispatchPacket(raw);
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
void handler(dispatch).catch(() => undefined);
|
|
312
|
+
void handler(raw).catch(() => undefined);
|
|
245
313
|
});
|
|
246
314
|
}
|
|
247
315
|
onCancel(handler) {
|
|
@@ -268,8 +336,12 @@ export function defaultBridgeServiceConfig(overrides = {}) {
|
|
|
268
336
|
projectLabels: splitCsv(process.env.FORGE_PROJECT_LABELS ?? ""),
|
|
269
337
|
repoMappings: [],
|
|
270
338
|
os: process.platform,
|
|
271
|
-
sandboxProfiles:
|
|
339
|
+
sandboxProfiles: overrides.sandboxProfiles,
|
|
272
340
|
supportsApiSecretGrant: true,
|
|
341
|
+
supportsManagedWorktrees: true,
|
|
342
|
+
supportsManagedForgeArtifacts: true,
|
|
343
|
+
supportsRunnerAuthoredPhaseArtifacts: true,
|
|
344
|
+
supportsDraftRunOnce: true,
|
|
273
345
|
version: process.env.FORGE_BRIDGE_VERSION ?? forgeBridgeVersion,
|
|
274
346
|
protocolVersion: 1,
|
|
275
347
|
minServerVersion: null,
|
|
@@ -279,6 +351,7 @@ export function defaultBridgeServiceConfig(overrides = {}) {
|
|
|
279
351
|
};
|
|
280
352
|
return {
|
|
281
353
|
...config,
|
|
354
|
+
sandboxProfiles: config.sandboxProfiles ?? defaultSandboxProfiles(config.cliProviders),
|
|
282
355
|
heartbeatIntervalMs: positiveFiniteNumber(config.heartbeatIntervalMs, 30_000)
|
|
283
356
|
};
|
|
284
357
|
}
|
|
@@ -297,6 +370,14 @@ function positiveFiniteNumber(value, fallback) {
|
|
|
297
370
|
function providerStatusesFor(providers) {
|
|
298
371
|
return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
|
|
299
372
|
}
|
|
373
|
+
function defaultSandboxProfiles(providers, platform = process.platform) {
|
|
374
|
+
if (!providers.some((provider) => provider.toLowerCase() === "codex")) {
|
|
375
|
+
return ["none"];
|
|
376
|
+
}
|
|
377
|
+
return platform === "win32"
|
|
378
|
+
? ["danger-full-access"]
|
|
379
|
+
: ["provider-workspace-write"];
|
|
380
|
+
}
|
|
300
381
|
function configuredModelsFor(provider) {
|
|
301
382
|
const envName = `FORGE_${provider.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}_MODELS`;
|
|
302
383
|
return splitCsv(process.env[envName] ?? "");
|
|
@@ -307,6 +388,62 @@ function splitCsv(value) {
|
|
|
307
388
|
.map((entry) => entry.trim())
|
|
308
389
|
.filter((entry) => entry.length > 0);
|
|
309
390
|
}
|
|
391
|
+
function repoMappingsFor(config) {
|
|
392
|
+
const explicit = [...config.repoMappings];
|
|
393
|
+
const discovered = discoverRepoRootMappings(config.defaultRepoRoot);
|
|
394
|
+
if (explicit.length === 0) {
|
|
395
|
+
return discovered;
|
|
396
|
+
}
|
|
397
|
+
const explicitKeys = new Set(explicit.flatMap(mappingKeys));
|
|
398
|
+
return [
|
|
399
|
+
...explicit,
|
|
400
|
+
...discovered.filter((mapping) => !mappingKeys(mapping).some((key) => explicitKeys.has(key)))
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
function discoverRepoRootMappings(defaultRepoRoot) {
|
|
404
|
+
const root = defaultRepoRoot.trim();
|
|
405
|
+
if (!root) {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
const resolvedRoot = path.resolve(root);
|
|
409
|
+
const mappings = [];
|
|
410
|
+
if (isGitCheckout(resolvedRoot)) {
|
|
411
|
+
mappings.push({ name: path.basename(resolvedRoot), localPath: resolvedRoot });
|
|
412
|
+
}
|
|
413
|
+
let entries;
|
|
414
|
+
try {
|
|
415
|
+
entries = fs.readdirSync(resolvedRoot, { withFileTypes: true });
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return mappings;
|
|
419
|
+
}
|
|
420
|
+
for (const entry of entries) {
|
|
421
|
+
if (!entry.isDirectory()) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const localPath = path.join(resolvedRoot, entry.name);
|
|
425
|
+
if (isGitCheckout(localPath)) {
|
|
426
|
+
mappings.push({ name: entry.name, localPath });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return mappings.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
430
|
+
}
|
|
431
|
+
function isGitCheckout(localPath) {
|
|
432
|
+
try {
|
|
433
|
+
return fs.existsSync(path.join(localPath, ".git"));
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function mappingKeys(mapping) {
|
|
440
|
+
return [
|
|
441
|
+
mapping.repoId ? `id:${mapping.repoId.toLowerCase()}` : null,
|
|
442
|
+
mapping.name ? `name:${mapping.name.trim().toLowerCase()}` : null,
|
|
443
|
+
mapping.remoteUrl ? `remote:${mapping.remoteUrl.trim().toLowerCase()}` : null,
|
|
444
|
+
mapping.localPath ? `path:${path.resolve(mapping.localPath).toLowerCase()}` : null
|
|
445
|
+
].filter((key) => Boolean(key));
|
|
446
|
+
}
|
|
310
447
|
function findRepoMapping(dispatch, mappings) {
|
|
311
448
|
return mappings.find((mapping) => {
|
|
312
449
|
if (!mapping.localPath?.trim()) {
|
|
@@ -317,16 +454,70 @@ function findRepoMapping(dispatch, mappings) {
|
|
|
317
454
|
(!!mapping.remoteUrl?.trim() && equalsIgnoreCase(mapping.remoteUrl, dispatch.repo.remoteUrl)));
|
|
318
455
|
});
|
|
319
456
|
}
|
|
457
|
+
function normalizeQaAppLaunchTarget(target) {
|
|
458
|
+
if (!target) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
const routes = [...new Set((target.routes ?? []).map((route) => route.trim()).filter(Boolean))];
|
|
462
|
+
const normalized = {
|
|
463
|
+
url: cleanString(target.url),
|
|
464
|
+
command: cleanString(target.command),
|
|
465
|
+
working_directory: cleanString(target.working_directory),
|
|
466
|
+
ready_url: cleanString(target.ready_url),
|
|
467
|
+
routes,
|
|
468
|
+
notes: cleanString(target.notes)
|
|
469
|
+
};
|
|
470
|
+
return normalized.url || normalized.command || normalized.ready_url || routes.length > 0
|
|
471
|
+
? normalized
|
|
472
|
+
: null;
|
|
473
|
+
}
|
|
474
|
+
function cleanString(value) {
|
|
475
|
+
const clean = value?.trim();
|
|
476
|
+
return clean ? clean : null;
|
|
477
|
+
}
|
|
320
478
|
function equalsIgnoreCase(left, right) {
|
|
321
479
|
return !!left?.trim() && !!right?.trim() && left.trim().toLowerCase() === right.trim().toLowerCase();
|
|
322
480
|
}
|
|
481
|
+
function readDispatchIdentifiers(rawDispatch) {
|
|
482
|
+
if (!rawDispatch || typeof rawDispatch !== "object" || Array.isArray(rawDispatch)) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const packet = rawDispatch;
|
|
486
|
+
const rawLease = packet.lease ?? packet.Lease;
|
|
487
|
+
if (!rawLease || typeof rawLease !== "object" || Array.isArray(rawLease)) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
const lease = rawLease;
|
|
491
|
+
const leaseId = readString(lease, "id", "Id");
|
|
492
|
+
const fencingToken = readString(lease, "fencingToken", "FencingToken");
|
|
493
|
+
return leaseId && fencingToken ? { leaseId, fencingToken } : null;
|
|
494
|
+
}
|
|
495
|
+
function readString(source, camelKey, pascalKey) {
|
|
496
|
+
const value = source[camelKey] ?? source[pascalKey];
|
|
497
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
498
|
+
}
|
|
499
|
+
function invalidDispatchSummary(error) {
|
|
500
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
501
|
+
return `Invalid bridge dispatch packet: ${detail}`;
|
|
502
|
+
}
|
|
503
|
+
function errorMessage(error) {
|
|
504
|
+
return error instanceof Error ? error.message : String(error);
|
|
505
|
+
}
|
|
506
|
+
function formatDelay(ms) {
|
|
507
|
+
if (ms >= 1000 && ms % 1000 === 0) {
|
|
508
|
+
return `${ms / 1000}s`;
|
|
509
|
+
}
|
|
510
|
+
return `${ms}ms`;
|
|
511
|
+
}
|
|
512
|
+
function logBridgeStatus(message) {
|
|
513
|
+
process.stderr.write(`[${new Date().toISOString()}] ${message}\n`);
|
|
514
|
+
}
|
|
323
515
|
function delay(ms, signal) {
|
|
324
516
|
if (signal?.aborted) {
|
|
325
517
|
return Promise.resolve();
|
|
326
518
|
}
|
|
327
519
|
return new Promise((resolve) => {
|
|
328
520
|
const timeout = setTimeout(resolve, ms);
|
|
329
|
-
timeout.unref?.();
|
|
330
521
|
signal?.addEventListener("abort", () => {
|
|
331
522
|
clearTimeout(timeout);
|
|
332
523
|
resolve();
|
|
@@ -342,6 +533,9 @@ function abortPromise(signal) {
|
|
|
342
533
|
}
|
|
343
534
|
return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
344
535
|
}
|
|
345
|
-
function exactSecrets(dispatch) {
|
|
346
|
-
return
|
|
536
|
+
function exactSecrets(dispatch, config) {
|
|
537
|
+
return [
|
|
538
|
+
dispatch.apiKeySecretGrant?.secret,
|
|
539
|
+
config.deviceToken
|
|
540
|
+
].filter((secret) => typeof secret === "string" && secret.length > 0);
|
|
347
541
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RunOnceResultPacket, RunnerDispatchEvent } from "../session/session.types.js";
|
|
2
2
|
export interface BridgeServiceConfig {
|
|
3
3
|
readonly serverUrl: string;
|
|
4
4
|
readonly runnerId: string;
|
|
@@ -13,6 +13,10 @@ export interface BridgeServiceConfig {
|
|
|
13
13
|
readonly os: string;
|
|
14
14
|
readonly sandboxProfiles: readonly string[];
|
|
15
15
|
readonly supportsApiSecretGrant: boolean;
|
|
16
|
+
readonly supportsManagedWorktrees: boolean;
|
|
17
|
+
readonly supportsManagedForgeArtifacts: boolean;
|
|
18
|
+
readonly supportsRunnerAuthoredPhaseArtifacts: boolean;
|
|
19
|
+
readonly supportsDraftRunOnce: boolean;
|
|
16
20
|
readonly version: string;
|
|
17
21
|
readonly protocolVersion: number;
|
|
18
22
|
readonly minServerVersion?: string | null;
|
|
@@ -24,6 +28,15 @@ export interface BridgeRepoMapping {
|
|
|
24
28
|
readonly name?: string | null;
|
|
25
29
|
readonly remoteUrl?: string | null;
|
|
26
30
|
readonly localPath?: string | null;
|
|
31
|
+
readonly qaAppLaunchTarget?: QaAppLaunchTarget | null;
|
|
32
|
+
}
|
|
33
|
+
export interface QaAppLaunchTarget {
|
|
34
|
+
readonly url?: string | null;
|
|
35
|
+
readonly command?: string | null;
|
|
36
|
+
readonly working_directory?: string | null;
|
|
37
|
+
readonly ready_url?: string | null;
|
|
38
|
+
readonly routes?: readonly string[] | null;
|
|
39
|
+
readonly notes?: string | null;
|
|
27
40
|
}
|
|
28
41
|
export interface BridgeProviderStatus {
|
|
29
42
|
readonly signedIn?: boolean | null;
|
|
@@ -40,6 +53,10 @@ export interface BridgeCapabilities {
|
|
|
40
53
|
readonly os?: string | null;
|
|
41
54
|
readonly sandboxProfiles?: readonly string[] | null;
|
|
42
55
|
readonly supportsApiSecretGrant?: boolean | null;
|
|
56
|
+
readonly supportsManagedWorktrees?: boolean | null;
|
|
57
|
+
readonly supportsManagedForgeArtifacts?: boolean | null;
|
|
58
|
+
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean | null;
|
|
59
|
+
readonly supportsDraftRunOnce?: boolean | null;
|
|
43
60
|
}
|
|
44
61
|
export interface BridgeHelloRequest {
|
|
45
62
|
readonly version: string;
|
|
@@ -73,5 +90,5 @@ export interface BridgeCancelRequest {
|
|
|
73
90
|
readonly fencingToken: string;
|
|
74
91
|
readonly reason?: string | null;
|
|
75
92
|
}
|
|
76
|
-
export type BridgeDispatchHandler = (dispatch:
|
|
93
|
+
export type BridgeDispatchHandler = (dispatch: unknown) => Promise<void>;
|
|
77
94
|
export type BridgeCancelHandler = (request: BridgeCancelRequest) => Promise<void>;
|
|
@@ -5,7 +5,8 @@ import type { CliIo, CliService } from "./cli.base.js";
|
|
|
5
5
|
export declare class ForgeBridgeCliService implements CliService {
|
|
6
6
|
private readonly sessions;
|
|
7
7
|
private readonly bridgeFactory;
|
|
8
|
-
|
|
8
|
+
private readonly pairRunner;
|
|
9
|
+
constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService, pairRunner?: PairRunnerClient);
|
|
9
10
|
run(argv: readonly string[], io?: CliIo): Promise<number>;
|
|
10
11
|
}
|
|
11
12
|
export declare function parseArgs(argv: readonly string[]): {
|
|
@@ -13,11 +14,18 @@ export declare function parseArgs(argv: readonly string[]): {
|
|
|
13
14
|
readonly command: "run-once";
|
|
14
15
|
readonly dispatchPath: string;
|
|
15
16
|
readonly resultPath: string;
|
|
17
|
+
} | {
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly command: "mcp";
|
|
20
|
+
readonly argv: readonly string[];
|
|
16
21
|
} | {
|
|
17
22
|
readonly ok: true;
|
|
18
23
|
readonly command: "connect";
|
|
19
24
|
readonly config: BridgeServiceConfig;
|
|
20
|
-
} | {
|
|
25
|
+
} | ({
|
|
26
|
+
readonly ok: true;
|
|
27
|
+
readonly command: "pair";
|
|
28
|
+
} & PairArgs) | {
|
|
21
29
|
readonly ok: true;
|
|
22
30
|
readonly command: "add-repo";
|
|
23
31
|
readonly label: string;
|
|
@@ -41,5 +49,22 @@ export declare function parseArgs(argv: readonly string[]): {
|
|
|
41
49
|
readonly ok: false;
|
|
42
50
|
readonly error: string;
|
|
43
51
|
};
|
|
52
|
+
interface PairArgs {
|
|
53
|
+
readonly serverUrl: string;
|
|
54
|
+
readonly code?: string;
|
|
55
|
+
readonly name: string;
|
|
56
|
+
readonly fingerprint: string;
|
|
57
|
+
readonly repoRoot?: string;
|
|
58
|
+
readonly cleanupWorktrees: boolean;
|
|
59
|
+
readonly cliProviders: readonly string[];
|
|
60
|
+
readonly projectLabels: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
interface PairRunnerClaim {
|
|
63
|
+
readonly runnerId: string;
|
|
64
|
+
readonly deviceToken: string;
|
|
65
|
+
}
|
|
66
|
+
type PairRunnerClient = (args: PairArgs) => Promise<PairRunnerClaim>;
|
|
67
|
+
export declare function parseCodexModelCatalog(raw: string): string[];
|
|
68
|
+
export declare function parseCursorModelList(raw: string): string[];
|
|
44
69
|
type CleanupMode = "stale" | "merged" | "all";
|
|
45
70
|
export {};
|