synabun 2026.4.26-beta.10 → 2026.4.26-beta.12

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.
@@ -5094,15 +5094,51 @@
5094
5094
  }
5095
5095
  .update-backup-btn:hover { background: rgba(52,199,89,0.16); }
5096
5096
  .update-backup-btn:disabled { opacity: 0.5; pointer-events: none; }
5097
- .update-backup-status {
5098
- display: flex;
5099
- align-items: center;
5100
- gap: 8px;
5097
+ .update-backup-status {
5098
+ display: flex;
5099
+ align-items: center;
5100
+ gap: 8px;
5101
5101
  font-size: 11px;
5102
5102
  color: var(--t-muted);
5103
- padding: 4px 0;
5104
- }
5105
- .update-backup-icon { font-size: 14px; font-weight: 700; }
5103
+ padding: 4px 0;
5104
+ }
5105
+ .update-run-status {
5106
+ align-items: flex-start;
5107
+ gap: 10px;
5108
+ padding: 10px 12px;
5109
+ border-radius: var(--r-card);
5110
+ border: 1px solid rgba(255,255,255,0.08);
5111
+ background: rgba(255,255,255,0.045);
5112
+ color: var(--t-secondary);
5113
+ font-size: 12px;
5114
+ line-height: 1.4;
5115
+ }
5116
+ .update-run-status .update-backup-icon {
5117
+ flex: 0 0 auto;
5118
+ margin-top: 1px;
5119
+ }
5120
+ .update-run-status strong {
5121
+ display: block;
5122
+ color: var(--t-primary);
5123
+ font-size: 12px;
5124
+ font-weight: 600;
5125
+ margin-bottom: 2px;
5126
+ }
5127
+ .update-run-detail,
5128
+ .update-run-hint {
5129
+ display: block;
5130
+ color: var(--t-muted);
5131
+ }
5132
+ .update-run-hint {
5133
+ margin-top: 5px;
5134
+ opacity: .82;
5135
+ }
5136
+ .update-run-status code {
5137
+ font-family: var(--ff-mono);
5138
+ font-size: 11px;
5139
+ color: var(--t-secondary);
5140
+ }
5141
+ .update-backup-icon { font-size: 14px; font-weight: 700; }
5106
5142
  .update-backup-icon.spin { animation: updateSpin 1s linear infinite; }
