@mutmutco/cli 3.79.0 → 3.80.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 +260 -218
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -6432,7 +6432,7 @@ function scanInstallDirs(root, fs2 = realFsProbe) {
|
|
|
6432
6432
|
return [factsFor(""), ...children.map(factsFor).filter((f) => f.hasPackageJson)];
|
|
6433
6433
|
}
|
|
6434
6434
|
function npmInstallTargets(dirs) {
|
|
6435
|
-
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install" }));
|
|
6435
|
+
return dirs.filter((d) => d.hasPackageJson && !d.hasNodeModules).map((d) => ({ dir: d.dir, command: d.hasLockfile ? "npm ci" : "npm install --no-package-lock" }));
|
|
6436
6436
|
}
|
|
6437
6437
|
function isLinkedWorktree(root, fs2 = realFsProbe) {
|
|
6438
6438
|
return fs2.isFile((0, import_node_path10.join)(root, ".git"));
|
|
@@ -9512,6 +9512,57 @@ async function runReleaseAbort(deps, options = {}) {
|
|
|
9512
9512
|
note: `removed the unpublished ${tag} candidate and restored local main; repair development before recutting`
|
|
9513
9513
|
};
|
|
9514
9514
|
}
|
|
9515
|
+
async function runReleasePublishRetry(deps, runId, options = {}) {
|
|
9516
|
+
if (!options.approved) {
|
|
9517
|
+
throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
|
|
9518
|
+
}
|
|
9519
|
+
if (!Number.isSafeInteger(runId) || runId <= 0) throw new Error(`invalid publish run id ${runId}`);
|
|
9520
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
9521
|
+
if (!isHubControlRepo(ctx.repo)) {
|
|
9522
|
+
throw new Error("release --retry-publish is limited to mutmutco/MMI-Hub");
|
|
9523
|
+
}
|
|
9524
|
+
await requireCleanTree(deps);
|
|
9525
|
+
await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
|
|
9526
|
+
const readRun = async () => {
|
|
9527
|
+
const raw = await deps.run("gh", ["run", "view", String(runId), "--repo", ctx.repo, "--json", "databaseId,workflowName,event,headBranch,headSha,status,conclusion,url"]);
|
|
9528
|
+
try {
|
|
9529
|
+
return JSON.parse(raw);
|
|
9530
|
+
} catch {
|
|
9531
|
+
throw new Error(`publish run ${runId} metadata was not valid JSON`);
|
|
9532
|
+
}
|
|
9533
|
+
};
|
|
9534
|
+
const run = await readRun();
|
|
9535
|
+
if (run.databaseId !== runId || run.workflowName !== "publish" || run.event !== "release" || run.status !== "completed" || run.conclusion !== "failure") {
|
|
9536
|
+
throw new Error(`run ${runId} is not a completed failed Hub publish release run; nothing was written`);
|
|
9537
|
+
}
|
|
9538
|
+
const tag = run.headBranch ?? "";
|
|
9539
|
+
const tagSha = run.headSha ?? "";
|
|
9540
|
+
if (!/^v\d+\.\d+\.\d+$/.test(tag) || !/^[0-9a-f]{40}$/.test(tagSha)) {
|
|
9541
|
+
throw new Error(`run ${runId} has invalid release identity (${tag || "(no tag)"} / ${tagSha || "(no sha)"}); nothing was written`);
|
|
9542
|
+
}
|
|
9543
|
+
if (await probeRemoteTag(deps, tag) !== tagSha) {
|
|
9544
|
+
throw new Error(`run ${runId} does not match origin ${tag}; nothing was written`);
|
|
9545
|
+
}
|
|
9546
|
+
await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
|
|
9547
|
+
await deps.run("gh", ["run", "rerun", String(runId), "--repo", ctx.repo, "--failed"]);
|
|
9548
|
+
if (options.watch) {
|
|
9549
|
+
await deps.run("gh", ["run", "watch", String(runId), "--repo", ctx.repo, "--exit-status"]);
|
|
9550
|
+
const completed = await readRun();
|
|
9551
|
+
if (completed.status !== "completed" || completed.conclusion !== "success") {
|
|
9552
|
+
throw new Error(`publish run ${runId} did not finish successfully after retry`);
|
|
9553
|
+
}
|
|
9554
|
+
}
|
|
9555
|
+
return {
|
|
9556
|
+
command: "release-retry-publish",
|
|
9557
|
+
repo: ctx.repo,
|
|
9558
|
+
tag,
|
|
9559
|
+
tagSha,
|
|
9560
|
+
runId,
|
|
9561
|
+
runUrl: run.url ?? `https://github.com/${ctx.repo}/actions/runs/${runId}`,
|
|
9562
|
+
status: options.watch ? "success" : "pending",
|
|
9563
|
+
note: options.watch ? `publish run ${runId} retried and passed` : `publish run ${runId} failed jobs queued for retry`
|
|
9564
|
+
};
|
|
9565
|
+
}
|
|
9515
9566
|
async function runTrainApplyPipeline(mode, input) {
|
|
9516
9567
|
const { deps, ctx, command, meta, branchHints, watch, options } = input;
|
|
9517
9568
|
const directTrack = input.directTrack ?? false;
|
|
@@ -14552,9 +14603,10 @@ async function fetchNpmReleasedVersion() {
|
|
|
14552
14603
|
}
|
|
14553
14604
|
}
|
|
14554
14605
|
var NPM_INSTALL_TIMEOUT_MS = 12e4;
|
|
14555
|
-
async function npmSelfUpdateCli(target) {
|
|
14606
|
+
async function npmSelfUpdateCli(target, onStep) {
|
|
14556
14607
|
const command = cliUpdateCommand(target);
|
|
14557
14608
|
try {
|
|
14609
|
+
onStep?.(command);
|
|
14558
14610
|
await runHostBin("npm", ["install", "-g", `@mutmutco/cli@${target ?? "latest"}`], { timeout: NPM_INSTALL_TIMEOUT_MS });
|
|
14559
14611
|
return { ok: true, detail: `${command} exited 0` };
|
|
14560
14612
|
} catch (e) {
|
|
@@ -14620,9 +14672,6 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
14620
14672
|
) : (0, import_node_fs18.existsSync)((0, import_node_path16.join)(root, "plugins", "cache", "mutmutco", "mmi"))
|
|
14621
14673
|
};
|
|
14622
14674
|
}
|
|
14623
|
-
function activePluginGuardState(isOrgRepo) {
|
|
14624
|
-
return buildPluginGuardDecision(snapshotPluginGuardInput(detectSurface(process.env), isOrgRepo)).state;
|
|
14625
|
-
}
|
|
14626
14675
|
async function runClaudePlugin(args) {
|
|
14627
14676
|
try {
|
|
14628
14677
|
await runHostBin("claude", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
@@ -14792,27 +14841,35 @@ async function applyPluginHeal(surface, log, opts) {
|
|
|
14792
14841
|
if (restored) log(` ${restored}`);
|
|
14793
14842
|
return true;
|
|
14794
14843
|
}
|
|
14795
|
-
async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
|
|
14844
|
+
async function healClaudePluginForDoctor(surface = detectSurface(process.env), onStep) {
|
|
14796
14845
|
if (surfaceToken(surface) !== "claude") {
|
|
14797
14846
|
return { ok: false, detail: `not a Claude surface (${surface}) \u2014 no Hub-shipped plugin to reinstall` };
|
|
14798
14847
|
}
|
|
14799
14848
|
const steps = [];
|
|
14800
|
-
const ok = await applyPluginHeal(surface, (msg) =>
|
|
14849
|
+
const ok = await applyPluginHeal(surface, (msg) => {
|
|
14850
|
+
const line = msg.trim();
|
|
14851
|
+
steps.push(line);
|
|
14852
|
+
if (line) onStep?.(line);
|
|
14853
|
+
});
|
|
14801
14854
|
const pinNote = steps.find((s) => s.startsWith("re-pinned ") || s.includes("re-pin by hand"));
|
|
14802
14855
|
return {
|
|
14803
14856
|
ok,
|
|
14804
14857
|
detail: ok ? `marketplace remove \u2192 add \u2192 install succeeded${pinNote ? `; ${pinNote}` : ""}` : `\`claude plugin\` reinstall failed or was skipped${steps.length ? ` (${steps[steps.length - 1]})` : ""}`
|
|
14805
14858
|
};
|
|
14806
14859
|
}
|
|
14807
|
-
async function healActivePluginForDoctor(surface = detectSurface(process.env)) {
|
|
14860
|
+
async function healActivePluginForDoctor(surface = detectSurface(process.env), onStep) {
|
|
14808
14861
|
const token = surfaceToken(surface);
|
|
14809
14862
|
if (token !== "claude" && token !== "codex" && token !== "cursor" && token !== "kilo") {
|
|
14810
14863
|
return { ok: false, detail: `not a supported plugin surface (${surface})` };
|
|
14811
14864
|
}
|
|
14812
|
-
if (token === "claude") return healClaudePluginForDoctor(surface);
|
|
14865
|
+
if (token === "claude") return healClaudePluginForDoctor(surface, onStep);
|
|
14813
14866
|
if (token === "cursor") return installCursorPluginCheckout();
|
|
14814
14867
|
const steps = [];
|
|
14815
|
-
const applied = await applyPluginHeal(surface, (msg) =>
|
|
14868
|
+
const applied = await applyPluginHeal(surface, (msg) => {
|
|
14869
|
+
const line = msg.trim();
|
|
14870
|
+
steps.push(line);
|
|
14871
|
+
if (line) onStep?.(line);
|
|
14872
|
+
});
|
|
14816
14873
|
const snapshot = applied ? snapshotPluginGuardInput(surface, true) : void 0;
|
|
14817
14874
|
const guardState = snapshot ? buildPluginGuardDecision(snapshot).state : "unresolved";
|
|
14818
14875
|
const ok = applied && guardState === "healthy";
|
|
@@ -27301,19 +27358,6 @@ function renderTally(checks) {
|
|
|
27301
27358
|
const healthy = total - checks.filter((c) => !c.ok).length;
|
|
27302
27359
|
return healthy === total ? `\u2713 all ${total} checks healthy` : `\u2713 ${healthy} of ${total} checks healthy`;
|
|
27303
27360
|
}
|
|
27304
|
-
function renderDoctorText(checks, opts) {
|
|
27305
|
-
const lines = [];
|
|
27306
|
-
const shown = opts.verbose ? checks : checks.filter((c) => !c.ok || c.warn);
|
|
27307
|
-
for (const check of shown) {
|
|
27308
|
-
lines.push(renderCheckLine(check));
|
|
27309
|
-
if (opts.verbose) {
|
|
27310
|
-
for (const evidence of check.verbose ?? []) lines.push(`${VERBOSE_INDENT}${evidence}`);
|
|
27311
|
-
}
|
|
27312
|
-
}
|
|
27313
|
-
lines.push(renderTally(checks));
|
|
27314
|
-
if (opts.restartPending) lines.push(RESTART_LINE);
|
|
27315
|
-
return lines.join("\n");
|
|
27316
|
-
}
|
|
27317
27361
|
function doctorReportExitCode(checks) {
|
|
27318
27362
|
return checks.some((c) => !c.ok && !c.reportOnly) ? 1 : 0;
|
|
27319
27363
|
}
|
|
@@ -28003,7 +28047,8 @@ var DOCTOR_SURFACES = Object.freeze(
|
|
|
28003
28047
|
installLocator: surface.install.locator,
|
|
28004
28048
|
upgradeMechanism: surface.upgrade.mechanism,
|
|
28005
28049
|
reload: surface.upgrade.reload,
|
|
28006
|
-
repairOwner: surface.ownership.repair === "mmi-cli" ? "mmi-cli" : "operator"
|
|
28050
|
+
repairOwner: surface.ownership.repair === "mmi-cli" ? "mmi-cli" : "operator",
|
|
28051
|
+
trustOwner: surface.ownership.trust === "operator" ? "operator" : "host"
|
|
28007
28052
|
}))
|
|
28008
28053
|
);
|
|
28009
28054
|
function doctorSurface(token) {
|
|
@@ -28036,11 +28081,8 @@ function diagnoseSurface(evidence) {
|
|
|
28036
28081
|
return { ...base, state: "clean" };
|
|
28037
28082
|
}
|
|
28038
28083
|
function reloadInstruction(descriptor) {
|
|
28039
|
-
|
|
28040
|
-
|
|
28041
|
-
if (descriptor.token === "kimi") return "restart Kimi Code CLI";
|
|
28042
|
-
if (descriptor.token === "kilo") return "restart Kilo Code";
|
|
28043
|
-
return descriptor.reload === "workspace" ? `reload ${descriptor.displayName}` : `restart ${descriptor.displayName}`;
|
|
28084
|
+
const verb = descriptor.reload === "workspace" ? "reload" : "restart";
|
|
28085
|
+
return `${verb} ${descriptor.displayName}`;
|
|
28044
28086
|
}
|
|
28045
28087
|
function planSurfaceRepair(diagnosis) {
|
|
28046
28088
|
if (diagnosis.state === "clean" || diagnosis.state === "skipped" || diagnosis.state === "freshness-unknown") return null;
|
|
@@ -28085,10 +28127,16 @@ function buildSurfaceDoctorCheck(diagnosis) {
|
|
|
28085
28127
|
detail: detailByState[state],
|
|
28086
28128
|
...plan ? { fix: plan.instruction } : state === "freshness-unknown" ? { fix: "check `mmi-cli --version` against `npm view @mutmutco/cli version`, then rerun doctor" } : {},
|
|
28087
28129
|
verbose: [
|
|
28130
|
+
// The three numbers the legacy builder used to print. They belong here now that this is the only
|
|
28131
|
+
// plugin row, and a report that names a state without naming the versions behind it is not evidence.
|
|
28132
|
+
`installed: ${diagnosis.installedVersion ?? "(none)"}`,
|
|
28133
|
+
`released: ${diagnosis.releasedVersion ?? "(not checked \u2014 offline or --fast)"}`,
|
|
28134
|
+
`state: ${state}`,
|
|
28088
28135
|
`registry surface: ${descriptor.token}`,
|
|
28089
28136
|
`install: ${descriptor.installMechanism} (${descriptor.installLocator})`,
|
|
28090
28137
|
`repair owner: ${descriptor.repairOwner}`,
|
|
28091
|
-
`artifacts: ${descriptor.artifactIds.join(", ")}
|
|
28138
|
+
`artifacts: ${descriptor.artifactIds.join(", ")}`,
|
|
28139
|
+
...diagnosis.repairDetail ? [`heal: ${diagnosis.repairDetail}`] : []
|
|
28092
28140
|
]
|
|
28093
28141
|
};
|
|
28094
28142
|
}
|
|
@@ -28186,77 +28234,23 @@ function checkWorktreeRoots(probe) {
|
|
|
28186
28234
|
verbose: evidence
|
|
28187
28235
|
};
|
|
28188
28236
|
}
|
|
28189
|
-
function
|
|
28190
|
-
if (probe.guardState === "no-install" || probe.guardState === "unresolved") return "unresolved";
|
|
28191
|
-
const behind = Boolean(probe.installed && probe.released) && compareVersions(probe.installed, probe.released) < 0;
|
|
28192
|
-
return behind ? "behind" : null;
|
|
28193
|
-
}
|
|
28194
|
-
function checkClaudePlugin(probe) {
|
|
28195
|
-
const { installed, released, guardState } = probe;
|
|
28196
|
-
const codex = probe.surface === "codex";
|
|
28197
|
-
const kilo = probe.surface === "kilo";
|
|
28198
|
-
const cursor = probe.surface === "cursor";
|
|
28199
|
-
const id = codex ? "codex-plugin" : kilo ? "kilo-plugin" : cursor ? "cursor-plugin" : "claude-plugin";
|
|
28200
|
-
const label = codex ? "Codex plugin" : kilo ? "Kilo plugin" : cursor ? "Cursor plugin" : "Claude plugin";
|
|
28201
|
-
const restart = codex ? "restart Codex" : kilo ? "reload Kilo" : cursor ? "reload Cursor" : "restart Claude";
|
|
28202
|
-
const evidence = [
|
|
28203
|
-
`installed: ${installed ?? "(none)"}`,
|
|
28204
|
-
// #3485 item 7: when the value was reused from the banner's once-a-day cache, say so and say how old.
|
|
28205
|
-
`released: ${released ?? "(not checked \u2014 offline or --fast)"}${released && probe.releasedNote ? ` ${probe.releasedNote}` : ""}`,
|
|
28206
|
-
`resolvable: ${guardState}`
|
|
28207
|
-
];
|
|
28208
|
-
const trigger = pluginHealTrigger(probe);
|
|
28209
|
-
if (trigger === "unresolved") {
|
|
28210
|
-
return {
|
|
28211
|
-
id,
|
|
28212
|
-
ok: false,
|
|
28213
|
-
label,
|
|
28214
|
-
detail: guardState === "no-install" ? "not installed" : "unresolved (delivery/cache missing)",
|
|
28215
|
-
fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall the MMI plugin, then ${restart}`,
|
|
28216
|
-
verbose: evidence
|
|
28217
|
-
};
|
|
28218
|
-
}
|
|
28219
|
-
if (trigger === "behind") {
|
|
28220
|
-
return {
|
|
28221
|
-
id,
|
|
28222
|
-
ok: false,
|
|
28223
|
-
label,
|
|
28224
|
-
detail: `${installed} \u2192 ${released}`,
|
|
28225
|
-
// Name the CLI verb that actually fixes it, like every other red row — `/plugin` is the manual
|
|
28226
|
-
// fallback, not the first resort (#3282).
|
|
28227
|
-
fix: `run \`mmi-cli doctor --apply\` (or \`mmi-cli plugin heal\`) to reinstall it, then ${restart}`,
|
|
28228
|
-
verbose: evidence
|
|
28229
|
-
};
|
|
28230
|
-
}
|
|
28231
|
-
if (!released) {
|
|
28232
|
-
return {
|
|
28233
|
-
id,
|
|
28234
|
-
ok: false,
|
|
28235
|
-
reportOnly: true,
|
|
28236
|
-
label,
|
|
28237
|
-
detail: `${installed ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
|
|
28238
|
-
fix: "check it directly: `npm view @mutmutco/cli version`, then `mmi-cli plugin heal` if behind",
|
|
28239
|
-
verbose: evidence
|
|
28240
|
-
};
|
|
28241
|
-
}
|
|
28242
|
-
return { id, ok: true, label, ...installed ? { detail: installed } : {}, verbose: evidence };
|
|
28243
|
-
}
|
|
28244
|
-
function checkCodexHookTrust(probe) {
|
|
28237
|
+
function checkCodexHookTrust(probe, displayName = "Codex") {
|
|
28245
28238
|
if (!probe?.applicable) return null;
|
|
28246
28239
|
const evidence = [
|
|
28247
28240
|
`stored approval rows: ${probe.trustedCount}/${probe.requiredCount}`,
|
|
28248
28241
|
"current command hashes: verify interactively in /hooks"
|
|
28249
28242
|
];
|
|
28243
|
+
const label = `${displayName} hook trust`;
|
|
28250
28244
|
if (probe.trusted) {
|
|
28251
|
-
return { id: "codex-hook-trust", ok: true, label
|
|
28245
|
+
return { id: "codex-hook-trust", ok: true, label, detail: "trusted", verbose: evidence };
|
|
28252
28246
|
}
|
|
28253
28247
|
return {
|
|
28254
28248
|
id: "codex-hook-trust",
|
|
28255
28249
|
ok: true,
|
|
28256
28250
|
warn: true,
|
|
28257
|
-
label
|
|
28251
|
+
label,
|
|
28258
28252
|
detail: probe.requiredCount > 0 ? `${probe.trustedCount}/${probe.requiredCount} approval rows present \u2014 current hashes unverified` : "hook bundle could not be verified",
|
|
28259
|
-
fix: probe.requiredCount > 0 ?
|
|
28253
|
+
fix: probe.requiredCount > 0 ? `${displayName} does not allow silent hook approval; run \`/hooks\`, review the MMI commands, and trust them` : `run \`mmi-cli plugin heal\`, restart ${displayName}, then review and trust MMI under \`/hooks\``,
|
|
28260
28254
|
verbose: evidence
|
|
28261
28255
|
};
|
|
28262
28256
|
}
|
|
@@ -28481,6 +28475,12 @@ function gcReapable(plan) {
|
|
|
28481
28475
|
async function runDoctorClean(opts, io, deps) {
|
|
28482
28476
|
const applyEnv = Boolean(opts.apply) || Boolean(opts.preflight);
|
|
28483
28477
|
const applyRepo = Boolean(opts.apply) && opts.repoWrites !== false;
|
|
28478
|
+
const lane = {
|
|
28479
|
+
banner: Boolean(opts.banner),
|
|
28480
|
+
fast: Boolean(opts.fast),
|
|
28481
|
+
preflight: Boolean(opts.preflight),
|
|
28482
|
+
full: !opts.fast && !opts.banner && !opts.preflight
|
|
28483
|
+
};
|
|
28484
28484
|
const probeReleased = !opts.fast || Boolean(opts.self);
|
|
28485
28485
|
const probeAws = !opts.fast && !opts.banner;
|
|
28486
28486
|
const [login, isOrgRepo, released, callerArn, reach] = await Promise.all([
|
|
@@ -28495,108 +28495,69 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
28495
28495
|
opts.banner || opts.preflight || opts.fast && !opts.self ? Promise.resolve(void 0) : deps.githubRepoReach?.() ?? Promise.resolve(void 0)
|
|
28496
28496
|
]);
|
|
28497
28497
|
const ghInstalled2 = login ? true : await deps.ghInstalled();
|
|
28498
|
-
const registryEvidence = deps.surfaceEvidence
|
|
28499
|
-
const
|
|
28500
|
-
const pluginSurface = deps.pluginSurface?.() ?? "claude-cli";
|
|
28501
|
-
const codexSurface = registryEvidence?.descriptor.token === "codex" || pluginSurface === "codex";
|
|
28502
|
-
const cursorSurface = pluginSurface === "cursor";
|
|
28503
|
-
const restartAction = registryEvidence ? surfaceRestartAction(registryEvidence.descriptor) : codexSurface ? "restart Codex" : cursorSurface ? "reload Cursor" : pluginSurface === "kilo" ? "restart Kilo Code" : "restart Claude";
|
|
28498
|
+
const registryEvidence = deps.surfaceEvidence(isOrgRepo);
|
|
28499
|
+
const restartAction = registryEvidence ? surfaceRestartAction(registryEvidence.descriptor) : "restart the agent host";
|
|
28504
28500
|
const checks = [];
|
|
28505
28501
|
let restartPending = false;
|
|
28506
|
-
|
|
28507
|
-
|
|
28508
|
-
|
|
28509
|
-
|
|
28510
|
-
|
|
28511
|
-
|
|
28512
|
-
|
|
28513
|
-
|
|
28514
|
-
const current = deps.readGitignore();
|
|
28515
|
-
const gi = planGitignore(current);
|
|
28516
|
-
const giEvidence = [
|
|
28517
|
-
`.gitignore: ${current === null ? "absent" : `${current.split("\n").length} lines`}`,
|
|
28518
|
-
`managed block: ${gi.ok ? "present and current" : "missing or out of date"}`
|
|
28519
|
-
];
|
|
28520
|
-
if (gi.ok) {
|
|
28521
|
-
checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
|
|
28522
|
-
} else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
|
|
28523
|
-
checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
|
|
28524
|
-
restartPending = true;
|
|
28525
|
-
} else {
|
|
28526
|
-
checks.push({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
28527
|
-
}
|
|
28528
|
-
}
|
|
28529
|
-
const releasedNote = deps.releasedVersionNote?.();
|
|
28530
|
-
const pluginProbe = {
|
|
28531
|
-
installed,
|
|
28532
|
-
released,
|
|
28533
|
-
guardState: registryEvidence?.guardState ?? deps.pluginGuardState(isOrgRepo),
|
|
28534
|
-
releasedNote,
|
|
28535
|
-
surface: pluginSurface
|
|
28502
|
+
const streamed = /* @__PURE__ */ new Set();
|
|
28503
|
+
const worthPrinting = (c) => Boolean(opts.verbose) || !c.ok || Boolean(c.warn);
|
|
28504
|
+
const emitNow = (check) => {
|
|
28505
|
+
checks.push(check);
|
|
28506
|
+
if (opts.json || !worthPrinting(check)) return;
|
|
28507
|
+
streamed.add(check);
|
|
28508
|
+
io.log(renderCheckLine(check));
|
|
28509
|
+
if (opts.verbose) for (const evidence of check.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
|
|
28536
28510
|
};
|
|
28537
|
-
const
|
|
28538
|
-
|
|
28539
|
-
|
|
28511
|
+
const healIntent = (line) => {
|
|
28512
|
+
if (!opts.json) io.log(`\u21BB ${line}`);
|
|
28513
|
+
};
|
|
28514
|
+
const healStep = (message) => {
|
|
28515
|
+
if (!opts.json) io.log(`${VERBOSE_INDENT}${message.trim()}`);
|
|
28516
|
+
};
|
|
28517
|
+
const releasedNote = deps.releasedVersionNote?.();
|
|
28540
28518
|
let pluginHealed = false;
|
|
28541
|
-
|
|
28542
|
-
|
|
28543
|
-
|
|
28544
|
-
|
|
28545
|
-
|
|
28519
|
+
async function runPluginRow() {
|
|
28520
|
+
if (!registryEvidence) return;
|
|
28521
|
+
const diagnosis = diagnoseSurface({ ...registryEvidence, releasedVersion: released });
|
|
28522
|
+
const repair = planSurfaceRepair(diagnosis);
|
|
28523
|
+
if (applyEnv && deps.healPlugin && repair?.supported) {
|
|
28524
|
+
const { descriptor } = registryEvidence;
|
|
28525
|
+
healIntent(`${descriptor.displayName} plugin \u2014 healing via ${descriptor.installMechanism} (${descriptor.installLocator})`);
|
|
28526
|
+
const heal = await deps.healPlugin(healStep);
|
|
28527
|
+
pluginHealed = heal.ok;
|
|
28528
|
+
const row = buildSurfaceDoctorCheck(diagnoseSurface({
|
|
28546
28529
|
...registryEvidence,
|
|
28547
28530
|
releasedVersion: released,
|
|
28548
28531
|
repair: { attempted: true, ok: heal.ok, detail: heal.detail }
|
|
28549
|
-
});
|
|
28550
|
-
|
|
28551
|
-
|
|
28552
|
-
|
|
28553
|
-
|
|
28554
|
-
|
|
28555
|
-
const measured = healTrigger === "behind" ? `${installed} \u2192 ${released}` : "unresolved install";
|
|
28556
|
-
const healEvidence = [
|
|
28557
|
-
`installed: ${installed ?? "(none)"}`,
|
|
28558
|
-
`released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
|
|
28559
|
-
`resolvable: ${pluginProbe.guardState}`,
|
|
28560
|
-
`heal: ${heal.detail}`
|
|
28561
|
-
];
|
|
28562
|
-
checks.push(heal.ok ? {
|
|
28563
|
-
id: codexSurface ? "codex-plugin" : pluginSurface === "kilo" ? "kilo-plugin" : cursorSurface ? "cursor-plugin" : "claude-plugin",
|
|
28564
|
-
ok: true,
|
|
28565
|
-
label: codexSurface ? "Codex plugin" : pluginSurface === "kilo" ? "Kilo plugin" : cursorSurface ? "Cursor plugin" : "Claude plugin",
|
|
28566
|
-
detail: pluginSurface === "kilo" ? `${measured} \u2014 reinstalled via \`kilo plugin\` (the plugin provisions the skills on next load)` : cursorSurface ? `${measured} \u2014 replaced the managed local checkout (previous copy quarantined)` : `${measured} \u2014 reinstalled via the MMI marketplace (remove \u2192 add \u2192 ${codexSurface ? "add" : "install"})${codexSurface ? "; review trust in /hooks" : ""}`,
|
|
28567
|
-
verbose: healEvidence
|
|
28568
|
-
} : {
|
|
28569
|
-
id: codexSurface ? "codex-plugin" : pluginSurface === "kilo" ? "kilo-plugin" : cursorSurface ? "cursor-plugin" : "claude-plugin",
|
|
28570
|
-
ok: false,
|
|
28571
|
-
label: codexSurface ? "Codex plugin" : pluginSurface === "kilo" ? "Kilo plugin" : cursorSurface ? "Cursor plugin" : "Claude plugin",
|
|
28572
|
-
detail: measured,
|
|
28573
|
-
// #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
|
|
28574
|
-
// not fix what it found — but it is not this invocation's to clear. The other process is doing it;
|
|
28575
|
-
// waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
|
|
28576
|
-
// genuine gap and still gates.
|
|
28577
|
-
...heal.skipped ? { reportOnly: true } : {},
|
|
28578
|
-
fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes` : `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then ${codexSurface ? "restart Codex" : pluginSurface === "kilo" ? "reload Kilo" : cursorSurface ? "reload Cursor" : "restart Claude"}`,
|
|
28579
|
-
verbose: healEvidence
|
|
28580
|
-
});
|
|
28532
|
+
}));
|
|
28533
|
+
if (heal.skipped) {
|
|
28534
|
+
row.reportOnly = true;
|
|
28535
|
+
row.fix = `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes`;
|
|
28536
|
+
}
|
|
28537
|
+
emitNow(row);
|
|
28581
28538
|
if (!heal.skipped) restartPending = true;
|
|
28539
|
+
} else if (diagnosis.state !== "skipped") {
|
|
28540
|
+
const row = buildSurfaceDoctorCheck(diagnosis);
|
|
28541
|
+
emitNow(row);
|
|
28542
|
+
if (!row.ok && repair?.supported) restartPending = true;
|
|
28543
|
+
}
|
|
28544
|
+
if (registryEvidence.descriptor.trustOwner === "operator" && (registryEvidence.guardState === "healthy" || pluginHealed)) {
|
|
28545
|
+
const trust = checkCodexHookTrust(deps.pluginTrustState?.(), registryEvidence.descriptor.displayName);
|
|
28546
|
+
if (trust) emitNow(trust);
|
|
28547
|
+
}
|
|
28548
|
+
}
|
|
28549
|
+
async function runCliRow() {
|
|
28550
|
+
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
28551
|
+
const cliReport = buildVersionLagReport(cliInput);
|
|
28552
|
+
if (!(applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm")) {
|
|
28553
|
+
const cli = checkCliVersion(cliInput, releasedNote);
|
|
28554
|
+
if (cli) emitNow(cli);
|
|
28555
|
+
return;
|
|
28582
28556
|
}
|
|
28583
|
-
|
|
28584
|
-
const
|
|
28585
|
-
if (plugin) {
|
|
28586
|
-
if (!("state" in plugin) || plugin.state !== "skipped") checks.push(plugin);
|
|
28587
|
-
if (!plugin.ok && (registryDiagnosis ? Boolean(registryRepairPlan?.supported) : true)) restartPending = true;
|
|
28588
|
-
}
|
|
28589
|
-
}
|
|
28590
|
-
if (codexSurface && (pluginProbe.guardState === "healthy" || pluginHealed)) {
|
|
28591
|
-
const trust = checkCodexHookTrust(deps.pluginTrustState?.());
|
|
28592
|
-
if (trust) checks.push(trust);
|
|
28593
|
-
}
|
|
28594
|
-
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
28595
|
-
const cliReport = buildVersionLagReport(cliInput);
|
|
28596
|
-
if (applyEnv && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
|
|
28597
|
-
const heal = await deps.updateCli(cliReport.releasedVersion);
|
|
28557
|
+
healIntent(`mmi-cli \u2014 self-updating ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion} via npm install -g`);
|
|
28558
|
+
const heal = await deps.updateCli(cliReport.releasedVersion, healStep);
|
|
28598
28559
|
const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
|
|
28599
|
-
|
|
28560
|
+
emitNow(heal.ok ? {
|
|
28600
28561
|
id: "cli-version",
|
|
28601
28562
|
ok: true,
|
|
28602
28563
|
label: "mmi-cli",
|
|
@@ -28612,17 +28573,47 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
28612
28573
|
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(cliReport.releasedVersion)}\``,
|
|
28613
28574
|
verbose: healEvidence
|
|
28614
28575
|
});
|
|
28615
|
-
} else {
|
|
28616
|
-
const cli = checkCliVersion(cliInput, releasedNote);
|
|
28617
|
-
if (cli) checks.push(cli);
|
|
28618
28576
|
}
|
|
28619
|
-
|
|
28620
|
-
|
|
28577
|
+
async function runGithubAuthRow() {
|
|
28578
|
+
emitNow(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
|
|
28579
|
+
}
|
|
28580
|
+
async function runGithubPoolsRows() {
|
|
28581
|
+
for (const pool of checkGithubPools(await deps.githubPools())) emitNow(pool);
|
|
28582
|
+
}
|
|
28583
|
+
async function runAwsRow() {
|
|
28584
|
+
const aws = checkAwsIdentity({ isOrgRepo, probed: probeAws, callerArn });
|
|
28585
|
+
if (aws) emitNow(aws);
|
|
28586
|
+
}
|
|
28587
|
+
async function runRepoWorktreesRow() {
|
|
28588
|
+
emitNow(checkRepoWorktrees({ isOrgRepo, hasRepoLocalWorktrees: deps.hasRepoLocalWorktrees() }));
|
|
28589
|
+
}
|
|
28590
|
+
async function runGitignoreRow() {
|
|
28591
|
+
const current = deps.readGitignore();
|
|
28592
|
+
const gi = planGitignore(current);
|
|
28593
|
+
const giEvidence = [
|
|
28594
|
+
`.gitignore: ${current === null ? "absent" : `${current.split("\n").length} lines`}`,
|
|
28595
|
+
`managed block: ${gi.ok ? "present and current" : "missing or out of date"}`
|
|
28596
|
+
];
|
|
28597
|
+
if (gi.ok) {
|
|
28598
|
+
emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
|
|
28599
|
+
} else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
|
|
28600
|
+
emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
|
|
28601
|
+
restartPending = true;
|
|
28602
|
+
} else {
|
|
28603
|
+
emitNow({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
28604
|
+
}
|
|
28605
|
+
}
|
|
28606
|
+
async function runPluginCacheRow() {
|
|
28607
|
+
emitNow(checkPluginCache(deps.pluginCache()));
|
|
28608
|
+
}
|
|
28609
|
+
async function runSessionPayloadRow() {
|
|
28621
28610
|
const payload = checkSessionPayload(deps.sessionPayload());
|
|
28622
|
-
if (payload)
|
|
28611
|
+
if (payload) emitNow(payload);
|
|
28623
28612
|
}
|
|
28624
|
-
|
|
28625
|
-
|
|
28613
|
+
async function runMarketplaceRows() {
|
|
28614
|
+
for (const row of deps.marketplaceRows()) emitNow(row);
|
|
28615
|
+
}
|
|
28616
|
+
async function runSchedulesRow() {
|
|
28626
28617
|
const probe = await deps.schedulesNotebook().catch((e) => ({
|
|
28627
28618
|
armed: 0,
|
|
28628
28619
|
live: 0,
|
|
@@ -28631,32 +28622,32 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
28631
28622
|
drift: []
|
|
28632
28623
|
}));
|
|
28633
28624
|
const sched = checkSchedules(probe);
|
|
28634
|
-
if (sched)
|
|
28625
|
+
if (sched) emitNow(sched);
|
|
28635
28626
|
}
|
|
28636
|
-
|
|
28627
|
+
async function runRedactorRow() {
|
|
28637
28628
|
const redactor = checkRedactorLiveness(deps.redactorLiveness());
|
|
28638
|
-
if (redactor)
|
|
28629
|
+
if (redactor) emitNow(redactor);
|
|
28639
28630
|
}
|
|
28640
|
-
|
|
28631
|
+
async function runDocsAuditRow() {
|
|
28641
28632
|
const probe = await deps.docsAudit().catch((e) => ({
|
|
28642
28633
|
armed: true,
|
|
28643
28634
|
ok: false,
|
|
28644
28635
|
detail: `docs-audit probe failed \u2014 ${e.message}`
|
|
28645
28636
|
}));
|
|
28646
28637
|
const docsAudit2 = checkDocsAudit(probe);
|
|
28647
|
-
if (docsAudit2)
|
|
28638
|
+
if (docsAudit2) emitNow(docsAudit2);
|
|
28648
28639
|
}
|
|
28649
|
-
|
|
28640
|
+
async function runWorktreeRootsRow() {
|
|
28650
28641
|
const probe = await deps.worktreeRoots().catch(() => void 0);
|
|
28651
28642
|
const roots = checkWorktreeRoots(probe);
|
|
28652
|
-
if (roots)
|
|
28643
|
+
if (roots) emitNow(roots);
|
|
28653
28644
|
}
|
|
28654
|
-
|
|
28645
|
+
async function runTrainSyncRow() {
|
|
28655
28646
|
try {
|
|
28656
|
-
|
|
28647
|
+
emitNow(checkTrainSync(await deps.syncTrain()));
|
|
28657
28648
|
} catch (e) {
|
|
28658
28649
|
const message = e instanceof Error ? e.message : String(e);
|
|
28659
|
-
|
|
28650
|
+
emitNow({
|
|
28660
28651
|
ok: false,
|
|
28661
28652
|
id: "train-branches",
|
|
28662
28653
|
label: "train branches",
|
|
@@ -28664,6 +28655,8 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
28664
28655
|
verbose: [`sync threw: ${message}`, "no train branch was fast-forwarded this run"]
|
|
28665
28656
|
});
|
|
28666
28657
|
}
|
|
28658
|
+
}
|
|
28659
|
+
async function runHousekeeperRows() {
|
|
28667
28660
|
const repoRoot2 = await deps.repoRoot();
|
|
28668
28661
|
try {
|
|
28669
28662
|
const plan = await deps.gcPlan("origin", 200);
|
|
@@ -28674,7 +28667,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
28674
28667
|
if (r.removedBranches.length || r.removedRemoteBranches.length) detailParts.push(`${r.removedBranches.length + r.removedRemoteBranches.length} merged branches`);
|
|
28675
28668
|
if (r.removedWorktreeDirs.length) detailParts.push(`${r.removedWorktreeDirs.length} dead worktrees`);
|
|
28676
28669
|
if (r.removedTrackingRefs.length) detailParts.push(`${r.removedTrackingRefs.length} stale refs`);
|
|
28677
|
-
|
|
28670
|
+
emitNow({
|
|
28678
28671
|
id: "branches-worktrees",
|
|
28679
28672
|
ok: true,
|
|
28680
28673
|
label: "branches / worktrees",
|
|
@@ -28697,7 +28690,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
28697
28690
|
...plan.trackingRefs.map((r) => `stale tracking ref: ${r.ref}`),
|
|
28698
28691
|
...plan.worktreeDirs.map((w) => `dead worktree: ${w.path} (${w.reason})`)
|
|
28699
28692
|
];
|
|
28700
|
-
|
|
28693
|
+
emitNow(n === 0 ? { id: "branches-worktrees", ok: true, label: "branches / worktrees", verbose: ["nothing reapable"] } : {
|
|
28701
28694
|
id: "branches-worktrees",
|
|
28702
28695
|
ok: false,
|
|
28703
28696
|
label: "branches / worktrees",
|
|
@@ -28715,12 +28708,35 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
28715
28708
|
if (applyRepo && scratch.plan.safeAuto.length > 0) {
|
|
28716
28709
|
const applied = deps.executeScratchGc(repoRoot2, { apply: true });
|
|
28717
28710
|
const pruned = applied.applied?.pruned.length ?? 0;
|
|
28718
|
-
|
|
28711
|
+
emitNow({ id: "scratch", ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
|
|
28719
28712
|
if (pruned) restartPending = true;
|
|
28720
28713
|
} else {
|
|
28721
|
-
|
|
28722
|
-
}
|
|
28723
|
-
}
|
|
28714
|
+
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 --apply`", verbose: scratchEvidence });
|
|
28715
|
+
}
|
|
28716
|
+
}
|
|
28717
|
+
const table = [
|
|
28718
|
+
// Heals first: the slowest work starts at second zero, and a stale mmi-cli is the binary every row
|
|
28719
|
+
// below is being measured with (#3954).
|
|
28720
|
+
{ id: "plugin", when: true, run: runPluginRow },
|
|
28721
|
+
{ id: "cli-version", when: true, run: runCliRow },
|
|
28722
|
+
{ id: "github-auth", when: true, run: runGithubAuthRow },
|
|
28723
|
+
{ id: "github-pools", when: lane.full, run: runGithubPoolsRows },
|
|
28724
|
+
{ id: "aws-identity", when: true, run: runAwsRow },
|
|
28725
|
+
{ id: "repo-worktrees", when: true, run: runRepoWorktreesRow },
|
|
28726
|
+
{ id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow },
|
|
28727
|
+
{ id: "plugin-cache", when: true, run: runPluginCacheRow },
|
|
28728
|
+
{ id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
|
|
28729
|
+
{ id: "marketplace", when: true, run: runMarketplaceRows },
|
|
28730
|
+
{ id: "schedules", when: lane.full, run: runSchedulesRow },
|
|
28731
|
+
{ id: "redactor-liveness", when: lane.full, run: runRedactorRow },
|
|
28732
|
+
{ id: "docs-audit", when: lane.full, run: runDocsAuditRow },
|
|
28733
|
+
{ id: "worktree-roots", when: lane.full, run: runWorktreeRootsRow },
|
|
28734
|
+
// The two most expensive things in this file — a real `git fetch` plus train-branch fast-forward,
|
|
28735
|
+
// and a `gh`-backed gc sweep with a 20s timeout — so org repos on the full lane only (#3485).
|
|
28736
|
+
{ id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
|
|
28737
|
+
{ id: "housekeeper", when: isOrgRepo && lane.full, run: runHousekeeperRows }
|
|
28738
|
+
];
|
|
28739
|
+
for (const entry of table) if (entry.when) await entry.run();
|
|
28724
28740
|
const exitCode = doctorReportExitCode(checks);
|
|
28725
28741
|
if (opts.json) {
|
|
28726
28742
|
const payload = opts.verbose ? checks : checks.map(({ verbose: _evidence, ...check }) => check);
|
|
@@ -28733,7 +28749,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
28733
28749
|
return exitCode;
|
|
28734
28750
|
}
|
|
28735
28751
|
if (opts.banner) {
|
|
28736
|
-
const actionable = checks.filter((c) => !c.ok || c.warn);
|
|
28752
|
+
const actionable = checks.filter((c) => (!c.ok || c.warn) && !streamed.has(c));
|
|
28737
28753
|
for (const c of actionable) {
|
|
28738
28754
|
io.log(renderReport([c], { restartPending: false }));
|
|
28739
28755
|
if (opts.verbose) for (const evidence of c.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
|
|
@@ -28741,10 +28757,16 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
28741
28757
|
if (restartPending) io.log(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
|
|
28742
28758
|
return 0;
|
|
28743
28759
|
}
|
|
28744
|
-
const
|
|
28745
|
-
const
|
|
28746
|
-
|
|
28747
|
-
|
|
28760
|
+
const rest = checks.filter((c) => !streamed.has(c));
|
|
28761
|
+
const shown = opts.verbose ? rest : rest.filter((c) => !c.ok || c.warn);
|
|
28762
|
+
const lines = [];
|
|
28763
|
+
for (const check of shown) {
|
|
28764
|
+
lines.push(renderCheckLine(check));
|
|
28765
|
+
if (opts.verbose) for (const evidence of check.verbose ?? []) lines.push(`${VERBOSE_INDENT}${evidence}`);
|
|
28766
|
+
}
|
|
28767
|
+
lines.push(renderTally(checks));
|
|
28768
|
+
if (restartPending) lines.push(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
|
|
28769
|
+
io.log(lines.join("\n"));
|
|
28748
28770
|
return exitCode;
|
|
28749
28771
|
}
|
|
28750
28772
|
|
|
@@ -29065,10 +29087,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
29065
29087
|
githubRepoReach: githubRepoReachProbe,
|
|
29066
29088
|
awsCallerArn,
|
|
29067
29089
|
isOrgRepo: () => isOrgRepoRoot(),
|
|
29068
|
-
installedPluginVersion: installedActivePluginVersion,
|
|
29069
29090
|
surfaceEvidence: surfaceEvidenceOnce,
|
|
29070
|
-
pluginGuardState: activePluginGuardState,
|
|
29071
|
-
pluginSurface: () => detectSurface(process.env),
|
|
29072
29091
|
pluginTrustState: () => codexHookTrustState(),
|
|
29073
29092
|
releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
|
|
29074
29093
|
releasedVersionNote: throttled ? throttled.note : void 0,
|
|
@@ -29077,13 +29096,13 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
29077
29096
|
// marketplace clone + plugin cache) and neither is idempotent under concurrency. They share ONE lock
|
|
29078
29097
|
// rather than one each, because they are not independent: the plugin heal reinstalls a bundle that
|
|
29079
29098
|
// carries a CLI shim, so a concurrent npm global install races the same PATH surface.
|
|
29080
|
-
updateCli: (target) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target)),
|
|
29099
|
+
updateCli: (target, onStep) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target, onStep)),
|
|
29081
29100
|
// #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
|
|
29082
29101
|
// `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
|
|
29083
|
-
healPlugin: () => {
|
|
29102
|
+
healPlugin: (onStep) => {
|
|
29084
29103
|
const surface = detectSurface(process.env);
|
|
29085
|
-
const host =
|
|
29086
|
-
return withEnvHealLock(`${host} plugin reinstall`, () => healActivePluginForDoctor(surface));
|
|
29104
|
+
const host = surfaceEvidenceOnce(true)?.descriptor.displayName ?? surface;
|
|
29105
|
+
return withEnvHealLock(`${host} plugin reinstall`, () => healActivePluginForDoctor(surface, onStep));
|
|
29087
29106
|
},
|
|
29088
29107
|
currentCliVersion: resolveClientVersion,
|
|
29089
29108
|
readGitignore,
|
|
@@ -31485,6 +31504,9 @@ function renderReleaseResume(r) {
|
|
|
31485
31504
|
function renderReleaseAbort(r) {
|
|
31486
31505
|
return `mmi-cli release --abort --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
|
|
31487
31506
|
}
|
|
31507
|
+
function renderReleasePublishRetry(r) {
|
|
31508
|
+
return `mmi-cli release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
|
|
31509
|
+
}
|
|
31488
31510
|
function renderRcandResume(r) {
|
|
31489
31511
|
return `mmi-cli rcand --resume: promoted ${r.repo} \u2192 rc at ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}]; ${renderDeployLine(r)}; ${r.note}`;
|
|
31490
31512
|
}
|
|
@@ -31532,7 +31554,8 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
31532
31554
|
{ flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line Hub Slack summary; required for a new MMI-Hub --apply (#883/#3901)" },
|
|
31533
31555
|
{ flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
|
|
31534
31556
|
{ flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
|
|
31535
|
-
{ flags: "--abort", description: "with --apply, delete only a proven unpublished failed Hub tag and restore local main for a clean recut (#3944)" }
|
|
31557
|
+
{ flags: "--abort", description: "with --apply, delete only a proven unpublished failed Hub tag and restore local main for a clean recut (#3944)" },
|
|
31558
|
+
{ flags: "--retry-publish <run-id>", description: "with --apply, retry failed jobs of one proven Hub publish release run (#3949)" }
|
|
31536
31559
|
];
|
|
31537
31560
|
for (const f of RELEASE_ONLY_FLAGS) {
|
|
31538
31561
|
if (commandName === "release") {
|
|
@@ -31559,6 +31582,25 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
31559
31582
|
if (o.abort && commandName !== "release") {
|
|
31560
31583
|
return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished Hub release tag. Run: mmi-cli release --abort --apply`);
|
|
31561
31584
|
}
|
|
31585
|
+
if (o.retryPublish && commandName !== "release") {
|
|
31586
|
+
return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli release --retry-publish <run-id> --apply --watch`);
|
|
31587
|
+
}
|
|
31588
|
+
if (o.retryPublish) {
|
|
31589
|
+
if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
|
|
31590
|
+
if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
|
|
31591
|
+
if (o.announceSummaryFile || o.ack || o.dev) return fail("release: --retry-publish accepts only --apply, --watch, --repo and --json");
|
|
31592
|
+
if (o.repo) {
|
|
31593
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli release --retry-publish ${o.retryPublish} --apply${o.watch ? " --watch" : ""}`);
|
|
31594
|
+
if (!guard.ok) return fail(`release: ${guard.message}`);
|
|
31595
|
+
}
|
|
31596
|
+
const runId = Number.parseInt(o.retryPublish, 10);
|
|
31597
|
+
try {
|
|
31598
|
+
const result = await runReleasePublishRetry(trainApplyDeps(), runId, { approved: true, watch: o.watch });
|
|
31599
|
+
return printLine(o.json ? JSON.stringify(result, null, 2) : renderReleasePublishRetry(result));
|
|
31600
|
+
} catch (e) {
|
|
31601
|
+
return failGraceful(`release --retry-publish: ${e.message}`);
|
|
31602
|
+
}
|
|
31603
|
+
}
|
|
31562
31604
|
if (o.abort) {
|
|
31563
31605
|
if (o.resume) return fail("release: --abort and --resume are mutually exclusive \u2014 abort removes an unpublished tag while resume preserves and promotes it");
|
|
31564
31606
|
if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
|
package/package.json
CHANGED