@mutmutco/cli 3.0.0 → 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 +424 -86
- 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
|
}
|
|
@@ -9684,6 +9696,12 @@ async function foldReleaseVersion(deps, model, tag, foldPaths) {
|
|
|
9684
9696
|
for (const path2 of foldPaths) {
|
|
9685
9697
|
await deps.run("git", ["add", "--", path2]).catch(() => void 0);
|
|
9686
9698
|
}
|
|
9699
|
+
const strayVersionEdits = (await deps.run("git", ["diff", "--name-only"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
9700
|
+
if (strayVersionEdits.length > 0) {
|
|
9701
|
+
throw new Error(
|
|
9702
|
+
`version fold left tracked changes outside the fold set uncommitted: ${strayVersionEdits.join(", ")}. The repo's \`version\` script modified paths the train does not stage (fold set: ${foldPaths.join(", ") || "(none)"}). Add them to that script's git-add list (or the fold set) so the release commit is complete \u2014 shipping now would tag a main whose version-lockstep check is red.`
|
|
9703
|
+
);
|
|
9704
|
+
}
|
|
9687
9705
|
const staged = (await deps.run("git", ["diff", "--cached", "--name-only"])).trim();
|
|
9688
9706
|
if (!staged) return `version fold: manifests already at ${version} \u2014 nothing committed`;
|
|
9689
9707
|
await deps.run("git", ["commit", "-m", `chore(release): bump distribution to ${tag}`]);
|
|
@@ -9937,9 +9955,12 @@ var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
|
|
|
9937
9955
|
var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
|
|
9938
9956
|
var TRAIN_CHECK_ATTEMPTS = 40;
|
|
9939
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;
|
|
9940
9960
|
async function correlateRun(deps, args) {
|
|
9941
9961
|
const sleep = resolveSleep(deps);
|
|
9942
9962
|
const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
|
|
9963
|
+
const repo = args.mode === "workflow" ? args.repo ?? HUB_REPO3 : HUB_REPO3;
|
|
9943
9964
|
let lastError;
|
|
9944
9965
|
let parsedAnyResponse = false;
|
|
9945
9966
|
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
@@ -9948,7 +9969,7 @@ async function correlateRun(deps, args) {
|
|
|
9948
9969
|
"run",
|
|
9949
9970
|
"list",
|
|
9950
9971
|
"--repo",
|
|
9951
|
-
|
|
9972
|
+
repo,
|
|
9952
9973
|
"--workflow",
|
|
9953
9974
|
args.workflow,
|
|
9954
9975
|
...args.mode === "workflow" ? ["--event", args.event] : [],
|
|
@@ -9991,10 +10012,10 @@ function correlateControlRun(deps, since, titleIncludes) {
|
|
|
9991
10012
|
async function correlateWorkflowRun(deps, args) {
|
|
9992
10013
|
return correlateRun(deps, { ...args, mode: "workflow" });
|
|
9993
10014
|
}
|
|
9994
|
-
async function watchTenantRun(deps, runId) {
|
|
10015
|
+
async function watchTenantRun(deps, runId, repo = HUB_REPO3) {
|
|
9995
10016
|
if (runId == null) return "pending";
|
|
9996
10017
|
try {
|
|
9997
|
-
await deps.run("gh", ["run", "watch", String(runId), "--repo",
|
|
10018
|
+
await deps.run("gh", ["run", "watch", String(runId), "--repo", repo, "--exit-status"]);
|
|
9998
10019
|
return "success";
|
|
9999
10020
|
} catch {
|
|
10000
10021
|
return "failure";
|
|
@@ -10007,9 +10028,9 @@ async function fetchControlRunLog(deps, runId) {
|
|
|
10007
10028
|
return "";
|
|
10008
10029
|
}
|
|
10009
10030
|
}
|
|
10010
|
-
async function watchWorkflowRun(deps, workflow, run) {
|
|
10031
|
+
async function watchWorkflowRun(deps, workflow, run, repo = HUB_REPO3) {
|
|
10011
10032
|
if (run.runId == null) return { workflow, conclusion: "pending" };
|
|
10012
|
-
const conclusion = await watchTenantRun(deps, run.runId);
|
|
10033
|
+
const conclusion = await watchTenantRun(deps, run.runId, repo);
|
|
10013
10034
|
return { workflow, runId: run.runId, runUrl: run.runUrl, conclusion };
|
|
10014
10035
|
}
|
|
10015
10036
|
function aggregateWorkflowRuns(runs) {
|
|
@@ -10068,7 +10089,7 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
10068
10089
|
status: "pr-pending",
|
|
10069
10090
|
prNumber: existing.number,
|
|
10070
10091
|
prUrl: existing.url,
|
|
10071
|
-
note: `alignment PR already open: ${existing.url} \u2014 land it with \`
|
|
10092
|
+
note: `alignment PR already open: ${existing.url} \u2014 land it with \`mmi-cli pr merge ${existing.number} --auto --merge\``
|
|
10072
10093
|
};
|
|
10073
10094
|
}
|
|
10074
10095
|
const ahead = clean(await deps.run("git", ["rev-list", "--count", "origin/development..main"]));
|
|
@@ -10077,14 +10098,14 @@ async function rollDevelopmentForward(deps, ctx, tag) {
|
|
|
10077
10098
|
}
|
|
10078
10099
|
const body = `Carries the ${tag} release (including the version fold) from \`main\` back to \`development\`.
|
|
10079
10100
|
|
|
10080
|
-
\`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge**
|
|
10101
|
+
\`development\` requires status checks, so the release train opens this alignment PR instead of a direct push of the un-checked merge commit (#1143). Land it with a **true merge** \u2014 \`mmi-cli pr merge <n> --auto --merge\` (not squash) so the merge parentage survives and the misalignment guard stays satisfied. \`--auto\` waits out the checks this PR triggers, which otherwise block an immediate merge right after the release.`;
|
|
10081
10102
|
const url = clean(await deps.run("gh", ["pr", "create", "--repo", ctx.repo, "--base", "development", "--head", "main", "--title", `chore(release): align development to ${tag}`, "--body", body]));
|
|
10082
10103
|
const number = parsePrNumber(url);
|
|
10083
10104
|
return {
|
|
10084
10105
|
status: "pr-pending",
|
|
10085
10106
|
prNumber: number,
|
|
10086
10107
|
prUrl: url || void 0,
|
|
10087
|
-
note: `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"} \u2014 land it with \`
|
|
10108
|
+
note: `development requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"} \u2014 land it with \`mmi-cli pr merge ${number ?? "<number>"} --auto --merge\``
|
|
10088
10109
|
};
|
|
10089
10110
|
}
|
|
10090
10111
|
function resolveContextState(context, checkRuns, statuses) {
|
|
@@ -10118,6 +10139,7 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
10118
10139
|
let lastStatus = "not checked";
|
|
10119
10140
|
let lastError;
|
|
10120
10141
|
const everObserved = /* @__PURE__ */ new Set();
|
|
10142
|
+
const autoSatisfied = /* @__PURE__ */ new Set();
|
|
10121
10143
|
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
|
|
10122
10144
|
if (attempt > 0) await sleep(TRAIN_CHECK_DELAY_MS);
|
|
10123
10145
|
let checkRuns;
|
|
@@ -10139,18 +10161,25 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
10139
10161
|
for (const c of required) {
|
|
10140
10162
|
if (checkRuns.some((r) => r.name === c) || statuses.some((s) => s.context === c)) everObserved.add(c);
|
|
10141
10163
|
}
|
|
10142
|
-
|
|
10143
|
-
|
|
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}`;
|
|
10144
10173
|
const failed = states.filter(([, s]) => s === "failed").map(([c]) => c);
|
|
10145
10174
|
if (failed.length > 0) {
|
|
10146
10175
|
throw new Error(`required train check failed: ${failed.join(", ")} (${lastStatus})`);
|
|
10147
10176
|
}
|
|
10148
10177
|
if (states.every(([, s]) => s === "success")) {
|
|
10149
|
-
return `required checks passed: ${
|
|
10178
|
+
return `required checks passed: ${pending.join(", ") || "(none pending)"}${satisfiedNote}`;
|
|
10150
10179
|
}
|
|
10151
10180
|
}
|
|
10152
10181
|
const waitedMin = Math.round((TRAIN_CHECK_ATTEMPTS - 1) * TRAIN_CHECK_DELAY_MS / 6e4);
|
|
10153
|
-
const neverMaterialized = required.filter((c) => !everObserved.has(c));
|
|
10182
|
+
const neverMaterialized = required.filter((c) => !everObserved.has(c) && !autoSatisfied.has(c));
|
|
10154
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(", ")}.` : "";
|
|
10155
10184
|
throw new Error(
|
|
10156
10185
|
`timed out after ~${waitedMin}m (${TRAIN_CHECK_ATTEMPTS} attempts) waiting for required train checks on ${sha}.${neverNote} Last observed: ${lastError ? `error: ${lastError}` : lastStatus}`
|
|
@@ -10240,6 +10269,19 @@ function tenantPublishRecoveryCommand(slug, repo, ref, stage2, publishDir) {
|
|
|
10240
10269
|
if (publishDir && publishDir !== ".") parts.push(`-f publishDir=${publishDir}`);
|
|
10241
10270
|
return parts.join(" ");
|
|
10242
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
|
+
}
|
|
10243
10285
|
async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure = "throw", publishDir) {
|
|
10244
10286
|
const since = (deps.now ?? Date.now)();
|
|
10245
10287
|
const dispatchArgs = [
|
|
@@ -10270,8 +10312,16 @@ async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFail
|
|
|
10270
10312
|
};
|
|
10271
10313
|
}
|
|
10272
10314
|
const { runId, runUrl } = await correlatePublishRun(deps, since, [ctx.slug, stage2]);
|
|
10273
|
-
|
|
10274
|
-
|
|
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 };
|
|
10275
10325
|
}
|
|
10276
10326
|
async function dispatchPublishIfRequired(deps, ctx, meta, model, stage2, publishRef, watch, dispatchFailure) {
|
|
10277
10327
|
if (!meta.publishRequired || stage2 !== "main") return null;
|
|
@@ -10288,6 +10338,18 @@ function appendPublishDispatch(deploy, publish) {
|
|
|
10288
10338
|
deployStatus: deploy.deployStatus === "failure" || publish.deployStatus === "failure" ? "failure" : deploy.deployStatus === "pending" || publish.deployStatus === "pending" ? "pending" : "success"
|
|
10289
10339
|
};
|
|
10290
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
|
+
}
|
|
10291
10353
|
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
|
|
10292
10354
|
if (model === "tenant-container" || model === "solo-container") {
|
|
10293
10355
|
const since = (deps.now ?? Date.now)();
|
|
@@ -10306,7 +10368,18 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
10306
10368
|
return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
10307
10369
|
}
|
|
10308
10370
|
if (model === "registry-publish") {
|
|
10309
|
-
|
|
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) };
|
|
10310
10383
|
}
|
|
10311
10384
|
if (model === "hub-serverless") {
|
|
10312
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)";
|
|
@@ -10317,15 +10390,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
10317
10390
|
{ workflow: "deploy.yml", event: "release" },
|
|
10318
10391
|
{ workflow: "publish.yml", event: "release" }
|
|
10319
10392
|
];
|
|
10320
|
-
const workflowRuns =
|
|
10321
|
-
for (const target of targets) {
|
|
10322
|
-
try {
|
|
10323
|
-
const run = await correlateWorkflowRun(deps, { ...target, since, headSha: autoRunHeadSha });
|
|
10324
|
-
workflowRuns.push(await watchWorkflowRun(deps, target.workflow, run));
|
|
10325
|
-
} catch {
|
|
10326
|
-
workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
|
|
10327
|
-
}
|
|
10328
|
-
}
|
|
10393
|
+
const workflowRuns = await watchOwnWorkflowRuns(deps, HUB_REPO3, targets, since, autoRunHeadSha);
|
|
10329
10394
|
const primary = workflowRuns[0];
|
|
10330
10395
|
return {
|
|
10331
10396
|
note,
|
|
@@ -11159,7 +11224,11 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11159
11224
|
runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
|
|
11160
11225
|
}
|
|
11161
11226
|
deployNote = "watched release-triggered deploy.yml + publish.yml";
|
|
11162
|
-
} 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") {
|
|
11163
11232
|
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
11164
11233
|
const deploy = await dispatchDeploy(
|
|
11165
11234
|
deps,
|
|
@@ -11175,7 +11244,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11175
11244
|
);
|
|
11176
11245
|
const publish = deploy.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report") : null;
|
|
11177
11246
|
let dispatch = appendPublishDispatch(deploy, publish);
|
|
11178
|
-
if (!publish && deploy.deployStatus !== "success" && meta.publishRequired
|
|
11247
|
+
if (!publish && deploy.deployStatus !== "success" && meta.publishRequired) {
|
|
11179
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";
|
|
11180
11249
|
dispatch = {
|
|
11181
11250
|
...dispatch,
|
|
@@ -11183,25 +11252,17 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11183
11252
|
};
|
|
11184
11253
|
}
|
|
11185
11254
|
deployNote = dispatch.note;
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
});
|
|
11192
|
-
}
|
|
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
|
+
});
|
|
11193
11260
|
if (publish?.runUrl) {
|
|
11194
11261
|
runs.push({
|
|
11195
11262
|
workflow: "tenant-publish.yml",
|
|
11196
11263
|
url: publish.runUrl,
|
|
11197
11264
|
conclusion: publish.deployStatus === "success" ? "success" : publish.deployStatus === "failure" ? "failure" : publish.deployStatus ?? "pending"
|
|
11198
11265
|
});
|
|
11199
|
-
} else if (deployModel === "registry-publish") {
|
|
11200
|
-
runs.push({
|
|
11201
|
-
workflow: "tenant-publish.yml",
|
|
11202
|
-
url: deploy.runUrl,
|
|
11203
|
-
conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
|
|
11204
|
-
});
|
|
11205
11266
|
}
|
|
11206
11267
|
} else {
|
|
11207
11268
|
deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
|
|
@@ -15323,10 +15384,12 @@ function isSemverVersion2(v) {
|
|
|
15323
15384
|
function staleRecordCommand(surface) {
|
|
15324
15385
|
return surface === "codex" ? CODEX_PLUGIN_RECOVERY : CLAUDE_PLUGIN_RECOVERY;
|
|
15325
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";
|
|
15326
15388
|
function staleSurfacesFix(stale, releasedVersion) {
|
|
15327
15389
|
const parts = stale.map((s) => {
|
|
15328
15390
|
const at = s.recordPath ? ` (${s.recordPath})` : "";
|
|
15329
|
-
|
|
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}`;
|
|
15330
15393
|
});
|
|
15331
15394
|
return `stale installed-plugin record on ${stale.map((s) => s.surface).join(" + ")}: ${parts.join("; ")}`;
|
|
15332
15395
|
}
|
|
@@ -15368,18 +15431,71 @@ function buildInstalledPluginVersionCheck(input) {
|
|
|
15368
15431
|
staleSurfaces: stale
|
|
15369
15432
|
};
|
|
15370
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
|
+
}
|
|
15371
15487
|
var OPENCODE_VERSION_LABEL = "installed OpenCode MMI adapter version (vs latest release)";
|
|
15372
15488
|
function buildOpencodeVersionCheck(input) {
|
|
15373
15489
|
const fix = pluginRecoveryFix("opencode");
|
|
15374
15490
|
const base = { ok: true, label: OPENCODE_VERSION_LABEL, fix };
|
|
15375
15491
|
if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
|
|
15376
15492
|
if (!isSemverVersion2(input.installedVersion)) {
|
|
15377
|
-
return { ...base, ok: false, releasedVersion: input.releasedVersion };
|
|
15493
|
+
return { ...base, ok: false, severityOverride: "hard", releasedVersion: input.releasedVersion };
|
|
15378
15494
|
}
|
|
15379
15495
|
if (compareVersions(input.installedVersion, input.releasedVersion) >= 0) {
|
|
15380
15496
|
return { ...base, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15381
15497
|
}
|
|
15382
|
-
return { ...base, ok: false, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15498
|
+
return { ...base, ok: false, severityOverride: "hard", installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15383
15499
|
}
|
|
15384
15500
|
function pickOpencodeActiveVersion(input) {
|
|
15385
15501
|
for (const value of [input.envStamp, input.cacheVersion, input.diskVersion]) {
|
|
@@ -15628,9 +15744,10 @@ function buildCursorPluginInstallCheck(input) {
|
|
|
15628
15744
|
return {
|
|
15629
15745
|
...base,
|
|
15630
15746
|
ok: false,
|
|
15747
|
+
severityOverride: "advisory",
|
|
15631
15748
|
cacheRoot: input.cacheRoot,
|
|
15632
15749
|
pins: input.pins,
|
|
15633
|
-
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}`
|
|
15634
15751
|
};
|
|
15635
15752
|
}
|
|
15636
15753
|
return { ...base, cacheRoot: input.cacheRoot, pins: input.pins };
|
|
@@ -15824,6 +15941,30 @@ function preflightOutcome(input) {
|
|
|
15824
15941
|
function pluginAutonomousHaltLine(reloadHint) {
|
|
15825
15942
|
return `\u26A0 PLUGIN RELOAD REQUIRED \u2014 mmi:* skills and agent types are unavailable until you ${reloadHint}. Halt autonomous /grind and /build until then.`;
|
|
15826
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
|
+
}
|
|
15827
15968
|
function renderPluginUpdateReportStaleOnly(report) {
|
|
15828
15969
|
const v = report.versions;
|
|
15829
15970
|
const released = v.released;
|
|
@@ -15838,24 +15979,97 @@ function renderPluginUpdateReportStaleOnly(report) {
|
|
|
15838
15979
|
return ["Update commands (stale surfaces):", ...blocks];
|
|
15839
15980
|
}
|
|
15840
15981
|
var DOCTOR_VERBOSE_HINT = "Run mmi-cli doctor --verbose for the full audit checklist + version report.";
|
|
15841
|
-
function
|
|
15842
|
-
|
|
15843
|
-
|
|
15844
|
-
|
|
15845
|
-
|
|
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.";
|
|
15846
16000
|
}
|
|
15847
|
-
const
|
|
15848
|
-
|
|
15849
|
-
|
|
15850
|
-
|
|
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.`);
|
|
15851
16024
|
}
|
|
15852
16025
|
const stale = renderPluginUpdateReportStaleOnly(input.updateReport);
|
|
15853
16026
|
if (stale.length) {
|
|
15854
|
-
lines.push("");
|
|
15855
|
-
|
|
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
|
+
}
|
|
15856
16072
|
}
|
|
15857
|
-
lines.push("");
|
|
15858
|
-
lines.push(`\u26A0 ${input.gaps.length} item(s) need attention \u2014 ${DOCTOR_VERBOSE_HINT}`);
|
|
15859
16073
|
return lines;
|
|
15860
16074
|
}
|
|
15861
16075
|
var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
|
|
@@ -16240,6 +16454,77 @@ function installedPluginSources() {
|
|
|
16240
16454
|
}
|
|
16241
16455
|
});
|
|
16242
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
|
+
}
|
|
16243
16528
|
function readClaudeSettings() {
|
|
16244
16529
|
try {
|
|
16245
16530
|
return JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path17.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
@@ -16698,7 +16983,6 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16698
16983
|
const repairLocal = !opts.json || Boolean(opts.apply) || Boolean(opts.preflight);
|
|
16699
16984
|
const repoWritesAllowed = !opts.noRepoWrites;
|
|
16700
16985
|
const runExtended = Boolean(opts.verbose) || Boolean(opts.json);
|
|
16701
|
-
const terseOutput = !opts.verbose && !opts.json && !opts.banner && !opts.preflight;
|
|
16702
16986
|
const checks = [];
|
|
16703
16987
|
const REWRITE_KEY = "url.https://github.com/.insteadOf";
|
|
16704
16988
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
@@ -16768,6 +17052,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16768
17052
|
const markPluginReloadRequired = () => {
|
|
16769
17053
|
pluginReloadRequired = true;
|
|
16770
17054
|
};
|
|
17055
|
+
let healedCount = 0;
|
|
17056
|
+
const markHealed = () => {
|
|
17057
|
+
healedCount += 1;
|
|
17058
|
+
};
|
|
16771
17059
|
let versionReport = buildVersionLagReport({
|
|
16772
17060
|
currentVersion: resolveClientVersion(),
|
|
16773
17061
|
repoVersion: readRepoVersion(),
|
|
@@ -16820,6 +17108,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16820
17108
|
try {
|
|
16821
17109
|
await execFileP2("git", ["config", "--global", "--add", REWRITE_KEY, "git@github.com:"]);
|
|
16822
17110
|
cloneOk = true;
|
|
17111
|
+
markHealed();
|
|
16823
17112
|
io.err(" \u21BB repaired: git insteadOf git@github.com \u2192 https (plugin clone over HTTPS)");
|
|
16824
17113
|
} catch {
|
|
16825
17114
|
}
|
|
@@ -16838,6 +17127,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16838
17127
|
if (!pluginCheck.ok && pluginCheck.recordToInsert && repairLocal) {
|
|
16839
17128
|
if (writeProjectInstallRecord(pluginCheck.recordToInsert)) {
|
|
16840
17129
|
pluginCheck = { ...pluginCheck, ok: true };
|
|
17130
|
+
markHealed();
|
|
16841
17131
|
io.err(` \u21BB repaired: registered mmi@mutmutco project install record \u2014 ${reloadHint} to load MMI commands`);
|
|
16842
17132
|
}
|
|
16843
17133
|
}
|
|
@@ -16857,6 +17147,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16857
17147
|
surface
|
|
16858
17148
|
});
|
|
16859
17149
|
if (legacyPluginCheck.ok) {
|
|
17150
|
+
markHealed();
|
|
16860
17151
|
io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via claude plugin \u2014 ${reloadHint} to load MMI commands`);
|
|
16861
17152
|
}
|
|
16862
17153
|
}
|
|
@@ -16867,6 +17158,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16867
17158
|
surface
|
|
16868
17159
|
});
|
|
16869
17160
|
if (legacyPluginCheck.ok) {
|
|
17161
|
+
markHealed();
|
|
16870
17162
|
io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via codex plugin \u2014 ${reloadHint} to load MMI commands`);
|
|
16871
17163
|
}
|
|
16872
17164
|
}
|
|
@@ -16885,6 +17177,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16885
17177
|
gitignoreCheck.removed?.length ? `removed ${gitignoreCheck.removed.join(", ")}` : ""
|
|
16886
17178
|
].filter(Boolean).join("; ") || "normalized the block";
|
|
16887
17179
|
gitignoreCheck = { ...gitignoreCheck, ok: true };
|
|
17180
|
+
markHealed();
|
|
16888
17181
|
io.err(` \u21BB repaired: org-managed .gitignore block \u2014 ${drift}`);
|
|
16889
17182
|
io.err(" this is an org-managed update (not unrelated churn) \u2014 stage & commit .gitignore so it stops recurring");
|
|
16890
17183
|
}
|
|
@@ -16898,6 +17191,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16898
17191
|
if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
|
|
16899
17192
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
16900
17193
|
driftCheck = { ...driftCheck, ok: true };
|
|
17194
|
+
markHealed();
|
|
16901
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`);
|
|
16902
17196
|
}
|
|
16903
17197
|
}
|
|
@@ -16930,6 +17224,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16930
17224
|
installedVersionCheck = healed;
|
|
16931
17225
|
if (healed.ok) {
|
|
16932
17226
|
markPluginReloadRequired();
|
|
17227
|
+
markHealed();
|
|
16933
17228
|
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via claude plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
16934
17229
|
}
|
|
16935
17230
|
}
|
|
@@ -16939,11 +17234,13 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16939
17234
|
installedVersionCheck = healed;
|
|
16940
17235
|
if (healed.ok) {
|
|
16941
17236
|
markPluginReloadRequired();
|
|
17237
|
+
markHealed();
|
|
16942
17238
|
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
16943
17239
|
}
|
|
16944
17240
|
}
|
|
16945
17241
|
}
|
|
16946
17242
|
checks.push(installedVersionCheck);
|
|
17243
|
+
checks.push(buildManagedPluginDriftCheck({ isOrgRepo, sources: managedPluginSurfaceSources() }));
|
|
16947
17244
|
let openCodeConfigSnapshot = opencodeConfigSnapshot();
|
|
16948
17245
|
const inspectOpenCode = surface === "opencode" || openCodeConfigSnapshot.hasConfig || runExtended;
|
|
16949
17246
|
if (inspectOpenCode) {
|
|
@@ -16968,6 +17265,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16968
17265
|
});
|
|
16969
17266
|
if (opencodeConfigCheck.ok) {
|
|
16970
17267
|
markPluginReloadRequired();
|
|
17268
|
+
markHealed();
|
|
16971
17269
|
io.err(` \u21BB repaired: wired ${OPENCODE_PLUGIN_PACKAGE} in OpenCode config \u2014 ${reloadAction("opencode")} to load MMI commands`);
|
|
16972
17270
|
}
|
|
16973
17271
|
}
|
|
@@ -16991,6 +17289,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16991
17289
|
});
|
|
16992
17290
|
if (opencodeVersionCheck.ok) {
|
|
16993
17291
|
markPluginReloadRequired();
|
|
17292
|
+
markHealed();
|
|
16994
17293
|
io.err(` \u21BB force-refreshed OpenCode MMI plugin \u2192 ${opencodeInstalledVersion ?? releasedVersion ?? "latest"} \u2014 ${reloadAction("opencode")} to load it`);
|
|
16995
17294
|
}
|
|
16996
17295
|
}
|
|
@@ -17029,6 +17328,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17029
17328
|
});
|
|
17030
17329
|
if (surfaceAssetsCheck.ok) {
|
|
17031
17330
|
markPluginReloadRequired();
|
|
17331
|
+
markHealed();
|
|
17032
17332
|
io.err(` \u21BB materialized OpenCode MMI commands + skills path \u2014 ${reloadAction("opencode")} to load them`);
|
|
17033
17333
|
}
|
|
17034
17334
|
}
|
|
@@ -17050,6 +17350,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17050
17350
|
if (!legacyOpenCodeCheck.ok && repairLocal && legacyOpenCodeConfig.legacyPath) {
|
|
17051
17351
|
if (quarantineOpencodeLegacyConfig(legacyOpenCodeConfig.legacyPath)) {
|
|
17052
17352
|
legacyOpenCodeCheck = buildOpencodeLegacyConfigCheck({ isOrgRepo: true });
|
|
17353
|
+
markHealed();
|
|
17053
17354
|
io.err(` \u21BB quarantined legacy OpenCode config \u2192 ${legacyOpenCodeConfig.legacyPath}.bak \u2014 restart OpenCode`);
|
|
17054
17355
|
}
|
|
17055
17356
|
}
|
|
@@ -17074,6 +17375,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17074
17375
|
const surfaces = [...new Set(cacheCleanupCheck.leftovers?.map((entry) => entry.surface) ?? [])].join("/");
|
|
17075
17376
|
const names = cacheCleanupCheck.leftovers?.map((entry) => entry.name).join(", ");
|
|
17076
17377
|
markPluginReloadRequired();
|
|
17378
|
+
markHealed();
|
|
17077
17379
|
io.err(` \u21BB quarantined ${moved} stale MMI plugin cache dir(s) for ${surfaces || "agent surfaces"}: ${names} \u2014 ${reloadHint} to load MMI commands`);
|
|
17078
17380
|
}
|
|
17079
17381
|
cacheCleanupCheck = {
|
|
@@ -17101,6 +17403,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17101
17403
|
const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
|
|
17102
17404
|
if (canDriveCodex && await applyPluginHeal("codex", surface, (m) => io.err(m), { force: true })) {
|
|
17103
17405
|
markPluginReloadRequired();
|
|
17406
|
+
markHealed();
|
|
17104
17407
|
io.err(` \u21BB restored Codex MMI plugin cache \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction("codex")} to load the new commands`);
|
|
17105
17408
|
codexActiveCacheCheck = buildCodexActiveCacheCheck({
|
|
17106
17409
|
isOrgRepo,
|
|
@@ -17129,6 +17432,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17129
17432
|
}
|
|
17130
17433
|
if (await applyPluginHeal("claude", surface, (m) => io.err(m))) {
|
|
17131
17434
|
markPluginReloadRequired();
|
|
17435
|
+
markHealed();
|
|
17132
17436
|
io.err(` \u21BB reinstalled MMI plugin after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
|
|
17133
17437
|
nestedPluginTreeCheck = buildNestedPluginTreeCheck({
|
|
17134
17438
|
isOrgRepo,
|
|
@@ -17171,6 +17475,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17171
17475
|
releasedVersion
|
|
17172
17476
|
});
|
|
17173
17477
|
if (cursorPluginCheck.ok) {
|
|
17478
|
+
markHealed();
|
|
17174
17479
|
io.err(` \u21BB seeded Cursor MMI plugin cache \u2192 ${releasedVersion ?? "latest"} \u2014 ${reloadAction(surface)}`);
|
|
17175
17480
|
}
|
|
17176
17481
|
}
|
|
@@ -17242,48 +17547,66 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17242
17547
|
} catch {
|
|
17243
17548
|
}
|
|
17244
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;
|
|
17245
17579
|
const gaps = checks.filter((c) => !c.ok);
|
|
17246
17580
|
if (opts.preflight) {
|
|
17247
|
-
const outcome = preflightOutcome({ gaps, needsEagerHeal:
|
|
17581
|
+
const outcome = preflightOutcome({ gaps, needsEagerHeal: ctx.needsEagerHeal, surface });
|
|
17248
17582
|
if (outcome.line) io.err(outcome.line);
|
|
17249
17583
|
if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
|
|
17250
17584
|
return;
|
|
17251
17585
|
}
|
|
17252
17586
|
if (opts.banner) {
|
|
17253
|
-
if (
|
|
17587
|
+
if (ctx.needsEagerHeal) io.err(doctorPreflightDoneLine(surface));
|
|
17254
17588
|
if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
|
|
17255
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}`);
|
|
17256
17590
|
return;
|
|
17257
17591
|
}
|
|
17258
|
-
const
|
|
17259
|
-
const cacheVersionsFor = (s) => cacheRoots.filter((r) => r.surface === s).flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
|
|
17260
|
-
const sourceVersions = (s) => installedPluginVersions(installedPluginSources().find((src) => src.surface === s)?.installed ?? null);
|
|
17261
|
-
const updateReport = buildPluginUpdateReport({
|
|
17262
|
-
cliVersion: resolveClientVersion(),
|
|
17263
|
-
claudePluginVersions: sourceVersions("claude"),
|
|
17264
|
-
codexPluginVersions: sourceVersions("codex"),
|
|
17265
|
-
codexCacheVersions: cacheVersionsFor("codex"),
|
|
17266
|
-
opencodePluginVersions: opencodePluginVersionsForReport(),
|
|
17267
|
-
releasedVersion
|
|
17268
|
-
});
|
|
17592
|
+
const updateReport = ctx.buildUpdateReport();
|
|
17269
17593
|
const resources = doctorResourcesForGaps(gaps);
|
|
17594
|
+
process.exitCode = doctorExitCode(checks);
|
|
17270
17595
|
if (opts.json) {
|
|
17271
17596
|
io.log(JSON.stringify(buildDoctorJsonPayload({ checks, updateReport, resources }), null, 2));
|
|
17272
17597
|
return;
|
|
17273
17598
|
}
|
|
17274
|
-
if (
|
|
17275
|
-
|
|
17276
|
-
|
|
17277
|
-
|
|
17278
|
-
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
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);
|
|
17282
17609
|
for (const r of resources) io.log(`Resource: ${r.label} \u2014 ${r.url}`);
|
|
17283
|
-
io.log("");
|
|
17284
|
-
for (const line of renderPluginUpdateReport(updateReport)) io.log(line);
|
|
17285
|
-
io.log(gaps.length ? `
|
|
17286
|
-
${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
|
|
17287
17610
|
}
|
|
17288
17611
|
var USER_SCOPE_GUARD_MARKER = "mmi-guard:v1";
|
|
17289
17612
|
var USER_SCOPE_GUARD_COMMAND = `mmi-cli guard --session-start || true # ${USER_SCOPE_GUARD_MARKER}`;
|
|
@@ -18868,6 +19191,21 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
18868
19191
|
pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
|
|
18869
19192
|
sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms))
|
|
18870
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
|
+
},
|
|
18871
19209
|
mergeAuto: async (prNumber, repo) => {
|
|
18872
19210
|
const args = repo ? ["--repo", repo] : [];
|
|
18873
19211
|
const readMergeState = () => readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
|
|
@@ -19625,7 +19963,7 @@ function renderTrainApply(commandName, r) {
|
|
|
19625
19963
|
if (r.rcRetirement) base = `${base}; rc retirement: ${r.rcRetirement.toUpperCase()} (${r.rcRetirementNote ?? ""})`;
|
|
19626
19964
|
if (r.devRollForward) {
|
|
19627
19965
|
const f = r.devRollForward;
|
|
19628
|
-
base = f.status === "pr-pending" ? `${base}; dev roll-forward: ALIGNMENT PR PENDING \u2014 land it with \`
|
|
19966
|
+
base = f.status === "pr-pending" ? `${base}; dev roll-forward: ALIGNMENT PR PENDING \u2014 land it with \`mmi-cli pr merge ${f.prNumber ?? "<number>"} --auto --merge\`${f.prUrl ? ` (${f.prUrl})` : ""}` : `${base}; dev roll-forward: ${f.note}`;
|
|
19629
19967
|
}
|
|
19630
19968
|
if (r.checkout) {
|
|
19631
19969
|
base = `${base}; checkout: ${r.checkout.note}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
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",
|