5107
5143
  @keyframes updateSpin {
5108
5144
  from { transform: rotate(0deg); }
@@ -7,6 +7,7 @@
7
7
  import { showUpdateToast } from './ui-tool-updates.js';
8
8
 
9
9
  const $ = (id) => document.getElementById(id);
10
+ const SYNABUN_GITHUB_URL = 'https://github.com/danilokhury/Synabun';
10
11
 
11
12
  let _versionData = null;
12
13
  let _btnWired = false;
@@ -187,27 +188,37 @@ function openUpdateModal() {
187
188
 
188
189
  function renderStep3() {
189
190
  const installPlan = _versionData.installPlan || {};
190
- const canAutoUpdate = installPlan.canAutoUpdate !== false;
191
- const installCmd = installPlan.displayCommand || 'npm i -g synabun@latest';
191
+ const canAutoUpdate = installPlan.canAutoUpdate === true;
192
+ const repoUrl = installPlan.openUrl || SYNABUN_GITHUB_URL;
193
+ const installCmd = canAutoUpdate
194
+ ? (installPlan.displayCommand || 'npm i -g synabun@latest')
195
+ : repoUrl;
192
196
  const installIntro = canAutoUpdate
193
197
  ? 'SynaBun will close and run this command in a new terminal window:'
194
- : 'GitHub clone detected. Run this command from your cloned SynaBun repository:';
198
+ : 'This SynaBun install was not detected as an npm install. Open the GitHub repository to update from your checkout or installer:';
199
+ const title = canAutoUpdate ? 'Run the Update' : 'Update from GitHub';
195
200
  const installSource = canAutoUpdate
196
201
  ? 'Installing from npm.'
197
- : (installPlan.manualHint || 'Restart SynaBun after the GitHub clone update finishes.');
202
+ : (installPlan.manualHint || 'Open the SynaBun repository for the correct update path.');
198
203
  const autoRestartControl = canAutoUpdate ? `
199
204
  <label class="update-skip-label" style="margin:14px 0 8px;display:flex;align-items:center;gap:8px;cursor:pointer">
200
205
  <input type="checkbox" id="update-auto-restart" checked>
201
206
  <span>Relaunch SynaBun after update</span>
202
207
  </label>` : '';
208
+ const manualButton = canAutoUpdate ? `
209
+ <button class="action-btn action-btn--ghost" id="update-manual" title="Run the command yourself in your own terminal">Manual</button>` : '';
203
210
  const updateNowButton = canAutoUpdate ? `
204
211
  <button class="action-btn action-btn--primary" id="update-run-now">
205
212
  <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><polyline points="8 17 12 21 16 17"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"/></svg>
206
213
  Update Now
207
- </button>` : '';
214
+ </button>` : `
215
+ <button class="action-btn action-btn--primary" id="update-run-now" data-url="${esc(repoUrl)}">
216
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
217
+ Open GitHub
218
+ </button>`;
208
219
 
209
220
  return `
210
- <h3 class="update-modal-title">Run the Update</h3>
221
+ <h3 class="update-modal-title">${title}</h3>
211
222
  <p style="font-size:12px;color:var(--t-muted);margin:4px 0 12px">
212
223
  ${esc(installIntro)}
213
224
  </p>
@@ -227,12 +238,12 @@ function openUpdateModal() {
227
238
 
228
239
  <div class="tag-delete-modal-actions" style="margin-top:14px;flex-wrap:wrap;gap:8px">
229
240
  <button class="action-btn action-btn--ghost" id="update-back">Back</button>
230
- <button class="action-btn action-btn--ghost" id="update-manual" title="Run the command yourself in your own terminal">Manual</button>
241
+ ${manualButton}
231
242
  ${updateNowButton}
232
243
  </div>
233
244
 
234
245
  <p id="update-manual-note" style="display:none;font-size:11px;color:var(--t-muted);opacity:.75;margin:10px 0 0">
235
- After running the command, restart SynaBun: <code>synabun</code>
246
+ After running the command, restart SynaBun with <code>synabun</code>.
236
247
  </p>`;
237
248
  }
238
249
 
@@ -298,7 +309,9 @@ function openUpdateModal() {
298
309
  }
299
310
 
300
311
  async function handleRunUpdate() {
301
- if (_versionData?.installPlan?.canAutoUpdate === false) return;
312
+ const installPlan = _versionData?.installPlan || {};
313
+ const canAutoUpdate = installPlan.canAutoUpdate === true;
314
+ const repoUrl = installPlan.openUrl || SYNABUN_GITHUB_URL;
302
315
 
303
316
  const autoRestartCb = overlay.querySelector('#update-auto-restart');
304
317
  const autoRestart = !!autoRestartCb?.checked;
@@ -310,16 +323,27 @@ function openUpdateModal() {
310
323
  const icon = overlay.querySelector('#update-run-icon');
311
324
  const text = overlay.querySelector('#update-run-text');
312
325
 
313
- if (!runBtn || !backBtn || !manualBtn || !status || !icon || !text) return;
326
+ if (!runBtn || !backBtn || !status || !icon || !text) return;
327
+
328
+ if (!canAutoUpdate) {
329
+ window.open(repoUrl, '_blank', 'noopener,noreferrer');
330
+ status.style.display = 'flex';
331
+ icon.className = 'update-backup-icon';
332
+ icon.textContent = '↗';
333
+ icon.style.color = '#4fc3f7';
334
+ text.innerHTML = `<strong>GitHub repository opened</strong><span class="update-run-detail">Use the repository instructions to update this non-npm installation.</span>`;
335
+ return;
336
+ }
314
337
 
315
338
  runBtn.disabled = true;
316
339
  backBtn.disabled = true;
317
- manualBtn.disabled = true;
340
+ if (manualBtn) manualBtn.disabled = true;
318
341
  runBtn.innerHTML = '<span class="update-spinner"></span> Launching updater...';
319
342
  status.style.display = 'flex';
320
343
  icon.className = 'update-backup-icon spin';
321
344
  icon.textContent = '⟳';
322
- text.textContent = 'Opening updater terminal and shutting down server...';
345
+ icon.style.color = '';
346
+ text.innerHTML = '<strong>Launching updater</strong><span class="update-run-detail">Opening a terminal window for the npm update.</span>';
323
347
 
324
348
  try {
325
349
  const res = await fetch('/api/system/run-update', {
@@ -332,12 +356,12 @@ function openUpdateModal() {
332
356
  try { const b = await res.json(); msg = b.error || msg; } catch {}
333
357
  throw new Error(msg);
334
358
  }
335
- const body = await res.json();
359
+ await res.json();
336
360
 
337
361
  icon.className = 'update-backup-icon';
338
362
  icon.textContent = '✓';
339
363
  icon.style.color = '#34c759';
340
- text.innerHTML = `Updater terminal launched. <strong>This window will go offline.</strong> Watch the new terminal for install progress${autoRestart ? ' — SynaBun relaunches when done.' : '. Run <code>synabun</code> after install completes.'}`;
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${autoRestart ? '; SynaBun will relaunch when it finishes.' : '; run <code>synabun</code> 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>`;
341
365
  runBtn.innerHTML = 'Updater running…';
342
366
 
343
367
  // Server is shutting down. Optionally redirect to a friendly offline
@@ -351,7 +375,7 @@ function openUpdateModal() {
351
375
  text.textContent = 'Failed to launch updater: ' + err.message;
352
376
  runBtn.disabled = false;
353
377
  backBtn.disabled = false;
354
- manualBtn.disabled = false;
378
+ if (manualBtn) manualBtn.disabled = false;
355
379
  runBtn.innerHTML = `<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><polyline points="8 17 12 21 16 17"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"/></svg> Retry`;
356
380
  }
357
381
  }
@@ -1594,11 +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
- if (process.platform === 'win32') {
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') {
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.
@@ -1635,9 +1639,45 @@ app.post('/api/server/shutdown', async (req, res) => {
1635
1639
  }, 200);
1636
1640
  });
1637
1641
 
1638
- function shellSingleQuote(s) {
1639
- return `'${String(s).replace(/'/g, `'\\''`)}'`;
1640
- }
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
+ }
1641
1681
 
1642
1682
  function cleanupOldUpdaterStages(baseDir) {
1643
1683
  try {
@@ -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}`);
@@ -1678,20 +1718,22 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
1678
1718
  displayCommand: installPlan.displayCommand,
1679
1719
  source: installPlan.source,
1680
1720
  current: installPlan.current,
1681
- target: installPlan.target,
1682
- channel: installPlan.channel,
1683
- };
1721
+ target: installPlan.target,
1722
+ channel: installPlan.channel,
1723
+ cleanupPids,
1724
+ };
1684
1725
  writeFileSync(payloadPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
1685
1726
 
1686
- if (process.platform === 'win32') {
1687
- writeFileSync(runnerPath, [
1688
- '@echo off',
1689
- 'setlocal enableextensions',
1690
- 'title SynaBun Updater',
1691
- 'cd /d "%~dp0"',
1692
- 'set "EXIT_CODE=0"',
1693
- 'set "NODE_BIN=node"',
1694
- 'if defined SYNABUN_NODE_BIN set "NODE_BIN=%SYNABUN_NODE_BIN%"',
1727
+ if (process.platform === 'win32') {
1728
+ const nodeBin = String(process.execPath || 'node').replace(/"/g, '');
1729
+ writeFileSync(runnerPath, [
1730
+ '@echo off',
1731
+ 'setlocal enableextensions',
1732
+ 'title SynaBun Updater',
1733
+ 'cd /d "%~dp0"',
1734
+ 'set "EXIT_CODE=0"',
1735
+ `set "NODE_BIN=${nodeBin}"`,
1736
+ 'if defined SYNABUN_NODE_BIN set "NODE_BIN=%SYNABUN_NODE_BIN%"',
1695
1737
  '"%NODE_BIN%" "%~dp0updater.mjs" --payload "%~dp0payload.json" --no-hold',
1696
1738
  'set "EXIT_CODE=%ERRORLEVEL%"',
1697
1739
  'echo.',
@@ -1750,8 +1792,9 @@ app.post('/api/system/run-update', async (req, res) => {
1750
1792
  }
1751
1793
  if (installPlan.canAutoUpdate === false) {
1752
1794
  return res.status(409).json({
1753
- error: installPlan.manualHint || `This installation cannot be auto-updated. Run manually: ${installPlan.displayCommand}`,
1795
+ error: installPlan.manualHint || `This installation cannot be auto-updated. Open ${installPlan.openUrl || SYNABUN_GITHUB_REPO_URL}`,
1754
1796
  installPlan,
1797
+ openUrl: installPlan.openUrl || SYNABUN_GITHUB_REPO_URL,
1755
1798
  });
1756
1799
  }
1757
1800
  if (!installPlan.installSpec) {
@@ -1760,19 +1803,20 @@ app.post('/api/system/run-update', async (req, res) => {
1760
1803
 
1761
1804
  const plat = process.platform;
1762
1805
  const serverPid = process.pid;
1763
- const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart });
1806
+ const cleanupPids = collectWindowsDescendantPids(serverPid);
1807
+ const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids });
1764
1808
 
1765
1809
  let spawned = null;
1766
1810
  let launchError = null;
1767
1811
 
1768
- if (plat === 'win32') {
1769
- try {
1770
- spawned = spawn('powershell.exe', [
1771
- '-NoProfile',
1772
- '-ExecutionPolicy', 'Bypass',
1773
- '-Command',
1774
- '$p=$env:SYNABUN_UPDATER_RUNNER; $w=$env:SYNABUN_UPDATER_CWD; Start-Process -FilePath $p -WorkingDirectory $w -WindowStyle Normal',
1775
- ], {
1812
+ if (plat === 'win32') {
1813
+ try {
1814
+ spawned = spawn('powershell.exe', [
1815
+ '-NoProfile',
1816
+ '-ExecutionPolicy', 'Bypass',
1817
+ '-Command',
1818
+ '$p=$env:SYNABUN_UPDATER_RUNNER; $w=$env:SYNABUN_UPDATER_CWD; $cmd=$env:ComSpec; if (-not $cmd) { $cmd="cmd.exe" }; Start-Process -FilePath $cmd -ArgumentList @("/k", "`"$p`"") -WorkingDirectory $w -WindowStyle Normal',
1819
+ ], {
1776
1820
  cwd: staged.stageDir,
1777
1821
  shell: false,
1778
1822
  detached: true,
@@ -10433,7 +10477,7 @@ app.post('/api/search/memories', async (req, res) => {
10433
10477
  // when EITHER source is newer than the installed version (catches one-sided
10434
10478
  // publishes — e.g. release tagged on GitHub before npm publish completes).
10435
10479
 
10436
- let _synabunUpdateCache = {
10480
+ let _synabunUpdateCache = {
10437
10481
  current: null,
10438
10482
  installedChannel: 'stable', // 'stable' | 'prerelease'
10439
10483
  npmLatestStable: null, // dist-tags.latest
@@ -10447,12 +10491,14 @@ let _synabunUpdateCache = {
10447
10491
  gitUpdateAvailable: false,
10448
10492
  updateAvailable: false, // npmUpdateAvailable || gitUpdateAvailable
10449
10493
  source: null, // 'npm' | 'github' | 'both' | null
10450
- installPlan: null, // server-owned npm install target for click-to-update
10494
+ installPlan: null, // server-owned npm/GitHub action target for click-to-update
10451
10495
  checkedAt: null,
10452
10496
  npmError: null,
10453
- gitError: null,
10454
- };
10455
-
10497
+ gitError: null,
10498
+ };
10499
+
10500
+ const SYNABUN_GITHUB_REPO_URL = 'https://github.com/danilokhury/Synabun';
10501
+
10456
10502
  function isSynabunGitHubRemote(remoteText) {
10457
10503
  return /github\.com[:/]danilokhury\/Synabun(?:\.git)?(?:[\s)]|$)/i.test(String(remoteText || ''));
10458
10504
  }
@@ -10504,17 +10550,21 @@ function buildSynabunInstallPlan({ current, installedChannel, latest, updateAvai
10504
10550
  if (!updateAvailable) return null;
10505
10551
 
10506
10552
  const installSource = detectSynabunInstallSource();
10507
- if (installSource.kind === 'github-clone') {
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.';
10508
10557
  return {
10509
- source: 'github-clone',
10558
+ source: 'github',
10510
10559
  installSource: installSource.kind,
10511
10560
  canAutoUpdate: false,
10512
10561
  current,
10513
10562
  target: latest,
10514
10563
  channel: installedChannel,
10515
- displayCommand: 'git pull --ff-only',
10516
- manualCommand: 'git pull --ff-only',
10517
- manualHint: 'GitHub clone detected. Run this from your cloned SynaBun repository, then restart SynaBun.',
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,
10518
10568
  remote: installSource.remote,
10519
10569
  };
10520
10570
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.26-beta.10",
3
+ "version": "2026.4.26-beta.12",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",
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
- forceKillTree(serverPid);
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 (isWin && serverPid) forceKillTree(serverPid);
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) {