@gakim-digital/dexter-bridge 0.13.2 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/bin/dexter-bridge.js +2 -0
- package/package.json +1 -1
- package/src/cli.js +74 -15
package/README.md
CHANGED
|
@@ -126,6 +126,11 @@ records the resolved Claude executable path in `~/.dexter-bridge/config.json`
|
|
|
126
126
|
so the background service can find installations managed by NVM or another
|
|
127
127
|
shell-specific tool.
|
|
128
128
|
|
|
129
|
+
The service keeps itself current. When Dexter names a newer bridge release and
|
|
130
|
+
no run is in progress, it runs `brew update` and `brew upgrade`, then restarts
|
|
131
|
+
on the new version. It tries at most once an hour and keeps running the current
|
|
132
|
+
version if the upgrade fails. Logs are in `$(brew --prefix)/var/log/dexter-bridge.log`.
|
|
133
|
+
|
|
129
134
|
Useful service commands:
|
|
130
135
|
|
|
131
136
|
```bash
|
package/bin/dexter-bridge.js
CHANGED
|
@@ -7,4 +7,6 @@ runCli(process.argv.slice(2)).catch((error) => {
|
|
|
7
7
|
: String(error);
|
|
8
8
|
console.error(`Dexter Bridge failed: ${message}`);
|
|
9
9
|
process.exitCode = typeof error?.exitCode === 'number' ? error.exitCode : 1;
|
|
10
|
+
// launchd restarts the service only once this process is gone.
|
|
11
|
+
if (error?.code === 'DEXTER_BRIDGE_RESTART') process.exit();
|
|
10
12
|
});
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import readline from 'node:readline/promises';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
4
|
+
import { execFile, spawn } from 'node:child_process';
|
|
5
5
|
import {
|
|
6
6
|
BRIDGE_BUILD_FINGERPRINT,
|
|
7
7
|
BRIDGE_CAPABILITIES,
|
|
@@ -313,20 +313,59 @@ function runCommand(command, args) {
|
|
|
313
313
|
});
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
function commandOutput(command, args) {
|
|
317
|
+
return new Promise((resolve) => {
|
|
318
|
+
execFile(command, args, { timeout: 60_000 }, (error, stdout) => resolve(error ? '' : String(stdout)));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function versionParts(value) {
|
|
323
|
+
return String(value || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/)?.slice(1).map(Number) || null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isNewerVersion(candidate, current = BRIDGE_VERSION) {
|
|
327
|
+
const next = versionParts(candidate);
|
|
328
|
+
const running = versionParts(current);
|
|
329
|
+
if (!next || !running) return false;
|
|
330
|
+
for (let index = 0; index < 3; index += 1) {
|
|
331
|
+
if (next[index] !== running[index]) return next[index] > running[index];
|
|
332
|
+
}
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// `brew list --versions` prints "dexter-bridge 0.13.1 0.13.2" while an old keg remains.
|
|
337
|
+
function newestListedVersion(output) {
|
|
338
|
+
return String(output || '').trim().split(/\s+/).slice(1)
|
|
339
|
+
.reduce((newest, version) => (isNewerVersion(version, newest || '0.0.0') ? version : newest), null);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const BRIDGE_RESTART_EXIT_CODE = 75;
|
|
343
|
+
// Set once Homebrew installed a newer bridge. Every polling loop stops at its
|
|
344
|
+
// next turn, so a claimed run finishes before launchd restarts the service.
|
|
345
|
+
let pendingRestart = null;
|
|
346
|
+
|
|
347
|
+
// Upgrades the Homebrew install at most once an hour, and restarts only when a
|
|
348
|
+
// newer version is actually installed: the tap can trail npm.
|
|
349
|
+
async function selfUpdateWithHomebrew(logger, message, {
|
|
350
|
+
configDir = defaultConfigDir(),
|
|
351
|
+
run = runCommand,
|
|
352
|
+
output = commandOutput,
|
|
353
|
+
} = {}) {
|
|
320
354
|
const lastAttempt = Date.parse(readConfig(configDir).lastSelfUpdateAt || '');
|
|
321
|
-
if (Number.isFinite(lastAttempt) && Date.now() - lastAttempt < SELF_UPDATE_INTERVAL_MS) return;
|
|
355
|
+
if (Number.isFinite(lastAttempt) && Date.now() - lastAttempt < SELF_UPDATE_INTERVAL_MS) return false;
|
|
322
356
|
saveConfigPatch({ lastSelfUpdateAt: new Date().toISOString() }, configDir);
|
|
323
|
-
console.log(
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
357
|
+
console.log(message);
|
|
358
|
+
const upgraded = await run('brew', ['update', '--quiet']) && await run('brew', ['upgrade', HOMEBREW_FORMULA]);
|
|
359
|
+
const installedVersion = upgraded
|
|
360
|
+
? newestListedVersion(await output('brew', ['list', '--versions', HOMEBREW_FORMULA]))
|
|
361
|
+
: null;
|
|
362
|
+
logger.info('self_update_finished', { upgraded, installedVersion, runningVersion: BRIDGE_VERSION });
|
|
363
|
+
if (!isNewerVersion(installedVersion)) return false;
|
|
364
|
+
pendingRestart = Object.assign(
|
|
365
|
+
new Error(`Dexter Bridge ${installedVersion} installed. Restarting on the new version.`),
|
|
366
|
+
{ code: 'DEXTER_BRIDGE_RESTART', exitCode: BRIDGE_RESTART_EXIT_CODE },
|
|
367
|
+
);
|
|
368
|
+
return true;
|
|
330
369
|
}
|
|
331
370
|
|
|
332
371
|
function productConfigDirs(rootDir) {
|
|
@@ -778,6 +817,7 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
|
|
|
778
817
|
|
|
779
818
|
try {
|
|
780
819
|
while (true) {
|
|
820
|
+
if (pendingRestart) return;
|
|
781
821
|
if (Date.now() >= nextCatalogRefreshAt) void refreshModelCatalog();
|
|
782
822
|
let poll;
|
|
783
823
|
try {
|
|
@@ -812,8 +852,12 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
|
|
|
812
852
|
pollFailureCount = 0;
|
|
813
853
|
continue;
|
|
814
854
|
}
|
|
815
|
-
if (
|
|
816
|
-
|
|
855
|
+
if (
|
|
856
|
+
isUpdateRequiredError(error)
|
|
857
|
+
&& runsAsBackgroundService()
|
|
858
|
+
&& await selfUpdateWithHomebrew(pollLogger, 'A newer Dexter Bridge is required. Updating with Homebrew...')
|
|
859
|
+
) {
|
|
860
|
+
return;
|
|
817
861
|
}
|
|
818
862
|
if (once) throw error;
|
|
819
863
|
pollFailureCount += 1;
|
|
@@ -872,6 +916,14 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
|
|
|
872
916
|
console.log(`No pending ${label} runs.`);
|
|
873
917
|
return;
|
|
874
918
|
}
|
|
919
|
+
if (
|
|
920
|
+
runsAsBackgroundService()
|
|
921
|
+
&& !activeRuns.size
|
|
922
|
+
&& isNewerVersion(poll?.latestVersion)
|
|
923
|
+
&& await selfUpdateWithHomebrew(pollLogger, `Dexter Bridge ${poll.latestVersion} is available. Updating with Homebrew...`)
|
|
924
|
+
) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
875
927
|
}
|
|
876
928
|
} finally {
|
|
877
929
|
for (const adapter of providerAdapters.values()) adapter?.close?.();
|
|
@@ -893,6 +945,7 @@ async function startPairedBridges({ apiFlag, flags, rootDir }) {
|
|
|
893
945
|
const apiBaseUrl = resolveApiBaseUrl({ flagValue: apiFlag, config });
|
|
894
946
|
return startCommand({ apiBaseUrl, config: { ...config, apiBaseUrl }, flags, configDir });
|
|
895
947
|
}));
|
|
948
|
+
if (pendingRestart) throw pendingRestart;
|
|
896
949
|
}
|
|
897
950
|
|
|
898
951
|
function printDiagnostics(diagnostics) {
|
|
@@ -986,6 +1039,8 @@ export const __private__ = {
|
|
|
986
1039
|
executeClaimedRun,
|
|
987
1040
|
startRunAwakeGuard,
|
|
988
1041
|
isInvalidPairingError,
|
|
1042
|
+
isNewerVersion,
|
|
1043
|
+
newestListedVersion,
|
|
989
1044
|
parseArgv,
|
|
990
1045
|
pollBackoffMs,
|
|
991
1046
|
requestedRuntime,
|
|
@@ -993,5 +1048,9 @@ export const __private__ = {
|
|
|
993
1048
|
requireAvailableRuntime,
|
|
994
1049
|
requireAvailableAgent,
|
|
995
1050
|
selectedAgentCheck,
|
|
1051
|
+
selfUpdateWithHomebrew,
|
|
1052
|
+
resetPendingRestart() {
|
|
1053
|
+
pendingRestart = null;
|
|
1054
|
+
},
|
|
996
1055
|
usage,
|
|
997
1056
|
};
|