@loucompanion/forge-bridge 0.1.1-dev.242bb53ef13f

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.
Files changed (76) hide show
  1. package/README.md +63 -0
  2. package/dist/bridge/bridge.base.d.ts +17 -0
  3. package/dist/bridge/bridge.base.js +1 -0
  4. package/dist/bridge/bridge.service.d.ts +45 -0
  5. package/dist/bridge/bridge.service.js +340 -0
  6. package/dist/bridge/bridge.types.d.ts +76 -0
  7. package/dist/bridge/bridge.types.js +1 -0
  8. package/dist/bridge/index.d.ts +2 -0
  9. package/dist/bridge/index.js +1 -0
  10. package/dist/bridge/internals.d.ts +3 -0
  11. package/dist/bridge/internals.js +1 -0
  12. package/dist/cleanup/cleanup.base.d.ts +4 -0
  13. package/dist/cleanup/cleanup.base.js +1 -0
  14. package/dist/cleanup/cleanup.service.d.ts +5 -0
  15. package/dist/cleanup/cleanup.service.js +5 -0
  16. package/dist/cleanup/cleanup.types.d.ts +6 -0
  17. package/dist/cleanup/cleanup.types.js +1 -0
  18. package/dist/cleanup/index.d.ts +2 -0
  19. package/dist/cleanup/index.js +1 -0
  20. package/dist/cleanup/internals.d.ts +3 -0
  21. package/dist/cleanup/internals.js +1 -0
  22. package/dist/cli/bin.d.ts +2 -0
  23. package/dist/cli/bin.js +4 -0
  24. package/dist/cli/cli.base.d.ts +7 -0
  25. package/dist/cli/cli.base.js +1 -0
  26. package/dist/cli/cli.service.d.ts +45 -0
  27. package/dist/cli/cli.service.js +400 -0
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/internals.d.ts +1 -0
  31. package/dist/cli/internals.js +1 -0
  32. package/dist/cli/local-config.d.ts +31 -0
  33. package/dist/cli/local-config.js +146 -0
  34. package/dist/config.d.ts +5 -0
  35. package/dist/config.js +8 -0
  36. package/dist/index.d.ts +7 -0
  37. package/dist/index.js +7 -0
  38. package/dist/session/index.d.ts +2 -0
  39. package/dist/session/index.js +1 -0
  40. package/dist/session/internals.d.ts +7 -0
  41. package/dist/session/internals.js +2 -0
  42. package/dist/session/provider-cli/index.d.ts +3 -0
  43. package/dist/session/provider-cli/index.js +1 -0
  44. package/dist/session/provider-cli/provider-cli.base.d.ts +6 -0
  45. package/dist/session/provider-cli/provider-cli.base.js +1 -0
  46. package/dist/session/provider-cli/provider-cli.service.d.ts +16 -0
  47. package/dist/session/provider-cli/provider-cli.service.js +111 -0
  48. package/dist/session/provider-cli/provider-cli.types.d.ts +12 -0
  49. package/dist/session/provider-cli/provider-cli.types.js +1 -0
  50. package/dist/session/session.base.d.ts +7 -0
  51. package/dist/session/session.base.js +1 -0
  52. package/dist/session/session.service.d.ts +10 -0
  53. package/dist/session/session.service.js +92 -0
  54. package/dist/session/session.types.d.ts +107 -0
  55. package/dist/session/session.types.js +151 -0
  56. package/dist/shared/git/git.d.ts +6 -0
  57. package/dist/shared/git/git.js +18 -0
  58. package/dist/shared/path/path.d.ts +4 -0
  59. package/dist/shared/path/path.js +29 -0
  60. package/dist/shared/process/process.d.ts +16 -0
  61. package/dist/shared/process/process.js +32 -0
  62. package/dist/shared/redaction/redaction.d.ts +2 -0
  63. package/dist/shared/redaction/redaction.js +30 -0
  64. package/dist/version.d.ts +1 -0
  65. package/dist/version.js +1 -0
  66. package/dist/worktree/index.d.ts +2 -0
  67. package/dist/worktree/index.js +1 -0
  68. package/dist/worktree/internals.d.ts +3 -0
  69. package/dist/worktree/internals.js +1 -0
  70. package/dist/worktree/worktree.base.d.ts +5 -0
  71. package/dist/worktree/worktree.base.js +1 -0
  72. package/dist/worktree/worktree.service.d.ts +12 -0
  73. package/dist/worktree/worktree.service.js +139 -0
  74. package/dist/worktree/worktree.types.d.ts +15 -0
  75. package/dist/worktree/worktree.types.js +1 -0
  76. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Forge Bridge CLI
