synabun 2026.4.26-beta.5 → 2026.4.26-beta.7
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/neural-interface/server.js +165 -48
- package/package.json +3 -1
- package/postinstall.js +20 -0
- package/preuninstall.js +74 -0
- package/updater.cmd +31 -0
- package/updater.sh +29 -0
|
@@ -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
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
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
|
|
1651
|
-
//
|
|
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
|
|
1662
|
-
|
|
1663
|
-
|
|
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
|
-
//
|
|
1675
|
-
//
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
const
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
'
|
|
1690
|
-
|
|
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
|
|
1693
|
-
|
|
1694
|
-
|
|
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',
|
|
1697
|
-
['gnome-terminal',
|
|
1698
|
-
['konsole',
|
|
1699
|
-
['xfce4-terminal',
|
|
1700
|
-
['
|
|
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
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1715
|
-
|
|
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
|
|
1719
|
-
// beat to
|
|
1720
|
-
// anyway so an extra second
|
|
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
|
|
|
@@ -4431,12 +4509,36 @@ function stopOpencodeServer() {
|
|
|
4431
4509
|
stopOpencodeSSERelay();
|
|
4432
4510
|
const managedPid = readOpencodeManagedPid();
|
|
4433
4511
|
const procPid = _ocpProc?.pid || null;
|
|
4512
|
+
const isWin = process.platform === 'win32';
|
|
4513
|
+
|
|
4514
|
+
// On Windows `process.kill(pid, 'SIGTERM')` is unreliable for non-Node
|
|
4515
|
+
// children (cmd.exe shells, opencode native binaries). Use taskkill /T /F
|
|
4516
|
+
// to walk the process tree and force-terminate every descendant.
|
|
4517
|
+
// This is the recurring source of EBUSY errors during `npm install -g
|
|
4518
|
+
// synabun@latest` — opencode-ai's `serve` process survives parent SynaBun
|
|
4519
|
+
// exit and keeps file handles inside neural-interface/ open.
|
|
4520
|
+
const winKill = (pid) => {
|
|
4521
|
+
try {
|
|
4522
|
+
spawn('taskkill', ['/pid', String(pid), '/T', '/F'], {
|
|
4523
|
+
shell: true, windowsHide: true, stdio: 'ignore',
|
|
4524
|
+
});
|
|
4525
|
+
} catch {}
|
|
4526
|
+
};
|
|
4527
|
+
|
|
4434
4528
|
if (_ocpProc && !_ocpProc.killed) {
|
|
4435
|
-
|
|
4529
|
+
if (isWin && procPid) {
|
|
4530
|
+
winKill(procPid);
|
|
4531
|
+
} else {
|
|
4532
|
+
try { _ocpProc.kill(); } catch {}
|
|
4533
|
+
}
|
|
4436
4534
|
_ocpProc = null;
|
|
4437
4535
|
}
|
|
4438
4536
|
if (managedPid && managedPid !== procPid) {
|
|
4439
|
-
|
|
4537
|
+
if (isWin) {
|
|
4538
|
+
winKill(managedPid);
|
|
4539
|
+
} else {
|
|
4540
|
+
signalPids([managedPid], 'SIGTERM');
|
|
4541
|
+
}
|
|
4440
4542
|
}
|
|
4441
4543
|
clearOpencodeManagedPid();
|
|
4442
4544
|
_ocpReady = false;
|
|
@@ -23509,6 +23611,21 @@ function saveSessionSnapshot() {
|
|
|
23509
23611
|
setInterval(saveSessionSnapshot, SESSION_SNAPSHOT_INTERVAL_MS);
|
|
23510
23612
|
|
|
23511
23613
|
// Also try to save on clean shutdown (works with Ctrl+C, not window close)
|
|
23512
|
-
|
|
23513
|
-
|
|
23514
|
-
|
|
23614
|
+
// Manual termination paths (Ctrl+C, kill, parent shell exit) all flow through
|
|
23615
|
+
// gracefulShutdown — same teardown the in-app restart/shutdown/update paths
|
|
23616
|
+
// use. Without this, hand-killed servers still leave orphaned opencode +
|
|
23617
|
+
// agent children holding file handles, which blocks `npm install -g synabun`.
|
|
23618
|
+
process.on('SIGINT', async () => {
|
|
23619
|
+
console.log('\n[server] SIGINT received.');
|
|
23620
|
+
await gracefulShutdown('sigint');
|
|
23621
|
+
process.exit(0);
|
|
23622
|
+
});
|
|
23623
|
+
process.on('SIGTERM', async () => {
|
|
23624
|
+
console.log('\n[server] SIGTERM received.');
|
|
23625
|
+
await gracefulShutdown('sigterm');
|
|
23626
|
+
process.exit(0);
|
|
23627
|
+
});
|
|
23628
|
+
// `exit` fires after process.exit() — only safe for synchronous final tasks.
|
|
23629
|
+
// gracefulShutdown is async so we cannot await here; just snapshot + ocp stop
|
|
23630
|
+
// best-effort. The async paths above are the primary teardown.
|
|
23631
|
+
process.on('exit', () => { try { stopOpencodeServer(); } catch {} try { saveSessionSnapshot(); } catch {} });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synabun",
|
|
3
|
-
"version": "2026.4.26-beta.
|
|
3
|
+
"version": "2026.4.26-beta.7",
|
|
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/preuninstall.js
CHANGED
|
@@ -69,6 +69,80 @@ async function tryGracefulServerShutdown() {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// ── Step 1b: kill any orphaned processes holding file handles ──
|
|
73
|
+
//
|
|
74
|
+
// Even after a graceful server shutdown (or when no server is running),
|
|
75
|
+
// orphaned children can survive: opencode-ai sidecars spawned via cmd.exe,
|
|
76
|
+
// MCP servers (claude-memory-mcp) spawned by Claude Code instances rather
|
|
77
|
+
// than by SynaBun itself, agent processes, etc. On Windows these orphans
|
|
78
|
+
// keep file handles inside the install dir open, causing EBUSY when npm
|
|
79
|
+
// tries to rename it.
|
|
80
|
+
//
|
|
81
|
+
// We nuke any process whose command line references the SynaBun install
|
|
82
|
+
// path. This catches every orphan regardless of how they got that way.
|
|
83
|
+
// Skips the current PID (we are the preuninstall process itself).
|
|
84
|
+
|
|
85
|
+
killOrphansInsideInstall();
|
|
86
|
+
|
|
87
|
+
function killOrphansInsideInstall() {
|
|
88
|
+
const installPath = __dirname;
|
|
89
|
+
const selfPid = process.pid;
|
|
90
|
+
const plat = platform();
|
|
91
|
+
|
|
92
|
+
if (plat === 'win32') {
|
|
93
|
+
// PowerShell: Get-CimInstance returns every process with CommandLine.
|
|
94
|
+
// Filter for our install path (escape backslashes for the -like wildcard).
|
|
95
|
+
// Stop-Process -Force kills synchronously; -ErrorAction SilentlyContinue
|
|
96
|
+
// suppresses "process already exited" noise.
|
|
97
|
+
const escaped = installPath.replace(/\\/g, '\\\\').replace(/'/g, `''`);
|
|
98
|
+
const ps = `
|
|
99
|
+
$ErrorActionPreference = 'SilentlyContinue';
|
|
100
|
+
Get-CimInstance Win32_Process |
|
|
101
|
+
Where-Object {
|
|
102
|
+
$_.ProcessId -ne ${selfPid} -and (
|
|
103
|
+
($_.CommandLine -and $_.CommandLine -like '*${escaped}*') -or
|
|
104
|
+
($_.ExecutablePath -and $_.ExecutablePath -like '${escaped}*')
|
|
105
|
+
)
|
|
106
|
+
} |
|
|
107
|
+
ForEach-Object {
|
|
108
|
+
Write-Host (' Killing orphan PID ' + $_.ProcessId + ' (' + $_.Name + ')');
|
|
109
|
+
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
|
110
|
+
}
|
|
111
|
+
`.trim();
|
|
112
|
+
try {
|
|
113
|
+
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${ps.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, {
|
|
114
|
+
stdio: 'inherit',
|
|
115
|
+
timeout: 8000,
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
// PowerShell unavailable, blocked, or timed out. Fall through.
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
// Unix: pgrep -f matches against the full command line. -P 1 skips
|
|
122
|
+
// self/children of init we shouldn't touch. We just enumerate then
|
|
123
|
+
// SIGKILL each one that isn't us.
|
|
124
|
+
try {
|
|
125
|
+
const out = execSync(`pgrep -f "${installPath.replace(/[/.*?+^$|(){}\\\[\]]/g, '\\$&')}"`, {
|
|
126
|
+
encoding: 'utf8',
|
|
127
|
+
timeout: 5000,
|
|
128
|
+
}).trim();
|
|
129
|
+
const pids = out.split('\n')
|
|
130
|
+
.map(s => Number(s.trim()))
|
|
131
|
+
.filter(n => Number.isFinite(n) && n > 0 && n !== selfPid);
|
|
132
|
+
for (const pid of pids) {
|
|
133
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// No matches or pgrep missing — fine.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Brief grace for OS to release file handles after killing.
|
|
141
|
+
// Synchronous busy-wait so we run before the rmSync calls below.
|
|
142
|
+
const until = Date.now() + 1500;
|
|
143
|
+
while (Date.now() < until) { /* spin */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
72
146
|
// ── Step 2: remove nested node_modules ──
|
|
73
147
|
|
|
74
148
|
const dirs = [
|
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
|