@yemi33/minions 0.1.1826 → 0.1.1828

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1828 (2026-05-09)
4
+
5
+ ### Other
6
+ - refactor(cli): atomic clear, soft-fail browser-open, fix stale test message
7
+
8
+ ## 0.1.1827 (2026-05-09)
9
+
10
+ ### Other
11
+ - refactor(cli): consolidate browser-open helpers and tighten restart flow
12
+
3
13
  ## 0.1.1826 (2026-05-09)
4
14
 
5
15
  ### Fixes
package/bin/minions.js CHANGED
@@ -40,9 +40,9 @@ const { spawn, spawnSync, execSync } = require('child_process');
40
40
 
41
41
  const PKG_ROOT = path.resolve(__dirname, '..');
42
42
  const shared = require(path.join(PKG_ROOT, 'engine', 'shared'));
43
+ const { openUrlInBrowser } = shared;
43
44
  const { waitForRestartHealth, formatRestartHealthError } = require(path.join(PKG_ROOT, 'engine', 'restart-health'));
44
45
  const DASH_PORT = 7331;
45
- const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
46
46
 
47
47
  /** Returns PIDs (as strings) of processes LISTENING on `port`. Empty on no match
48
48
  * or when the platform tool (netstat/findstr/lsof) is unavailable. */
