@mutmutco/cli 3.95.0 → 3.96.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 +377 -34
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -16990,7 +16990,7 @@ async function boardDoctor(options, deps = {}) {
|
|
|
16990
16990
|
};
|
|
16991
16991
|
await Promise.all(Array.from({ length: Math.min(concurrency, suspicious.length) }, () => ghostWorker()));
|
|
16992
16992
|
}
|
|
16993
|
-
const claimedItems = collected.items.filter(
|
|
16993
|
+
const claimedItems = options.skipClaimLiveness ? [] : collected.items.filter(
|
|
16994
16994
|
(item) => item.contentType === "Issue" && item.assignees.length > 0 && item.status === "In Progress"
|
|
16995
16995
|
);
|
|
16996
16996
|
const liveClaims = [];
|
|
@@ -19342,6 +19342,12 @@ var INDEXABLE_EXT = /* @__PURE__ */ new Set([
|
|
|
19342
19342
|
".html"
|
|
19343
19343
|
]);
|
|
19344
19344
|
var SYMBOL_RE = /^(?:export\s+)?(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\s+([A-Za-z_$][\w$]*)/gm;
|
|
19345
|
+
function repoIndexPathTieBreak(path2) {
|
|
19346
|
+
const p = path2.replace(/\\/g, "/");
|
|
19347
|
+
if (/(^|\/)__tests__\//.test(p) || /(^|\/)tests?\//.test(p)) return 1;
|
|
19348
|
+
if (/\.(test|spec)\.[^.]+$/i.test(p)) return 1;
|
|
19349
|
+
return 0;
|
|
19350
|
+
}
|
|
19345
19351
|
function repoIndexStorePath(cwd) {
|
|
19346
19352
|
return repoRuntimeStatePath(cwd, "repo-index", "index.json");
|
|
19347
19353
|
}
|
|
@@ -19552,7 +19558,9 @@ function searchRepoIndex(idx, query, limit = 20) {
|
|
|
19552
19558
|
}
|
|
19553
19559
|
}
|
|
19554
19560
|
}
|
|
19555
|
-
hits.sort(
|
|
19561
|
+
hits.sort(
|
|
19562
|
+
(a, b) => b.score - a.score || repoIndexPathTieBreak(a.path) - repoIndexPathTieBreak(b.path) || a.path.localeCompare(b.path)
|
|
19563
|
+
);
|
|
19556
19564
|
const seen = /* @__PURE__ */ new Set();
|
|
19557
19565
|
const out = [];
|
|
19558
19566
|
for (const h of hits) {
|
|
@@ -19568,10 +19576,10 @@ function inferRepoSlug(cwd, exec = import_node_child_process11.execFileSync) {
|
|
|
19568
19576
|
try {
|
|
19569
19577
|
const url = String(exec("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" })).trim();
|
|
19570
19578
|
const m = /[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
|
|
19571
|
-
if (m?.[1]) return m[1];
|
|
19579
|
+
if (m?.[1]) return m[1].toLowerCase();
|
|
19572
19580
|
} catch {
|
|
19573
19581
|
}
|
|
19574
|
-
return (0, import_node_path20.basename)(cwd) || "local";
|
|
19582
|
+
return ((0, import_node_path20.basename)(cwd) || "local").toLowerCase();
|
|
19575
19583
|
}
|
|
19576
19584
|
|
|
19577
19585
|
// src/repo-index-cloud-client.ts
|
|
@@ -19718,8 +19726,11 @@ var import_node_child_process12 = require("node:child_process");
|
|
|
19718
19726
|
var MAX_EMBED_BACKFILL_ROUNDS = 40;
|
|
19719
19727
|
function normalizeRepo(raw) {
|
|
19720
19728
|
const t = raw.trim().replace(/\.git$/, "");
|
|
19721
|
-
if (t.includes("/"))
|
|
19722
|
-
|
|
19729
|
+
if (t.includes("/")) {
|
|
19730
|
+
const [owner, name] = t.split("/");
|
|
19731
|
+
return `${(owner || "").toLowerCase()}/${(name || "").toLowerCase()}`;
|
|
19732
|
+
}
|
|
19733
|
+
return `mutmutco/${t.toLowerCase()}`;
|
|
19723
19734
|
}
|
|
19724
19735
|
function rosterRepos(projects) {
|
|
19725
19736
|
const set = /* @__PURE__ */ new Set();
|
|
@@ -19730,10 +19741,11 @@ function rosterRepos(projects) {
|
|
|
19730
19741
|
return [...set].sort((a, b) => a.localeCompare(b));
|
|
19731
19742
|
}
|
|
19732
19743
|
function shallowClone(repo, dest, token) {
|
|
19744
|
+
const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
|
|
19733
19745
|
(0, import_node_child_process12.execFileSync)(
|
|
19734
19746
|
"git",
|
|
19735
|
-
["-c", `http.extraHeader=Authorization:
|
|
19736
|
-
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
19747
|
+
["-c", `http.extraHeader=Authorization: Basic ${basic}`, "clone", "--depth", "1", "--single-branch", `https://github.com/${repo}.git`, dest],
|
|
19748
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
|
|
19737
19749
|
);
|
|
19738
19750
|
}
|
|
19739
19751
|
async function syncEstateRepoIndex(opts) {
|
|
@@ -27866,6 +27878,11 @@ async function recommendNext(repo, deps) {
|
|
|
27866
27878
|
const top = sorted[0];
|
|
27867
27879
|
return { number: top.number, title: top.title, url: top.url, priority: top.priority, repo: top.repository };
|
|
27868
27880
|
}
|
|
27881
|
+
function requiredEstateCommandsPresent(commandNames) {
|
|
27882
|
+
const required = ["repo-index"];
|
|
27883
|
+
const present = new Set(commandNames);
|
|
27884
|
+
return required.filter((name) => !present.has(name));
|
|
27885
|
+
}
|
|
27869
27886
|
function onboardPluginGate(deps) {
|
|
27870
27887
|
const { registered, declared, ref } = readKnownMarketplace(deps.readKnown(), MMI_MARKETPLACE_NAME);
|
|
27871
27888
|
if (!registered) {
|
|
@@ -27883,8 +27900,13 @@ function onboardPluginGate(deps) {
|
|
|
27883
27900
|
if (catalog && !catalog.agrees) gaps.push(`the catalog is ${catalog.unpinned ? "unpinned" : `pinned to ${catalog.ref}`} \u2014 ${CATALOG_REF_PIN_STEPS}`);
|
|
27884
27901
|
return gaps.length === 0 ? { ok: true, detail: `auto-update on, catalog pinned to ${catalog?.ref}` } : { ok: false, detail: gaps.join("; ") };
|
|
27885
27902
|
}
|
|
27886
|
-
async function collectOnboardStatus() {
|
|
27903
|
+
async function collectOnboardStatus(opts = {}) {
|
|
27887
27904
|
const cfg = await loadConfig();
|
|
27905
|
+
const missingEstate = requiredEstateCommandsPresent(opts.commandNames ?? []);
|
|
27906
|
+
const estateCli = missingEstate.length === 0 ? { ok: true, detail: "estate commands present (repo-index)" } : {
|
|
27907
|
+
ok: false,
|
|
27908
|
+
detail: `global CLI missing ${missingEstate.join(", ")} \u2014 run mmi-cli doctor to heal`
|
|
27909
|
+
};
|
|
27888
27910
|
let track = "unknown";
|
|
27889
27911
|
try {
|
|
27890
27912
|
const revParse2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
@@ -27938,7 +27960,9 @@ async function collectOnboardStatus() {
|
|
|
27938
27960
|
}
|
|
27939
27961
|
}
|
|
27940
27962
|
let nextCommand = "mmi-cli doctor";
|
|
27941
|
-
if (!
|
|
27963
|
+
if (!estateCli.ok) {
|
|
27964
|
+
nextCommand = "mmi-cli doctor \u2014 heal global CLI (missing repo-index)";
|
|
27965
|
+
} else if (!cfg.sagaApiUrl) {
|
|
27942
27966
|
nextCommand = "mmi-cli doctor \u2014 fix Hub API URL configuration first";
|
|
27943
27967
|
} else if (!registry2.ok) {
|
|
27944
27968
|
nextCommand = "mmi-cli org project set <owner/repo>";
|
|
@@ -27961,7 +27985,7 @@ async function collectOnboardStatus() {
|
|
|
27961
27985
|
readKnown: () => readFileSyncSafe((0, import_node_path36.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs37.readFileSync),
|
|
27962
27986
|
readSettings: () => readFileSyncSafe((0, import_node_path36.join)(home, ".claude", "settings.json"), import_node_fs37.readFileSync)
|
|
27963
27987
|
});
|
|
27964
|
-
return { track, board, registry: registry2, secrets, plugin, nextCommand };
|
|
27988
|
+
return { track, board, registry: registry2, secrets, plugin, estateCli, nextCommand };
|
|
27965
27989
|
}
|
|
27966
27990
|
function registerDiscoveryCommands(program3) {
|
|
27967
27991
|
program3.command("status").description("unified repo snapshot: branch, worktrees, open PRs (yours), claimed board items, stage state").option("--json", "machine-readable output").action(async (o) => {
|
|
@@ -27998,7 +28022,8 @@ function registerDiscoveryCommands(program3) {
|
|
|
27998
28022
|
});
|
|
27999
28023
|
program3.command("onboard").description("report repo track/board/secrets/registry readiness and the concrete next command").option("--json", "machine-readable output").action(async (o) => {
|
|
28000
28024
|
try {
|
|
28001
|
-
const
|
|
28025
|
+
const commandNames = program3.commands.map((c) => c.name());
|
|
28026
|
+
const report = await collectOnboardStatus({ commandNames });
|
|
28002
28027
|
if (o.json) {
|
|
28003
28028
|
console.log(JSON.stringify(report, null, 2));
|
|
28004
28029
|
} else {
|
|
@@ -28007,6 +28032,7 @@ function registerDiscoveryCommands(program3) {
|
|
|
28007
28032
|
console.log(`Registry: ${report.registry.ok ? "\u2713" : "\u2717"} ${report.registry.detail}`);
|
|
28008
28033
|
console.log(`Secrets: ${report.secrets.ok ? "\u2713" : "\u2717"} ${report.secrets.detail}`);
|
|
28009
28034
|
console.log(`Plugin: ${report.plugin.ok ? "\u2713" : "\u2717"} ${report.plugin.detail}`);
|
|
28035
|
+
console.log(`Estate CLI: ${report.estateCli.ok ? "\u2713" : "\u2717"} ${report.estateCli.detail}`);
|
|
28010
28036
|
console.log(`
|
|
28011
28037
|
Next command: ${report.nextCommand}`);
|
|
28012
28038
|
}
|
|
@@ -30056,7 +30082,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30056
30082
|
if (!opts.json) io.log(`\u21BB ${line}`);
|
|
30057
30083
|
};
|
|
30058
30084
|
const healStep = (message) => {
|
|
30059
|
-
if (!opts.json) io.log(`${VERBOSE_INDENT}${message.trim()}`);
|
|
30085
|
+
if (!opts.json && opts.verbose) io.log(`${VERBOSE_INDENT}${message.trim()}`);
|
|
30060
30086
|
};
|
|
30061
30087
|
const releasedNote = deps.releasedVersionNote?.();
|
|
30062
30088
|
let pluginHealed = false;
|
|
@@ -30091,30 +30117,63 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30091
30117
|
}
|
|
30092
30118
|
}
|
|
30093
30119
|
async function runCliRow() {
|
|
30120
|
+
const missing = deps.missingCliCommands?.() ?? [];
|
|
30094
30121
|
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
30095
30122
|
const cliReport = buildVersionLagReport(cliInput);
|
|
30096
|
-
|
|
30123
|
+
const capabilityGap = missing.length > 0 && Boolean(cliReport.releasedVersion);
|
|
30124
|
+
const shouldUpdate = Boolean(
|
|
30125
|
+
applyEnv && deps.updateCli && (versionAutoUpdateAction(cliReport) === "npm" || capabilityGap)
|
|
30126
|
+
);
|
|
30127
|
+
if (!shouldUpdate) {
|
|
30097
30128
|
const cli = checkCliVersion(cliInput, releasedNote);
|
|
30098
|
-
if (cli)
|
|
30129
|
+
if (cli) {
|
|
30130
|
+
if (missing.length) {
|
|
30131
|
+
emitNow({
|
|
30132
|
+
...cli,
|
|
30133
|
+
ok: false,
|
|
30134
|
+
detail: cli.ok ? `missing commands: ${missing.join(", ")}` : `${cli.detail ?? ""}`.trim() || `missing commands: ${missing.join(", ")}`,
|
|
30135
|
+
fix: `run \`mmi-cli doctor\` to heal the global CLI (missing: ${missing.join(", ")}), or \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
|
|
30136
|
+
verbose: [...cli.verbose ?? [], `missing commands: ${missing.join(", ")}`]
|
|
30137
|
+
});
|
|
30138
|
+
} else {
|
|
30139
|
+
emitNow(cli);
|
|
30140
|
+
}
|
|
30141
|
+
} else if (missing.length) {
|
|
30142
|
+
emitNow({
|
|
30143
|
+
id: "cli-version",
|
|
30144
|
+
ok: false,
|
|
30145
|
+
label: "mmi-cli",
|
|
30146
|
+
detail: `missing commands: ${missing.join(", ")}`,
|
|
30147
|
+
fix: `run \`mmi-cli doctor\` to heal the global CLI, or \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
|
|
30148
|
+
verbose: [`missing commands: ${missing.join(", ")}`]
|
|
30149
|
+
});
|
|
30150
|
+
}
|
|
30099
30151
|
return;
|
|
30100
30152
|
}
|
|
30101
|
-
|
|
30102
|
-
const
|
|
30103
|
-
|
|
30153
|
+
const target = cliReport.releasedVersion;
|
|
30154
|
+
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`;
|
|
30155
|
+
healIntent(intent);
|
|
30156
|
+
const heal = await deps.updateCli(target, healStep);
|
|
30157
|
+
const healEvidence = [
|
|
30158
|
+
`running: ${cliReport.currentVersion}`,
|
|
30159
|
+
`published: ${target ?? "(unknown)"}`,
|
|
30160
|
+
`heal: ${heal.detail}`,
|
|
30161
|
+
...missing.length ? [`missing commands before heal: ${missing.join(", ")}`] : []
|
|
30162
|
+
];
|
|
30104
30163
|
emitNow(heal.ok ? {
|
|
30105
30164
|
id: "cli-version",
|
|
30106
30165
|
ok: true,
|
|
30107
30166
|
label: "mmi-cli",
|
|
30108
|
-
detail: `${cliReport.currentVersion} \u2192 ${cliReport.
|
|
30167
|
+
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`,
|
|
30109
30168
|
verbose: healEvidence
|
|
30110
30169
|
} : {
|
|
30111
30170
|
id: "cli-version",
|
|
30112
30171
|
ok: false,
|
|
30113
30172
|
label: "mmi-cli",
|
|
30114
|
-
detail: `${cliReport.currentVersion} \u2192 ${
|
|
30173
|
+
detail: capabilityGap && cliReport.ok ? `missing commands: ${missing.join(", ")}` : `${cliReport.currentVersion} \u2192 ${target}`,
|
|
30115
30174
|
// #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
|
|
30116
30175
|
...heal.skipped ? { reportOnly: true } : {},
|
|
30117
|
-
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(
|
|
30176
|
+
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)}\``,
|
|
30118
30177
|
verbose: healEvidence
|
|
30119
30178
|
});
|
|
30120
30179
|
}
|
|
@@ -30177,8 +30236,10 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30177
30236
|
}
|
|
30178
30237
|
async function runDocsIndexRow() {
|
|
30179
30238
|
let probe;
|
|
30239
|
+
let root;
|
|
30180
30240
|
try {
|
|
30181
|
-
|
|
30241
|
+
root = await deps.repoRoot();
|
|
30242
|
+
probe = deps.docsIndexState(root);
|
|
30182
30243
|
} catch (e) {
|
|
30183
30244
|
const message = e instanceof Error ? e.message : String(e);
|
|
30184
30245
|
emitNow({
|
|
@@ -30194,10 +30255,44 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30194
30255
|
});
|
|
30195
30256
|
return;
|
|
30196
30257
|
}
|
|
30258
|
+
let healedWrite = false;
|
|
30259
|
+
if (applyRepo && probe?.drift && deps.healDocsIndex) {
|
|
30260
|
+
healIntent("docs index \u2014 regenerating docs/index.md");
|
|
30261
|
+
try {
|
|
30262
|
+
probe = deps.healDocsIndex(root);
|
|
30263
|
+
healedWrite = !probe.drift;
|
|
30264
|
+
} catch (e) {
|
|
30265
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30266
|
+
emitNow({
|
|
30267
|
+
ok: false,
|
|
30268
|
+
id: "docs-index",
|
|
30269
|
+
label: "docs index",
|
|
30270
|
+
detail: `heal failed \u2014 ${message}`,
|
|
30271
|
+
fix: "run `mmi-cli docs index --write` and commit docs/index.md",
|
|
30272
|
+
verbose: [`healDocsIndex threw: ${message}`]
|
|
30273
|
+
});
|
|
30274
|
+
return;
|
|
30275
|
+
}
|
|
30276
|
+
}
|
|
30197
30277
|
const docs2 = checkDocsIndex(probe);
|
|
30198
|
-
if (docs2)
|
|
30278
|
+
if (!docs2) return;
|
|
30279
|
+
if (healedWrite) {
|
|
30280
|
+
emitNow({
|
|
30281
|
+
...docs2,
|
|
30282
|
+
detail: "rewrote docs/index.md (working-tree heal \u2014 commit still needed)"
|
|
30283
|
+
});
|
|
30284
|
+
restartPending = true;
|
|
30285
|
+
return;
|
|
30286
|
+
}
|
|
30287
|
+
emitNow(docs2);
|
|
30199
30288
|
}
|
|
30200
30289
|
async function runRepoIndexRow() {
|
|
30290
|
+
const missing = deps.missingCliCommands?.() ?? [];
|
|
30291
|
+
if (missing.includes("repo-index")) {
|
|
30292
|
+
const row2 = checkRepoIndexCloud({ kind: "command-absent" });
|
|
30293
|
+
if (row2) emitNow(row2);
|
|
30294
|
+
return;
|
|
30295
|
+
}
|
|
30201
30296
|
if (lane.preflight && !opts.self && !lane.full) {
|
|
30202
30297
|
emitNow({
|
|
30203
30298
|
ok: true,
|
|
@@ -30227,8 +30322,176 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30227
30322
|
const row = checkRepoIndexCloud(probe);
|
|
30228
30323
|
if (row) emitNow(row);
|
|
30229
30324
|
}
|
|
30325
|
+
async function runBoardDoctorRow() {
|
|
30326
|
+
if (!deps.boardDoctorFix) return;
|
|
30327
|
+
try {
|
|
30328
|
+
const result = await deps.boardDoctorFix({ fix: applyRepo });
|
|
30329
|
+
if (!result) {
|
|
30330
|
+
emitNow({
|
|
30331
|
+
ok: true,
|
|
30332
|
+
warn: true,
|
|
30333
|
+
reportOnly: true,
|
|
30334
|
+
id: "board-doctor",
|
|
30335
|
+
label: "board doctor",
|
|
30336
|
+
detail: "skipped",
|
|
30337
|
+
verbose: ["boardDoctorFix returned undefined"]
|
|
30338
|
+
});
|
|
30339
|
+
return;
|
|
30340
|
+
}
|
|
30341
|
+
if (result.timedOut) {
|
|
30342
|
+
emitNow({
|
|
30343
|
+
ok: false,
|
|
30344
|
+
warn: true,
|
|
30345
|
+
reportOnly: true,
|
|
30346
|
+
id: "board-doctor",
|
|
30347
|
+
label: "board doctor",
|
|
30348
|
+
detail: "timed out",
|
|
30349
|
+
fix: "run `mmi-cli board doctor --fix`",
|
|
30350
|
+
verbose: [`scanned before timeout: ${result.scanned}`, `findings: ${result.findings}`]
|
|
30351
|
+
});
|
|
30352
|
+
return;
|
|
30353
|
+
}
|
|
30354
|
+
const allFixed = applyRepo && result.findings > 0 && result.failed === 0 && result.fixed >= result.findings;
|
|
30355
|
+
const ok = result.findings === 0 || Boolean(allFixed);
|
|
30356
|
+
emitNow(ok ? {
|
|
30357
|
+
ok: true,
|
|
30358
|
+
id: "board-doctor",
|
|
30359
|
+
label: "board doctor",
|
|
30360
|
+
detail: `scanned ${result.scanned}; fixed ${result.fixed}`,
|
|
30361
|
+
verbose: [`findings: ${result.findings}`, `fixed: ${result.fixed}`, `failed: ${result.failed}`]
|
|
30362
|
+
} : {
|
|
30363
|
+
ok: false,
|
|
30364
|
+
// Gate when fix was attempted and mechanical findings remain; detect-only stays visible but
|
|
30365
|
+
// report-only would hide a red that `board doctor --fix` clears — prefer gate after a failed fix.
|
|
30366
|
+
reportOnly: !applyRepo,
|
|
30367
|
+
id: "board-doctor",
|
|
30368
|
+
label: "board doctor",
|
|
30369
|
+
detail: `scanned ${result.scanned}; ${result.findings} findings; fixed ${result.fixed}; failed ${result.failed}`,
|
|
30370
|
+
fix: "run `mmi-cli board doctor --fix`",
|
|
30371
|
+
verbose: [`findings: ${result.findings}`, `fixed: ${result.fixed}`, `failed: ${result.failed}`]
|
|
30372
|
+
});
|
|
30373
|
+
} catch (e) {
|
|
30374
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30375
|
+
emitNow({
|
|
30376
|
+
ok: true,
|
|
30377
|
+
warn: true,
|
|
30378
|
+
reportOnly: true,
|
|
30379
|
+
id: "board-doctor",
|
|
30380
|
+
label: "board doctor",
|
|
30381
|
+
detail: `skipped \u2014 ${message}`,
|
|
30382
|
+
verbose: [`boardDoctorFix threw: ${message}`]
|
|
30383
|
+
});
|
|
30384
|
+
}
|
|
30385
|
+
}
|
|
30386
|
+
async function runSchedulesDriftRow() {
|
|
30387
|
+
if (!deps.schedulesDriftState) return;
|
|
30388
|
+
try {
|
|
30389
|
+
const state = await deps.schedulesDriftState();
|
|
30390
|
+
if (!state) {
|
|
30391
|
+
emitNow({
|
|
30392
|
+
ok: true,
|
|
30393
|
+
warn: true,
|
|
30394
|
+
reportOnly: true,
|
|
30395
|
+
id: "schedules-drift",
|
|
30396
|
+
label: "schedules drift",
|
|
30397
|
+
detail: "skipped",
|
|
30398
|
+
verbose: ["schedulesDriftState returned undefined"]
|
|
30399
|
+
});
|
|
30400
|
+
return;
|
|
30401
|
+
}
|
|
30402
|
+
if (state.timedOut) {
|
|
30403
|
+
emitNow({
|
|
30404
|
+
ok: false,
|
|
30405
|
+
warn: true,
|
|
30406
|
+
reportOnly: true,
|
|
30407
|
+
id: "schedules-drift",
|
|
30408
|
+
label: "schedules drift",
|
|
30409
|
+
detail: "timed out",
|
|
30410
|
+
fix: "run `mmi-cli org schedules` / `org schedules register`",
|
|
30411
|
+
verbose: ["schedulesDriftState timed out"]
|
|
30412
|
+
});
|
|
30413
|
+
return;
|
|
30414
|
+
}
|
|
30415
|
+
if (state.incomplete.length) {
|
|
30416
|
+
emitNow({
|
|
30417
|
+
ok: true,
|
|
30418
|
+
warn: true,
|
|
30419
|
+
reportOnly: true,
|
|
30420
|
+
id: "schedules-drift",
|
|
30421
|
+
label: "schedules drift",
|
|
30422
|
+
detail: `incomplete \u2014 ${state.incomplete.join("; ")}`,
|
|
30423
|
+
fix: "run `mmi-cli org schedules` / `org schedules register`",
|
|
30424
|
+
verbose: state.incomplete
|
|
30425
|
+
});
|
|
30426
|
+
return;
|
|
30427
|
+
}
|
|
30428
|
+
if (state.driftLines.length === 0) {
|
|
30429
|
+
emitNow({
|
|
30430
|
+
ok: true,
|
|
30431
|
+
reportOnly: true,
|
|
30432
|
+
id: "schedules-drift",
|
|
30433
|
+
label: "schedules drift",
|
|
30434
|
+
detail: "no drift",
|
|
30435
|
+
verbose: ["no drift lines"]
|
|
30436
|
+
});
|
|
30437
|
+
return;
|
|
30438
|
+
}
|
|
30439
|
+
emitNow({
|
|
30440
|
+
ok: false,
|
|
30441
|
+
reportOnly: true,
|
|
30442
|
+
id: "schedules-drift",
|
|
30443
|
+
label: "schedules drift",
|
|
30444
|
+
detail: `${state.driftLines.length} drift line(s)`,
|
|
30445
|
+
fix: "run `mmi-cli org schedules` / `org schedules register`",
|
|
30446
|
+
verbose: state.driftLines
|
|
30447
|
+
});
|
|
30448
|
+
} catch (e) {
|
|
30449
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30450
|
+
emitNow({
|
|
30451
|
+
ok: true,
|
|
30452
|
+
warn: true,
|
|
30453
|
+
reportOnly: true,
|
|
30454
|
+
id: "schedules-drift",
|
|
30455
|
+
label: "schedules drift",
|
|
30456
|
+
detail: `skipped \u2014 ${message}`,
|
|
30457
|
+
verbose: [`schedulesDriftState threw: ${message}`]
|
|
30458
|
+
});
|
|
30459
|
+
}
|
|
30460
|
+
}
|
|
30230
30461
|
async function runHousekeeperRows() {
|
|
30231
30462
|
const repoRoot2 = await deps.repoRoot();
|
|
30463
|
+
if (applyRepo && deps.sweepDeferred) {
|
|
30464
|
+
try {
|
|
30465
|
+
const sweep = await deps.sweepDeferred();
|
|
30466
|
+
const detailParts = [];
|
|
30467
|
+
if (sweep.removed.length) detailParts.push(`removed ${sweep.removed.length}`);
|
|
30468
|
+
if (sweep.stillQueued) detailParts.push(`${sweep.stillQueued} still queued`);
|
|
30469
|
+
if (sweep.skipped) detailParts.push(`${sweep.skipped} skipped`);
|
|
30470
|
+
emitNow({
|
|
30471
|
+
id: "deferred-worktrees",
|
|
30472
|
+
ok: true,
|
|
30473
|
+
label: "deferred worktrees",
|
|
30474
|
+
detail: detailParts.length ? detailParts.join("; ") : "nothing queued",
|
|
30475
|
+
verbose: [
|
|
30476
|
+
...sweep.removed.map((p) => `removed: ${p}`),
|
|
30477
|
+
`stillQueued: ${sweep.stillQueued}`,
|
|
30478
|
+
`skipped: ${sweep.skipped}`
|
|
30479
|
+
]
|
|
30480
|
+
});
|
|
30481
|
+
if (sweep.removed.length) restartPending = true;
|
|
30482
|
+
} catch (e) {
|
|
30483
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30484
|
+
emitNow({
|
|
30485
|
+
id: "deferred-worktrees",
|
|
30486
|
+
ok: true,
|
|
30487
|
+
warn: true,
|
|
30488
|
+
reportOnly: true,
|
|
30489
|
+
label: "deferred worktrees",
|
|
30490
|
+
detail: `sweep failed (fail-soft) \u2014 ${message}`,
|
|
30491
|
+
verbose: [`sweepDeferred threw: ${message}`]
|
|
30492
|
+
});
|
|
30493
|
+
}
|
|
30494
|
+
}
|
|
30232
30495
|
try {
|
|
30233
30496
|
const plan = await deps.gcPlan("origin", 200);
|
|
30234
30497
|
if (applyRepo) {
|
|
@@ -30285,9 +30548,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30285
30548
|
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 });
|
|
30286
30549
|
}
|
|
30287
30550
|
}
|
|
30288
|
-
const
|
|
30289
|
-
// Heals first: the slowest work starts at second zero, and a stale mmi-cli is the binary every row
|
|
30290
|
-
// below is being measured with (#3954).
|
|
30551
|
+
const prefix = [
|
|
30291
30552
|
{ id: "plugin", when: true, run: runPluginRow },
|
|
30292
30553
|
{ id: "cli-version", when: true, run: runCliRow },
|
|
30293
30554
|
{ id: "github-auth", when: true, run: runGithubAuthRow },
|
|
@@ -30296,18 +30557,22 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
30296
30557
|
{ id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow },
|
|
30297
30558
|
{ id: "plugin-cache", when: true, run: runPluginCacheRow },
|
|
30298
30559
|
{ id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
|
|
30299
|
-
{ id: "marketplace", when: true, run: runMarketplaceRows }
|
|
30300
|
-
|
|
30301
|
-
|
|
30560
|
+
{ id: "marketplace", when: true, run: runMarketplaceRows }
|
|
30561
|
+
];
|
|
30562
|
+
for (const entry of prefix) if (entry.when) await entry.run();
|
|
30563
|
+
const parallel = [
|
|
30302
30564
|
{ id: "docs-index", when: isOrgRepo && lane.full, run: runDocsIndexRow },
|
|
30303
30565
|
// Hub cloud probe (short timeout). Full lane + `--self`; `--preflight` may emit command-absent only (#4156).
|
|
30304
30566
|
{ id: "repo-index", when: isOrgRepo && (lane.full || Boolean(opts.self) || lane.preflight), run: runRepoIndexRow },
|
|
30305
|
-
|
|
30306
|
-
|
|
30567
|
+
{ id: "board-doctor", when: isOrgRepo && lane.full, run: runBoardDoctorRow },
|
|
30568
|
+
{ id: "schedules-drift", when: isOrgRepo && lane.full, run: runSchedulesDriftRow }
|
|
30569
|
+
];
|
|
30570
|
+
await Promise.all(parallel.filter((entry) => entry.when).map((entry) => entry.run()));
|
|
30571
|
+
const suffix = [
|
|
30307
30572
|
{ id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
|
|
30308
30573
|
{ id: "housekeeper", when: isOrgRepo && lane.full, run: runHousekeeperRows }
|
|
30309
30574
|
];
|
|
30310
|
-
for (const entry of
|
|
30575
|
+
for (const entry of suffix) if (entry.when) await entry.run();
|
|
30311
30576
|
const exitCode = doctorReportExitCode(checks);
|
|
30312
30577
|
if (opts.json) {
|
|
30313
30578
|
const payload = opts.verbose ? checks : checks.map(({ verbose: _evidence, ...check }) => check);
|
|
@@ -30767,6 +31032,84 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
30767
31032
|
const drift = docsIndex({ ...real, listDocs }, { check: true }).drift;
|
|
30768
31033
|
return { drift, docCount: listDocs().length };
|
|
30769
31034
|
},
|
|
31035
|
+
// #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
|
|
31036
|
+
healDocsIndex: (root) => {
|
|
31037
|
+
if (!(0, import_node_fs39.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
|
|
31038
|
+
const real = createDocsIndexDeps(root);
|
|
31039
|
+
let docs2;
|
|
31040
|
+
const listDocs = () => docs2 ??= real.listDocs();
|
|
31041
|
+
docsIndex({ ...real, listDocs }, { check: false });
|
|
31042
|
+
const drift = docsIndex({ ...real, listDocs }, { check: true }).drift;
|
|
31043
|
+
return { drift, docCount: listDocs().length };
|
|
31044
|
+
},
|
|
31045
|
+
// #4169: mirror `worktree gc --apply` — sweep deferred removals before applyGcPlan.
|
|
31046
|
+
sweepDeferred: async () => {
|
|
31047
|
+
const deferredStore = await createDeferredWorktreeStore();
|
|
31048
|
+
const removalContext = await currentWorktreeRemovalContext("doctor deferred sweep");
|
|
31049
|
+
const result = await sweepDeferredWorktrees(
|
|
31050
|
+
deferredStore,
|
|
31051
|
+
// #2841: same `-c core.fsmonitor=false` guard as `worktree gc --apply` / sweep-deferred.
|
|
31052
|
+
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
31053
|
+
removalContext
|
|
31054
|
+
);
|
|
31055
|
+
return {
|
|
31056
|
+
removed: result.removed,
|
|
31057
|
+
stillQueued: result.stillDeferred.length,
|
|
31058
|
+
skipped: result.skipped.length
|
|
31059
|
+
};
|
|
31060
|
+
},
|
|
31061
|
+
// #4170: light board doctor with 15s ceiling. skipClaimLiveness keeps Phase 3 off.
|
|
31062
|
+
boardDoctorFix: async ({ fix } = {}) => {
|
|
31063
|
+
const BOARD_DOCTOR_TIMEOUT_MS = 15e3;
|
|
31064
|
+
try {
|
|
31065
|
+
const cfg = await loadConfigForRepo();
|
|
31066
|
+
const work = boardDoctor({
|
|
31067
|
+
config: cfg,
|
|
31068
|
+
fix: Boolean(fix),
|
|
31069
|
+
skipClaimLiveness: true
|
|
31070
|
+
});
|
|
31071
|
+
const raced = await Promise.race([
|
|
31072
|
+
work.then((r) => ({ ...r, timedOut: false })),
|
|
31073
|
+
new Promise((resolve5) => {
|
|
31074
|
+
setTimeout(() => resolve5({ timedOut: true, scanned: 0, findings: 0, fixed: 0, failed: 0 }), BOARD_DOCTOR_TIMEOUT_MS);
|
|
31075
|
+
})
|
|
31076
|
+
]);
|
|
31077
|
+
if (raced.timedOut) return { scanned: 0, findings: 0, fixed: 0, failed: 0, timedOut: true };
|
|
31078
|
+
return {
|
|
31079
|
+
scanned: raced.scanned,
|
|
31080
|
+
findings: raced.findings,
|
|
31081
|
+
fixed: raced.fixed,
|
|
31082
|
+
failed: raced.failed
|
|
31083
|
+
};
|
|
31084
|
+
} catch {
|
|
31085
|
+
return void 0;
|
|
31086
|
+
}
|
|
31087
|
+
},
|
|
31088
|
+
// #4171: schedules drift notebook — report-only, ~20s ceiling.
|
|
31089
|
+
schedulesDriftState: async () => {
|
|
31090
|
+
const SCHEDULES_DRIFT_TIMEOUT_MS = 2e4;
|
|
31091
|
+
try {
|
|
31092
|
+
const raced = await Promise.race([
|
|
31093
|
+
fetchNotebook().then((nb) => ({
|
|
31094
|
+
driftLines: nb.drift,
|
|
31095
|
+
incomplete: nb.incomplete,
|
|
31096
|
+
timedOut: false
|
|
31097
|
+
})),
|
|
31098
|
+
new Promise((resolve5) => {
|
|
31099
|
+
setTimeout(() => resolve5({ driftLines: [], incomplete: [], timedOut: true }), SCHEDULES_DRIFT_TIMEOUT_MS);
|
|
31100
|
+
})
|
|
31101
|
+
]);
|
|
31102
|
+
return raced;
|
|
31103
|
+
} catch {
|
|
31104
|
+
return void 0;
|
|
31105
|
+
}
|
|
31106
|
+
},
|
|
31107
|
+
// #4173: estate commands absent from the running Commander program (stale global CLI).
|
|
31108
|
+
missingCliCommands: () => {
|
|
31109
|
+
const required = ["repo-index"];
|
|
31110
|
+
const names = new Set((opts.program?.commands ?? []).map((c) => c.name()));
|
|
31111
|
+
return required.filter((n) => !names.has(n));
|
|
31112
|
+
},
|
|
30770
31113
|
// #4156: short-timeout Hub status for the current repo. Fail-soft ? never throws to doctor.
|
|
30771
31114
|
repoIndexCloudState: async (root) => {
|
|
30772
31115
|
const local = repoIndexStatus(root);
|
|
@@ -33801,7 +34144,7 @@ program2.command("doctor").description("heal CLI/plugin wiring and clean up repo
|
|
|
33801
34144
|
verbose: opts.verbose
|
|
33802
34145
|
},
|
|
33803
34146
|
consoleIo,
|
|
33804
|
-
mmiDoctorDeps()
|
|
34147
|
+
mmiDoctorDeps({ program: program2 })
|
|
33805
34148
|
);
|
|
33806
34149
|
});
|
|
33807
34150
|
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk").action(() => runGuard());
|
|
@@ -33944,7 +34287,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
33944
34287
|
// #3485 item 7: the ONLY lane that throttles the npm read ? it runs on every session start, on a
|
|
33945
34288
|
// blocking hook. Every interactive lane still reads live.
|
|
33946
34289
|
doctor: async (io) => {
|
|
33947
|
-
await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true }));
|
|
34290
|
+
await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true, program: program2 }));
|
|
33948
34291
|
}
|
|
33949
34292
|
});
|
|
33950
34293
|
await runSessionStart(parallel, sequential, bannerIo);
|
package/package.json
CHANGED