@yemi33/minions 0.1.1825 → 0.1.1827

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.1827 (2026-05-09)
4
+
5
+ ### Other
6
+ - refactor(cli): consolidate browser-open helpers and tighten restart flow
7
+
8
+ ## 0.1.1826 (2026-05-09)
9
+
10
+ ### Fixes
11
+ - always open dashboard if no live tab reconnects after restart
12
+
3
13
  ## 0.1.1825 (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,16 +74,37 @@ function killByPort(port) {
74
74
 
75
75
  const isPortListening = (port) => getListeningPids(port).length > 0;
76
76
 
77
- function hasRecentDashboardBrowserTab(minionsHome, now = Date.now()) {
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.
81
+ function _clearDashboardBrowserState(minionsHome) {
78
82
  try {
79
83
  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; }
84
+ fs.writeFileSync(fp, JSON.stringify({ tabs: {}, updatedAt: new Date().toISOString() }));
85
+ } catch {}
86
+ }
87
+
88
+ async function _waitForBrowserReconnect(minionsHome, { afterMs, timeoutMs = 5000, pollMs = 500 } = {}) {
89
+ const start = Date.now();
90
+ while (Date.now() - start < timeoutMs) {
91
+ try {
92
+ const fp = path.join(minionsHome, 'engine', 'dashboard-browser.json');
93
+ const state = JSON.parse(fs.readFileSync(fp, 'utf8'));
94
+ const tabs = state && state.tabs ? Object.values(state.tabs) : [];
95
+ if (tabs.some(t => Number(t && t.lastSeen) > afterMs)) return true;
96
+ } catch { /* file may be temporarily missing during write */ }
97
+ await new Promise(r => setTimeout(r, pollMs));
98
+ }
99
+ return false;
100
+ }
101
+
102
+ function _openInBrowser(url) {
103
+ const result = openUrlInBrowser(url);
104
+ if (!result.ok) {
105
+ console.log(` Could not auto-open browser: ${result.error}`);
106
+ console.log(` Please open ${url} manually.`);
107
+ }
87
108
  }
88
109
 
