synabun 2026.4.26-beta.5 → 2026.4.26-beta.6

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.
@@ -1637,9 +1637,12 @@ app.post('/api/server/shutdown', async (req, res) => {
1637
1637
 
1638
1638
  // POST /api/system/run-update — Click-to-update flow for npm-installed users.
1639
1639
  //
1640
- // Spawns updater.mjs in a NEW visible terminal window (cmd on Windows,
1641
- // Terminal.app on macOS, x-terminal-emulator/gnome-terminal/konsole/xterm
1642
- // fallback on Linux). The updater:
1640
+ // Spawns the per-OS updater shim (updater.cmd on Windows, updater.sh on
1641
+ // macOS/Linux) in a NEW visible terminal window. Each shim invokes
1642
+ // updater.mjs and handles its own "press a key to close" lifecycle so we
1643
+ // don't have to thread cmd /k or bash -c quoting through node's spawn.
1644
+ //
1645
+ // The updater shim:
1643
1646
  // 1. Waits for the server PID to exit
1644
1647
  // 2. Runs `npm install -g synabun@<tag>`
1645
1648
  // 3. Optionally relaunches synabun
@@ -1647,8 +1650,8 @@ app.post('/api/server/shutdown', async (req, res) => {
1647
1650
  //
1648
1651
  // Right after spawning the detached updater, we run gracefulShutdown +
1649
1652
  // process.exit so file handles in neural-interface/ release and npm's
1650
- // rename succeeds. The updater handles waiting on us — it only starts npm
1651
- // after our PID is gone.
1653
+ // rename succeeds. The updater polls our PID and only starts npm after
1654
+ // we're gone.
1652
1655
  app.post('/api/system/run-update', async (req, res) => {
1653
1656
  try {
1654
1657
  const tag = String((req.body?.tag || 'latest')).replace(/[^a-zA-Z0-9_.-]/g, '');
@@ -1658,73 +1661,148 @@ app.post('/api/system/run-update', async (req, res) => {
1658
1661
  return res.status(400).json({ error: `tag must be one of: ${[...allowedTags].join(', ')}` });
1659
1662
  }
1660
1663
 
1661
- const updaterPath = resolve(PACKAGE_ROOT, 'updater.mjs');
1662
- if (!existsSync(updaterPath)) {
1663
- return res.status(500).json({ error: `updater.mjs not found at ${updaterPath}. SynaBun install may be corrupt.` });
1664
+ const plat = process.platform;
1665
+ const shimPath = plat === 'win32'
1666
+ ? resolve(PACKAGE_ROOT, 'updater.cmd')
1667
+ : resolve(PACKAGE_ROOT, 'updater.sh');
1668
+ const updaterMjsPath = resolve(PACKAGE_ROOT, 'updater.mjs');
1669
+
1670
+ if (!existsSync(updaterMjsPath)) {
1671
+ return res.status(500).json({ error: `updater.mjs not found at ${updaterMjsPath}. SynaBun install may be corrupt — reinstall: npm install -g synabun@${tag}` });
1672
+ }
1673
+ if (!existsSync(shimPath)) {
1674
+ return res.status(500).json({ error: `${plat === 'win32' ? 'updater.cmd' : 'updater.sh'} not found at ${shimPath}. SynaBun install may be corrupt — reinstall: npm install -g synabun@${tag}` });
1675
+ }
1676
+
1677
+ // On Unix, ensure the shim is executable. npm publish preserves the
1678
+ // executable bit only if it was set when packed; defensive chmod here
1679
+ // covers users whose tarball lost it (some Windows-staged pipelines do).
1680
+ if (plat !== 'win32') {
1681
+ try { chmodSync(shimPath, 0o755); } catch {}
1664
1682
  }
1665
1683
 
1666
1684
  const serverPid = process.pid;
1667
1685
  const argsBase = ['--tag', tag, '--pid', String(serverPid)];
1668
1686
  if (autoRestart) argsBase.push('--restart');
1669
1687
 
1670
- const plat = process.platform;
1671
1688
  let spawned = null;
1689
+ let launchError = null;
1672
1690
 
1673
1691
  if (plat === 'win32') {
1674
- // `cmd /c start "<title>" cmd /k "node <updater> ..."` opens a new
1675
- // visible cmd window that stays open after the script finishes (/k).
1676
- const nodeExe = process.execPath;
1677
- const cmdLine = `"${nodeExe}" "${updaterPath}" ${argsBase.map(a => `"${a}"`).join(' ')}`;
1678
- spawned = spawn('cmd', ['/c', 'start', '"SynaBun Update"', 'cmd', '/k', cmdLine], {
1679
- detached: true, stdio: 'ignore', windowsHide: false, shell: false,
1680
- });
1692
+ // Use shell:true so cmd parses our string directly — bypasses node's
1693
+ // arg-escape algorithm that was mangling the title arg ("SynaBun
1694
+ // Update" was being eaten by start's title slot, leaving Windows to
1695
+ // try to launch "Update" as a program).
1696
+ //
1697
+ // `start "" "<shim>" args` — empty title (cmd's start ALWAYS treats
1698
+ // the first quoted arg as a title), then runs the .cmd shim with
1699
+ // args. The shim handles its own pause-before-close.
1700
+ const argsLine = argsBase.map(a => `"${String(a).replace(/"/g, '""')}"`).join(' ');
1701
+ const cmdLine = `start "" "${shimPath}" ${argsLine}`;
1702
+ try {
1703
+ spawned = spawn(cmdLine, {
1704
+ shell: true,
1705
+ detached: true,
1706
+ stdio: 'ignore',
1707
+ windowsHide: false,
1708
+ });
1709
+ } catch (err) { launchError = err; }
1681
1710
  } else if (plat === 'darwin') {
1682
- // Use osascript to drive Terminal.app — opens a visible window the
1683
- // user can watch. Escape internal quotes for AppleScript string.
1684
- const nodeExe = process.execPath;
1685
- const cmd = `${JSON.stringify(nodeExe)} ${JSON.stringify(updaterPath)} ${argsBase.map(a => JSON.stringify(a)).join(' ')}`;
1686
- const escaped = cmd.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
1687
- spawned = spawn('osascript', [
1688
- '-e', `tell application "Terminal" to activate`,
1689
- '-e', `tell application "Terminal" to do script "${escaped}"`,
1690
- ], { detached: true, stdio: 'ignore' });
1711
+ // Drive Terminal.app via osascript. The shim path may contain spaces;
1712
+ // wrap it in single quotes inside the AppleScript string. AppleScript
1713
+ // string is wrapped in JS double-quote, so we escape JS backslashes
1714
+ // and double-quotes after building the inner shell command.
1715
+ const shellEscape = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
1716
+ const appleCmd = `${shellEscape(shimPath)} ${argsBase.map(shellEscape).join(' ')}`;
1717
+ try {
1718
+ spawned = spawn('osascript', [
1719
+ '-e', 'tell application "Terminal" to activate',
1720
+ '-e', `tell application "Terminal" to do script "${appleCmd.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`,
1721
+ ], { detached: true, stdio: 'ignore' });
1722
+ } catch (err) { launchError = err; }
1691
1723
  } else {
1692
- // Linux — try common terminal emulators in order, fall back to xterm.
1693
- const nodeExe = process.execPath;
1694
- const inner = `${nodeExe} ${updaterPath} ${argsBase.join(' ')}; echo; echo '[Press Enter to close]'; read`;
1724
+ // Linux/BSD/etc — try common terminal emulators in order. Each gets
1725
+ // the shim path as the program to run; the shim itself is a bash
1726
+ // script so we let the terminal exec it directly without bash -c.
1695
1727
  const candidates = [
1696
- ['x-terminal-emulator', ['-e', `bash -c "${inner.replace(/"/g, '\\"')}"`]],
1697
- ['gnome-terminal', ['--', 'bash', '-c', inner]],
1698
- ['konsole', ['-e', 'bash', '-c', inner]],
1699
- ['xfce4-terminal', ['-e', `bash -c "${inner.replace(/"/g, '\\"')}"`]],
1700
- ['xterm', ['-e', 'bash', '-c', inner]],
1728
+ ['x-terminal-emulator', ['-e', shimPath, ...argsBase]],
1729
+ ['gnome-terminal', ['--', shimPath, ...argsBase]],
1730
+ ['konsole', ['-e', shimPath, ...argsBase]],
1731
+ ['xfce4-terminal', ['-e', `${shimPath} ${argsBase.map(a => `"${a}"`).join(' ')}`]],
1732
+ ['mate-terminal', ['-e', `${shimPath} ${argsBase.map(a => `"${a}"`).join(' ')}`]],
1733
+ ['lxterminal', ['-e', shimPath, ...argsBase]],
1734
+ ['terminator', ['-e', `${shimPath} ${argsBase.map(a => `"${a}"`).join(' ')}`]],
1735
+ ['alacritty', ['-e', shimPath, ...argsBase]],
1736
+ ['kitty', [shimPath, ...argsBase]],
1737
+ ['wezterm', ['start', '--', shimPath, ...argsBase]],
1738
+ ['urxvt', ['-e', shimPath, ...argsBase]],
1739
+ ['xterm', ['-e', shimPath, ...argsBase]],
1701
1740
  ];
1702
1741
  for (const [bin, args] of candidates) {
1742
+ let child = null;
1703
1743
  try {
1704
- spawned = spawn(bin, args, { detached: true, stdio: 'ignore' });
1705
- break;
1706
- } catch {}
1744
+ child = spawn(bin, args, { detached: true, stdio: 'ignore' });
1745
+ } catch {
1746
+ continue;
1747
+ }
1748
+ // spawn() doesn't throw synchronously when bin is missing — it emits
1749
+ // 'error' async (ENOENT). Probe briefly to detect that case before
1750
+ // declaring success and unrefing.
1751
+ const survived = await new Promise((resolveProbe) => {
1752
+ let settled = false;
1753
+ const onErr = () => {
1754
+ if (settled) return;
1755
+ settled = true;
1756
+ child.removeAllListeners('error');
1757
+ resolveProbe(false);
1758
+ };
1759
+ child.once('error', onErr);
1760
+ setTimeout(() => {
1761
+ if (settled) return;
1762
+ settled = true;
1763
+ child.removeAllListeners('error');
1764
+ resolveProbe(true);
1765
+ }, 150);
1766
+ });
1767
+ if (survived) { spawned = child; break; }
1707
1768
  }
1708
1769
  if (!spawned) {
1709
- return res.status(500).json({ error: 'No supported terminal emulator found. Install xterm or run `npm install -g synabun@' + tag + '` manually.' });
1770
+ launchError = new Error(
1771
+ 'No supported terminal emulator found (tried: x-terminal-emulator, gnome-terminal, konsole, xfce4-terminal, mate-terminal, lxterminal, terminator, alacritty, kitty, wezterm, urxvt, xterm). ' +
1772
+ `Install one (e.g. \`apt install xterm\`) or run manually: npm install -g synabun@${tag}`
1773
+ );
1710
1774
  }
1711
1775
  }
1712
1776
 
1777
+ if (!spawned) {
1778
+ const msg = launchError?.message || `Failed to launch updater terminal on ${plat}`;
1779
+ console.error('[server] run-update launch failed:', msg);
1780
+ return res.status(500).json({ error: msg });
1781
+ }
1782
+
1783
+ // Don't tie our shutdown to the spawned process — it's a long-living
1784
+ // terminal we don't want to wait on.
1713
1785
  spawned.unref();
1714
- console.log(`[server] Updater terminal launched (tag=${tag}, autoRestart=${autoRestart}). Shutting down...`);
1715
- res.json({ ok: true, tag, autoRestart, updaterPath });
1786
+ spawned.on('error', (err) => {
1787
+ // Async ENOENT etc. — log but the response already went out.
1788
+ console.error('[server] updater spawn async error:', err.message);
1789
+ });
1790
+
1791
+ console.log(`[server] Updater terminal launched (tag=${tag}, autoRestart=${autoRestart}, shim=${shimPath}). Shutting down...`);
1792
+ res.json({ ok: true, tag, autoRestart, shimPath, platform: plat });
1716
1793
 
1717
1794
  // Give the spawned terminal time to fully launch before we tear down
1718
- // (on Windows, `start` returns instantly but the new window takes a
1719
- // beat to actually init node). 1.2s is generous; updater polls our PID
1720
- // anyway so an extra second here is harmless.
1795
+ // (on Windows `start` returns instantly but the new window takes a
1796
+ // beat to init node; on macOS osascript dispatches AppleScript async).
1797
+ // 1.2s is generous; updater polls our PID anyway so an extra second
1798
+ // here is harmless.
1721
1799
  setTimeout(async () => {
1722
1800
  await gracefulShutdown('run-update');
1723
1801
  process.exit(0);
1724
1802
  }, 1200);
1725
1803
  } catch (err) {
1726
1804
  console.error('[server] run-update error:', err?.message);
1727
- res.status(500).json({ error: err.message });
1805
+ if (!res.headersSent) res.status(500).json({ error: err.message });
1728
1806
  }
1729
1807
  });
1730
1808
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.26-beta.5",
3
+ "version": "2026.4.26-beta.6",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",
@@ -18,6 +18,8 @@
18
18
  "postinstall.js",
19
19
  "preuninstall.js",
20
20
  "updater.mjs",
21
+ "updater.cmd",
22
+ "updater.sh",
21
23
  "mcp-server/package.json",
22
24
  "mcp-server/dist/",
23
25
  "mcp-server/tsconfig.json",
package/postinstall.js CHANGED
@@ -197,6 +197,25 @@ function registerProtocolHandler() {
197
197
  }
198
198
  }
199
199
 
200
+ // ── Updater shim executable bit ──
201
+ //
202
+ // npm publish drops the executable bit on macOS/Linux when the tarball is
203
+ // built on Windows (a common pipeline issue). The in-app updater needs
204
+ // updater.sh to be executable so the per-OS terminal-launch path can
205
+ // `exec()` it directly. Restore the bit defensively on every postinstall.
206
+
207
+ function ensureUpdaterShExecutable() {
208
+ if (platform() === 'win32') return;
209
+ const shPath = resolve(__dirname, 'updater.sh');
210
+ if (!existsSync(shPath)) return;
211
+ try {
212
+ chmodSync(shPath, 0o755);
213
+ ok('updater.sh marked executable');
214
+ } catch (err) {
215
+ warn(`updater.sh chmod failed (${err.message}) — manual fix: chmod +x "${shPath}"`);
216
+ }
217
+ }
218
+
200
219
  // ── Main ──
201
220
 
202
221
  function main() {
@@ -207,6 +226,7 @@ function main() {
207
226
  rebuildPty();
208
227
  buildMcpServer();
209
228
  registerProtocolHandler();
229
+ ensureUpdaterShExecutable();
210
230
 
211
231
  console.log(`\n ${c.green}\u2713${c.reset} Setup complete. Run ${c.cyan}synabun${c.reset} to start.\n`);
212
232
  }
package/updater.cmd ADDED
@@ -0,0 +1,31 @@
1
+ @echo off
2
+ setlocal enableextensions
3
+ title SynaBun Updater
4
+
5
+ rem ───────────────────────────────────────────────────────
6
+ rem Windows shim for the cross-platform updater (updater.mjs).
7
+ rem Invoked by the running SynaBun server via:
8
+ rem start "" "<install>\updater.cmd" --tag X --pid Y [--restart]
9
+ rem
10
+ rem We use a shim instead of spawning node directly so cmd's
11
+ rem quoting rules don't mangle the command line, and so the
12
+ rem window stays open for the user to read npm output even
13
+ rem if node exits unexpectedly.
14
+ rem ───────────────────────────────────────────────────────
15
+
16
+ set "SCRIPT_DIR=%~dp0"
17
+ set "EXIT_CODE=0"
18
+
19
+ rem %~dp0 ends with backslash. updater.mjs lives next to this file.
20
+ node "%SCRIPT_DIR%updater.mjs" %*
21
+ set EXIT_CODE=%ERRORLEVEL%
22
+
23
+ echo.
24
+ if %EXIT_CODE%==0 (
25
+ echo [Update finished. Press any key to close this window.]
26
+ ) else (
27
+ echo [Update exited with code %EXIT_CODE%. Press any key to close this window.]
28
+ )
29
+ pause >nul
30
+
31
+ endlocal ^& exit /b %EXIT_CODE%
package/updater.sh ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # ───────────────────────────────────────────────────────
3
+ # Unix shim for the cross-platform updater (updater.mjs).
4
+ # Invoked by the running SynaBun server inside a fresh
5
+ # terminal window — keeps the terminal open after node
6
+ # exits so the user can read npm output and any errors.
7
+ # ───────────────────────────────────────────────────────
8
+
9
+ set +e
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ NODE_BIN="${SYNABUN_NODE_BIN:-node}"
13
+
14
+ # Pretty terminal title (no-op in some terminals).
15
+ printf '\033]0;SynaBun Updater\007' 2>/dev/null || true
16
+
17
+ "$NODE_BIN" "$SCRIPT_DIR/updater.mjs" "$@"
18
+ EXIT_CODE=$?
19
+
20
+ echo
21
+ if [ "$EXIT_CODE" = "0" ]; then
22
+ echo "[Update finished. Press Enter to close this window.]"
23
+ else
24
+ echo "[Update exited with code $EXIT_CODE. Press Enter to close this window.]"
25
+ fi
26
+ # Read one line. If stdin is closed (e.g. piped run), fall through immediately.
27
+ read -r _ || true
28
+
29
+ exit $EXIT_CODE