mobygate 0.6.1 → 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,26 @@ 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
+
7
27
  ## [0.6.1] — 2026-04-24
8
28
 
9
29
  ### Fixed
package/bin/mobygate.js CHANGED
@@ -581,6 +581,25 @@ async function cmdUpdate() {
581
581
  }
582
582
  print('');
583
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
+
584
603
  // ---- Perform the upgrade
585
604
  if (mode === 'npm') {
586
605
  info(`Running \`npm install -g mobygate@latest\`...`);
@@ -599,19 +618,19 @@ async function cmdUpdate() {
599
618
  return die(`Install mode is "${mode}" — can't auto-update. Reinstall via npm or git.`);
600
619
  }
601
620
 
602
- // ---- Restart the managed service so the new code is running
621
+ // ---- Bring the service back up on the new code
603
622
  section('Restart');
604
- info('Restarting service so the new build is running...');
623
+ info('Starting service on the new build...');
605
624
  if (IS_MAC) {
606
625
  const p = plistPathForLabel(SERVER_LABEL);
607
- launchctlUnload(p); launchctlLoad(p);
608
- ok(`Reloaded ${SERVER_LABEL}`);
626
+ launchctlLoad(p);
627
+ ok(`Loaded ${SERVER_LABEL}`);
609
628
  } else if (IS_WIN) {
610
- stopWindowsTask(WIN_LABELS.server); startWindowsTask(WIN_LABELS.server);
611
- ok(`Restarted ${WIN_LABELS.server}`);
629
+ startWindowsTask(WIN_LABELS.server);
630
+ ok(`Started ${WIN_LABELS.server}`);
612
631
  } else if (IS_LINUX) {
613
- stopLinuxUnit(LINUX_UNITS.server); startLinuxUnit(LINUX_UNITS.server);
614
- ok(`Restarted ${LINUX_UNITS.server}`);
632
+ startLinuxUnit(LINUX_UNITS.server);
633
+ ok(`Started ${LINUX_UNITS.server}`);
615
634
  }
616
635
  print('');
617
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.1",
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",