@yemi33/minions 0.1.2384 → 0.1.2386

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.
Files changed (3) hide show
  1. package/dashboard.js +122 -39
  2. package/minions.js +5 -3
  3. package/package.json +1 -1
package/dashboard.js CHANGED
@@ -1965,51 +1965,125 @@ function getEngineState() { return queries.getControl(); }
1965
1965
 
1966
1966
  let _worktreeCountCache = 0;
1967
1967
  let _worktreeCountCacheTs = 0;
1968
+ let _worktreeCountRefreshPromise = null;
1969
+ const WORKTREE_COUNT_SCAN_CONCURRENCY = 8;
1968
1970
 
1969
- function _countWorktrees() {
1970
- const now = Date.now();
1971
- if (_worktreeCountCacheTs && (now - _worktreeCountCacheTs) < shared.ENGINE_DEFAULTS.worktreeCountCacheTtl) {
1972
- return _worktreeCountCache;
1971
+ function _isMissingWorktreePathError(error) {
1972
+ return !!(error && (error.code === 'ENOENT' || error.code === 'ENOTDIR'));
1973
+ }
1974
+
1975
+ async function _readWorktreeDirectory(dirPath) {
1976
+ try {
1977
+ return await fs.promises.readdir(dirPath, { withFileTypes: true });
1978
+ } catch (error) {
1979
+ if (_isMissingWorktreePathError(error)) return [];
1980
+ throw error;
1973
1981
  }
1982
+ }
1983
+
1984
+ async function _hasGitEntry(dirPath) {
1974
1985
  try {
1975
- const config = queries.getConfig();
1976
- const projects = shared.getProjects(config);
1977
- const worktreeRoots = new Map();
1978
- for (const p of projects) {
1979
- if (!p.localPath) continue;
1980
- const wtRoot = path.resolve(
1981
- p.localPath,
1982
- config.engine?.worktreeRoot || shared.ENGINE_DEFAULTS.worktreeRoot,
1983
- );
1984
- const rootKey = shared._normalizeWorktreePath(wtRoot);
1985
- if (!worktreeRoots.has(rootKey)) worktreeRoots.set(rootKey, wtRoot);
1986
+ await fs.promises.access(path.join(dirPath, '.git'), fs.constants.F_OK);
1987
+ return true;
1988
+ } catch (error) {
1989
+ if (_isMissingWorktreePathError(error)) return false;
1990
+ throw error;
1991
+ }
1992
+ }
1993
+
1994
+ async function _mapWorktreeEntriesBounded(entries, worker) {
1995
+ if (entries.length === 0) return [];
1996
+ const results = new Array(entries.length);
1997
+ let nextIndex = 0;
1998
+
1999
+ async function runWorker() {
2000
+ while (nextIndex < entries.length) {
2001
+ const index = nextIndex++;
2002
+ results[index] = await worker(entries[index]);
1986
2003
  }
2004
+ }
1987
2005
 
1988
- let count = 0;
1989
- for (const wtRoot of worktreeRoots.values()) {
1990
- try {
1991
- for (const dir of fs.readdirSync(wtRoot)) {
1992
- const dirPath = path.join(wtRoot, dir);
1993
- try { if (!fs.statSync(dirPath).isDirectory()) continue; } catch { continue; }
1994
- // A git worktree has a .git file (not directory) in its root
1995
- if (fs.existsSync(path.join(dirPath, '.git'))) {
1996
- count++;
1997
- } else {
1998
- // Parent directory scan subdirs for nested worktrees
1999
- try {
2000
- for (const sub of fs.readdirSync(dirPath)) {
2001
- const subPath = path.join(dirPath, sub);
2002
- try { if (fs.statSync(subPath).isDirectory() && fs.existsSync(path.join(subPath, '.git'))) count++; } catch {}
2003
- }
2004
- } catch {}
2005
- }
2006
- }
2007
- } catch {}
2006
+ const workerCount = Math.min(WORKTREE_COUNT_SCAN_CONCURRENCY, entries.length);
2007
+ await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
2008
+ return results;
2009
+ }
2010
+
2011
+ async function _countWorktreesInRoot(worktreeRoot) {
2012
+ const entries = await _readWorktreeDirectory(worktreeRoot);
2013
+ const directories = entries.filter(entry => entry.isDirectory());
2014
+ const inspected = await _mapWorktreeEntriesBounded(directories, async entry => {
2015
+ const dirPath = path.join(worktreeRoot, entry.name);
2016
+ if (await _hasGitEntry(dirPath)) return { direct: 1, nested: [] };
2017
+
2018
+ const nestedEntries = await _readWorktreeDirectory(dirPath);
2019
+ return {
2020
+ direct: 0,
2021
+ nested: nestedEntries
2022
+ .filter(nestedEntry => nestedEntry.isDirectory())
2023
+ .map(nestedEntry => path.join(dirPath, nestedEntry.name)),
2024
+ };
2025
+ });
2026
+ const directCount = inspected.reduce((total, result) => total + result.direct, 0);
2027
+ const nestedPaths = inspected.flatMap(result => result.nested);
2028
+ const nestedMatches = await _mapWorktreeEntriesBounded(nestedPaths, _hasGitEntry);
2029
+ return directCount + nestedMatches.filter(Boolean).length;
2030
+ }
2031
+
2032
+ async function _scanWorktreeCount() {
2033
+ const config = queries.getConfig();
2034
+ const worktreeRoots = new Map();
2035
+ for (const project of shared.getProjects(config)) {
2036
+ if (!project.localPath) continue;
2037
+ const worktreeRoot = path.resolve(
2038
+ project.localPath,
2039
+ config.engine?.worktreeRoot || shared.ENGINE_DEFAULTS.worktreeRoot,
2040
+ );
2041
+ const rootKey = shared._normalizeWorktreePath(worktreeRoot);
2042
+ if (!worktreeRoots.has(rootKey)) worktreeRoots.set(rootKey, worktreeRoot);
2043
+ }
2044
+
2045
+ let count = 0;
2046
+ for (const worktreeRoot of worktreeRoots.values()) {
2047
+ count += await _countWorktreesInRoot(worktreeRoot);
2048
+ }
2049
+ return count;
2050
+ }
2051
+
2052
+ function _refreshWorktreeCount() {
2053
+ if (_worktreeCountRefreshPromise) return _worktreeCountRefreshPromise;
2054
+
2055
+ _worktreeCountRefreshPromise = (async () => {
2056
+ try {
2057
+ const count = await _scanWorktreeCount();
2058
+ const changed = count !== _worktreeCountCache;
2059
+ _worktreeCountCache = count;
2060
+ _worktreeCountCacheTs = Date.now();
2061
+ // A cold outer rebuild may already have captured the previous count.
2062
+ if (changed && (_statusCache || _statusRebuildPromise)) invalidateStatusCache();
2063
+ return count;
2064
+ } catch (error) {
2065
+ _worktreeCountCacheTs = Date.now();
2066
+ console.warn(`[dashboard] worktree count refresh failed: ${error.message}`);
2067
+ return _worktreeCountCache;
2068
+ } finally {
2069
+ _worktreeCountRefreshPromise = null;
2008
2070
  }
2009
- _worktreeCountCache = count;
2010
- _worktreeCountCacheTs = now;
2011
- return count;
2012
- } catch { return 0; }
2071
+ })();
2072
+ return _worktreeCountRefreshPromise;
2073
+ }
2074
+
2075
+ function _countWorktrees() {
2076
+ const now = Date.now();
2077
+ if (!_worktreeCountCacheTs || (now - _worktreeCountCacheTs) >= shared.ENGINE_DEFAULTS.worktreeCountCacheTtl) {
2078
+ _refreshWorktreeCount();
2079
+ }
2080
+ return _worktreeCountCache;
2081
+ }
2082
+
2083
+ function _resetWorktreeCountCacheForTesting() {
2084
+ _worktreeCountCache = 0;
2085
+ _worktreeCountCacheTs = 0;
2086
+ _worktreeCountRefreshPromise = null;
2013
2087
  }
2014
2088
 
2015
2089
  // ── npm update check ────────────────────────────────────────────────────────
@@ -14770,6 +14844,7 @@ module.exports = {
14770
14844
  _setStatusRefreshHook,
14771
14845
  _resetStatusCacheForTesting,
14772
14846
  _ifNoneMatchHasEtag,
14847
+ _countWorktrees, _refreshWorktreeCount, _scanWorktreeCount, _resetWorktreeCountCacheForTesting, // exported for testing
14773
14848
  // W-status-swr — exported for direct unit tests of the subprocess-free git
14774
14849
  // HEAD parser and the prefix-tolerant short-SHA comparison. No production
14775
14850
  // caller imports these; they are test seams.
@@ -14833,6 +14908,14 @@ if (require.main === module) {
14833
14908
  // block boot.
14834
14909
  (async () => {
14835
14910
  const _warmStartedAt = Date.now();
14911
+ // This reporting-only scan must never delay port binding.
14912
+ _refreshWorktreeCount()
14913
+ .then(worktreeCount => {
14914
+ console.log(`[boot] pre-warmed worktree count (${worktreeCount}) off the request path`);
14915
+ })
14916
+ .catch(error => {
14917
+ console.warn(`[boot] worktree count pre-warm failed: ${error && error.message}`);
14918
+ });
14836
14919
  try {
14837
14920
  await warmProjectGitStatusCache();
14838
14921
  const ms = Date.now() - _warmStartedAt;
package/minions.js CHANGED
@@ -423,11 +423,13 @@ async function initMinions({ skipScan = false, scanRoot, scanDepth } = {}) {
423
423
  config.engine.defaultCli = detected[0];
424
424
  console.log(`\n ✓ Detected ${detected[0]} CLI — set as fleet default runtime`);
425
425
  } else if (detected.length > 1) {
426
- // Both available prefer claude (the historical default and broader skill coverage)
427
- config.engine.defaultCli = detected.includes('claude') ? 'claude' : detected[0];
426
+ // Prefer the canonical fleet default when it is installed.
427
+ config.engine.defaultCli = detected.includes(ENGINE_DEFAULTS.defaultCli)
428
+ ? ENGINE_DEFAULTS.defaultCli
429
+ : detected[0];
428
430
  console.log(`\n ✓ Detected ${detected.join(' + ')} — fleet default set to ${config.engine.defaultCli}`);
429
431
  }
430
- // If nothing detected, leave defaultCli unset (engine falls back to 'claude')
432
+ // If nothing is detected, leave defaultCli unset so the engine uses its canonical default.
431
433
  }
432
434
  saveConfig(config);
433
435
  console.log(`\n Minions initialized at ${MINIONS_HOME}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2384",
3
+ "version": "0.1.2386",
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"