@uncensoredcode/openbridge 0.1.0 → 0.1.2
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/package.json +1 -1
- package/packages/cli/dist/args.js +23 -2
- package/packages/runtime/dist/tool-name-aliases.js +2 -1
- package/packages/server/dist/bridge/chat-completions/chat-completion-service.js +24 -1
- package/packages/server/dist/bridge/stores/local-session-package-store.js +49 -2
- package/packages/server/dist/bridge/stores/session-package-store.d.ts +2 -0
- package/packages/server/dist/bridge/stores/session-package-store.js +58 -1
- package/packages/server/dist/cli/index.d.ts +9 -0
- package/packages/server/dist/cli/index.js +1 -0
- package/packages/server/dist/cli/run-bridge-server-cli.d.ts +92 -0
- package/packages/server/dist/cli/run-bridge-server-cli.js +973 -46
- package/packages/server/dist/client/bridge-api-client.d.ts +81 -1
- package/packages/server/dist/client/bridge-api-client.js +184 -1
- package/packages/server/dist/client/index.d.ts +39 -1
- package/packages/server/dist/client/index.js +20 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
|
-
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { chmod, mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import process from "node:process";
|
|
5
6
|
import { createInterface } from "node:readline/promises";
|
|
@@ -10,17 +11,23 @@ import { clientModule } from "../client/index.js";
|
|
|
10
11
|
import { configModule } from "../config/index.js";
|
|
11
12
|
import { httpModule } from "../http/index.js";
|
|
12
13
|
import { securityModule } from "../security/index.js";
|
|
13
|
-
const { createBridgeChatCompletion, DEFAULT_BRIDGE_API_BASE_URL, streamBridgeChatCompletion } = clientModule;
|
|
14
|
+
const { createBridgeChatCompletion, createBridgeModel, createBridgeProvider, DEFAULT_BRIDGE_API_BASE_URL, deleteBridgeProvider, deleteBridgeProviderSessionPackage, deleteBridgeSession, getBridgeProvider, getBridgeProviderSessionPackage, getBridgeSession, listBridgeModels, listBridgeProviders, listBridgeSessions, putBridgeProviderSessionPackage, streamBridgeChatCompletion, updateBridgeProvider, checkBridgeHealth } = clientModule;
|
|
14
15
|
const { getBridgeServerStartupWarnings, loadBridgeServerConfig } = configModule;
|
|
15
16
|
const { clearLocalSessionVault, formatLiveProviderExtractionCanaryResult, runLiveProviderExtractionCanary } = bridgeModule;
|
|
16
17
|
const { sanitizeSensitiveText } = securityModule;
|
|
17
18
|
const { startBridgeApiServer } = httpModule;
|
|
19
|
+
const DEFAULT_LOG_TAIL_LINES = 50;
|
|
20
|
+
const DAEMON_READY_TIMEOUT_MS = 4_000;
|
|
21
|
+
const DAEMON_POLL_INTERVAL_MS = 100;
|
|
22
|
+
const STOP_TIMEOUT_MS = 5_000;
|
|
23
|
+
const LOG_FOLLOW_POLL_INTERVAL_MS = 250;
|
|
18
24
|
async function runBridgeServerCli(input) {
|
|
19
25
|
const stdout = input.stdout ?? process.stdout;
|
|
20
26
|
const stderr = input.stderr ?? process.stderr;
|
|
21
27
|
const stdin = input.stdin ?? process.stdin;
|
|
22
28
|
const startServer = input.startServer ?? startBridgeApiServer;
|
|
23
29
|
const runLiveCanary = input.runLiveCanary ?? runLiveProviderExtractionCanary;
|
|
30
|
+
const spawnDetachedServerProcess = input.spawnDetachedServerProcess ?? defaultSpawnDetachedServerProcess;
|
|
24
31
|
let command;
|
|
25
32
|
try {
|
|
26
33
|
command = parseBridgeServerCliArgs({
|
|
@@ -90,28 +97,199 @@ async function runBridgeServerCli(input) {
|
|
|
90
97
|
stdout.write(`Emptied session vault at ${sessionVaultPath}\n`);
|
|
91
98
|
return 0;
|
|
92
99
|
}
|
|
100
|
+
if (command.kind === "status") {
|
|
101
|
+
const status = await getServerStatus(command.config, input.fetchImpl);
|
|
102
|
+
const { logPath: _logPath, ...statusOutput } = status;
|
|
103
|
+
writeJson(stdout, statusOutput);
|
|
104
|
+
return status.running && status.healthy !== false ? 0 : 1;
|
|
105
|
+
}
|
|
106
|
+
if (command.kind === "stop") {
|
|
107
|
+
const result = await stopServer(command.config);
|
|
108
|
+
stdout.write(`${result.message}\n`);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
if (command.kind === "logs") {
|
|
112
|
+
await printServerLogs(command.config, {
|
|
113
|
+
follow: command.follow,
|
|
114
|
+
lines: command.lines,
|
|
115
|
+
stdout
|
|
116
|
+
});
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
if (command.kind === "providers-list") {
|
|
120
|
+
writeJson(stdout, await listBridgeProviders({
|
|
121
|
+
baseUrl: command.baseUrl,
|
|
122
|
+
fetchImpl: input.fetchImpl
|
|
123
|
+
}));
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
if (command.kind === "providers-get") {
|
|
127
|
+
writeJson(stdout, await getBridgeProvider({
|
|
128
|
+
baseUrl: command.baseUrl,
|
|
129
|
+
id: command.id,
|
|
130
|
+
fetchImpl: input.fetchImpl
|
|
131
|
+
}));
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
if (command.kind === "providers-add") {
|
|
135
|
+
writeJson(stdout, await createBridgeProvider({
|
|
136
|
+
baseUrl: command.baseUrl,
|
|
137
|
+
id: command.id,
|
|
138
|
+
kind: command.providerKind,
|
|
139
|
+
label: command.label,
|
|
140
|
+
enabled: command.enabled,
|
|
141
|
+
config: command.config,
|
|
142
|
+
fetchImpl: input.fetchImpl
|
|
143
|
+
}));
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
if (command.kind === "providers-remove") {
|
|
147
|
+
writeJson(stdout, await deleteBridgeProvider({
|
|
148
|
+
baseUrl: command.baseUrl,
|
|
149
|
+
id: command.id,
|
|
150
|
+
fetchImpl: input.fetchImpl
|
|
151
|
+
}));
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
if (command.kind === "providers-enable" || command.kind === "providers-disable") {
|
|
155
|
+
writeJson(stdout, await updateBridgeProvider({
|
|
156
|
+
baseUrl: command.baseUrl,
|
|
157
|
+
id: command.id,
|
|
158
|
+
patch: {
|
|
159
|
+
enabled: command.kind === "providers-enable"
|
|
160
|
+
},
|
|
161
|
+
fetchImpl: input.fetchImpl
|
|
162
|
+
}));
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
if (command.kind === "providers-import-session") {
|
|
166
|
+
const sessionPackage = await readSessionPackageInput({
|
|
167
|
+
filePath: command.filePath,
|
|
168
|
+
stdin,
|
|
169
|
+
stderr
|
|
170
|
+
});
|
|
171
|
+
writeJson(stdout, await putBridgeProviderSessionPackage({
|
|
172
|
+
baseUrl: command.baseUrl,
|
|
173
|
+
id: command.id,
|
|
174
|
+
sessionPackage,
|
|
175
|
+
fetchImpl: input.fetchImpl
|
|
176
|
+
}));
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
if (command.kind === "providers-session-status") {
|
|
180
|
+
writeJson(stdout, await getBridgeProviderSessionPackage({
|
|
181
|
+
baseUrl: command.baseUrl,
|
|
182
|
+
id: command.id,
|
|
183
|
+
fetchImpl: input.fetchImpl
|
|
184
|
+
}));
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
if (command.kind === "providers-clear-session") {
|
|
188
|
+
writeJson(stdout, await deleteBridgeProviderSessionPackage({
|
|
189
|
+
baseUrl: command.baseUrl,
|
|
190
|
+
id: command.id,
|
|
191
|
+
fetchImpl: input.fetchImpl
|
|
192
|
+
}));
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
if (command.kind === "models-list") {
|
|
196
|
+
writeJson(stdout, await listBridgeModels({
|
|
197
|
+
baseUrl: command.baseUrl,
|
|
198
|
+
fetchImpl: input.fetchImpl
|
|
199
|
+
}));
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
if (command.kind === "models-add") {
|
|
203
|
+
writeJson(stdout, await createBridgeModel({
|
|
204
|
+
baseUrl: command.baseUrl,
|
|
205
|
+
provider: command.provider,
|
|
206
|
+
model: command.model,
|
|
207
|
+
fetchImpl: input.fetchImpl
|
|
208
|
+
}));
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
if (command.kind === "sessions-list") {
|
|
212
|
+
writeJson(stdout, await listBridgeSessions({
|
|
213
|
+
baseUrl: command.baseUrl,
|
|
214
|
+
fetchImpl: input.fetchImpl
|
|
215
|
+
}));
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
if (command.kind === "sessions-get") {
|
|
219
|
+
writeJson(stdout, await getBridgeSession({
|
|
220
|
+
baseUrl: command.baseUrl,
|
|
221
|
+
id: command.id,
|
|
222
|
+
fetchImpl: input.fetchImpl
|
|
223
|
+
}));
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
if (command.kind === "sessions-remove") {
|
|
227
|
+
writeJson(stdout, await deleteBridgeSession({
|
|
228
|
+
baseUrl: command.baseUrl,
|
|
229
|
+
id: command.id,
|
|
230
|
+
fetchImpl: input.fetchImpl
|
|
231
|
+
}));
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
93
234
|
for (const warning of getBridgeServerStartupWarnings(command.config)) {
|
|
94
235
|
stderr.write(`Warning: ${warning}\n`);
|
|
95
236
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
237
|
+
const preparedConfig = await ensureCliVaultKey(command.config, {
|
|
238
|
+
env: input.env ?? process.env,
|
|
239
|
+
stdin,
|
|
240
|
+
stdout,
|
|
241
|
+
stderr,
|
|
242
|
+
promptForVaultKey: input.promptForVaultKey
|
|
243
|
+
});
|
|
244
|
+
if (!command.foreground) {
|
|
245
|
+
const status = await getServerStatus(preparedConfig, input.fetchImpl);
|
|
246
|
+
if (status.running) {
|
|
247
|
+
throw new Error(`Bridge server is already running${status.pid ? ` (pid ${status.pid})` : ""}.`);
|
|
248
|
+
}
|
|
249
|
+
if (preparedConfig.port === 0) {
|
|
250
|
+
throw new Error("Detached start requires a fixed port. Use --foreground with --port 0.");
|
|
251
|
+
}
|
|
252
|
+
const processFiles = getServerProcessFiles(preparedConfig);
|
|
253
|
+
await ensureServerProcessDirectories(processFiles);
|
|
254
|
+
const daemonArgv = toForegroundStartArgv(input.argv);
|
|
255
|
+
const daemon = await spawnDetachedServerProcess({
|
|
256
|
+
argv: daemonArgv,
|
|
257
|
+
env: {
|
|
258
|
+
...(input.env ?? process.env)
|
|
259
|
+
},
|
|
260
|
+
logPath: processFiles.logPath,
|
|
261
|
+
cwd: process.cwd()
|
|
262
|
+
});
|
|
263
|
+
if (!Number.isInteger(daemon.pid) || daemon.pid <= 0) {
|
|
264
|
+
throw new Error("Failed to spawn detached bridge server process.");
|
|
265
|
+
}
|
|
266
|
+
const readyState = await waitForServerReady({
|
|
267
|
+
config: preparedConfig,
|
|
268
|
+
pid: daemon.pid,
|
|
269
|
+
statePath: processFiles.statePath,
|
|
270
|
+
fetchImpl: input.fetchImpl
|
|
271
|
+
});
|
|
272
|
+
const statusMessage = readyState === null
|
|
273
|
+
? [
|
|
274
|
+
`Bridge server started in background (pid ${daemon.pid}).`,
|
|
275
|
+
`Startup is still pending; check status with "openbridge status".`
|
|
276
|
+
].join("\n")
|
|
277
|
+
: [
|
|
278
|
+
`Bridge server started in background (pid ${readyState.pid}).`,
|
|
279
|
+
`Base URL: ${readyState.baseUrl}`
|
|
280
|
+
].join("\n");
|
|
281
|
+
stdout.write(`${statusMessage}\n`);
|
|
282
|
+
return 0;
|
|
107
283
|
}
|
|
108
284
|
const server = await startServer({
|
|
109
|
-
config:
|
|
285
|
+
config: preparedConfig
|
|
110
286
|
});
|
|
287
|
+
const processState = await writeRunningServerState(preparedConfig, server);
|
|
288
|
+
registerForegroundServerCleanup(server, processState);
|
|
111
289
|
input.onServerStarted?.(server);
|
|
112
290
|
const address = server.address();
|
|
113
291
|
if (typeof address === "object" && address) {
|
|
114
|
-
stdout.write(`Bridge server listening on http://${address.address}:${address.port}\n`);
|
|
292
|
+
stdout.write(`Bridge server listening on http://${formatHostForUrl(address.address)}:${address.port}\n`);
|
|
115
293
|
}
|
|
116
294
|
return 0;
|
|
117
295
|
}
|
|
@@ -133,11 +311,28 @@ function parseBridgeServerCliArgs(input) {
|
|
|
133
311
|
onStart(options) {
|
|
134
312
|
parsedCommand = {
|
|
135
313
|
kind: "serve",
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
314
|
+
foreground: options.foreground === true,
|
|
315
|
+
config: loadCliServerConfig(env, options)
|
|
316
|
+
};
|
|
317
|
+
},
|
|
318
|
+
onStatus(options) {
|
|
319
|
+
parsedCommand = {
|
|
320
|
+
kind: "status",
|
|
321
|
+
config: loadCliServerConfig(env, options)
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
onStop(options) {
|
|
325
|
+
parsedCommand = {
|
|
326
|
+
kind: "stop",
|
|
327
|
+
config: loadCliServerConfig(env, options)
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
onLogs(options) {
|
|
331
|
+
parsedCommand = {
|
|
332
|
+
kind: "logs",
|
|
333
|
+
config: loadCliServerConfig(env, options),
|
|
334
|
+
follow: options.follow === true,
|
|
335
|
+
lines: readPositiveInteger(options.lines, "lines", DEFAULT_LOG_TAIL_LINES)
|
|
141
336
|
};
|
|
142
337
|
},
|
|
143
338
|
onChat(options) {
|
|
@@ -178,6 +373,107 @@ function parseBridgeServerCliArgs(input) {
|
|
|
178
373
|
sessionVaultPath: optionalNonEmptyString(options.sessionVaultPath, "session-vault-path") ?? undefined
|
|
179
374
|
})
|
|
180
375
|
};
|
|
376
|
+
},
|
|
377
|
+
onProvidersList(options) {
|
|
378
|
+
parsedCommand = {
|
|
379
|
+
kind: "providers-list",
|
|
380
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env)
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
onProvidersGet(id, options) {
|
|
384
|
+
parsedCommand = {
|
|
385
|
+
kind: "providers-get",
|
|
386
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
387
|
+
id: requireNonEmptyString(id, "id")
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
onProvidersAdd(options) {
|
|
391
|
+
parsedCommand = {
|
|
392
|
+
kind: "providers-add",
|
|
393
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
394
|
+
id: requireNonEmptyString(options.id, "id"),
|
|
395
|
+
providerKind: requireNonEmptyString(options.kind, "kind"),
|
|
396
|
+
label: requireNonEmptyString(options.label, "label"),
|
|
397
|
+
enabled: options.disable !== true,
|
|
398
|
+
config: options.config ? parseJsonObject(options.config, "config") : undefined
|
|
399
|
+
};
|
|
400
|
+
},
|
|
401
|
+
onProvidersRemove(id, options) {
|
|
402
|
+
parsedCommand = {
|
|
403
|
+
kind: "providers-remove",
|
|
404
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
405
|
+
id: requireNonEmptyString(id, "id")
|
|
406
|
+
};
|
|
407
|
+
},
|
|
408
|
+
onProvidersEnable(id, options) {
|
|
409
|
+
parsedCommand = {
|
|
410
|
+
kind: "providers-enable",
|
|
411
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
412
|
+
id: requireNonEmptyString(id, "id")
|
|
413
|
+
};
|
|
414
|
+
},
|
|
415
|
+
onProvidersDisable(id, options) {
|
|
416
|
+
parsedCommand = {
|
|
417
|
+
kind: "providers-disable",
|
|
418
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
419
|
+
id: requireNonEmptyString(id, "id")
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
onProvidersImportSession(id, options) {
|
|
423
|
+
parsedCommand = {
|
|
424
|
+
kind: "providers-import-session",
|
|
425
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
426
|
+
id: requireNonEmptyString(id, "id"),
|
|
427
|
+
filePath: optionalNonEmptyString(options.file, "file") ?? undefined
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
onProvidersSessionStatus(id, options) {
|
|
431
|
+
parsedCommand = {
|
|
432
|
+
kind: "providers-session-status",
|
|
433
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
434
|
+
id: requireNonEmptyString(id, "id")
|
|
435
|
+
};
|
|
436
|
+
},
|
|
437
|
+
onProvidersClearSession(id, options) {
|
|
438
|
+
parsedCommand = {
|
|
439
|
+
kind: "providers-clear-session",
|
|
440
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
441
|
+
id: requireNonEmptyString(id, "id")
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
onModelsList(options) {
|
|
445
|
+
parsedCommand = {
|
|
446
|
+
kind: "models-list",
|
|
447
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env)
|
|
448
|
+
};
|
|
449
|
+
},
|
|
450
|
+
onModelsAdd(options) {
|
|
451
|
+
parsedCommand = {
|
|
452
|
+
kind: "models-add",
|
|
453
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
454
|
+
provider: requireNonEmptyString(options.provider, "provider"),
|
|
455
|
+
model: requireNonEmptyString(options.model, "model")
|
|
456
|
+
};
|
|
457
|
+
},
|
|
458
|
+
onSessionsList(options) {
|
|
459
|
+
parsedCommand = {
|
|
460
|
+
kind: "sessions-list",
|
|
461
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env)
|
|
462
|
+
};
|
|
463
|
+
},
|
|
464
|
+
onSessionsGet(id, options) {
|
|
465
|
+
parsedCommand = {
|
|
466
|
+
kind: "sessions-get",
|
|
467
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
468
|
+
id: requireNonEmptyString(id, "id")
|
|
469
|
+
};
|
|
470
|
+
},
|
|
471
|
+
onSessionsRemove(id, options) {
|
|
472
|
+
parsedCommand = {
|
|
473
|
+
kind: "sessions-remove",
|
|
474
|
+
baseUrl: resolveBaseUrl(options.baseUrl, env),
|
|
475
|
+
id: requireNonEmptyString(id, "id")
|
|
476
|
+
};
|
|
181
477
|
}
|
|
182
478
|
});
|
|
183
479
|
const normalizedArgv = args.length === 0 || args[0]?.startsWith("--") ? ["start", ...args] : args;
|
|
@@ -197,31 +493,6 @@ function parseBridgeServerCliArgs(input) {
|
|
|
197
493
|
function getBridgeServerCliHelpText() {
|
|
198
494
|
return buildBridgeServerCliProgram(process.env).helpInformation();
|
|
199
495
|
}
|
|
200
|
-
function optionalNonEmptyString(value, key) {
|
|
201
|
-
if (value === undefined) {
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
const trimmed = value.trim();
|
|
205
|
-
if (!trimmed) {
|
|
206
|
-
throw new Error(`${key} must be a non-empty string.`);
|
|
207
|
-
}
|
|
208
|
-
return trimmed;
|
|
209
|
-
}
|
|
210
|
-
function requireNonEmptyString(value, key) {
|
|
211
|
-
const normalized = optionalNonEmptyString(value, key);
|
|
212
|
-
if (!normalized) {
|
|
213
|
-
throw new Error(`${key} is required.`);
|
|
214
|
-
}
|
|
215
|
-
return normalized;
|
|
216
|
-
}
|
|
217
|
-
function formatCliError(error) {
|
|
218
|
-
if (error instanceof ZodError) {
|
|
219
|
-
return sanitizeSensitiveText(error.issues
|
|
220
|
-
.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`)
|
|
221
|
-
.join("; "));
|
|
222
|
-
}
|
|
223
|
-
return sanitizeSensitiveText(error instanceof Error ? error.message : String(error));
|
|
224
|
-
}
|
|
225
496
|
function buildBridgeServerCliProgram(env, handlers = {}) {
|
|
226
497
|
const program = new Command();
|
|
227
498
|
program
|
|
@@ -235,13 +506,48 @@ function buildBridgeServerCliProgram(env, handlers = {}) {
|
|
|
235
506
|
});
|
|
236
507
|
program
|
|
237
508
|
.command("start")
|
|
238
|
-
.description("Start the standalone bridge HTTP server.")
|
|
509
|
+
.description("Start the standalone bridge HTTP server. Defaults to detached/background mode.")
|
|
239
510
|
.option("--host <host>", "Bind host", trimOrUndefined(env.BRIDGE_SERVER_HOST) ?? "127.0.0.1")
|
|
240
511
|
.option("--port <port>", "Bind port", trimOrUndefined(env.BRIDGE_SERVER_PORT))
|
|
512
|
+
.option("--state-root <path>", "State root for bridge artifacts")
|
|
513
|
+
.option("--runtime-root <path>", "Runtime root for workspace execution")
|
|
241
514
|
.option("--token <token>", "Optional local bridge auth token", trimOrUndefined(env.BRIDGE_AUTH_TOKEN))
|
|
515
|
+
.option("--foreground", "Keep the server attached to the current terminal")
|
|
242
516
|
.action((options) => {
|
|
243
517
|
handlers.onStart?.(options);
|
|
244
518
|
});
|
|
519
|
+
program
|
|
520
|
+
.command("status")
|
|
521
|
+
.description("Show the detached bridge server status.")
|
|
522
|
+
.option("--host <host>", "Expected bind host", trimOrUndefined(env.BRIDGE_SERVER_HOST))
|
|
523
|
+
.option("--port <port>", "Expected bind port", trimOrUndefined(env.BRIDGE_SERVER_PORT))
|
|
524
|
+
.option("--state-root <path>", "State root for bridge artifacts")
|
|
525
|
+
.option("--runtime-root <path>", "Runtime root for workspace execution")
|
|
526
|
+
.action((options) => {
|
|
527
|
+
handlers.onStatus?.(options);
|
|
528
|
+
});
|
|
529
|
+
program
|
|
530
|
+
.command("stop")
|
|
531
|
+
.description("Stop the detached bridge server.")
|
|
532
|
+
.option("--host <host>", "Expected bind host", trimOrUndefined(env.BRIDGE_SERVER_HOST))
|
|
533
|
+
.option("--port <port>", "Expected bind port", trimOrUndefined(env.BRIDGE_SERVER_PORT))
|
|
534
|
+
.option("--state-root <path>", "State root for bridge artifacts")
|
|
535
|
+
.option("--runtime-root <path>", "Runtime root for workspace execution")
|
|
536
|
+
.action((options) => {
|
|
537
|
+
handlers.onStop?.(options);
|
|
538
|
+
});
|
|
539
|
+
program
|
|
540
|
+
.command("logs")
|
|
541
|
+
.description("Print bridge server logs.")
|
|
542
|
+
.option("--host <host>", "Expected bind host", trimOrUndefined(env.BRIDGE_SERVER_HOST))
|
|
543
|
+
.option("--port <port>", "Expected bind port", trimOrUndefined(env.BRIDGE_SERVER_PORT))
|
|
544
|
+
.option("--state-root <path>", "State root for bridge artifacts")
|
|
545
|
+
.option("--runtime-root <path>", "Runtime root for workspace execution")
|
|
546
|
+
.option("--follow", "Follow log output")
|
|
547
|
+
.option("--lines <count>", "Number of trailing log lines to print", String(DEFAULT_LOG_TAIL_LINES))
|
|
548
|
+
.action((options) => {
|
|
549
|
+
handlers.onLogs?.(options);
|
|
550
|
+
});
|
|
245
551
|
program
|
|
246
552
|
.command("chat")
|
|
247
553
|
.description("Send a chat completion request through the standalone bridge HTTP API.")
|
|
@@ -283,8 +589,222 @@ function buildBridgeServerCliProgram(env, handlers = {}) {
|
|
|
283
589
|
: undefined
|
|
284
590
|
});
|
|
285
591
|
});
|
|
592
|
+
const providersCommand = program
|
|
593
|
+
.command("providers")
|
|
594
|
+
.description("Manage installed providers and provider session packages.");
|
|
595
|
+
providersCommand
|
|
596
|
+
.command("list")
|
|
597
|
+
.description("List providers.")
|
|
598
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
599
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
600
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
601
|
+
.action((options) => {
|
|
602
|
+
handlers.onProvidersList?.(options);
|
|
603
|
+
});
|
|
604
|
+
providersCommand
|
|
605
|
+
.command("get <id>")
|
|
606
|
+
.description("Get a provider by id.")
|
|
607
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
608
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
609
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
610
|
+
.action((id, options) => {
|
|
611
|
+
handlers.onProvidersGet?.(id, options);
|
|
612
|
+
});
|
|
613
|
+
providersCommand
|
|
614
|
+
.command("add")
|
|
615
|
+
.description("Create a provider.")
|
|
616
|
+
.requiredOption("--id <id>", "Provider id")
|
|
617
|
+
.requiredOption("--kind <kind>", "Provider kind")
|
|
618
|
+
.requiredOption("--label <label>", "Provider label")
|
|
619
|
+
.option("--disable", "Create the provider in a disabled state")
|
|
620
|
+
.option("--config <json>", "Provider config JSON object")
|
|
621
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
622
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
623
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
624
|
+
.action((options) => {
|
|
625
|
+
handlers.onProvidersAdd?.(options);
|
|
626
|
+
});
|
|
627
|
+
providersCommand
|
|
628
|
+
.command("remove <id>")
|
|
629
|
+
.description("Delete a provider.")
|
|
630
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
631
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
632
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
633
|
+
.action((id, options) => {
|
|
634
|
+
handlers.onProvidersRemove?.(id, options);
|
|
635
|
+
});
|
|
636
|
+
providersCommand
|
|
637
|
+
.command("enable <id>")
|
|
638
|
+
.description("Enable a provider.")
|
|
639
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
640
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
641
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
642
|
+
.action((id, options) => {
|
|
643
|
+
handlers.onProvidersEnable?.(id, options);
|
|
644
|
+
});
|
|
645
|
+
providersCommand
|
|
646
|
+
.command("disable <id>")
|
|
647
|
+
.description("Disable a provider.")
|
|
648
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
649
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
650
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
651
|
+
.action((id, options) => {
|
|
652
|
+
handlers.onProvidersDisable?.(id, options);
|
|
653
|
+
});
|
|
654
|
+
providersCommand
|
|
655
|
+
.command("import-session <id>")
|
|
656
|
+
.description("Install or replace a provider session package from stdin or a JSON file.")
|
|
657
|
+
.option("--file <path>", "Read the session package from a JSON file")
|
|
658
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
659
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
660
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
661
|
+
.action((id, options) => {
|
|
662
|
+
handlers.onProvidersImportSession?.(id, options);
|
|
663
|
+
});
|
|
664
|
+
providersCommand
|
|
665
|
+
.command("session-status <id>")
|
|
666
|
+
.description("Show the safe session-package status for a provider.")
|
|
667
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
668
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
669
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
670
|
+
.action((id, options) => {
|
|
671
|
+
handlers.onProvidersSessionStatus?.(id, options);
|
|
672
|
+
});
|
|
673
|
+
providersCommand
|
|
674
|
+
.command("clear-session <id>")
|
|
675
|
+
.description("Delete a provider session package.")
|
|
676
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
677
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
678
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
679
|
+
.action((id, options) => {
|
|
680
|
+
handlers.onProvidersClearSession?.(id, options);
|
|
681
|
+
});
|
|
682
|
+
const modelsCommand = program.command("models").description("Manage exposed bridge models.");
|
|
683
|
+
modelsCommand
|
|
684
|
+
.command("list")
|
|
685
|
+
.description("List models.")
|
|
686
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
687
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
688
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
689
|
+
.action((options) => {
|
|
690
|
+
handlers.onModelsList?.(options);
|
|
691
|
+
});
|
|
692
|
+
modelsCommand
|
|
693
|
+
.command("add")
|
|
694
|
+
.description("Add a model id to an existing provider.")
|
|
695
|
+
.requiredOption("--provider <id>", "Provider id")
|
|
696
|
+
.requiredOption("--model <id>", "Model id")
|
|
697
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
698
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
699
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
700
|
+
.action((options) => {
|
|
701
|
+
handlers.onModelsAdd?.(options);
|
|
702
|
+
});
|
|
703
|
+
const sessionsCommand = program.command("sessions").description("Manage bridge sessions.");
|
|
704
|
+
sessionsCommand
|
|
705
|
+
.command("list")
|
|
706
|
+
.description("List sessions.")
|
|
707
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
708
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
709
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
710
|
+
.action((options) => {
|
|
711
|
+
handlers.onSessionsList?.(options);
|
|
712
|
+
});
|
|
713
|
+
sessionsCommand
|
|
714
|
+
.command("get <id>")
|
|
715
|
+
.description("Get a session by id.")
|
|
716
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
717
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
718
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
719
|
+
.action((id, options) => {
|
|
720
|
+
handlers.onSessionsGet?.(id, options);
|
|
721
|
+
});
|
|
722
|
+
sessionsCommand
|
|
723
|
+
.command("remove <id>")
|
|
724
|
+
.description("Delete a session.")
|
|
725
|
+
.option("--base-url <url>", "Bridge base URL", trimOrUndefined(env.BRIDGE_API_BASE_URL) ??
|
|
726
|
+
trimOrUndefined(env.BRIDGE_SERVER_BASE_URL) ??
|
|
727
|
+
DEFAULT_BRIDGE_API_BASE_URL)
|
|
728
|
+
.action((id, options) => {
|
|
729
|
+
handlers.onSessionsRemove?.(id, options);
|
|
730
|
+
});
|
|
286
731
|
return program;
|
|
287
732
|
}
|
|
733
|
+
function loadCliServerConfig(env, options) {
|
|
734
|
+
return loadBridgeServerConfig(env, {
|
|
735
|
+
host: optionalNonEmptyString(options.host, "host") ?? undefined,
|
|
736
|
+
port: options.port,
|
|
737
|
+
authToken: "token" in options
|
|
738
|
+
? (optionalNonEmptyString(options.token, "token") ?? undefined)
|
|
739
|
+
: undefined,
|
|
740
|
+
stateRoot: optionalNonEmptyString(options.stateRoot, "state-root") ?? undefined,
|
|
741
|
+
runtimeRoot: optionalNonEmptyString(options.runtimeRoot, "runtime-root") ?? undefined
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
function resolveBaseUrl(value, env) {
|
|
745
|
+
return (optionalNonEmptyString(value, "base-url") ??
|
|
746
|
+
optionalNonEmptyString(env.BRIDGE_API_BASE_URL, "BRIDGE_API_BASE_URL") ??
|
|
747
|
+
optionalNonEmptyString(env.BRIDGE_SERVER_BASE_URL, "BRIDGE_SERVER_BASE_URL") ??
|
|
748
|
+
DEFAULT_BRIDGE_API_BASE_URL);
|
|
749
|
+
}
|
|
750
|
+
function optionalNonEmptyString(value, key) {
|
|
751
|
+
if (value === undefined) {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
const trimmed = value.trim();
|
|
755
|
+
if (!trimmed) {
|
|
756
|
+
throw new Error(`${key} must be a non-empty string.`);
|
|
757
|
+
}
|
|
758
|
+
return trimmed;
|
|
759
|
+
}
|
|
760
|
+
function requireNonEmptyString(value, key) {
|
|
761
|
+
const normalized = optionalNonEmptyString(value, key);
|
|
762
|
+
if (!normalized) {
|
|
763
|
+
throw new Error(`${key} is required.`);
|
|
764
|
+
}
|
|
765
|
+
return normalized;
|
|
766
|
+
}
|
|
767
|
+
function readPositiveInteger(value, key, fallback) {
|
|
768
|
+
if (value === undefined) {
|
|
769
|
+
return fallback;
|
|
770
|
+
}
|
|
771
|
+
const normalized = optionalNonEmptyString(value, key);
|
|
772
|
+
if (!normalized) {
|
|
773
|
+
return fallback;
|
|
774
|
+
}
|
|
775
|
+
if (!/^\d+$/.test(normalized)) {
|
|
776
|
+
throw new Error(`${key} must be a positive integer.`);
|
|
777
|
+
}
|
|
778
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
779
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
780
|
+
throw new Error(`${key} must be a positive integer.`);
|
|
781
|
+
}
|
|
782
|
+
return parsed;
|
|
783
|
+
}
|
|
784
|
+
function parseJsonObject(raw, key) {
|
|
785
|
+
let parsed;
|
|
786
|
+
try {
|
|
787
|
+
parsed = JSON.parse(raw);
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
throw new Error(`${key} must be valid JSON.`);
|
|
791
|
+
}
|
|
792
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
793
|
+
throw new Error(`${key} must be a JSON object.`);
|
|
794
|
+
}
|
|
795
|
+
return parsed;
|
|
796
|
+
}
|
|
797
|
+
function writeJson(stream, value) {
|
|
798
|
+
stream.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
799
|
+
}
|
|
800
|
+
function formatCliError(error) {
|
|
801
|
+
if (error instanceof ZodError) {
|
|
802
|
+
return sanitizeSensitiveText(error.issues
|
|
803
|
+
.map((issue) => `${issue.path.join(".") || "config"}: ${issue.message}`)
|
|
804
|
+
.join("; "));
|
|
805
|
+
}
|
|
806
|
+
return sanitizeSensitiveText(error instanceof Error ? error.message : String(error));
|
|
807
|
+
}
|
|
288
808
|
function normalizeCommanderError(error) {
|
|
289
809
|
if (error instanceof Error && "code" in error) {
|
|
290
810
|
const code = String(error.code);
|
|
@@ -346,6 +866,401 @@ async function defaultPromptForVaultKey(input) {
|
|
|
346
866
|
readline.close();
|
|
347
867
|
}
|
|
348
868
|
}
|
|
869
|
+
async function readSessionPackageInput(input) {
|
|
870
|
+
const raw = input.filePath
|
|
871
|
+
? await readFile(path.resolve(input.filePath), "utf8")
|
|
872
|
+
: await readSessionPackageFromStdin(input.stdin, input.stderr);
|
|
873
|
+
return parseJsonObject(raw, "session package");
|
|
874
|
+
}
|
|
875
|
+
async function readSessionPackageFromStdin(stdin, stderr) {
|
|
876
|
+
if (stdin.isTTY) {
|
|
877
|
+
stderr.write("Paste the session package JSON, then press Ctrl-D to submit.\n");
|
|
878
|
+
}
|
|
879
|
+
return await readStreamToString(stdin);
|
|
880
|
+
}
|
|
881
|
+
function readStreamToString(stream) {
|
|
882
|
+
return new Promise((resolve, reject) => {
|
|
883
|
+
let data = "";
|
|
884
|
+
stream.setEncoding("utf8");
|
|
885
|
+
stream.on("data", (chunk) => {
|
|
886
|
+
data += chunk;
|
|
887
|
+
});
|
|
888
|
+
stream.once("end", () => resolve(data));
|
|
889
|
+
stream.once("error", reject);
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
function getServerProcessFiles(config) {
|
|
893
|
+
const runRoot = path.join(config.stateRoot, "run");
|
|
894
|
+
const logRoot = path.join(config.stateRoot, "logs");
|
|
895
|
+
return {
|
|
896
|
+
statePath: path.join(runRoot, "server-process.json"),
|
|
897
|
+
runRoot,
|
|
898
|
+
logPath: path.join(logRoot, "server.log"),
|
|
899
|
+
logRoot
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
async function ensureServerProcessDirectories(files) {
|
|
903
|
+
await mkdir(files.runRoot, {
|
|
904
|
+
recursive: true,
|
|
905
|
+
mode: 0o700
|
|
906
|
+
});
|
|
907
|
+
await mkdir(files.logRoot, {
|
|
908
|
+
recursive: true,
|
|
909
|
+
mode: 0o700
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
async function writeRunningServerState(config, server) {
|
|
913
|
+
const files = getServerProcessFiles(config);
|
|
914
|
+
await ensureServerProcessDirectories(files);
|
|
915
|
+
const address = server.address();
|
|
916
|
+
if (!address || typeof address === "string") {
|
|
917
|
+
throw new Error("Bridge server did not expose a network address.");
|
|
918
|
+
}
|
|
919
|
+
const state = {
|
|
920
|
+
pid: process.pid,
|
|
921
|
+
baseUrl: `http://${formatHostForUrl(address.address)}:${address.port}`,
|
|
922
|
+
host: address.address,
|
|
923
|
+
port: address.port,
|
|
924
|
+
logPath: files.logPath,
|
|
925
|
+
startedAt: new Date().toISOString(),
|
|
926
|
+
stateRoot: config.stateRoot
|
|
927
|
+
};
|
|
928
|
+
await writeServerProcessState(files.statePath, state);
|
|
929
|
+
return state;
|
|
930
|
+
}
|
|
931
|
+
async function writeServerProcessState(statePath, state) {
|
|
932
|
+
await mkdir(path.dirname(statePath), {
|
|
933
|
+
recursive: true,
|
|
934
|
+
mode: 0o700
|
|
935
|
+
});
|
|
936
|
+
await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, {
|
|
937
|
+
encoding: "utf8",
|
|
938
|
+
mode: 0o600
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
async function readServerProcessState(statePath) {
|
|
942
|
+
const raw = await readOptionalFile(statePath);
|
|
943
|
+
if (!raw) {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
let parsed;
|
|
947
|
+
try {
|
|
948
|
+
parsed = JSON.parse(raw);
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
const value = parsed;
|
|
957
|
+
if (!Number.isInteger(value.pid) ||
|
|
958
|
+
typeof value.baseUrl !== "string" ||
|
|
959
|
+
typeof value.host !== "string" ||
|
|
960
|
+
!Number.isInteger(value.port) ||
|
|
961
|
+
typeof value.logPath !== "string" ||
|
|
962
|
+
typeof value.startedAt !== "string" ||
|
|
963
|
+
typeof value.stateRoot !== "string") {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
return value;
|
|
967
|
+
}
|
|
968
|
+
async function deleteServerProcessState(statePath) {
|
|
969
|
+
try {
|
|
970
|
+
await rm(statePath, {
|
|
971
|
+
force: true
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
if (error.code !== "ENOENT") {
|
|
976
|
+
throw error;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function registerForegroundServerCleanup(server, state) {
|
|
981
|
+
if (typeof server.once !== "function" || typeof server.close !== "function") {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
let shuttingDown = false;
|
|
985
|
+
const statePath = path.join(state.stateRoot, "run", "server-process.json");
|
|
986
|
+
const cleanup = () => {
|
|
987
|
+
void deleteServerProcessState(statePath);
|
|
988
|
+
};
|
|
989
|
+
server.once("close", cleanup);
|
|
990
|
+
const handleSignal = (signal) => {
|
|
991
|
+
if (shuttingDown) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
shuttingDown = true;
|
|
995
|
+
void closeServer(server)
|
|
996
|
+
.catch(() => { })
|
|
997
|
+
.finally(() => {
|
|
998
|
+
cleanup();
|
|
999
|
+
process.exit(signal === "SIGINT" ? 130 : 0);
|
|
1000
|
+
});
|
|
1001
|
+
};
|
|
1002
|
+
process.once("SIGINT", () => {
|
|
1003
|
+
handleSignal("SIGINT");
|
|
1004
|
+
});
|
|
1005
|
+
process.once("SIGTERM", () => {
|
|
1006
|
+
handleSignal("SIGTERM");
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
async function getServerStatus(config, fetchImpl) {
|
|
1010
|
+
const files = getServerProcessFiles(config);
|
|
1011
|
+
const state = await readServerProcessState(files.statePath);
|
|
1012
|
+
const pid = state?.pid ?? null;
|
|
1013
|
+
const baseUrl = state?.baseUrl ?? `http://${formatHostForUrl(config.host)}:${config.port}`;
|
|
1014
|
+
const logPath = state?.logPath ?? files.logPath;
|
|
1015
|
+
const running = state ? isProcessRunning(state.pid) : false;
|
|
1016
|
+
let healthy = null;
|
|
1017
|
+
if (running) {
|
|
1018
|
+
try {
|
|
1019
|
+
await checkBridgeHealth({
|
|
1020
|
+
baseUrl,
|
|
1021
|
+
fetchImpl
|
|
1022
|
+
});
|
|
1023
|
+
healthy = true;
|
|
1024
|
+
}
|
|
1025
|
+
catch {
|
|
1026
|
+
healthy = false;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
else if (state) {
|
|
1030
|
+
await deleteServerProcessState(files.statePath);
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
running,
|
|
1034
|
+
healthy,
|
|
1035
|
+
pid,
|
|
1036
|
+
baseUrl,
|
|
1037
|
+
logPath,
|
|
1038
|
+
startedAt: state?.startedAt ?? null
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
async function stopServer(config) {
|
|
1042
|
+
const files = getServerProcessFiles(config);
|
|
1043
|
+
const state = await readServerProcessState(files.statePath);
|
|
1044
|
+
if (!state || !isProcessRunning(state.pid)) {
|
|
1045
|
+
await deleteServerProcessState(files.statePath);
|
|
1046
|
+
return {
|
|
1047
|
+
stopped: false,
|
|
1048
|
+
message: "Bridge server is not running."
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
process.kill(state.pid, "SIGTERM");
|
|
1052
|
+
const terminated = await waitForProcessExit(state.pid, STOP_TIMEOUT_MS);
|
|
1053
|
+
if (!terminated) {
|
|
1054
|
+
process.kill(state.pid, "SIGKILL");
|
|
1055
|
+
const killed = await waitForProcessExit(state.pid, 1_000);
|
|
1056
|
+
if (!killed) {
|
|
1057
|
+
throw new Error(`Failed to stop bridge server pid ${state.pid}.`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
await deleteServerProcessState(files.statePath);
|
|
1061
|
+
return {
|
|
1062
|
+
stopped: true,
|
|
1063
|
+
message: `Stopped bridge server pid ${state.pid}.`
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
async function printServerLogs(config, input) {
|
|
1067
|
+
const files = getServerProcessFiles(config);
|
|
1068
|
+
const content = await readOptionalRawFile(files.logPath);
|
|
1069
|
+
if (content) {
|
|
1070
|
+
const tail = tailLines(content, input.lines);
|
|
1071
|
+
if (tail) {
|
|
1072
|
+
input.stdout.write(tail);
|
|
1073
|
+
if (!tail.endsWith("\n")) {
|
|
1074
|
+
input.stdout.write("\n");
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
else if (!input.follow) {
|
|
1079
|
+
throw new Error(`Bridge server log file was not found at ${files.logPath}.`);
|
|
1080
|
+
}
|
|
1081
|
+
if (!input.follow) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
let position = await getFileSize(files.logPath);
|
|
1085
|
+
const stopSignal = createStopSignal();
|
|
1086
|
+
while (!stopSignal.stopped) {
|
|
1087
|
+
await delay(LOG_FOLLOW_POLL_INTERVAL_MS);
|
|
1088
|
+
const size = await getFileSize(files.logPath);
|
|
1089
|
+
if (size < position) {
|
|
1090
|
+
position = 0;
|
|
1091
|
+
}
|
|
1092
|
+
if (size === position) {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
const chunk = await readFileSlice(files.logPath, position, size - position);
|
|
1096
|
+
if (chunk.length > 0) {
|
|
1097
|
+
input.stdout.write(chunk);
|
|
1098
|
+
}
|
|
1099
|
+
position = size;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function createStopSignal() {
|
|
1103
|
+
const signal = {
|
|
1104
|
+
stopped: false
|
|
1105
|
+
};
|
|
1106
|
+
const stop = () => {
|
|
1107
|
+
signal.stopped = true;
|
|
1108
|
+
};
|
|
1109
|
+
process.once("SIGINT", stop);
|
|
1110
|
+
process.once("SIGTERM", stop);
|
|
1111
|
+
return signal;
|
|
1112
|
+
}
|
|
1113
|
+
function tailLines(text, lineCount) {
|
|
1114
|
+
if (!text) {
|
|
1115
|
+
return "";
|
|
1116
|
+
}
|
|
1117
|
+
const normalized = text.split(/\r?\n/);
|
|
1118
|
+
const hasTrailingNewline = text.endsWith("\n");
|
|
1119
|
+
const lines = hasTrailingNewline ? normalized.slice(0, -1) : normalized;
|
|
1120
|
+
const tail = lines.slice(-lineCount).join("\n");
|
|
1121
|
+
return tail.length === 0 ? "" : `${tail}${hasTrailingNewline ? "\n" : ""}`;
|
|
1122
|
+
}
|
|
1123
|
+
async function readFileSlice(filePath, offset, length) {
|
|
1124
|
+
const handle = await open(filePath, "r");
|
|
1125
|
+
try {
|
|
1126
|
+
const buffer = Buffer.alloc(length);
|
|
1127
|
+
const result = await handle.read(buffer, 0, length, offset);
|
|
1128
|
+
return buffer.subarray(0, result.bytesRead).toString("utf8");
|
|
1129
|
+
}
|
|
1130
|
+
finally {
|
|
1131
|
+
await handle.close();
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
async function getFileSize(filePath) {
|
|
1135
|
+
try {
|
|
1136
|
+
const info = await stat(filePath);
|
|
1137
|
+
return info.size;
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
if (error.code === "ENOENT") {
|
|
1141
|
+
return 0;
|
|
1142
|
+
}
|
|
1143
|
+
throw error;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
async function waitForServerReady(input) {
|
|
1147
|
+
const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
|
|
1148
|
+
while (Date.now() < deadline) {
|
|
1149
|
+
const state = await readServerProcessState(input.statePath);
|
|
1150
|
+
if (state && state.pid === input.pid) {
|
|
1151
|
+
try {
|
|
1152
|
+
await checkBridgeHealth({
|
|
1153
|
+
baseUrl: state.baseUrl,
|
|
1154
|
+
fetchImpl: input.fetchImpl
|
|
1155
|
+
});
|
|
1156
|
+
return state;
|
|
1157
|
+
}
|
|
1158
|
+
catch {
|
|
1159
|
+
// Ignore until timeout or process death.
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (!isProcessRunning(input.pid)) {
|
|
1163
|
+
throw new Error(`Bridge server exited before becoming ready. Check logs at ${getServerProcessFiles(input.config).logPath}.`);
|
|
1164
|
+
}
|
|
1165
|
+
await delay(DAEMON_POLL_INTERVAL_MS);
|
|
1166
|
+
}
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
async function defaultSpawnDetachedServerProcess(input) {
|
|
1170
|
+
const scriptPath = process.argv[1];
|
|
1171
|
+
if (!scriptPath) {
|
|
1172
|
+
throw new Error("Cannot determine the openbridge CLI entrypoint for detached start.");
|
|
1173
|
+
}
|
|
1174
|
+
await mkdir(path.dirname(input.logPath), {
|
|
1175
|
+
recursive: true,
|
|
1176
|
+
mode: 0o700
|
|
1177
|
+
});
|
|
1178
|
+
const logHandle = await open(input.logPath, "a", 0o600);
|
|
1179
|
+
try {
|
|
1180
|
+
const launch = buildDetachedServerLaunchCommand({
|
|
1181
|
+
scriptPath,
|
|
1182
|
+
argv: input.argv,
|
|
1183
|
+
execPath: process.execPath,
|
|
1184
|
+
execArgv: process.execArgv
|
|
1185
|
+
});
|
|
1186
|
+
const child = spawn(launch.command, launch.args, {
|
|
1187
|
+
cwd: input.cwd,
|
|
1188
|
+
env: input.env,
|
|
1189
|
+
detached: true,
|
|
1190
|
+
stdio: ["ignore", logHandle.fd, logHandle.fd]
|
|
1191
|
+
});
|
|
1192
|
+
child.unref();
|
|
1193
|
+
if (!child.pid) {
|
|
1194
|
+
throw new Error("Detached bridge server process did not expose a pid.");
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
pid: child.pid
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
finally {
|
|
1201
|
+
await logHandle.close();
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function buildDetachedServerLaunchCommand(input) {
|
|
1205
|
+
return {
|
|
1206
|
+
command: input.execPath,
|
|
1207
|
+
args: [...input.execArgv, input.scriptPath, ...input.argv]
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function toForegroundStartArgv(argv) {
|
|
1211
|
+
const normalized = argv.length === 0 || argv[0]?.startsWith("--") ? ["start", ...argv] : [...argv];
|
|
1212
|
+
if (normalized[0] !== "start") {
|
|
1213
|
+
throw new Error("Detached launch is only supported for the start command.");
|
|
1214
|
+
}
|
|
1215
|
+
if (!normalized.includes("--foreground")) {
|
|
1216
|
+
normalized.push("--foreground");
|
|
1217
|
+
}
|
|
1218
|
+
return normalized;
|
|
1219
|
+
}
|
|
1220
|
+
function isProcessRunning(pid) {
|
|
1221
|
+
try {
|
|
1222
|
+
process.kill(pid, 0);
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1225
|
+
catch (error) {
|
|
1226
|
+
if (error.code === "ESRCH") {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
if (error.code === "EPERM") {
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
throw error;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
1236
|
+
const deadline = Date.now() + timeoutMs;
|
|
1237
|
+
while (Date.now() < deadline) {
|
|
1238
|
+
if (!isProcessRunning(pid)) {
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
await delay(100);
|
|
1242
|
+
}
|
|
1243
|
+
return !isProcessRunning(pid);
|
|
1244
|
+
}
|
|
1245
|
+
function delay(ms) {
|
|
1246
|
+
return new Promise((resolve) => {
|
|
1247
|
+
setTimeout(resolve, ms);
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
function formatHostForUrl(host) {
|
|
1251
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
1252
|
+
}
|
|
1253
|
+
function closeServer(server) {
|
|
1254
|
+
return new Promise((resolve, reject) => {
|
|
1255
|
+
server.close((error) => {
|
|
1256
|
+
if (error) {
|
|
1257
|
+
reject(error);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
resolve();
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
349
1264
|
async function readOptionalFile(targetPath) {
|
|
350
1265
|
try {
|
|
351
1266
|
const value = (await readFile(targetPath, "utf8")).trim();
|
|
@@ -358,6 +1273,17 @@ async function readOptionalFile(targetPath) {
|
|
|
358
1273
|
throw error;
|
|
359
1274
|
}
|
|
360
1275
|
}
|
|
1276
|
+
async function readOptionalRawFile(targetPath) {
|
|
1277
|
+
try {
|
|
1278
|
+
return await readFile(targetPath, "utf8");
|
|
1279
|
+
}
|
|
1280
|
+
catch (error) {
|
|
1281
|
+
if (error.code === "ENOENT") {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
throw error;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
361
1287
|
function requireConfigPath(value, key) {
|
|
362
1288
|
if (!value) {
|
|
363
1289
|
throw new Error(`${key} is required.`);
|
|
@@ -365,6 +1291,7 @@ function requireConfigPath(value, key) {
|
|
|
365
1291
|
return value;
|
|
366
1292
|
}
|
|
367
1293
|
export const runBridgeServerCliModule = {
|
|
1294
|
+
buildDetachedServerLaunchCommand,
|
|
368
1295
|
runBridgeServerCli,
|
|
369
1296
|
parseBridgeServerCliArgs,
|
|
370
1297
|
getBridgeServerCliHelpText
|