mobygate 0.6.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ All notable changes to mobygate are documented here. Format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version numbers are
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.6.2] — 2026-04-24
8
+
9
+ ### Fixed
10
+
11
+ - **Update on Windows failed with `EBUSY` after the npm-spawn fix**
12
+ in v0.6.1 because the running mobygate Node process holds open
13
+ file handles inside its own install dir
14
+ (`...\AppData\Roaming\npm\node_modules\mobygate`). When `npm install -g`
15
+ tries to atomically rename the directory, Windows refuses — POSIX
16
+ systems can replace open files, Windows can't.
17
+ - **Fix:** stop the service *before* running npm install, then start
18
+ on the new build. Affects both `mobygate update` (CLI) and the
19
+ dashboard `/update/apply` endpoint. The detached update child
20
+ survives because we spawn it with `detached: true` +
21
+ `windowsHide: true`, putting it in its own console group
22
+ independent of the parent that gets killed.
23
+ - POSIX systems now also stop-then-start (instead of unload-load /
24
+ restart), purely for symmetry and a cleaner log progression. No
25
+ behavior change there.
26
+
27
+ ## [0.6.1] — 2026-04-24
28
+
29
+ ### Fixed
30
+
31
+ - **`mobygate update` on Windows** failed with `spawnSync npm ENOENT`
32
+ because `npm` resolves to `npm.cmd` (a batch file) on Windows, and
33
+ Node's `spawn` won't pick up `.cmd` extensions without going through
34
+ cmd.exe. Added `shell: IS_WIN` to every npm/git invocation in the
35
+ CLI's update path. The dashboard's update endpoint already had this
36
+ fix in v0.6.0; now the CLI matches.
37
+
7
38
  ## [0.6.0] — 2026-04-24
8
39
 
9
40
  Big one. Native tool calling + in-dashboard self-update.
