@yemi33/minions 0.1.1781 → 0.1.1782

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1782 (2026-05-07)
4
+
5
+ ### Other
6
+ - refactor(worktree): consolidate porcelain parsers behind shared helper
7
+
3
8
  ## 0.1.1781 (2026-05-07)
4
9
 
5
10
  ### Fixes
package/engine/cleanup.js CHANGED
@@ -68,11 +68,13 @@ function isProtectedLocalBranch(branch, project = {}) {
68
68
 
69
69
  function localBranchWorktreeInUse(root, branch) {
70
70
  try {
71
- const out = String(shared.execSilent('git worktree list --porcelain', {
71
+ const raw = String(shared.execSilent('git worktree list --porcelain', {
72
72
  cwd: root, encoding: 'utf8', stdio: 'pipe', timeout: 10000, windowsHide: true,
73
73
  }) || '');
74
- return out.split(/\r?\n/).some(line => line.trim() === `branch refs/heads/${branch}`);
74
+ return shared.parseWorktreePorcelain(raw).some(w => w.branch === branch);
75
75
  } catch {
76
+ // Defensive: assume "in use" so the caller doesn't proceed to delete a
77
+ // branch when worktree state can't be enumerated.
76
78
  return true;
77
79
  }
78
80
  }
@@ -386,17 +388,16 @@ async function runCleanup(config, verbose = false) {
386
388
  log('warn', `Project "${project.name || root}" has localPath "${root}" which does not exist on disk — skipping worktree scan`);
387
389
  continue;
388
390
  }
389
- let raw;
391
+ let trees;
390
392
  try {
391
- raw = String(shared.execSilent('git worktree list --porcelain', { cwd: root, timeout: 10000, windowsHide: true }) || '');
393
+ const raw = String(shared.execSilent('git worktree list --porcelain', { cwd: root, timeout: 10000, windowsHide: true }) || '');
394
+ trees = shared.parseWorktreePorcelain(raw);
392
395
  } catch (e) { log('warn', `nested-worktree scan for ${project.name || root}: ${e.message}`); continue; }
393
- for (const line of raw.split('\n')) {
394
- if (!line.startsWith('worktree ')) continue;
395
- const wt = line.slice('worktree '.length).trim();
396
- if (!wt) continue;
397
- if (!shared.isPathInside(wt, root)) continue; // strict — main worktree (equal path) is expected, descendants are the leak
396
+ for (const wt of trees) {
397
+ // Strict — main worktree (equal path) is expected, descendants are the leak.
398
+ if (!shared.isPathInside(wt.path, root)) continue;
398
399
  cleaned.nestedWorktrees++;
399
- log('warn', `Nested worktree in project "${project.name || root}": "${wt}" is inside "${root}". This causes glob tools to match both copies and produces mirror writes. Run: git worktree remove "${wt}"`);
400
+ log('warn', `Nested worktree in project "${project.name || root}": "${wt.path}" is inside "${root}". This causes glob tools to match both copies and produces mirror writes. Run: git worktree remove "${wt.path}"`);
400
401
  }
401
402
  }
402
403
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-07T23:33:01.432Z"
4
+ "cachedAt": "2026-05-07T23:40:59.915Z"
5
5
  }
package/engine/shared.js CHANGED
@@ -2177,6 +2177,42 @@ function isPathInsideOrEqual(childPath, parentPath) {
2177
2177
  return isPathInside(childAbs, parentAbs);
2178
2178
  }
2179
2179
 
2180
+ /**
2181
+ * Parse `git worktree list --porcelain` output. Pure — callers run the
2182
+ * subprocess (sync `execSilent` or async `execAsync`) and feed stdout in.
2183
+ *
2184
+ * Returns `[{path, head, branch, bare, detached, locked, prunable}]`.
2185
+ * `branch` strips the `refs/heads/` prefix; flag fields default to false.
2186
+ *
2187
+ * Porcelain format: each worktree is a block of `key value\n` lines
2188
+ * terminated by a blank line. First line of every block is `worktree <path>`
2189
+ * (always present). Optional follow-ups: `HEAD <sha>`, `branch refs/heads/<name>`,
2190
+ * and the flag lines `bare`, `detached`, `locked [<reason>]`, `prunable [<reason>]`.
2191
+ */
2192
+ function parseWorktreePorcelain(raw) {
2193
+ const trees = [];
2194
+ if (!raw) return trees;
2195
+ let current = null;
2196
+ const flush = () => { if (current) { trees.push(current); current = null; } };
2197
+ for (const line of String(raw).split(/\r?\n/)) {
2198
+ if (line === '') { flush(); continue; }
2199
+ if (line.startsWith('worktree ')) {
2200
+ flush();
2201
+ current = { path: line.slice('worktree '.length).trim(), head: '', branch: '', bare: false, detached: false, locked: false, prunable: false };
2202
+ continue;
2203
+ }
2204
+ if (!current) continue;
2205
+ if (line.startsWith('HEAD ')) current.head = line.slice('HEAD '.length).trim();
2206
+ else if (line.startsWith('branch ')) current.branch = line.slice('branch '.length).trim().replace(/^refs\/heads\//, '');
2207
+ else if (line === 'bare') current.bare = true;
2208
+ else if (line === 'detached') current.detached = true;
2209
+ else if (line === 'locked' || line.startsWith('locked ')) current.locked = true;
2210
+ else if (line === 'prunable' || line.startsWith('prunable ')) current.prunable = true;
2211
+ }
2212
+ flush();
2213
+ return trees;
2214
+ }
2215
+
2180
2216
  /**
2181
2217
  * Throws when `worktreePath` would land inside (or equal) `projectRoot`.
2182
2218
  * Called by the engine spawn path before `git worktree add`, and by the
@@ -3269,6 +3305,7 @@ module.exports = {
3269
3305
  buildWorktreeDirName, // exported for testing
3270
3306
  isPathInside,
3271
3307
  isPathInsideOrEqual,
3308
+ parseWorktreePorcelain,
3272
3309
  assertWorktreeOutsideProject,
3273
3310
  isLiveCommandCenterPath,
3274
3311
  describeCcProtectedPaths,
package/engine.js CHANGED
@@ -474,20 +474,8 @@ async function syncReusedWorktree(rootDir, worktreePath, branchName, gitOpts = {
474
474
  async function findExistingWorktree(repoDir, branchName) {
475
475
  try {
476
476
  const out = await execAsync(`git worktree list --porcelain`, { cwd: repoDir, timeout: 10000 });
477
- const branchRef = `branch refs/heads/${branchName}`;
478
- const lines = out.split('\n');
479
- for (let i = 0; i < lines.length; i++) {
480
- if (lines[i].trim() === branchRef) {
481
- // Walk back to find the worktree path
482
- for (let j = i - 1; j >= 0; j--) {
483
- if (lines[j].startsWith('worktree ')) {
484
- const wtPath = lines[j].slice('worktree '.length).trim();
485
- if (fs.existsSync(wtPath)) return wtPath;
486
- break;
487
- }
488
- }
489
- }
490
- }
477
+ const found = shared.parseWorktreePorcelain(out).find(w => w.branch === branchName);
478
+ if (found && fs.existsSync(found.path)) return found.path;
491
479
  } catch (e) { log('warn', 'git: ' + e.message); }
492
480
  return null;
493
481
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1781",
3
+ "version": "0.1.1782",
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"