@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 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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
4
4
  "description": "Local bridge for InstaWebAI and Dexter — runs Codex, Claude Code, or OpenCode on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
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
- // Upgrades the Homebrew install at most once an hour. On success the process
317
- // exits non-zero so launchd restarts the service on the new version.
318
- async function selfUpdateWithHomebrew(logger) {
319
- const configDir = defaultConfigDir();
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('A newer Dexter Bridge is required. Updating with Homebrew...');
324
- const updated = await runCommand('brew', ['upgrade', HOMEBREW_FORMULA]);
325
- logger.info('self_update_finished', { updated });
326
- if (!updated) return;
327
- const restart = new Error('Dexter Bridge updated. Restarting on the new version.');
328
- restart.exitCode = 75;
329
- throw restart;
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 (isUpdateRequiredError(error) && runsAsBackgroundService()) {
816
- await selfUpdateWithHomebrew(pollLogger);
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
  };