89
110
  /**
@@ -143,12 +164,10 @@ function killMinionsProcesses(patterns) {
143
164
  } catch {}
144
165
  }
145
166
 
146
- /** Spawn a detached dashboard. When `suppressOpen` is true, the new dashboard
147
- * skips its auto-open of the browser the existing tab will reconnect. */
148
- function spawnDashboard(suppressOpen) {
149
- const env = { ...process.env };
150
- if (suppressOpen) env.MINIONS_NO_AUTO_OPEN = '1';
151
- else delete env.MINIONS_NO_AUTO_OPEN;
167
+ /** Spawn a detached dashboard with self-open suppressed the CLI decides
168
+ * when to open a browser based on whether a real tab reconnects post-health. */
169
+ function spawnDashboard() {
170
+ const env = { ...process.env, MINIONS_NO_AUTO_OPEN: '1' };
152
171
  const proc = spawn(process.execPath, [path.join(MINIONS_HOME, 'dashboard.js')], {
153
172
  cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true, env
154
173
  });
@@ -411,16 +430,15 @@ function init() {
411
430
  if (isUpgrade && skipStart) return;
412
431
 
413
432
  // Auto-start on fresh install; direct force-upgrade restarts automatically.
414
- // Probe before kill so we suppress browser auto-open only when a browser tab
415
- // was recently polling the dashboard. A bare/orphan dashboard process on the
416
- // port is not enough; cold starts should still open the UI.
417
433
  const dashWasUp = isPortListening(DASH_PORT);
418
- const suppressDashboardOpen = !forceOpen && dashWasUp && hasRecentDashboardBrowserTab(MINIONS_HOME);
434
+ const restartStartMs = Date.now();
419
435
  if (isUpgrade) {
420
436
  try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
421
437
  // Free the dashboard port too — without this the new dashboard EADDRINUSE-dies
422
438
  // silently and the user keeps running stale code from the old dashboard process.
423
439
  killByPort(DASH_PORT);
440
+ // Clear AFTER kill so the old dashboard can't repopulate during shutdown.
441
+ _clearDashboardBrowserState(MINIONS_HOME);
424
442
  }
425
443
  console.log(isUpgrade
426
444
  ? `\n Upgrade complete (${pkgVersion}). Restarting engine and dashboard...\n`
@@ -431,10 +449,21 @@ function init() {
431
449
  engineProc.unref();
432
450
  console.log(` Engine started (PID: ${engineProc.pid})`);
433
451
 
434
- const dashProc = spawnDashboard(suppressDashboardOpen);
452
+ const dashProc = spawnDashboard();
435
453
  console.log(` Dashboard started (PID: ${dashProc.pid})`);
436
454
  console.log(` Dashboard: http://localhost:${DASH_PORT}`);
437
455
 
456
+ void (async () => {
457
+ const shouldOpen = forceOpen || !dashWasUp ||
458
+ !(await _waitForBrowserReconnect(MINIONS_HOME, { afterMs: restartStartMs, timeoutMs: 5000 }));
459
+ if (shouldOpen) {
460
+ console.log(` Opening dashboard in browser...`);
461
+ _openInBrowser(`http://localhost:${DASH_PORT}`);
462
+ }
463
+ })().catch(err => {
464
+ console.log(` Could not open dashboard: ${err.message}`);
465
+ });
466
+
438
467
  // Next steps guidance
439
468
  console.log(`
440
469
  Next steps:
@@ -685,34 +714,30 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
685
714
  } else if (cmd === 'add' || cmd === 'remove' || cmd === 'list' || cmd === 'scan') {
686
715
  delegate('minions.js', [cmd, ...rest]);
687
716
  } else if (cmd === 'restart') {
688
- // Start both engine and dashboard — the go-to command after a reboot.
689
717
  // `--cli` / `--model` flags forward to `engine.js start` so the runtime
690
718
  // fleet flips before the daemon spawns (P-6b3f9c2e AC: works on restart).
691
719
  ensureInstalled();
692
- // Probe before kill so we suppress browser auto-open only when a browser tab
693
- // was recently polling the dashboard. A bare/orphan dashboard process on the
694
- // port is not enough; cold starts should still open the UI.
695
720
  const dashWasUp = isPortListening(DASH_PORT);
696
- const suppressDashboardOpen = !forceOpen && dashWasUp && hasRecentDashboardBrowserTab(MINIONS_HOME);
697
- // Layered kill — each step is best-effort, layered so the next still runs if
698
- // one fails. Goal: the old engine is gone before we spawn a new one, even if
721
+ const restartStartMs = Date.now();
722
+ // Layered kill — each step is best-effort, so the next still runs if one
723
+ // fails. Goal: the old engine is gone before we spawn a new one, even if
699
724
  // PowerShell is unavailable, the engine is hung, or its cmdline doesn't match.
700
725
  const oldEnginePid = readEnginePid(MINIONS_HOME);
701
- // 1. Graceful stop — short timeout so a hung engine can't block what follows.
702
726
  try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
703
- // 2. Force-kill the recorded engine PID (NOT the tree — agent children must
704
- // survive so the new engine can re-attach them via PID files).
727
+ // Force-kill the recorded engine PID (NOT the tree — agent children must
728
+ // survive so the new engine can re-attach them via PID files).
705
729
  killPidOnly(oldEnginePid);
706
- // 3. Free dashboard port (catches orphan dashboards with no recorded PID).
707
730
  killByPort(DASH_PORT);
708
- // 4. Belt-and-suspenders cmdline match for anything still alive.
709
731
  killMinionsProcesses(['engine.js', 'dashboard.js']);
732
+ // Clear stale beacons AFTER the kill so the old dashboard's last writes
733
+ // can't repopulate the file in the gap between clear and shutdown.
734
+ _clearDashboardBrowserState(MINIONS_HOME);
710
735
  const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
711
736
  cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
712
737
  });
713
738
  engineProc.unref();
714
739
  console.log(`\n Engine started (PID: ${engineProc.pid})`);
715
- const dashProc = spawnDashboard(suppressDashboardOpen);
740
+ const dashProc = spawnDashboard();
716
741
  console.log(` Dashboard started (PID: ${dashProc.pid})`);
717
742
  console.log(` Dashboard: http://localhost:${DASH_PORT}`);
718
743
  console.log(' Verifying restart health...');
@@ -725,7 +750,15 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
725
750
  console.error(formatRestartHealthError(result));
726
751
  process.exit(1);
727
752
  }
728
- console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.\n`);
753
+ console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.`);
754
+
755
+ const shouldOpen = forceOpen || !dashWasUp ||
756
+ !(await _waitForBrowserReconnect(MINIONS_HOME, { afterMs: restartStartMs, timeoutMs: 5000 }));
757
+ if (shouldOpen) {
758
+ console.log(` Opening dashboard in browser...`);
759
+ _openInBrowser(`http://localhost:${DASH_PORT}`);
760
+ }
761
+ console.log('');
729
762
  })().catch(err => {
730
763
  console.error(`\n ERROR: Restart verification failed: ${err.message}\n`);
731
764
  process.exit(1);
@@ -897,10 +930,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
897
930
  handled = true;
898
931
  const url = `http://localhost:${DASH_PORT}`;
899
932
  console.log(`\n Dashboard already running: ${url}\n`);
900
- try {
901
- const openCmd = process.platform === 'win32' ? `start ${url}` : process.platform === 'darwin' ? `open ${url}` : `xdg-open ${url}`;
902
- execSync(openCmd, { stdio: 'ignore', windowsHide: true });
903
- } catch {}
933
+ openUrlInBrowser(url);
904
934
  });
905
935
  sock.on('error', () => {
906
936
  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-09T20:04:56.707Z"
4
+ "cachedAt": "2026-05-09T23:03:46.416Z"
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.1825",
3
+ "version": "0.1.1827",
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"