@@ -74,28 +74,16 @@ function killByPort(port) {
74
74
 
75
75
  const isPortListening = (port) => getListeningPids(port).length > 0;
76
76
 
77
- function hasRecentDashboardBrowserTab(minionsHome, now = Date.now()) {
78
- try {
79
- const fp = path.join(minionsHome, 'engine', 'dashboard-browser.json');
80
- const state = JSON.parse(fs.readFileSync(fp, 'utf8'));
81
- const tabs = state && state.tabs && typeof state.tabs === 'object' ? state.tabs : {};
82
- return Object.values(tabs).some(tab => {
83
- const lastSeen = Number(tab && tab.lastSeen);
84
- return Number.isFinite(lastSeen) && now - lastSeen <= DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS;
85
- });
86
- } catch { return false; }
87
- }
88
-
89
- // Clear the browser-presence cache. Pre-restart beacons can outlive the
90
- // browser window that produced them (closed Edge, locked-screen RDP session,
91
- // stale heuristic timer) and falsely tell `restart` to skip the auto-open. We
92
- // wipe it during restart so the post-restart probe only counts beacons that
93
- // arrive AFTER the new dashboard is up — i.e. from a still-living tab.
77
+ // Pre-restart beacons can outlive the browser window that produced them
78
+ // (closed Edge, locked-screen RDP session) and falsely tell restart to skip
79
+ // the auto-open. We wipe the file during restart so the post-restart probe
80
+ // only counts beacons that arrive AFTER the new dashboard is up.
94
81
  function _clearDashboardBrowserState(minionsHome) {
95
- try {
96
- const fp = path.join(minionsHome, 'engine', 'dashboard-browser.json');
97
- fs.writeFileSync(fp, JSON.stringify({ tabs: {}, updatedAt: new Date().toISOString() }));
98
- } catch {}
82
+ // Atomic write so the new dashboard can't read a torn file mid-write.
83
+ shared.safeWrite(
84
+ path.join(minionsHome, 'engine', 'dashboard-browser.json'),
85
+ { tabs: {}, updatedAt: new Date().toISOString() },
86
+ );
99
87
  }
100
88
 
101
89
  async function _waitForBrowserReconnect(minionsHome, { afterMs, timeoutMs = 5000, pollMs = 500 } = {}) {
@@ -113,12 +101,9 @@ async function _waitForBrowserReconnect(minionsHome, { afterMs, timeoutMs = 5000
113
101
  }
114
102
 
115
103
  function _openInBrowser(url) {
116
- try {
117
- if (process.platform === 'win32') execSync(`start "" "${url}"`, { stdio: 'ignore', windowsHide: true });
118
- else if (process.platform === 'darwin') execSync(`open "${url}"`, { stdio: 'ignore' });
119
- else execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
120
- } catch (e) {
121
- console.log(` Could not auto-open browser: ${e.message}`);
104
+ const result = openUrlInBrowser(url);
105
+ if (!result.ok) {
106
+ console.log(` Could not auto-open browser: ${result.error}`);
122
107
  console.log(` Please open ${url} manually.`);
123
108
  }
124
109
  }
@@ -180,12 +165,10 @@ function killMinionsProcesses(patterns) {
180
165
  } catch {}
181
166
  }
182
167
 
183
- /** Spawn a detached dashboard. When `suppressOpen` is true, the new dashboard
184
- * skips its auto-open of the browser the existing tab will reconnect. */
185
- function spawnDashboard(suppressOpen) {
186
- const env = { ...process.env };
187
- if (suppressOpen) env.MINIONS_NO_AUTO_OPEN = '1';
188
- else delete env.MINIONS_NO_AUTO_OPEN;
168
+ /** Spawn a detached dashboard with self-open suppressed the CLI decides
169
+ * when to open a browser based on whether a real tab reconnects post-health. */
170
+ function spawnDashboard() {
171
+ const env = { ...process.env, MINIONS_NO_AUTO_OPEN: '1' };
189
172
  const proc = spawn(process.execPath, [path.join(MINIONS_HOME, 'dashboard.js')], {
190
173
  cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true, env
191
174
  });
@@ -451,13 +434,12 @@ function init() {
451
434
  const dashWasUp = isPortListening(DASH_PORT);
452
435
  const restartStartMs = Date.now();
453
436
  if (isUpgrade) {
454
- // Clear stale beacons so the post-restart probe only counts tabs that
455
- // reconnect to the NEW dashboard.
456
- _clearDashboardBrowserState(MINIONS_HOME);
457
437
  try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
458
438
  // Free the dashboard port too — without this the new dashboard EADDRINUSE-dies
459
439
  // silently and the user keeps running stale code from the old dashboard process.
460
440
  killByPort(DASH_PORT);
441
+ // Clear AFTER kill so the old dashboard can't repopulate during shutdown.
442
+ _clearDashboardBrowserState(MINIONS_HOME);
461
443
  }
462
444
  console.log(isUpgrade
463
445
  ? `\n Upgrade complete (${pkgVersion}). Restarting engine and dashboard...\n`
@@ -468,27 +450,20 @@ function init() {
468
450
  engineProc.unref();
469
451
  console.log(` Engine started (PID: ${engineProc.pid})`);
470
452
 
471
- // Always suppress dashboard's self-open — we decide here after the health
472
- // check based on whether an existing browser tab actually reconnects.
473
- const dashProc = spawnDashboard(true);
453
+ const dashProc = spawnDashboard();
474
454
  console.log(` Dashboard started (PID: ${dashProc.pid})`);
475
455
  console.log(` Dashboard: http://localhost:${DASH_PORT}`);
476
456
 
477
457
  void (async () => {
478
- let shouldOpen = forceOpen || !dashWasUp;
479
- if (!shouldOpen) {
480
- // Hot upgrade: give an existing browser tab up to 5s to reconnect via
481
- // its 4s auto-refresh poll. If nothing beacons, the tab is gone — open.
482
- const reconnected = await _waitForBrowserReconnect(MINIONS_HOME, {
483
- afterMs: restartStartMs, timeoutMs: 5000,
484
- });
485
- shouldOpen = !reconnected;
486
- }
458
+ const shouldOpen = forceOpen || !dashWasUp ||
459
+ !(await _waitForBrowserReconnect(MINIONS_HOME, { afterMs: restartStartMs, timeoutMs: 5000 }));
487
460
  if (shouldOpen) {
488
461
  console.log(` Opening dashboard in browser...`);
489
462
  _openInBrowser(`http://localhost:${DASH_PORT}`);
490
463
  }
491
- })();
464
+ })().catch(err => {
465
+ console.log(` Could not open dashboard: ${err.message}`);
466
+ });
492
467
 
493
468
  // Next steps guidance
494
469
  console.log(`
@@ -740,36 +715,30 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
740
715
  } else if (cmd === 'add' || cmd === 'remove' || cmd === 'list' || cmd === 'scan') {
741
716
  delegate('minions.js', [cmd, ...rest]);
742
717
  } else if (cmd === 'restart') {
743
- // Start both engine and dashboard — the go-to command after a reboot.
744
718
  // `--cli` / `--model` flags forward to `engine.js start` so the runtime
745
719
  // fleet flips before the daemon spawns (P-6b3f9c2e AC: works on restart).
746
720
  ensureInstalled();
747
721
  const dashWasUp = isPortListening(DASH_PORT);
748
- // Mark the restart boundary so post-restart beacon timestamps are unambiguous,
749
- // and clear stale beacons (browsers that closed without notifying).
750
722
  const restartStartMs = Date.now();
751
- _clearDashboardBrowserState(MINIONS_HOME);
752
- // Layered kill each step is best-effort, layered so the next still runs if
753
- // one fails. Goal: the old engine is gone before we spawn a new one, even if
723
+ // Layered kill — each step is best-effort, so the next still runs if one
724
+ // fails. Goal: the old engine is gone before we spawn a new one, even if
754
725
  // PowerShell is unavailable, the engine is hung, or its cmdline doesn't match.
755
726
  const oldEnginePid = readEnginePid(MINIONS_HOME);
756
- // 1. Graceful stop — short timeout so a hung engine can't block what follows.
757
727
  try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
758
- // 2. Force-kill the recorded engine PID (NOT the tree — agent children must
759
- // survive so the new engine can re-attach them via PID files).
728
+ // Force-kill the recorded engine PID (NOT the tree — agent children must
729
+ // survive so the new engine can re-attach them via PID files).
760
730
  killPidOnly(oldEnginePid);
761
- // 3. Free dashboard port (catches orphan dashboards with no recorded PID).
762
731
  killByPort(DASH_PORT);
763
- // 4. Belt-and-suspenders cmdline match for anything still alive.
764
732
  killMinionsProcesses(['engine.js', 'dashboard.js']);
733
+ // Clear stale beacons AFTER the kill so the old dashboard's last writes
734
+ // can't repopulate the file in the gap between clear and shutdown.
735
+ _clearDashboardBrowserState(MINIONS_HOME);
765
736
  const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
766
737
  cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
767
738
  });
768
739
  engineProc.unref();
769
740
  console.log(`\n Engine started (PID: ${engineProc.pid})`);
770
- // Always tell the dashboard to skip its own auto-open — we'll decide here
771
- // after observing whether an existing browser tab reconnects.
772
- const dashProc = spawnDashboard(true);
741
+ const dashProc = spawnDashboard();
773
742
  console.log(` Dashboard started (PID: ${dashProc.pid})`);
774
743
  console.log(` Dashboard: http://localhost:${DASH_PORT}`);
775
744
  console.log(' Verifying restart health...');
@@ -784,21 +753,17 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
784
753
  }
785
754
  console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.`);
786
755
 
787
- let shouldOpen = forceOpen || !dashWasUp;
788
- if (!shouldOpen) {
789
- const reconnected = await _waitForBrowserReconnect(MINIONS_HOME, {
790
- afterMs: restartStartMs, timeoutMs: 5000,
791
- });
792
- shouldOpen = !reconnected;
793
- }
756
+ const shouldOpen = forceOpen || !dashWasUp ||
757
+ !(await _waitForBrowserReconnect(MINIONS_HOME, { afterMs: restartStartMs, timeoutMs: 5000 }));
794
758
  if (shouldOpen) {
795
759
  console.log(` Opening dashboard in browser...`);
796
760
  _openInBrowser(`http://localhost:${DASH_PORT}`);
797
761
  }
798
762
  console.log('');
799
763
  })().catch(err => {
800
- console.error(`\n ERROR: Restart verification failed: ${err.message}\n`);
801
- process.exit(1);
764
+ // Health check failures already process.exit(1) above; reaching here means
765
+ // the browser-open path threw, which is non-fatal — restart already verified.
766
+ console.log(` Could not open dashboard: ${err.message}`);
802
767
  });
803
768
  } else if (cmd === 'nuke') {
804
769
  ensureInstalled();
@@ -967,10 +932,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
967
932
  handled = true;
968
933
  const url = `http://localhost:${DASH_PORT}`;
969
934
  console.log(`\n Dashboard already running: ${url}\n`);
970
- try {
971
- const openCmd = process.platform === 'win32' ? `start ${url}` : process.platform === 'darwin' ? `open ${url}` : `xdg-open ${url}`;
972
- execSync(openCmd, { stdio: 'ignore', windowsHide: true });
973
- } catch {}
935
+ openUrlInBrowser(url);
974
936
  });
975
937
  sock.on('error', () => {
976
938
  sock.destroy();
package/dashboard.js CHANGED
@@ -9422,20 +9422,12 @@ if (require.main === module) {
9422
9422
  console.log(`\n Auto-refreshes every 4s. Ctrl+C to stop.\n`);
9423
9423
 
9424
9424
  // Auto-open the browser unless suppressed. `minions restart` and the
9425
- // upgrade path set MINIONS_NO_AUTO_OPEN=1 only when a browser tab was
9426
- // recently polling the old dashboard, so a new tab would just be a duplicate.
9425
+ // upgrade path set MINIONS_NO_AUTO_OPEN=1 because the CLI orchestrates the
9426
+ // open itself after observing whether an existing tab reconnected.
9427
9427
  if (!process.env.MINIONS_NO_AUTO_OPEN) {
9428
- const { exec } = require('child_process');
9429
- try {
9430
- if (process.platform === 'win32') {
9431
- exec(`start "" "http://localhost:${PORT}"`);
9432
- } else if (process.platform === 'darwin') {
9433
- exec(`open http://localhost:${PORT}`);
9434
- } else {
9435
- exec(`xdg-open http://localhost:${PORT}`);
9436
- }
9437
- } catch (e) {
9438
- console.log(` Could not auto-open browser: ${e.message}`);
9428
+ const result = shared.openUrlInBrowser(`http://localhost:${PORT}`);
9429
+ if (!result.ok) {
9430
+ console.log(` Could not auto-open browser: ${result.error}`);
9439
9431
  console.log(` Please open http://localhost:${PORT} manually.`);
9440
9432
  }
9441
9433
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-09T22:56:52.587Z"
4
+ "cachedAt": "2026-05-09T23:11:31.200Z"
5
5
  }
package/engine/shared.js CHANGED
@@ -251,6 +251,21 @@ function resolveEngineCacheDir(fallbackEngineDir) {
251
251
  return fallbackEngineDir;
252
252
  }
253
253
 
254
+ // Cross-platform URL opener. Uses execSync so failures fall through the
255
+ // try/catch and the caller sees them. Dashboard self-open and `minions dash`
256
+ // / `minions restart` post-health open all funnel through here.
257
+ function openUrlInBrowser(url) {
258
+ const { execSync } = require('child_process');
259
+ try {
260
+ if (process.platform === 'win32') execSync(`start "" "${url}"`, { stdio: 'ignore', windowsHide: true });
261
+ else if (process.platform === 'darwin') execSync(`open "${url}"`, { stdio: 'ignore' });
262
+ else execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
263
+ return { ok: true };
264
+ } catch (e) {
265
+ return { ok: false, error: e && e.message ? e.message : String(e) };
266
+ }
267
+ }
268
+
254
269
  function _flushLogBuffer() {
255
270
  if (_logBuffer.length === 0) return;
256
271
  const drained = _logBuffer.splice(0);
@@ -3380,6 +3395,7 @@ module.exports = {
3380
3395
  MINIONS_DIR,
3381
3396
  ENGINE_DIR,
3382
3397
  resolveEngineCacheDir,
3398
+ openUrlInBrowser,
3383
3399
  CONTROL_PATH,
3384
3400
  COOLDOWNS_PATH,
3385
3401
  PR_LINKS_PATH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1826",
3
+ "version": "0.1.1828",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"