synabun 2026.4.26-beta.12 → 2026.4.26-beta.14
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.
|
@@ -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
|
|
@@ -1594,15 +1594,15 @@ async function gracefulShutdown(reason = 'shutdown') {
|
|
|
1594
1594
|
|
|
1595
1595
|
try { closeDb(); } catch {}
|
|
1596
1596
|
|
|
1597
|
-
// 5. Belt-and-suspenders: tree-kill THIS process's descendants on Windows.
|
|
1598
|
-
// Synabun spawns CLI links, pty terminals, cloudflare tunnel etc. that may
|
|
1599
|
-
// not be reachable through the registries above. The taskkill /T flag walks
|
|
1600
|
-
// the process tree and terminates every descendant.
|
|
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') {
|
|
1597
|
+
// 5. Belt-and-suspenders: tree-kill THIS process's descendants on Windows.
|
|
1598
|
+
// Synabun spawns CLI links, pty terminals, cloudflare tunnel etc. that may
|
|
1599
|
+
// not be reachable through the registries above. The taskkill /T flag walks
|
|
1600
|
+
// the process tree and terminates every descendant.
|
|
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') {
|
|
1606
1606
|
try {
|
|
1607
1607
|
// Use spawn(detached) so taskkill survives our own exit and finishes
|
|
1608
1608
|
// killing children even if we exit before it completes.
|
|
@@ -1639,45 +1639,45 @@ app.post('/api/server/shutdown', async (req, res) => {
|
|
|
1639
1639
|
}, 200);
|
|
1640
1640
|
});
|
|
1641
1641
|
|
|
1642
|
-
function shellSingleQuote(s) {
|
|
1643
|
-
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
1644
|
-
}
|
|
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
|
-
}
|
|
1642
|
+
function shellSingleQuote(s) {
|
|
1643
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
1644
|
+
}
|
|
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
1681
|
|
|
1682
1682
|
function cleanupOldUpdaterStages(baseDir) {
|
|
1683
1683
|
try {
|
|
@@ -1693,7 +1693,7 @@ function cleanupOldUpdaterStages(baseDir) {
|
|
|
1693
1693
|
} catch {}
|
|
1694
1694
|
}
|
|
1695
1695
|
|
|
1696
|
-
function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids = [] }) {
|
|
1696
|
+
function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids = [] }) {
|
|
1697
1697
|
const sourceUpdater = resolve(PACKAGE_ROOT, 'updater.mjs');
|
|
1698
1698
|
if (!existsSync(sourceUpdater)) {
|
|
1699
1699
|
throw new Error(`updater.mjs not found at ${sourceUpdater}. SynaBun install may be corrupt - reinstall manually: ${installPlan.displayCommand}`);
|
|
@@ -1718,22 +1718,27 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids
|
|
|
1718
1718
|
displayCommand: installPlan.displayCommand,
|
|
1719
1719
|
source: installPlan.source,
|
|
1720
1720
|
current: installPlan.current,
|
|
1721
|
-
target: installPlan.target,
|
|
1722
|
-
channel: installPlan.channel,
|
|
1723
|
-
cleanupPids,
|
|
1724
|
-
};
|
|
1721
|
+
target: installPlan.target,
|
|
1722
|
+
channel: installPlan.channel,
|
|
1723
|
+
cleanupPids,
|
|
1724
|
+
};
|
|
1725
1725
|
writeFileSync(payloadPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
1726
1726
|
|
|
1727
|
-
if (process.platform === 'win32') {
|
|
1728
|
-
const nodeBin = String(process.execPath || 'node').replace(/"/g, '');
|
|
1729
|
-
writeFileSync(runnerPath, [
|
|
1730
|
-
'@echo off',
|
|
1731
|
-
'setlocal enableextensions',
|
|
1732
|
-
'
|
|
1733
|
-
'
|
|
1734
|
-
'
|
|
1735
|
-
|
|
1736
|
-
|
|
1727
|
+
if (process.platform === 'win32') {
|
|
1728
|
+
const nodeBin = String(process.execPath || 'node').replace(/"/g, '');
|
|
1729
|
+
writeFileSync(runnerPath, [
|
|
1730
|
+
'@echo off',
|
|
1731
|
+
'setlocal enableextensions',
|
|
1732
|
+
'chcp 65001 >nul 2>&1',
|
|
1733
|
+
'title SynaBun Updater',
|
|
1734
|
+
'cd /d "%~dp0"',
|
|
1735
|
+
'set "EXIT_CODE=0"',
|
|
1736
|
+
`set "NODE_BIN=${nodeBin}"`,
|
|
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.',
|
|
1737
1742
|
'"%NODE_BIN%" "%~dp0updater.mjs" --payload "%~dp0payload.json" --no-hold',
|
|
1738
1743
|
'set "EXIT_CODE=%ERRORLEVEL%"',
|
|
1739
1744
|
'echo.',
|
|
@@ -1781,52 +1786,60 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids
|
|
|
1781
1786
|
// can lock the directory npm is trying to replace.
|
|
1782
1787
|
app.post('/api/system/run-update', async (req, res) => {
|
|
1783
1788
|
try {
|
|
1784
|
-
|
|
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;
|
|
1785
1792
|
|
|
1786
1793
|
if (!_synabunUpdateCache.checkedAt) {
|
|
1787
1794
|
try { await checkSynabunUpdate(); } catch {}
|
|
1788
|
-
}
|
|
1789
|
-
const installPlan = _synabunUpdateCache.installPlan;
|
|
1790
|
-
if (!_synabunUpdateCache.updateAvailable || !installPlan) {
|
|
1791
|
-
return res.status(409).json({ error: 'No SynaBun update is currently available.' });
|
|
1792
|
-
}
|
|
1793
|
-
if (installPlan.canAutoUpdate === false) {
|
|
1794
|
-
return res.status(409).json({
|
|
1795
|
-
error: installPlan.manualHint || `This installation cannot be auto-updated. Open ${installPlan.openUrl || SYNABUN_GITHUB_REPO_URL}`,
|
|
1796
|
-
installPlan,
|
|
1797
|
-
openUrl: installPlan.openUrl || SYNABUN_GITHUB_REPO_URL,
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
|
-
if (!installPlan.installSpec) {
|
|
1801
|
-
return res.status(500).json({ error: 'SynaBun update plan is missing an npm install target.' });
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
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;
|
|
1805
1812
|
const serverPid = process.pid;
|
|
1806
|
-
const cleanupPids = collectWindowsDescendantPids(serverPid);
|
|
1807
|
-
const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids });
|
|
1813
|
+
const cleanupPids = collectWindowsDescendantPids(serverPid);
|
|
1814
|
+
const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids });
|
|
1808
1815
|
|
|
1809
1816
|
let spawned = null;
|
|
1810
1817
|
let launchError = null;
|
|
1811
1818
|
|
|
1812
|
-
if (plat === 'win32') {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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
|
+
], {
|
|
1820
1835
|
cwd: staged.stageDir,
|
|
1821
1836
|
shell: false,
|
|
1822
1837
|
detached: true,
|
|
1823
1838
|
stdio: 'ignore',
|
|
1824
|
-
windowsHide: true,
|
|
1839
|
+
windowsHide: true, // hides only the wrapper cmd.exe; new console stays visible
|
|
1825
1840
|
env: {
|
|
1826
1841
|
...process.env,
|
|
1827
1842
|
SYNABUN_NODE_BIN: process.execPath,
|
|
1828
|
-
SYNABUN_UPDATER_RUNNER: staged.runnerPath,
|
|
1829
|
-
SYNABUN_UPDATER_CWD: staged.stageDir,
|
|
1830
1843
|
},
|
|
1831
1844
|
});
|
|
1832
1845
|
} catch (err) { launchError = err; }
|
|
@@ -4637,7 +4650,7 @@ function stopOpencodeServer() {
|
|
|
4637
4650
|
// On Windows `process.kill(pid, 'SIGTERM')` is unreliable for non-Node
|
|
4638
4651
|
// children (cmd.exe shells, opencode native binaries). Use taskkill /T /F
|
|
4639
4652
|
// to walk the process tree and force-terminate every descendant.
|
|
4640
|
-
// 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
|
|
4641
4654
|
// synabun@latest` — opencode-ai's `serve` process survives parent SynaBun
|
|
4642
4655
|
// exit and keeps file handles inside neural-interface/ open.
|
|
4643
4656
|
const winKill = (pid) => {
|
|
@@ -10477,7 +10490,7 @@ app.post('/api/search/memories', async (req, res) => {
|
|
|
10477
10490
|
// when EITHER source is newer than the installed version (catches one-sided
|
|
10478
10491
|
// publishes — e.g. release tagged on GitHub before npm publish completes).
|
|
10479
10492
|
|
|
10480
|
-
let _synabunUpdateCache = {
|
|
10493
|
+
let _synabunUpdateCache = {
|
|
10481
10494
|
current: null,
|
|
10482
10495
|
installedChannel: 'stable', // 'stable' | 'prerelease'
|
|
10483
10496
|
npmLatestStable: null, // dist-tags.latest
|
|
@@ -10491,97 +10504,97 @@ let _synabunUpdateCache = {
|
|
|
10491
10504
|
gitUpdateAvailable: false,
|
|
10492
10505
|
updateAvailable: false, // npmUpdateAvailable || gitUpdateAvailable
|
|
10493
10506
|
source: null, // 'npm' | 'github' | 'both' | null
|
|
10494
|
-
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
|
|
10495
10508
|
checkedAt: null,
|
|
10496
10509
|
npmError: null,
|
|
10497
|
-
gitError: null,
|
|
10498
|
-
};
|
|
10499
|
-
|
|
10500
|
-
const SYNABUN_GITHUB_REPO_URL = 'https://github.com/danilokhury/Synabun';
|
|
10501
|
-
|
|
10502
|
-
function isSynabunGitHubRemote(remoteText) {
|
|
10503
|
-
return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
|
|
10504
|
-
}
|
|
10505
|
-
|
|
10506
|
-
function detectSynabunInstallSource() {
|
|
10507
|
-
const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
|
|
10508
|
-
if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
|
|
10509
|
-
return { kind: 'npm-global' };
|
|
10510
|
-
}
|
|
10511
|
-
|
|
10512
|
-
try {
|
|
10513
|
-
const inside = execSync('git rev-parse --is-inside-work-tree', {
|
|
10514
|
-
cwd: PACKAGE_ROOT,
|
|
10515
|
-
encoding: 'utf8',
|
|
10516
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10517
|
-
timeout: 2000,
|
|
10518
|
-
}).trim();
|
|
10519
|
-
if (inside === 'true') {
|
|
10520
|
-
let remote = '';
|
|
10521
|
-
let remotes = '';
|
|
10522
|
-
try {
|
|
10523
|
-
remote = execSync('git remote get-url origin', {
|
|
10524
|
-
cwd: PACKAGE_ROOT,
|
|
10525
|
-
encoding: 'utf8',
|
|
10526
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10527
|
-
timeout: 2000,
|
|
10528
|
-
}).trim();
|
|
10529
|
-
} catch {}
|
|
10530
|
-
try {
|
|
10531
|
-
remotes = execSync('git remote -v', {
|
|
10532
|
-
cwd: PACKAGE_ROOT,
|
|
10533
|
-
encoding: 'utf8',
|
|
10534
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10535
|
-
timeout: 2000,
|
|
10536
|
-
}).trim();
|
|
10537
|
-
} catch {}
|
|
10538
|
-
if (isSynabunGitHubRemote(`${remote}\n${remotes}`)) {
|
|
10539
|
-
const firstRemote = remote || remotes.split(/\s+/).find(part => isSynabunGitHubRemote(part)) || null;
|
|
10540
|
-
return { kind: 'github-clone', remote: firstRemote };
|
|
10541
|
-
}
|
|
10542
|
-
return { kind: 'git-clone', remote: remote || null };
|
|
10543
|
-
}
|
|
10544
|
-
} catch {}
|
|
10545
|
-
|
|
10546
|
-
return { kind: 'local' };
|
|
10547
|
-
}
|
|
10548
|
-
|
|
10549
|
-
function buildSynabunInstallPlan({ current, installedChannel, latest, updateAvailable }) {
|
|
10550
|
-
if (!updateAvailable) return null;
|
|
10551
|
-
|
|
10552
|
-
const installSource = detectSynabunInstallSource();
|
|
10553
|
-
if (installSource.kind !== 'npm-global') {
|
|
10554
|
-
const clonedHint = installSource.kind === 'github-clone'
|
|
10555
|
-
? 'This SynaBun install is a GitHub checkout. Open the repository to pull or reinstall from the correct source.'
|
|
10556
|
-
: 'This SynaBun install was not detected as npm. Open the repository for the correct update path.';
|
|
10557
|
-
return {
|
|
10558
|
-
source: 'github',
|
|
10559
|
-
installSource: installSource.kind,
|
|
10560
|
-
canAutoUpdate: false,
|
|
10561
|
-
current,
|
|
10562
|
-
target: latest,
|
|
10563
|
-
channel: installedChannel,
|
|
10564
|
-
displayCommand: SYNABUN_GITHUB_REPO_URL,
|
|
10565
|
-
openUrl: SYNABUN_GITHUB_REPO_URL,
|
|
10566
|
-
manualCommand: installSource.kind === 'github-clone' ? 'git pull --ff-only' : null,
|
|
10567
|
-
manualHint: clonedHint,
|
|
10568
|
-
remote: installSource.remote,
|
|
10569
|
-
};
|
|
10570
|
-
}
|
|
10571
|
-
|
|
10572
|
-
const installSpec = 'synabun@latest';
|
|
10573
|
-
return {
|
|
10574
|
-
source: 'npm',
|
|
10575
|
-
installSource: installSource.kind,
|
|
10576
|
-
canAutoUpdate: true,
|
|
10577
|
-
current,
|
|
10578
|
-
target: latest,
|
|
10579
|
-
channel: installedChannel,
|
|
10580
|
-
installSpec,
|
|
10581
|
-
displayCommand: 'npm i -g synabun@latest',
|
|
10582
|
-
npmTag: 'latest',
|
|
10583
|
-
};
|
|
10584
|
-
}
|
|
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
|
+
}
|
|
10585
10598
|
|
|
10586
10599
|
async function checkSynabunUpdate() {
|
|
10587
10600
|
let current = _synabunUpdateCache.current;
|
|
@@ -10591,7 +10604,10 @@ async function checkSynabunUpdate() {
|
|
|
10591
10604
|
} catch { /* keep prior current if package.json read fails */ }
|
|
10592
10605
|
|
|
10593
10606
|
const curParsed = parseSemver(current);
|
|
10594
|
-
|
|
10607
|
+
// Numeric-only suffix (`2026.4.26-1`) is a post-release iteration, not a
|
|
10608
|
+
// prerelease — keep those on the stable channel. Only true alpha/beta/rc
|
|
10609
|
+
// suffixes flip the channel.
|
|
10610
|
+
const installedChannel = (curParsed?.pre && curParsed.iter == null) ? 'prerelease' : 'stable';
|
|
10595
10611
|
|
|
10596
10612
|
const fetchJsonWithTimeout = async (url, ms = 5000) => {
|
|
10597
10613
|
const controller = new AbortController();
|
|
@@ -10710,12 +10726,12 @@ async function checkSynabunUpdate() {
|
|
|
10710
10726
|
else if (npmUpdateAvailable) source = 'npm';
|
|
10711
10727
|
else if (gitUpdateAvailable) source = 'github';
|
|
10712
10728
|
|
|
10713
|
-
const installPlan = buildSynabunInstallPlan({
|
|
10714
|
-
current,
|
|
10715
|
-
installedChannel,
|
|
10716
|
-
latest,
|
|
10717
|
-
updateAvailable,
|
|
10718
|
-
});
|
|
10729
|
+
const installPlan = buildSynabunInstallPlan({
|
|
10730
|
+
current,
|
|
10731
|
+
installedChannel,
|
|
10732
|
+
latest,
|
|
10733
|
+
updateAvailable,
|
|
10734
|
+
});
|
|
10719
10735
|
|
|
10720
10736
|
_synabunUpdateCache = {
|
|
10721
10737
|
current,
|
|
@@ -10786,25 +10802,36 @@ function parseVersion(raw) {
|
|
|
10786
10802
|
}
|
|
10787
10803
|
|
|
10788
10804
|
// Structured semver parse — handles calver bumps and prerelease tags:
|
|
10789
|
-
// "2026.4.26" -> { major:2026, minor:4, patch:26, pre:null }
|
|
10790
|
-
// "2026.4.20-2" -> { major:2026, minor:4, patch:20, pre:"2"
|
|
10791
|
-
// "2026.4.26-beta.3" -> { major:2026, minor:4, patch:26, pre:"beta.3" }
|
|
10792
|
-
// "v1.2.3-rc.1" -> { major:1, minor:2, patch:3, pre:"rc.1" }
|
|
10805
|
+
// "2026.4.26" -> { major:2026, minor:4, patch:26, pre:null, iter:null }
|
|
10806
|
+
// "2026.4.20-2" -> { major:2026, minor:4, patch:20, pre:"2", iter:2 }
|
|
10807
|
+
// "2026.4.26-beta.3" -> { major:2026, minor:4, patch:26, pre:"beta.3", iter:null }
|
|
10808
|
+
// "v1.2.3-rc.1" -> { major:1, minor:2, patch:3, pre:"rc.1", iter:null }
|
|
10809
|
+
//
|
|
10810
|
+
// `iter` is set when the suffix is purely numeric (SynaBun's calver iteration
|
|
10811
|
+
// scheme: `2026.4.26-1` is the FIRST patch published after `2026.4.26`).
|
|
10812
|
+
// Strict semver §9 treats any prerelease suffix as PRE-release (lower than no
|
|
10813
|
+
// suffix). SynaBun publishes calver iterations as `latest` on npm, so we need
|
|
10814
|
+
// to treat numeric-only suffixes as POST-release iterations instead.
|
|
10793
10815
|
function parseSemver(raw) {
|
|
10794
10816
|
if (raw == null) return null;
|
|
10795
10817
|
const s = String(raw).trim().replace(/^v\.?/, '');
|
|
10796
10818
|
const m = s.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
|
|
10797
10819
|
if (!m) return null;
|
|
10820
|
+
const pre = m[4] || null;
|
|
10821
|
+
const iter = pre && /^\d+$/.test(pre) ? Number(pre) : null;
|
|
10798
10822
|
return {
|
|
10799
10823
|
major: +m[1],
|
|
10800
10824
|
minor: +m[2],
|
|
10801
10825
|
patch: +m[3],
|
|
10802
|
-
pre
|
|
10826
|
+
pre,
|
|
10827
|
+
iter,
|
|
10803
10828
|
};
|
|
10804
10829
|
}
|
|
10805
10830
|
|
|
10806
|
-
//
|
|
10807
|
-
// 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0
|
|
10831
|
+
// Precedence (mix of semver §11 + SynaBun calver-iteration convention):
|
|
10832
|
+
// 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0 < 1.0.0-1 < 1.0.0-2
|
|
10833
|
+
// Numeric-only suffix = post-release iteration (NEWER than no-suffix release).
|
|
10834
|
+
// Mixed/alpha suffix = prerelease (OLDER than no-suffix release, per semver).
|
|
10808
10835
|
// Returns negative if a<b, positive if a>b, 0 if equal. Accepts strings or
|
|
10809
10836
|
// parsed objects.
|
|
10810
10837
|
function compareSemver(a, b) {
|
|
@@ -10816,11 +10843,19 @@ function compareSemver(a, b) {
|
|
|
10816
10843
|
if (pa.major !== pb.major) return pa.major - pb.major;
|
|
10817
10844
|
if (pa.minor !== pb.minor) return pa.minor - pb.minor;
|
|
10818
10845
|
if (pa.patch !== pb.patch) return pa.patch - pb.patch;
|
|
10819
|
-
// Equal x.y.z.
|
|
10846
|
+
// Equal x.y.z. Order from oldest → newest:
|
|
10847
|
+
// alpha-prerelease < no-suffix release < numeric iteration
|
|
10820
10848
|
if (!pa.pre && !pb.pre) return 0;
|
|
10849
|
+
// Numeric iterations are POST-release: always newer than no-suffix and
|
|
10850
|
+
// newer than alpha prereleases.
|
|
10851
|
+
if (pa.iter != null && pb.iter == null) return 1;
|
|
10852
|
+
if (pa.iter == null && pb.iter != null) return -1;
|
|
10853
|
+
if (pa.iter != null && pb.iter != null) return pa.iter - pb.iter;
|
|
10854
|
+
// Neither side is a numeric iteration. Standard semver §11: no-suffix is
|
|
10855
|
+
// greater than alpha prerelease.
|
|
10821
10856
|
if (!pa.pre && pb.pre) return 1;
|
|
10822
10857
|
if ( pa.pre && !pb.pre) return -1;
|
|
10823
|
-
// Both have prerelease — compare identifiers per semver §11.4.
|
|
10858
|
+
// Both have alpha-flavoured prerelease — compare identifiers per semver §11.4.
|
|
10824
10859
|
const ia = pa.pre.split('.');
|
|
10825
10860
|
const ib = pb.pre.split('.');
|
|
10826
10861
|
const len = Math.max(ia.length, ib.length);
|
|
@@ -23852,7 +23887,7 @@ setInterval(saveSessionSnapshot, SESSION_SNAPSHOT_INTERVAL_MS);
|
|
|
23852
23887
|
// Manual termination paths (Ctrl+C, kill, parent shell exit) all flow through
|
|
23853
23888
|
// gracefulShutdown — same teardown the in-app restart/shutdown/update paths
|
|
23854
23889
|
// use. Without this, hand-killed servers still leave orphaned opencode +
|
|
23855
|
-
// agent children holding file handles, which blocks `npm i -g synabun`.
|
|
23890
|
+
// agent children holding file handles, which blocks `npm i -g synabun`.
|
|
23856
23891
|
process.on('SIGINT', async () => {
|
|
23857
23892
|
console.log('\n[server] SIGINT received.');
|
|
23858
23893
|
await gracefulShutdown('sigint');
|