@mutmutco/cli 3.64.0 → 3.65.0
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/dist/main.cjs +106 -40
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5165,16 +5165,20 @@ function validateDeadWorktreeDirContent(planned, inspected) {
|
|
|
5165
5165
|
return { ok: false, reason: "unknown dead worktree cleanup kind" };
|
|
5166
5166
|
}
|
|
5167
5167
|
function validateBranchCleanupHead(planned, currentHeads) {
|
|
5168
|
+
const currentHeadOid = currentHeads.find((h) => h.branch === planned.branch)?.oid;
|
|
5169
|
+
if (planned.containedInBase && planned.plannedHeadOid) {
|
|
5170
|
+
if (!currentHeadOid) return { ok: false, reason: "local branch head could not be read" };
|
|
5171
|
+
return currentHeadOid === planned.plannedHeadOid ? { ok: true } : { ok: false, reason: `branch moved since planning (${currentHeadOid} != ${planned.plannedHeadOid})` };
|
|
5172
|
+
}
|
|
5168
5173
|
const reviewedHeadOids = planned.reviewedHeadOids?.filter(Boolean) ?? [];
|
|
5169
5174
|
if (!reviewedHeadOids.length) {
|
|
5170
5175
|
return { ok: false, reason: "planned branch head was not verified against a PR head" };
|
|
5171
5176
|
}
|
|
5172
|
-
|
|
5173
|
-
if (!currentHead) {
|
|
5177
|
+
if (!currentHeadOid) {
|
|
5174
5178
|
return { ok: false, reason: "local branch head could not be verified against the PR head" };
|
|
5175
5179
|
}
|
|
5176
|
-
if (!reviewedHeadOids.includes(
|
|
5177
|
-
return { ok: false, reason: `${
|
|
5180
|
+
if (!reviewedHeadOids.includes(currentHeadOid)) {
|
|
5181
|
+
return { ok: false, reason: `${currentHeadOid} != ${reviewedHeadOids.join("|")}` };
|
|
5178
5182
|
}
|
|
5179
5183
|
return { ok: true };
|
|
5180
5184
|
}
|
|
@@ -5274,6 +5278,7 @@ function buildGcPlan(inputs) {
|
|
|
5274
5278
|
const unpushedWorktrees = new Set((inputs.worktrees ?? []).filter((w) => w.unpushed).map((w) => w.branch));
|
|
5275
5279
|
const preservedBranches2 = new Set(inputs.preservedBranches ?? []);
|
|
5276
5280
|
const preservedWorktrees = new Set((inputs.worktrees ?? []).filter((w) => w.preserved).map((w) => w.branch));
|
|
5281
|
+
const mergedIntoBase = new Set((inputs.mergedIntoBase ?? []).map((b) => b.trim()).filter(Boolean));
|
|
5277
5282
|
const skipped = [];
|
|
5278
5283
|
const branches = [];
|
|
5279
5284
|
const skipTrackingBranches = /* @__PURE__ */ new Set();
|
|
@@ -5313,7 +5318,8 @@ function buildGcPlan(inputs) {
|
|
|
5313
5318
|
continue;
|
|
5314
5319
|
}
|
|
5315
5320
|
const localHead = branchHeads?.get(branch);
|
|
5316
|
-
|
|
5321
|
+
const containedInBase = mergedIntoBase.has(branch);
|
|
5322
|
+
if (!containedInBase && (worktree2?.unpushed || branchHeads && (!localHead || !state.headOids.length || !state.headOids.includes(localHead)))) {
|
|
5317
5323
|
const detail = worktree2?.unpushed ? worktree2.path : localHead && state.headOids.length ? `${localHead} != ${state.headOids.join("|")}` : "local branch head could not be verified against the PR head";
|
|
5318
5324
|
skipped.push({ branch, reason: "unpushed-branch", detail });
|
|
5319
5325
|
skipTrackingBranches.add(branch);
|
|
@@ -5324,7 +5330,8 @@ function buildGcPlan(inputs) {
|
|
|
5324
5330
|
prState: state.state,
|
|
5325
5331
|
prNumbers: state.numbers,
|
|
5326
5332
|
worktreePath: worktree2?.path,
|
|
5327
|
-
...state.headOids.length ? { reviewedHeadOids: state.headOids } : {}
|
|
5333
|
+
...state.headOids.length ? { reviewedHeadOids: state.headOids } : {},
|
|
5334
|
+
...containedInBase ? { containedInBase: true, ...localHead ? { plannedHeadOid: localHead } : {} } : {}
|
|
5328
5335
|
});
|
|
5329
5336
|
}
|
|
5330
5337
|
const trackingRefs = [...new Set(inputs.staleTrackingRefs ?? [])].map((ref) => {
|
|
@@ -10811,7 +10818,7 @@ function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
|
|
|
10811
10818
|
return explicitRepoWorktreesRoot(explicitRoot, repoRoot2, rootDirs);
|
|
10812
10819
|
}
|
|
10813
10820
|
async function gcPlan(remote, limit, opts = {}) {
|
|
10814
|
-
const [branches, heads, current, stale, prs, worktrees, siblingDirs, preserved] = await Promise.all([
|
|
10821
|
+
const [branches, heads, current, stale, prs, worktrees, siblingDirs, preserved, mergedIntoBase] = await Promise.all([
|
|
10815
10822
|
gitOut(["branch", "--format=%(refname:short)"]),
|
|
10816
10823
|
localBranchHeads(),
|
|
10817
10824
|
gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
@@ -10821,7 +10828,8 @@ async function gcPlan(remote, limit, opts = {}) {
|
|
|
10821
10828
|
ghPrs(limit),
|
|
10822
10829
|
worktreeBranches(),
|
|
10823
10830
|
siblingWorktreeDirs(opts.root),
|
|
10824
|
-
preservedBranches()
|
|
10831
|
+
preservedBranches(),
|
|
10832
|
+
branchesMergedIntoBase(remote)
|
|
10825
10833
|
]);
|
|
10826
10834
|
return buildGcPlan({
|
|
10827
10835
|
localBranches: branches.split(/\r?\n/).map((b) => b.trim()).filter(Boolean),
|
|
@@ -10832,9 +10840,23 @@ async function gcPlan(remote, limit, opts = {}) {
|
|
|
10832
10840
|
pullRequests: prs,
|
|
10833
10841
|
worktrees,
|
|
10834
10842
|
remote,
|
|
10835
|
-
preservedBranches: preserved
|
|
10843
|
+
preservedBranches: preserved,
|
|
10844
|
+
mergedIntoBase
|
|
10836
10845
|
});
|
|
10837
10846
|
}
|
|
10847
|
+
async function branchesMergedIntoBase(remote) {
|
|
10848
|
+
const out = /* @__PURE__ */ new Set();
|
|
10849
|
+
for (const base of ["development", "main", "master"]) {
|
|
10850
|
+
let listed;
|
|
10851
|
+
try {
|
|
10852
|
+
listed = await gitOut(["branch", "--merged", `${remote}/${base}`, "--format=%(refname:short)"]);
|
|
10853
|
+
} catch {
|
|
10854
|
+
continue;
|
|
10855
|
+
}
|
|
10856
|
+
for (const b of listed.split(/\r?\n/).map((x) => x.trim()).filter(Boolean)) out.add(b);
|
|
10857
|
+
}
|
|
10858
|
+
return [...out];
|
|
10859
|
+
}
|
|
10838
10860
|
var STRAY_ROOT_WALK_BUDGET_MS = 4e3;
|
|
10839
10861
|
function measureWorktreeRoot(root, deadline) {
|
|
10840
10862
|
let dirs = 0;
|
|
@@ -16429,20 +16451,37 @@ function readFileOrNull2(path2) {
|
|
|
16429
16451
|
return null;
|
|
16430
16452
|
}
|
|
16431
16453
|
}
|
|
16432
|
-
function
|
|
16454
|
+
function removedPaths(changed) {
|
|
16455
|
+
return new Set(
|
|
16456
|
+
changed.map((f) => f.status === "D" ? f.path : f.status === "R" || f.status === "C" ? f.from : void 0).filter((p) => typeof p === "string")
|
|
16457
|
+
);
|
|
16458
|
+
}
|
|
16459
|
+
function classify(changed, policy, present = () => false) {
|
|
16433
16460
|
const matchers = (policy.mandatory ?? []).map((m) => ({ ...m, re: globToRegExp(m.glob) }));
|
|
16434
16461
|
const mandatoryHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path)));
|
|
16435
16462
|
const testChanges = changed.filter((f) => isTestPath(f.path));
|
|
16436
16463
|
const addedTests = testChanges.filter((f) => f.status === "A");
|
|
16464
|
+
const removed = removedPaths(changed);
|
|
16465
|
+
const discharged = (m) => (m.satisfiedBy?.length ?? 0) > 0 && m.satisfiedBy.every((p) => present(p) && !removed.has(p));
|
|
16466
|
+
const untestedHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path) && !discharged(m)));
|
|
16437
16467
|
const protectedBy = new Map((policy.protected ?? []).map((p) => [p.path, p.why ?? ""]));
|
|
16438
|
-
|
|
16439
|
-
|
|
16468
|
+
for (const m of policy.mandatory ?? []) {
|
|
16469
|
+
for (const p of m.satisfiedBy ?? []) {
|
|
16470
|
+
if (!protectedBy.has(p)) protectedBy.set(p, `the standing coverage \`${m.glob}\` is discharged by. Removing it silently empties that glob.`);
|
|
16471
|
+
}
|
|
16472
|
+
}
|
|
16473
|
+
const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
|
|
16474
|
+
return { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected };
|
|
16440
16475
|
}
|
|
16441
16476
|
function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs19.existsSync)(path2)) {
|
|
16442
16477
|
return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path17.join)(root, p)));
|
|
16443
16478
|
}
|
|
16444
|
-
function
|
|
16445
|
-
const
|
|
16479
|
+
function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs19.existsSync)(path2)) {
|
|
16480
|
+
const declared = (policy.mandatory ?? []).flatMap((m) => m.satisfiedBy ?? []);
|
|
16481
|
+
return [...new Set(declared)].filter((p) => !exists((0, import_node_path17.join)(root, p)));
|
|
16482
|
+
}
|
|
16483
|
+
function evaluate(changed, policy, present = () => false) {
|
|
16484
|
+
const { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected } = classify(changed, policy, present);
|
|
16446
16485
|
const findings = [];
|
|
16447
16486
|
if (removedProtected.length > 0) {
|
|
16448
16487
|
findings.push({
|
|
@@ -16453,12 +16492,12 @@ function evaluate(changed, policy) {
|
|
|
16453
16492
|
${f.why}`).join("\n") + "\n If you are removing one because it looked unrequired, read the reason above first \u2014 it is\n the answer to exactly that question. If this removal was asked for, add a commit trailer:\n Test-Policy-Override: <reason>"
|
|
16454
16493
|
});
|
|
16455
16494
|
}
|
|
16456
|
-
if (
|
|
16495
|
+
if (untestedHits.length > 0 && testChanges.length === 0) {
|
|
16457
16496
|
findings.push({
|
|
16458
16497
|
kind: "mandatory-zone-untested",
|
|
16459
|
-
paths:
|
|
16460
|
-
detail: `MANDATORY ZONE UNTESTED \u2014 this change touches ${
|
|
16461
|
-
` +
|
|
16498
|
+
paths: untestedHits.map((f) => f.path),
|
|
16499
|
+
detail: `MANDATORY ZONE UNTESTED \u2014 this change touches ${untestedHits.length} path(s) in the mandatory zone but changes no test:
|
|
16500
|
+
` + untestedHits.map((f) => ` ${f.path}`).join("\n") + "\n These are the paths where a silent failure costs real money, real data, a security breach,\n or production. Add or update a test that would fail if the guarded property broke."
|
|
16462
16501
|
});
|
|
16463
16502
|
}
|
|
16464
16503
|
if (policy.declared !== false && addedTests.length > 0 && mandatoryHits.length === 0) {
|
|
@@ -16499,28 +16538,37 @@ function runTestPolicy(root, deps = {}) {
|
|
|
16499
16538
|
const policy = deps.policy ?? loadPolicy(root);
|
|
16500
16539
|
const exists = deps.exists ?? ((path2) => (0, import_node_fs19.existsSync)(path2));
|
|
16501
16540
|
const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
|
|
16502
|
-
const
|
|
16541
|
+
const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
|
|
16542
|
+
const changed = deps.changed ?? (base ? changedFilesSince(base, root) : []);
|
|
16543
|
+
const override = deps.overrideReason !== void 0 ? deps.overrideReason : !deps.changed && base ? OVERRIDE_RE.exec(git(["log", `${base}..HEAD`, "--format=%B"], root))?.[1]?.trim() ?? null : null;
|
|
16544
|
+
if (override) return { ok: true, findings: [], changedCount: changed.length, base, overriddenBy: override, ...counts };
|
|
16545
|
+
const present = (path2) => exists((0, import_node_path17.join)(root, path2));
|
|
16546
|
+
const removedByThisDiff = removedPaths(changed);
|
|
16547
|
+
const staleFindings = [];
|
|
16548
|
+
const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
|
|
16503
16549
|
if (unresolved.length > 0) {
|
|
16504
|
-
|
|
16505
|
-
|
|
16506
|
-
|
|
16507
|
-
|
|
16508
|
-
...counts,
|
|
16509
|
-
findings: [{
|
|
16510
|
-
kind: "stale-protected-entry",
|
|
16511
|
-
paths: unresolved,
|
|
16512
|
-
detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist:
|
|
16550
|
+
staleFindings.push({
|
|
16551
|
+
kind: "stale-protected-entry",
|
|
16552
|
+
paths: unresolved,
|
|
16553
|
+
detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist:
|
|
16513
16554
|
` + unresolved.map((p) => ` ${p}`).join("\n") + "\n An entry naming a missing file reads as protection while protecting nothing.\n Restore the file, or remove its entry."
|
|
16514
|
-
|
|
16515
|
-
|
|
16555
|
+
});
|
|
16556
|
+
}
|
|
16557
|
+
const staleSatisfiers = unresolvedSatisfiers(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
|
|
16558
|
+
if (staleSatisfiers.length > 0) {
|
|
16559
|
+
staleFindings.push({
|
|
16560
|
+
kind: "stale-satisfied-by",
|
|
16561
|
+
paths: staleSatisfiers,
|
|
16562
|
+
detail: `STALE STANDING COVERAGE \u2014 a mandatory glob claims ${staleSatisfiers.length} path(s) as satisfiedBy that do not exist:
|
|
16563
|
+
` + staleSatisfiers.map((p) => ` ${p}`).join("\n") + "\n A glob discharged by coverage that is not there is a glob enforcing nothing, quietly.\n Restore the file, or drop it from satisfiedBy so the glob asks for a test again."
|
|
16564
|
+
});
|
|
16565
|
+
}
|
|
16566
|
+
if (staleFindings.length > 0) {
|
|
16567
|
+
return { ok: false, base, changedCount: changed.length, ...counts, findings: staleFindings };
|
|
16516
16568
|
}
|
|
16517
|
-
const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
|
|
16518
16569
|
if (!deps.changed && !base) return { ok: true, findings: [], changedCount: 0, base: null, ...counts };
|
|
16519
|
-
const changed = deps.changed ?? changedFilesSince(base, root);
|
|
16520
16570
|
if (changed.length === 0) return { ok: true, findings: [], changedCount: 0, base, ...counts };
|
|
16521
|
-
const
|
|
16522
|
-
if (override) return { ok: true, findings: [], changedCount: changed.length, base, overriddenBy: override, ...counts };
|
|
16523
|
-
const findings = evaluate(changed, policy);
|
|
16571
|
+
const findings = evaluate(changed, policy, present);
|
|
16524
16572
|
return { ok: findings.length === 0, findings, changedCount: changed.length, base, ...counts };
|
|
16525
16573
|
}
|
|
16526
16574
|
|
|
@@ -21875,7 +21923,18 @@ function recordGcWorktreeRemoval(primaryRoot, actor, owners, path2, branch) {
|
|
|
21875
21923
|
});
|
|
21876
21924
|
dropWorktreeOwner(primaryRoot, path2);
|
|
21877
21925
|
}
|
|
21878
|
-
function
|
|
21926
|
+
function renderPrLandCleanupLines(cleanup) {
|
|
21927
|
+
const report = cleanup;
|
|
21928
|
+
const wt = report?.worktree;
|
|
21929
|
+
if (!wt?.path) return [];
|
|
21930
|
+
const lines = [];
|
|
21931
|
+
if (wt.status === "removed") lines.push(`pr land: removed worktree ${wt.path} \u2014 that directory is gone; cd elsewhere before your next command`);
|
|
21932
|
+
else if (wt.status === "deferred") lines.push(`pr land: worktree ${wt.path} could not be removed yet and is registered for a later sweep`);
|
|
21933
|
+
else lines.push(`pr land: worktree ${wt.path} \u2014 ${wt.status ?? "unknown"}${wt.reason ? ` (${wt.reason})` : ""}`);
|
|
21934
|
+
if (wt.deferredNote) lines.push(`pr land: ${wt.deferredNote}`);
|
|
21935
|
+
return lines;
|
|
21936
|
+
}
|
|
21937
|
+
function renderGcApplyResult(result, skipped = []) {
|
|
21879
21938
|
const lines = ["worktree gc apply result:"];
|
|
21880
21939
|
lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
|
|
21881
21940
|
lines.push(` remote branches removed: ${result.removedRemoteBranches.length ? result.removedRemoteBranches.join(", ") : "none"}`);
|
|
@@ -21892,9 +21951,15 @@ function renderGcApplyResult(result) {
|
|
|
21892
21951
|
}
|
|
21893
21952
|
const removedNothing = result.removedBranches.length === 0 && result.removedRemoteBranches.length === 0 && result.removedTrackingRefs.length === 0 && result.removedWorktreeDirs.length === 0;
|
|
21894
21953
|
if (removedNothing && !result.refused.length) {
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21954
|
+
const actionable = skipped.filter((s) => s.reason !== "protected" && s.reason !== "current-branch");
|
|
21955
|
+
if (actionable.length) {
|
|
21956
|
+
lines.push(` NOTHING WAS REMOVED \u2014 every candidate was skipped for a stated reason (${actionable.length}):`);
|
|
21957
|
+
for (const s of actionable) lines.push(` - ${s.branch}: ${s.reason}${s.detail ? ` (${s.detail})` : ""}`);
|
|
21958
|
+
} else {
|
|
21959
|
+
lines.push(" NOTHING WAS REMOVED. A stale worktree whose directory still exists is not reachable by");
|
|
21960
|
+
lines.push(" this verb \u2014 remove it from inside it with `mmi-cli worktree land --apply`, or delete the");
|
|
21961
|
+
lines.push(" directory and re-run to clear its registration.");
|
|
21962
|
+
}
|
|
21898
21963
|
}
|
|
21899
21964
|
return lines.join("\n");
|
|
21900
21965
|
}
|
|
@@ -25848,7 +25913,7 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
25848
25913
|
console.log(formatGcPlan(plan, false));
|
|
25849
25914
|
} else {
|
|
25850
25915
|
if (applyResult) console.log(`
|
|
25851
|
-
${renderGcApplyResult(applyResult)}`);
|
|
25916
|
+
${renderGcApplyResult(applyResult, plan.skipped)}`);
|
|
25852
25917
|
}
|
|
25853
25918
|
if (applyResult?.failed.length) process.exitCode = 1;
|
|
25854
25919
|
} catch (e) {
|
|
@@ -27268,6 +27333,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
27268
27333
|
if (o.json) printLine(JSON.stringify(result));
|
|
27269
27334
|
else {
|
|
27270
27335
|
printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
27336
|
+
for (const line of renderPrLandCleanupLines(result.cleanup)) printLine(line);
|
|
27271
27337
|
if (result.cleanupError) printLine(`pr land cleanup: ${result.cleanupError}`);
|
|
27272
27338
|
}
|
|
27273
27339
|
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.65.0",
|
|
4
4
|
"description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|