synabun 2026.4.26-beta.11 → 2026.4.26-beta.13
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/public/shared/ui-update.js +6 -9
- package/neural-interface/server.js +188 -132
- package/package.json +1 -1
- package/updater.mjs +39 -3
|
@@ -201,10 +201,9 @@ function openUpdateModal() {
|
|
|
201
201
|
? 'Installing from npm.'
|
|
202
202
|
: (installPlan.manualHint || 'Open the SynaBun repository for the correct update path.');
|
|
203
203
|
const autoRestartControl = canAutoUpdate ? `
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
</label>` : '';
|
|
204
|
+
<p style="font-size:11px;color:var(--t-muted);opacity:.75;margin:10px 0 0">
|
|
205
|
+
SynaBun will relaunch automatically after the update finishes.
|
|
206
|
+
</p>` : '';
|
|
208
207
|
const manualButton = canAutoUpdate ? `
|
|
209
208
|
<button class="action-btn action-btn--ghost" id="update-manual" title="Run the command yourself in your own terminal">Manual</button>` : '';
|
|
210
209
|
const updateNowButton = canAutoUpdate ? `
|
|
@@ -293,8 +292,6 @@ function openUpdateModal() {
|
|
|
293
292
|
overlay.querySelector('#update-manual-note').style.display = '';
|
|
294
293
|
const runNow = overlay.querySelector('#update-run-now');
|
|
295
294
|
if (runNow) runNow.style.display = 'none';
|
|
296
|
-
const autoRestartLabel = overlay.querySelector('#update-auto-restart')?.closest('label');
|
|
297
|
-
if (autoRestartLabel) autoRestartLabel.style.display = 'none';
|
|
298
295
|
});
|
|
299
296
|
|
|
300
297
|
// Skip checkbox
|
|
@@ -313,8 +310,8 @@ function openUpdateModal() {
|
|
|
313
310
|
const canAutoUpdate = installPlan.canAutoUpdate === true;
|
|
314
311
|
const repoUrl = installPlan.openUrl || SYNABUN_GITHUB_URL;
|
|
315
312
|
|
|
316
|
-
|
|
317
|
-
const autoRestart =
|
|
313
|
+
// Auto-restart is always on; server forces it true regardless of payload.
|
|
314
|
+
const autoRestart = true;
|
|
318
315
|
|
|
319
316
|
const runBtn = overlay.querySelector('#update-run-now');
|
|
320
317
|
const backBtn = overlay.querySelector('#update-back');
|
|
@@ -361,7 +358,7 @@ function openUpdateModal() {
|
|
|
361
358
|
icon.className = 'update-backup-icon';
|
|
362
359
|
icon.textContent = '✓';
|
|
363
360
|
icon.style.color = '#34c759';
|
|
364
|
-
text.innerHTML = `<strong>Updater terminal launched</strong><span class="update-run-detail">SynaBun is shutting down so npm can replace the package. Watch the terminal for install progress
|
|
361
|
+
text.innerHTML = `<strong>Updater terminal launched</strong><span class="update-run-detail">SynaBun is shutting down so npm can replace the package. Watch the terminal for install progress; SynaBun will relaunch automatically when it finishes.</span><span class="update-run-hint">If no terminal window appears, run <code>${esc(installPlan.displayCommand || 'npm i -g synabun@latest')}</code> manually.</span>`;
|
|
365
362
|
runBtn.innerHTML = 'Updater running…';
|
|
366
363
|
|
|
367
364
|
// Server is shutting down. Optionally redirect to a friendly offline
|
|
@@ -1552,7 +1552,7 @@ app.put('/api/settings', (req, res) => {
|
|
|
1552
1552
|
// leaving spawned children orphaned (opencode sidecar, agents, _sidecarProcess
|
|
1553
1553
|
// CLI links, pty terminals, cloudflare tunnel). On Windows those orphans
|
|
1554
1554
|
// kept file handles inside neural-interface/ open, causing EBUSY errors when
|
|
1555
|
-
// `npm i -g synabun@latest` tried to rename the package directory.
|
|
1555
|
+
// `npm i -g synabun@latest` tried to rename the package directory.
|
|
1556
1556
|
//
|
|
1557
1557
|
// gracefulShutdown() centralizes shutdown so /api/server/restart,
|
|
1558
1558
|
// /api/server/shutdown, and the new /api/system/run-update all release
|
|
@@ -1598,7 +1598,11 @@ async function gracefulShutdown(reason = 'shutdown') {
|
|
|
1598
1598
|
// Synabun spawns CLI links, pty terminals, cloudflare tunnel etc. that may
|
|
1599
1599
|
// not be reachable through the registries above. The taskkill /T flag walks
|
|
1600
1600
|
// the process tree and terminates every descendant.
|
|
1601
|
-
|
|
1601
|
+
//
|
|
1602
|
+
// Do not run this for click-to-update. The updater terminal is deliberately
|
|
1603
|
+
// launched before this server exits; taskkill /T would kill that brand-new
|
|
1604
|
+
// terminal too, which looks like a brief blink and no update on Windows.
|
|
1605
|
+
if (process.platform === 'win32' && reason !== 'run-update') {
|
|
1602
1606
|
try {
|
|
1603
1607
|
// Use spawn(detached) so taskkill survives our own exit and finishes
|
|
1604
1608
|
// killing children even if we exit before it completes.
|
|
@@ -1639,6 +1643,42 @@ function shellSingleQuote(s) {
|
|
|
1639
1643
|
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
1640
1644
|
}
|
|
1641
1645
|
|
|
1646
|
+
function collectWindowsDescendantPids(rootPid) {
|
|
1647
|
+
if (process.platform !== 'win32' || !rootPid) return [];
|
|
1648
|
+
try {
|
|
1649
|
+
const script = [
|
|
1650
|
+
`$root=${Number(rootPid)}`,
|
|
1651
|
+
'$procs=Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId',
|
|
1652
|
+
'$seen=@{}',
|
|
1653
|
+
'$front=@($root)',
|
|
1654
|
+
'$out=@()',
|
|
1655
|
+
'while ($front.Count -gt 0) {',
|
|
1656
|
+
' $next=@()',
|
|
1657
|
+
' foreach ($p in $procs) {',
|
|
1658
|
+
' if ($front -contains $p.ParentProcessId -and -not $seen.ContainsKey($p.ProcessId)) {',
|
|
1659
|
+
' $seen[$p.ProcessId]=$true',
|
|
1660
|
+
' $out += $p.ProcessId',
|
|
1661
|
+
' $next += $p.ProcessId',
|
|
1662
|
+
' }',
|
|
1663
|
+
' }',
|
|
1664
|
+
' $front=$next',
|
|
1665
|
+
'}',
|
|
1666
|
+
'$out -join ","',
|
|
1667
|
+
].join('; ');
|
|
1668
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
1669
|
+
encoding: 'utf8',
|
|
1670
|
+
windowsHide: true,
|
|
1671
|
+
timeout: 5000,
|
|
1672
|
+
});
|
|
1673
|
+
const out = result.status === 0 ? String(result.stdout || '').trim() : '';
|
|
1674
|
+
return out
|
|
1675
|
+
? out.split(',').map(v => Number(v)).filter(pid => Number.isFinite(pid) && pid > 0 && pid !== rootPid)
|
|
1676
|
+
: [];
|
|
1677
|
+
} catch {
|
|
1678
|
+
return [];
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1642
1682
|
function cleanupOldUpdaterStages(baseDir) {
|
|
1643
1683
|
try {
|
|
1644
1684
|
if (!existsSync(baseDir)) return;
|
|
@@ -1653,7 +1693,7 @@ function cleanupOldUpdaterStages(baseDir) {
|
|
|
1653
1693
|
} catch {}
|
|
1654
1694
|
}
|
|
1655
1695
|
|
|
1656
|
-
function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
|
|
1696
|
+
function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids = [] }) {
|
|
1657
1697
|
const sourceUpdater = resolve(PACKAGE_ROOT, 'updater.mjs');
|
|
1658
1698
|
if (!existsSync(sourceUpdater)) {
|
|
1659
1699
|
throw new Error(`updater.mjs not found at ${sourceUpdater}. SynaBun install may be corrupt - reinstall manually: ${installPlan.displayCommand}`);
|
|
@@ -1680,18 +1720,25 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
|
|
|
1680
1720
|
current: installPlan.current,
|
|
1681
1721
|
target: installPlan.target,
|
|
1682
1722
|
channel: installPlan.channel,
|
|
1723
|
+
cleanupPids,
|
|
1683
1724
|
};
|
|
1684
1725
|
writeFileSync(payloadPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
1685
1726
|
|
|
1686
1727
|
if (process.platform === 'win32') {
|
|
1728
|
+
const nodeBin = String(process.execPath || 'node').replace(/"/g, '');
|
|
1687
1729
|
writeFileSync(runnerPath, [
|
|
1688
1730
|
'@echo off',
|
|
1689
1731
|
'setlocal enableextensions',
|
|
1732
|
+
'chcp 65001 >nul 2>&1',
|
|
1690
1733
|
'title SynaBun Updater',
|
|
1691
1734
|
'cd /d "%~dp0"',
|
|
1692
1735
|
'set "EXIT_CODE=0"',
|
|
1693
|
-
|
|
1736
|
+
`set "NODE_BIN=${nodeBin}"`,
|
|
1694
1737
|
'if defined SYNABUN_NODE_BIN set "NODE_BIN=%SYNABUN_NODE_BIN%"',
|
|
1738
|
+
'echo SynaBun Updater starting...',
|
|
1739
|
+
'echo Node: %NODE_BIN%',
|
|
1740
|
+
'echo Stage: %~dp0',
|
|
1741
|
+
'echo.',
|
|
1695
1742
|
'"%NODE_BIN%" "%~dp0updater.mjs" --payload "%~dp0payload.json" --no-hold',
|
|
1696
1743
|
'set "EXIT_CODE=%ERRORLEVEL%"',
|
|
1697
1744
|
'echo.',
|
|
@@ -1739,51 +1786,60 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
|
|
|
1739
1786
|
// can lock the directory npm is trying to replace.
|
|
1740
1787
|
app.post('/api/system/run-update', async (req, res) => {
|
|
1741
1788
|
try {
|
|
1742
|
-
|
|
1789
|
+
// Auto-restart is always on — running the click-to-update flow implies the
|
|
1790
|
+
// user wants the new version live without a manual relaunch step.
|
|
1791
|
+
const autoRestart = true;
|
|
1743
1792
|
|
|
1744
1793
|
if (!_synabunUpdateCache.checkedAt) {
|
|
1745
1794
|
try { await checkSynabunUpdate(); } catch {}
|
|
1746
|
-
}
|
|
1747
|
-
const installPlan = _synabunUpdateCache.installPlan;
|
|
1748
|
-
if (!_synabunUpdateCache.updateAvailable || !installPlan) {
|
|
1749
|
-
return res.status(409).json({ error: 'No SynaBun update is currently available.' });
|
|
1750
|
-
}
|
|
1751
|
-
if (installPlan.canAutoUpdate === false) {
|
|
1752
|
-
return res.status(409).json({
|
|
1753
|
-
error: installPlan.manualHint || `This installation cannot be auto-updated. Open ${installPlan.openUrl || SYNABUN_GITHUB_REPO_URL}`,
|
|
1754
|
-
installPlan,
|
|
1755
|
-
openUrl: installPlan.openUrl || SYNABUN_GITHUB_REPO_URL,
|
|
1756
|
-
});
|
|
1757
|
-
}
|
|
1758
|
-
if (!installPlan.installSpec) {
|
|
1759
|
-
return res.status(500).json({ error: 'SynaBun update plan is missing an npm install target.' });
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
const plat = process.platform;
|
|
1795
|
+
}
|
|
1796
|
+
const installPlan = _synabunUpdateCache.installPlan;
|
|
1797
|
+
if (!_synabunUpdateCache.updateAvailable || !installPlan) {
|
|
1798
|
+
return res.status(409).json({ error: 'No SynaBun update is currently available.' });
|
|
1799
|
+
}
|
|
1800
|
+
if (installPlan.canAutoUpdate === false) {
|
|
1801
|
+
return res.status(409).json({
|
|
1802
|
+
error: installPlan.manualHint || `This installation cannot be auto-updated. Open ${installPlan.openUrl || SYNABUN_GITHUB_REPO_URL}`,
|
|
1803
|
+
installPlan,
|
|
1804
|
+
openUrl: installPlan.openUrl || SYNABUN_GITHUB_REPO_URL,
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
if (!installPlan.installSpec) {
|
|
1808
|
+
return res.status(500).json({ error: 'SynaBun update plan is missing an npm install target.' });
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
const plat = process.platform;
|
|
1763
1812
|
const serverPid = process.pid;
|
|
1764
|
-
const
|
|
1813
|
+
const cleanupPids = collectWindowsDescendantPids(serverPid);
|
|
1814
|
+
const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids });
|
|
1765
1815
|
|
|
1766
1816
|
let spawned = null;
|
|
1767
1817
|
let launchError = null;
|
|
1768
1818
|
|
|
1769
|
-
if (plat === 'win32') {
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1819
|
+
if (plat === 'win32') {
|
|
1820
|
+
// Use cmd's `start` builtin to spawn a fresh visible console.
|
|
1821
|
+
// Prior implementation used PowerShell + Start-Process -ArgumentList,
|
|
1822
|
+
// which re-quoted the runner path and produced `cmd /k """C:\path\..."""`
|
|
1823
|
+
// — cmd's quote-stripping then mangled it and the new window died before
|
|
1824
|
+
// running anything (visible to users as a brief blink). `cmd /c start ""
|
|
1825
|
+
// /D <dir> <runner>` is the canonical, reliable way to launch a fresh
|
|
1826
|
+
// console window on Windows.
|
|
1827
|
+
try {
|
|
1828
|
+
const comSpec = process.env.ComSpec || 'cmd.exe';
|
|
1829
|
+
spawned = spawn(comSpec, [
|
|
1830
|
+
'/c', 'start',
|
|
1831
|
+
'""', // empty title — required when next token is quoted path
|
|
1832
|
+
'/D', staged.stageDir, // working dir for new console
|
|
1833
|
+
staged.runnerPath, // batch file to run in new window
|
|
1834
|
+
], {
|
|
1777
1835
|
cwd: staged.stageDir,
|
|
1778
1836
|
shell: false,
|
|
1779
1837
|
detached: true,
|
|
1780
1838
|
stdio: 'ignore',
|
|
1781
|
-
windowsHide: true,
|
|
1839
|
+
windowsHide: true, // hides only the wrapper cmd.exe; new console stays visible
|
|
1782
1840
|
env: {
|
|
1783
1841
|
...process.env,
|
|
1784
1842
|
SYNABUN_NODE_BIN: process.execPath,
|
|
1785
|
-
SYNABUN_UPDATER_RUNNER: staged.runnerPath,
|
|
1786
|
-
SYNABUN_UPDATER_CWD: staged.stageDir,
|
|
1787
1843
|
},
|
|
1788
1844
|
});
|
|
1789
1845
|
} catch (err) { launchError = err; }
|
|
@@ -4594,7 +4650,7 @@ function stopOpencodeServer() {
|
|
|
4594
4650
|
// On Windows `process.kill(pid, 'SIGTERM')` is unreliable for non-Node
|
|
4595
4651
|
// children (cmd.exe shells, opencode native binaries). Use taskkill /T /F
|
|
4596
4652
|
// to walk the process tree and force-terminate every descendant.
|
|
4597
|
-
// This is the recurring source of EBUSY errors during `npm i -g
|
|
4653
|
+
// This is the recurring source of EBUSY errors during `npm i -g
|
|
4598
4654
|
// synabun@latest` — opencode-ai's `serve` process survives parent SynaBun
|
|
4599
4655
|
// exit and keeps file handles inside neural-interface/ open.
|
|
4600
4656
|
const winKill = (pid) => {
|
|
@@ -10434,7 +10490,7 @@ app.post('/api/search/memories', async (req, res) => {
|
|
|
10434
10490
|
// when EITHER source is newer than the installed version (catches one-sided
|
|
10435
10491
|
// publishes — e.g. release tagged on GitHub before npm publish completes).
|
|
10436
10492
|
|
|
10437
|
-
let _synabunUpdateCache = {
|
|
10493
|
+
let _synabunUpdateCache = {
|
|
10438
10494
|
current: null,
|
|
10439
10495
|
installedChannel: 'stable', // 'stable' | 'prerelease'
|
|
10440
10496
|
npmLatestStable: null, // dist-tags.latest
|
|
@@ -10448,97 +10504,97 @@ let _synabunUpdateCache = {
|
|
|
10448
10504
|
gitUpdateAvailable: false,
|
|
10449
10505
|
updateAvailable: false, // npmUpdateAvailable || gitUpdateAvailable
|
|
10450
10506
|
source: null, // 'npm' | 'github' | 'both' | null
|
|
10451
|
-
installPlan: null, // server-owned npm/GitHub action target for click-to-update
|
|
10507
|
+
installPlan: null, // server-owned npm/GitHub action target for click-to-update
|
|
10452
10508
|
checkedAt: null,
|
|
10453
10509
|
npmError: null,
|
|
10454
|
-
gitError: null,
|
|
10455
|
-
};
|
|
10456
|
-
|
|
10457
|
-
const SYNABUN_GITHUB_REPO_URL = 'https://github.com/danilokhury/Synabun';
|
|
10458
|
-
|
|
10459
|
-
function isSynabunGitHubRemote(remoteText) {
|
|
10460
|
-
return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
|
|
10461
|
-
}
|
|
10462
|
-
|
|
10463
|
-
function detectSynabunInstallSource() {
|
|
10464
|
-
const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
|
|
10465
|
-
if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
|
|
10466
|
-
return { kind: 'npm-global' };
|
|
10467
|
-
}
|
|
10468
|
-
|
|
10469
|
-
try {
|
|
10470
|
-
const inside = execSync('git rev-parse --is-inside-work-tree', {
|
|
10471
|
-
cwd: PACKAGE_ROOT,
|
|
10472
|
-
encoding: 'utf8',
|
|
10473
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10474
|
-
timeout: 2000,
|
|
10475
|
-
}).trim();
|
|
10476
|
-
if (inside === 'true') {
|
|
10477
|
-
let remote = '';
|
|
10478
|
-
let remotes = '';
|
|
10479
|
-
try {
|
|
10480
|
-
remote = execSync('git remote get-url origin', {
|
|
10481
|
-
cwd: PACKAGE_ROOT,
|
|
10482
|
-
encoding: 'utf8',
|
|
10483
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10484
|
-
timeout: 2000,
|
|
10485
|
-
}).trim();
|
|
10486
|
-
} catch {}
|
|
10487
|
-
try {
|
|
10488
|
-
remotes = execSync('git remote -v', {
|
|
10489
|
-
cwd: PACKAGE_ROOT,
|
|
10490
|
-
encoding: 'utf8',
|
|
10491
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10492
|
-
timeout: 2000,
|
|
10493
|
-
}).trim();
|
|
10494
|
-
} catch {}
|
|
10495
|
-
if (isSynabunGitHubRemote(`${remote}\n${remotes}`)) {
|
|
10496
|
-
const firstRemote = remote || remotes.split(/\s+/).find(part => isSynabunGitHubRemote(part)) || null;
|
|
10497
|
-
return { kind: 'github-clone', remote: firstRemote };
|
|
10498
|
-
}
|
|
10499
|
-
return { kind: 'git-clone', remote: remote || null };
|
|
10500
|
-
}
|
|
10501
|
-
} catch {}
|
|
10502
|
-
|
|
10503
|
-
return { kind: 'local' };
|
|
10504
|
-
}
|
|
10505
|
-
|
|
10506
|
-
function buildSynabunInstallPlan({ current, installedChannel, latest, updateAvailable }) {
|
|
10507
|
-
if (!updateAvailable) return null;
|
|
10508
|
-
|
|
10509
|
-
const installSource = detectSynabunInstallSource();
|
|
10510
|
-
if (installSource.kind !== 'npm-global') {
|
|
10511
|
-
const clonedHint = installSource.kind === 'github-clone'
|
|
10512
|
-
? 'This SynaBun install is a GitHub checkout. Open the repository to pull or reinstall from the correct source.'
|
|
10513
|
-
: 'This SynaBun install was not detected as npm. Open the repository for the correct update path.';
|
|
10514
|
-
return {
|
|
10515
|
-
source: 'github',
|
|
10516
|
-
installSource: installSource.kind,
|
|
10517
|
-
canAutoUpdate: false,
|
|
10518
|
-
current,
|
|
10519
|
-
target: latest,
|
|
10520
|
-
channel: installedChannel,
|
|
10521
|
-
displayCommand: SYNABUN_GITHUB_REPO_URL,
|
|
10522
|
-
openUrl: SYNABUN_GITHUB_REPO_URL,
|
|
10523
|
-
manualCommand: installSource.kind === 'github-clone' ? 'git pull --ff-only' : null,
|
|
10524
|
-
manualHint: clonedHint,
|
|
10525
|
-
remote: installSource.remote,
|
|
10526
|
-
};
|
|
10527
|
-
}
|
|
10528
|
-
|
|
10529
|
-
const installSpec = 'synabun@latest';
|
|
10530
|
-
return {
|
|
10531
|
-
source: 'npm',
|
|
10532
|
-
installSource: installSource.kind,
|
|
10533
|
-
canAutoUpdate: true,
|
|
10534
|
-
current,
|
|
10535
|
-
target: latest,
|
|
10536
|
-
channel: installedChannel,
|
|
10537
|
-
installSpec,
|
|
10538
|
-
displayCommand: 'npm i -g synabun@latest',
|
|
10539
|
-
npmTag: 'latest',
|
|
10540
|
-
};
|
|
10541
|
-
}
|
|
10510
|
+
gitError: null,
|
|
10511
|
+
};
|
|
10512
|
+
|
|
10513
|
+
const SYNABUN_GITHUB_REPO_URL = 'https://github.com/danilokhury/Synabun';
|
|
10514
|
+
|
|
10515
|
+
function isSynabunGitHubRemote(remoteText) {
|
|
10516
|
+
return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
|
|
10517
|
+
}
|
|
10518
|
+
|
|
10519
|
+
function detectSynabunInstallSource() {
|
|
10520
|
+
const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
|
|
10521
|
+
if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
|
|
10522
|
+
return { kind: 'npm-global' };
|
|
10523
|
+
}
|
|
10524
|
+
|
|
10525
|
+
try {
|
|
10526
|
+
const inside = execSync('git rev-parse --is-inside-work-tree', {
|
|
10527
|
+
cwd: PACKAGE_ROOT,
|
|
10528
|
+
encoding: 'utf8',
|
|
10529
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10530
|
+
timeout: 2000,
|
|
10531
|
+
}).trim();
|
|
10532
|
+
if (inside === 'true') {
|
|
10533
|
+
let remote = '';
|
|
10534
|
+
let remotes = '';
|
|
10535
|
+
try {
|
|
10536
|
+
remote = execSync('git remote get-url origin', {
|
|
10537
|
+
cwd: PACKAGE_ROOT,
|
|
10538
|
+
encoding: 'utf8',
|
|
10539
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10540
|
+
timeout: 2000,
|
|
10541
|
+
}).trim();
|
|
10542
|
+
} catch {}
|
|
10543
|
+
try {
|
|
10544
|
+
remotes = execSync('git remote -v', {
|
|
10545
|
+
cwd: PACKAGE_ROOT,
|
|
10546
|
+
encoding: 'utf8',
|
|
10547
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10548
|
+
timeout: 2000,
|
|
10549
|
+
}).trim();
|
|
10550
|
+
} catch {}
|
|
10551
|
+
if (isSynabunGitHubRemote(`${remote}\n${remotes}`)) {
|
|
10552
|
+
const firstRemote = remote || remotes.split(/\s+/).find(part => isSynabunGitHubRemote(part)) || null;
|
|
10553
|
+
return { kind: 'github-clone', remote: firstRemote };
|
|
10554
|
+
}
|
|
10555
|
+
return { kind: 'git-clone', remote: remote || null };
|
|
10556
|
+
}
|
|
10557
|
+
} catch {}
|
|
10558
|
+
|
|
10559
|
+
return { kind: 'local' };
|
|
10560
|
+
}
|
|
10561
|
+
|
|
10562
|
+
function buildSynabunInstallPlan({ current, installedChannel, latest, updateAvailable }) {
|
|
10563
|
+
if (!updateAvailable) return null;
|
|
10564
|
+
|
|
10565
|
+
const installSource = detectSynabunInstallSource();
|
|
10566
|
+
if (installSource.kind !== 'npm-global') {
|
|
10567
|
+
const clonedHint = installSource.kind === 'github-clone'
|
|
10568
|
+
? 'This SynaBun install is a GitHub checkout. Open the repository to pull or reinstall from the correct source.'
|
|
10569
|
+
: 'This SynaBun install was not detected as npm. Open the repository for the correct update path.';
|
|
10570
|
+
return {
|
|
10571
|
+
source: 'github',
|
|
10572
|
+
installSource: installSource.kind,
|
|
10573
|
+
canAutoUpdate: false,
|
|
10574
|
+
current,
|
|
10575
|
+
target: latest,
|
|
10576
|
+
channel: installedChannel,
|
|
10577
|
+
displayCommand: SYNABUN_GITHUB_REPO_URL,
|
|
10578
|
+
openUrl: SYNABUN_GITHUB_REPO_URL,
|
|
10579
|
+
manualCommand: installSource.kind === 'github-clone' ? 'git pull --ff-only' : null,
|
|
10580
|
+
manualHint: clonedHint,
|
|
10581
|
+
remote: installSource.remote,
|
|
10582
|
+
};
|
|
10583
|
+
}
|
|
10584
|
+
|
|
10585
|
+
const installSpec = 'synabun@latest';
|
|
10586
|
+
return {
|
|
10587
|
+
source: 'npm',
|
|
10588
|
+
installSource: installSource.kind,
|
|
10589
|
+
canAutoUpdate: true,
|
|
10590
|
+
current,
|
|
10591
|
+
target: latest,
|
|
10592
|
+
channel: installedChannel,
|
|
10593
|
+
installSpec,
|
|
10594
|
+
displayCommand: 'npm i -g synabun@latest',
|
|
10595
|
+
npmTag: 'latest',
|
|
10596
|
+
};
|
|
10597
|
+
}
|
|
10542
10598
|
|
|
10543
10599
|
async function checkSynabunUpdate() {
|
|
10544
10600
|
let current = _synabunUpdateCache.current;
|
|
@@ -10667,12 +10723,12 @@ async function checkSynabunUpdate() {
|
|
|
10667
10723
|
else if (npmUpdateAvailable) source = 'npm';
|
|
10668
10724
|
else if (gitUpdateAvailable) source = 'github';
|
|
10669
10725
|
|
|
10670
|
-
const installPlan = buildSynabunInstallPlan({
|
|
10671
|
-
current,
|
|
10672
|
-
installedChannel,
|
|
10673
|
-
latest,
|
|
10674
|
-
updateAvailable,
|
|
10675
|
-
});
|
|
10726
|
+
const installPlan = buildSynabunInstallPlan({
|
|
10727
|
+
current,
|
|
10728
|
+
installedChannel,
|
|
10729
|
+
latest,
|
|
10730
|
+
updateAvailable,
|
|
10731
|
+
});
|
|
10676
10732
|
|
|
10677
10733
|
_synabunUpdateCache = {
|
|
10678
10734
|
current,
|
|
@@ -23809,7 +23865,7 @@ setInterval(saveSessionSnapshot, SESSION_SNAPSHOT_INTERVAL_MS);
|
|
|
23809
23865
|
// Manual termination paths (Ctrl+C, kill, parent shell exit) all flow through
|
|
23810
23866
|
// gracefulShutdown — same teardown the in-app restart/shutdown/update paths
|
|
23811
23867
|
// use. Without this, hand-killed servers still leave orphaned opencode +
|
|
23812
|
-
// agent children holding file handles, which blocks `npm i -g synabun`.
|
|
23868
|
+
// agent children holding file handles, which blocks `npm i -g synabun`.
|
|
23813
23869
|
process.on('SIGINT', async () => {
|
|
23814
23870
|
console.log('\n[server] SIGINT received.');
|
|
23815
23871
|
await gracefulShutdown('sigint');
|
package/package.json
CHANGED
package/updater.mjs
CHANGED
|
@@ -20,6 +20,7 @@ const legacyTag = sanitizeTag(args.tag || 'latest');
|
|
|
20
20
|
const installSpec = sanitizeInstallSpec(payload.installSpec || args['install-spec'] || `synabun@${legacyTag}`);
|
|
21
21
|
const displayCommand = payload.displayCommand || `npm i -g ${installSpec}`;
|
|
22
22
|
const serverPid = toPid(payload.serverPid ?? args.pid);
|
|
23
|
+
const cleanupPids = sanitizePidList(payload.cleanupPids).filter(pid => pid !== serverPid && pid !== process.pid);
|
|
23
24
|
const autoRestart = payload.autoRestart != null ? !!payload.autoRestart : !!args.restart;
|
|
24
25
|
const safeCwd = payload.cwd || process.cwd();
|
|
25
26
|
const noHold = !!args['no-hold'];
|
|
@@ -46,8 +47,8 @@ try {
|
|
|
46
47
|
if (exited) {
|
|
47
48
|
console.log(' Server exited.\n');
|
|
48
49
|
} else {
|
|
49
|
-
console.warn(' Server still alive after 30s - forcing kill.\n');
|
|
50
|
-
|
|
50
|
+
console.warn(' Server still alive after 30s - forcing server process kill.\n');
|
|
51
|
+
forceKillPid(serverPid);
|
|
51
52
|
await sleep(1500);
|
|
52
53
|
}
|
|
53
54
|
} else {
|
|
@@ -55,7 +56,15 @@ try {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
await sleep(1500);
|
|
58
|
-
if (
|
|
59
|
+
if (cleanupPids.length) {
|
|
60
|
+
console.log(`Cleaning up ${cleanupPids.length} pre-existing child process${cleanupPids.length === 1 ? '' : 'es'}...`);
|
|
61
|
+
for (const pid of cleanupPids) {
|
|
62
|
+
if (!isPidAlive(pid)) continue;
|
|
63
|
+
forceKillTree(pid);
|
|
64
|
+
}
|
|
65
|
+
await sleep(1000);
|
|
66
|
+
console.log(' Cleanup complete.\n');
|
|
67
|
+
}
|
|
59
68
|
|
|
60
69
|
console.log(`Running: ${displayCommand}\n`);
|
|
61
70
|
console.log(HR);
|
|
@@ -143,6 +152,19 @@ function toPid(raw) {
|
|
|
143
152
|
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
function sanitizePidList(raw) {
|
|
156
|
+
if (!Array.isArray(raw)) return [];
|
|
157
|
+
const out = [];
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
for (const item of raw) {
|
|
160
|
+
const pid = toPid(item);
|
|
161
|
+
if (!pid || seen.has(pid)) continue;
|
|
162
|
+
seen.add(pid);
|
|
163
|
+
out.push(pid);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
146
168
|
async function waitForExit(pid, timeoutMs) {
|
|
147
169
|
const deadline = Date.now() + timeoutMs;
|
|
148
170
|
while (Date.now() < deadline) {
|
|
@@ -176,6 +198,20 @@ function forceKillTree(pid) {
|
|
|
176
198
|
} catch {}
|
|
177
199
|
}
|
|
178
200
|
|
|
201
|
+
function forceKillPid(pid) {
|
|
202
|
+
try {
|
|
203
|
+
if (isWin) {
|
|
204
|
+
spawnSync('taskkill', ['/pid', String(pid), '/F'], {
|
|
205
|
+
shell: true,
|
|
206
|
+
windowsHide: true,
|
|
207
|
+
stdio: 'ignore',
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
211
|
+
}
|
|
212
|
+
} catch {}
|
|
213
|
+
}
|
|
214
|
+
|
|
179
215
|
function printFixTips(command) {
|
|
180
216
|
console.error('Common fixes:');
|
|
181
217
|
if (isWin) {
|