@mutmutco/cli 3.0.1 → 3.1.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 +414 -82
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -6994,6 +6994,18 @@ var PR_LAND_POLL_MS = 3e4;
|
|
|
6994
6994
|
var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
|
|
6995
6995
|
var PR_LAND_STATE_READ_RETRIES = 3;
|
|
6996
6996
|
var PR_LAND_STATE_READ_DELAY_MS = 2e3;
|
|
6997
|
+
var PR_LAND_MERGE_RETRY_DELAY_MS = 3e3;
|
|
6998
|
+
async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
|
|
6999
|
+
const first = await deps.mergeAuto(prNumber, repo);
|
|
7000
|
+
if (first.mergeStatus !== "failed") return first;
|
|
7001
|
+
const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false }));
|
|
7002
|
+
if (!ready.open || !ready.mergeable || !ready.checksPassing) return first;
|
|
7003
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
|
|
7004
|
+
await sleep(PR_LAND_MERGE_RETRY_DELAY_MS);
|
|
7005
|
+
const retried = await deps.mergeAuto(prNumber, repo);
|
|
7006
|
+
if (retried.mergeStatus !== "failed") return retried;
|
|
7007
|
+
return { mergeStatus: "failed", error: `merge retry after transient failure also failed: ${retried.error ?? first.error ?? "unknown error"}` };
|
|
7008
|
+
}
|
|
6997
7009
|
async function readGhPrStateWithRetry(fetchState, options) {
|
|
6998
7010
|
const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
|
|
6999
7011
|
const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
|
|
@@ -7037,7 +7049,7 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
7037
7049
|
error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
|
|
7038
7050
|
};
|
|
7039
7051
|
}
|
|
7040
|
-
const merge = await
|
|
7052
|
+
const merge = await mergeAutoWithTransientRetry(prNumber, repo, deps);
|
|
7041
7053
|
base.mergeStatus = merge.mergeStatus;
|
|
7042
7054
|
if (merge.mergeStatus === "failed") {
|
|
7043
7055
|
return { ...base, error: merge.error ?? "merge failed" };
|
|
@@ -9411,7 +9423,7 @@ function trainPlan(command, options = {}) {
|
|
|
9411
9423
|
{ label: "merge development to main", gated: true },
|
|
9412
9424
|
{ label: "fold the version bump into the release commit (Hub: full distribution set; app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
|
|
9413
9425
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
9414
|
-
{ label: "trigger the repo deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release; other models deploy via their own workflow", gated: true },
|
|
9426
|
+
{ label: "trigger the repo deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release; registry-publish: own publish.yml auto-fires, watched on that repo, never a central dispatch (#2428); other models deploy via their own workflow", gated: true },
|
|
9415
9427
|
{ label: "roll development forward", gated: true }
|
|
9416
9428
|
];
|
|
9417
9429
|
}
|
|
@@ -9439,7 +9451,7 @@ function trainPlan(command, options = {}) {
|
|
|
9439
9451
|
{ label: "merge rc to main", gated: true },
|
|
9440
9452
|
{ label: "fold the version bump into the release commit (app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
|
|
9441
9453
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
9442
|
-
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml + publish.yml auto-fire on the release, correlate/watch those runs", gated: true },
|
|
9454
|
+
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml + publish.yml auto-fire on the release, correlate/watch those runs; registry-publish: no manual dispatch, own publish.yml auto-fires on the release, correlate/watch that run on the product repo (#2428)", gated: true },
|
|
9443
9455
|
{ label: "roll development forward", gated: true }
|
|
9444
9456
|
];
|
|
9445
9457
|
}
|
|
@@ -9943,9 +9955,12 @@ var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
|
|
|
9943
9955
|
var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
|
|
9944
9956
|
var TRAIN_CHECK_ATTEMPTS = 40;
|
|
9945
9957
|
var TRAIN_CHECK_DELAY_MS = 15e3;
|
|
9958
|
+
var TRAIN_PR_ONLY_AUTOMATION_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done"]);
|
|
9959
|
+
var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
|
|
9946
9960
|
async function correlateRun(deps, args) {
|
|
9947
9961
|
const sleep = resolveSleep(deps);
|
|
9948
9962
|
const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
|
|
9963
|
+
const repo = args.mode === "workflow" ? args.repo ?? HUB_REPO3 : HUB_REPO3;
|
|
9949
9964
|
let lastError;
|
|
9950
9965
|
let parsedAnyResponse = false;
|
|
9951
9966
|
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
@@ -9954,7 +9969,7 @@ async function correlateRun(deps, args) {
|
|
|
9954
9969
|
"run",
|
|
9955
9970
|
"list",
|
|
9956
9971
|
"--repo",
|
|
9957
|
-
|
|
9972
|
+
repo,
|
|
9958
9973
|
"--workflow",
|
|
9959
9974
|
args.workflow,
|
|
9960
9975
|
...args.mode === "workflow" ? ["--event", args.event] : [],
|
|
@@ -9997,10 +10012,10 @@ function correlateControlRun(deps, since, titleIncludes) {
|
|
|
9997
10012
|
async function correlateWorkflowRun(deps, args) {
|
|
9998
10013
|
return correlateRun(deps, { ...args, mode: "workflow" });
|
|
9999
10014
|
}
|
|
10000
|
-
async function watchTenantRun(deps, runId) {
|
|
10015
|
+
async function watchTenantRun(deps, runId, repo = HUB_REPO3) {
|
|
10001
10016
|
if (runId == null) return "pending";
|
|
10002
10017
|
try {
|
|
10003
|
-
await deps.run("gh", ["run", "watch", String(runId), "--repo",
|
|
10018
|
+
await deps.run("gh", ["run", "watch", String(runId), "--repo", repo, "--exit-status"]);
|
|
10004
10019
|
return "success";
|
|
10005
10020
|
} catch {
|
|
10006
10021
|
return "failure";
|
|
@@ -10013,9 +10028,9 @@ async function fetchControlRunLog(deps, runId) {
|
|
|
10013
10028
|
return "";
|
|
10014
10029
|
}
|
|
10015
10030
|
}
|
|
10016
|
-
async function watchWorkflowRun(deps, workflow, run) {
|
|
10031
|
+
async function watchWorkflowRun(deps, workflow, run, repo = HUB_REPO3) {
|
|
10017
10032
|
if (run.runId == null) return { workflow, conclusion: "pending" };
|
|
10018
|
-
const conclusion = await watchTenantRun(deps, run.runId);
|
|
10033
|
+
const conclusion = await watchTenantRun(deps, run.runId, repo);
|
|
10019
10034
|
return { workflow, runId: run.runId, runUrl: run.runUrl, conclusion };
|
|
10020
10035
|
}
|
|
10021
10036
|
function aggregateWorkflowRuns(runs) {
|
|
@@ -10124,6 +10139,7 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
10124
10139
|
let lastStatus = "not checked";
|
|
10125
10140
|
let lastError;
|
|
10126
10141
|
const everObserved = /* @__PURE__ */ new Set();
|
|
10142
|
+
const autoSatisfied = /* @__PURE__ */ new Set();
|
|
10127
10143
|
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
|
|
10128
10144
|
if (attempt > 0) await sleep(TRAIN_CHECK_DELAY_MS);
|
|
10129
10145
|
let checkRuns;
|
|
@@ -10145,18 +10161,25 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
10145
10161
|
for (const c of required) {
|
|
10146
10162
|
if (checkRuns.some((r) => r.name === c) || statuses.some((s) => s.context === c)) everObserved.add(c);
|
|
10147
10163
|
}
|
|
10148
|
-
|
|
10149
|
-
|
|
10164
|
+
if (attempt >= TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS - 1) {
|
|
10165
|
+
for (const c of required) {
|
|
10166
|
+
if (TRAIN_PR_ONLY_AUTOMATION_CONTEXTS.has(c) && !everObserved.has(c)) autoSatisfied.add(c);
|
|
10167
|
+
}
|
|
10168
|
+
}
|
|
10169
|
+
const pending = required.filter((c) => !autoSatisfied.has(c));
|
|
10170
|
+
const states = pending.map((c) => [c, resolveContextState(c, checkRuns, statuses)]);
|
|
10171
|
+
const satisfiedNote = autoSatisfied.size ? `, auto-satisfied (never runs on a tag, see #2404): ${[...autoSatisfied].join(", ")}` : "";
|
|
10172
|
+
lastStatus = `${states.map(([c, s]) => `${c}=${s}`).join(", ")}${satisfiedNote}`;
|
|
10150
10173
|
const failed = states.filter(([, s]) => s === "failed").map(([c]) => c);
|
|
10151
10174
|
if (failed.length > 0) {
|
|
10152
10175
|
throw new Error(`required train check failed: ${failed.join(", ")} (${lastStatus})`);
|
|
10153
10176
|
}
|
|
10154
10177
|
if (states.every(([, s]) => s === "success")) {
|
|
10155
|
-
return `required checks passed: ${
|
|
10178
|
+
return `required checks passed: ${pending.join(", ") || "(none pending)"}${satisfiedNote}`;
|
|
10156
10179
|
}
|
|
10157
10180
|
}
|
|
10158
10181
|
const waitedMin = Math.round((TRAIN_CHECK_ATTEMPTS - 1) * TRAIN_CHECK_DELAY_MS / 6e4);
|
|
10159
|
-
const neverMaterialized = required.filter((c) => !everObserved.has(c));
|
|
10182
|
+
const neverMaterialized = required.filter((c) => !everObserved.has(c) && !autoSatisfied.has(c));
|
|
10160
10183
|
const neverNote = neverMaterialized.length ? ` Never materialized on ${sha} (no check-run or commit status ever appeared \u2014 the workflow that produces them was likely not triggered by the tag event): ${neverMaterialized.join(", ")}.` : "";
|
|
10161
10184
|
throw new Error(
|
|
10162
10185
|
`timed out after ~${waitedMin}m (${TRAIN_CHECK_ATTEMPTS} attempts) waiting for required train checks on ${sha}.${neverNote} Last observed: ${lastError ? `error: ${lastError}` : lastStatus}`
|
|
@@ -10246,6 +10269,19 @@ function tenantPublishRecoveryCommand(slug, repo, ref, stage2, publishDir) {
|
|
|
10246
10269
|
if (publishDir && publishDir !== ".") parts.push(`-f publishDir=${publishDir}`);
|
|
10247
10270
|
return parts.join(" ");
|
|
10248
10271
|
}
|
|
10272
|
+
var PUBLISH_IDEMPOTENT_MARKER = /already on npm/i;
|
|
10273
|
+
var PUBLISH_LOG_RETRY_ATTEMPTS = 4;
|
|
10274
|
+
var PUBLISH_LOG_RETRY_DELAY_MS = 6e3;
|
|
10275
|
+
async function reconcilePublishFailure(deps, runId) {
|
|
10276
|
+
if (runId == null) return "failure";
|
|
10277
|
+
const sleep = resolveSleep(deps);
|
|
10278
|
+
for (let attempt = 0; attempt < PUBLISH_LOG_RETRY_ATTEMPTS; attempt++) {
|
|
10279
|
+
if (attempt > 0) await sleep(PUBLISH_LOG_RETRY_DELAY_MS);
|
|
10280
|
+
const log = await fetchControlRunLog(deps, runId);
|
|
10281
|
+
if (PUBLISH_IDEMPOTENT_MARKER.test(log)) return "success";
|
|
10282
|
+
}
|
|
10283
|
+
return "failure";
|
|
10284
|
+
}
|
|
10249
10285
|
async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure = "throw", publishDir) {
|
|
10250
10286
|
const since = (deps.now ?? Date.now)();
|
|
10251
10287
|
const dispatchArgs = [
|
|
@@ -10276,8 +10312,16 @@ async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFail
|
|
|
10276
10312
|
};
|
|
10277
10313
|
}
|
|
10278
10314
|
const { runId, runUrl } = await correlatePublishRun(deps, since, [ctx.slug, stage2]);
|
|
10279
|
-
|
|
10280
|
-
|
|
10315
|
+
let deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
10316
|
+
let note = `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`;
|
|
10317
|
+
if (deployStatus === "failure") {
|
|
10318
|
+
const reconciled = await reconcilePublishFailure(deps, runId);
|
|
10319
|
+
if (reconciled === "success") {
|
|
10320
|
+
deployStatus = "success";
|
|
10321
|
+
note = `${note}; run reported failure but its log confirms the version is already on npm (idempotent E409 \u2014 #2428)`;
|
|
10322
|
+
}
|
|
10323
|
+
}
|
|
10324
|
+
return { note, runId, runUrl, deployStatus };
|
|
10281
10325
|
}
|
|
10282
10326
|
async function dispatchPublishIfRequired(deps, ctx, meta, model, stage2, publishRef, watch, dispatchFailure) {
|
|
10283
10327
|
if (!meta.publishRequired || stage2 !== "main") return null;
|
|
@@ -10294,6 +10338,18 @@ function appendPublishDispatch(deploy, publish) {
|
|
|
10294
10338
|
deployStatus: deploy.deployStatus === "failure" || publish.deployStatus === "failure" ? "failure" : deploy.deployStatus === "pending" || publish.deployStatus === "pending" ? "pending" : "success"
|
|
10295
10339
|
};
|
|
10296
10340
|
}
|
|
10341
|
+
async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha) {
|
|
10342
|
+
const workflowRuns = [];
|
|
10343
|
+
for (const target of targets) {
|
|
10344
|
+
try {
|
|
10345
|
+
const run = await correlateWorkflowRun(deps, { ...target, since, headSha, repo });
|
|
10346
|
+
workflowRuns.push(await watchWorkflowRun(deps, target.workflow, run, repo));
|
|
10347
|
+
} catch {
|
|
10348
|
+
workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
|
|
10349
|
+
}
|
|
10350
|
+
}
|
|
10351
|
+
return workflowRuns;
|
|
10352
|
+
}
|
|
10297
10353
|
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
|
|
10298
10354
|
if (model === "tenant-container" || model === "solo-container") {
|
|
10299
10355
|
const since = (deps.now ?? Date.now)();
|
|
@@ -10312,7 +10368,18 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
10312
10368
|
return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
10313
10369
|
}
|
|
10314
10370
|
if (model === "registry-publish") {
|
|
10315
|
-
|
|
10371
|
+
const note = ref === "rc" ? "no dispatch on rc: registry-publish repos have no rc-stage Release to publish from" : "no central dispatch: this repo's own publish.yml auto-fires on the published Release (#2428)";
|
|
10372
|
+
if (ref === "rc" || !watch || !autoRunHeadSha) return { note, deployStatus: "pending" };
|
|
10373
|
+
const since = autoRunSince ?? (deps.now ?? Date.now)();
|
|
10374
|
+
const workflowRuns = await watchOwnWorkflowRuns(
|
|
10375
|
+
deps,
|
|
10376
|
+
ctx.repo,
|
|
10377
|
+
[{ workflow: "publish.yml", event: "release" }],
|
|
10378
|
+
since,
|
|
10379
|
+
autoRunHeadSha
|
|
10380
|
+
);
|
|
10381
|
+
const primary = workflowRuns[0];
|
|
10382
|
+
return { note, runId: primary?.runId, runUrl: primary?.runUrl, workflowRuns, deployStatus: aggregateWorkflowRuns(workflowRuns) };
|
|
10316
10383
|
}
|
|
10317
10384
|
if (model === "hub-serverless") {
|
|
10318
10385
|
const note = ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)";
|
|
@@ -10323,15 +10390,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
10323
10390
|
{ workflow: "deploy.yml", event: "release" },
|
|
10324
10391
|
{ workflow: "publish.yml", event: "release" }
|
|
10325
10392
|
];
|
|
10326
|
-
const workflowRuns =
|
|
10327
|
-
for (const target of targets) {
|
|
10328
|
-
try {
|
|
10329
|
-
const run = await correlateWorkflowRun(deps, { ...target, since, headSha: autoRunHeadSha });
|
|
10330
|
-
workflowRuns.push(await watchWorkflowRun(deps, target.workflow, run));
|
|
10331
|
-
} catch {
|
|
10332
|
-
workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
|
|
10333
|
-
}
|
|
10334
|
-
}
|
|
10393
|
+
const workflowRuns = await watchOwnWorkflowRuns(deps, HUB_REPO3, targets, since, autoRunHeadSha);
|
|
10335
10394
|
const primary = workflowRuns[0];
|
|
10336
10395
|
return {
|
|
10337
10396
|
note,
|
|
@@ -11165,7 +11224,11 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11165
11224
|
runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
|
|
11166
11225
|
}
|
|
11167
11226
|
deployNote = "watched release-triggered deploy.yml + publish.yml";
|
|
11168
|
-
} else if (deployModel === "
|
|
11227
|
+
} else if (deployModel === "registry-publish") {
|
|
11228
|
+
const run = await watchReleaseRun(deps, ctx, "publish.yml", mergedSha);
|
|
11229
|
+
runs.push(run);
|
|
11230
|
+
deployNote = "watched this repo's own release-triggered publish.yml (#2428 \u2014 no central dispatch)";
|
|
11231
|
+
} else if (deployModel === "tenant-container" || deployModel === "solo-container") {
|
|
11169
11232
|
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
11170
11233
|
const deploy = await dispatchDeploy(
|
|
11171
11234
|
deps,
|
|
@@ -11181,7 +11244,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11181
11244
|
);
|
|
11182
11245
|
const publish = deploy.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report") : null;
|
|
11183
11246
|
let dispatch = appendPublishDispatch(deploy, publish);
|
|
11184
|
-
if (!publish && deploy.deployStatus !== "success" && meta.publishRequired
|
|
11247
|
+
if (!publish && deploy.deployStatus !== "success" && meta.publishRequired) {
|
|
11185
11248
|
const reason = deploy.deployStatus === "failure" ? "box deploy failed \u2014 redeploy the box before publishing" : "box deploy not confirmed (run with --watch) \u2014 publish after the box deploy lands";
|
|
11186
11249
|
dispatch = {
|
|
11187
11250
|
...dispatch,
|
|
@@ -11189,25 +11252,17 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11189
11252
|
};
|
|
11190
11253
|
}
|
|
11191
11254
|
deployNote = dispatch.note;
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
});
|
|
11198
|
-
}
|
|
11255
|
+
runs.push({
|
|
11256
|
+
workflow: "tenant-deploy.yml",
|
|
11257
|
+
url: deploy.runUrl,
|
|
11258
|
+
conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
|
|
11259
|
+
});
|
|
11199
11260
|
if (publish?.runUrl) {
|
|
11200
11261
|
runs.push({
|
|
11201
11262
|
workflow: "tenant-publish.yml",
|
|
11202
11263
|
url: publish.runUrl,
|
|
11203
11264
|
conclusion: publish.deployStatus === "success" ? "success" : publish.deployStatus === "failure" ? "failure" : publish.deployStatus ?? "pending"
|
|
11204
11265
|
});
|
|
11205
|
-
} else if (deployModel === "registry-publish") {
|
|
11206
|
-
runs.push({
|
|
11207
|
-
workflow: "tenant-publish.yml",
|
|
11208
|
-
url: deploy.runUrl,
|
|
11209
|
-
conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
|
|
11210
|
-
});
|
|
11211
11266
|
}
|
|
11212
11267
|
} else {
|
|
11213
11268
|
deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
|
|
@@ -15329,10 +15384,12 @@ function isSemverVersion2(v) {
|
|
|
15329
15384
|
function staleRecordCommand(surface) {
|
|
15330
15385
|
return surface === "codex" ? CODEX_PLUGIN_RECOVERY : CLAUDE_PLUGIN_RECOVERY;
|
|
15331
15386
|
}
|
|
15387
|
+
var CLAUDE_UPDATE_BUTTON_WARNING = "Windows: do NOT use the in-app Update button for this plugin (#2411, hits the #1126 MAX_PATH nesting bug) \u2014 run `mmi-cli doctor --apply` instead";
|
|
15332
15388
|
function staleSurfacesFix(stale, releasedVersion) {
|
|
15333
15389
|
const parts = stale.map((s) => {
|
|
15334
15390
|
const at = s.recordPath ? ` (${s.recordPath})` : "";
|
|
15335
|
-
|
|
15391
|
+
const warning = s.surface === "claude" ? ` \u2014 ${CLAUDE_UPDATE_BUTTON_WARNING}` : "";
|
|
15392
|
+
return `${s.surface} record${at} is at ${s.installedVersion}${releasedVersion ? ` < ${releasedVersion}` : ""} \u2014 run: ${staleRecordCommand(s.surface)}${warning}`;
|
|
15336
15393
|
});
|
|
15337
15394
|
return `stale installed-plugin record on ${stale.map((s) => s.surface).join(" + ")}: ${parts.join("; ")}`;
|
|
15338
15395
|
}
|
|
@@ -15374,18 +15431,71 @@ function buildInstalledPluginVersionCheck(input) {
|
|
|
15374
15431
|
staleSurfaces: stale
|
|
15375
15432
|
};
|
|
15376
15433
|
}
|
|
15434
|
+
var MANAGED_PLUGINS = [
|
|
15435
|
+
{
|
|
15436
|
+
// Marketplace-qualified key exactly as installed_plugins.json stores it (same convention as
|
|
15437
|
+
// MMI_PLUGIN_ID = 'mmi@mutmutco') — a bare 'jervaise-powertools' would never match a real record
|
|
15438
|
+
// and the check would be a permanent no-op. The same `<marketplace>/<name>` segments also address its
|
|
15439
|
+
// Cursor Team Marketplace cache dir (~/.cursor/plugins/cache/jervaise/jervaise-powertools/<version>/).
|
|
15440
|
+
id: "jervaise-powertools@jervaise",
|
|
15441
|
+
label: "jervaise-powertools",
|
|
15442
|
+
healCommand: "jerv-cli doctor --apply",
|
|
15443
|
+
opencodePackage: "@jervaise/opencode-jerv"
|
|
15444
|
+
}
|
|
15445
|
+
];
|
|
15446
|
+
function parseManagedPluginId(id) {
|
|
15447
|
+
const at = id.lastIndexOf("@");
|
|
15448
|
+
if (at <= 0 || at === id.length - 1) return null;
|
|
15449
|
+
return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
|
|
15450
|
+
}
|
|
15451
|
+
var MANAGED_PLUGIN_DRIFT_LABEL = "managed org plugin version drift (cross-surface)";
|
|
15452
|
+
function managedPluginDriftFix(drifted) {
|
|
15453
|
+
return drifted.map((d) => {
|
|
15454
|
+
const versions = d.versions.map((v) => `${v.version} (${v.surface})`).join(" vs ");
|
|
15455
|
+
return `${d.label} drift: ${versions} \u2192 run \`${d.healCommand}\``;
|
|
15456
|
+
}).join(" ; ");
|
|
15457
|
+
}
|
|
15458
|
+
function buildManagedPluginDriftCheck(input) {
|
|
15459
|
+
const base = { ok: true, label: MANAGED_PLUGIN_DRIFT_LABEL, fix: "" };
|
|
15460
|
+
if (!input.isOrgRepo) return base;
|
|
15461
|
+
const registry2 = input.plugins ?? MANAGED_PLUGINS;
|
|
15462
|
+
const drifted = [];
|
|
15463
|
+
const normalize = (v) => v.replace(/^v/, "");
|
|
15464
|
+
for (const descriptor of registry2) {
|
|
15465
|
+
const versions = [];
|
|
15466
|
+
for (const source of input.sources) {
|
|
15467
|
+
if (source.directVersions && descriptor.id in source.directVersions) {
|
|
15468
|
+
const version2 = source.directVersions[descriptor.id];
|
|
15469
|
+
if (isSemverVersion2(version2)) versions.push({ surface: source.surface, version: normalize(version2) });
|
|
15470
|
+
continue;
|
|
15471
|
+
}
|
|
15472
|
+
const records = source.installed?.plugins?.[descriptor.id];
|
|
15473
|
+
if (!Array.isArray(records) || records.length === 0) continue;
|
|
15474
|
+
const recordVersion = bestRecord(records).version;
|
|
15475
|
+
const version = isSemverVersion2(recordVersion) ? recordVersion : highestSemver(source.cacheVersions?.[descriptor.id] ?? []);
|
|
15476
|
+
if (!isSemverVersion2(version)) continue;
|
|
15477
|
+
versions.push({ surface: source.surface, version: normalize(version) });
|
|
15478
|
+
}
|
|
15479
|
+
if (versions.length < 2) continue;
|
|
15480
|
+
const distinctVersions = new Set(versions.map((v) => v.version));
|
|
15481
|
+
if (distinctVersions.size < 2) continue;
|
|
15482
|
+
drifted.push({ pluginId: descriptor.id, label: descriptor.label, healCommand: descriptor.healCommand, versions });
|
|
15483
|
+
}
|
|
15484
|
+
if (drifted.length === 0) return base;
|
|
15485
|
+
return { ...base, ok: false, fix: managedPluginDriftFix(drifted), drifted };
|
|
15486
|
+
}
|
|
15377
15487
|
var OPENCODE_VERSION_LABEL = "installed OpenCode MMI adapter version (vs latest release)";
|
|
15378
15488
|
function buildOpencodeVersionCheck(input) {
|
|
15379
15489
|
const fix = pluginRecoveryFix("opencode");
|
|
15380
15490
|
const base = { ok: true, label: OPENCODE_VERSION_LABEL, fix };
|
|
15381
15491
|
if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
|
|
15382
15492
|
if (!isSemverVersion2(input.installedVersion)) {
|
|
15383
|
-
return { ...base, ok: false, releasedVersion: input.releasedVersion };
|
|
15493
|
+
return { ...base, ok: false, severityOverride: "hard", releasedVersion: input.releasedVersion };
|
|
15384
15494
|
}
|
|
15385
15495
|
if (compareVersions(input.installedVersion, input.releasedVersion) >= 0) {
|
|
15386
15496
|
return { ...base, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15387
15497
|
}
|
|
15388
|
-
return { ...base, ok: false, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15498
|
+
return { ...base, ok: false, severityOverride: "hard", installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15389
15499
|
}
|
|
15390
15500
|
function pickOpencodeActiveVersion(input) {
|
|
15391
15501
|
for (const value of [input.envStamp, input.cacheVersion, input.diskVersion]) {
|
|
@@ -15634,9 +15744,10 @@ function buildCursorPluginInstallCheck(input) {
|
|
|
15634
15744
|
return {
|
|
15635
15745
|
...base,
|
|
15636
15746
|
ok: false,
|
|
15747
|
+
severityOverride: "advisory",
|
|
15637
15748
|
cacheRoot: input.cacheRoot,
|
|
15638
15749
|
pins: input.pins,
|
|
15639
|
-
fix: `Cursor MMI plugin cache is behind ${input.releasedVersion} (${stale}) \u2014 run \`mmi-cli doctor --apply\` to seed the cache from the latest release (effective after restart Cursor);
|
|
15750
|
+
fix: `Cursor MMI plugin cache is behind ${input.releasedVersion} (${stale}) \u2014 run \`mmi-cli doctor --apply\` to seed the cache from the latest release (effective after restart Cursor); cache healthy after that, but the Team Marketplace REGISTRATION may still show stale \u2014 a master-admin must manually refresh it: Cursor Settings \u2192 Plugins \u2192 Marketplace, click Update next to the MMI Team Marketplace (no per-user CLI exists to do this automatically, #2409); ${CURSOR_MARKETPLACE_INSTALL_GUIDE}`
|
|
15640
15751
|
};
|
|
15641
15752
|
}
|
|
15642
15753
|
return { ...base, cacheRoot: input.cacheRoot, pins: input.pins };
|
|
@@ -15830,6 +15941,30 @@ function preflightOutcome(input) {
|
|
|
15830
15941
|
function pluginAutonomousHaltLine(reloadHint) {
|
|
15831
15942
|
return `\u26A0 PLUGIN RELOAD REQUIRED \u2014 mmi:* skills and agent types are unavailable until you ${reloadHint}. Halt autonomous /grind and /build until then.`;
|
|
15832
15943
|
}
|
|
15944
|
+
var DOCTOR_EXTENDED_CHECK_LABELS = /* @__PURE__ */ new Set([
|
|
15945
|
+
AWS_CROSS_ACCOUNT_LABEL,
|
|
15946
|
+
HUB_DEPLOY_FRESHNESS_LABEL,
|
|
15947
|
+
PLAYWRIGHT_MCP_VISION_CAP_LABEL,
|
|
15948
|
+
PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL,
|
|
15949
|
+
BROWSER_ARTIFACTS_LABEL,
|
|
15950
|
+
SCRATCH_GC_LABEL,
|
|
15951
|
+
GIT_GC_LABEL,
|
|
15952
|
+
OPENCODE_VERSION_LABEL,
|
|
15953
|
+
OPENCODE_DESKTOP_BOOTSTRAP_LABEL
|
|
15954
|
+
]);
|
|
15955
|
+
function isDoctorExtendedCheck(label) {
|
|
15956
|
+
if (DOCTOR_EXTENDED_CHECK_LABELS.has(label)) return true;
|
|
15957
|
+
if (label.startsWith(HUB_DEPLOY_FRESHNESS_LABEL)) return true;
|
|
15958
|
+
if (label.startsWith("@mutmutco design-system") || label.startsWith("@mutmutco registry components")) return true;
|
|
15959
|
+
return false;
|
|
15960
|
+
}
|
|
15961
|
+
function isAdvisoryDoctorCheck(check) {
|
|
15962
|
+
if (check.severityOverride) return check.severityOverride === "advisory";
|
|
15963
|
+
return isDoctorExtendedCheck(check.label);
|
|
15964
|
+
}
|
|
15965
|
+
function doctorExitCode(checks) {
|
|
15966
|
+
return checks.some((c) => !c.ok && !isAdvisoryDoctorCheck(c)) ? 1 : 0;
|
|
15967
|
+
}
|
|
15833
15968
|
function renderPluginUpdateReportStaleOnly(report) {
|
|
15834
15969
|
const v = report.versions;
|
|
15835
15970
|
const released = v.released;
|
|
@@ -15844,24 +15979,97 @@ function renderPluginUpdateReportStaleOnly(report) {
|
|
|
15844
15979
|
return ["Update commands (stale surfaces):", ...blocks];
|
|
15845
15980
|
}
|
|
15846
15981
|
var DOCTOR_VERBOSE_HINT = "Run mmi-cli doctor --verbose for the full audit checklist + version report.";
|
|
15847
|
-
function
|
|
15848
|
-
|
|
15849
|
-
|
|
15850
|
-
|
|
15851
|
-
|
|
15982
|
+
function doctorCheckGlyph(check) {
|
|
15983
|
+
if (check.ok) return "\u2713";
|
|
15984
|
+
return isAdvisoryDoctorCheck(check) ? "\u26A0" : "\u2717";
|
|
15985
|
+
}
|
|
15986
|
+
function doctorVerdictLine(input) {
|
|
15987
|
+
const gaps = input.checks.filter((c) => !c.ok);
|
|
15988
|
+
const hardGaps = gaps.filter((c) => !isAdvisoryDoctorCheck(c));
|
|
15989
|
+
const softGaps = gaps.filter((c) => isAdvisoryDoctorCheck(c));
|
|
15990
|
+
if (input.shouldApply) {
|
|
15991
|
+
if (hardGaps.length > 0) {
|
|
15992
|
+
return `\u2717 ${hardGaps.length} repair${hardGaps.length === 1 ? "" : "s"} failed \u2014 see the items above.`;
|
|
15993
|
+
}
|
|
15994
|
+
if (softGaps.length > 0) {
|
|
15995
|
+
return `\u26A0 Healed what I could; ${softGaps.length} item${softGaps.length === 1 ? "" : "s"} still need${softGaps.length === 1 ? "s" : ""} attention (above).`;
|
|
15996
|
+
}
|
|
15997
|
+
const healed = input.healedCount ?? 0;
|
|
15998
|
+
if (healed > 0) return `\u2713 Healed ${healed} item${healed === 1 ? "" : "s"} \u2014 everything checks out now.`;
|
|
15999
|
+
return "\u2713 Everything healthy \u2014 nothing needed fixing.";
|
|
15852
16000
|
}
|
|
15853
|
-
const
|
|
15854
|
-
|
|
15855
|
-
|
|
15856
|
-
|
|
16001
|
+
const attention = gaps.length;
|
|
16002
|
+
if (attention > 0) {
|
|
16003
|
+
return `\u26A0 ${attention} item${attention === 1 ? " needs" : "s need"} attention \u2014 run \`mmi-cli doctor --apply\` to heal.`;
|
|
16004
|
+
}
|
|
16005
|
+
return "\u2713 Everything healthy.";
|
|
16006
|
+
}
|
|
16007
|
+
function doctorHumanLines(input) {
|
|
16008
|
+
const gaps = input.checks.filter((c) => !c.ok);
|
|
16009
|
+
const lines = [
|
|
16010
|
+
doctorVerdictLine({ checks: input.checks, shouldApply: input.shouldApply, healedCount: input.healedCount }),
|
|
16011
|
+
"",
|
|
16012
|
+
"Checks",
|
|
16013
|
+
...input.checks.map((c) => ` ${doctorCheckGlyph(c)} ${c.label}`)
|
|
16014
|
+
];
|
|
16015
|
+
if (gaps.length > 0) {
|
|
16016
|
+
lines.push("", input.shouldApply ? "Repairs" : "Will fix on --apply");
|
|
16017
|
+
for (const c of gaps) {
|
|
16018
|
+
const mark = input.shouldApply ? isAdvisoryDoctorCheck(c) ? "\u26A0" : "\u2717" : "\u2192";
|
|
16019
|
+
lines.push(` ${mark} ${c.label} \u2014 ${c.fix}`);
|
|
16020
|
+
}
|
|
16021
|
+
}
|
|
16022
|
+
if (input.shouldApply && input.pluginReloadRequired) {
|
|
16023
|
+
lines.push("", `\u21BB ${input.reloadHint ?? "reload"} so the healed plugin/MCP installs load.`);
|
|
15857
16024
|
}
|
|
15858
16025
|
const stale = renderPluginUpdateReportStaleOnly(input.updateReport);
|
|
15859
16026
|
if (stale.length) {
|
|
15860
|
-
lines.push("");
|
|
15861
|
-
|
|
16027
|
+
lines.push("", ...stale);
|
|
16028
|
+
} else if (!input.verbose) {
|
|
16029
|
+
lines.push("", DOCTOR_VERBOSE_HINT);
|
|
16030
|
+
}
|
|
16031
|
+
if (!input.verbose) return lines;
|
|
16032
|
+
lines.push("", ...renderPluginUpdateReport(input.updateReport));
|
|
16033
|
+
lines.push(
|
|
16034
|
+
"",
|
|
16035
|
+
doctorSummaryLine({ checks: input.checks, updateReport: input.updateReport, shouldApply: input.shouldApply, healedCount: input.healedCount }),
|
|
16036
|
+
...doctorAuditLines({
|
|
16037
|
+
checks: input.checks,
|
|
16038
|
+
shouldApply: input.shouldApply,
|
|
16039
|
+
healedCount: input.healedCount,
|
|
16040
|
+
cliVersion: input.updateReport.versions.cli,
|
|
16041
|
+
releasedVersion: input.updateReport.versions.released
|
|
16042
|
+
})
|
|
16043
|
+
);
|
|
16044
|
+
return lines;
|
|
16045
|
+
}
|
|
16046
|
+
function doctorSummaryLine(input) {
|
|
16047
|
+
const gaps = input.checks.filter((c) => !c.ok);
|
|
16048
|
+
const mode = input.shouldApply ? "apply" : "plan";
|
|
16049
|
+
const released = input.updateReport.versions.released ?? "unknown";
|
|
16050
|
+
const failed = gaps.filter((c) => !isAdvisoryDoctorCheck(c)).length;
|
|
16051
|
+
const countSummary = input.shouldApply ? `${input.healedCount ?? 0} healed` : `${gaps.length} planned`;
|
|
16052
|
+
return `summary: mode ${mode} \xB7 released ${released} \xB7 ${countSummary} \xB7 ${failed} failed`;
|
|
16053
|
+
}
|
|
16054
|
+
function doctorAuditLines(input) {
|
|
16055
|
+
const gaps = input.checks.filter((c) => !c.ok);
|
|
16056
|
+
const failed = gaps.filter((c) => !isAdvisoryDoctorCheck(c)).length;
|
|
16057
|
+
const lines = [
|
|
16058
|
+
"MMI doctor audit",
|
|
16059
|
+
`mode: ${input.shouldApply ? "apply" : "plan"}`,
|
|
16060
|
+
`cli: ${input.cliVersion ?? "unknown"}`,
|
|
16061
|
+
`released: ${input.releasedVersion ?? "unknown"}`,
|
|
16062
|
+
`checks: ${input.checks.length} (${input.checks.length - gaps.length} ok, ${gaps.length} gap)`,
|
|
16063
|
+
`planned actions: ${gaps.length}`,
|
|
16064
|
+
`healed actions: ${input.healedCount ?? 0}`,
|
|
16065
|
+
`failed actions: ${failed}`
|
|
16066
|
+
];
|
|
16067
|
+
if (gaps.length) {
|
|
16068
|
+
lines.push("actions:");
|
|
16069
|
+
for (const c of gaps) {
|
|
16070
|
+
lines.push(`[${isAdvisoryDoctorCheck(c) ? "advisory" : "auto"}] ${c.label}: ${c.fix}`);
|
|
16071
|
+
}
|
|
15862
16072
|
}
|
|
15863
|
-
lines.push("");
|
|
15864
|
-
lines.push(`\u26A0 ${input.gaps.length} item(s) need attention \u2014 ${DOCTOR_VERBOSE_HINT}`);
|
|
15865
16073
|
return lines;
|
|
15866
16074
|
}
|
|
15867
16075
|
var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
|
|
@@ -16246,6 +16454,77 @@ function installedPluginSources() {
|
|
|
16246
16454
|
}
|
|
16247
16455
|
});
|
|
16248
16456
|
}
|
|
16457
|
+
function managedPluginSurfaceSources() {
|
|
16458
|
+
const claudeCodex = installedPluginSources().map(({ surface, installed }) => {
|
|
16459
|
+
const cacheVersions = {};
|
|
16460
|
+
for (const descriptor of MANAGED_PLUGINS) {
|
|
16461
|
+
const segments = parseManagedPluginId(descriptor.id);
|
|
16462
|
+
if (!segments) continue;
|
|
16463
|
+
try {
|
|
16464
|
+
cacheVersions[descriptor.id] = (0, import_node_fs17.readdirSync)(
|
|
16465
|
+
(0, import_node_path17.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "cache", segments.marketplace, segments.name),
|
|
16466
|
+
{ withFileTypes: true }
|
|
16467
|
+
).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
16468
|
+
} catch {
|
|
16469
|
+
}
|
|
16470
|
+
}
|
|
16471
|
+
return { surface, installed, cacheVersions };
|
|
16472
|
+
});
|
|
16473
|
+
return [
|
|
16474
|
+
...claudeCodex,
|
|
16475
|
+
{ surface: "cursor", directVersions: managedPluginCursorDirectVersions() },
|
|
16476
|
+
{ surface: "opencode", directVersions: managedPluginOpencodeDirectVersions() }
|
|
16477
|
+
];
|
|
16478
|
+
}
|
|
16479
|
+
function managedPluginCursorDirectVersions() {
|
|
16480
|
+
const out = {};
|
|
16481
|
+
for (const descriptor of MANAGED_PLUGINS) {
|
|
16482
|
+
const segments = parseManagedPluginId(descriptor.id);
|
|
16483
|
+
if (!segments) continue;
|
|
16484
|
+
try {
|
|
16485
|
+
const versions = (0, import_node_fs17.readdirSync)(
|
|
16486
|
+
(0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", segments.marketplace, segments.name),
|
|
16487
|
+
{ withFileTypes: true }
|
|
16488
|
+
).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
16489
|
+
out[descriptor.id] = highestSemver(versions);
|
|
16490
|
+
} catch {
|
|
16491
|
+
}
|
|
16492
|
+
}
|
|
16493
|
+
return out;
|
|
16494
|
+
}
|
|
16495
|
+
function managedPluginOpencodeDirectVersions() {
|
|
16496
|
+
const out = {};
|
|
16497
|
+
for (const descriptor of MANAGED_PLUGINS) {
|
|
16498
|
+
if (!descriptor.opencodePackage) continue;
|
|
16499
|
+
out[descriptor.id] = readOpencodePackageVersion(descriptor.opencodePackage);
|
|
16500
|
+
}
|
|
16501
|
+
return out;
|
|
16502
|
+
}
|
|
16503
|
+
function readOpencodePackageVersionFrom(packageJsonPath) {
|
|
16504
|
+
try {
|
|
16505
|
+
const parsed = JSON.parse((0, import_node_fs17.readFileSync)(packageJsonPath, "utf8"));
|
|
16506
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
|
|
16507
|
+
} catch {
|
|
16508
|
+
return void 0;
|
|
16509
|
+
}
|
|
16510
|
+
}
|
|
16511
|
+
function readOpencodePackageVersion(packageName) {
|
|
16512
|
+
const [scope, name] = packageName.startsWith("@") ? packageName.slice(1).split("/", 2) : [void 0, packageName];
|
|
16513
|
+
if (!name) return void 0;
|
|
16514
|
+
const diskCandidates = [
|
|
16515
|
+
(0, import_node_path17.join)(opencodeConfigDir(), "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json"),
|
|
16516
|
+
(0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json")
|
|
16517
|
+
];
|
|
16518
|
+
const diskVersion = diskCandidates.map(readOpencodePackageVersionFrom).find((v) => v !== void 0);
|
|
16519
|
+
const packagesRoot = (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "packages", ...scope ? [`@${scope}`] : []);
|
|
16520
|
+
let cacheVersion;
|
|
16521
|
+
try {
|
|
16522
|
+
const cacheVersions = (0, import_node_fs17.readdirSync)(packagesRoot, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith(`${name}@`) && !e.name.startsWith(".")).map((e) => readOpencodePackageVersionFrom((0, import_node_path17.join)(packagesRoot, e.name, "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json"))).filter((v) => Boolean(v));
|
|
16523
|
+
if (cacheVersions.length) cacheVersion = cacheVersions.reduce((lowest, v) => compareVersions(v, lowest) < 0 ? v : lowest);
|
|
16524
|
+
} catch {
|
|
16525
|
+
}
|
|
16526
|
+
return pickOpencodeActiveVersion({ cacheVersion, diskVersion });
|
|
16527
|
+
}
|
|
16249
16528
|
function readClaudeSettings() {
|
|
16250
16529
|
try {
|
|
16251
16530
|
return JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path17.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
@@ -16704,7 +16983,6 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16704
16983
|
const repairLocal = !opts.json || Boolean(opts.apply) || Boolean(opts.preflight);
|
|
16705
16984
|
const repoWritesAllowed = !opts.noRepoWrites;
|
|
16706
16985
|
const runExtended = Boolean(opts.verbose) || Boolean(opts.json);
|
|
16707
|
-
const terseOutput = !opts.verbose && !opts.json && !opts.banner && !opts.preflight;
|
|
16708
16986
|
const checks = [];
|
|
16709
16987
|
const REWRITE_KEY = "url.https://github.com/.insteadOf";
|
|
16710
16988
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
@@ -16774,6 +17052,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16774
17052
|
const markPluginReloadRequired = () => {
|
|
16775
17053
|
pluginReloadRequired = true;
|
|
16776
17054
|
};
|
|
17055
|
+
let healedCount = 0;
|
|
17056
|
+
const markHealed = () => {
|
|
17057
|
+
healedCount += 1;
|
|
17058
|
+
};
|
|
16777
17059
|
let versionReport = buildVersionLagReport({
|
|
16778
17060
|
currentVersion: resolveClientVersion(),
|
|
16779
17061
|
repoVersion: readRepoVersion(),
|
|
@@ -16826,6 +17108,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16826
17108
|
try {
|
|
16827
17109
|
await execFileP2("git", ["config", "--global", "--add", REWRITE_KEY, "git@github.com:"]);
|
|
16828
17110
|
cloneOk = true;
|
|
17111
|
+
markHealed();
|
|
16829
17112
|
io.err(" \u21BB repaired: git insteadOf git@github.com \u2192 https (plugin clone over HTTPS)");
|
|
16830
17113
|
} catch {
|
|
16831
17114
|
}
|
|
@@ -16844,6 +17127,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16844
17127
|
if (!pluginCheck.ok && pluginCheck.recordToInsert && repairLocal) {
|
|
16845
17128
|
if (writeProjectInstallRecord(pluginCheck.recordToInsert)) {
|
|
16846
17129
|
pluginCheck = { ...pluginCheck, ok: true };
|
|
17130
|
+
markHealed();
|
|
16847
17131
|
io.err(` \u21BB repaired: registered mmi@mutmutco project install record \u2014 ${reloadHint} to load MMI commands`);
|
|
16848
17132
|
}
|
|
16849
17133
|
}
|
|
@@ -16863,6 +17147,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16863
17147
|
surface
|
|
16864
17148
|
});
|
|
16865
17149
|
if (legacyPluginCheck.ok) {
|
|
17150
|
+
markHealed();
|
|
16866
17151
|
io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via claude plugin \u2014 ${reloadHint} to load MMI commands`);
|
|
16867
17152
|
}
|
|
16868
17153
|
}
|
|
@@ -16873,6 +17158,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16873
17158
|
surface
|
|
16874
17159
|
});
|
|
16875
17160
|
if (legacyPluginCheck.ok) {
|
|
17161
|
+
markHealed();
|
|
16876
17162
|
io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via codex plugin \u2014 ${reloadHint} to load MMI commands`);
|
|
16877
17163
|
}
|
|
16878
17164
|
}
|
|
@@ -16891,6 +17177,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16891
17177
|
gitignoreCheck.removed?.length ? `removed ${gitignoreCheck.removed.join(", ")}` : ""
|
|
16892
17178
|
].filter(Boolean).join("; ") || "normalized the block";
|
|
16893
17179
|
gitignoreCheck = { ...gitignoreCheck, ok: true };
|
|
17180
|
+
markHealed();
|
|
16894
17181
|
io.err(` \u21BB repaired: org-managed .gitignore block \u2014 ${drift}`);
|
|
16895
17182
|
io.err(" this is an org-managed update (not unrelated churn) \u2014 stage & commit .gitignore so it stops recurring");
|
|
16896
17183
|
}
|
|
@@ -16904,6 +17191,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16904
17191
|
if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
|
|
16905
17192
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
16906
17193
|
driftCheck = { ...driftCheck, ok: true };
|
|
17194
|
+
markHealed();
|
|
16907
17195
|
io.err(` \u21BB repaired: collapsed mmi@mutmutco to one user-scope entry (backup at installed_plugins.json.bak) \u2014 ${reloadHint} to load MMI commands`);
|
|
16908
17196
|
}
|
|
16909
17197
|
}
|
|
@@ -16936,6 +17224,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16936
17224
|
installedVersionCheck = healed;
|
|
16937
17225
|
if (healed.ok) {
|
|
16938
17226
|
markPluginReloadRequired();
|
|
17227
|
+
markHealed();
|
|
16939
17228
|
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via claude plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
16940
17229
|
}
|
|
16941
17230
|
}
|
|
@@ -16945,11 +17234,13 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16945
17234
|
installedVersionCheck = healed;
|
|
16946
17235
|
if (healed.ok) {
|
|
16947
17236
|
markPluginReloadRequired();
|
|
17237
|
+
markHealed();
|
|
16948
17238
|
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
16949
17239
|
}
|
|
16950
17240
|
}
|
|
16951
17241
|
}
|
|
16952
17242
|
checks.push(installedVersionCheck);
|
|
17243
|
+
checks.push(buildManagedPluginDriftCheck({ isOrgRepo, sources: managedPluginSurfaceSources() }));
|
|
16953
17244
|
let openCodeConfigSnapshot = opencodeConfigSnapshot();
|
|
16954
17245
|
const inspectOpenCode = surface === "opencode" || openCodeConfigSnapshot.hasConfig || runExtended;
|
|
16955
17246
|
if (inspectOpenCode) {
|
|
@@ -16974,6 +17265,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16974
17265
|
});
|
|
16975
17266
|
if (opencodeConfigCheck.ok) {
|
|
16976
17267
|
markPluginReloadRequired();
|
|
17268
|
+
markHealed();
|
|
16977
17269
|
io.err(` \u21BB repaired: wired ${OPENCODE_PLUGIN_PACKAGE} in OpenCode config \u2014 ${reloadAction("opencode")} to load MMI commands`);
|
|
16978
17270
|
}
|
|
16979
17271
|
}
|
|
@@ -16997,6 +17289,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16997
17289
|
});
|
|
16998
17290
|
if (opencodeVersionCheck.ok) {
|
|
16999
17291
|
markPluginReloadRequired();
|
|
17292
|
+
markHealed();
|
|
17000
17293
|
io.err(` \u21BB force-refreshed OpenCode MMI plugin \u2192 ${opencodeInstalledVersion ?? releasedVersion ?? "latest"} \u2014 ${reloadAction("opencode")} to load it`);
|
|
17001
17294
|
}
|
|
17002
17295
|
}
|
|
@@ -17035,6 +17328,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17035
17328
|
});
|
|
17036
17329
|
if (surfaceAssetsCheck.ok) {
|
|
17037
17330
|
markPluginReloadRequired();
|
|
17331
|
+
markHealed();
|
|
17038
17332
|
io.err(` \u21BB materialized OpenCode MMI commands + skills path \u2014 ${reloadAction("opencode")} to load them`);
|
|
17039
17333
|
}
|
|
17040
17334
|
}
|
|
@@ -17056,6 +17350,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17056
17350
|
if (!legacyOpenCodeCheck.ok && repairLocal && legacyOpenCodeConfig.legacyPath) {
|
|
17057
17351
|
if (quarantineOpencodeLegacyConfig(legacyOpenCodeConfig.legacyPath)) {
|
|
17058
17352
|
legacyOpenCodeCheck = buildOpencodeLegacyConfigCheck({ isOrgRepo: true });
|
|
17353
|
+
markHealed();
|
|
17059
17354
|
io.err(` \u21BB quarantined legacy OpenCode config \u2192 ${legacyOpenCodeConfig.legacyPath}.bak \u2014 restart OpenCode`);
|
|
17060
17355
|
}
|
|
17061
17356
|
}
|
|
@@ -17080,6 +17375,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17080
17375
|
const surfaces = [...new Set(cacheCleanupCheck.leftovers?.map((entry) => entry.surface) ?? [])].join("/");
|
|
17081
17376
|
const names = cacheCleanupCheck.leftovers?.map((entry) => entry.name).join(", ");
|
|
17082
17377
|
markPluginReloadRequired();
|
|
17378
|
+
markHealed();
|
|
17083
17379
|
io.err(` \u21BB quarantined ${moved} stale MMI plugin cache dir(s) for ${surfaces || "agent surfaces"}: ${names} \u2014 ${reloadHint} to load MMI commands`);
|
|
17084
17380
|
}
|
|
17085
17381
|
cacheCleanupCheck = {
|
|
@@ -17107,6 +17403,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17107
17403
|
const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
|
|
17108
17404
|
if (canDriveCodex && await applyPluginHeal("codex", surface, (m) => io.err(m), { force: true })) {
|
|
17109
17405
|
markPluginReloadRequired();
|
|
17406
|
+
markHealed();
|
|
17110
17407
|
io.err(` \u21BB restored Codex MMI plugin cache \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction("codex")} to load the new commands`);
|
|
17111
17408
|
codexActiveCacheCheck = buildCodexActiveCacheCheck({
|
|
17112
17409
|
isOrgRepo,
|
|
@@ -17135,6 +17432,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17135
17432
|
}
|
|
17136
17433
|
if (await applyPluginHeal("claude", surface, (m) => io.err(m))) {
|
|
17137
17434
|
markPluginReloadRequired();
|
|
17435
|
+
markHealed();
|
|
17138
17436
|
io.err(` \u21BB reinstalled MMI plugin after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
|
|
17139
17437
|
nestedPluginTreeCheck = buildNestedPluginTreeCheck({
|
|
17140
17438
|
isOrgRepo,
|
|
@@ -17177,6 +17475,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17177
17475
|
releasedVersion
|
|
17178
17476
|
});
|
|
17179
17477
|
if (cursorPluginCheck.ok) {
|
|
17478
|
+
markHealed();
|
|
17180
17479
|
io.err(` \u21BB seeded Cursor MMI plugin cache \u2192 ${releasedVersion ?? "latest"} \u2014 ${reloadAction(surface)}`);
|
|
17181
17480
|
}
|
|
17182
17481
|
}
|
|
@@ -17248,48 +17547,66 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17248
17547
|
} catch {
|
|
17249
17548
|
}
|
|
17250
17549
|
}
|
|
17550
|
+
emitDoctorReport(opts, io, {
|
|
17551
|
+
checks,
|
|
17552
|
+
surface,
|
|
17553
|
+
needsEagerHeal: healPlan.needsEagerHeal,
|
|
17554
|
+
healedCount,
|
|
17555
|
+
pluginReloadRequired,
|
|
17556
|
+
reloadHint,
|
|
17557
|
+
// Surface-aware update report (#865): the per-surface version snapshot + copy-paste update recipes, so
|
|
17558
|
+
// an agent told "make sure the CLI and plugin are up to date" can run the right command per surface and
|
|
17559
|
+
// echo back an unambiguous version line (CLI / Claude plugin / Codex marketplace / Codex active cache).
|
|
17560
|
+
// Passed lazily: emitDoctorReport calls it only past the --preflight/--banner early returns, keeping
|
|
17561
|
+
// the SessionStart hot path free of its synchronous fs I/O.
|
|
17562
|
+
buildUpdateReport: () => {
|
|
17563
|
+
const cacheRoots = mmiPluginCacheRootSnapshots();
|
|
17564
|
+
const cacheVersionsFor = (s) => cacheRoots.filter((r) => r.surface === s).flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
|
|
17565
|
+
const sourceVersions = (s) => installedPluginVersions(installedPluginSources().find((src) => src.surface === s)?.installed ?? null);
|
|
17566
|
+
return buildPluginUpdateReport({
|
|
17567
|
+
cliVersion: resolveClientVersion(),
|
|
17568
|
+
claudePluginVersions: sourceVersions("claude"),
|
|
17569
|
+
codexPluginVersions: sourceVersions("codex"),
|
|
17570
|
+
codexCacheVersions: cacheVersionsFor("codex"),
|
|
17571
|
+
opencodePluginVersions: opencodePluginVersionsForReport(),
|
|
17572
|
+
releasedVersion
|
|
17573
|
+
});
|
|
17574
|
+
}
|
|
17575
|
+
});
|
|
17576
|
+
}
|
|
17577
|
+
function emitDoctorReport(opts, io, ctx) {
|
|
17578
|
+
const { checks, surface, healedCount, pluginReloadRequired, reloadHint } = ctx;
|
|
17251
17579
|
const gaps = checks.filter((c) => !c.ok);
|
|
17252
17580
|
if (opts.preflight) {
|
|
17253
|
-
const outcome = preflightOutcome({ gaps, needsEagerHeal:
|
|
17581
|
+
const outcome = preflightOutcome({ gaps, needsEagerHeal: ctx.needsEagerHeal, surface });
|
|
17254
17582
|
if (outcome.line) io.err(outcome.line);
|
|
17255
17583
|
if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
|
|
17256
17584
|
return;
|
|
17257
17585
|
}
|
|
17258
17586
|
if (opts.banner) {
|
|
17259
|
-
if (
|
|
17587
|
+
if (ctx.needsEagerHeal) io.err(doctorPreflightDoneLine(surface));
|
|
17260
17588
|
if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
|
|
17261
17589
|
if (gaps.length) io.log(`\u26A0 MMI setup needed \u2014 ${gaps.map((g) => g.fix).join(" \xB7 ")} \xB7 guide: ${MMI_AGENTIC_ONBOARDING_GUIDE.url}`);
|
|
17262
17590
|
return;
|
|
17263
17591
|
}
|
|
17264
|
-
const
|
|
17265
|
-
const cacheVersionsFor = (s) => cacheRoots.filter((r) => r.surface === s).flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
|
|
17266
|
-
const sourceVersions = (s) => installedPluginVersions(installedPluginSources().find((src) => src.surface === s)?.installed ?? null);
|
|
17267
|
-
const updateReport = buildPluginUpdateReport({
|
|
17268
|
-
cliVersion: resolveClientVersion(),
|
|
17269
|
-
claudePluginVersions: sourceVersions("claude"),
|
|
17270
|
-
codexPluginVersions: sourceVersions("codex"),
|
|
17271
|
-
codexCacheVersions: cacheVersionsFor("codex"),
|
|
17272
|
-
opencodePluginVersions: opencodePluginVersionsForReport(),
|
|
17273
|
-
releasedVersion
|
|
17274
|
-
});
|
|
17592
|
+
const updateReport = ctx.buildUpdateReport();
|
|
17275
17593
|
const resources = doctorResourcesForGaps(gaps);
|
|
17594
|
+
process.exitCode = doctorExitCode(checks);
|
|
17276
17595
|
if (opts.json) {
|
|
17277
17596
|
io.log(JSON.stringify(buildDoctorJsonPayload({ checks, updateReport, resources }), null, 2));
|
|
17278
17597
|
return;
|
|
17279
17598
|
}
|
|
17280
|
-
if (
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
17285
|
-
|
|
17286
|
-
|
|
17287
|
-
|
|
17599
|
+
if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
|
|
17600
|
+
for (const line of doctorHumanLines({
|
|
17601
|
+
checks,
|
|
17602
|
+
updateReport,
|
|
17603
|
+
shouldApply: Boolean(opts.apply),
|
|
17604
|
+
healedCount,
|
|
17605
|
+
pluginReloadRequired,
|
|
17606
|
+
reloadHint,
|
|
17607
|
+
verbose: Boolean(opts.verbose)
|
|
17608
|
+
})) io.log(line);
|
|
17288
17609
|
for (const r of resources) io.log(`Resource: ${r.label} \u2014 ${r.url}`);
|
|
17289
|
-
io.log("");
|
|
17290
|
-
for (const line of renderPluginUpdateReport(updateReport)) io.log(line);
|
|
17291
|
-
io.log(gaps.length ? `
|
|
17292
|
-
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
17293
17610
|
}
|
|
17294
17611
|
var USER_SCOPE_GUARD_MARKER = "mmi-guard:v1";
|
|
17295
17612
|
var USER_SCOPE_GUARD_COMMAND = `mmi-cli guard --session-start || true # ${USER_SCOPE_GUARD_MARKER}`;
|
|
@@ -18874,6 +19191,21 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
18874
19191
|
pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
|
|
18875
19192
|
sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms))
|
|
18876
19193
|
}),
|
|
19194
|
+
// #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
|
|
19195
|
+
// fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
|
|
19196
|
+
probeMergeReady: async (prNumber, repo) => {
|
|
19197
|
+
const args = repo ? ["--repo", repo] : [];
|
|
19198
|
+
const [state, checks] = await Promise.all([
|
|
19199
|
+
execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state,mergeable", "--jq", '.state + " " + .mergeable'], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => r.stdout.trim().split(/\s+/)).catch(() => ["", ""]),
|
|
19200
|
+
pollGhPrChecks(prNumber, args).catch(() => "error")
|
|
19201
|
+
]);
|
|
19202
|
+
const [prState, mergeable] = state;
|
|
19203
|
+
return {
|
|
19204
|
+
open: prState === "OPEN",
|
|
19205
|
+
mergeable: mergeable === "MERGEABLE",
|
|
19206
|
+
checksPassing: checks === "success" || checks === "no-checks-reported"
|
|
19207
|
+
};
|
|
19208
|
+
},
|
|
18877
19209
|
mergeAuto: async (prNumber, repo) => {
|
|
18878
19210
|
const args = repo ? ["--repo", repo] : [];
|
|
18879
19211
|
const readMergeState = () => readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|