package/bin/mobygate.js CHANGED
@@ -564,8 +564,13 @@ async function cmdUpdate() {
564
564
  print(c.dim(`Current: v${pkg.version} · ${mode} install at ${REPO_ROOT}`));
565
565
 
566
566
  // ---- Look up latest published version on npm
567
+ // shell: IS_WIN is required on Windows because `npm` is `npm.cmd`
568
+ // (a batch file), and Node's spawn won't resolve .cmd extensions
569
+ // without going through cmd.exe. Same for git on Windows where some
570
+ // distributions install git as a shim. On macOS/Linux these are real
571
+ // binaries, so the flag is a no-op.
567
572
  info('Checking npm for the latest release...');
568
- const view = spawnSync('npm', ['view', 'mobygate', 'version'], { encoding: 'utf8', timeout: 10_000 });
573
+ const view = spawnSync('npm', ['view', 'mobygate', 'version'], { encoding: 'utf8', timeout: 10_000, shell: IS_WIN });
569
574
  if (view.status !== 0) {
570
575
  return die(`Couldn't reach npm registry: ${view.stderr?.trim() || view.error?.message || 'unknown'}`);
571
576
  }
@@ -576,37 +581,56 @@ async function cmdUpdate() {
576
581
  }
577
582
  print('');
578
583
 
584
+ // ---- Stop the service FIRST on Windows, otherwise running Node holds
585
+ // open file handles inside the install dir and `npm install -g` fails
586
+ // with EBUSY when it tries to rename the directory. On macOS/Linux we
587
+ // can replace open files freely, but stopping early there too is harmless
588
+ // and gives a cleaner restart sequence — so we do it everywhere.
589
+ let stoppedForUpdate = false;
590
+ if (IS_WIN) {
591
+ info('Stopping service so npm install can replace files...');
592
+ stopWindowsTask(WIN_LABELS.server);
593
+ stoppedForUpdate = true;
594
+ } else if (IS_MAC) {
595
+ const p = plistPathForLabel(SERVER_LABEL);
596
+ launchctlUnload(p);
597
+ stoppedForUpdate = true;
598
+ } else if (IS_LINUX) {
599
+ stopLinuxUnit(LINUX_UNITS.server);
600
+ stoppedForUpdate = true;
601
+ }
602
+
579
603
  // ---- Perform the upgrade
580
604
  if (mode === 'npm') {
581
605
  info(`Running \`npm install -g mobygate@latest\`...`);
582
- const r = spawnSync('npm', ['install', '-g', 'mobygate@latest'], { stdio: 'inherit' });
606
+ const r = spawnSync('npm', ['install', '-g', 'mobygate@latest'], { stdio: 'inherit', shell: IS_WIN });
583
607
  if (r.status !== 0) return die('npm install failed. See output above.');
584
608
  ok(`Installed mobygate@${latest}`);
585
609
  } else if (mode === 'git') {
586
610
  info(`Running \`git pull\` in ${REPO_ROOT}...`);
587
- const pull = spawnSync('git', ['-C', REPO_ROOT, 'pull', '--ff-only'], { stdio: 'inherit' });
611
+ const pull = spawnSync('git', ['-C', REPO_ROOT, 'pull', '--ff-only'], { stdio: 'inherit', shell: IS_WIN });
588
612
  if (pull.status !== 0) return die('git pull failed. Resolve conflicts and retry.');
589
613
  info(`Running \`npm install\`...`);
590
- const install = spawnSync('npm', ['install'], { cwd: REPO_ROOT, stdio: 'inherit' });
614
+ const install = spawnSync('npm', ['install'], { cwd: REPO_ROOT, stdio: 'inherit', shell: IS_WIN });
591
615
  if (install.status !== 0) return die('npm install failed. See output above.');
592
616
  ok(`Pulled and installed. See git log for what changed.`);
593
617
  } else {
594
618
  return die(`Install mode is "${mode}" — can't auto-update. Reinstall via npm or git.`);
595
619
  }
596
620
 
597
- // ---- Restart the managed service so the new code is running
621
+ // ---- Bring the service back up on the new code
598
622
  section('Restart');
599
- info('Restarting service so the new build is running...');
623
+ info('Starting service on the new build...');
600
624
  if (IS_MAC) {
601
625
  const p = plistPathForLabel(SERVER_LABEL);
602
- launchctlUnload(p); launchctlLoad(p);
603
- ok(`Reloaded ${SERVER_LABEL}`);
626
+ launchctlLoad(p);
627
+ ok(`Loaded ${SERVER_LABEL}`);
604
628
  } else if (IS_WIN) {
605
- stopWindowsTask(WIN_LABELS.server); startWindowsTask(WIN_LABELS.server);
606
- ok(`Restarted ${WIN_LABELS.server}`);
629
+ startWindowsTask(WIN_LABELS.server);
630
+ ok(`Started ${WIN_LABELS.server}`);
607
631
  } else if (IS_LINUX) {
608
- stopLinuxUnit(LINUX_UNITS.server); startLinuxUnit(LINUX_UNITS.server);
609
- ok(`Restarted ${LINUX_UNITS.server}`);
632
+ startLinuxUnit(LINUX_UNITS.server);
633
+ ok(`Started ${LINUX_UNITS.server}`);
610
634
  }
611
635
  print('');
612
636
  info(`Tip: if the install-layout changed (new service file, new paths), run \`mobygate init\` to re-install the service definitions.`);
package/lib/updater.js CHANGED
@@ -160,6 +160,16 @@ function writeUpdateState(patch) {
160
160
  * Build the shell command that performs update + restart. Returned as a
161
161
  * single string we can hand to `sh -c` / `cmd /c`. Written as a string
162
162
  * (not an array) because we want shell redirection for log capture.
163
+ *
164
+ * Order of operations matters on Windows: the running mobygate process
165
+ * holds open file handles inside its own install dir (`...\node_modules\mobygate`),
166
+ * so `npm install -g` fails with EBUSY when it tries to rename the dir.
167
+ * Fix: stop the service FIRST (which kills our parent), then install,
168
+ * then start. The detached child survives because we spawn with
169
+ * `detached: true` + `windowsHide: true`, putting it in its own console
170
+ * group independent of the parent. POSIX systems can replace open files
171
+ * freely, but stopping early there too is harmless and gives a cleaner
172
+ * "off → install → on" sequence in the log.
163
173
  */
164
174
  function buildUpdateCommand({ mode, repoRoot, logPath }) {
165
175
  if (IS_WIN) {
@@ -167,6 +177,11 @@ function buildUpdateCommand({ mode, repoRoot, logPath }) {
167
177
  // line so failures short-circuit via `||`.
168
178
  const steps = [];
169
179
  steps.push(`echo [mobygate-update] start at %DATE% %TIME%`);
180
+ // Stop FIRST so npm can replace files without EBUSY. /F forces close
181
+ // even if the process is mid-request; the SDK session map writes are
182
+ // synchronous and the SIGTERM handler flushes before exit.
183
+ steps.push(`echo [mobygate-update] stopping service`);
184
+ steps.push(`schtasks /End /TN "${WIN_SERVER_TASK}"`);
170
185
  if (mode === 'npm') {
171
186
  steps.push(`npm install -g mobygate@latest`);
172
187
  } else if (mode === 'git') {
@@ -175,15 +190,22 @@ function buildUpdateCommand({ mode, repoRoot, logPath }) {
175
190
  steps.push(`npm install`);
176
191
  }
177
192
  steps.push(`echo [mobygate-update] restarting service`);
178
- steps.push(`schtasks /End /TN "${WIN_SERVER_TASK}"`);
179
- steps.push(`schtasks /Run /TN "${WIN_SERVER_TASK}"`);
193
+ steps.push(`schtasks /Run /TN "${WIN_SERVER_TASK}"`);
180
194
  steps.push(`echo [mobygate-update] done`);
181
195
  // Join with && so any failure stops the chain. Final redirect to log.
182
196
  const inner = steps.map((s) => `(${s})`).join(' && ');
183
197
  return { shell: 'cmd', cmd: `${inner} >> "${logPath}" 2>&1` };
184
198
  }
185
- // POSIX: sh -c, bail-on-first-failure via set -e
199
+ // POSIX: sh -c, bail-on-first-failure via set -e. Stop service first
200
+ // for the same reason — symmetry, cleaner restart, no harm.
186
201
  const parts = [`set -e`, `echo "[mobygate-update] start $(date)"`];
202
+ parts.push(`echo "[mobygate-update] stopping service"`);
203
+ if (IS_MAC) {
204
+ const plist = join(process.env.HOME || '~', 'Library', 'LaunchAgents', `${SERVER_LABEL}.plist`);
205
+ parts.push(`launchctl unload "${plist}" 2>/dev/null || true`);
206
+ } else if (IS_LINUX) {
207
+ parts.push(`systemctl --user stop ${LINUX_SERVER_UNIT} 2>/dev/null || true`);
208
+ }
187
209
  if (mode === 'npm') {
188
210
  parts.push(`npm install -g mobygate@latest`);
189
211
  } else if (mode === 'git') {
@@ -191,14 +213,12 @@ function buildUpdateCommand({ mode, repoRoot, logPath }) {
191
213
  parts.push(`git pull --ff-only`);
192
214
  parts.push(`npm install`);
193
215
  }
194
- parts.push(`echo "[mobygate-update] restarting service"`);
216
+ parts.push(`echo "[mobygate-update] starting service on new build"`);
195
217
  if (IS_MAC) {
196
218
  const plist = join(process.env.HOME || '~', 'Library', 'LaunchAgents', `${SERVER_LABEL}.plist`);
197
- // unload may fail if not loaded — tolerate that specific case
198
- parts.push(`launchctl unload "${plist}" 2>/dev/null || true`);
199
- parts.push(`launchctl load "${plist}"`);
219
+ parts.push(`launchctl load "${plist}"`);
200
220
  } else if (IS_LINUX) {
201
- parts.push(`systemctl --user restart ${LINUX_SERVER_UNIT}`);
221
+ parts.push(`systemctl --user start ${LINUX_SERVER_UNIT}`);
202
222
  }
203
223
  parts.push(`echo "[mobygate-update] done"`);
204
224
  const script = parts.join('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobygate",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "OpenAI-compatible local proxy for Claude Max. The Möbius-strip gateway: OpenAI shape in, Claude Max out.",
5
5
  "type": "module",
6
6
  "main": "server.js",