2
+
3
+ `@loucompanion/forge-bridge` connects a local runner machine to a Forge server so Forge can dispatch work to approved local tooling and repositories.
4
+
5
+ ## Install
6
+
7
+ Stable production channel:
8
+
9
+ ```bash
10
+ npm install -g @loucompanion/forge-bridge@latest
11
+ ```
12
+
13
+ Tailnet dev channel:
14
+
15
+ ```bash
16
+ npm install -g @loucompanion/forge-bridge@next
17
+ ```
18
+
19
+ ## Basic usage
20
+
21
+ ```bash
22
+ forge-bridge status
23
+ forge-bridge connect --server <forge-url> --runner-id <runner-id> --device-token <device-token>
24
+ ```
25
+
26
+ The bridge can also read connection settings from environment variables:
27
+
28
+ - `FORGE_URL`
29
+ - `FORGE_RUNNER_ID`
30
+ - `FORGE_DEVICE_TOKEN`
31
+ - `FORGE_DEVICE_NAME`
32
+ - `FORGE_REPO_ROOT`
33
+ - `FORGE_CLI_PROVIDERS`
34
+
35
+ ## Repo-local development
36
+
37
+ For active Forge branch development, run the bridge directly from the checkout instead of publishing a package:
38
+
39
+ ```bash
40
+ scripts/run-bridge-dev.sh status
41
+ scripts/run-bridge-dev.sh connect --server <forge-url> --runner-id <runner-id> --device-token <device-token>
42
+ ```
43
+
44
+ The tailnet dev server advertises the installable `@next` prerelease version. Repo-local execution is for contributors testing the current checkout.
45
+
46
+ ## Repository mappings
47
+
48
+ Map Forge project labels to local repository paths:
49
+
50
+ ```bash
51
+ forge-bridge add-repo Forge /path/to/Forge
52
+ forge-bridge remove-repo Forge
53
+ ```
54
+
55
+ ## Package smoke check
56
+
57
+ From this package directory:
58
+
59
+ ```bash
60
+ npm run smoke:package
61
+ ```
62
+
63
+ The smoke check builds the CLI, packs the package, installs the tarball into a temporary prefix, and verifies the installed `forge-bridge` binary can run `status`.
@@ -0,0 +1,17 @@
1
+ import type { BridgeCancelHandler, BridgeDispatchHandler, BridgeServiceConfig } from "./bridge.types.js";
2
+ export interface BridgeService {
3
+ connectOnce(): Promise<void>;
4
+ run(signal?: AbortSignal): Promise<void>;
5
+ stop(): Promise<void>;
6
+ }
7
+ export interface BridgeConnection {
8
+ start(): Promise<void>;
9
+ stop(): Promise<void>;
10
+ onClose(handler: (error?: Error) => void): void;
11
+ onDispatch(handler: BridgeDispatchHandler): void;
12
+ onCancel(handler: BridgeCancelHandler): void;
13
+ invoke<TResponse>(methodName: string, payload: unknown): Promise<TResponse>;
14
+ }
15
+ export interface BridgeConnectionFactory {
16
+ create(config: BridgeServiceConfig): BridgeConnection;
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import type { SessionService } from "../session/session.base.js";
2
+ import type { BridgeConnection, BridgeConnectionFactory, BridgeService } from "./bridge.base.js";
3
+ import type { BridgeCancelHandler, BridgeDispatchHandler, BridgeServiceConfig } from "./bridge.types.js";
4
+ export declare class ForgeBridgeService implements BridgeService {
5
+ private readonly config;
6
+ private readonly sessions;
7
+ private readonly connections;
8
+ private connection;
9
+ private heartbeatTimer;
10
+ private closed;
11
+ private stopped;
12
+ private readonly activeSessions;
13
+ constructor(config: BridgeServiceConfig, sessions?: SessionService, connections?: BridgeConnectionFactory);
14
+ connectOnce(): Promise<void>;
15
+ run(signal?: AbortSignal): Promise<void>;
16
+ stop(): Promise<void>;
17
+ private handleDispatch;
18
+ private handleCancel;
19
+ private abortActiveSessions;
20
+ private normalizeDispatch;
21
+ private sendResult;
22
+ private startHeartbeat;
23
+ private stopHeartbeat;
24
+ private sendHeartbeat;
25
+ private capabilities;
26
+ private invokeAck;
27
+ private waitForClose;
28
+ private stopConnection;
29
+ }
30
+ export declare class SignalRBridgeConnectionFactory implements BridgeConnectionFactory {
31
+ create(config: BridgeServiceConfig): BridgeConnection;
32
+ }
33
+ export declare class SignalRBridgeConnection implements BridgeConnection {
34
+ private readonly config;
35
+ private readonly connection;
36
+ constructor(config: BridgeServiceConfig);
37
+ start(): Promise<void>;
38
+ stop(): Promise<void>;
39
+ onClose(handler: (error?: Error) => void): void;
40
+ onDispatch(handler: BridgeDispatchHandler): void;
41
+ onCancel(handler: BridgeCancelHandler): void;
42
+ invoke<TResponse>(methodName: string, payload: unknown): Promise<TResponse>;
43
+ }
44
+ export declare function defaultBridgeServiceConfig(overrides?: Partial<BridgeServiceConfig>): BridgeServiceConfig;
45
+ export declare function resolveBridgeRunnerHubUrl(serverUrl: string): URL;
@@ -0,0 +1,340 @@
1
+ import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr";
2
+ import path from "node:path";
3
+ import { defaultForgeBridgeConfig } from "../config.js";
4
+ import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
5
+ import { BridgeSessionService } from "../session/session.service.js";
6
+ import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
7
+ import { forgeBridgeVersion } from "../version.js";
8
+ export class ForgeBridgeService {
9
+ config;
10
+ sessions;
11
+ connections;
12
+ connection = null;
13
+ heartbeatTimer = null;
14
+ closed = null;
15
+ stopped = false;
16
+ activeSessions = new Map();
17
+ constructor(config, sessions = new BridgeSessionService(), connections = new SignalRBridgeConnectionFactory()) {
18
+ this.config = config;
19
+ this.sessions = sessions;
20
+ this.connections = connections;
21
+ }
22
+ async connectOnce() {
23
+ await this.stopConnection();
24
+ this.stopped = false;
25
+ const connection = this.connections.create(this.config);
26
+ this.connection = connection;
27
+ this.closed = new Promise((resolve) => {
28
+ connection.onClose(() => {
29
+ this.stopHeartbeat();
30
+ this.abortActiveSessions("bridge connection closed");
31
+ resolve();
32
+ });
33
+ });
34
+ connection.onDispatch((dispatch) => this.handleDispatch(dispatch));
35
+ connection.onCancel((request) => this.handleCancel(request));
36
+ await connection.start();
37
+ await this.invokeAck("Hello", {
38
+ version: this.config.version,
39
+ protocolVersion: this.config.protocolVersion,
40
+ minServerVersion: this.config.minServerVersion ?? null,
41
+ capabilities: this.capabilities()
42
+ });
43
+ this.startHeartbeat();
44
+ }
45
+ async run(signal) {
46
+ let attempt = 0;
47
+ try {
48
+ while (!this.stopped && !signal?.aborted) {
49
+ try {
50
+ await this.connectOnce();
51
+ attempt = 0;
52
+ await this.waitForClose(signal);
53
+ }
54
+ catch (error) {
55
+ if (this.stopped || signal?.aborted) {
56
+ break;
57
+ }
58
+ const backoff = this.config.reconnectBackoffMs[Math.min(attempt, this.config.reconnectBackoffMs.length - 1)] ?? 5000;
59
+ attempt += 1;
60
+ await delay(backoff, signal);
61
+ }
62
+ }
63
+ }
64
+ finally {
65
+ if (signal?.aborted) {
66
+ this.abortActiveSessions(String(signal.reason ?? "bridge stopping"));
67
+ }
68
+ await this.stopConnection();
69
+ }
70
+ }
71
+ async stop() {
72
+ this.stopped = true;
73
+ this.abortActiveSessions("bridge stopping");
74
+ await this.stopConnection();
75
+ }
76
+ async handleDispatch(dispatch) {
77
+ const leaseId = dispatch.lease.id;
78
+ const fencingToken = dispatch.lease.fencingToken;
79
+ const redactionSecrets = exactSecrets(dispatch);
80
+ if (this.activeSessions.has(leaseId)) {
81
+ await this.sendResult(leaseId, fencingToken, failureResult("Dispatch is already active on this bridge.")).catch(() => undefined);
82
+ return;
83
+ }
84
+ const controller = new AbortController();
85
+ this.activeSessions.set(leaseId, { fencingToken, controller });
86
+ try {
87
+ const normalized = this.normalizeDispatch(dispatch);
88
+ const result = await this.sessions.runOnce(normalized, {
89
+ emit: async (event) => {
90
+ await this.invokeAck("DispatchEvent", {
91
+ leaseId,
92
+ fencingToken,
93
+ event: redactSecrets(event, redactionSecrets)
94
+ });
95
+ }
96
+ }, controller.signal);
97
+ await this.sendResult(leaseId, fencingToken, redactSecrets(result, redactionSecrets));
98
+ }
99
+ catch (error) {
100
+ const summary = controller.signal.aborted
101
+ ? `Dispatch cancelled: ${String(controller.signal.reason ?? "cancelled")}`
102
+ : redactText(error instanceof Error ? error.message : String(error), redactionSecrets);
103
+ await this.sendResult(leaseId, fencingToken, failureResult(summary)).catch(() => undefined);
104
+ }
105
+ finally {
106
+ this.activeSessions.delete(leaseId);
107
+ }
108
+ }
109
+ async handleCancel(request) {
110
+ const active = this.activeSessions.get(request.leaseId);
111
+ if (!active || active.fencingToken !== request.fencingToken) {
112
+ return;
113
+ }
114
+ active.controller.abort(request.reason ?? "cancelled by server");
115
+ }
116
+ abortActiveSessions(reason) {
117
+ for (const active of this.activeSessions.values()) {
118
+ active.controller.abort(reason);
119
+ }
120
+ this.activeSessions.clear();
121
+ }
122
+ normalizeDispatch(dispatch) {
123
+ const mapping = findRepoMapping(dispatch, this.config.repoMappings);
124
+ const localPath = mapping?.localPath?.trim();
125
+ const repoRoot = localPath ? path.dirname(path.resolve(localPath)) : this.config.defaultRepoRoot;
126
+ return {
127
+ ...dispatch,
128
+ repo: {
129
+ ...dispatch.repo,
130
+ serverLocalPath: localPath ? path.resolve(localPath) : null,
131
+ cloneOnDemand: localPath ? false : dispatch.repo.cloneOnDemand
132
+ },
133
+ serverLocal: {
134
+ repoRoot,
135
+ cleanupWorktrees: this.config.cleanupWorktrees
136
+ }
137
+ };
138
+ }
139
+ async sendResult(leaseId, fencingToken, result) {
140
+ const payload = {
141
+ leaseId,
142
+ fencingToken,
143
+ result
144
+ };
145
+ await this.invokeAck("DispatchResult", payload);
146
+ }
147
+ startHeartbeat() {
148
+ this.stopHeartbeat();
149
+ const intervalMs = positiveFiniteNumber(this.config.heartbeatIntervalMs, 30_000);
150
+ this.heartbeatTimer = setInterval(() => {
151
+ this.sendHeartbeat().catch(() => undefined);
152
+ }, intervalMs);
153
+ this.heartbeatTimer.unref?.();
154
+ }
155
+ stopHeartbeat() {
156
+ if (this.heartbeatTimer) {
157
+ clearInterval(this.heartbeatTimer);
158
+ this.heartbeatTimer = null;
159
+ }
160
+ }
161
+ async sendHeartbeat() {
162
+ await this.invokeAck("Heartbeat", {
163
+ status: "online",
164
+ capabilities: this.capabilities(),
165
+ version: this.config.version,
166
+ protocolVersion: this.config.protocolVersion
167
+ });
168
+ }
169
+ capabilities() {
170
+ return {
171
+ schemaVersion: 1,
172
+ activeSessionCount: this.activeSessions.size,
173
+ cliProviders: this.config.cliProviders,
174
+ providerStatuses: this.config.providerStatuses,
175
+ projectLabels: this.config.projectLabels,
176
+ repoMappings: this.config.repoMappings,
177
+ os: this.config.os,
178
+ sandboxProfiles: this.config.sandboxProfiles,
179
+ supportsApiSecretGrant: this.config.supportsApiSecretGrant
180
+ };
181
+ }
182
+ async invokeAck(methodName, payload) {
183
+ const connection = this.connection;
184
+ if (!connection) {
185
+ throw new Error("Bridge is not connected.");
186
+ }
187
+ const ack = await connection.invoke(methodName, payload);
188
+ if (!ack.accepted) {
189
+ throw new Error(ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
190
+ }
191
+ return ack;
192
+ }
193
+ async waitForClose(signal) {
194
+ if (!this.closed) {
195
+ return;
196
+ }
197
+ await Promise.race([this.closed, abortPromise(signal)]);
198
+ }
199
+ async stopConnection() {
200
+ this.stopHeartbeat();
201
+ if (this.connection) {
202
+ await this.connection.stop().catch(() => undefined);
203
+ this.connection = null;
204
+ }
205
+ }
206
+ }
207
+ export class SignalRBridgeConnectionFactory {
208
+ create(config) {
209
+ return new SignalRBridgeConnection(config);
210
+ }
211
+ }
212
+ export class SignalRBridgeConnection {
213
+ config;
214
+ connection;
215
+ constructor(config) {
216
+ this.config = config;
217
+ const url = resolveBridgeRunnerHubUrl(config.serverUrl);
218
+ url.searchParams.set("runnerId", config.runnerId);
219
+ this.connection = new HubConnectionBuilder()
220
+ .withUrl(url.toString(), {
221
+ accessTokenFactory: () => config.deviceToken
222
+ })
223
+ .configureLogging(LogLevel.Warning)
224
+ .build();
225
+ }
226
+ start() {
227
+ return this.connection.start();
228
+ }
229
+ stop() {
230
+ return this.connection.stop();
231
+ }
232
+ onClose(handler) {
233
+ this.connection.onclose(handler);
234
+ }
235
+ onDispatch(handler) {
236
+ this.connection.on("Dispatch", (raw) => {
237
+ let dispatch;
238
+ try {
239
+ dispatch = parseRunOnceDispatchPacket(raw);
240
+ }
241
+ catch {
242
+ return;
243
+ }
244
+ void handler(dispatch).catch(() => undefined);
245
+ });
246
+ }
247
+ onCancel(handler) {
248
+ this.connection.on("Cancel", (request) => {
249
+ void handler(request).catch(() => undefined);
250
+ });
251
+ }
252
+ invoke(methodName, payload) {
253
+ return this.connection.invoke(methodName, payload);
254
+ }
255
+ }
256
+ export function defaultBridgeServiceConfig(overrides = {}) {
257
+ const defaults = defaultForgeBridgeConfig();
258
+ const cliProviders = splitCsv(process.env.FORGE_CLI_PROVIDERS ?? "codex");
259
+ const config = {
260
+ serverUrl: process.env.FORGE_URL ?? "http://localhost:5208",
261
+ runnerId: process.env.FORGE_RUNNER_ID ?? "",
262
+ deviceToken: process.env.FORGE_DEVICE_TOKEN ?? "",
263
+ deviceName: process.env.FORGE_DEVICE_NAME ?? "",
264
+ defaultRepoRoot: process.env.FORGE_REPO_ROOT ?? defaults.defaultRepoRoot,
265
+ cleanupWorktrees: process.env.FORGE_CLEANUP_WORKTREES !== "false" && defaults.cleanupWorktrees,
266
+ cliProviders,
267
+ providerStatuses: Object.fromEntries(cliProviders.map((provider) => [provider, { signedIn: true, version: "" }])),
268
+ projectLabels: splitCsv(process.env.FORGE_PROJECT_LABELS ?? ""),
269
+ repoMappings: [],
270
+ os: process.platform,
271
+ sandboxProfiles: ["default"],
272
+ supportsApiSecretGrant: true,
273
+ version: process.env.FORGE_BRIDGE_VERSION ?? forgeBridgeVersion,
274
+ protocolVersion: 1,
275
+ minServerVersion: null,
276
+ heartbeatIntervalMs: positiveFiniteNumber(Number(process.env.FORGE_HEARTBEAT_INTERVAL_MS ?? 30_000), 30_000),
277
+ reconnectBackoffMs: [1000, 2000, 5000, 10000, 30000],
278
+ ...overrides
279
+ };
280
+ return {
281
+ ...config,
282
+ heartbeatIntervalMs: positiveFiniteNumber(config.heartbeatIntervalMs, 30_000)
283
+ };
284
+ }
285
+ export function resolveBridgeRunnerHubUrl(serverUrl) {
286
+ return new URL("hubs/bridge-runners", normalizeServerUrl(serverUrl));
287
+ }
288
+ function normalizeServerUrl(serverUrl) {
289
+ if (!serverUrl.trim()) {
290
+ throw new Error("Forge server URL is required.");
291
+ }
292
+ return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
293
+ }
294
+ function positiveFiniteNumber(value, fallback) {
295
+ return Number.isFinite(value) && value > 0 ? value : fallback;
296
+ }
297
+ function splitCsv(value) {
298
+ return value
299
+ .split(",")
300
+ .map((entry) => entry.trim())
301
+ .filter((entry) => entry.length > 0);
302
+ }
303
+ function findRepoMapping(dispatch, mappings) {
304
+ return mappings.find((mapping) => {
305
+ if (!mapping.localPath?.trim()) {
306
+ return false;
307
+ }
308
+ return (equalsIgnoreCase(mapping.repoId, dispatch.repo.id) ||
309
+ equalsIgnoreCase(mapping.name, dispatch.repo.name) ||
310
+ (!!mapping.remoteUrl?.trim() && equalsIgnoreCase(mapping.remoteUrl, dispatch.repo.remoteUrl)));
311
+ });
312
+ }
313
+ function equalsIgnoreCase(left, right) {
314
+ return !!left?.trim() && !!right?.trim() && left.trim().toLowerCase() === right.trim().toLowerCase();
315
+ }
316
+ function delay(ms, signal) {
317
+ if (signal?.aborted) {
318
+ return Promise.resolve();
319
+ }
320
+ return new Promise((resolve) => {
321
+ const timeout = setTimeout(resolve, ms);
322
+ timeout.unref?.();
323
+ signal?.addEventListener("abort", () => {
324
+ clearTimeout(timeout);
325
+ resolve();
326
+ }, { once: true });
327
+ });
328
+ }
329
+ function abortPromise(signal) {
330
+ if (!signal) {
331
+ return new Promise(() => undefined);
332
+ }
333
+ if (signal.aborted) {
334
+ return Promise.resolve();
335
+ }
336
+ return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
337
+ }
338
+ function exactSecrets(dispatch) {
339
+ return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
340
+ }
@@ -0,0 +1,76 @@
1
+ import type { RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchEvent } from "../session/session.types.js";
2
+ export interface BridgeServiceConfig {
3
+ readonly serverUrl: string;
4
+ readonly runnerId: string;
5
+ readonly deviceToken: string;
6
+ readonly deviceName: string;
7
+ readonly defaultRepoRoot: string;
8
+ readonly cleanupWorktrees: boolean;
9
+ readonly cliProviders: readonly string[];
10
+ readonly providerStatuses: Readonly<Record<string, BridgeProviderStatus>>;
11
+ readonly projectLabels: readonly string[];
12
+ readonly repoMappings: readonly BridgeRepoMapping[];
13
+ readonly os: string;
14
+ readonly sandboxProfiles: readonly string[];
15
+ readonly supportsApiSecretGrant: boolean;
16
+ readonly version: string;
17
+ readonly protocolVersion: number;
18
+ readonly minServerVersion?: string | null;
19
+ readonly heartbeatIntervalMs: number;
20
+ readonly reconnectBackoffMs: readonly number[];
21
+ }
22
+ export interface BridgeRepoMapping {
23
+ readonly repoId?: string | null;
24
+ readonly name?: string | null;
25
+ readonly remoteUrl?: string | null;
26
+ readonly localPath?: string | null;
27
+ }
28
+ export interface BridgeProviderStatus {
29
+ readonly signedIn?: boolean | null;
30
+ readonly version?: string | null;
31
+ }
32
+ export interface BridgeCapabilities {
33
+ readonly schemaVersion?: number | null;
34
+ readonly activeSessionCount?: number | null;
35
+ readonly cliProviders?: readonly string[] | null;
36
+ readonly providerStatuses?: Readonly<Record<string, BridgeProviderStatus>> | null;
37
+ readonly projectLabels?: readonly string[] | null;
38
+ readonly repoMappings?: readonly BridgeRepoMapping[] | null;
39
+ readonly os?: string | null;
40
+ readonly sandboxProfiles?: readonly string[] | null;
41
+ readonly supportsApiSecretGrant?: boolean | null;
42
+ }
43
+ export interface BridgeHelloRequest {
44
+ readonly version: string;
45
+ readonly protocolVersion: number;
46
+ readonly capabilities: BridgeCapabilities;
47
+ readonly minServerVersion?: string | null;
48
+ }
49
+ export interface BridgeHeartbeatRequest {
50
+ readonly status?: string | null;
51
+ readonly capabilities?: BridgeCapabilities | null;
52
+ readonly version?: string | null;
53
+ readonly protocolVersion?: number | null;
54
+ }
55
+ export interface BridgeCallbackAck {
56
+ readonly accepted: boolean;
57
+ readonly code?: string | null;
58
+ readonly message?: string | null;
59
+ }
60
+ export interface BridgeDispatchEventCallback {
61
+ readonly leaseId: string;
62
+ readonly fencingToken: string;
63
+ readonly event: RunnerDispatchEvent;
64
+ }
65
+ export interface BridgeDispatchResultCallback {
66
+ readonly leaseId: string;
67
+ readonly fencingToken: string;
68
+ readonly result: RunOnceResultPacket;
69
+ }
70
+ export interface BridgeCancelRequest {
71
+ readonly leaseId: string;
72
+ readonly fencingToken: string;
73
+ readonly reason?: string | null;
74
+ }
75
+ export type BridgeDispatchHandler = (dispatch: RunOnceDispatchPacket) => Promise<void>;
76
+ export type BridgeCancelHandler = (request: BridgeCancelRequest) => Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { ForgeBridgeService } from "./bridge.service.js";
2
+ export type { BridgeService } from "./bridge.base.js";
@@ -0,0 +1 @@
1
+ export { ForgeBridgeService } from "./bridge.service.js";
@@ -0,0 +1,3 @@
1
+ export { SignalRBridgeConnection, SignalRBridgeConnectionFactory, defaultBridgeServiceConfig } from "./bridge.service.js";
2
+ export type { BridgeConnection, BridgeConnectionFactory } from "./bridge.base.js";
3
+ export type { BridgeCallbackAck, BridgeCancelHandler, BridgeCancelRequest, BridgeCapabilities, BridgeDispatchEventCallback, BridgeDispatchHandler, BridgeDispatchResultCallback, BridgeHeartbeatRequest, BridgeHelloRequest, BridgeRepoMapping, BridgeServiceConfig } from "./bridge.types.js";
@@ -0,0 +1 @@
1
+ export { SignalRBridgeConnection, SignalRBridgeConnectionFactory, defaultBridgeServiceConfig } from "./bridge.service.js";
@@ -0,0 +1,4 @@
1
+ import type { CleanupRequest, CleanupResult } from "./cleanup.types.js";
2
+ export interface CleanupService {
3
+ run(request: CleanupRequest): Promise<CleanupResult>;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { CleanupService } from "./cleanup.base.js";
2
+ import type { CleanupRequest, CleanupResult } from "./cleanup.types.js";
3
+ export declare class NoopCleanupService implements CleanupService {
4
+ run(_request: CleanupRequest): Promise<CleanupResult>;
5
+ }
@@ -0,0 +1,5 @@
1
+ export class NoopCleanupService {
2
+ async run(_request) {
3
+ return { removedWorktrees: 0 };
4
+ }
5
+ }
@@ -0,0 +1,6 @@
1
+ export interface CleanupRequest {
2
+ readonly repoRoot: string;
3
+ }
4
+ export interface CleanupResult {
5
+ readonly removedWorktrees: number;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { NoopCleanupService } from "./cleanup.service.js";
2
+ export type { CleanupService } from "./cleanup.base.js";
@@ -0,0 +1 @@
1
+ export { NoopCleanupService } from "./cleanup.service.js";
@@ -0,0 +1,3 @@
1
+ export { NoopCleanupService } from "./cleanup.service.js";
2
+ export type { CleanupService } from "./cleanup.base.js";
3
+ export type { CleanupRequest, CleanupResult } from "./cleanup.types.js";
@@ -0,0 +1 @@
1
+ export { NoopCleanupService } from "./cleanup.service.js";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { ForgeBridgeCliService } from "./cli.service.js";
3
+ const service = new ForgeBridgeCliService();
4
+ process.exitCode = await service.run(process.argv.slice(2));
@@ -0,0 +1,7 @@
1
+ export interface CliService {
2
+ run(argv: readonly string[], io?: CliIo): Promise<number>;
3
+ }
4
+ export interface CliIo {
5
+ readonly stdout: Pick<NodeJS.WriteStream, "write">;
6
+ readonly stderr: Pick<NodeJS.WriteStream, "write">;
7
+ }
@@ -0,0 +1 @@
1
+ export {};