@mutmutco/cli 3.97.0 → 3.99.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 +1108 -528
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3416,8 +3416,8 @@ var program = new Command();
|
|
|
3416
3416
|
|
|
3417
3417
|
// src/index.ts
|
|
3418
3418
|
var import_promises11 = require("node:fs/promises");
|
|
3419
|
-
var
|
|
3420
|
-
var
|
|
3419
|
+
var import_node_fs40 = require("node:fs");
|
|
3420
|
+
var import_node_child_process19 = require("node:child_process");
|
|
3421
3421
|
|
|
3422
3422
|
// src/cli-shared.ts
|
|
3423
3423
|
var import_node_child_process3 = require("node:child_process");
|
|
@@ -5779,6 +5779,7 @@ function buildGcPlan(inputs) {
|
|
|
5779
5779
|
const preservedBranches2 = new Set(inputs.preservedBranches ?? []);
|
|
5780
5780
|
const preservedWorktrees = new Set((inputs.worktrees ?? []).filter((w) => w.preserved).map((w) => w.branch));
|
|
5781
5781
|
const mergedIntoBase = new Set((inputs.mergedIntoBase ?? []).map((b) => b.trim()).filter(Boolean));
|
|
5782
|
+
const prLookupFailures = new Map((inputs.prLookupFailures ?? []).map((f) => [f.branch.trim(), f.detail]));
|
|
5782
5783
|
const skipped = [];
|
|
5783
5784
|
const branches = [];
|
|
5784
5785
|
const skipTrackingBranches = /* @__PURE__ */ new Set();
|
|
@@ -5794,6 +5795,11 @@ function buildGcPlan(inputs) {
|
|
|
5794
5795
|
skipped.push({ branch, reason: "protected" });
|
|
5795
5796
|
continue;
|
|
5796
5797
|
}
|
|
5798
|
+
if (prLookupFailures.has(branch)) {
|
|
5799
|
+
skipped.push({ branch, reason: "pr-lookup-failed", detail: prLookupFailures.get(branch) });
|
|
5800
|
+
skipTrackingBranches.add(branch);
|
|
5801
|
+
continue;
|
|
5802
|
+
}
|
|
5797
5803
|
const prSet = prs.get(branch);
|
|
5798
5804
|
if (prSet?.some((pr2) => pr2.state === "OPEN")) {
|
|
5799
5805
|
skipped.push({ branch, reason: "open-pr" });
|
|
@@ -5838,6 +5844,7 @@ function buildGcPlan(inputs) {
|
|
|
5838
5844
|
const branch = branchForTrackingRef(ref, remote);
|
|
5839
5845
|
if (!branch || protectedBranches.has(branch)) return null;
|
|
5840
5846
|
if (branch === inputs.currentBranch || dirtyWorktrees.has(branch) || unpushedWorktrees.has(branch) || preservedBranches2.has(branch) || preservedWorktrees.has(branch) || skipTrackingBranches.has(branch)) return null;
|
|
5847
|
+
if (prLookupFailures.has(branch)) return null;
|
|
5841
5848
|
const prSet = prs.get(branch);
|
|
5842
5849
|
if (prSet?.some((pr2) => pr2.state === "OPEN")) return null;
|
|
5843
5850
|
const state = closedState(prSet);
|
|
@@ -6999,7 +7006,7 @@ function commandLadderHint() {
|
|
|
6999
7006
|
}
|
|
7000
7007
|
|
|
7001
7008
|
// src/index.ts
|
|
7002
|
-
var
|
|
7009
|
+
var import_node_path38 = require("node:path");
|
|
7003
7010
|
|
|
7004
7011
|
// src/merge-ci-policy.ts
|
|
7005
7012
|
function resolveMergeCiPolicy(input) {
|
|
@@ -7643,6 +7650,9 @@ var CMD_RE = /`mmi-cli ((?:[a-z][a-z-]*)(?: [a-z][a-z-]*){0,3})/g;
|
|
|
7643
7650
|
var RETIRED_RE = /\b(retired|historical|removed|deleted|gone|no longer|superseded|legacy)\b/i;
|
|
7644
7651
|
var ROOT_DOCS = ["README.md", "architecture.md"];
|
|
7645
7652
|
var SKIP_WALK = ["docs/Archive/", "docs/incidents/", "docs/research/"];
|
|
7653
|
+
function refFirstSegment(ref) {
|
|
7654
|
+
return ref.replace(/^(\.\/)+/, "").split("/")[0];
|
|
7655
|
+
}
|
|
7646
7656
|
function stripFences(markdown) {
|
|
7647
7657
|
let inFence = false;
|
|
7648
7658
|
return markdown.split(/\r?\n/).map((line) => {
|
|
@@ -7745,6 +7755,12 @@ function extractCommands(markdown) {
|
|
|
7745
7755
|
}
|
|
7746
7756
|
function checkRefs(root, deps, docs2) {
|
|
7747
7757
|
const { exists, isIgnored = () => /* @__PURE__ */ new Set() } = deps;
|
|
7758
|
+
const allFirstSegments = /* @__PURE__ */ new Set();
|
|
7759
|
+
for (const markdown of Object.values(docs2)) {
|
|
7760
|
+
for (const { ref } of extractRefs(markdown)) allFirstSegments.add(refFirstSegment(ref));
|
|
7761
|
+
}
|
|
7762
|
+
const tracked = allFirstSegments.size ? deps.trackedFirstSegments?.([...allFirstSegments]) ?? null : null;
|
|
7763
|
+
const firstVerifiable = (first) => tracked ? tracked.has(first) : exists((0, import_node_path12.join)(root, first));
|
|
7748
7764
|
const candidates = [];
|
|
7749
7765
|
const direct = [];
|
|
7750
7766
|
for (const [doc, markdown] of Object.entries(docs2)) {
|
|
@@ -7781,8 +7797,7 @@ function checkRefs(root, deps, docs2) {
|
|
|
7781
7797
|
}
|
|
7782
7798
|
}
|
|
7783
7799
|
for (const { ref, line } of extractRefs(markdown)) {
|
|
7784
|
-
|
|
7785
|
-
if (!exists((0, import_node_path12.join)(root, first))) continue;
|
|
7800
|
+
if (!firstVerifiable(refFirstSegment(ref))) continue;
|
|
7786
7801
|
if (!exists((0, import_node_path12.join)(root, ref))) candidates.push({ kind: "missing-path", doc, line, detail: ref });
|
|
7787
7802
|
}
|
|
7788
7803
|
for (const { target, line, resolved, missing } of links) {
|
|
@@ -7872,18 +7887,43 @@ function defaultIsIgnored(root, relPaths, exec = import_node_child_process5.exec
|
|
|
7872
7887
|
throw error;
|
|
7873
7888
|
}
|
|
7874
7889
|
}
|
|
7890
|
+
function defaultTrackedFirstSegments(root, firstSegments, exec = import_node_child_process5.execFileSync) {
|
|
7891
|
+
if (firstSegments.length === 0) return /* @__PURE__ */ new Set();
|
|
7892
|
+
try {
|
|
7893
|
+
const out = exec("git", ["ls-files", "-z", "--", ...firstSegments.map((s) => `:(literal)${s}`)], {
|
|
7894
|
+
cwd: root,
|
|
7895
|
+
encoding: "utf8",
|
|
7896
|
+
maxBuffer: CHECK_IGNORE_MAX_BUFFER
|
|
7897
|
+
});
|
|
7898
|
+
const tracked = /* @__PURE__ */ new Set();
|
|
7899
|
+
for (const path2 of out.split("\0")) {
|
|
7900
|
+
if (path2) tracked.add(path2.split("/")[0]);
|
|
7901
|
+
}
|
|
7902
|
+
return tracked;
|
|
7903
|
+
} catch (error) {
|
|
7904
|
+
if (error?.status === 128) return null;
|
|
7905
|
+
if (error?.code === "ENOENT") return null;
|
|
7906
|
+
if (error?.code === "ENOBUFS") {
|
|
7907
|
+
throw new Error(
|
|
7908
|
+
`git ls-files produced more than ${CHECK_IGNORE_MAX_BUFFER} bytes for ${firstSegments.length} segment(s) \u2014 the tracked set cannot be read, and treating it as empty would silently skip every ref (#4208)`
|
|
7909
|
+
);
|
|
7910
|
+
}
|
|
7911
|
+
throw error;
|
|
7912
|
+
}
|
|
7913
|
+
}
|
|
7875
7914
|
function runDocRefs(root, deps = {}) {
|
|
7876
7915
|
const readFile9 = deps.readFile ?? readFileOrNull;
|
|
7877
7916
|
const exists = deps.exists ?? import_node_fs13.existsSync;
|
|
7878
7917
|
const listDocs = deps.listDocs ?? defaultListDocs;
|
|
7879
7918
|
const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
|
|
7919
|
+
const trackedFirstSegments = deps.trackedFirstSegments ?? ((segs) => defaultTrackedFirstSegments(root, segs));
|
|
7880
7920
|
const commandPaths = deps.commandPaths ?? null;
|
|
7881
7921
|
const walked = listDocs(root);
|
|
7882
7922
|
const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
|
|
7883
7923
|
const docs2 = Object.fromEntries(
|
|
7884
7924
|
walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile9((0, import_node_path12.join)(root, rel))]).filter(([, body]) => body != null)
|
|
7885
7925
|
);
|
|
7886
|
-
const refResult = checkRefs(root, { exists, isIgnored }, docs2);
|
|
7926
|
+
const refResult = checkRefs(root, { exists, isIgnored, trackedFirstSegments }, docs2);
|
|
7887
7927
|
const findings = [
|
|
7888
7928
|
...checkPins(root, readFile9, docs2).findings,
|
|
7889
7929
|
...refResult.findings
|
|
@@ -9218,15 +9258,15 @@ function healJervCodePackageRegistration(opts = {}) {
|
|
|
9218
9258
|
return { available: true, ok: true, changed: false, version: null, detail: "skipped \u2014 no installed MMI clone carries the pi wrapper (install the Claude plugin first)" };
|
|
9219
9259
|
}
|
|
9220
9260
|
const packagePath = `${clone.path.replace(/\\/g, "/").replace(/\/+$/, "")}/${JERVCODE_WRAPPER_DIR}`;
|
|
9221
|
-
const
|
|
9222
|
-
const settings = readPiSettings(
|
|
9261
|
+
const settingsPath2 = (0, import_node_path14.join)(agentDir, "settings.json");
|
|
9262
|
+
const settings = readPiSettings(settingsPath2);
|
|
9223
9263
|
if (settings === null) {
|
|
9224
9264
|
return {
|
|
9225
9265
|
available: true,
|
|
9226
9266
|
ok: false,
|
|
9227
9267
|
changed: false,
|
|
9228
9268
|
version: clone.version,
|
|
9229
|
-
detail: `package NOT registered \u2014 ${
|
|
9269
|
+
detail: `package NOT registered \u2014 ${settingsPath2} is not a strict JSON object, so mmi left it alone; add ${packagePath} to its packages array by hand`
|
|
9230
9270
|
};
|
|
9231
9271
|
}
|
|
9232
9272
|
const current = settings ?? {};
|
|
@@ -9237,12 +9277,12 @@ function healJervCodePackageRegistration(opts = {}) {
|
|
|
9237
9277
|
}
|
|
9238
9278
|
current.packages = merged.next;
|
|
9239
9279
|
try {
|
|
9240
|
-
(0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(
|
|
9241
|
-
const tmp = `${
|
|
9280
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(settingsPath2), { recursive: true });
|
|
9281
|
+
const tmp = `${settingsPath2}.tmp-${process.pid}`;
|
|
9242
9282
|
(0, import_node_fs15.writeFileSync)(tmp, `${JSON.stringify(current, null, 2)}
|
|
9243
9283
|
`, "utf8");
|
|
9244
9284
|
try {
|
|
9245
|
-
(0, import_node_fs15.renameSync)(tmp,
|
|
9285
|
+
(0, import_node_fs15.renameSync)(tmp, settingsPath2);
|
|
9246
9286
|
} catch (renameError) {
|
|
9247
9287
|
(0, import_node_fs15.rmSync)(tmp, { force: true });
|
|
9248
9288
|
throw renameError;
|
|
@@ -12011,6 +12051,12 @@ async function auditRepoCi(repo, deps) {
|
|
|
12011
12051
|
label: "delete_branch_on_merge enabled",
|
|
12012
12052
|
detail: info.delete_branch_on_merge === true ? void 0 : "false or unavailable"
|
|
12013
12053
|
});
|
|
12054
|
+
checks.push({
|
|
12055
|
+
ok: info.has_wiki === false,
|
|
12056
|
+
label: "has_wiki disabled",
|
|
12057
|
+
detail: info.has_wiki === false ? void 0 : "wikis are retired org-wide",
|
|
12058
|
+
remediation: `gh api -X PATCH repos/${repo} -F has_wiki=false`
|
|
12059
|
+
});
|
|
12014
12060
|
const hasCanonicalGateWorkflow = repoClass === "hub" ? true : repoClass === "content" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
|
|
12015
12061
|
let prWorkflowPaths = repoClass === "deployable" && hasCanonicalGateWorkflow ? [PRODUCT_GATE_PATH] : [];
|
|
12016
12062
|
if (repoClass === "deployable" && !hasCanonicalGateWorkflow) {
|
|
@@ -12269,7 +12315,7 @@ async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
|
|
|
12269
12315
|
const applied = [];
|
|
12270
12316
|
const skipped = [];
|
|
12271
12317
|
const errors = [];
|
|
12272
|
-
const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch"));
|
|
12318
|
+
const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch") || c.label.startsWith("has_wiki"));
|
|
12273
12319
|
const needsPatch = mergeChecks.some((c) => !c.ok);
|
|
12274
12320
|
if (!needsPatch) {
|
|
12275
12321
|
skipped.push("merge settings already canonical");
|
|
@@ -12280,10 +12326,11 @@ async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
|
|
|
12280
12326
|
body: {
|
|
12281
12327
|
allow_auto_merge: true,
|
|
12282
12328
|
allow_squash_merge: true,
|
|
12283
|
-
delete_branch_on_merge: true
|
|
12329
|
+
delete_branch_on_merge: true,
|
|
12330
|
+
has_wiki: false
|
|
12284
12331
|
}
|
|
12285
12332
|
});
|
|
12286
|
-
applied.push("allow_auto_merge, allow_squash_merge, delete_branch_on_merge");
|
|
12333
|
+
applied.push("allow_auto_merge, allow_squash_merge, delete_branch_on_merge, has_wiki=false");
|
|
12287
12334
|
} catch (e) {
|
|
12288
12335
|
errors.push(e.message);
|
|
12289
12336
|
}
|
|
@@ -12466,7 +12513,7 @@ async function parkProductRuleset(repo, deps) {
|
|
|
12466
12513
|
return result;
|
|
12467
12514
|
}
|
|
12468
12515
|
function reconcileOwnedFailures(report) {
|
|
12469
|
-
return report.checks.filter((check) => !check.ok).filter((check) => check.label.startsWith("allow_") || check.label.startsWith("delete_branch") || check.label.startsWith("gate workflow committed on ") || check.label.startsWith("product ruleset reference committed on ") || check.label === "product required status checks active" || check.label === RULESET_REFERENCE_MATCH_LABEL || check.label === REQUIRED_CONTEXTS_EMITTED_LABEL || check.label === TAG_ADDRESSABLE_CONTEXTS_LABEL).map((check) => check.label);
|
|
12516
|
+
return report.checks.filter((check) => !check.ok).filter((check) => check.label.startsWith("allow_") || check.label.startsWith("delete_branch") || check.label.startsWith("has_wiki") || check.label.startsWith("gate workflow committed on ") || check.label.startsWith("product ruleset reference committed on ") || check.label === "product required status checks active" || check.label === RULESET_REFERENCE_MATCH_LABEL || check.label === REQUIRED_CONTEXTS_EMITTED_LABEL || check.label === TAG_ADDRESSABLE_CONTEXTS_LABEL).map((check) => check.label);
|
|
12470
12517
|
}
|
|
12471
12518
|
async function finalizeCiReconcile(repo, deps, result, before, pendingReason) {
|
|
12472
12519
|
if (result.errors.length > 0) {
|
|
@@ -13757,23 +13804,6 @@ async function fetchSchedulesList(deps) {
|
|
|
13757
13804
|
return null;
|
|
13758
13805
|
}
|
|
13759
13806
|
}
|
|
13760
|
-
async function fetchDocsAuditList(deps) {
|
|
13761
|
-
if (!deps.baseUrl) return { notArmed: true };
|
|
13762
|
-
try {
|
|
13763
|
-
const token = await deps.token();
|
|
13764
|
-
if (!token) return { notArmed: true };
|
|
13765
|
-
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/docs-audit/list`, {
|
|
13766
|
-
method: "GET",
|
|
13767
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
13768
|
-
});
|
|
13769
|
-
if (res.status === 404) return { notArmed: true };
|
|
13770
|
-
if (!res.ok) return { ok: false, error: `docs-audit list HTTP ${res.status}` };
|
|
13771
|
-
const body = await res.json();
|
|
13772
|
-
return { ok: true, rows: Array.isArray(body?.docsAudits) ? body.docsAudits : [] };
|
|
13773
|
-
} catch (e) {
|
|
13774
|
-
return { ok: false, error: e.message };
|
|
13775
|
-
}
|
|
13776
|
-
}
|
|
13777
13807
|
async function fetchOrgConfig(deps) {
|
|
13778
13808
|
if (!deps.baseUrl) return null;
|
|
13779
13809
|
const token = await deps.token();
|
|
@@ -13824,9 +13854,6 @@ async function retireProject(slug, deps) {
|
|
|
13824
13854
|
async function setDeployCoords(slug, payload, deps) {
|
|
13825
13855
|
return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
|
|
13826
13856
|
}
|
|
13827
|
-
async function recordDocsAudit(verdict, deps) {
|
|
13828
|
-
return postJson("/docs-audit/record", { ...verdict }, deps);
|
|
13829
|
-
}
|
|
13830
13857
|
async function postSchedulesLift(repo, schedules, deps) {
|
|
13831
13858
|
const res = await postJson("/schedules/lift", { repo, schedules }, deps);
|
|
13832
13859
|
if (!res.ok && res.status === 404) return { ...res, unreachable: "route-absent" };
|
|
@@ -15547,13 +15574,27 @@ async function isOrgRegisteredRepo(cfg, deps = {}) {
|
|
|
15547
15574
|
if (!read.ok) return false;
|
|
15548
15575
|
return read.project !== null;
|
|
15549
15576
|
}
|
|
15550
|
-
|
|
15551
|
-
|
|
15552
|
-
const
|
|
15553
|
-
|
|
15554
|
-
|
|
15555
|
-
]
|
|
15556
|
-
|
|
15577
|
+
var GC_PR_LOOKUP_CONCURRENCY = 8;
|
|
15578
|
+
async function resolveBranchPrs(branches, limit) {
|
|
15579
|
+
const queue = [...new Set(branches.map((b) => b.trim()).filter(Boolean))];
|
|
15580
|
+
const prs = [];
|
|
15581
|
+
const failures = [];
|
|
15582
|
+
const args = (branch) => ["pr", "list", "--head", branch, "--state", "all", "--limit", String(limit), "--json", "number,headRefName,headRefOid,state"];
|
|
15583
|
+
let next = 0;
|
|
15584
|
+
const worker = async () => {
|
|
15585
|
+
for (let i = next++; i < queue.length; i = next++) {
|
|
15586
|
+
const branch = queue[i];
|
|
15587
|
+
try {
|
|
15588
|
+
const { stdout } = await execFileP2("gh", args(branch), { timeout: GC_GH_TIMEOUT_MS });
|
|
15589
|
+
const rows = JSON.parse(stdout || "[]");
|
|
15590
|
+
prs.push(...rows.filter((pr2) => pr2.headRefName?.trim() === branch));
|
|
15591
|
+
} catch (e) {
|
|
15592
|
+
failures.push({ branch, detail: e.message });
|
|
15593
|
+
}
|
|
15594
|
+
}
|
|
15595
|
+
};
|
|
15596
|
+
await Promise.all(Array.from({ length: Math.min(GC_PR_LOOKUP_CONCURRENCY, queue.length) }, worker));
|
|
15597
|
+
return { prs, failures };
|
|
15557
15598
|
}
|
|
15558
15599
|
async function localBranchHeads() {
|
|
15559
15600
|
try {
|
|
@@ -15697,21 +15738,26 @@ function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
|
|
|
15697
15738
|
return explicitRepoWorktreesRoot(explicitRoot, repoRoot2, rootDirs);
|
|
15698
15739
|
}
|
|
15699
15740
|
async function gcPlan(remote, limit, opts = {}) {
|
|
15700
|
-
const [branches, heads, current, stale,
|
|
15741
|
+
const [branches, heads, current, stale, worktrees, siblingDirs, preserved, mergedIntoBase] = await Promise.all([
|
|
15701
15742
|
gitOut(["branch", "--format=%(refname:short)"]),
|
|
15702
15743
|
localBranchHeads(),
|
|
15703
15744
|
gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
15704
15745
|
collectStaleTrackingRefs(remote, {
|
|
15705
15746
|
execGit: (args) => execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })
|
|
15706
15747
|
}),
|
|
15707
|
-
ghPrs(limit),
|
|
15708
15748
|
worktreeBranches(),
|
|
15709
15749
|
siblingWorktreeDirs(opts.root),
|
|
15710
15750
|
preservedBranches(),
|
|
15711
15751
|
branchesMergedIntoBase(remote)
|
|
15712
15752
|
]);
|
|
15753
|
+
const localBranches = branches.split(/\r?\n/).map((b) => b.trim()).filter(Boolean);
|
|
15754
|
+
const { prs, failures } = await resolveBranchPrs(
|
|
15755
|
+
[...localBranches, ...stale.map((ref) => branchForTrackingRef(ref, remote)).filter((b) => Boolean(b))].filter((b) => !isProtectedBranch(b)),
|
|
15756
|
+
limit
|
|
15757
|
+
);
|
|
15713
15758
|
return buildGcPlan({
|
|
15714
|
-
localBranches
|
|
15759
|
+
localBranches,
|
|
15760
|
+
prLookupFailures: failures,
|
|
15715
15761
|
localBranchHeads: heads,
|
|
15716
15762
|
currentBranch: current,
|
|
15717
15763
|
siblingWorktreeDirs: siblingDirs,
|
|
@@ -17813,8 +17859,6 @@ function consolidateCommandNamespaces(program3) {
|
|
|
17813
17859
|
move(program3, plugin, "plugin-heal", "heal");
|
|
17814
17860
|
move(program3, plugin, "plugin-prune", "prune");
|
|
17815
17861
|
move(program3, plugin, "session-start");
|
|
17816
|
-
const docs2 = child(program3, "docs");
|
|
17817
|
-
move(program3, docs2, "docs-audit", "audit");
|
|
17818
17862
|
const train = child(program3, "train");
|
|
17819
17863
|
const fullTrack2 = child(program3, "full-track");
|
|
17820
17864
|
move(fullTrack2, train, "readiness");
|
|
@@ -17825,18 +17869,25 @@ function consolidateCommandNamespaces(program3) {
|
|
|
17825
17869
|
move(program3, stage, "port-range");
|
|
17826
17870
|
}
|
|
17827
17871
|
|
|
17872
|
+
// src/pi-plugin-registration.ts
|
|
17873
|
+
var import_node_fs22 = require("node:fs");
|
|
17874
|
+
var import_node_path20 = require("node:path");
|
|
17875
|
+
|
|
17828
17876
|
// src/plugin-cache-prune.ts
|
|
17829
17877
|
var PLUGIN_CACHE_KEEP = 2;
|
|
17830
17878
|
var VERSION_DIR = /^\d+\.\d+\.\d+(?:[-+].*)?$/;
|
|
17831
17879
|
function isVersionDirName(name) {
|
|
17832
17880
|
return VERSION_DIR.test(name);
|
|
17833
17881
|
}
|
|
17834
|
-
function selectPrunablePluginVersions(names, currentVersion) {
|
|
17882
|
+
function selectPrunablePluginVersions(names, currentVersion, installedVersion) {
|
|
17835
17883
|
const versions = [...new Set(names)].filter(isVersionDirName);
|
|
17836
17884
|
if (versions.length <= PLUGIN_CACHE_KEEP) return [];
|
|
17885
|
+
if (!currentVersion && !installedVersion) return [];
|
|
17837
17886
|
const newestFirst = [...versions].sort((a, b) => compareVersions(b, a));
|
|
17838
17887
|
const keep = /* @__PURE__ */ new Set();
|
|
17888
|
+
keep.add(newestFirst[0]);
|
|
17839
17889
|
if (currentVersion && versions.includes(currentVersion)) keep.add(currentVersion);
|
|
17890
|
+
if (installedVersion && versions.includes(installedVersion)) keep.add(installedVersion);
|
|
17840
17891
|
for (const v of newestFirst) {
|
|
17841
17892
|
if (keep.size >= PLUGIN_CACHE_KEEP) break;
|
|
17842
17893
|
keep.add(v);
|
|
@@ -17964,7 +18015,7 @@ function buildPluginCachePlan(home, running, deps, opts = {}) {
|
|
|
17964
18015
|
return absent;
|
|
17965
18016
|
}
|
|
17966
18017
|
const versions = names.filter(isVersionDirName);
|
|
17967
|
-
const prune = selectPrunablePluginVersions(versions, running);
|
|
18018
|
+
const prune = selectPrunablePluginVersions(versions, running, opts.installedVersion);
|
|
17968
18019
|
const pruneSet = new Set(prune);
|
|
17969
18020
|
const keep = [...versions].sort((a, b) => compareVersions(b, a)).filter((v) => !pruneSet.has(v));
|
|
17970
18021
|
const bytes = opts.withBytes ? prune.reduce((sum, v) => sum + deps.dirBytes(`${cacheRoot}/${v}`), 0) : 0;
|
|
@@ -18063,6 +18114,68 @@ function renderPluginCachePlan(plan, applied) {
|
|
|
18063
18114
|
return lines.join("\n");
|
|
18064
18115
|
}
|
|
18065
18116
|
|
|
18117
|
+
// src/pi-plugin-registration.ts
|
|
18118
|
+
function isMmiPiPackage(entry) {
|
|
18119
|
+
return /[\\/]mutmutco[\\/]mmi[\\/][^\\/]+[\\/]\.pi-plugin\/?$/.test(entry);
|
|
18120
|
+
}
|
|
18121
|
+
function expectedPiPluginPath(home, env, installedVersion) {
|
|
18122
|
+
const root = env.CLAUDE_PLUGIN_ROOT?.trim();
|
|
18123
|
+
if (root && /[\\/]mutmutco[\\/]mmi[\\/]/.test(root)) return (0, import_node_path20.join)(root, ".pi-plugin");
|
|
18124
|
+
const version = installedVersion ?? runningPluginVersion(env);
|
|
18125
|
+
if (!version) return void 0;
|
|
18126
|
+
return (0, import_node_path20.join)(home, ".claude", "plugins", "cache", "mutmutco", "mmi", version, ".pi-plugin");
|
|
18127
|
+
}
|
|
18128
|
+
function settingsPath(home) {
|
|
18129
|
+
return (0, import_node_path20.join)(home, ".pi", "agent", "settings.json");
|
|
18130
|
+
}
|
|
18131
|
+
function readPiPluginState(home, env, installedVersion) {
|
|
18132
|
+
if (!(0, import_node_fs22.existsSync)((0, import_node_path20.join)(home, ".pi", "agent"))) return void 0;
|
|
18133
|
+
const expectedPath = expectedPiPluginPath(home, env, installedVersion);
|
|
18134
|
+
if (!expectedPath || !(0, import_node_fs22.existsSync)(expectedPath)) return void 0;
|
|
18135
|
+
const file = settingsPath(home);
|
|
18136
|
+
if (!(0, import_node_fs22.existsSync)(file)) return { expectedPath, settingsReadable: true };
|
|
18137
|
+
try {
|
|
18138
|
+
const parsed = JSON.parse((0, import_node_fs22.readFileSync)(file, "utf8"));
|
|
18139
|
+
const packages = Array.isArray(parsed.packages) ? parsed.packages : [];
|
|
18140
|
+
return { expectedPath, registeredPath: packages.find((p) => typeof p === "string" && isMmiPiPackage(p)), settingsReadable: true };
|
|
18141
|
+
} catch {
|
|
18142
|
+
return { expectedPath, settingsReadable: false };
|
|
18143
|
+
}
|
|
18144
|
+
}
|
|
18145
|
+
function healPiPluginRegistration(home, env, installedVersion) {
|
|
18146
|
+
const state = readPiPluginState(home, env, installedVersion);
|
|
18147
|
+
if (!state) return { ok: false, detail: "no ~/.pi/agent or no installed .pi-plugin payload" };
|
|
18148
|
+
if (!state.settingsReadable) return { ok: false, detail: "settings.json unreadable \u2014 nothing written (fail closed)" };
|
|
18149
|
+
const file = settingsPath(home);
|
|
18150
|
+
try {
|
|
18151
|
+
const parsed = (0, import_node_fs22.existsSync)(file) ? JSON.parse((0, import_node_fs22.readFileSync)(file, "utf8")) : {};
|
|
18152
|
+
const packages = Array.isArray(parsed.packages) ? parsed.packages : [];
|
|
18153
|
+
const next = packages.filter((p) => !(typeof p === "string" && isMmiPiPackage(p)));
|
|
18154
|
+
next.push(state.expectedPath);
|
|
18155
|
+
(0, import_node_fs22.writeFileSync)(file, `${JSON.stringify({ ...parsed, packages: next }, null, 2)}
|
|
18156
|
+
`);
|
|
18157
|
+
return { ok: true, detail: state.registeredPath ? `replaced ${state.registeredPath}` : `registered ${state.expectedPath}` };
|
|
18158
|
+
} catch (e) {
|
|
18159
|
+
return { ok: false, detail: e.message };
|
|
18160
|
+
}
|
|
18161
|
+
}
|
|
18162
|
+
|
|
18163
|
+
// src/statusline-invalidate.ts
|
|
18164
|
+
var import_node_child_process10 = require("node:child_process");
|
|
18165
|
+
function invalidateStatuslineBoardCache() {
|
|
18166
|
+
try {
|
|
18167
|
+
const child2 = (0, import_node_child_process10.spawn)("jerv-cli", ["lane", "ops", "invalidate-board"], {
|
|
18168
|
+
detached: true,
|
|
18169
|
+
stdio: "ignore",
|
|
18170
|
+
windowsHide: true
|
|
18171
|
+
});
|
|
18172
|
+
child2.on("error", () => {
|
|
18173
|
+
});
|
|
18174
|
+
child2.unref();
|
|
18175
|
+
} catch {
|
|
18176
|
+
}
|
|
18177
|
+
}
|
|
18178
|
+
|
|
18066
18179
|
// src/skill-lesson.ts
|
|
18067
18180
|
var SKILL_LESSON_LABEL = "skill-lesson";
|
|
18068
18181
|
var SKILL_NAMES = ["bootstrap", "browser-automation", "doctor", "epic", "hotfix", "mmi", "onboard", "rcand", "release", "resume", "secrets", "stage", "worktree"];
|
|
@@ -18302,12 +18415,12 @@ function renderVerifyBroker(input) {
|
|
|
18302
18415
|
}
|
|
18303
18416
|
|
|
18304
18417
|
// src/hotfix-coverage.ts
|
|
18305
|
-
var
|
|
18418
|
+
var import_node_child_process11 = require("node:child_process");
|
|
18306
18419
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
18307
18420
|
function checkHotfixCoverage(options = {}) {
|
|
18308
18421
|
const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
|
|
18309
18422
|
const ack = (options.ack ?? []).filter(Boolean);
|
|
18310
|
-
const git2 = options.git ?? ((args, opts) => (0,
|
|
18423
|
+
const git2 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
18311
18424
|
const revList = (range) => {
|
|
18312
18425
|
const out = git2(["rev-list", "--no-merges", range]).trim();
|
|
18313
18426
|
return out ? out.split("\n") : [];
|
|
@@ -18375,7 +18488,7 @@ function checkHotfixCoverage(options = {}) {
|
|
|
18375
18488
|
}
|
|
18376
18489
|
function checkHotfixCarries(options) {
|
|
18377
18490
|
const { cwd = process.cwd(), branch, baseRef, targets } = options;
|
|
18378
|
-
const git2 = options.git ?? ((args, opts) => (0,
|
|
18491
|
+
const git2 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
18379
18492
|
const isAncestor = (sha, ref) => {
|
|
18380
18493
|
try {
|
|
18381
18494
|
git2(["merge-base", "--is-ancestor", sha, ref]);
|
|
@@ -19489,9 +19602,9 @@ function renderAccessReport(report) {
|
|
|
19489
19602
|
|
|
19490
19603
|
// src/repo-index.ts
|
|
19491
19604
|
var import_node_crypto4 = require("node:crypto");
|
|
19492
|
-
var
|
|
19493
|
-
var
|
|
19494
|
-
var
|
|
19605
|
+
var import_node_child_process12 = require("node:child_process");
|
|
19606
|
+
var import_node_fs23 = require("node:fs");
|
|
19607
|
+
var import_node_path21 = require("node:path");
|
|
19495
19608
|
var REPO_INDEX_SCHEMA = 1;
|
|
19496
19609
|
var HARD_DENY = [
|
|
19497
19610
|
/(^|\/)\.env(\.|$)/i,
|
|
@@ -19616,11 +19729,11 @@ function loadReadmeHints(cwd, candidatePaths) {
|
|
|
19616
19729
|
}
|
|
19617
19730
|
for (const rel of readmes) {
|
|
19618
19731
|
if (isHardDeniedPath(rel)) continue;
|
|
19619
|
-
const abs = (0,
|
|
19620
|
-
if (!(0,
|
|
19732
|
+
const abs = (0, import_node_path21.join)(cwd, ...rel.split("/"));
|
|
19733
|
+
if (!(0, import_node_fs23.existsSync)(abs)) continue;
|
|
19621
19734
|
let text;
|
|
19622
19735
|
try {
|
|
19623
|
-
text = (0,
|
|
19736
|
+
text = (0, import_node_fs23.readFileSync)(abs, "utf8");
|
|
19624
19737
|
} catch {
|
|
19625
19738
|
continue;
|
|
19626
19739
|
}
|
|
@@ -19633,9 +19746,9 @@ function loadReadmeHints(cwd, candidatePaths) {
|
|
|
19633
19746
|
return hints;
|
|
19634
19747
|
}
|
|
19635
19748
|
function toPosix(p) {
|
|
19636
|
-
return p.split(
|
|
19749
|
+
return p.split(import_node_path21.sep).join("/");
|
|
19637
19750
|
}
|
|
19638
|
-
function listCandidatePaths(cwd, exec =
|
|
19751
|
+
function listCandidatePaths(cwd, exec = import_node_child_process12.execFileSync) {
|
|
19639
19752
|
try {
|
|
19640
19753
|
const out = exec("git", ["ls-files", "-z", "-c", "-o", "--exclude-standard"], {
|
|
19641
19754
|
cwd,
|
|
@@ -19655,11 +19768,11 @@ function rebuildRepoIndex(cwd, repoSlug2) {
|
|
|
19655
19768
|
for (const rel of candidates) {
|
|
19656
19769
|
if (ignored.has(rel)) continue;
|
|
19657
19770
|
if (isHardDeniedPath(rel)) continue;
|
|
19658
|
-
const abs = (0,
|
|
19659
|
-
if (!(0,
|
|
19771
|
+
const abs = (0, import_node_path21.join)(cwd, ...rel.split("/"));
|
|
19772
|
+
if (!(0, import_node_fs23.existsSync)(abs)) continue;
|
|
19660
19773
|
let text;
|
|
19661
19774
|
try {
|
|
19662
|
-
text = (0,
|
|
19775
|
+
text = (0, import_node_fs23.readFileSync)(abs, "utf8");
|
|
19663
19776
|
} catch {
|
|
19664
19777
|
continue;
|
|
19665
19778
|
}
|
|
@@ -19684,16 +19797,16 @@ function rebuildRepoIndex(cwd, repoSlug2) {
|
|
|
19684
19797
|
entries
|
|
19685
19798
|
};
|
|
19686
19799
|
const store = repoIndexStorePath(cwd);
|
|
19687
|
-
(0,
|
|
19688
|
-
(0,
|
|
19800
|
+
(0, import_node_fs23.mkdirSync)((0, import_node_path21.dirname)(store), { recursive: true });
|
|
19801
|
+
(0, import_node_fs23.writeFileSync)(store, `${JSON.stringify(projection, null, 2)}
|
|
19689
19802
|
`, "utf8");
|
|
19690
19803
|
return projection;
|
|
19691
19804
|
}
|
|
19692
19805
|
function loadRepoIndex(cwd) {
|
|
19693
19806
|
const store = repoIndexStorePath(cwd);
|
|
19694
|
-
if (!(0,
|
|
19807
|
+
if (!(0, import_node_fs23.existsSync)(store)) return null;
|
|
19695
19808
|
try {
|
|
19696
|
-
const raw = JSON.parse((0,
|
|
19809
|
+
const raw = JSON.parse((0, import_node_fs23.readFileSync)(store, "utf8"));
|
|
19697
19810
|
if (raw?.schema !== REPO_INDEX_SCHEMA || !Array.isArray(raw.entries)) return null;
|
|
19698
19811
|
return raw;
|
|
19699
19812
|
} catch {
|
|
@@ -19758,14 +19871,14 @@ function searchRepoIndex(idx, query, limit = 20) {
|
|
|
19758
19871
|
}
|
|
19759
19872
|
return out;
|
|
19760
19873
|
}
|
|
19761
|
-
function inferRepoSlug(cwd, exec =
|
|
19874
|
+
function inferRepoSlug(cwd, exec = import_node_child_process12.execFileSync) {
|
|
19762
19875
|
try {
|
|
19763
19876
|
const url = String(exec("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" })).trim();
|
|
19764
19877
|
const m = /[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
|
|
19765
19878
|
if (m?.[1]) return m[1].toLowerCase();
|
|
19766
19879
|
} catch {
|
|
19767
19880
|
}
|
|
19768
|
-
return ((0,
|
|
19881
|
+
return ((0, import_node_path21.basename)(cwd) || "local").toLowerCase();
|
|
19769
19882
|
}
|
|
19770
19883
|
|
|
19771
19884
|
// src/repo-index-cloud-client.ts
|
|
@@ -19905,10 +20018,10 @@ async function gcRepoIndexCloud(deps) {
|
|
|
19905
20018
|
}
|
|
19906
20019
|
|
|
19907
20020
|
// src/repo-index-sync.ts
|
|
19908
|
-
var
|
|
20021
|
+
var import_node_fs24 = require("node:fs");
|
|
19909
20022
|
var import_node_os9 = require("node:os");
|
|
19910
|
-
var
|
|
19911
|
-
var
|
|
20023
|
+
var import_node_path22 = require("node:path");
|
|
20024
|
+
var import_node_child_process13 = require("node:child_process");
|
|
19912
20025
|
var MAX_EMBED_BACKFILL_ROUNDS = 40;
|
|
19913
20026
|
function normalizeRepo(raw) {
|
|
19914
20027
|
const t = raw.trim().replace(/\.git$/, "");
|
|
@@ -19928,7 +20041,7 @@ function rosterRepos(projects) {
|
|
|
19928
20041
|
}
|
|
19929
20042
|
function shallowClone(repo, dest, token) {
|
|
19930
20043
|
const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
|
|
19931
|
-
(0,
|
|
20044
|
+
(0, import_node_child_process13.execFileSync)(
|
|
19932
20045
|
"git",
|
|
19933
20046
|
["-c", `http.extraHeader=Authorization: Basic ${basic}`, "clone", "--depth", "1", "--single-branch", `https://github.com/${repo}.git`, dest],
|
|
19934
20047
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
|
|
@@ -19951,7 +20064,7 @@ async function syncEstateRepoIndex(opts) {
|
|
|
19951
20064
|
const failed = [];
|
|
19952
20065
|
const skipped = [];
|
|
19953
20066
|
for (const repo of repos) {
|
|
19954
|
-
const dir = (0,
|
|
20067
|
+
const dir = (0, import_node_fs24.mkdtempSync)((0, import_node_path22.join)((0, import_node_os9.tmpdir)(), "mmi-repo-index-"));
|
|
19955
20068
|
try {
|
|
19956
20069
|
shallowClone(repo, dir, opts.githubToken);
|
|
19957
20070
|
const built = rebuildRepoIndex(dir, repo);
|
|
@@ -20001,7 +20114,7 @@ async function syncEstateRepoIndex(opts) {
|
|
|
20001
20114
|
failed.push({ repo, error: e.message });
|
|
20002
20115
|
} finally {
|
|
20003
20116
|
try {
|
|
20004
|
-
(0,
|
|
20117
|
+
(0, import_node_fs24.rmSync)(dir, { recursive: true, force: true });
|
|
20005
20118
|
} catch {
|
|
20006
20119
|
}
|
|
20007
20120
|
}
|
|
@@ -20010,7 +20123,7 @@ async function syncEstateRepoIndex(opts) {
|
|
|
20010
20123
|
}
|
|
20011
20124
|
|
|
20012
20125
|
// src/repo-index-health.ts
|
|
20013
|
-
var
|
|
20126
|
+
var import_node_fs25 = require("node:fs");
|
|
20014
20127
|
|
|
20015
20128
|
// testdata/repo-index-golden-queries.json
|
|
20016
20129
|
var repo_index_golden_queries_default = {
|
|
@@ -20052,7 +20165,7 @@ function assertGoldenSuite(raw, source) {
|
|
|
20052
20165
|
function loadGoldenSuite(path2) {
|
|
20053
20166
|
let text;
|
|
20054
20167
|
try {
|
|
20055
|
-
text = (0,
|
|
20168
|
+
text = (0, import_node_fs25.readFileSync)(path2, "utf8");
|
|
20056
20169
|
} catch (e) {
|
|
20057
20170
|
throw new Error(`golden suite unreadable at ${path2}: ${e.message}`);
|
|
20058
20171
|
}
|
|
@@ -20195,9 +20308,9 @@ async function runRepoIndexHealth(opts) {
|
|
|
20195
20308
|
}
|
|
20196
20309
|
|
|
20197
20310
|
// src/spawn-policy-core.ts
|
|
20198
|
-
var
|
|
20199
|
-
var
|
|
20200
|
-
var
|
|
20311
|
+
var import_node_child_process14 = require("node:child_process");
|
|
20312
|
+
var import_node_fs26 = require("node:fs");
|
|
20313
|
+
var import_node_path23 = require("node:path");
|
|
20201
20314
|
var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
|
|
20202
20315
|
var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
|
|
20203
20316
|
var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
|
|
@@ -20266,7 +20379,7 @@ function findViolationsInSource(raw) {
|
|
|
20266
20379
|
return found;
|
|
20267
20380
|
}
|
|
20268
20381
|
function policedFiles(root) {
|
|
20269
|
-
const r = (0,
|
|
20382
|
+
const r = (0, import_node_child_process14.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
20270
20383
|
cwd: root,
|
|
20271
20384
|
encoding: "utf8",
|
|
20272
20385
|
windowsHide: true,
|
|
@@ -20283,7 +20396,7 @@ function runSpawnPolicy(root) {
|
|
|
20283
20396
|
for (const file of files) {
|
|
20284
20397
|
let raw;
|
|
20285
20398
|
try {
|
|
20286
|
-
raw = (0,
|
|
20399
|
+
raw = (0, import_node_fs26.readFileSync)((0, import_node_path23.join)(root, file), "utf8");
|
|
20287
20400
|
} catch {
|
|
20288
20401
|
continue;
|
|
20289
20402
|
}
|
|
@@ -20300,9 +20413,9 @@ function runSpawnPolicy(root) {
|
|
|
20300
20413
|
}
|
|
20301
20414
|
|
|
20302
20415
|
// src/test-policy-core.ts
|
|
20303
|
-
var
|
|
20304
|
-
var
|
|
20305
|
-
var
|
|
20416
|
+
var import_node_child_process15 = require("node:child_process");
|
|
20417
|
+
var import_node_fs27 = require("node:fs");
|
|
20418
|
+
var import_node_path24 = require("node:path");
|
|
20306
20419
|
var POLICY_FILE = "test-policy.json";
|
|
20307
20420
|
var TEST_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
20308
20421
|
var PY_TEST_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
|
|
@@ -20355,7 +20468,7 @@ function isTestPath(path2) {
|
|
|
20355
20468
|
return TEST_RE.test(path2) || PY_TEST_RE.test(path2);
|
|
20356
20469
|
}
|
|
20357
20470
|
function loadPolicy(root, readFile9 = readFileOrNull2) {
|
|
20358
|
-
const raw = readFile9((0,
|
|
20471
|
+
const raw = readFile9((0, import_node_path24.join)(root, POLICY_FILE));
|
|
20359
20472
|
if (raw == null) return { mandatory: [], declared: false };
|
|
20360
20473
|
try {
|
|
20361
20474
|
return { ...JSON.parse(raw), declared: true };
|
|
@@ -20365,7 +20478,7 @@ function loadPolicy(root, readFile9 = readFileOrNull2) {
|
|
|
20365
20478
|
}
|
|
20366
20479
|
function readFileOrNull2(path2) {
|
|
20367
20480
|
try {
|
|
20368
|
-
return (0,
|
|
20481
|
+
return (0, import_node_fs27.readFileSync)(path2, "utf8");
|
|
20369
20482
|
} catch {
|
|
20370
20483
|
return null;
|
|
20371
20484
|
}
|
|
@@ -20392,12 +20505,12 @@ function classify(changed, policy, present = () => false) {
|
|
|
20392
20505
|
const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
|
|
20393
20506
|
return { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected };
|
|
20394
20507
|
}
|
|
20395
|
-
function unresolvedProtectedEntries(policy, root, exists = (path2) => (0,
|
|
20396
|
-
return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0,
|
|
20508
|
+
function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs27.existsSync)(path2)) {
|
|
20509
|
+
return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path24.join)(root, p)));
|
|
20397
20510
|
}
|
|
20398
|
-
function unresolvedSatisfiers(policy, root, exists = (path2) => (0,
|
|
20511
|
+
function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs27.existsSync)(path2)) {
|
|
20399
20512
|
const declared = (policy.mandatory ?? []).flatMap((m) => m.satisfiedBy ?? []);
|
|
20400
|
-
return [...new Set(declared)].filter((p) => !exists((0,
|
|
20513
|
+
return [...new Set(declared)].filter((p) => !exists((0, import_node_path24.join)(root, p)));
|
|
20401
20514
|
}
|
|
20402
20515
|
function evaluate(changed, policy, present = () => false) {
|
|
20403
20516
|
const { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected } = classify(changed, policy, present);
|
|
@@ -20430,14 +20543,14 @@ function evaluate(changed, policy, present = () => false) {
|
|
|
20430
20543
|
return findings;
|
|
20431
20544
|
}
|
|
20432
20545
|
function git(args, cwd) {
|
|
20433
|
-
return (0,
|
|
20546
|
+
return (0, import_node_child_process15.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
20434
20547
|
}
|
|
20435
20548
|
var COAUTHOR_KEY = "Co-authored-by";
|
|
20436
20549
|
var GH_MESSAGE_SEPARATOR = /^-{5,}$/;
|
|
20437
20550
|
var LIFTED_KEYS = new RegExp(`^(?:${TRAILER_KEY}|${COAUTHOR_KEY}):`, "i");
|
|
20438
20551
|
function parseTrailers(message, cwd) {
|
|
20439
20552
|
try {
|
|
20440
|
-
return (0,
|
|
20553
|
+
return (0, import_node_child_process15.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
|
|
20441
20554
|
windowsHide: true,
|
|
20442
20555
|
cwd,
|
|
20443
20556
|
input: message,
|
|
@@ -20579,13 +20692,13 @@ function changedFilesSince(base, cwd) {
|
|
|
20579
20692
|
}
|
|
20580
20693
|
function runTestPolicy(root, deps = {}) {
|
|
20581
20694
|
const policy = deps.policy ?? loadPolicy(root);
|
|
20582
|
-
const exists = deps.exists ?? ((path2) => (0,
|
|
20695
|
+
const exists = deps.exists ?? ((path2) => (0, import_node_fs27.existsSync)(path2));
|
|
20583
20696
|
const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
|
|
20584
20697
|
const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
|
|
20585
20698
|
const refusal = deps.changed ? null : untrustworthyRange(root, base);
|
|
20586
20699
|
const changed = deps.changed ?? (refusal ? [] : changedFilesSince(base, root));
|
|
20587
20700
|
const lookup = deps.override !== void 0 ? { override: deps.override, refusals: [] } : !deps.changed && !refusal ? readOverride(base, root) : { override: null, refusals: [] };
|
|
20588
|
-
const present = (path2) => exists((0,
|
|
20701
|
+
const present = (path2) => exists((0, import_node_path24.join)(root, path2));
|
|
20589
20702
|
const removedByThisDiff = removedPaths(changed);
|
|
20590
20703
|
const staleFindings = [];
|
|
20591
20704
|
const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
|
|
@@ -20622,127 +20735,9 @@ function runTestPolicy(root, deps = {}) {
|
|
|
20622
20735
|
return result;
|
|
20623
20736
|
}
|
|
20624
20737
|
|
|
20625
|
-
// src/docs-audit-command.ts
|
|
20626
|
-
function serializeOutcome(outcome) {
|
|
20627
|
-
switch (outcome.kind) {
|
|
20628
|
-
case "clean":
|
|
20629
|
-
return "clean";
|
|
20630
|
-
case "refreshed":
|
|
20631
|
-
return `refreshed-${outcome.count}`;
|
|
20632
|
-
case "failed":
|
|
20633
|
-
return "failed";
|
|
20634
|
-
}
|
|
20635
|
-
}
|
|
20636
|
-
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
20637
|
-
function isValidIsoDate(date) {
|
|
20638
|
-
if (typeof date !== "string" || !ISO_DATE_RE.test(date)) return false;
|
|
20639
|
-
const parsed = Date.parse(`${date}T00:00:00Z`);
|
|
20640
|
-
if (Number.isNaN(parsed)) return false;
|
|
20641
|
-
return new Date(parsed).toISOString().slice(0, 10) === date;
|
|
20642
|
-
}
|
|
20643
|
-
var OUTCOME_WIRE_RE = /^(clean|refreshed-[1-9]\d*|failed)$/;
|
|
20644
|
-
function isValidOutcomeWire(outcome) {
|
|
20645
|
-
return typeof outcome === "string" && OUTCOME_WIRE_RE.test(outcome);
|
|
20646
|
-
}
|
|
20647
|
-
function docsAuditRecord(input) {
|
|
20648
|
-
const repo = input.repo.trim();
|
|
20649
|
-
const date = input.date.trim();
|
|
20650
|
-
const shaRange = input.shaRange.trim();
|
|
20651
|
-
const checkerVendor = input.checkerVendor.trim();
|
|
20652
|
-
if (!repo) throw new Error("docs audit record: repo is required");
|
|
20653
|
-
if (!date) throw new Error("docs audit record: date is required");
|
|
20654
|
-
if (!isValidIsoDate(date)) throw new Error(`docs audit record: date must be a real ISO date (YYYY-MM-DD), got "${date}"`);
|
|
20655
|
-
if (!shaRange) throw new Error("docs audit record: shaRange is required");
|
|
20656
|
-
if (!checkerVendor) throw new Error("docs audit record: checkerVendor is required");
|
|
20657
|
-
if (input.outcome.kind === "refreshed" && !(Number.isInteger(input.outcome.count) && input.outcome.count > 0)) {
|
|
20658
|
-
throw new Error("docs audit record: a `refreshed` outcome must carry a positive integer count");
|
|
20659
|
-
}
|
|
20660
|
-
if (input.outcome.kind === "failed" && !input.outcome.reason.trim()) {
|
|
20661
|
-
throw new Error("docs audit record: a `failed` outcome must carry a reason");
|
|
20662
|
-
}
|
|
20663
|
-
const outcome = serializeOutcome(input.outcome);
|
|
20664
|
-
if (!isValidOutcomeWire(outcome)) {
|
|
20665
|
-
throw new Error(`docs audit record: outcome serialized to an invalid wire form "${outcome}"`);
|
|
20666
|
-
}
|
|
20667
|
-
return { repo, date, shaRange, outcome, checkerVendor };
|
|
20668
|
-
}
|
|
20669
|
-
function ageInDays(verdictDate, today) {
|
|
20670
|
-
const a = Date.parse(`${verdictDate}T00:00:00Z`);
|
|
20671
|
-
const b = Date.parse(`${today}T00:00:00Z`);
|
|
20672
|
-
return Math.max(0, Math.round((b - a) / 864e5));
|
|
20673
|
-
}
|
|
20674
|
-
function docsAuditStatus(fetch2, opts) {
|
|
20675
|
-
if ("notArmed" in fetch2) {
|
|
20676
|
-
return { ok: true, state: "not-armed", line: `docs audit: ${opts.repo} janitor not armed (registry route not live yet)` };
|
|
20677
|
-
}
|
|
20678
|
-
if (!fetch2.ok) {
|
|
20679
|
-
return { ok: false, state: "error", line: `docs audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
|
|
20680
|
-
}
|
|
20681
|
-
if (!isValidIsoDate(opts.today)) {
|
|
20682
|
-
throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
|
|
20683
|
-
}
|
|
20684
|
-
if (fetch2.verdict === null) {
|
|
20685
|
-
const armedAt = opts.armedAt;
|
|
20686
|
-
if (armedAt && isValidIsoDate(armedAt)) {
|
|
20687
|
-
const cadenceDays = opts.cadenceDays ?? 7;
|
|
20688
|
-
const graceDays = opts.graceDays ?? 3;
|
|
20689
|
-
const sinceArmed = ageInDays(armedAt, opts.today);
|
|
20690
|
-
if (sinceArmed <= cadenceDays + graceDays) {
|
|
20691
|
-
return {
|
|
20692
|
-
ok: true,
|
|
20693
|
-
state: "awaiting-first-tick",
|
|
20694
|
-
line: `docs audit: ${opts.repo} armed ${armedAt} (${sinceArmed}d ago) \u2014 no verdict expected until the first tick`
|
|
20695
|
-
};
|
|
20696
|
-
}
|
|
20697
|
-
return {
|
|
20698
|
-
ok: false,
|
|
20699
|
-
state: "missing",
|
|
20700
|
-
line: `docs audit: ${opts.repo} no verdict on record ${sinceArmed}d after arming ${armedAt} \u2014 janitor blind or dead`
|
|
20701
|
-
};
|
|
20702
|
-
}
|
|
20703
|
-
return { ok: false, state: "missing", line: `docs audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
|
|
20704
|
-
}
|
|
20705
|
-
const verdict = fetch2.verdict;
|
|
20706
|
-
for (const field of ["repo", "shaRange", "checkerVendor"]) {
|
|
20707
|
-
const value = verdict[field];
|
|
20708
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
20709
|
-
const shown = typeof value === "string" ? `"${value}"` : `type ${Array.isArray(value) ? "array" : typeof value}`;
|
|
20710
|
-
return {
|
|
20711
|
-
ok: false,
|
|
20712
|
-
state: "malformed",
|
|
20713
|
-
line: `docs audit: ${opts.repo} malformed verdict ${field} (${shown}, expected a non-empty string) \u2014 registry record corrupt, treating as RED`
|
|
20714
|
-
};
|
|
20715
|
-
}
|
|
20716
|
-
}
|
|
20717
|
-
if (!isValidIsoDate(verdict.date)) {
|
|
20718
|
-
return {
|
|
20719
|
-
ok: false,
|
|
20720
|
-
state: "malformed",
|
|
20721
|
-
line: `docs audit: ${opts.repo} malformed verdict date "${verdict.date}" \u2014 registry record corrupt, treating as RED`
|
|
20722
|
-
};
|
|
20723
|
-
}
|
|
20724
|
-
if (!isValidOutcomeWire(verdict.outcome)) {
|
|
20725
|
-
return {
|
|
20726
|
-
ok: false,
|
|
20727
|
-
state: "malformed",
|
|
20728
|
-
line: `docs audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
|
|
20729
|
-
};
|
|
20730
|
-
}
|
|
20731
|
-
const cadence = opts.cadenceDays ?? 7;
|
|
20732
|
-
const grace = opts.graceDays ?? 3;
|
|
20733
|
-
const age = ageInDays(verdict.date, opts.today);
|
|
20734
|
-
if (age > cadence + grace) {
|
|
20735
|
-
return { ok: false, state: "stale", line: `docs audit: ${opts.repo} last verdict ${age}d old \u2014 janitor blind or dead` };
|
|
20736
|
-
}
|
|
20737
|
-
if (verdict.outcome === "failed") {
|
|
20738
|
-
return { ok: false, state: "failed", line: `docs audit: ${opts.repo} last run FAILED (${verdict.date}) \u2014 janitor needs attention` };
|
|
20739
|
-
}
|
|
20740
|
-
return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
|
|
20741
|
-
}
|
|
20742
|
-
|
|
20743
20738
|
// src/project-info-sync.ts
|
|
20744
|
-
var
|
|
20745
|
-
var
|
|
20739
|
+
var import_node_fs28 = require("node:fs");
|
|
20740
|
+
var import_node_path25 = require("node:path");
|
|
20746
20741
|
var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!, $readme: String!) {
|
|
20747
20742
|
updateProjectV2(input: { projectId: $projectId, shortDescription: $shortDescription, readme: $readme }) {
|
|
20748
20743
|
projectV2 { id }
|
|
@@ -20787,14 +20782,14 @@ function sharedName(entries, fallback) {
|
|
|
20787
20782
|
}
|
|
20788
20783
|
function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
|
|
20789
20784
|
if (!project2.projectId) throw new Error(`org project sync-info: ${targetRepo2} registry META has no projectId`);
|
|
20790
|
-
const readmePath = (0,
|
|
20791
|
-
if (!(0,
|
|
20785
|
+
const readmePath = (0, import_node_path25.join)(repoRoot2, "README.md");
|
|
20786
|
+
if (!(0, import_node_fs28.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
|
|
20792
20787
|
const entries = entriesFor(project2, projects);
|
|
20793
20788
|
const memberRepos = [...new Set(entries.flatMap((entry) => entry.repos ?? []))].filter((repo) => /^[^/]+\/[^/]+$/.test(repo)).sort((a, b) => a.localeCompare(b));
|
|
20794
20789
|
const projectName = sharedName(entries, project2.name?.trim() || targetRepo2.split("/").pop() || targetRepo2);
|
|
20795
20790
|
if (!memberRepos.length) throw new Error(`org project sync-info: project ${projectName} has no registered member repos`);
|
|
20796
20791
|
const entryNames = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
|
|
20797
|
-
const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0,
|
|
20792
|
+
const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs28.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
|
|
20798
20793
|
const lines = [
|
|
20799
20794
|
`# ${projectName}`,
|
|
20800
20795
|
"",
|
|
@@ -20813,8 +20808,8 @@ function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
|
|
|
20813
20808
|
const targetBase = `https://github.com/${targetRepo2}`;
|
|
20814
20809
|
const targetBranch = branchFor(targetRepo2, projects);
|
|
20815
20810
|
const orgDocs = [
|
|
20816
|
-
(0,
|
|
20817
|
-
(0,
|
|
20811
|
+
(0, import_node_fs28.existsSync)((0, import_node_path25.join)(repoRoot2, "docs", "org-readme.md")) ? `- [Org identity](${targetBase}/blob/${targetBranch}/docs/org-readme.md)` : "",
|
|
20812
|
+
(0, import_node_fs28.existsSync)((0, import_node_path25.join)(repoRoot2, "docs", "org-architecture.md")) ? `- [Org architecture](${targetBase}/blob/${targetBranch}/docs/org-architecture.md)` : ""
|
|
20818
20813
|
].filter(Boolean);
|
|
20819
20814
|
if (orgDocs.length) lines.push("", "## Organisation docs", "", ...orgDocs);
|
|
20820
20815
|
return { projectId: project2.projectId, projectName, targetRepo: targetRepo2, memberRepos, shortDescription, readme: `${lines.join("\n")}
|
|
@@ -21673,8 +21668,8 @@ function writeError(res) {
|
|
|
21673
21668
|
}
|
|
21674
21669
|
|
|
21675
21670
|
// src/secrets-commands.ts
|
|
21676
|
-
var
|
|
21677
|
-
var
|
|
21671
|
+
var import_node_fs29 = require("node:fs");
|
|
21672
|
+
var import_node_path26 = require("node:path");
|
|
21678
21673
|
var import_node_os10 = require("node:os");
|
|
21679
21674
|
|
|
21680
21675
|
// src/project-runtime.ts
|
|
@@ -21798,18 +21793,18 @@ function collectMap(value, previous = []) {
|
|
|
21798
21793
|
return [...previous, value];
|
|
21799
21794
|
}
|
|
21800
21795
|
async function decryptRailsCredentials(input) {
|
|
21801
|
-
const appDir = (0,
|
|
21796
|
+
const appDir = (0, import_node_path26.resolve)(input.appDir ?? process.cwd());
|
|
21802
21797
|
const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
|
|
21803
21798
|
const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
|
|
21804
|
-
const credentialsPath = (0,
|
|
21805
|
-
const masterKeyPath = (0,
|
|
21799
|
+
const credentialsPath = (0, import_node_path26.resolve)(appDir, credentialsFile);
|
|
21800
|
+
const masterKeyPath = (0, import_node_path26.resolve)(appDir, masterKeyFile);
|
|
21806
21801
|
const env = {
|
|
21807
21802
|
...process.env,
|
|
21808
21803
|
MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
|
|
21809
21804
|
MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
|
|
21810
21805
|
};
|
|
21811
|
-
if ((0,
|
|
21812
|
-
env.RAILS_MASTER_KEY = (0,
|
|
21806
|
+
if ((0, import_node_fs29.existsSync)(masterKeyPath)) {
|
|
21807
|
+
env.RAILS_MASTER_KEY = (0, import_node_fs29.readFileSync)(masterKeyPath, "utf8").trim();
|
|
21813
21808
|
}
|
|
21814
21809
|
const script = [
|
|
21815
21810
|
'require "json"',
|
|
@@ -21819,9 +21814,9 @@ async function decryptRailsCredentials(input) {
|
|
|
21819
21814
|
'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
|
|
21820
21815
|
"puts JSON.generate(config.config)"
|
|
21821
21816
|
].join("\n");
|
|
21822
|
-
const scriptDir = (0,
|
|
21823
|
-
const scriptPath = (0,
|
|
21824
|
-
(0,
|
|
21817
|
+
const scriptDir = (0, import_node_fs29.mkdtempSync)((0, import_node_path26.join)((0, import_node_os10.tmpdir)(), "mmi-rails-decrypt-"));
|
|
21818
|
+
const scriptPath = (0, import_node_path26.join)(scriptDir, "decrypt.rb");
|
|
21819
|
+
(0, import_node_fs29.writeFileSync)(scriptPath, script, "utf8");
|
|
21825
21820
|
try {
|
|
21826
21821
|
const args = ["exec", "ruby", scriptPath];
|
|
21827
21822
|
const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
|
|
@@ -21833,7 +21828,7 @@ async function decryptRailsCredentials(input) {
|
|
|
21833
21828
|
});
|
|
21834
21829
|
return JSON.parse(stdout);
|
|
21835
21830
|
} finally {
|
|
21836
|
-
(0,
|
|
21831
|
+
(0, import_node_fs29.rmSync)(scriptDir, { recursive: true, force: true });
|
|
21837
21832
|
}
|
|
21838
21833
|
}
|
|
21839
21834
|
async function readSecretStdin() {
|
|
@@ -21923,7 +21918,7 @@ function registerSecretsCommands(program3) {
|
|
|
21923
21918
|
let body;
|
|
21924
21919
|
if (o.file) {
|
|
21925
21920
|
try {
|
|
21926
|
-
body = (0,
|
|
21921
|
+
body = (0, import_node_fs29.readFileSync)((0, import_node_path26.resolve)(o.file), "utf8");
|
|
21927
21922
|
} catch (e) {
|
|
21928
21923
|
return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
|
|
21929
21924
|
}
|
|
@@ -22028,7 +22023,7 @@ function registerSecretsCommands(program3) {
|
|
|
22028
22023
|
{
|
|
22029
22024
|
...d,
|
|
22030
22025
|
decryptRailsCredentials,
|
|
22031
|
-
removeFile: (path2) => (0,
|
|
22026
|
+
removeFile: (path2) => (0, import_node_fs29.unlinkSync)((0, import_node_path26.resolve)(o.appDir ?? process.cwd(), path2))
|
|
22032
22027
|
},
|
|
22033
22028
|
{
|
|
22034
22029
|
repo: o.repo,
|
|
@@ -22192,7 +22187,7 @@ async function activateAppActor(commandPath3, env, mint) {
|
|
|
22192
22187
|
}
|
|
22193
22188
|
|
|
22194
22189
|
// src/box-commands.ts
|
|
22195
|
-
var
|
|
22190
|
+
var import_node_fs30 = require("node:fs");
|
|
22196
22191
|
|
|
22197
22192
|
// src/box.ts
|
|
22198
22193
|
var BOX_KEYS = {
|
|
@@ -22395,7 +22390,7 @@ function registerBoxCommands(program3) {
|
|
|
22395
22390
|
}
|
|
22396
22391
|
if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
|
|
22397
22392
|
else if (o.ssh && o.script) {
|
|
22398
|
-
(0,
|
|
22393
|
+
(0, import_node_fs30.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
|
|
22399
22394
|
console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
|
|
22400
22395
|
} else if (o.ssh) console.log(`${formatSshRecipe(found)}
|
|
22401
22396
|
${SSH_RECIPE_AGENT_NOTE}`);
|
|
@@ -22410,7 +22405,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
22410
22405
|
|
|
22411
22406
|
// src/schedules-commands.ts
|
|
22412
22407
|
var import_promises4 = require("node:fs/promises");
|
|
22413
|
-
var
|
|
22408
|
+
var import_node_child_process16 = require("node:child_process");
|
|
22414
22409
|
var import_node_util7 = require("node:util");
|
|
22415
22410
|
|
|
22416
22411
|
// src/schedules.ts
|
|
@@ -22721,7 +22716,7 @@ function cadenceStale(registryCadence, liveCadence) {
|
|
|
22721
22716
|
if (!liveCrons.length) return false;
|
|
22722
22717
|
return !liveCrons.every((cron) => registered.has(cron));
|
|
22723
22718
|
}
|
|
22724
|
-
function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22719
|
+
function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set(), disabledWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22725
22720
|
const registryGithub = registry2.filter((r) => r.executor === "github-actions");
|
|
22726
22721
|
const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
|
|
22727
22722
|
const registryById = new Map(registryGithub.map((r) => [r.id, r]));
|
|
@@ -22752,6 +22747,18 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22752
22747
|
continue;
|
|
22753
22748
|
}
|
|
22754
22749
|
if (activeWorkflowNames.has(r.id)) continue;
|
|
22750
|
+
if (disabledWorkflowNames.has(r.id)) {
|
|
22751
|
+
if (readRepos.has(r.repo)) {
|
|
22752
|
+
drifts.push({
|
|
22753
|
+
class: "registered-but-disabled",
|
|
22754
|
+
name: r.id,
|
|
22755
|
+
executor: r.executor || "github-actions",
|
|
22756
|
+
detail: "workflow file present but DISABLED in GitHub Actions \u2014 the dispatcher cannot run it",
|
|
22757
|
+
remedy: `\`gh workflow enable\` it in ${r.repo} to re-arm (or, if it was retired on purpose, re-run schedules register so the replace prunes the SCHEDULE# row) \u2014 never prune a lane parked on purpose`
|
|
22758
|
+
});
|
|
22759
|
+
}
|
|
22760
|
+
continue;
|
|
22761
|
+
}
|
|
22755
22762
|
if (readRepos.has(r.repo)) {
|
|
22756
22763
|
drifts.push({
|
|
22757
22764
|
class: "registered-but-dead",
|
|
@@ -22827,7 +22834,7 @@ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
|
|
|
22827
22834
|
}
|
|
22828
22835
|
return drifts.sort((a, b) => a.name.localeCompare(b.name));
|
|
22829
22836
|
}
|
|
22830
|
-
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}) {
|
|
22837
|
+
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}, disabledWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22831
22838
|
if (registry2 === null) {
|
|
22832
22839
|
return {
|
|
22833
22840
|
reconciliation: [],
|
|
@@ -22837,7 +22844,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
|
|
|
22837
22844
|
}
|
|
22838
22845
|
const live = githubEntries2.filter((e) => e.executor === "github-actions");
|
|
22839
22846
|
const drifts = [
|
|
22840
|
-
...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames),
|
|
22847
|
+
...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames, disabledWorkflowNames),
|
|
22841
22848
|
...registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
|
|
22842
22849
|
schedulerRead: Boolean(harbour.schedulerRead),
|
|
22843
22850
|
now: harbour.now
|
|
@@ -22847,7 +22854,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
|
|
|
22847
22854
|
}
|
|
22848
22855
|
|
|
22849
22856
|
// src/schedules-commands.ts
|
|
22850
|
-
var execFileP5 = (0, import_node_util7.promisify)(
|
|
22857
|
+
var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process16.execFile);
|
|
22851
22858
|
var AWS_REGION = "eu-central-1";
|
|
22852
22859
|
var AWS_TIMEOUT_MS = 3e4;
|
|
22853
22860
|
var AWS_RETRY_DELAY_MS = 1500;
|
|
@@ -22871,9 +22878,15 @@ async function repoWorkflowEntries(client, repo) {
|
|
|
22871
22878
|
const entries = [];
|
|
22872
22879
|
const failures = [];
|
|
22873
22880
|
const workflows = [];
|
|
22881
|
+
const disabled = [];
|
|
22874
22882
|
for (const wf of workflowsList) {
|
|
22875
|
-
if (
|
|
22883
|
+
if (typeof wf?.path !== "string" || !wf.path) continue;
|
|
22876
22884
|
if (!wf.path.startsWith(".github/workflows/")) continue;
|
|
22885
|
+
if (wf.state !== "active") {
|
|
22886
|
+
const basename5 = wf.path.split("/").pop() ?? wf.path;
|
|
22887
|
+
disabled.push(`${repo}/${basename5.replace(/\.ya?ml$/, "")}`);
|
|
22888
|
+
continue;
|
|
22889
|
+
}
|
|
22877
22890
|
try {
|
|
22878
22891
|
const contents = await client.rest(
|
|
22879
22892
|
"GET",
|
|
@@ -22892,7 +22905,7 @@ async function repoWorkflowEntries(client, repo) {
|
|
|
22892
22905
|
else throw e;
|
|
22893
22906
|
}
|
|
22894
22907
|
}
|
|
22895
|
-
return { entries, failures, workflows };
|
|
22908
|
+
return { entries, failures, workflows, disabled };
|
|
22896
22909
|
}
|
|
22897
22910
|
async function githubEntries(client) {
|
|
22898
22911
|
const entries = [];
|
|
@@ -22900,12 +22913,13 @@ async function githubEntries(client) {
|
|
|
22900
22913
|
const drift = [];
|
|
22901
22914
|
const readRepos = [];
|
|
22902
22915
|
const workflows = [];
|
|
22916
|
+
const disabledWorkflowNames = [];
|
|
22903
22917
|
let repos;
|
|
22904
22918
|
try {
|
|
22905
22919
|
const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
|
|
22906
22920
|
repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
|
|
22907
22921
|
} catch (e) {
|
|
22908
|
-
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [] };
|
|
22922
|
+
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [], disabledWorkflowNames: [] };
|
|
22909
22923
|
}
|
|
22910
22924
|
const results = await Promise.all(
|
|
22911
22925
|
repos.map(async (repo) => {
|
|
@@ -22922,6 +22936,7 @@ async function githubEntries(client) {
|
|
|
22922
22936
|
readRepos.push(r.repo);
|
|
22923
22937
|
entries.push(...r.entries);
|
|
22924
22938
|
workflows.push(...r.workflows);
|
|
22939
|
+
disabledWorkflowNames.push(...r.disabled);
|
|
22925
22940
|
if (r.failures.length) {
|
|
22926
22941
|
drift.push(`${r.repo}: ${r.failures.length} workflow record(s) listed active with no file on the default branch (deleted one-offs?) \u2014 deregister them: ${r.failures.map((f) => f.split(": ")[1]).join(", ")}`);
|
|
22927
22942
|
}
|
|
@@ -22929,7 +22944,7 @@ async function githubEntries(client) {
|
|
|
22929
22944
|
}
|
|
22930
22945
|
const reconciliation = [...strayCronDrifts(workflows), ...unlauncheredLlmDrifts(entries)];
|
|
22931
22946
|
drift.push(...reconciliation.map(renderDrift));
|
|
22932
|
-
return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name) };
|
|
22947
|
+
return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name), disabledWorkflowNames };
|
|
22933
22948
|
}
|
|
22934
22949
|
async function awsJson(args) {
|
|
22935
22950
|
const run = async () => {
|
|
@@ -22984,7 +22999,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
|
|
|
22984
22999
|
// #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
|
|
22985
23000
|
awsEntries: aws.entries,
|
|
22986
23001
|
schedulerRead: Boolean(aws.schedulerRead)
|
|
22987
|
-
});
|
|
23002
|
+
}, new Set(gh.disabledWorkflowNames));
|
|
22988
23003
|
const selfManaged = /* @__PURE__ */ new Set();
|
|
22989
23004
|
for (const proj of projects ?? []) {
|
|
22990
23005
|
if (proj?.schedulesMode !== "self-managed") continue;
|
|
@@ -23084,7 +23099,7 @@ function registerSchedulesCommands(program3) {
|
|
|
23084
23099
|
|
|
23085
23100
|
// src/file-lock.ts
|
|
23086
23101
|
var import_promises5 = require("node:fs/promises");
|
|
23087
|
-
var
|
|
23102
|
+
var import_node_path27 = require("node:path");
|
|
23088
23103
|
var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
23089
23104
|
var IMMEDIATE_RETRY_BUDGET = 3;
|
|
23090
23105
|
var FileLockBusyError = class extends Error {
|
|
@@ -23169,7 +23184,7 @@ async function releaseFileLock(lockPath, guard) {
|
|
|
23169
23184
|
}
|
|
23170
23185
|
async function withFileLock(lockPath, opts, fn) {
|
|
23171
23186
|
const resolved = resolveFileLockOpts(opts);
|
|
23172
|
-
await (0, import_promises5.mkdir)((0,
|
|
23187
|
+
await (0, import_promises5.mkdir)((0, import_node_path27.dirname)(lockPath), { recursive: true }).catch(() => void 0);
|
|
23173
23188
|
const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
|
|
23174
23189
|
try {
|
|
23175
23190
|
return await fn();
|
|
@@ -23180,7 +23195,7 @@ async function withFileLock(lockPath, opts, fn) {
|
|
|
23180
23195
|
|
|
23181
23196
|
// src/schedules-lift-command.ts
|
|
23182
23197
|
var import_promises6 = require("node:fs/promises");
|
|
23183
|
-
var
|
|
23198
|
+
var import_node_path28 = require("node:path");
|
|
23184
23199
|
|
|
23185
23200
|
// src/schedules-lift.ts
|
|
23186
23201
|
var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
|
|
@@ -23286,7 +23301,7 @@ async function readWorkflowFiles(dir) {
|
|
|
23286
23301
|
const files = [];
|
|
23287
23302
|
for (const name of names.sort()) {
|
|
23288
23303
|
if (!/\.ya?ml$/.test(name)) continue;
|
|
23289
|
-
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises6.readFile)((0,
|
|
23304
|
+
files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises6.readFile)((0, import_node_path28.join)(dir, name), "utf8") });
|
|
23290
23305
|
}
|
|
23291
23306
|
return files;
|
|
23292
23307
|
}
|
|
@@ -23828,11 +23843,12 @@ function registerQueryCommands(program3) {
|
|
|
23828
23843
|
}
|
|
23829
23844
|
|
|
23830
23845
|
// src/bootstrap-commands.ts
|
|
23831
|
-
var
|
|
23846
|
+
var import_node_fs31 = require("node:fs");
|
|
23832
23847
|
var import_node_os11 = require("node:os");
|
|
23833
|
-
var
|
|
23848
|
+
var import_node_path29 = require("node:path");
|
|
23834
23849
|
|
|
23835
23850
|
// src/bootstrap-drift.ts
|
|
23851
|
+
var import_node_crypto6 = require("node:crypto");
|
|
23836
23852
|
function byteComparableSeeds(manifest, cls) {
|
|
23837
23853
|
return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
|
|
23838
23854
|
}
|
|
@@ -23841,6 +23857,9 @@ function compareSeedBytes(hubContent, repoContent) {
|
|
|
23841
23857
|
const normalize = (s) => s.replace(/\r\n/g, "\n");
|
|
23842
23858
|
return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
|
|
23843
23859
|
}
|
|
23860
|
+
function seedContentHash(content) {
|
|
23861
|
+
return (0, import_node_crypto6.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
|
|
23862
|
+
}
|
|
23844
23863
|
function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
|
|
23845
23864
|
const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
|
|
23846
23865
|
const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
|
|
@@ -23863,11 +23882,14 @@ function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
|
|
|
23863
23882
|
findings.push({ repo, target: seed.target, state: "waived", detail: `${state} \u2014 waived: ${why}` });
|
|
23864
23883
|
continue;
|
|
23865
23884
|
}
|
|
23885
|
+
const repoContent = byTarget.get(seed.target);
|
|
23866
23886
|
findings.push({
|
|
23867
23887
|
repo,
|
|
23868
23888
|
target: seed.target,
|
|
23869
23889
|
state,
|
|
23870
|
-
detail: state === "absent" ? "declared org-owned in the manifest but not present on the base branch" : "differs from MMI-Hub's copy \u2014 propagate
|
|
23890
|
+
detail: state === "absent" ? "declared org-owned in the manifest but not present on the base branch" : "differs from MMI-Hub's copy \u2014 fix this in MMI-Hub and let it propagate (#4233); `mmi-cli bootstrap apply <repo> --only <target> --execute` remains for THIS repo's own bootstrap/onboarding, never a fleet-wide fan-out (#4241 fails a hand-edited fleet copy at the merge gate on repos carrying the new seed), or, if this repo is RIGHT to differ, declare a waiver for it on the seed in the manifest (#3842)",
|
|
23891
|
+
// #4242: only a real 'drift' has bytes worth hashing — 'absent' has none to compare against history.
|
|
23892
|
+
...state === "drift" && repoContent != null ? { contentHash: seedContentHash(repoContent) } : {}
|
|
23871
23893
|
});
|
|
23872
23894
|
}
|
|
23873
23895
|
return findings;
|
|
@@ -23886,6 +23908,115 @@ function renderSeedDriftReport(findings, reposAudited, seedsPerRepo) {
|
|
|
23886
23908
|
return lines.join("\n");
|
|
23887
23909
|
}
|
|
23888
23910
|
|
|
23911
|
+
// src/bootstrap-propagate.ts
|
|
23912
|
+
function assertPropagationCoverage(rosterCount, independentRegistryCount) {
|
|
23913
|
+
if (rosterCount === 0) {
|
|
23914
|
+
throw new Error("bootstrap propagate: roster read 0 repos \u2014 refusing to report a plan from a roster this command could not establish (#4232 coverage invariant)");
|
|
23915
|
+
}
|
|
23916
|
+
if (rosterCount < independentRegistryCount) {
|
|
23917
|
+
throw new Error(
|
|
23918
|
+
`bootstrap propagate: roster read ${rosterCount} repo(s) but the independent registry count is ${independentRegistryCount} \u2014 this read did not cover the fleet (#4232 coverage invariant)`
|
|
23919
|
+
);
|
|
23920
|
+
}
|
|
23921
|
+
}
|
|
23922
|
+
function assignWaves(repos, canarySlug) {
|
|
23923
|
+
const waves = /* @__PURE__ */ new Map();
|
|
23924
|
+
if (!canarySlug) return waves;
|
|
23925
|
+
const rest = repos.filter((r) => r.slug !== canarySlug).slice().sort((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
|
|
23926
|
+
const canary = repos.find((r) => r.slug === canarySlug);
|
|
23927
|
+
if (canary) waves.set(canary.repo, 0);
|
|
23928
|
+
const wave1Count = Math.ceil(rest.length * 0.25);
|
|
23929
|
+
rest.forEach((r, i) => waves.set(r.repo, i < wave1Count ? 1 : 2));
|
|
23930
|
+
return waves;
|
|
23931
|
+
}
|
|
23932
|
+
function statusFor(read) {
|
|
23933
|
+
if (!read) return { status: "pending", record: {} };
|
|
23934
|
+
if (read.drift === "match") return { status: "match", record: { mergeSha: read.pr?.mergeSha } };
|
|
23935
|
+
const pr2 = read.pr;
|
|
23936
|
+
if (!pr2) return { status: "pending", record: {} };
|
|
23937
|
+
if (pr2.state === "closed") return { status: "closed-unmerged", record: { prNumber: pr2.number, prUrl: pr2.url } };
|
|
23938
|
+
if (pr2.checks === "red") return { status: "red", record: { prNumber: pr2.number, prUrl: pr2.url } };
|
|
23939
|
+
return { status: "open-pending", record: { prNumber: pr2.number, prUrl: pr2.url, mergeSha: pr2.mergeSha } };
|
|
23940
|
+
}
|
|
23941
|
+
var HALTING_STATUSES = /* @__PURE__ */ new Set(["red", "closed-unmerged"]);
|
|
23942
|
+
function planPropagationTick(input) {
|
|
23943
|
+
const { target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied } = input;
|
|
23944
|
+
const readByRepo = new Map(reads.map((r) => [r.repo, r]));
|
|
23945
|
+
const records = [];
|
|
23946
|
+
const opened = [];
|
|
23947
|
+
const waived = repos.filter((r) => r.waiver);
|
|
23948
|
+
for (const r of waived) {
|
|
23949
|
+
records.push({ repo: r.repo, target, wave: null, status: "skipped-waived", action: "none", detail: `waived: ${r.waiver}` });
|
|
23950
|
+
}
|
|
23951
|
+
const eligible = repos.filter((r) => !r.waiver);
|
|
23952
|
+
const refusedNoCanary = !canarySlug || !eligible.some((r) => r.slug === canarySlug);
|
|
23953
|
+
if (refusedNoCanary) {
|
|
23954
|
+
for (const r of eligible) {
|
|
23955
|
+
records.push({ repo: r.repo, target, wave: null, status: "not-yet-reached", action: "none", detail: "no canary declared for this target \u2014 refusing to plan any wave (#4238 canary requirement)" });
|
|
23956
|
+
}
|
|
23957
|
+
return { target, reposInScope: repos.length, canary: null, refusedNoCanary: true, halted: false, haltReason: null, opened, records };
|
|
23958
|
+
}
|
|
23959
|
+
const waveOf = assignWaves(eligible, canarySlug);
|
|
23960
|
+
const byWave = { 0: [], 1: [], 2: [] };
|
|
23961
|
+
for (const r of eligible) byWave[waveOf.get(r.repo) ?? 2].push(r);
|
|
23962
|
+
let halted = false;
|
|
23963
|
+
let haltReason = null;
|
|
23964
|
+
let waveGateOpen = true;
|
|
23965
|
+
for (const waveNum of [0, 1, 2]) {
|
|
23966
|
+
const waveRepos = byWave[waveNum];
|
|
23967
|
+
if (!waveRepos.length) continue;
|
|
23968
|
+
if (!waveGateOpen || halted) {
|
|
23969
|
+
for (const r of waveRepos) {
|
|
23970
|
+
records.push({ repo: r.repo, target, wave: waveNum, status: "not-yet-reached", action: "none", detail: "prior wave not yet converged" });
|
|
23971
|
+
}
|
|
23972
|
+
continue;
|
|
23973
|
+
}
|
|
23974
|
+
let waveAllMatch = true;
|
|
23975
|
+
let waveHasRed = false;
|
|
23976
|
+
for (const r of waveRepos) {
|
|
23977
|
+
const { status, record } = statusFor(readByRepo.get(r.repo));
|
|
23978
|
+
const shouldOpen = status === "pending";
|
|
23979
|
+
if (shouldOpen) opened.push(r.repo);
|
|
23980
|
+
records.push({
|
|
23981
|
+
repo: r.repo,
|
|
23982
|
+
target,
|
|
23983
|
+
wave: waveNum,
|
|
23984
|
+
status,
|
|
23985
|
+
action: shouldOpen ? "open-pr" : "none",
|
|
23986
|
+
detail: shouldOpen ? "no open propagation PR and not yet matching \u2014 opening this tick" : status,
|
|
23987
|
+
...record
|
|
23988
|
+
});
|
|
23989
|
+
if (status !== "match") waveAllMatch = false;
|
|
23990
|
+
if (HALTING_STATUSES.has(status)) waveHasRed = true;
|
|
23991
|
+
}
|
|
23992
|
+
if (waveHasRed) {
|
|
23993
|
+
halted = true;
|
|
23994
|
+
haltReason = `wave ${waveNum} has a red or closed-unmerged PR \u2014 halting; no further wave opens (#4238 halt-and-alarm)`;
|
|
23995
|
+
waveGateOpen = false;
|
|
23996
|
+
continue;
|
|
23997
|
+
}
|
|
23998
|
+
if (waveNum === 0 && isWorkflowSeed && !functionGateSatisfied) {
|
|
23999
|
+
waveGateOpen = false;
|
|
24000
|
+
continue;
|
|
24001
|
+
}
|
|
24002
|
+
waveGateOpen = waveAllMatch;
|
|
24003
|
+
}
|
|
24004
|
+
return { target, reposInScope: repos.length, canary: canarySlug, refusedNoCanary: false, halted, haltReason, opened, records };
|
|
24005
|
+
}
|
|
24006
|
+
function renderPropagationReport(plan) {
|
|
24007
|
+
const lines = [`bootstrap propagate \u2014 target ${plan.target}: ${plan.reposInScope} repo(s) in scope, canary=${plan.canary ?? "NONE"}`];
|
|
24008
|
+
if (plan.refusedNoCanary) {
|
|
24009
|
+
lines.push(" REFUSED \u2014 no canary declared for this target; plan every repo not-yet-reached");
|
|
24010
|
+
return lines.join("\n");
|
|
24011
|
+
}
|
|
24012
|
+
for (const r of plan.records) {
|
|
24013
|
+
const wave2 = r.wave == null ? "-" : String(r.wave);
|
|
24014
|
+
lines.push(` wave${wave2.padEnd(2)} ${r.status.padEnd(16)} ${r.repo}${r.prNumber ? ` PR#${r.prNumber}` : ""} \u2014 ${r.detail}`);
|
|
24015
|
+
}
|
|
24016
|
+
lines.push(plan.halted ? ` HALTED \u2014 ${plan.haltReason}` : ` opened this tick: ${plan.opened.length ? plan.opened.join(", ") : "(none)"}`);
|
|
24017
|
+
return lines.join("\n");
|
|
24018
|
+
}
|
|
24019
|
+
|
|
23889
24020
|
// src/bootstrap-verify.ts
|
|
23890
24021
|
var TRAIN_BRANCHES2 = ["development", "rc", "main"];
|
|
23891
24022
|
var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
|
|
@@ -24102,6 +24233,11 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
24102
24233
|
const repoInfo = await restJson3(deps, `repos/${repo}`, {});
|
|
24103
24234
|
checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
|
|
24104
24235
|
checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
|
|
24236
|
+
checks.push({
|
|
24237
|
+
ok: repoInfo.has_wiki === false,
|
|
24238
|
+
label: "has_wiki disabled",
|
|
24239
|
+
detail: repoInfo.has_wiki === false ? void 0 : `wikis are retired org-wide \u2014 gh api -X PATCH repos/${repo} -F has_wiki=false`
|
|
24240
|
+
});
|
|
24105
24241
|
const branchList = await restPagedJson2(deps, `repos/${repo}/branches`, []);
|
|
24106
24242
|
const branchNames = new Set(branchList.map((b) => b.name));
|
|
24107
24243
|
for (const branch of branchesWanted) {
|
|
@@ -24543,13 +24679,13 @@ function registerBootstrapCommands(program3) {
|
|
|
24543
24679
|
client: defaultGitHubClient(),
|
|
24544
24680
|
projectMeta: meta,
|
|
24545
24681
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
24546
|
-
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0,
|
|
24682
|
+
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs31.existsSync)(path2) ? (0, import_node_fs31.readFileSync)(path2, "utf8") : null,
|
|
24547
24683
|
// requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
|
|
24548
24684
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
24549
24685
|
// #3689: the same committed map the org access audit reads (#3664), so a sanctioned admin is not a
|
|
24550
24686
|
// permanent bootstrap failure on one surface and an intended state on the other. Absent file → no
|
|
24551
24687
|
// sanction, which is the pre-#3664 behaviour.
|
|
24552
|
-
sanctionedAdmins: (0,
|
|
24688
|
+
sanctionedAdmins: (0, import_node_fs31.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs31.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
|
|
24553
24689
|
requiredGcpApis: (() => {
|
|
24554
24690
|
const v = meta?.requiredGcpApis;
|
|
24555
24691
|
if (Array.isArray(v)) return v;
|
|
@@ -24602,12 +24738,12 @@ function registerBootstrapCommands(program3) {
|
|
|
24602
24738
|
bootstrap.command("drift").description("#3818: compare every org-owned whole-file seed against MMI-Hub's copy across the registry roster; read-only").option("--repo <owner/repo>", "audit one repo instead of the roster (never a fleet verdict)").option("--json", "machine-readable output").action(async () => {
|
|
24603
24739
|
const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
|
|
24604
24740
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
24605
|
-
if (!(0,
|
|
24606
|
-
const manifest = loadBootstrapSeeds((0,
|
|
24741
|
+
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
|
|
24742
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
24607
24743
|
const hubContents = /* @__PURE__ */ new Map();
|
|
24608
24744
|
for (const s of manifest.seeds) {
|
|
24609
24745
|
if (s.ownership !== "org" || s.source !== "self") continue;
|
|
24610
|
-
hubContents.set(s.target, (0,
|
|
24746
|
+
hubContents.set(s.target, (0, import_node_fs31.existsSync)(s.target) ? (0, import_node_fs31.readFileSync)(s.target, "utf8") : null);
|
|
24611
24747
|
}
|
|
24612
24748
|
let targets;
|
|
24613
24749
|
let classOf = (_repo) => "deployable";
|
|
@@ -24684,8 +24820,8 @@ function registerBootstrapCommands(program3) {
|
|
|
24684
24820
|
return fail(`bootstrap apply: ${e.message}`);
|
|
24685
24821
|
}
|
|
24686
24822
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
24687
|
-
if (!(0,
|
|
24688
|
-
const manifest = loadBootstrapSeeds((0,
|
|
24823
|
+
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
|
|
24824
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
24689
24825
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
24690
24826
|
const slug = parsedRepo.slug;
|
|
24691
24827
|
const onlyTarget = o.only.trim();
|
|
@@ -24696,16 +24832,16 @@ function registerBootstrapCommands(program3) {
|
|
|
24696
24832
|
${known}`);
|
|
24697
24833
|
}
|
|
24698
24834
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
24699
|
-
const readFile9 = (p) => (0,
|
|
24835
|
+
const readFile9 = (p) => (0, import_node_fs31.existsSync)(p) ? (0, import_node_fs31.readFileSync)(p, "utf8") : null;
|
|
24700
24836
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
24701
24837
|
const putSeed = async (target, content, ref, sha) => {
|
|
24702
|
-
const tmp = (0,
|
|
24703
|
-
(0,
|
|
24838
|
+
const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
24839
|
+
(0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
|
|
24704
24840
|
try {
|
|
24705
24841
|
await gh(contentPutInputArgs(repo, target, tmp));
|
|
24706
24842
|
} finally {
|
|
24707
24843
|
try {
|
|
24708
|
-
(0,
|
|
24844
|
+
(0, import_node_fs31.unlinkSync)(tmp);
|
|
24709
24845
|
} catch {
|
|
24710
24846
|
}
|
|
24711
24847
|
}
|
|
@@ -24879,6 +25015,14 @@ function registerBootstrapCommands(program3) {
|
|
|
24879
25015
|
});
|
|
24880
25016
|
applied.push(autoMergeEnabled ? `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)` : `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge refused \u2014 PR is already clean. Land it: mmi-cli pr land <n> --repo ${repo})`);
|
|
24881
25017
|
}
|
|
25018
|
+
if (o.execute && !onlyTarget) {
|
|
25019
|
+
try {
|
|
25020
|
+
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-F", "has_wiki=false"]);
|
|
25021
|
+
applied.push("has_wiki=false (wikis retired org-wide)");
|
|
25022
|
+
} catch (e) {
|
|
25023
|
+
applied.push(`has_wiki=false (failed: ${e.message})`);
|
|
25024
|
+
}
|
|
25025
|
+
}
|
|
24882
25026
|
if (o.execute && !onlyTarget && o.class === "deployable") {
|
|
24883
25027
|
try {
|
|
24884
25028
|
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]);
|
|
@@ -24959,15 +25103,183 @@ LIVE apply to ${repo}:
|
|
|
24959
25103
|
${applied.join("\n ")}`);
|
|
24960
25104
|
}
|
|
24961
25105
|
});
|
|
25106
|
+
bootstrap.command("propagate").description("#4238: re-entrant canary\u2192wave tick \u2014 plan (or, with --execute, open) per-repo PRs fanning an org-owned seed out to the fleet").option("--target <path>", "the manifest target to propagate (an ownership:org + source:self seed, e.g. .github/workflows/agent-pr.yml)").option("--execute", "LIVE tick via gh (master-gated) \u2014 opens/reuses per-repo seed-propagate PRs; dry-run prints the plan only").option("--json", "machine-readable output").action(async () => {
|
|
25107
|
+
const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
|
|
25108
|
+
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
25109
|
+
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
|
|
25110
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
25111
|
+
const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
|
|
25112
|
+
if (!o.target) {
|
|
25113
|
+
return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
|
|
25114
|
+
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
25115
|
+
}
|
|
25116
|
+
const seed = propagatable.find((s) => s.target === o.target);
|
|
25117
|
+
if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable targets:
|
|
25118
|
+
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
25119
|
+
if (!(0, import_node_fs31.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
|
|
25120
|
+
const hubContent = (0, import_node_fs31.readFileSync)(seed.target, "utf8");
|
|
25121
|
+
const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
|
|
25122
|
+
const cfg = await loadConfig();
|
|
25123
|
+
const projects = await fetchProjectsList(registryClientDeps(cfg));
|
|
25124
|
+
if (!projects || projects.length === 0) {
|
|
25125
|
+
return failGraceful("bootstrap propagate: the registry roster is unreadable or empty \u2014 refusing to plan a tick from a scope this command could not establish (#4232 coverage invariant)");
|
|
25126
|
+
}
|
|
25127
|
+
const rosterRepos2 = collectRegistryRepos(projects).filter((r) => r.toLowerCase() !== "mutmutco/mmi-hub");
|
|
25128
|
+
let independentCount = rosterRepos2.length;
|
|
25129
|
+
if ((0, import_node_fs31.existsSync)("projects.json")) {
|
|
25130
|
+
try {
|
|
25131
|
+
const local = JSON.parse((0, import_node_fs31.readFileSync)("projects.json", "utf8"));
|
|
25132
|
+
const localRepos = /* @__PURE__ */ new Set();
|
|
25133
|
+
for (const p of local.projects ?? []) for (const r of p.repos ?? []) {
|
|
25134
|
+
const full = (r.includes("/") ? r : `mutmutco/${r}`).toLowerCase();
|
|
25135
|
+
if (full !== "mutmutco/mmi-hub") localRepos.add(full);
|
|
25136
|
+
}
|
|
25137
|
+
if (localRepos.size > 0) independentCount = localRepos.size;
|
|
25138
|
+
} catch {
|
|
25139
|
+
}
|
|
25140
|
+
}
|
|
25141
|
+
try {
|
|
25142
|
+
assertPropagationCoverage(rosterRepos2.length, independentCount);
|
|
25143
|
+
} catch (e) {
|
|
25144
|
+
return fail(e.message);
|
|
25145
|
+
}
|
|
25146
|
+
const bySlugMeta = new Map(projects.flatMap((p) => (p.repos ?? []).map((r) => [(r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p])));
|
|
25147
|
+
const classOf = (repo) => bySlugMeta.get(repo.toLowerCase())?.class ?? "deployable";
|
|
25148
|
+
const canaryProject = projects.find((p) => p.seedCanary === true);
|
|
25149
|
+
const canarySlug = canaryProject ? (canaryProject.repos ?? [])[0]?.split("/").pop()?.toLowerCase() ?? null : null;
|
|
25150
|
+
const repos = rosterRepos2.map((repo) => {
|
|
25151
|
+
const slug = repo.split("/").pop().toLowerCase();
|
|
25152
|
+
const waiver = seed.waivers?.[slug];
|
|
25153
|
+
return { repo, slug, waiver };
|
|
25154
|
+
});
|
|
25155
|
+
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
25156
|
+
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
25157
|
+
const branchPrefix = "seed-propagate";
|
|
25158
|
+
const reads = [];
|
|
25159
|
+
for (const r of repos) {
|
|
25160
|
+
if (r.waiver) continue;
|
|
25161
|
+
const baseBranch = classOf(r.repo) === "content" ? "main" : "development";
|
|
25162
|
+
let content = null;
|
|
25163
|
+
try {
|
|
25164
|
+
const resp = await gh(["api", `repos/${r.repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
|
|
25165
|
+
const parsed = JSON.parse(resp.stdout);
|
|
25166
|
+
content = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
|
|
25167
|
+
} catch {
|
|
25168
|
+
content = null;
|
|
25169
|
+
}
|
|
25170
|
+
const drift = compareSeedBytes(hubContent, content);
|
|
25171
|
+
let pr2;
|
|
25172
|
+
try {
|
|
25173
|
+
const branch = `${branchPrefix}-${r.slug}`;
|
|
25174
|
+
const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit", "--limit", "1"]);
|
|
25175
|
+
const arr = JSON.parse(listed.stdout || "[]");
|
|
25176
|
+
const p = arr[0];
|
|
25177
|
+
if (p) {
|
|
25178
|
+
const rollup = p.statusCheckRollup ?? [];
|
|
25179
|
+
const checks = rollup.length === 0 ? "none" : rollup.some((c) => c.conclusion === "FAILURE" || c.state === "FAILURE") ? "red" : rollup.every((c) => c.conclusion === "SUCCESS" || c.state === "SUCCESS") ? "success" : "pending";
|
|
25180
|
+
pr2 = { number: p.number, url: p.url, state: p.state === "MERGED" ? "merged" : p.state === "CLOSED" ? "closed" : "open", checks, mergeSha: p.mergeCommit?.oid };
|
|
25181
|
+
}
|
|
25182
|
+
} catch {
|
|
25183
|
+
pr2 = void 0;
|
|
25184
|
+
}
|
|
25185
|
+
reads.push({ repo: r.repo, drift: drift === "waived" ? "match" : drift, pr: pr2 });
|
|
25186
|
+
}
|
|
25187
|
+
let functionGateSatisfied = !isWorkflowSeed;
|
|
25188
|
+
if (isWorkflowSeed && canarySlug) {
|
|
25189
|
+
const canaryRepo = repos.find((r) => r.slug === canarySlug)?.repo;
|
|
25190
|
+
const canaryRead = reads.find((r) => r.repo === canaryRepo);
|
|
25191
|
+
if (canaryRead?.drift === "match") {
|
|
25192
|
+
try {
|
|
25193
|
+
const workflowFile = seed.target.split("/").pop();
|
|
25194
|
+
const runs = await gh(["api", `repos/${canaryRepo}/actions/workflows/${workflowFile}/runs?status=success&per_page=1`]);
|
|
25195
|
+
const parsed = JSON.parse(runs.stdout);
|
|
25196
|
+
functionGateSatisfied = Array.isArray(parsed.workflow_runs) && parsed.workflow_runs.length > 0;
|
|
25197
|
+
} catch {
|
|
25198
|
+
functionGateSatisfied = false;
|
|
25199
|
+
}
|
|
25200
|
+
}
|
|
25201
|
+
}
|
|
25202
|
+
const plan = planPropagationTick({ target: seed.target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied });
|
|
25203
|
+
if (o.execute) {
|
|
25204
|
+
if (plan.refusedNoCanary) return fail("bootstrap propagate --execute: no canary declared for this target \u2014 refusing to write (set seedCanary:true on exactly one registry repo)");
|
|
25205
|
+
const headSha = (await gh(["api", "repos/mutmutco/MMI-Hub/commits/development", "--jq", ".sha"])).stdout.trim();
|
|
25206
|
+
for (const rec of plan.records) {
|
|
25207
|
+
if (rec.action !== "open-pr") continue;
|
|
25208
|
+
const repoEntry = repos.find((r) => r.repo === rec.repo);
|
|
25209
|
+
const baseBranch = classOf(rec.repo) === "content" ? "main" : "development";
|
|
25210
|
+
const branch = `${branchPrefix}-${repoEntry.slug}`;
|
|
25211
|
+
const baseRef = await gh(["api", `repos/${rec.repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
|
|
25212
|
+
const baseSha = baseRef.stdout.trim();
|
|
25213
|
+
let branchExists = true;
|
|
25214
|
+
try {
|
|
25215
|
+
await gh(["api", `repos/${rec.repo}/git/ref/heads/${branch}`]);
|
|
25216
|
+
} catch {
|
|
25217
|
+
branchExists = false;
|
|
25218
|
+
}
|
|
25219
|
+
if (!branchExists) await gh(["api", `repos/${rec.repo}/git/refs`, "-f", `ref=refs/heads/${branch}`, "-f", `sha=${baseSha}`]);
|
|
25220
|
+
let existingSha;
|
|
25221
|
+
try {
|
|
25222
|
+
const cur = await gh(["api", `repos/${rec.repo}/contents/${enc(seed.target)}?ref=${branch}`]);
|
|
25223
|
+
existingSha = JSON.parse(cur.stdout).sha;
|
|
25224
|
+
} catch {
|
|
25225
|
+
existingSha = void 0;
|
|
25226
|
+
}
|
|
25227
|
+
const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
25228
|
+
(0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, hubContent, branch, existingSha)), "utf8");
|
|
25229
|
+
try {
|
|
25230
|
+
await gh(contentPutInputArgs(rec.repo, seed.target, tmp));
|
|
25231
|
+
} finally {
|
|
25232
|
+
try {
|
|
25233
|
+
(0, import_node_fs31.unlinkSync)(tmp);
|
|
25234
|
+
} catch {
|
|
25235
|
+
}
|
|
25236
|
+
}
|
|
25237
|
+
const openPrs = await gh(["pr", "list", "--repo", rec.repo, "--head", branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
25238
|
+
const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
25239
|
+
let prUrl;
|
|
25240
|
+
if (prDecision.action === "reuse") {
|
|
25241
|
+
prUrl = prDecision.url;
|
|
25242
|
+
} else {
|
|
25243
|
+
const created = await ghCreate([
|
|
25244
|
+
"pr",
|
|
25245
|
+
"create",
|
|
25246
|
+
"--repo",
|
|
25247
|
+
rec.repo,
|
|
25248
|
+
"--base",
|
|
25249
|
+
baseBranch,
|
|
25250
|
+
"--head",
|
|
25251
|
+
branch,
|
|
25252
|
+
"--title",
|
|
25253
|
+
`chore: propagate org-owned ${seed.target} from MMI-Hub (wave ${rec.wave})`,
|
|
25254
|
+
"--body",
|
|
25255
|
+
`Auto-opened by \`mmi-cli bootstrap propagate --target ${seed.target} --execute\` (#4238), wave ${rec.wave}.
|
|
25256
|
+
|
|
25257
|
+
Propagates MMI-Hub@${headSha} 's copy of \`${seed.target}\` to this repo \u2014 the file this PR carries and nothing else.
|
|
25258
|
+
|
|
25259
|
+
Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --execute\` (#4240) reverts this PR's merge commit; never re-run propagate with old bytes.`
|
|
25260
|
+
]);
|
|
25261
|
+
prUrl = created.url;
|
|
25262
|
+
}
|
|
25263
|
+
if (rec.wave !== 0) {
|
|
25264
|
+
await gh(["pr", "merge", prUrl, "--repo", rec.repo, "--auto", "--squash"]).catch(() => {
|
|
25265
|
+
});
|
|
25266
|
+
}
|
|
25267
|
+
rec.prUrl = prUrl;
|
|
25268
|
+
}
|
|
25269
|
+
}
|
|
25270
|
+
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25271
|
+
else console.log(renderPropagationReport(plan));
|
|
25272
|
+
if (plan.halted) process.exitCode = 1;
|
|
25273
|
+
});
|
|
24962
25274
|
}
|
|
24963
25275
|
|
|
24964
25276
|
// src/stage-commands.ts
|
|
24965
|
-
var
|
|
24966
|
-
var
|
|
25277
|
+
var import_node_fs33 = require("node:fs");
|
|
25278
|
+
var import_node_path31 = require("node:path");
|
|
24967
25279
|
|
|
24968
25280
|
// src/port-registry.ts
|
|
24969
|
-
var
|
|
24970
|
-
var
|
|
25281
|
+
var import_node_fs32 = require("node:fs");
|
|
25282
|
+
var import_node_path30 = require("node:path");
|
|
24971
25283
|
|
|
24972
25284
|
// ../infra/port-geometry.mjs
|
|
24973
25285
|
var PORT_BLOCK = 100;
|
|
@@ -24981,8 +25293,8 @@ function nextPortBlock(registry2) {
|
|
|
24981
25293
|
return [base, base + PORT_SPAN];
|
|
24982
25294
|
}
|
|
24983
25295
|
function loadPortRegistry(path2) {
|
|
24984
|
-
if (!(0,
|
|
24985
|
-
const raw = JSON.parse((0,
|
|
25296
|
+
if (!(0, import_node_fs32.existsSync)(path2)) return {};
|
|
25297
|
+
const raw = JSON.parse((0, import_node_fs32.readFileSync)(path2, "utf8"));
|
|
24986
25298
|
const out = {};
|
|
24987
25299
|
for (const [key, value] of Object.entries(raw)) {
|
|
24988
25300
|
if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
|
|
@@ -24996,9 +25308,9 @@ function ensurePortRange(repo, path2) {
|
|
|
24996
25308
|
const existing = registry2[repo];
|
|
24997
25309
|
if (existing) return existing;
|
|
24998
25310
|
const range = nextPortBlock(registry2);
|
|
24999
|
-
const raw = (0,
|
|
25311
|
+
const raw = (0, import_node_fs32.existsSync)(path2) ? JSON.parse((0, import_node_fs32.readFileSync)(path2, "utf8")) : {};
|
|
25000
25312
|
raw[repo] = range;
|
|
25001
|
-
(0,
|
|
25313
|
+
(0, import_node_fs32.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
|
|
25002
25314
|
return range;
|
|
25003
25315
|
}
|
|
25004
25316
|
function portCursorSeed(registry2) {
|
|
@@ -25020,22 +25332,22 @@ function existingPortRange(repo, registry2) {
|
|
|
25020
25332
|
return registry2[repo] ?? null;
|
|
25021
25333
|
}
|
|
25022
25334
|
function portRangeInfraAt(root, source) {
|
|
25023
|
-
const registryPath = (0,
|
|
25024
|
-
const ddbScriptPath = (0,
|
|
25025
|
-
if (!(0,
|
|
25335
|
+
const registryPath = (0, import_node_path30.join)(root, "infra", "port-ranges.json");
|
|
25336
|
+
const ddbScriptPath = (0, import_node_path30.join)(root, "infra", "port-ddb.mjs");
|
|
25337
|
+
if (!(0, import_node_fs32.existsSync)(registryPath) || !(0, import_node_fs32.existsSync)(ddbScriptPath)) return null;
|
|
25026
25338
|
return { root, source, registryPath, ddbScriptPath };
|
|
25027
25339
|
}
|
|
25028
25340
|
function resolvePortRangeInfra(cwd, packageDir) {
|
|
25029
25341
|
const direct = portRangeInfraAt(cwd, "cwd");
|
|
25030
25342
|
if (direct) return direct;
|
|
25031
|
-
for (let dir = cwd; ; dir = (0,
|
|
25032
|
-
const sibling = portRangeInfraAt((0,
|
|
25343
|
+
for (let dir = cwd; ; dir = (0, import_node_path30.dirname)(dir)) {
|
|
25344
|
+
const sibling = portRangeInfraAt((0, import_node_path30.join)(dir, "MMI-Hub"), "sibling-hub");
|
|
25033
25345
|
if (sibling) return sibling;
|
|
25034
|
-
const parent = (0,
|
|
25346
|
+
const parent = (0, import_node_path30.dirname)(dir);
|
|
25035
25347
|
if (parent === dir) break;
|
|
25036
25348
|
}
|
|
25037
25349
|
if (packageDir) {
|
|
25038
|
-
const pkgRoot = (0,
|
|
25350
|
+
const pkgRoot = (0, import_node_path30.join)(packageDir, "..", "..");
|
|
25039
25351
|
const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
|
|
25040
25352
|
if (pkgFrom) return pkgFrom;
|
|
25041
25353
|
}
|
|
@@ -25229,8 +25541,8 @@ function registerStageCommands(program3) {
|
|
|
25229
25541
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
25230
25542
|
return decideStage({
|
|
25231
25543
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
25232
|
-
hasCompose: (0,
|
|
25233
|
-
hasEnvExample: (0,
|
|
25544
|
+
hasCompose: (0, import_node_fs33.existsSync)((0, import_node_path31.join)(process.cwd(), "docker-compose.yml")),
|
|
25545
|
+
hasEnvExample: (0, import_node_fs33.existsSync)((0, import_node_path31.join)(process.cwd(), ".env.example"))
|
|
25234
25546
|
});
|
|
25235
25547
|
}
|
|
25236
25548
|
async function fetchStageVaultEnvMerge() {
|
|
@@ -25499,6 +25811,7 @@ function registerBoardCommands(program3) {
|
|
|
25499
25811
|
force: o.force,
|
|
25500
25812
|
allowPartial: o.allowPartial
|
|
25501
25813
|
});
|
|
25814
|
+
invalidateStatuslineBoardCache();
|
|
25502
25815
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25503
25816
|
console.log(result.partial ? `Partially claimed ${result.item.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.item.ref} - In Progress (no change)` : `Claimed ${result.item.ref} - In Progress`);
|
|
25504
25817
|
} catch (e) {
|
|
@@ -25515,6 +25828,7 @@ function registerBoardCommands(program3) {
|
|
|
25515
25828
|
force: o.force,
|
|
25516
25829
|
allowPartial: o.allowPartial
|
|
25517
25830
|
});
|
|
25831
|
+
if (bulk.results.some((r) => r.claimed)) invalidateStatuslineBoardCache();
|
|
25518
25832
|
if (o.json) {
|
|
25519
25833
|
console.log(JSON.stringify(bulk.results));
|
|
25520
25834
|
} else {
|
|
@@ -25559,6 +25873,7 @@ function registerBoardCommands(program3) {
|
|
|
25559
25873
|
repo: o.repo,
|
|
25560
25874
|
allowPartial: o.allowPartial
|
|
25561
25875
|
});
|
|
25876
|
+
if (bulk.results.some((r) => r.moved)) invalidateStatuslineBoardCache();
|
|
25562
25877
|
if (o.json) {
|
|
25563
25878
|
console.log(JSON.stringify(bulk.results));
|
|
25564
25879
|
} else {
|
|
@@ -25583,6 +25898,7 @@ function registerBoardCommands(program3) {
|
|
|
25583
25898
|
if (issueRefs.length === 1) {
|
|
25584
25899
|
try {
|
|
25585
25900
|
const result = await moveBoardItem({ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo), selector: issueRefs[0], status: canonicalStatus, repo: o.repo, allowPartial: o.allowPartial });
|
|
25901
|
+
invalidateStatuslineBoardCache();
|
|
25586
25902
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25587
25903
|
console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> ${result.status}`);
|
|
25588
25904
|
} catch (e) {
|
|
@@ -25634,6 +25950,7 @@ function registerBoardCommands(program3) {
|
|
|
25634
25950
|
force: o.force,
|
|
25635
25951
|
allowPartial: o.allowPartial
|
|
25636
25952
|
});
|
|
25953
|
+
invalidateStatuslineBoardCache();
|
|
25637
25954
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25638
25955
|
console.log(result.partial ? `Partially unclaimed ${result.item.ref}: ${result.warning}` : `Unclaimed ${result.item.ref} -> ${result.status}`);
|
|
25639
25956
|
} catch (e) {
|
|
@@ -25668,11 +25985,11 @@ function registerBoardCommands(program3) {
|
|
|
25668
25985
|
}
|
|
25669
25986
|
|
|
25670
25987
|
// src/merge-cleanup.ts
|
|
25671
|
-
var
|
|
25988
|
+
var import_node_fs34 = require("node:fs");
|
|
25672
25989
|
var import_promises8 = require("node:fs/promises");
|
|
25673
|
-
var
|
|
25990
|
+
var import_node_path33 = require("node:path");
|
|
25674
25991
|
var import_node_os12 = require("node:os");
|
|
25675
|
-
var
|
|
25992
|
+
var import_node_child_process17 = require("node:child_process");
|
|
25676
25993
|
|
|
25677
25994
|
// src/board-advance.ts
|
|
25678
25995
|
function repoOf2(ref) {
|
|
@@ -25758,7 +26075,7 @@ function boardAdvanceFailureMessage(result) {
|
|
|
25758
26075
|
|
|
25759
26076
|
// src/deferred-registry-store.ts
|
|
25760
26077
|
var import_promises7 = require("node:fs/promises");
|
|
25761
|
-
var
|
|
26078
|
+
var import_node_path32 = require("node:path");
|
|
25762
26079
|
var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
25763
26080
|
async function atomicWrite(target, contents) {
|
|
25764
26081
|
const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
@@ -25809,12 +26126,12 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
|
|
|
25809
26126
|
},
|
|
25810
26127
|
// Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
|
|
25811
26128
|
write: async (entries) => {
|
|
25812
|
-
await (0, import_promises7.mkdir)((0,
|
|
26129
|
+
await (0, import_promises7.mkdir)((0, import_node_path32.dirname)(registryPath), { recursive: true });
|
|
25813
26130
|
await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
|
|
25814
26131
|
},
|
|
25815
26132
|
// Serialized read-modify-write under the repo-wide lock (#2846).
|
|
25816
26133
|
update: async (mutate) => {
|
|
25817
|
-
await (0, import_promises7.mkdir)((0,
|
|
26134
|
+
await (0, import_promises7.mkdir)((0, import_node_path32.dirname)(registryPath), { recursive: true });
|
|
25818
26135
|
const deadline = Date.now() + opts.maxWaitMs;
|
|
25819
26136
|
for (; ; ) {
|
|
25820
26137
|
const guard = await acquireLock(lockPath, opts, deadline);
|
|
@@ -25955,6 +26272,22 @@ function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
|
|
|
25955
26272
|
if (verdict.blocked) throw new Error(verdict.reason);
|
|
25956
26273
|
return housekeeping;
|
|
25957
26274
|
}
|
|
26275
|
+
async function bestEffortLeaseClose(wtPath, exec = (cmd, args) => execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })) {
|
|
26276
|
+
const step = "close jerv worktree lease";
|
|
26277
|
+
try {
|
|
26278
|
+
await exec("jerv-cli", ["lease", "close", "--ref", wtPath]);
|
|
26279
|
+
return { step, status: "done" };
|
|
26280
|
+
} catch (e) {
|
|
26281
|
+
const err = e;
|
|
26282
|
+
const detail = `${err.message}
|
|
26283
|
+
${err.stderr ?? ""}`;
|
|
26284
|
+
if (/ENOENT|not found|not recognized/i.test(detail)) {
|
|
26285
|
+
return { step, status: "skipped: jerv-cli not on PATH" };
|
|
26286
|
+
}
|
|
26287
|
+
const msg = (err.stderr?.trim() || err.message).split("\n")[0];
|
|
26288
|
+
return { step, status: `failed: ${msg}` };
|
|
26289
|
+
}
|
|
26290
|
+
}
|
|
25958
26291
|
async function applyGcPlan(plan, remote, opts = {}) {
|
|
25959
26292
|
const result = { removedBranches: [], removedRemoteBranches: [], removedTrackingRefs: [], removedWorktreeDirs: [], refused: [], failed: [], pruned: false };
|
|
25960
26293
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
@@ -25962,7 +26295,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
25962
26295
|
);
|
|
25963
26296
|
const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
25964
26297
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
25965
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
26298
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path33.dirname)((0, import_node_path33.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
25966
26299
|
const gcActor = describeActor({ env: process.env, surface: detectSurface(process.env), cwd: process.cwd() });
|
|
25967
26300
|
const owners = readWorktreeOwners(primaryRepoRoot);
|
|
25968
26301
|
const removalNow = Date.now();
|
|
@@ -25993,7 +26326,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
25993
26326
|
const cleanup = await cleanupPrMergeLocalBranch(branch.branch, {
|
|
25994
26327
|
beforeWorktrees,
|
|
25995
26328
|
startingPath: branch.worktreePath,
|
|
25996
|
-
pathExists: (p) => (0,
|
|
26329
|
+
pathExists: (p) => (0, import_node_fs34.existsSync)(p),
|
|
25997
26330
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
25998
26331
|
teardownWorktreeStage,
|
|
25999
26332
|
deferredStore,
|
|
@@ -26003,6 +26336,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
26003
26336
|
removeWorktreeDir: wtDeps.removeWorktreeDir,
|
|
26004
26337
|
removalContext: { primaryRoot: primaryRepoRoot, actor: gcActor, command: "worktree gc", force: opts.force }
|
|
26005
26338
|
});
|
|
26339
|
+
if (cleanup.worktree?.status === "removed") await bestEffortLeaseClose(cleanup.worktree.path);
|
|
26006
26340
|
return cleanup;
|
|
26007
26341
|
},
|
|
26008
26342
|
cleanupRemoteBranch: (branch, expectedHeadOid) => deleteReviewedRemoteBranch(remote, branch.branch, expectedHeadOid),
|
|
@@ -26021,7 +26355,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
26021
26355
|
let removalAttempted = false;
|
|
26022
26356
|
try {
|
|
26023
26357
|
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
|
|
26024
|
-
realpath: (path2) => (0,
|
|
26358
|
+
realpath: (path2) => (0, import_node_fs34.realpathSync)(path2)
|
|
26025
26359
|
});
|
|
26026
26360
|
if (!cleanupTarget.ok) {
|
|
26027
26361
|
result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
|
|
@@ -26049,6 +26383,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
26049
26383
|
owner,
|
|
26050
26384
|
reason: owner ? "branch merged/closed or directory dead; owner registration was stale" : "branch merged/closed or directory dead; no owner registered"
|
|
26051
26385
|
});
|
|
26386
|
+
await bestEffortLeaseClose(wt.path);
|
|
26052
26387
|
} catch (e) {
|
|
26053
26388
|
const error = e.message.split("\n")[0];
|
|
26054
26389
|
result.failed.push(`${wt.path}: ${error}`);
|
|
@@ -26107,13 +26442,13 @@ async function composeOverrideBodyFile(prNumber, repoArgs, gh) {
|
|
|
26107
26442
|
const commits = JSON.parse(raw).commits ?? [];
|
|
26108
26443
|
const body = squashBodyWithOverride(commits.map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" })), process.cwd());
|
|
26109
26444
|
if (!body) return void 0;
|
|
26110
|
-
const dir = (0,
|
|
26111
|
-
const path2 = (0,
|
|
26112
|
-
(0,
|
|
26445
|
+
const dir = (0, import_node_fs34.mkdtempSync)((0, import_node_path33.join)((0, import_node_os12.tmpdir)(), "mmi-squash-body-"));
|
|
26446
|
+
const path2 = (0, import_node_path33.join)(dir, "body.txt");
|
|
26447
|
+
(0, import_node_fs34.writeFileSync)(path2, `${body}
|
|
26113
26448
|
`, "utf8");
|
|
26114
26449
|
return { path: path2, cleanup: () => {
|
|
26115
26450
|
try {
|
|
26116
|
-
(0,
|
|
26451
|
+
(0, import_node_fs34.rmSync)(dir, { recursive: true, force: true });
|
|
26117
26452
|
} catch {
|
|
26118
26453
|
}
|
|
26119
26454
|
} };
|
|
@@ -26221,7 +26556,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
26221
26556
|
}
|
|
26222
26557
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
26223
26558
|
function spawnDeferredGcSweep() {
|
|
26224
|
-
spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn:
|
|
26559
|
+
spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process17.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
26225
26560
|
}
|
|
26226
26561
|
async function createDeferredWorktreeStore() {
|
|
26227
26562
|
try {
|
|
@@ -26235,13 +26570,13 @@ var realWorktreeDirRemover = {
|
|
|
26235
26570
|
probe: (p) => {
|
|
26236
26571
|
let st;
|
|
26237
26572
|
try {
|
|
26238
|
-
st = (0,
|
|
26573
|
+
st = (0, import_node_fs34.lstatSync)(p);
|
|
26239
26574
|
} catch {
|
|
26240
26575
|
return null;
|
|
26241
26576
|
}
|
|
26242
26577
|
if (st.isSymbolicLink()) return "link";
|
|
26243
26578
|
try {
|
|
26244
|
-
(0,
|
|
26579
|
+
(0, import_node_fs34.readlinkSync)(p);
|
|
26245
26580
|
return "link";
|
|
26246
26581
|
} catch {
|
|
26247
26582
|
}
|
|
@@ -26249,7 +26584,7 @@ var realWorktreeDirRemover = {
|
|
|
26249
26584
|
},
|
|
26250
26585
|
readdir: (p) => {
|
|
26251
26586
|
try {
|
|
26252
|
-
return (0,
|
|
26587
|
+
return (0, import_node_fs34.readdirSync)(p);
|
|
26253
26588
|
} catch {
|
|
26254
26589
|
return [];
|
|
26255
26590
|
}
|
|
@@ -26258,9 +26593,9 @@ var realWorktreeDirRemover = {
|
|
|
26258
26593
|
// leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
|
|
26259
26594
|
detachLink: (p) => {
|
|
26260
26595
|
try {
|
|
26261
|
-
(0,
|
|
26596
|
+
(0, import_node_fs34.rmdirSync)(p);
|
|
26262
26597
|
} catch {
|
|
26263
|
-
(0,
|
|
26598
|
+
(0, import_node_fs34.unlinkSync)(p);
|
|
26264
26599
|
}
|
|
26265
26600
|
},
|
|
26266
26601
|
removeTree: (p) => (0, import_promises8.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
@@ -26293,9 +26628,9 @@ async function worktreeHasStageState(worktreePath) {
|
|
|
26293
26628
|
}
|
|
26294
26629
|
}
|
|
26295
26630
|
function stageStateFileBelongsToWorktree(statePath, worktreePath) {
|
|
26296
|
-
if (!(0,
|
|
26631
|
+
if (!(0, import_node_fs34.existsSync)(statePath)) return false;
|
|
26297
26632
|
try {
|
|
26298
|
-
const state = JSON.parse((0,
|
|
26633
|
+
const state = JSON.parse((0, import_node_fs34.readFileSync)(statePath, "utf8"));
|
|
26299
26634
|
const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
|
|
26300
26635
|
return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
|
|
26301
26636
|
} catch {
|
|
@@ -26626,10 +26961,11 @@ async function checkDocsIndexAtHead(opts, deps) {
|
|
|
26626
26961
|
}
|
|
26627
26962
|
|
|
26628
26963
|
// src/worktree-lifecycle-commands.ts
|
|
26629
|
-
var
|
|
26964
|
+
var import_node_fs35 = require("node:fs");
|
|
26630
26965
|
var import_promises9 = require("node:fs/promises");
|
|
26631
|
-
var
|
|
26966
|
+
var import_node_path34 = require("node:path");
|
|
26632
26967
|
var GH_TIMEOUT_MS = 2e4;
|
|
26968
|
+
var STALE_PR_LOOKUP_LIMIT = 20;
|
|
26633
26969
|
var DEFAULT_BASE = "origin/development";
|
|
26634
26970
|
var DEFAULT_REMOTE = "origin";
|
|
26635
26971
|
var PROTECTED_BRANCHES2 = /* @__PURE__ */ new Set(["development", "main", "master", "rc"]);
|
|
@@ -26774,7 +27110,7 @@ function classifyStaleLeaks(input) {
|
|
|
26774
27110
|
var defaultOrphanDirScanDeps = {
|
|
26775
27111
|
listDirs: (root) => {
|
|
26776
27112
|
try {
|
|
26777
|
-
return (0,
|
|
27113
|
+
return (0, import_node_fs35.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path34.join)(root, e.name));
|
|
26778
27114
|
} catch {
|
|
26779
27115
|
return [];
|
|
26780
27116
|
}
|
|
@@ -26789,13 +27125,16 @@ function scanOrphanDirs(worktreesRoot, worktreeGitRoot, deps = defaultOrphanDirS
|
|
|
26789
27125
|
}
|
|
26790
27126
|
return candidates;
|
|
26791
27127
|
}
|
|
26792
|
-
function formatStaleLeaks(leaks) {
|
|
26793
|
-
|
|
26794
|
-
const lines = [`worktree list --stale: ${leaks.length} leak(s)`];
|
|
27128
|
+
function formatStaleLeaks(leaks, prLookupFailures = []) {
|
|
27129
|
+
const lines = leaks.length ? [`worktree list --stale: ${leaks.length} leak(s)`] : ["worktree list --stale: no leaks found"];
|
|
26795
27130
|
for (const leak of leaks) {
|
|
26796
27131
|
lines.push(` [${leak.kind}] ${leak.ref} \u2014 ${leak.detail}`);
|
|
26797
27132
|
lines.push(` fix: ${leak.remediation}`);
|
|
26798
27133
|
}
|
|
27134
|
+
if (prLookupFailures.length) {
|
|
27135
|
+
lines.push(` INCOMPLETE: merge state could not be read for ${prLookupFailures.length} branch(es) \u2014 they are NOT covered above`);
|
|
27136
|
+
for (const f of prLookupFailures) lines.push(` ${f.branch}${f.detail ? ` \u2014 ${f.detail}` : ""}`);
|
|
27137
|
+
}
|
|
26799
27138
|
return lines.join("\n");
|
|
26800
27139
|
}
|
|
26801
27140
|
async function repoRootOf() {
|
|
@@ -26927,13 +27266,13 @@ function registerWorktreeCommands(program3) {
|
|
|
26927
27266
|
const detached = headBorn && !symbolicBranch;
|
|
26928
27267
|
const branch = symbolicBranch || (detached ? "HEAD" : "");
|
|
26929
27268
|
if (!wtPath || !branch) return fail("worktree land: not inside a git worktree");
|
|
26930
|
-
const gitFile = (0,
|
|
26931
|
-
const isLinked = (0,
|
|
27269
|
+
const gitFile = (0, import_node_path34.join)(wtPath, ".git");
|
|
27270
|
+
const isLinked = (0, import_node_fs35.existsSync)(gitFile) && (0, import_node_fs35.statSync)(gitFile).isFile();
|
|
26932
27271
|
if (apply && !isLinked) {
|
|
26933
27272
|
return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
|
|
26934
27273
|
}
|
|
26935
27274
|
const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
26936
|
-
const primaryCheckout = commonDir ? (0,
|
|
27275
|
+
const primaryCheckout = commonDir ? (0, import_node_path34.dirname)(commonDir) : wtPath;
|
|
26937
27276
|
const localBranchNames = await execFileP2("git", ["-C", primaryCheckout, "for-each-ref", "--format=%(refname:short)", "refs/heads"], { timeout: GIT_TIMEOUT_MS }).then(({ stdout }) => new Set((stdout || "").split("\n").map((l) => l.trim()).filter(Boolean))).catch(() => void 0);
|
|
26938
27277
|
const orphan = classifyOrphanedWorktree({
|
|
26939
27278
|
branch,
|
|
@@ -27066,6 +27405,7 @@ function registerWorktreeCommands(program3) {
|
|
|
27066
27405
|
}
|
|
27067
27406
|
}
|
|
27068
27407
|
report.push(await bestEffortGit(["worktree", "prune"], primaryCheckout, "prune worktree metadata"));
|
|
27408
|
+
report.push(await bestEffortLeaseClose(wtPath));
|
|
27069
27409
|
const result = {
|
|
27070
27410
|
dryRun: false,
|
|
27071
27411
|
...plan,
|
|
@@ -27096,8 +27436,9 @@ function registerWorktreeCommands(program3) {
|
|
|
27096
27436
|
const ctx = await gatherWorktreeContext();
|
|
27097
27437
|
if (o.stale) {
|
|
27098
27438
|
const leaks = classifyStaleLeaks(ctx);
|
|
27099
|
-
|
|
27100
|
-
return console.log(
|
|
27439
|
+
const failures = ctx.prLookupFailures ?? [];
|
|
27440
|
+
if (o.json) return console.log(JSON.stringify({ stale: leaks, count: leaks.length, prLookupFailures: failures, complete: failures.length === 0 }, null, 2));
|
|
27441
|
+
return console.log(formatStaleLeaks(leaks, failures));
|
|
27101
27442
|
}
|
|
27102
27443
|
if (o.json) return console.log(JSON.stringify({ worktrees: ctx.worktrees }, null, 2));
|
|
27103
27444
|
if (!ctx.worktrees.length) return console.log("worktree list: no worktrees");
|
|
@@ -27121,22 +27462,18 @@ async function gatherWorktreeContext() {
|
|
|
27121
27462
|
const branchOut = (await execFileP2("git", ["branch", "--format=%(refname:short)"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
|
|
27122
27463
|
const localBranches = branchOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
27123
27464
|
const currentBranch2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || void 0;
|
|
27465
|
+
const { prs, failures: prLookupFailures } = await resolveBranchPrs(localBranches.filter((b) => !PROTECTED_BRANCHES2.has(b)), STALE_PR_LOOKUP_LIMIT);
|
|
27124
27466
|
const openPrBranches = /* @__PURE__ */ new Set();
|
|
27125
27467
|
const closedBranches = /* @__PURE__ */ new Set();
|
|
27126
|
-
|
|
27127
|
-
|
|
27128
|
-
const
|
|
27129
|
-
|
|
27130
|
-
|
|
27131
|
-
|
|
27132
|
-
|
|
27133
|
-
|
|
27134
|
-
|
|
27135
|
-
for (const [br, states] of byBranch) {
|
|
27136
|
-
if (states.some((s) => s === "OPEN")) openPrBranches.add(br);
|
|
27137
|
-
else if (states.some((s) => s === "MERGED" || s === "CLOSED")) closedBranches.add(br);
|
|
27138
|
-
}
|
|
27139
|
-
} catch {
|
|
27468
|
+
const byBranch = /* @__PURE__ */ new Map();
|
|
27469
|
+
for (const pr2 of prs) {
|
|
27470
|
+
const arr = byBranch.get(pr2.headRefName) ?? [];
|
|
27471
|
+
arr.push(pr2.state);
|
|
27472
|
+
byBranch.set(pr2.headRefName, arr);
|
|
27473
|
+
}
|
|
27474
|
+
for (const [br, states] of byBranch) {
|
|
27475
|
+
if (states.some((s) => s === "OPEN")) openPrBranches.add(br);
|
|
27476
|
+
else if (states.some((s) => s === "MERGED" || s === "CLOSED")) closedBranches.add(br);
|
|
27140
27477
|
}
|
|
27141
27478
|
const stages = [];
|
|
27142
27479
|
for (const wt of worktrees) {
|
|
@@ -27144,16 +27481,16 @@ async function gatherWorktreeContext() {
|
|
|
27144
27481
|
if (s) stages.push({ path: wt.path, port: s.port });
|
|
27145
27482
|
}
|
|
27146
27483
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
27147
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
27484
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path34.dirname)((0, import_node_path34.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
27148
27485
|
const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
|
|
27149
27486
|
let orphanDirs = [];
|
|
27150
|
-
if ((0,
|
|
27487
|
+
if ((0, import_node_fs35.existsSync)(wtRoot)) {
|
|
27151
27488
|
orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
|
|
27152
27489
|
...defaultOrphanDirScanDeps,
|
|
27153
27490
|
listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
|
|
27154
27491
|
});
|
|
27155
27492
|
}
|
|
27156
|
-
return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs };
|
|
27493
|
+
return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs, prLookupFailures };
|
|
27157
27494
|
}
|
|
27158
27495
|
async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
|
|
27159
27496
|
try {
|
|
@@ -27173,8 +27510,8 @@ ${err.stderr ?? ""}`;
|
|
|
27173
27510
|
}
|
|
27174
27511
|
|
|
27175
27512
|
// src/issue-commands.ts
|
|
27176
|
-
var
|
|
27177
|
-
var
|
|
27513
|
+
var import_node_fs36 = require("node:fs");
|
|
27514
|
+
var import_node_crypto7 = require("node:crypto");
|
|
27178
27515
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
27179
27516
|
var ReparentConflictError = class extends Error {
|
|
27180
27517
|
constructor(message, payload) {
|
|
@@ -27191,7 +27528,7 @@ async function editIssue(client, options, deps = {}) {
|
|
|
27191
27528
|
const url = `https://github.com/${repo}/issues/${parsed.number}`;
|
|
27192
27529
|
const patch = {};
|
|
27193
27530
|
let bodyChanged = false;
|
|
27194
|
-
const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0,
|
|
27531
|
+
const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs36.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
|
|
27195
27532
|
if (options.titleFile !== void 0) {
|
|
27196
27533
|
patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
|
|
27197
27534
|
} else if (options.title !== void 0) {
|
|
@@ -27250,6 +27587,44 @@ async function editIssue(client, options, deps = {}) {
|
|
|
27250
27587
|
...parentResult ? { parent: parentResult } : {}
|
|
27251
27588
|
};
|
|
27252
27589
|
}
|
|
27590
|
+
var EVIDENCE_COMMENT_URL_RE = /^https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)#issuecomment-(\d+)$/;
|
|
27591
|
+
async function verifyCloseEvidence(client, evidence, repo, issueNumber, duplicateOf) {
|
|
27592
|
+
const match = evidence.trim().match(EVIDENCE_COMMENT_URL_RE);
|
|
27593
|
+
if (!match) {
|
|
27594
|
+
throw new Error(
|
|
27595
|
+
`--evidence must be a canonical issue-comment URL (https://github.com/<owner>/<repo>/issues/<n>#issuecomment-<id>), got: ${evidence}`
|
|
27596
|
+
);
|
|
27597
|
+
}
|
|
27598
|
+
const [, evidenceRepo, evidenceIssue, commentId] = match;
|
|
27599
|
+
if (evidenceRepo.toLowerCase() !== repo.toLowerCase() || Number(evidenceIssue) !== issueNumber) {
|
|
27600
|
+
throw new Error(
|
|
27601
|
+
`--evidence comment belongs to ${evidenceRepo}#${evidenceIssue}, not the issue being closed (${repo}#${issueNumber})`
|
|
27602
|
+
);
|
|
27603
|
+
}
|
|
27604
|
+
let comment;
|
|
27605
|
+
try {
|
|
27606
|
+
comment = await client.rest(
|
|
27607
|
+
"GET",
|
|
27608
|
+
`repos/${repo}/issues/comments/${commentId}`
|
|
27609
|
+
);
|
|
27610
|
+
} catch (e) {
|
|
27611
|
+
if (e instanceof GitHubApiError && e.status === 404) {
|
|
27612
|
+
throw new Error(`--evidence comment ${commentId} does not exist on ${repo} \u2014 nothing to anchor the close to`);
|
|
27613
|
+
}
|
|
27614
|
+
throw e;
|
|
27615
|
+
}
|
|
27616
|
+
const expectedAnchor = `/issues/${issueNumber}#issuecomment-${commentId}`;
|
|
27617
|
+
if (!comment.html_url || !comment.html_url.toLowerCase().endsWith(expectedAnchor.toLowerCase())) {
|
|
27618
|
+
throw new Error(
|
|
27619
|
+
`--evidence comment ${commentId} is not a comment on ${repo}#${issueNumber} (it lives at ${comment.html_url ?? "unknown"})`
|
|
27620
|
+
);
|
|
27621
|
+
}
|
|
27622
|
+
if (duplicateOf !== void 0 && !new RegExp(`#${duplicateOf}\\b`).test(comment.body ?? "")) {
|
|
27623
|
+
throw new Error(
|
|
27624
|
+
`--reason duplicate-of ${duplicateOf}: the --evidence comment must cite #${duplicateOf} so the duplicate has an auditable destination`
|
|
27625
|
+
);
|
|
27626
|
+
}
|
|
27627
|
+
}
|
|
27253
27628
|
async function closeIssue(client, options, deps = {}) {
|
|
27254
27629
|
const parsed = parseIssueRef(options.ref);
|
|
27255
27630
|
const repo = parsed.repo ?? options.defaultRepo;
|
|
@@ -27257,6 +27632,9 @@ async function closeIssue(client, options, deps = {}) {
|
|
|
27257
27632
|
const url = `https://github.com/${repo}/issues/${parsed.number}`;
|
|
27258
27633
|
const reason = options.reason ?? "completed";
|
|
27259
27634
|
const stateReason = reason === "duplicate-of" ? "not_planned" : reason === "not-planned" ? "not_planned" : "completed";
|
|
27635
|
+
if (options.evidence !== void 0) {
|
|
27636
|
+
await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
|
|
27637
|
+
}
|
|
27260
27638
|
await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
|
|
27261
27639
|
body: { state: "closed", state_reason: stateReason }
|
|
27262
27640
|
});
|
|
@@ -27424,7 +27802,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
27424
27802
|
const identity = `${spec.type}
|
|
27425
27803
|
${spec.title.trim()}
|
|
27426
27804
|
${spec.body ?? ""}`;
|
|
27427
|
-
const hash = (0,
|
|
27805
|
+
const hash = (0, import_node_crypto7.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
27428
27806
|
return `${batchKey}:${hash}`;
|
|
27429
27807
|
}
|
|
27430
27808
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -27643,7 +28021,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
27643
28021
|
}
|
|
27644
28022
|
});
|
|
27645
28023
|
mutating(
|
|
27646
|
-
issue2.command("close <ref>").description("close an issue and move its board item to Done (--reason completed|not-planned|duplicate-of --duplicate-of <n>)").option("--reason <reason>", "completed | not-planned | duplicate-of (defaults to completed)").option("--duplicate-of <number>", "the issue number this duplicates (use with --reason duplicate-of)").option("--comment <text>", "a closing comment to post").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
|
|
28024
|
+
issue2.command("close <ref>").description("close an issue and move its board item to Done (--reason completed|not-planned|duplicate-of --duplicate-of <n>)").option("--reason <reason>", "completed | not-planned | duplicate-of (defaults to completed)").option("--duplicate-of <number>", "the issue number this duplicates (use with --reason duplicate-of)").option("--comment <text>", "a closing comment to post").option("--evidence <comment-url>", "URL of an existing comment on this issue carrying the close evidence \u2014 verified before closing (JPT#4668; the sanctioned agent-shell close shape)").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
|
|
27647
28025
|
(opts, args) => {
|
|
27648
28026
|
let reason;
|
|
27649
28027
|
try {
|
|
@@ -27672,7 +28050,8 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
27672
28050
|
defaultRepo,
|
|
27673
28051
|
reason: parsed.reason,
|
|
27674
28052
|
duplicateOf: o.duplicateOf ? Number(o.duplicateOf) : void 0,
|
|
27675
|
-
comment: o.comment
|
|
28053
|
+
comment: o.comment,
|
|
28054
|
+
evidence: o.evidence
|
|
27676
28055
|
});
|
|
27677
28056
|
console.log(JSON.stringify(result));
|
|
27678
28057
|
} catch (e) {
|
|
@@ -27754,7 +28133,7 @@ function extendCreateCommand(issue2, batchAttach) {
|
|
|
27754
28133
|
if (opts.batch) {
|
|
27755
28134
|
let specs;
|
|
27756
28135
|
try {
|
|
27757
|
-
const raw = (0,
|
|
28136
|
+
const raw = (0, import_node_fs36.readFileSync)(opts.batch, "utf8");
|
|
27758
28137
|
specs = JSON.parse(raw);
|
|
27759
28138
|
if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
|
|
27760
28139
|
} catch (e) {
|
|
@@ -27803,6 +28182,7 @@ ${lines}`, {
|
|
|
27803
28182
|
surface: batchSurface,
|
|
27804
28183
|
noSurface: batchNoSurface
|
|
27805
28184
|
}, { attach: batchAttach });
|
|
28185
|
+
if (result.created.some((row) => !row.idempotent)) invalidateStatuslineBoardCache();
|
|
27806
28186
|
console.log(JSON.stringify(result));
|
|
27807
28187
|
if (result.failures.length) process.exitCode = 1;
|
|
27808
28188
|
} catch (e) {
|
|
@@ -27828,8 +28208,8 @@ ${lines}`, {
|
|
|
27828
28208
|
}
|
|
27829
28209
|
|
|
27830
28210
|
// src/train-commands.ts
|
|
27831
|
-
var
|
|
27832
|
-
var
|
|
28211
|
+
var import_node_fs37 = require("node:fs");
|
|
28212
|
+
var import_node_path35 = require("node:path");
|
|
27833
28213
|
|
|
27834
28214
|
// src/train-status.ts
|
|
27835
28215
|
function buildTrainStatusReport(input) {
|
|
@@ -27869,7 +28249,7 @@ function formatTrainStatus(r) {
|
|
|
27869
28249
|
// src/train-commands.ts
|
|
27870
28250
|
function readRepoVersion() {
|
|
27871
28251
|
try {
|
|
27872
|
-
return JSON.parse((0,
|
|
28252
|
+
return JSON.parse((0, import_node_fs37.readFileSync)((0, import_node_path35.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
|
|
27873
28253
|
} catch {
|
|
27874
28254
|
return void 0;
|
|
27875
28255
|
}
|
|
@@ -28015,9 +28395,9 @@ function registerDeployCommands(program3) {
|
|
|
28015
28395
|
}
|
|
28016
28396
|
|
|
28017
28397
|
// src/discovery-commands.ts
|
|
28018
|
-
var
|
|
28398
|
+
var import_node_fs38 = require("node:fs");
|
|
28019
28399
|
var import_node_os13 = require("node:os");
|
|
28020
|
-
var
|
|
28400
|
+
var import_node_path36 = require("node:path");
|
|
28021
28401
|
var GC_GH_TIMEOUT_MS3 = 2e4;
|
|
28022
28402
|
async function collectStatus() {
|
|
28023
28403
|
const repo = await resolveRepo();
|
|
@@ -28207,8 +28587,8 @@ async function collectOnboardStatus(opts = {}) {
|
|
|
28207
28587
|
}
|
|
28208
28588
|
const home = (0, import_node_os13.homedir)();
|
|
28209
28589
|
const plugin = onboardPluginGate({
|
|
28210
|
-
readKnown: () => readFileSyncSafe((0,
|
|
28211
|
-
readSettings: () => readFileSyncSafe((0,
|
|
28590
|
+
readKnown: () => readFileSyncSafe((0, import_node_path36.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs38.readFileSync),
|
|
28591
|
+
readSettings: () => readFileSyncSafe((0, import_node_path36.join)(home, ".claude", "settings.json"), import_node_fs38.readFileSync)
|
|
28212
28592
|
});
|
|
28213
28593
|
return { track, board, registry: registry2, secrets, plugin, estateCli, nextCommand };
|
|
28214
28594
|
}
|
|
@@ -29985,6 +30365,10 @@ function surfaceRestartAction(descriptor) {
|
|
|
29985
30365
|
}
|
|
29986
30366
|
|
|
29987
30367
|
// src/doctor-clean.ts
|
|
30368
|
+
function doctorSnapshotFingerprint(checks) {
|
|
30369
|
+
const rows = [...checks].sort((a, b) => `${a.id ?? a.label}`.localeCompare(`${b.id ?? b.label}`)).map((c) => [c.id ?? c.label, c.ok, c.verified ?? null, c.detail ?? null]);
|
|
30370
|
+
return JSON.stringify(rows);
|
|
30371
|
+
}
|
|
29988
30372
|
function checkGithubAuth(probe) {
|
|
29989
30373
|
const login = probe.login?.trim();
|
|
29990
30374
|
const authed = Boolean(login);
|
|
@@ -30351,7 +30735,7 @@ function gcReapable(plan) {
|
|
|
30351
30735
|
}
|
|
30352
30736
|
async function runDoctorClean(opts, io, deps) {
|
|
30353
30737
|
const full = !opts.fast && !opts.banner && !opts.preflight;
|
|
30354
|
-
const applyEnv = full
|
|
30738
|
+
const applyEnv = full;
|
|
30355
30739
|
const applyRepo = full && opts.repoWrites !== false;
|
|
30356
30740
|
const lane = {
|
|
30357
30741
|
banner: Boolean(opts.banner),
|
|
@@ -30378,11 +30762,14 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30378
30762
|
const checks = [];
|
|
30379
30763
|
let restartPending = false;
|
|
30380
30764
|
const streamed = /* @__PURE__ */ new Set();
|
|
30765
|
+
const streamedLines = /* @__PURE__ */ new Set();
|
|
30766
|
+
const alreadyShown = (c) => streamed.has(c) || streamedLines.has(renderCheckLine(c));
|
|
30381
30767
|
const worthPrinting = (c) => Boolean(opts.verbose) || !c.ok || Boolean(c.warn);
|
|
30382
30768
|
const emitNow = (check) => {
|
|
30383
30769
|
checks.push(check);
|
|
30384
|
-
if (opts.json || !worthPrinting(check)) return;
|
|
30770
|
+
if (opts.json || !streamingPass || !worthPrinting(check)) return;
|
|
30385
30771
|
streamed.add(check);
|
|
30772
|
+
streamedLines.add(renderCheckLine(check));
|
|
30386
30773
|
io.log(renderCheckLine(check));
|
|
30387
30774
|
if (opts.verbose) for (const evidence of check.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
|
|
30388
30775
|
};
|
|
@@ -30394,13 +30781,35 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30394
30781
|
};
|
|
30395
30782
|
const releasedNote = deps.releasedVersionNote?.();
|
|
30396
30783
|
let pluginHealed = false;
|
|
30784
|
+
const spentHeals = /* @__PURE__ */ new Set();
|
|
30785
|
+
let healChangedThisPass = false;
|
|
30786
|
+
const markHealChanged = () => {
|
|
30787
|
+
healChangedThisPass = true;
|
|
30788
|
+
};
|
|
30789
|
+
const traceHeal = (kind) => opts.healTrace?.(kind);
|
|
30790
|
+
const spendOnce = (kind) => {
|
|
30791
|
+
if (spentHeals.has(kind)) return false;
|
|
30792
|
+
spentHeals.add(kind);
|
|
30793
|
+
return true;
|
|
30794
|
+
};
|
|
30795
|
+
const spentRows = /* @__PURE__ */ new Map();
|
|
30397
30796
|
async function runPluginRow() {
|
|
30398
30797
|
if (!registryEvidence) return;
|
|
30798
|
+
const spentRow = spentRows.get("plugin-chain");
|
|
30799
|
+
if (spentRow) {
|
|
30800
|
+
emitNow(spentRow);
|
|
30801
|
+
if (registryEvidence.descriptor.trustOwner === "operator" && (registryEvidence.guardState === "healthy" || pluginHealed)) {
|
|
30802
|
+
const trust = checkCodexHookTrust(deps.pluginTrustState?.(), registryEvidence.descriptor.displayName);
|
|
30803
|
+
if (trust) emitNow(trust);
|
|
30804
|
+
}
|
|
30805
|
+
return;
|
|
30806
|
+
}
|
|
30399
30807
|
const diagnosis = diagnoseSurface({ ...registryEvidence, releasedVersion: released });
|
|
30400
30808
|
const repair = planSurfaceRepair(diagnosis);
|
|
30401
|
-
if (applyEnv && deps.healPlugin && repair?.supported) {
|
|
30809
|
+
if (applyEnv && deps.healPlugin && repair?.supported && spendOnce("plugin-chain")) {
|
|
30402
30810
|
const { descriptor } = registryEvidence;
|
|
30403
30811
|
healIntent(`${descriptor.displayName} plugin \u2014 healing via ${descriptor.installMechanism} (${descriptor.installLocator})`);
|
|
30812
|
+
traceHeal("plugin-chain");
|
|
30404
30813
|
const heal = await deps.healPlugin(healStep);
|
|
30405
30814
|
pluginHealed = heal.ok;
|
|
30406
30815
|
const row = buildSurfaceDoctorCheck(diagnoseSurface({
|
|
@@ -30413,7 +30822,12 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30413
30822
|
row.fix = `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes`;
|
|
30414
30823
|
}
|
|
30415
30824
|
emitNow(row);
|
|
30825
|
+
if (heal.skipped) spentHeals.delete("plugin-chain");
|
|
30416
30826
|
if (!heal.skipped) restartPending = true;
|
|
30827
|
+
if (heal.ok && !heal.skipped) {
|
|
30828
|
+
markHealChanged();
|
|
30829
|
+
spentRows.set("plugin-chain", row);
|
|
30830
|
+
}
|
|
30417
30831
|
} else if (diagnosis.state !== "skipped") {
|
|
30418
30832
|
const row = buildSurfaceDoctorCheck(diagnosis);
|
|
30419
30833
|
emitNow(row);
|
|
@@ -30425,13 +30839,18 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30425
30839
|
}
|
|
30426
30840
|
}
|
|
30427
30841
|
async function runCliRow() {
|
|
30842
|
+
const spentRow = spentRows.get("cli-self-update");
|
|
30843
|
+
if (spentRow) {
|
|
30844
|
+
emitNow(spentRow);
|
|
30845
|
+
return;
|
|
30846
|
+
}
|
|
30428
30847
|
const missing = deps.missingCliCommands?.() ?? [];
|
|
30429
30848
|
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
30430
30849
|
const cliReport = buildVersionLagReport(cliInput);
|
|
30431
30850
|
const capabilityGap = missing.length > 0 && Boolean(cliReport.releasedVersion);
|
|
30432
30851
|
const shouldUpdate = Boolean(
|
|
30433
30852
|
applyEnv && deps.updateCli && (versionAutoUpdateAction(cliReport) === "npm" || capabilityGap)
|
|
30434
|
-
);
|
|
30853
|
+
) && spendOnce("cli-self-update");
|
|
30435
30854
|
if (!shouldUpdate) {
|
|
30436
30855
|
const cli = checkCliVersion(cliInput, releasedNote);
|
|
30437
30856
|
if (cli) {
|
|
@@ -30461,29 +30880,40 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30461
30880
|
const target = cliReport.releasedVersion;
|
|
30462
30881
|
const intent = capabilityGap && cliReport.ok ? `mmi-cli \u2014 self-updating to ${target} via npm install -g (missing commands: ${missing.join(", ")})` : `mmi-cli \u2014 self-updating ${cliReport.currentVersion} \u2192 ${target} via npm install -g`;
|
|
30463
30882
|
healIntent(intent);
|
|
30883
|
+
traceHeal("cli-self-update");
|
|
30464
30884
|
const heal = await deps.updateCli(target, healStep);
|
|
30885
|
+
if (heal.skipped) spentHeals.delete("cli-self-update");
|
|
30886
|
+
if (heal.ok && !heal.skipped) markHealChanged();
|
|
30465
30887
|
const healEvidence = [
|
|
30466
30888
|
`running: ${cliReport.currentVersion}`,
|
|
30467
30889
|
`published: ${target ?? "(unknown)"}`,
|
|
30468
30890
|
`heal: ${heal.detail}`,
|
|
30469
30891
|
...missing.length ? [`missing commands before heal: ${missing.join(", ")}`] : []
|
|
30470
30892
|
];
|
|
30471
|
-
|
|
30472
|
-
|
|
30473
|
-
|
|
30474
|
-
|
|
30475
|
-
|
|
30476
|
-
|
|
30477
|
-
|
|
30478
|
-
|
|
30479
|
-
|
|
30480
|
-
|
|
30481
|
-
|
|
30482
|
-
|
|
30483
|
-
|
|
30484
|
-
|
|
30485
|
-
|
|
30486
|
-
|
|
30893
|
+
if (heal.ok) {
|
|
30894
|
+
const healedRow = {
|
|
30895
|
+
id: "cli-version",
|
|
30896
|
+
ok: true,
|
|
30897
|
+
label: "mmi-cli",
|
|
30898
|
+
detail: capabilityGap && cliReport.ok ? `${cliReport.currentVersion} \u2192 ${target} \u2014 self-updated via npm install -g (was missing: ${missing.join(", ")}); the next mmi-cli invocation runs the new version` : `${cliReport.currentVersion} \u2192 ${target} \u2014 self-updated via npm install -g; the next mmi-cli invocation runs the new version`,
|
|
30899
|
+
verbose: healEvidence
|
|
30900
|
+
};
|
|
30901
|
+
spentRows.set("cli-self-update", healedRow);
|
|
30902
|
+
emitNow(healedRow);
|
|
30903
|
+
return;
|
|
30904
|
+
}
|
|
30905
|
+
emitNow(
|
|
30906
|
+
{
|
|
30907
|
+
id: "cli-version",
|
|
30908
|
+
ok: false,
|
|
30909
|
+
label: "mmi-cli",
|
|
30910
|
+
detail: capabilityGap && cliReport.ok ? `missing commands: ${missing.join(", ")}` : `${cliReport.currentVersion} \u2192 ${target}`,
|
|
30911
|
+
// #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
|
|
30912
|
+
...heal.skipped ? { reportOnly: true } : {},
|
|
30913
|
+
fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(target)}\``,
|
|
30914
|
+
verbose: healEvidence
|
|
30915
|
+
}
|
|
30916
|
+
);
|
|
30487
30917
|
}
|
|
30488
30918
|
async function runGithubAuthRow() {
|
|
30489
30919
|
emitNow(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
|
|
@@ -30505,6 +30935,8 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30505
30935
|
if (gi.ok) {
|
|
30506
30936
|
emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
|
|
30507
30937
|
} else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
|
|
30938
|
+
traceHeal("repo-cleans");
|
|
30939
|
+
markHealChanged();
|
|
30508
30940
|
emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
|
|
30509
30941
|
restartPending = true;
|
|
30510
30942
|
} else {
|
|
@@ -30512,8 +30944,100 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30512
30944
|
}
|
|
30513
30945
|
}
|
|
30514
30946
|
async function runPluginCacheRow() {
|
|
30947
|
+
if (applyEnv && deps.prunePluginCache && spendOnce("cache-prune")) {
|
|
30948
|
+
const cache = deps.pluginCache();
|
|
30949
|
+
if (cache.stale.length === 0 && cache.staging.length === 0) {
|
|
30950
|
+
emitNow(checkPluginCache(cache));
|
|
30951
|
+
return;
|
|
30952
|
+
}
|
|
30953
|
+
healIntent(`plugin cache \u2014 pruning ${cache.stale.length} superseded version(s) (guarded)`);
|
|
30954
|
+
traceHeal("cache-prune");
|
|
30955
|
+
const outcome = deps.prunePluginCache();
|
|
30956
|
+
if (outcome.removed.length) markHealChanged();
|
|
30957
|
+
const evidence = [
|
|
30958
|
+
...outcome.removed.map((v) => `pruned: ${v}`),
|
|
30959
|
+
...outcome.held.map((h) => `still held: ${h.version} (${h.error}) \u2014 never forced`),
|
|
30960
|
+
...outcome.kept.map((k) => `kept: ${k.version} \u2014 ${k.reason}`)
|
|
30961
|
+
];
|
|
30962
|
+
if (outcome.held.length === 0) {
|
|
30963
|
+
emitNow({
|
|
30964
|
+
id: "plugin-cache",
|
|
30965
|
+
ok: true,
|
|
30966
|
+
label: "plugin cache",
|
|
30967
|
+
detail: outcome.removed.length ? `pruned ${outcome.removed.length} superseded version(s)` : "nothing provably superseded (guards kept every candidate)",
|
|
30968
|
+
verbose: evidence
|
|
30969
|
+
});
|
|
30970
|
+
} else {
|
|
30971
|
+
emitNow({
|
|
30972
|
+
id: "plugin-cache",
|
|
30973
|
+
ok: false,
|
|
30974
|
+
reportOnly: true,
|
|
30975
|
+
label: "plugin cache",
|
|
30976
|
+
detail: `${outcome.removed.length} pruned; ${outcome.held.length} still held by a live session`,
|
|
30977
|
+
fix: "close the session holding it and re-run `mmi-cli doctor`",
|
|
30978
|
+
verbose: evidence
|
|
30979
|
+
});
|
|
30980
|
+
}
|
|
30981
|
+
return;
|
|
30982
|
+
}
|
|
30515
30983
|
emitNow(checkPluginCache(deps.pluginCache()));
|
|
30516
30984
|
}
|
|
30985
|
+
async function runPiPluginRow() {
|
|
30986
|
+
const state = deps.piPluginState?.();
|
|
30987
|
+
if (!state) return;
|
|
30988
|
+
if (!state.settingsReadable) {
|
|
30989
|
+
emitNow({
|
|
30990
|
+
id: "pi-plugin",
|
|
30991
|
+
ok: false,
|
|
30992
|
+
reportOnly: true,
|
|
30993
|
+
label: "pi plugin",
|
|
30994
|
+
detail: "settings.json unreadable",
|
|
30995
|
+
fix: "repair ~/.pi/agent/settings.json, then re-run doctor",
|
|
30996
|
+
verbose: ["~/.pi/agent exists but settings.json could not be parsed \u2014 fail closed, nothing written"]
|
|
30997
|
+
});
|
|
30998
|
+
return;
|
|
30999
|
+
}
|
|
31000
|
+
const current = state.registeredPath === state.expectedPath;
|
|
31001
|
+
if (current) {
|
|
31002
|
+
emitNow({
|
|
31003
|
+
id: "pi-plugin",
|
|
31004
|
+
ok: true,
|
|
31005
|
+
label: "pi plugin",
|
|
31006
|
+
detail: "package registered in settings.json",
|
|
31007
|
+
verbose: [`registered: ${state.registeredPath}`]
|
|
31008
|
+
});
|
|
31009
|
+
return;
|
|
31010
|
+
}
|
|
31011
|
+
if (applyEnv && deps.healPiPlugin) {
|
|
31012
|
+
healIntent(`pi plugin \u2014 registering ${state.expectedPath}`);
|
|
31013
|
+
traceHeal("env-heals");
|
|
31014
|
+
const heal = deps.healPiPlugin();
|
|
31015
|
+
if (heal.ok) markHealChanged();
|
|
31016
|
+
emitNow(heal.ok ? {
|
|
31017
|
+
id: "pi-plugin",
|
|
31018
|
+
ok: true,
|
|
31019
|
+
label: "pi plugin",
|
|
31020
|
+
detail: state.registeredPath ? "replaced stale package path" : "registered package in settings.json",
|
|
31021
|
+
verbose: [`was: ${state.registeredPath ?? "(absent)"}`, `now: ${state.expectedPath}`, `heal: ${heal.detail}`]
|
|
31022
|
+
} : {
|
|
31023
|
+
id: "pi-plugin",
|
|
31024
|
+
ok: false,
|
|
31025
|
+
label: "pi plugin",
|
|
31026
|
+
detail: state.registeredPath ? "stale package path" : "package not registered",
|
|
31027
|
+
fix: `heal failed (${heal.detail}) \u2014 add ${state.expectedPath} to packages[] in ~/.pi/agent/settings.json`,
|
|
31028
|
+
verbose: [`expected: ${state.expectedPath}`]
|
|
31029
|
+
});
|
|
31030
|
+
return;
|
|
31031
|
+
}
|
|
31032
|
+
emitNow({
|
|
31033
|
+
id: "pi-plugin",
|
|
31034
|
+
ok: false,
|
|
31035
|
+
label: "pi plugin",
|
|
31036
|
+
detail: state.registeredPath ? "stale package path" : "package not registered",
|
|
31037
|
+
fix: "run `mmi-cli doctor` to register the mmi .pi-plugin in ~/.pi/agent/settings.json",
|
|
31038
|
+
verbose: [`expected: ${state.expectedPath}`, `registered: ${state.registeredPath ?? "(absent)"}`]
|
|
31039
|
+
});
|
|
31040
|
+
}
|
|
30517
31041
|
async function runSessionPayloadRow() {
|
|
30518
31042
|
const payload = checkSessionPayload(deps.sessionPayload());
|
|
30519
31043
|
if (payload) emitNow(payload);
|
|
@@ -30523,7 +31047,11 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30523
31047
|
const healed = deps.healMarketplacePins();
|
|
30524
31048
|
if (healed) {
|
|
30525
31049
|
healIntent(`marketplace pins \u2014 ${healed.detail}`);
|
|
30526
|
-
if (healed.wrote)
|
|
31050
|
+
if (healed.wrote) {
|
|
31051
|
+
traceHeal("env-heals");
|
|
31052
|
+
markHealChanged();
|
|
31053
|
+
restartPending = true;
|
|
31054
|
+
}
|
|
30527
31055
|
}
|
|
30528
31056
|
}
|
|
30529
31057
|
for (const row of deps.marketplaceRows()) emitNow(row);
|
|
@@ -30566,6 +31094,8 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30566
31094
|
let healedWrite = false;
|
|
30567
31095
|
if (applyRepo && probe?.drift && deps.healDocsIndex) {
|
|
30568
31096
|
healIntent("docs index \u2014 regenerating docs/index.md");
|
|
31097
|
+
traceHeal("repo-cleans");
|
|
31098
|
+
markHealChanged();
|
|
30569
31099
|
try {
|
|
30570
31100
|
probe = deps.healDocsIndex(root);
|
|
30571
31101
|
healedWrite = !probe.drift;
|
|
@@ -30786,7 +31316,11 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30786
31316
|
`skipped: ${sweep.skipped}`
|
|
30787
31317
|
]
|
|
30788
31318
|
});
|
|
30789
|
-
if (sweep.removed.length)
|
|
31319
|
+
if (sweep.removed.length) {
|
|
31320
|
+
traceHeal("repo-cleans");
|
|
31321
|
+
markHealChanged();
|
|
31322
|
+
restartPending = true;
|
|
31323
|
+
}
|
|
30790
31324
|
} catch (e) {
|
|
30791
31325
|
const message = e instanceof Error ? e.message : String(e);
|
|
30792
31326
|
emitNow({
|
|
@@ -30824,7 +31358,11 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30824
31358
|
...r.failed.map((f) => `FAILED: ${f}`)
|
|
30825
31359
|
] : ["nothing reapable"]
|
|
30826
31360
|
});
|
|
30827
|
-
if (reaped)
|
|
31361
|
+
if (reaped) {
|
|
31362
|
+
traceHeal("repo-cleans");
|
|
31363
|
+
markHealChanged();
|
|
31364
|
+
restartPending = true;
|
|
31365
|
+
}
|
|
30828
31366
|
} else {
|
|
30829
31367
|
const n = gcReapable(plan);
|
|
30830
31368
|
const gcEvidence = [
|
|
@@ -30851,36 +31389,62 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30851
31389
|
const applied = deps.executeScratchGc(repoRoot2, { apply: true });
|
|
30852
31390
|
const pruned = applied.applied?.pruned.length ?? 0;
|
|
30853
31391
|
emitNow({ id: "scratch", ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
|
|
30854
|
-
if (pruned)
|
|
31392
|
+
if (pruned) {
|
|
31393
|
+
traceHeal("repo-cleans");
|
|
31394
|
+
markHealChanged();
|
|
31395
|
+
restartPending = true;
|
|
31396
|
+
}
|
|
30855
31397
|
} else {
|
|
30856
31398
|
emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor` (without --no-repo-writes)", verbose: scratchEvidence });
|
|
30857
31399
|
}
|
|
30858
31400
|
}
|
|
30859
31401
|
const prefix = [
|
|
30860
|
-
{ id: "plugin", when: true, run: runPluginRow },
|
|
30861
31402
|
{ id: "cli-version", when: true, run: runCliRow },
|
|
31403
|
+
{ id: "plugin", when: true, run: runPluginRow },
|
|
31404
|
+
{ id: "plugin-cache", when: true, run: runPluginCacheRow },
|
|
30862
31405
|
{ id: "github-auth", when: true, run: runGithubAuthRow },
|
|
30863
31406
|
{ id: "aws-identity", when: true, run: runAwsRow },
|
|
30864
31407
|
{ id: "repo-worktrees", when: true, run: runRepoWorktreesRow },
|
|
30865
|
-
{ id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow },
|
|
30866
|
-
{ id: "plugin-cache", when: true, run: runPluginCacheRow },
|
|
30867
31408
|
{ id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
|
|
30868
|
-
{ id: "marketplace", when: true, run: runMarketplaceRows }
|
|
31409
|
+
{ id: "marketplace", when: true, run: runMarketplaceRows },
|
|
31410
|
+
{ id: "pi-plugin", when: true, run: runPiPluginRow },
|
|
31411
|
+
{ id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow }
|
|
30869
31412
|
];
|
|
30870
|
-
|
|
30871
|
-
|
|
30872
|
-
|
|
30873
|
-
|
|
30874
|
-
|
|
30875
|
-
|
|
30876
|
-
|
|
30877
|
-
|
|
30878
|
-
|
|
30879
|
-
|
|
30880
|
-
|
|
30881
|
-
|
|
30882
|
-
|
|
30883
|
-
|
|
31413
|
+
const maxPasses = 3;
|
|
31414
|
+
let passes = 0;
|
|
31415
|
+
let prevFingerprint;
|
|
31416
|
+
let streamingPass = true;
|
|
31417
|
+
for (; ; ) {
|
|
31418
|
+
passes += 1;
|
|
31419
|
+
opts.onPass?.(passes);
|
|
31420
|
+
checks.length = 0;
|
|
31421
|
+
healChangedThisPass = false;
|
|
31422
|
+
for (const entry of prefix) if (entry.when) await entry.run();
|
|
31423
|
+
const parallelRows = [
|
|
31424
|
+
{ id: "docs-index", when: isOrgRepo && lane.full, run: runDocsIndexRow },
|
|
31425
|
+
// Hub cloud probe (short timeout). Full lane + `--self`; `--preflight` may emit command-absent only (#4156).
|
|
31426
|
+
{ id: "repo-index", when: isOrgRepo && (lane.full || Boolean(opts.self) || lane.preflight), run: runRepoIndexRow },
|
|
31427
|
+
{ id: "board-doctor", when: isOrgRepo && lane.full, run: runBoardDoctorRow },
|
|
31428
|
+
{ id: "schedules-drift", when: isOrgRepo && lane.full, run: runSchedulesDriftRow }
|
|
31429
|
+
];
|
|
31430
|
+
await Promise.all(parallelRows.filter((entry) => entry.when).map((entry) => entry.run()));
|
|
31431
|
+
const suffix = [
|
|
31432
|
+
{ id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
|
|
31433
|
+
{ id: "housekeeper", when: isOrgRepo && lane.full, run: runHousekeeperRows }
|
|
31434
|
+
];
|
|
31435
|
+
for (const entry of suffix) if (entry.when) await entry.run();
|
|
31436
|
+
if (!lane.full) break;
|
|
31437
|
+
const fingerprint = doctorSnapshotFingerprint(checks);
|
|
31438
|
+
const stillActionable = checks.some((c) => !c.ok && !c.reportOnly);
|
|
31439
|
+
if (!healChangedThisPass || !stillActionable || fingerprint === prevFingerprint) break;
|
|
31440
|
+
if (passes >= maxPasses) {
|
|
31441
|
+
healIntent(`convergence \u2014 pass bound (${maxPasses}) reached with rows still moving; re-run doctor to continue`);
|
|
31442
|
+
break;
|
|
31443
|
+
}
|
|
31444
|
+
prevFingerprint = fingerprint;
|
|
31445
|
+
streamingPass = false;
|
|
31446
|
+
healIntent(`convergence \u2014 pass ${passes} healed; re-measuring (pass ${passes + 1}/${maxPasses})`);
|
|
31447
|
+
}
|
|
30884
31448
|
const exitCode = doctorReportExitCode(checks);
|
|
30885
31449
|
if (opts.json) {
|
|
30886
31450
|
const payload = opts.verbose ? checks : checks.map(({ verbose: _evidence, ...check }) => check);
|
|
@@ -30893,7 +31457,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30893
31457
|
return exitCode;
|
|
30894
31458
|
}
|
|
30895
31459
|
if (opts.banner) {
|
|
30896
|
-
const actionable = checks.filter((c) => (!c.ok || c.warn) && !
|
|
31460
|
+
const actionable = checks.filter((c) => (!c.ok || c.warn) && !alreadyShown(c));
|
|
30897
31461
|
for (const c of actionable) {
|
|
30898
31462
|
io.log(renderReport([c], { restartPending: false }));
|
|
30899
31463
|
if (opts.verbose) for (const evidence of c.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
|
|
@@ -30901,7 +31465,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30901
31465
|
if (restartPending) io.log(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
|
|
30902
31466
|
return 0;
|
|
30903
31467
|
}
|
|
30904
|
-
const rest = checks.filter((c) => !
|
|
31468
|
+
const rest = checks.filter((c) => !alreadyShown(c));
|
|
30905
31469
|
const shown = opts.verbose ? rest : rest.filter((c) => !c.ok || c.warn);
|
|
30906
31470
|
const lines = [];
|
|
30907
31471
|
for (const check of shown) {
|
|
@@ -30933,17 +31497,17 @@ function parseOriginRepo(remoteUrl) {
|
|
|
30933
31497
|
}
|
|
30934
31498
|
function ghHostsConfigPath(env, platform2) {
|
|
30935
31499
|
const sep3 = platform2 === "win32" ? "\\" : "/";
|
|
30936
|
-
const
|
|
31500
|
+
const join34 = (...parts) => parts.join(sep3);
|
|
30937
31501
|
const explicit = env.GH_CONFIG_DIR?.trim();
|
|
30938
|
-
if (explicit) return
|
|
31502
|
+
if (explicit) return join34(explicit, "hosts.yml");
|
|
30939
31503
|
if (platform2 === "win32") {
|
|
30940
31504
|
const appData = (env.AppData ?? env.APPDATA)?.trim();
|
|
30941
|
-
return appData ?
|
|
31505
|
+
return appData ? join34(appData, "GitHub CLI", "hosts.yml") : void 0;
|
|
30942
31506
|
}
|
|
30943
31507
|
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
30944
|
-
if (xdg) return
|
|
31508
|
+
if (xdg) return join34(xdg, "gh", "hosts.yml");
|
|
30945
31509
|
const home = env.HOME?.trim();
|
|
30946
|
-
return home ?
|
|
31510
|
+
return home ? join34(home, ".config", "gh", "hosts.yml") : void 0;
|
|
30947
31511
|
}
|
|
30948
31512
|
function parseGhHostsAccounts(yaml, host = "github.com") {
|
|
30949
31513
|
let hostIndent = null;
|
|
@@ -30993,17 +31557,17 @@ function ghAccountCaveat(announcedLogin, accounts) {
|
|
|
30993
31557
|
}
|
|
30994
31558
|
|
|
30995
31559
|
// src/doctor-io.ts
|
|
30996
|
-
var
|
|
31560
|
+
var import_node_fs39 = require("node:fs");
|
|
30997
31561
|
var import_node_os14 = require("node:os");
|
|
30998
|
-
var
|
|
30999
|
-
var
|
|
31562
|
+
var import_node_path37 = require("node:path");
|
|
31563
|
+
var import_node_child_process18 = require("node:child_process");
|
|
31000
31564
|
var import_node_util8 = require("node:util");
|
|
31001
|
-
var execFileP6 = (0, import_node_util8.promisify)(
|
|
31565
|
+
var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process18.execFile);
|
|
31002
31566
|
var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
31003
31567
|
function installedClaudePluginVersion() {
|
|
31004
31568
|
try {
|
|
31005
31569
|
const file = JSON.parse(
|
|
31006
|
-
(0,
|
|
31570
|
+
(0, import_node_fs39.readFileSync)((0, import_node_path37.join)((0, import_node_os14.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
|
|
31007
31571
|
);
|
|
31008
31572
|
const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
|
|
31009
31573
|
if (versions.length === 0) return void 0;
|
|
@@ -31014,7 +31578,7 @@ function installedClaudePluginVersion() {
|
|
|
31014
31578
|
}
|
|
31015
31579
|
function manifestVersion(path2) {
|
|
31016
31580
|
try {
|
|
31017
|
-
const manifest = JSON.parse((0,
|
|
31581
|
+
const manifest = JSON.parse((0, import_node_fs39.readFileSync)(path2, "utf8"));
|
|
31018
31582
|
return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
|
|
31019
31583
|
} catch {
|
|
31020
31584
|
return void 0;
|
|
@@ -31024,32 +31588,32 @@ function installedSurfacePluginVersion(surface) {
|
|
|
31024
31588
|
const token = surfaceToken(surface);
|
|
31025
31589
|
if (token === "kilo") {
|
|
31026
31590
|
try {
|
|
31027
|
-
const stamp = (0,
|
|
31591
|
+
const stamp = (0, import_node_fs39.readFileSync)((0, import_node_path37.join)((0, import_node_os14.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
|
|
31028
31592
|
return stamp || void 0;
|
|
31029
31593
|
} catch {
|
|
31030
31594
|
return void 0;
|
|
31031
31595
|
}
|
|
31032
31596
|
}
|
|
31033
31597
|
if (token === "cursor") {
|
|
31034
|
-
return manifestVersion((0,
|
|
31598
|
+
return manifestVersion((0, import_node_path37.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
|
|
31035
31599
|
}
|
|
31036
31600
|
if (token === "jervcode") {
|
|
31037
31601
|
const entry = mmiPiWrapperEntry();
|
|
31038
31602
|
if (!entry) return void 0;
|
|
31039
|
-
return manifestVersion((0,
|
|
31603
|
+
return manifestVersion((0, import_node_path37.join)(decodeURIComponent(entry.replace(/^file:\/\/\/?/, "")), "package.json"));
|
|
31040
31604
|
}
|
|
31041
31605
|
if (token === "kimi") {
|
|
31042
|
-
return manifestVersion((0,
|
|
31606
|
+
return manifestVersion((0, import_node_path37.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
|
|
31043
31607
|
}
|
|
31044
31608
|
if (token === "claude") return installedClaudePluginVersion();
|
|
31045
31609
|
if (token !== "codex") return void 0;
|
|
31046
31610
|
try {
|
|
31047
|
-
const raw = process.platform === "win32" ? (0,
|
|
31611
|
+
const raw = process.platform === "win32" ? (0, import_node_child_process18.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
|
|
31048
31612
|
encoding: "utf8",
|
|
31049
31613
|
stdio: ["ignore", "pipe", "ignore"],
|
|
31050
31614
|
timeout: 15e3,
|
|
31051
31615
|
windowsHide: true
|
|
31052
|
-
}) : (0,
|
|
31616
|
+
}) : (0, import_node_child_process18.execFileSync)("codex", ["plugin", "list", "--json"], {
|
|
31053
31617
|
encoding: "utf8",
|
|
31054
31618
|
stdio: ["ignore", "pipe", "ignore"],
|
|
31055
31619
|
timeout: 15e3,
|
|
@@ -31067,7 +31631,7 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
|
|
|
31067
31631
|
}
|
|
31068
31632
|
function worktreeRootSync() {
|
|
31069
31633
|
try {
|
|
31070
|
-
const out = (0,
|
|
31634
|
+
const out = (0, import_node_child_process18.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
31071
31635
|
let root = out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
31072
31636
|
if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
|
|
31073
31637
|
return root || null;
|
|
@@ -31077,13 +31641,13 @@ function worktreeRootSync() {
|
|
|
31077
31641
|
}
|
|
31078
31642
|
var gitignorePath = () => {
|
|
31079
31643
|
const root = worktreeRootSync();
|
|
31080
|
-
return root === null ? null : (0,
|
|
31644
|
+
return root === null ? null : (0, import_node_path37.join)(root, ".gitignore");
|
|
31081
31645
|
};
|
|
31082
31646
|
function readGitignore() {
|
|
31083
31647
|
const path2 = gitignorePath();
|
|
31084
31648
|
if (path2 === null) return null;
|
|
31085
31649
|
try {
|
|
31086
|
-
return (0,
|
|
31650
|
+
return (0, import_node_fs39.readFileSync)(path2, "utf8");
|
|
31087
31651
|
} catch {
|
|
31088
31652
|
return null;
|
|
31089
31653
|
}
|
|
@@ -31092,7 +31656,7 @@ function writeGitignore(content) {
|
|
|
31092
31656
|
const path2 = gitignorePath();
|
|
31093
31657
|
if (path2 === null) return false;
|
|
31094
31658
|
try {
|
|
31095
|
-
(0,
|
|
31659
|
+
(0, import_node_fs39.writeFileSync)(path2, content, "utf8");
|
|
31096
31660
|
return true;
|
|
31097
31661
|
} catch {
|
|
31098
31662
|
return false;
|
|
@@ -31116,28 +31680,11 @@ async function repoRoot() {
|
|
|
31116
31680
|
}
|
|
31117
31681
|
function hasRepoLocalWorktrees() {
|
|
31118
31682
|
const root = worktreeRootSync();
|
|
31119
|
-
return root !== null && (0,
|
|
31683
|
+
return root !== null && (0, import_node_fs39.existsSync)((0, import_node_path37.join)(root, ".worktrees"));
|
|
31120
31684
|
}
|
|
31121
31685
|
|
|
31122
31686
|
// src/index.ts
|
|
31123
31687
|
var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
31124
|
-
async function currentRepoFullName() {
|
|
31125
|
-
const remote = (await gitOut(["remote", "get-url", "origin"])).replace(/\.git$/, "");
|
|
31126
|
-
const parts = remote.split(/[:/]/).filter(Boolean);
|
|
31127
|
-
if (parts.length >= 2) return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
|
|
31128
|
-
return `mutmutco/${await repoSlug()}`;
|
|
31129
|
-
}
|
|
31130
|
-
async function readDocsAuditFetch(repo) {
|
|
31131
|
-
const list = await fetchDocsAuditList(registryClientDeps(await loadConfig()));
|
|
31132
|
-
if ("notArmed" in list) return { notArmed: true };
|
|
31133
|
-
if (!list.ok) return { ok: false, error: list.error };
|
|
31134
|
-
const wanted = repo.toLowerCase();
|
|
31135
|
-
const row = list.rows.find((r) => String(r.repo ?? "").toLowerCase() === wanted);
|
|
31136
|
-
return {
|
|
31137
|
-
ok: true,
|
|
31138
|
-
verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
|
|
31139
|
-
};
|
|
31140
|
-
}
|
|
31141
31688
|
async function githubRepoReachProbe() {
|
|
31142
31689
|
const remote = await execFileP2("git", ["remote", "get-url", "origin"], { timeout: GIT_TIMEOUT_MS }).then((r) => r.stdout).catch(() => "");
|
|
31143
31690
|
const repo = parseOriginRepo(remote);
|
|
@@ -31152,8 +31699,8 @@ ${r.stderr ?? ""}`).catch(() => "");
|
|
|
31152
31699
|
function ghMultiAccountCaveat(announcedLogin) {
|
|
31153
31700
|
try {
|
|
31154
31701
|
const hostsPath = ghHostsConfigPath(process.env, process.platform);
|
|
31155
|
-
if (!hostsPath || !(0,
|
|
31156
|
-
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0,
|
|
31702
|
+
if (!hostsPath || !(0, import_node_fs40.existsSync)(hostsPath)) return void 0;
|
|
31703
|
+
return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs40.readFileSync)(hostsPath, "utf8")));
|
|
31157
31704
|
} catch {
|
|
31158
31705
|
return void 0;
|
|
31159
31706
|
}
|
|
@@ -31161,7 +31708,7 @@ function ghMultiAccountCaveat(announcedLogin) {
|
|
|
31161
31708
|
var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
|
|
31162
31709
|
var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
|
|
31163
31710
|
function envHealLockPath(home) {
|
|
31164
|
-
return (0,
|
|
31711
|
+
return (0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
|
|
31165
31712
|
}
|
|
31166
31713
|
async function withEnvHealLock(what, run) {
|
|
31167
31714
|
try {
|
|
@@ -31276,6 +31823,45 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
31276
31823
|
stagingBytes: plan.stagingBytes
|
|
31277
31824
|
};
|
|
31278
31825
|
},
|
|
31826
|
+
// #4199: guarded auto-prune (docs/doctor-contract.md § Guarded cache prune). The plan is rebuilt
|
|
31827
|
+
// HERE, from live evidence, immediately before the delete — plan is the revalidation. Guards live in
|
|
31828
|
+
// `selectPrunablePluginVersions` (never running/newest/installed; unreadable evidence keeps the dir,
|
|
31829
|
+
// named). No `force`: a dir whose delete refuses (a session holds it) surfaces as `held`, never freed
|
|
31830
|
+
// out from under the holder.
|
|
31831
|
+
prunePluginCache: () => {
|
|
31832
|
+
const surface = detectSurface(process.env);
|
|
31833
|
+
const configRoot = surfaceConfigRoot(surface);
|
|
31834
|
+
const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
|
|
31835
|
+
const installed = installedActivePluginVersion(surface);
|
|
31836
|
+
const plan = buildPluginCachePlan(
|
|
31837
|
+
(0, import_node_os15.homedir)(),
|
|
31838
|
+
running,
|
|
31839
|
+
pluginCacheFsDeps(configRoot, () => 0),
|
|
31840
|
+
{ configRoot, includeStaging: surface !== "codex", installedVersion: installed }
|
|
31841
|
+
);
|
|
31842
|
+
const result = applyPluginCachePlan(
|
|
31843
|
+
plan,
|
|
31844
|
+
(p) => (0, import_node_fs40.rmSync)(p, { recursive: true }),
|
|
31845
|
+
stagingApplyFsGuard(configRoot)
|
|
31846
|
+
);
|
|
31847
|
+
return {
|
|
31848
|
+
removed: [...result.removed, ...result.removedStaging],
|
|
31849
|
+
held: [
|
|
31850
|
+
...result.failed,
|
|
31851
|
+
...result.failedStaging.map((f) => ({ version: f.name, error: f.error }))
|
|
31852
|
+
],
|
|
31853
|
+
kept: [
|
|
31854
|
+
...plan.keep.map((version) => ({
|
|
31855
|
+
version,
|
|
31856
|
+
reason: version === running ? "running" : version === installed ? "installed" : "newest / keep policy"
|
|
31857
|
+
})),
|
|
31858
|
+
...result.skippedStaging.map((s) => ({ version: s.name, reason: s.reason }))
|
|
31859
|
+
]
|
|
31860
|
+
};
|
|
31861
|
+
},
|
|
31862
|
+
// #4201: mmi `.pi-plugin` registration in ~/.pi/agent/settings.json — mirror of jerv-cli's own heal.
|
|
31863
|
+
piPluginState: () => readPiPluginState((0, import_node_os15.homedir)(), process.env),
|
|
31864
|
+
healPiPlugin: () => healPiPluginRegistration((0, import_node_os15.homedir)(), process.env),
|
|
31279
31865
|
// #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
|
|
31280
31866
|
// A local record read ? cheap enough for every lane, including the banner.
|
|
31281
31867
|
sessionPayload: () => readSessionPayload(process.cwd()),
|
|
@@ -31287,14 +31873,14 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
31287
31873
|
const home = (0, import_node_os15.homedir)();
|
|
31288
31874
|
const rows = marketplaceRows(
|
|
31289
31875
|
MMI_MARKETPLACE_NAME,
|
|
31290
|
-
readFileSyncSafe((0,
|
|
31291
|
-
readFileSyncSafe((0,
|
|
31876
|
+
readFileSyncSafe((0, import_node_path38.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs40.readFileSync),
|
|
31877
|
+
readFileSyncSafe((0, import_node_path38.join)(home, ".claude", "settings.json"), import_node_fs40.readFileSync),
|
|
31292
31878
|
// #3974: this CLI heals these rows, so report mode names the doctor run rather than the hand
|
|
31293
31879
|
// edit. Unconditional ? it is a fact about mmi-cli, not about the lane this run is on.
|
|
31294
31880
|
true
|
|
31295
31881
|
);
|
|
31296
31882
|
const pending = readMarketplacePinPending(
|
|
31297
|
-
(0,
|
|
31883
|
+
(0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-marketplace-pin-pending.json"),
|
|
31298
31884
|
MMI_MARKETPLACE_NAME
|
|
31299
31885
|
);
|
|
31300
31886
|
if (!pending) return rows;
|
|
@@ -31320,9 +31906,9 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
31320
31906
|
if (detectSurface(process.env) === "codex") return void 0;
|
|
31321
31907
|
const home = (0, import_node_os15.homedir)();
|
|
31322
31908
|
const names = [MMI_MARKETPLACE_NAME];
|
|
31323
|
-
const result = applyOrgMarketplacePins((0,
|
|
31909
|
+
const result = applyOrgMarketplacePins((0, import_node_path38.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), names);
|
|
31324
31910
|
if (result?.wrote) {
|
|
31325
|
-
writeMarketplacePinPending((0,
|
|
31911
|
+
writeMarketplacePinPending((0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-marketplace-pin-pending.json"), names);
|
|
31326
31912
|
}
|
|
31327
31913
|
return result;
|
|
31328
31914
|
} catch {
|
|
@@ -31338,7 +31924,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
31338
31924
|
// adopted the generated routing index (MMG-Unlive: `docs/Archive/**` only, no index, no gate.yml) would
|
|
31339
31925
|
// get a permanent ? demanding an artifact it never asked for.
|
|
31340
31926
|
docsIndexState: (root) => {
|
|
31341
|
-
if (!(0,
|
|
31927
|
+
if (!(0, import_node_fs40.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return void 0;
|
|
31342
31928
|
const real = createDocsIndexDeps(root);
|
|
31343
31929
|
let docs2;
|
|
31344
31930
|
const listDocs = () => docs2 ??= real.listDocs();
|
|
@@ -31347,7 +31933,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
31347
31933
|
},
|
|
31348
31934
|
// #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
|
|
31349
31935
|
healDocsIndex: (root) => {
|
|
31350
|
-
if (!(0,
|
|
31936
|
+
if (!(0, import_node_fs40.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
|
|
31351
31937
|
const real = createDocsIndexDeps(root);
|
|
31352
31938
|
let docs2;
|
|
31353
31939
|
const listDocs = () => docs2 ??= real.listDocs();
|
|
@@ -31365,6 +31951,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
31365
31951
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
31366
31952
|
removalContext
|
|
31367
31953
|
);
|
|
31954
|
+
for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
|
|
31368
31955
|
return {
|
|
31369
31956
|
removed: result.removed,
|
|
31370
31957
|
stillQueued: result.stillDeferred.length,
|
|
@@ -31681,19 +32268,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
31681
32268
|
});
|
|
31682
32269
|
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
31683
32270
|
rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
|
|
31684
|
-
const path2 = (0,
|
|
31685
|
-
const current = (0,
|
|
32271
|
+
const path2 = (0, import_node_path38.join)(process.cwd(), ".gitignore");
|
|
32272
|
+
const current = (0, import_node_fs40.existsSync)(path2) ? (0, import_node_fs40.readFileSync)(path2, "utf8") : null;
|
|
31686
32273
|
const plan = planManagedGitignore(current);
|
|
31687
32274
|
const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
|
|
31688
32275
|
if (opts.json) {
|
|
31689
|
-
if (opts.write && plan.changed) (0,
|
|
32276
|
+
if (opts.write && plan.changed) (0, import_node_fs40.writeFileSync)(path2, plan.content, "utf8");
|
|
31690
32277
|
console.log(JSON.stringify(plan, null, 2));
|
|
31691
32278
|
if (!opts.write && plan.changed) process.exitCode = 1;
|
|
31692
32279
|
return;
|
|
31693
32280
|
}
|
|
31694
32281
|
if (opts.write) {
|
|
31695
32282
|
if (plan.changed) {
|
|
31696
|
-
(0,
|
|
32283
|
+
(0, import_node_fs40.writeFileSync)(path2, plan.content, "utf8");
|
|
31697
32284
|
console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
|
|
31698
32285
|
} else {
|
|
31699
32286
|
console.log("mmi-cli org rules gitignore: up to date");
|
|
@@ -31819,6 +32406,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
31819
32406
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
31820
32407
|
{ removalContext }
|
|
31821
32408
|
);
|
|
32409
|
+
for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
|
|
31822
32410
|
if (o.json) return console.log(JSON.stringify(result));
|
|
31823
32411
|
if (!o.quiet || result.removed.length || result.stillDeferred.length || result.skipped.length) {
|
|
31824
32412
|
if (result.removed.length) console.log(`worktree gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
|
|
@@ -31834,7 +32422,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
31834
32422
|
process.exit(process.exitCode ?? 0);
|
|
31835
32423
|
});
|
|
31836
32424
|
});
|
|
31837
|
-
gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to
|
|
32425
|
+
gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to read PER BRANCH ? an effort bound, not a correctness one: merge state is resolved per branch, so no branch is ever skipped for being old (#4227)", "200").option("--force", "remove worktrees even when the ownership registry shows another session created or worked in them recently (#3580)").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471) ? descends into this repo container when present; ownership, dead-dir, and content guards still apply").action(async (o) => {
|
|
31838
32426
|
if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
|
|
31839
32427
|
if (o.scratch) {
|
|
31840
32428
|
try {
|
|
@@ -31850,8 +32438,8 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
31850
32438
|
if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
|
|
31851
32439
|
let root;
|
|
31852
32440
|
if (o.root !== void 0) {
|
|
31853
|
-
root = (0,
|
|
31854
|
-
if (!(0,
|
|
32441
|
+
root = (0, import_node_path38.resolve)(o.root);
|
|
32442
|
+
if (!(0, import_node_fs40.existsSync)(root) || !(0, import_node_fs40.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
|
|
31855
32443
|
const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
31856
32444
|
if (isPathUnderDirectory(gcRepoRoot, root)) {
|
|
31857
32445
|
return fail(`worktree gc: --root ${root} contains this checkout ? name a worktrees root, not the repo or an ancestor of it`);
|
|
@@ -31866,13 +32454,14 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
31866
32454
|
if (o.apply) {
|
|
31867
32455
|
const deferredStore = await createDeferredWorktreeStore();
|
|
31868
32456
|
const removalContext = await currentWorktreeRemovalContext("worktree gc", o.force);
|
|
31869
|
-
await sweepDeferredWorktrees(
|
|
32457
|
+
const sweepResult = await sweepDeferredWorktrees(
|
|
31870
32458
|
deferredStore,
|
|
31871
32459
|
// #2841: same `-c core.fsmonitor=false` guard as the detached `gc sweep-deferred` worker ? keep a
|
|
31872
32460
|
// git fsmonitor daemon from inheriting this sweep's stdio pipe and wedging the git call on Windows.
|
|
31873
32461
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
31874
32462
|
removalContext
|
|
31875
32463
|
).catch(() => void 0);
|
|
32464
|
+
for (const removedPath of sweepResult?.removed ?? []) await bestEffortLeaseClose(removedPath);
|
|
31876
32465
|
applyResult = await applyGcPlan(plan, o.remote, { root, force: o.force });
|
|
31877
32466
|
}
|
|
31878
32467
|
if (o.json) {
|
|
@@ -31893,11 +32482,11 @@ var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
|
|
|
31893
32482
|
function runWorktreeInstall(command, cwd, quiet, opts) {
|
|
31894
32483
|
const stdio = quiet ? "ignore" : "inherit";
|
|
31895
32484
|
return new Promise((resolve5, reject) => {
|
|
31896
|
-
const child2 = opts?.shell ? (0,
|
|
32485
|
+
const child2 = opts?.shell ? (0, import_node_child_process19.spawn)(command, { cwd, stdio, windowsHide: true, shell: true }) : (() => {
|
|
31897
32486
|
const [bin, ...args] = command.split(" ");
|
|
31898
32487
|
const file = isWin2 ? "cmd.exe" : bin;
|
|
31899
32488
|
const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
|
|
31900
|
-
return (0,
|
|
32489
|
+
return (0, import_node_child_process19.spawn)(file, spawnArgs, { cwd, stdio, windowsHide: true });
|
|
31901
32490
|
})();
|
|
31902
32491
|
const timer = setTimeout(() => {
|
|
31903
32492
|
try {
|
|
@@ -31930,7 +32519,7 @@ async function currentWorktreeRemovalContext(command, force) {
|
|
|
31930
32519
|
};
|
|
31931
32520
|
}
|
|
31932
32521
|
async function unprovenWorktreeReason(wtPath, repoRoot2) {
|
|
31933
|
-
if (!(0,
|
|
32522
|
+
if (!(0, import_node_fs40.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
|
|
31934
32523
|
const porcelain = (await execFileP2("git", ["-C", repoRoot2, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
|
|
31935
32524
|
const registered = parseWorktreePorcelainEntries(porcelain);
|
|
31936
32525
|
if (!registered.length) {
|
|
@@ -31952,26 +32541,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
|
|
|
31952
32541
|
function acquireWorktreeSetupLock(worktreeRoot) {
|
|
31953
32542
|
const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
|
|
31954
32543
|
const take = () => {
|
|
31955
|
-
const fd = (0,
|
|
32544
|
+
const fd = (0, import_node_fs40.openSync)(lockPath, "wx");
|
|
31956
32545
|
try {
|
|
31957
|
-
(0,
|
|
32546
|
+
(0, import_node_fs40.writeSync)(fd, String(Date.now()));
|
|
31958
32547
|
} finally {
|
|
31959
|
-
(0,
|
|
32548
|
+
(0, import_node_fs40.closeSync)(fd);
|
|
31960
32549
|
}
|
|
31961
32550
|
return () => {
|
|
31962
32551
|
try {
|
|
31963
|
-
(0,
|
|
32552
|
+
(0, import_node_fs40.rmSync)(lockPath, { force: true });
|
|
31964
32553
|
} catch {
|
|
31965
32554
|
}
|
|
31966
32555
|
};
|
|
31967
32556
|
};
|
|
31968
32557
|
try {
|
|
31969
|
-
(0,
|
|
32558
|
+
(0, import_node_fs40.mkdirSync)((0, import_node_path38.dirname)(lockPath), { recursive: true });
|
|
31970
32559
|
return take();
|
|
31971
32560
|
} catch {
|
|
31972
32561
|
try {
|
|
31973
|
-
if (Date.now() - (0,
|
|
31974
|
-
(0,
|
|
32562
|
+
if (Date.now() - (0, import_node_fs40.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
|
|
32563
|
+
(0, import_node_fs40.rmSync)(lockPath, { force: true });
|
|
31975
32564
|
return take();
|
|
31976
32565
|
}
|
|
31977
32566
|
} catch {
|
|
@@ -32108,6 +32697,34 @@ withExamples(mutating(
|
|
|
32108
32697
|
const owner = { path: wtPath, branch, createdAt, lastSeenAt: createdAt, actor: createActor };
|
|
32109
32698
|
recordWorktreeOwner(repoRoot2, owner);
|
|
32110
32699
|
appendWorktreeEvent(repoRoot2, { action: "created", command: "worktree create", target: wtPath, branch, actor: owner.actor });
|
|
32700
|
+
let lease;
|
|
32701
|
+
try {
|
|
32702
|
+
await execFileP2("jerv-cli", [
|
|
32703
|
+
"lease",
|
|
32704
|
+
"open",
|
|
32705
|
+
"--kind",
|
|
32706
|
+
"worktree",
|
|
32707
|
+
"--ref",
|
|
32708
|
+
wtPath,
|
|
32709
|
+
...selector ? ["--repo", selector.repo] : [],
|
|
32710
|
+
// 72h, not the 24h default: a feature worktree routinely sits untouched over a weekend, and
|
|
32711
|
+
// an expired lease is what makes a CLEAN, unheld tree reapable. Erring long costs disk;
|
|
32712
|
+
// erring short reaps a tree someone is still using between sessions.
|
|
32713
|
+
"--ttl",
|
|
32714
|
+
"72",
|
|
32715
|
+
"--note",
|
|
32716
|
+
`${branch} via mmi-cli worktree create`
|
|
32717
|
+
], { timeout: GIT_TIMEOUT_MS });
|
|
32718
|
+
lease = { ok: true };
|
|
32719
|
+
} catch (e) {
|
|
32720
|
+
const detail = (e.stderr?.trim() || e.message.trim()).split("\n")[0];
|
|
32721
|
+
lease = { ok: false, error: detail };
|
|
32722
|
+
if (!o.json) {
|
|
32723
|
+
console.error(
|
|
32724
|
+
` worktree created, but no jerv lease was opened for it: ${detail} \u2014 nothing on the lease plane can expire this tree, so it will need manual cleanup (MMI-Hub#4231).`
|
|
32725
|
+
);
|
|
32726
|
+
}
|
|
32727
|
+
}
|
|
32111
32728
|
if (o.json) {
|
|
32112
32729
|
return console.log(JSON.stringify({
|
|
32113
32730
|
branch,
|
|
@@ -32115,6 +32732,7 @@ withExamples(mutating(
|
|
|
32115
32732
|
base,
|
|
32116
32733
|
resumed,
|
|
32117
32734
|
...report,
|
|
32735
|
+
lease,
|
|
32118
32736
|
...issueForm && selector ? { issue: `${selector.repo}#${selector.number}` } : {},
|
|
32119
32737
|
...issueForm && o.claim ? { claim: claimError ? { ok: false, error: claimError } : claim } : {}
|
|
32120
32738
|
}, null, 2));
|
|
@@ -32226,7 +32844,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
32226
32844
|
try {
|
|
32227
32845
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
|
|
32228
32846
|
if (o.repo) args.push("--repo", o.repo);
|
|
32229
|
-
spawnDetachedSelf(args, { spawn:
|
|
32847
|
+
spawnDetachedSelf(args, { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
|
|
32230
32848
|
} catch {
|
|
32231
32849
|
}
|
|
32232
32850
|
}
|
|
@@ -32554,48 +33172,6 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
|
|
|
32554
33172
|
await failGraceful(e.message);
|
|
32555
33173
|
}
|
|
32556
33174
|
});
|
|
32557
|
-
var docsAudit = program2.command("docs-audit").description("the docs janitor verdict ledger ? record a dated run verdict, or read the dead-man status back");
|
|
32558
|
-
docsAudit.command("record").description("write a dated janitor verdict for one repo to the registry ledger (master-only server-side)").option("--repo <owner/name>", "the repo the verdict is for (default: the current repo)").option("--date <YYYY-MM-DD>", "the ISO day the run examined (default: today)").requiredOption("--sha-range <a..b>", "the git range the janitor read (e.g. <lastVerdictSha>..HEAD)").addOption(new Option("--outcome <kind>", "clean | refreshed | failed").makeOptionMandatory().choices(["clean", "refreshed", "failed"])).option("--count <n>", "docs refreshed (required when --outcome refreshed)").option("--reason <text>", "why the run failed (required when --outcome failed)").requiredOption("--checker-vendor <vendor>", "which vendor's model actually ran the check").action(async (o) => {
|
|
32559
|
-
try {
|
|
32560
|
-
const repo = o.repo ?? await currentRepoFullName();
|
|
32561
|
-
const date = o.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
32562
|
-
let outcome;
|
|
32563
|
-
if (o.outcome === "clean") outcome = { kind: "clean" };
|
|
32564
|
-
else if (o.outcome === "refreshed") outcome = { kind: "refreshed", count: Number(o.count) };
|
|
32565
|
-
else if (o.outcome === "failed") outcome = { kind: "failed", reason: o.reason ?? "" };
|
|
32566
|
-
else return failGraceful(`docs audit record: --outcome must be clean|refreshed|failed, got "${o.outcome}"`);
|
|
32567
|
-
const verdict = docsAuditRecord({ repo, date, shaRange: o.shaRange, outcome, checkerVendor: o.checkerVendor });
|
|
32568
|
-
await reportWrite("docs-audit record", await recordDocsAudit(verdict, registryClientDeps(await loadConfig())));
|
|
32569
|
-
} catch (e) {
|
|
32570
|
-
await failGraceful(e.message);
|
|
32571
|
-
}
|
|
32572
|
-
});
|
|
32573
|
-
docsAudit.command("status").description("read the janitor dead-man verdict back for one repo ? missing/stale/failed is RED; a not-yet-armed registry route is an informational exit 0").option("--repo <owner/name>", "the repo to check (default: the current repo)").option("--json", "machine-readable output ? the discrete state rather than the sentence").action(async (o) => {
|
|
32574
|
-
try {
|
|
32575
|
-
const repo = o.repo ?? await currentRepoFullName();
|
|
32576
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
32577
|
-
const fetched = await readDocsAuditFetch(repo);
|
|
32578
|
-
const result = docsAuditStatus(fetched, { repo, today });
|
|
32579
|
-
if (o.json) {
|
|
32580
|
-
const verdict = "ok" in fetched && fetched.ok ? fetched.verdict : null;
|
|
32581
|
-
console.log(JSON.stringify({
|
|
32582
|
-
repo,
|
|
32583
|
-
armed: !("notArmed" in fetched),
|
|
32584
|
-
ok: result.ok,
|
|
32585
|
-
state: result.state,
|
|
32586
|
-
date: verdict?.date ?? null,
|
|
32587
|
-
outcome: verdict?.outcome ?? null,
|
|
32588
|
-
checkerVendor: verdict?.checkerVendor ?? null,
|
|
32589
|
-
line: result.line
|
|
32590
|
-
}, null, 2));
|
|
32591
|
-
} else {
|
|
32592
|
-
console.log(result.line);
|
|
32593
|
-
}
|
|
32594
|
-
if (!result.ok) process.exitCode = 1;
|
|
32595
|
-
} catch (e) {
|
|
32596
|
-
await failGraceful(e.message);
|
|
32597
|
-
}
|
|
32598
|
-
});
|
|
32599
33175
|
async function reportWrite(label, res) {
|
|
32600
33176
|
if (res.ok) {
|
|
32601
33177
|
console.log(JSON.stringify(res.body));
|
|
@@ -32832,7 +33408,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
32832
33408
|
if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
|
|
32833
33409
|
if (o.secretsFile) {
|
|
32834
33410
|
try {
|
|
32835
|
-
vars.push(`secrets=${(0,
|
|
33411
|
+
vars.push(`secrets=${(0, import_node_fs40.readFileSync)(o.secretsFile, "utf8")}`);
|
|
32836
33412
|
} catch (e) {
|
|
32837
33413
|
return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
|
|
32838
33414
|
}
|
|
@@ -33255,6 +33831,7 @@ withExamples(mutating(
|
|
|
33255
33831
|
}
|
|
33256
33832
|
}
|
|
33257
33833
|
if (o.related !== false) scheduleRelatedDiscovery({ repo: o.repo, number: created.number, title, body });
|
|
33834
|
+
invalidateStatuslineBoardCache();
|
|
33258
33835
|
console.log(JSON.stringify({
|
|
33259
33836
|
...created,
|
|
33260
33837
|
label: issueType,
|
|
@@ -33555,6 +34132,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
33555
34132
|
return fail(`pr create: ${docsCheck.detail} ? ${docsCheck.fix}`);
|
|
33556
34133
|
}
|
|
33557
34134
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
34135
|
+
invalidateStatuslineBoardCache();
|
|
33558
34136
|
console.log(JSON.stringify(created));
|
|
33559
34137
|
}), [
|
|
33560
34138
|
'mmi-cli pr create --title "Add the schema" --body "Closes #2680"',
|
|
@@ -33584,11 +34162,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
33584
34162
|
}
|
|
33585
34163
|
});
|
|
33586
34164
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
33587
|
-
const wfDir = (0,
|
|
33588
|
-
if (!(0,
|
|
33589
|
-
return (0,
|
|
34165
|
+
const wfDir = (0, import_node_path38.join)(cwd, ".github", "workflows");
|
|
34166
|
+
if (!(0, import_node_fs40.existsSync)(wfDir)) return [];
|
|
34167
|
+
return (0, import_node_fs40.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
33590
34168
|
try {
|
|
33591
|
-
return workflowReportsPrChecks((0,
|
|
34169
|
+
return workflowReportsPrChecks((0, import_node_fs40.readFileSync)((0, import_node_path38.join)(wfDir, name), "utf8"));
|
|
33592
34170
|
} catch {
|
|
33593
34171
|
return true;
|
|
33594
34172
|
}
|
|
@@ -33620,16 +34198,16 @@ function ciAuditDeps() {
|
|
|
33620
34198
|
// gate re-seed step is skipped gracefully rather than failing mid-run.
|
|
33621
34199
|
readSeedFile: (path2) => {
|
|
33622
34200
|
if (!root) return null;
|
|
33623
|
-
const fullPath = (0,
|
|
33624
|
-
return (0,
|
|
34201
|
+
const fullPath = (0, import_node_path38.join)(root, path2);
|
|
34202
|
+
return (0, import_node_fs40.existsSync)(fullPath) ? (0, import_node_fs40.readFileSync)(fullPath, "utf8") : null;
|
|
33625
34203
|
}
|
|
33626
34204
|
};
|
|
33627
34205
|
}
|
|
33628
34206
|
function hubRoot() {
|
|
33629
|
-
const fromPkg = (0,
|
|
34207
|
+
const fromPkg = (0, import_node_path38.join)(__dirname, "..", "..");
|
|
33630
34208
|
const marker = "skills/bootstrap/seeds/manifest.json";
|
|
33631
|
-
if ((0,
|
|
33632
|
-
if ((0,
|
|
34209
|
+
if ((0, import_node_fs40.existsSync)((0, import_node_path38.join)(fromPkg, marker))) return fromPkg;
|
|
34210
|
+
if ((0, import_node_fs40.existsSync)((0, import_node_path38.join)(process.cwd(), marker))) return process.cwd();
|
|
33633
34211
|
return null;
|
|
33634
34212
|
}
|
|
33635
34213
|
pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
|
|
@@ -33940,7 +34518,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
33940
34518
|
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
33941
34519
|
beforeWorktrees,
|
|
33942
34520
|
startingPath,
|
|
33943
|
-
pathExists: (p) => (0,
|
|
34521
|
+
pathExists: (p) => (0, import_node_fs40.existsSync)(p),
|
|
33944
34522
|
execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
33945
34523
|
teardownWorktreeStage,
|
|
33946
34524
|
deferredStore,
|
|
@@ -33954,6 +34532,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
33954
34532
|
preserveWorktree: o.preserveWorktree,
|
|
33955
34533
|
removalContext
|
|
33956
34534
|
});
|
|
34535
|
+
if (localCleanup.worktree?.status === "removed") await bestEffortLeaseClose(localCleanup.worktree.path);
|
|
33957
34536
|
} catch (e) {
|
|
33958
34537
|
localCleanup = {
|
|
33959
34538
|
branch: headRef,
|
|
@@ -33966,6 +34545,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
33966
34545
|
};
|
|
33967
34546
|
}
|
|
33968
34547
|
const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
|
|
34548
|
+
invalidateStatuslineBoardCache();
|
|
33969
34549
|
console.log(JSON.stringify({
|
|
33970
34550
|
...buildPrMergeResultPayload({
|
|
33971
34551
|
number,
|
|
@@ -34434,19 +35014,19 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
34434
35014
|
targets = resolution.targets;
|
|
34435
35015
|
}
|
|
34436
35016
|
const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
|
|
34437
|
-
const fileMatrix = (0,
|
|
35017
|
+
const fileMatrix = (0, import_node_fs40.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs40.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
34438
35018
|
const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
|
|
34439
35019
|
const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
|
|
34440
|
-
const fileContracts = (0,
|
|
35020
|
+
const fileContracts = (0, import_node_fs40.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs40.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
|
|
34441
35021
|
const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
|
|
34442
|
-
const sanctioned = (0,
|
|
35022
|
+
const sanctioned = (0, import_node_fs40.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs40.readFileSync)("access-matrix.json", "utf8")) : {};
|
|
34443
35023
|
const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
|
|
34444
35024
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
34445
35025
|
if (!report.ok) process.exitCode = 1;
|
|
34446
35026
|
});
|
|
34447
35027
|
access.command("capabilities").description("enumerate your effective vault reach ? every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
|
|
34448
35028
|
var isWin2 = process.platform === "win32";
|
|
34449
|
-
program2.command("doctor").description("heal CLI/plugin wiring and clean up repo cruft ? repairs run by default (#3975); use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; read-only, skips slow checks and network probes").option("--preflight", "
|
|
35029
|
+
program2.command("doctor").description("heal CLI/plugin wiring and clean up repo cruft ? repairs run by default (#3975); use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; read-only, skips slow checks and network probes").option("--preflight", "read-only fast gate for automation: measures and reports with the shared exit code, zero writes (#4199, canon); heal env drift with a plain run or --no-repo-writes").option("--verbose", "print the evidence behind every check ? probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (an output format ? repairs still run by lane)").option("--apply", "deprecated no-op: repairs run by default now (#3975); kept so older instructions still parse").option("--no-repo-writes", "env/plugin repairs only ? never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nA plain run heals env drift (CLI, plugin, marketplace pins) and cleans repo cruft (gitignore block,\nmerged branches, dead worktrees, aged scratch) automatically (#3975). --no-repo-writes keeps the\nworking tree untouched for train preflights; --banner/--fast/--self are read-only lanes.\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
|
|
34450
35030
|
if (opts.guide) {
|
|
34451
35031
|
consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
|
|
34452
35032
|
return;
|
|
@@ -34471,16 +35051,16 @@ function directoryBytes(path2) {
|
|
|
34471
35051
|
let total = 0;
|
|
34472
35052
|
let entries;
|
|
34473
35053
|
try {
|
|
34474
|
-
entries = (0,
|
|
35054
|
+
entries = (0, import_node_fs40.readdirSync)(path2, { withFileTypes: true });
|
|
34475
35055
|
} catch {
|
|
34476
35056
|
return 0;
|
|
34477
35057
|
}
|
|
34478
35058
|
for (const entry of entries) {
|
|
34479
|
-
const child2 = (0,
|
|
35059
|
+
const child2 = (0, import_node_path38.join)(path2, entry.name);
|
|
34480
35060
|
if (entry.isDirectory()) total += directoryBytes(child2);
|
|
34481
35061
|
else {
|
|
34482
35062
|
try {
|
|
34483
|
-
total += (0,
|
|
35063
|
+
total += (0, import_node_fs40.statSync)(child2).size;
|
|
34484
35064
|
} catch {
|
|
34485
35065
|
}
|
|
34486
35066
|
}
|
|
@@ -34488,25 +35068,25 @@ function directoryBytes(path2) {
|
|
|
34488
35068
|
return total;
|
|
34489
35069
|
}
|
|
34490
35070
|
function listDirEntries(dir) {
|
|
34491
|
-
return (0,
|
|
35071
|
+
return (0, import_node_fs40.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
|
|
34492
35072
|
}
|
|
34493
35073
|
function readInstalledPluginRefs(configRoot) {
|
|
34494
35074
|
const p = installedPluginsPathForConfig(configRoot);
|
|
34495
|
-
if (!(0,
|
|
35075
|
+
if (!(0, import_node_fs40.existsSync)(p)) return [];
|
|
34496
35076
|
try {
|
|
34497
|
-
return installedPluginPaths((0,
|
|
35077
|
+
return installedPluginPaths((0, import_node_fs40.readFileSync)(p, "utf8"));
|
|
34498
35078
|
} catch {
|
|
34499
35079
|
return null;
|
|
34500
35080
|
}
|
|
34501
35081
|
}
|
|
34502
35082
|
function pluginCacheFsDeps(configRoot, dirBytes) {
|
|
34503
35083
|
return {
|
|
34504
|
-
exists: (p) => (0,
|
|
34505
|
-
listVersionDirs: (root) => (0,
|
|
35084
|
+
exists: (p) => (0, import_node_fs40.existsSync)(p),
|
|
35085
|
+
listVersionDirs: (root) => (0, import_node_fs40.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
|
|
34506
35086
|
dirBytes,
|
|
34507
|
-
listStagingDirs: (root) => (0,
|
|
35087
|
+
listStagingDirs: (root) => (0, import_node_fs40.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
34508
35088
|
try {
|
|
34509
|
-
return { name: d.name, mtimeMs: newestMtimeMs((0,
|
|
35089
|
+
return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path38.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs40.statSync)(p).mtimeMs) };
|
|
34510
35090
|
} catch {
|
|
34511
35091
|
return { name: d.name, mtimeMs: Date.now() };
|
|
34512
35092
|
}
|
|
@@ -34520,10 +35100,10 @@ function stagingApplyFsGuard(configRoot) {
|
|
|
34520
35100
|
return {
|
|
34521
35101
|
referencedPaths: () => readInstalledPluginRefs(configRoot),
|
|
34522
35102
|
mtimeMs: (name) => {
|
|
34523
|
-
const p = (0,
|
|
34524
|
-
if (!(0,
|
|
35103
|
+
const p = (0, import_node_path38.join)(stagingRoot, name);
|
|
35104
|
+
if (!(0, import_node_fs40.existsSync)(p)) return null;
|
|
34525
35105
|
try {
|
|
34526
|
-
return newestMtimeMs(p, listDirEntries, (q) => (0,
|
|
35106
|
+
return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs40.statSync)(q).mtimeMs);
|
|
34527
35107
|
} catch {
|
|
34528
35108
|
return null;
|
|
34529
35109
|
}
|
|
@@ -34549,7 +35129,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
|
|
|
34549
35129
|
{ withBytes: true, configRoot, includeStaging: surface !== "codex" }
|
|
34550
35130
|
);
|
|
34551
35131
|
const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
|
|
34552
|
-
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0,
|
|
35132
|
+
const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs40.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
|
|
34553
35133
|
const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
|
|
34554
35134
|
if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
|
|
34555
35135
|
else console.log(renderPluginCachePlan(plan, result));
|
|
@@ -34612,7 +35192,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
34612
35192
|
for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
|
|
34613
35193
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
34614
35194
|
if (worktreeBanner) {
|
|
34615
|
-
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn:
|
|
35195
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
34616
35196
|
bannerIo.log(worktreeBanner);
|
|
34617
35197
|
}
|
|
34618
35198
|
if (isLinkedWorktree(process.cwd())) {
|