@mutmutco/cli 3.0.1 → 3.2.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 +1167 -200
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -4789,7 +4789,7 @@ mutation($projectId: ID!, $itemId: ID!) {
|
|
|
4789
4789
|
async function updateItemSingleSelect(client, projectId, itemId, fieldId, optionId) {
|
|
4790
4790
|
await client.graphql(UPDATE_ITEM_FIELD_MUTATION, { projectId, itemId, fieldId, optionId });
|
|
4791
4791
|
}
|
|
4792
|
-
function
|
|
4792
|
+
function missingBoardConfigFields(cfg) {
|
|
4793
4793
|
const missing = [];
|
|
4794
4794
|
if (!cfg.projectOwner) missing.push("projectOwner");
|
|
4795
4795
|
if (!cfg.projectNumber) missing.push("projectNumber");
|
|
@@ -4799,9 +4799,20 @@ function resolveBoardConfig(cfg) {
|
|
|
4799
4799
|
for (const status of BOARD_STATUSES) {
|
|
4800
4800
|
if (!cfg.statusOptions?.[status]) missing.push(`statusOptions.${status}`);
|
|
4801
4801
|
}
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4802
|
+
return missing;
|
|
4803
|
+
}
|
|
4804
|
+
function diagnoseBoardConfigGap(cfg) {
|
|
4805
|
+
const missing = missingBoardConfigFields(cfg);
|
|
4806
|
+
if (!missing.length) return null;
|
|
4807
|
+
return cfg.projectId ? { kind: "registry-meta-missing", missing } : { kind: "no-project-detected" };
|
|
4808
|
+
}
|
|
4809
|
+
function resolveBoardConfig(cfg) {
|
|
4810
|
+
const gap = diagnoseBoardConfigGap(cfg);
|
|
4811
|
+
if (gap) {
|
|
4812
|
+
throw gap.kind === "no-project-detected" ? new Error(
|
|
4813
|
+
"no MMI project detected for this workspace (no Hub registry match for this repo, or no git origin in this folder); run `mmi-cli board read --repo <owner/repo>` to target a specific registered repo, or `mmi-cli project get <owner/repo>` to check registration"
|
|
4814
|
+
) : new Error(
|
|
4815
|
+
`Hub registry board META missing ${gap.missing.join(", ")}; run \`gh auth login\`, then \`mmi-cli project get <owner/repo>\`, or ask a master-admin to register/backfill board coords`
|
|
4805
4816
|
);
|
|
4806
4817
|
}
|
|
4807
4818
|
return {
|
|
@@ -4864,6 +4875,11 @@ function detailCandidates(report) {
|
|
|
4864
4875
|
...report.secondary.claimable
|
|
4865
4876
|
].filter((item) => item.contentType === "Issue");
|
|
4866
4877
|
}
|
|
4878
|
+
function boardNotFoundError(ref, board2, opts = {}) {
|
|
4879
|
+
const verb = opts.verb ?? "is not on";
|
|
4880
|
+
const remedy = opts.remedy ?? "if it lives on a different board, pass --repo <owner/repo> for the repo that owns it";
|
|
4881
|
+
return new Error(`${ref} ${verb} the ${board2.owner} #${board2.number} board; ${remedy}`);
|
|
4882
|
+
}
|
|
4867
4883
|
function findClaimableItem(report, selector) {
|
|
4868
4884
|
const candidates = [...report.primary.claimable, ...report.secondary.claimable];
|
|
4869
4885
|
const found = candidates.find((item) => item.repository.toLowerCase() === selector.repo.toLowerCase() && item.number === selector.number);
|
|
@@ -4882,7 +4898,10 @@ function findClaimableItem(report, selector) {
|
|
|
4882
4898
|
if (existing.assignees.length) throw new Error(`${existing.ref} is already assigned to @${existing.assignees.join(", @")}`);
|
|
4883
4899
|
throw new Error(`${existing.ref} is not claimable`);
|
|
4884
4900
|
}
|
|
4885
|
-
throw
|
|
4901
|
+
throw boardNotFoundError(`${selector.repo}#${selector.number}`, report.project, {
|
|
4902
|
+
verb: "is not a claimable item on",
|
|
4903
|
+
remedy: "it may already be Done (excluded from claim tracking), or it lives on a different board \u2014 pass --repo <owner/repo> for the repo that owns it"
|
|
4904
|
+
});
|
|
4886
4905
|
}
|
|
4887
4906
|
function renderBoardItem(item) {
|
|
4888
4907
|
const assignees = item.assignees.length ? `@${item.assignees.join(", @")}` : "unassigned";
|
|
@@ -5022,11 +5041,11 @@ async function readBoard(options, deps = {}) {
|
|
|
5022
5041
|
}
|
|
5023
5042
|
return report;
|
|
5024
5043
|
}
|
|
5025
|
-
function findBoardItem(items, selector) {
|
|
5044
|
+
function findBoardItem(items, selector, board2) {
|
|
5026
5045
|
const found = items.find(
|
|
5027
5046
|
(candidate) => candidate.repository.toLowerCase() === selector.repo.toLowerCase() && candidate.number === selector.number
|
|
5028
5047
|
);
|
|
5029
|
-
if (!found) throw
|
|
5048
|
+
if (!found) throw boardNotFoundError(`${selector.repo}#${selector.number}`, board2);
|
|
5030
5049
|
return found;
|
|
5031
5050
|
}
|
|
5032
5051
|
async function resolveCurrentRepo(options, deps) {
|
|
@@ -5045,7 +5064,9 @@ async function moveBoardItem(options, deps = {}) {
|
|
|
5045
5064
|
const selector = parseIssueSelector(options.selector, currentRepo);
|
|
5046
5065
|
const lookup = await fetchIssueProjectItem(client, cfg, selector);
|
|
5047
5066
|
const item = lookup.item;
|
|
5048
|
-
if (!item)
|
|
5067
|
+
if (!item) {
|
|
5068
|
+
throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
5069
|
+
}
|
|
5049
5070
|
const optionId = cfg.statusOptions[options.status];
|
|
5050
5071
|
try {
|
|
5051
5072
|
await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId);
|
|
@@ -5091,7 +5112,7 @@ async function showBoardItem(options, deps = {}) {
|
|
|
5091
5112
|
const currentRepo = await resolveCurrentRepo(options, deps);
|
|
5092
5113
|
const selector = parseIssueSelector(options.selector, currentRepo);
|
|
5093
5114
|
const { item } = await fetchIssueProjectItem(client, cfg, selector);
|
|
5094
|
-
if (!item) throw
|
|
5115
|
+
if (!item) throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
5095
5116
|
if (item.contentType === "Issue") {
|
|
5096
5117
|
try {
|
|
5097
5118
|
item.details = await fetchIssueDetails(client, item.repository, item.number);
|
|
@@ -5104,9 +5125,10 @@ async function showBoardItem(options, deps = {}) {
|
|
|
5104
5125
|
async function prepareClaimContext(options, selectors, deps, collected) {
|
|
5105
5126
|
const cfg = resolveBoardConfig(options.config);
|
|
5106
5127
|
const client = deps.client ?? defaultGitHubClient();
|
|
5128
|
+
const board2 = { owner: cfg.projectOwner, number: cfg.projectNumber };
|
|
5107
5129
|
for (const selector of selectors) {
|
|
5108
5130
|
try {
|
|
5109
|
-
findBoardItem(collected.items, selector);
|
|
5131
|
+
findBoardItem(collected.items, selector, board2);
|
|
5110
5132
|
} catch {
|
|
5111
5133
|
const fallback = (await fetchIssueProjectItem(client, cfg, selector)).item;
|
|
5112
5134
|
if (fallback) collected.items.push(fallback);
|
|
@@ -5132,7 +5154,7 @@ async function prepareClaimContext(options, selectors, deps, collected) {
|
|
|
5132
5154
|
}
|
|
5133
5155
|
async function claimOneBoardItem(ctx, selector, options) {
|
|
5134
5156
|
const { cfg, client, report } = ctx;
|
|
5135
|
-
const flatItem = findBoardItem(ctx.items, selector);
|
|
5157
|
+
const flatItem = findBoardItem(ctx.items, selector, { owner: cfg.projectOwner, number: cfg.projectNumber });
|
|
5136
5158
|
if (flatItem.status === "Todo" && flatItem.assignees.length === 0 && !ctx.writable.has(flatItem.repository.toLowerCase())) {
|
|
5137
5159
|
throw new Error(`${flatItem.ref} is not claimable: viewer does not have write access to ${flatItem.repository}`);
|
|
5138
5160
|
}
|
|
@@ -6994,6 +7016,18 @@ var PR_LAND_POLL_MS = 3e4;
|
|
|
6994
7016
|
var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
|
|
6995
7017
|
var PR_LAND_STATE_READ_RETRIES = 3;
|
|
6996
7018
|
var PR_LAND_STATE_READ_DELAY_MS = 2e3;
|
|
7019
|
+
var PR_LAND_MERGE_RETRY_DELAY_MS = 3e3;
|
|
7020
|
+
async function mergeAutoWithTransientRetry(prNumber, repo, deps) {
|
|
7021
|
+
const first = await deps.mergeAuto(prNumber, repo);
|
|
7022
|
+
if (first.mergeStatus !== "failed") return first;
|
|
7023
|
+
const ready = await deps.probeMergeReady(prNumber, repo).catch(() => ({ open: false, mergeable: false, checksPassing: false }));
|
|
7024
|
+
if (!ready.open || !ready.mergeable || !ready.checksPassing) return first;
|
|
7025
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
|
|
7026
|
+
await sleep(PR_LAND_MERGE_RETRY_DELAY_MS);
|
|
7027
|
+
const retried = await deps.mergeAuto(prNumber, repo);
|
|
7028
|
+
if (retried.mergeStatus !== "failed") return retried;
|
|
7029
|
+
return { mergeStatus: "failed", error: `merge retry after transient failure also failed: ${retried.error ?? first.error ?? "unknown error"}` };
|
|
7030
|
+
}
|
|
6997
7031
|
async function readGhPrStateWithRetry(fetchState, options) {
|
|
6998
7032
|
const retries = options?.retries ?? PR_LAND_STATE_READ_RETRIES;
|
|
6999
7033
|
const delayMs = options?.delayMs ?? PR_LAND_STATE_READ_DELAY_MS;
|
|
@@ -7037,7 +7071,7 @@ async function runPrLand(prNumber, options, deps) {
|
|
|
7037
7071
|
error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
|
|
7038
7072
|
};
|
|
7039
7073
|
}
|
|
7040
|
-
const merge = await
|
|
7074
|
+
const merge = await mergeAutoWithTransientRetry(prNumber, repo, deps);
|
|
7041
7075
|
base.mergeStatus = merge.mergeStatus;
|
|
7042
7076
|
if (merge.mergeStatus === "failed") {
|
|
7043
7077
|
return { ...base, error: merge.error ?? "merge failed" };
|
|
@@ -9411,7 +9445,7 @@ function trainPlan(command, options = {}) {
|
|
|
9411
9445
|
{ label: "merge development to main", gated: true },
|
|
9412
9446
|
{ 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
9447
|
{ 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 },
|
|
9448
|
+
{ 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
9449
|
{ label: "roll development forward", gated: true }
|
|
9416
9450
|
];
|
|
9417
9451
|
}
|
|
@@ -9439,7 +9473,7 @@ function trainPlan(command, options = {}) {
|
|
|
9439
9473
|
{ label: "merge rc to main", gated: true },
|
|
9440
9474
|
{ 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
9475
|
{ 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 },
|
|
9476
|
+
{ 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
9477
|
{ label: "roll development forward", gated: true }
|
|
9444
9478
|
];
|
|
9445
9479
|
}
|
|
@@ -9943,9 +9977,12 @@ var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
|
|
|
9943
9977
|
var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
|
|
9944
9978
|
var TRAIN_CHECK_ATTEMPTS = 40;
|
|
9945
9979
|
var TRAIN_CHECK_DELAY_MS = 15e3;
|
|
9980
|
+
var TRAIN_PR_ONLY_AUTOMATION_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done"]);
|
|
9981
|
+
var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
|
|
9946
9982
|
async function correlateRun(deps, args) {
|
|
9947
9983
|
const sleep = resolveSleep(deps);
|
|
9948
9984
|
const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
|
|
9985
|
+
const repo = args.mode === "workflow" ? args.repo ?? HUB_REPO3 : HUB_REPO3;
|
|
9949
9986
|
let lastError;
|
|
9950
9987
|
let parsedAnyResponse = false;
|
|
9951
9988
|
for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
|
|
@@ -9954,7 +9991,7 @@ async function correlateRun(deps, args) {
|
|
|
9954
9991
|
"run",
|
|
9955
9992
|
"list",
|
|
9956
9993
|
"--repo",
|
|
9957
|
-
|
|
9994
|
+
repo,
|
|
9958
9995
|
"--workflow",
|
|
9959
9996
|
args.workflow,
|
|
9960
9997
|
...args.mode === "workflow" ? ["--event", args.event] : [],
|
|
@@ -9997,10 +10034,10 @@ function correlateControlRun(deps, since, titleIncludes) {
|
|
|
9997
10034
|
async function correlateWorkflowRun(deps, args) {
|
|
9998
10035
|
return correlateRun(deps, { ...args, mode: "workflow" });
|
|
9999
10036
|
}
|
|
10000
|
-
async function watchTenantRun(deps, runId) {
|
|
10037
|
+
async function watchTenantRun(deps, runId, repo = HUB_REPO3) {
|
|
10001
10038
|
if (runId == null) return "pending";
|
|
10002
10039
|
try {
|
|
10003
|
-
await deps.run("gh", ["run", "watch", String(runId), "--repo",
|
|
10040
|
+
await deps.run("gh", ["run", "watch", String(runId), "--repo", repo, "--exit-status"]);
|
|
10004
10041
|
return "success";
|
|
10005
10042
|
} catch {
|
|
10006
10043
|
return "failure";
|
|
@@ -10013,9 +10050,9 @@ async function fetchControlRunLog(deps, runId) {
|
|
|
10013
10050
|
return "";
|
|
10014
10051
|
}
|
|
10015
10052
|
}
|
|
10016
|
-
async function watchWorkflowRun(deps, workflow, run) {
|
|
10053
|
+
async function watchWorkflowRun(deps, workflow, run, repo = HUB_REPO3) {
|
|
10017
10054
|
if (run.runId == null) return { workflow, conclusion: "pending" };
|
|
10018
|
-
const conclusion = await watchTenantRun(deps, run.runId);
|
|
10055
|
+
const conclusion = await watchTenantRun(deps, run.runId, repo);
|
|
10019
10056
|
return { workflow, runId: run.runId, runUrl: run.runUrl, conclusion };
|
|
10020
10057
|
}
|
|
10021
10058
|
function aggregateWorkflowRuns(runs) {
|
|
@@ -10124,6 +10161,7 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
10124
10161
|
let lastStatus = "not checked";
|
|
10125
10162
|
let lastError;
|
|
10126
10163
|
const everObserved = /* @__PURE__ */ new Set();
|
|
10164
|
+
const autoSatisfied = /* @__PURE__ */ new Set();
|
|
10127
10165
|
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
|
|
10128
10166
|
if (attempt > 0) await sleep(TRAIN_CHECK_DELAY_MS);
|
|
10129
10167
|
let checkRuns;
|
|
@@ -10145,18 +10183,25 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
|
|
|
10145
10183
|
for (const c of required) {
|
|
10146
10184
|
if (checkRuns.some((r) => r.name === c) || statuses.some((s) => s.context === c)) everObserved.add(c);
|
|
10147
10185
|
}
|
|
10148
|
-
|
|
10149
|
-
|
|
10186
|
+
if (attempt >= TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS - 1) {
|
|
10187
|
+
for (const c of required) {
|
|
10188
|
+
if (TRAIN_PR_ONLY_AUTOMATION_CONTEXTS.has(c) && !everObserved.has(c)) autoSatisfied.add(c);
|
|
10189
|
+
}
|
|
10190
|
+
}
|
|
10191
|
+
const pending = required.filter((c) => !autoSatisfied.has(c));
|
|
10192
|
+
const states = pending.map((c) => [c, resolveContextState(c, checkRuns, statuses)]);
|
|
10193
|
+
const satisfiedNote = autoSatisfied.size ? `, auto-satisfied (never runs on a tag, see #2404): ${[...autoSatisfied].join(", ")}` : "";
|
|
10194
|
+
lastStatus = `${states.map(([c, s]) => `${c}=${s}`).join(", ")}${satisfiedNote}`;
|
|
10150
10195
|
const failed = states.filter(([, s]) => s === "failed").map(([c]) => c);
|
|
10151
10196
|
if (failed.length > 0) {
|
|
10152
10197
|
throw new Error(`required train check failed: ${failed.join(", ")} (${lastStatus})`);
|
|
10153
10198
|
}
|
|
10154
10199
|
if (states.every(([, s]) => s === "success")) {
|
|
10155
|
-
return `required checks passed: ${
|
|
10200
|
+
return `required checks passed: ${pending.join(", ") || "(none pending)"}${satisfiedNote}`;
|
|
10156
10201
|
}
|
|
10157
10202
|
}
|
|
10158
10203
|
const waitedMin = Math.round((TRAIN_CHECK_ATTEMPTS - 1) * TRAIN_CHECK_DELAY_MS / 6e4);
|
|
10159
|
-
const neverMaterialized = required.filter((c) => !everObserved.has(c));
|
|
10204
|
+
const neverMaterialized = required.filter((c) => !everObserved.has(c) && !autoSatisfied.has(c));
|
|
10160
10205
|
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
10206
|
throw new Error(
|
|
10162
10207
|
`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 +10291,19 @@ function tenantPublishRecoveryCommand(slug, repo, ref, stage2, publishDir) {
|
|
|
10246
10291
|
if (publishDir && publishDir !== ".") parts.push(`-f publishDir=${publishDir}`);
|
|
10247
10292
|
return parts.join(" ");
|
|
10248
10293
|
}
|
|
10294
|
+
var PUBLISH_IDEMPOTENT_MARKER = /already on npm/i;
|
|
10295
|
+
var PUBLISH_LOG_RETRY_ATTEMPTS = 4;
|
|
10296
|
+
var PUBLISH_LOG_RETRY_DELAY_MS = 6e3;
|
|
10297
|
+
async function reconcilePublishFailure(deps, runId) {
|
|
10298
|
+
if (runId == null) return "failure";
|
|
10299
|
+
const sleep = resolveSleep(deps);
|
|
10300
|
+
for (let attempt = 0; attempt < PUBLISH_LOG_RETRY_ATTEMPTS; attempt++) {
|
|
10301
|
+
if (attempt > 0) await sleep(PUBLISH_LOG_RETRY_DELAY_MS);
|
|
10302
|
+
const log = await fetchControlRunLog(deps, runId);
|
|
10303
|
+
if (PUBLISH_IDEMPOTENT_MARKER.test(log)) return "success";
|
|
10304
|
+
}
|
|
10305
|
+
return "failure";
|
|
10306
|
+
}
|
|
10249
10307
|
async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFailure = "throw", publishDir) {
|
|
10250
10308
|
const since = (deps.now ?? Date.now)();
|
|
10251
10309
|
const dispatchArgs = [
|
|
@@ -10276,8 +10334,16 @@ async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFail
|
|
|
10276
10334
|
};
|
|
10277
10335
|
}
|
|
10278
10336
|
const { runId, runUrl } = await correlatePublishRun(deps, since, [ctx.slug, stage2]);
|
|
10279
|
-
|
|
10280
|
-
|
|
10337
|
+
let deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
10338
|
+
let note = `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`;
|
|
10339
|
+
if (deployStatus === "failure") {
|
|
10340
|
+
const reconciled = await reconcilePublishFailure(deps, runId);
|
|
10341
|
+
if (reconciled === "success") {
|
|
10342
|
+
deployStatus = "success";
|
|
10343
|
+
note = `${note}; run reported failure but its log confirms the version is already on npm (idempotent E409 \u2014 #2428)`;
|
|
10344
|
+
}
|
|
10345
|
+
}
|
|
10346
|
+
return { note, runId, runUrl, deployStatus };
|
|
10281
10347
|
}
|
|
10282
10348
|
async function dispatchPublishIfRequired(deps, ctx, meta, model, stage2, publishRef, watch, dispatchFailure) {
|
|
10283
10349
|
if (!meta.publishRequired || stage2 !== "main") return null;
|
|
@@ -10294,6 +10360,18 @@ function appendPublishDispatch(deploy, publish) {
|
|
|
10294
10360
|
deployStatus: deploy.deployStatus === "failure" || publish.deployStatus === "failure" ? "failure" : deploy.deployStatus === "pending" || publish.deployStatus === "pending" ? "pending" : "success"
|
|
10295
10361
|
};
|
|
10296
10362
|
}
|
|
10363
|
+
async function watchOwnWorkflowRuns(deps, repo, targets, since, headSha) {
|
|
10364
|
+
const workflowRuns = [];
|
|
10365
|
+
for (const target of targets) {
|
|
10366
|
+
try {
|
|
10367
|
+
const run = await correlateWorkflowRun(deps, { ...target, since, headSha, repo });
|
|
10368
|
+
workflowRuns.push(await watchWorkflowRun(deps, target.workflow, run, repo));
|
|
10369
|
+
} catch {
|
|
10370
|
+
workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
|
|
10371
|
+
}
|
|
10372
|
+
}
|
|
10373
|
+
return workflowRuns;
|
|
10374
|
+
}
|
|
10297
10375
|
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw", publishDir) {
|
|
10298
10376
|
if (model === "tenant-container" || model === "solo-container") {
|
|
10299
10377
|
const since = (deps.now ?? Date.now)();
|
|
@@ -10312,7 +10390,18 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
10312
10390
|
return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
10313
10391
|
}
|
|
10314
10392
|
if (model === "registry-publish") {
|
|
10315
|
-
|
|
10393
|
+
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)";
|
|
10394
|
+
if (ref === "rc" || !watch || !autoRunHeadSha) return { note, deployStatus: "pending" };
|
|
10395
|
+
const since = autoRunSince ?? (deps.now ?? Date.now)();
|
|
10396
|
+
const workflowRuns = await watchOwnWorkflowRuns(
|
|
10397
|
+
deps,
|
|
10398
|
+
ctx.repo,
|
|
10399
|
+
[{ workflow: "publish.yml", event: "release" }],
|
|
10400
|
+
since,
|
|
10401
|
+
autoRunHeadSha
|
|
10402
|
+
);
|
|
10403
|
+
const primary = workflowRuns[0];
|
|
10404
|
+
return { note, runId: primary?.runId, runUrl: primary?.runUrl, workflowRuns, deployStatus: aggregateWorkflowRuns(workflowRuns) };
|
|
10316
10405
|
}
|
|
10317
10406
|
if (model === "hub-serverless") {
|
|
10318
10407
|
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 +10412,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
|
|
|
10323
10412
|
{ workflow: "deploy.yml", event: "release" },
|
|
10324
10413
|
{ workflow: "publish.yml", event: "release" }
|
|
10325
10414
|
];
|
|
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
|
-
}
|
|
10415
|
+
const workflowRuns = await watchOwnWorkflowRuns(deps, HUB_REPO3, targets, since, autoRunHeadSha);
|
|
10335
10416
|
const primary = workflowRuns[0];
|
|
10336
10417
|
return {
|
|
10337
10418
|
note,
|
|
@@ -10364,25 +10445,113 @@ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix
|
|
|
10364
10445
|
}
|
|
10365
10446
|
return { foldPaths, tolerated, predicted };
|
|
10366
10447
|
}
|
|
10367
|
-
async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted) {
|
|
10448
|
+
async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted, preFold) {
|
|
10368
10449
|
await deps.run("git", ["checkout", "main"]);
|
|
10369
10450
|
await ffOnlyPull(deps, "main");
|
|
10451
|
+
preFold.mainSha = clean(await deps.run("git", ["rev-parse", "main"]));
|
|
10370
10452
|
if (predicted.length === 0) {
|
|
10371
10453
|
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
10372
10454
|
} else {
|
|
10373
10455
|
await mergeWithToleratedResolution(deps, sourceRef, mergeLabel, "theirs", tolerated);
|
|
10374
10456
|
}
|
|
10375
10457
|
}
|
|
10376
|
-
async function
|
|
10377
|
-
const
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10458
|
+
async function probeFoldFailureState(deps) {
|
|
10459
|
+
const branch = await currentBranch(deps);
|
|
10460
|
+
const mergeInProgress = await deps.run("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).then(() => true).catch(() => false);
|
|
10461
|
+
const dirty = porcelainHasBlockingChanges(await deps.run("git", ["status", "--porcelain"]).catch(() => ""));
|
|
10462
|
+
const mainSha = await deps.run("git", ["rev-parse", "main"]).then(clean).catch(() => "");
|
|
10463
|
+
const originMainSha = await deps.run("git", ["rev-parse", "origin/main"]).then(clean).catch(() => "");
|
|
10464
|
+
const originIsAncestorOfMain = Boolean(mainSha) && Boolean(originMainSha) ? await deps.run("git", ["merge-base", "--is-ancestor", "origin/main", "main"]).then(() => true).catch(() => false) : false;
|
|
10465
|
+
return { branch, mergeInProgress, dirty, mainSha, originMainSha, originIsAncestorOfMain };
|
|
10466
|
+
}
|
|
10467
|
+
function shaLabel(sha) {
|
|
10468
|
+
return sha ? sha.slice(0, 7) : "(unknown)";
|
|
10469
|
+
}
|
|
10470
|
+
async function finishFoldAutoRestore(deps, causeMessage, startBranch, what) {
|
|
10471
|
+
try {
|
|
10472
|
+
await deps.run("git", ["checkout", startBranch]);
|
|
10473
|
+
} catch (e) {
|
|
10474
|
+
return new Error(
|
|
10475
|
+
`${causeMessage}
|
|
10476
|
+
|
|
10477
|
+
fold failed; ${what}, but returning to ${startBranch} afterwards failed: ${e instanceof Error ? e.message : String(e)}. Finish manually: git checkout ${startBranch}`
|
|
10478
|
+
);
|
|
10479
|
+
}
|
|
10480
|
+
return new Error(
|
|
10481
|
+
`${causeMessage}
|
|
10482
|
+
|
|
10483
|
+
fold failed; ${what} and the checkout was returned to ${startBranch}. Nothing was pushed to origin and no tag was created \u2014 rerun the release once the fold issue above is fixed.`
|
|
10383
10484
|
);
|
|
10384
|
-
|
|
10385
|
-
|
|
10485
|
+
}
|
|
10486
|
+
function foldFailureGuidance(causeMessage, probe, startBranch, preFoldMainSha) {
|
|
10487
|
+
const steps = preFoldMainSha ? [
|
|
10488
|
+
` 1. git status # inspect what is actually there`,
|
|
10489
|
+
` 2. git log --oneline ${preFoldMainSha}..main # commits beyond the pre-fold main \u2014 confirm they are only this run's merge/fold attempt`,
|
|
10490
|
+
` 3. git checkout main && git reset --hard ${preFoldMainSha} # back to the exact commit the fold built on`,
|
|
10491
|
+
` 4. git checkout ${startBranch}`
|
|
10492
|
+
] : [
|
|
10493
|
+
` 1. git status # inspect what is actually there`,
|
|
10494
|
+
` 2. git log --oneline origin/main..main # this run never committed to main (it failed before the merge) \u2014 any commits here predate this run; preserve them`,
|
|
10495
|
+
` 3. git checkout ${startBranch}`
|
|
10496
|
+
];
|
|
10497
|
+
return new Error(
|
|
10498
|
+
`${causeMessage}
|
|
10499
|
+
|
|
10500
|
+
fold failed leaving the checkout in a state train --apply will not auto-restore (branch=${probe.branch || "(unknown)"}, dirty-working-tree=${probe.dirty}, main=${shaLabel(probe.mainSha)}, origin/main=${shaLabel(probe.originMainSha)}, pre-fold main=${preFoldMainSha ? shaLabel(preFoldMainSha) : "not captured"}, origin/main is${probe.originIsAncestorOfMain ? "" : " NOT"} an ancestor of main). Nothing was pushed to origin and no tag was created.
|
|
10501
|
+
Recovery sequence:
|
|
10502
|
+
${steps.join("\n")}`
|
|
10503
|
+
);
|
|
10504
|
+
}
|
|
10505
|
+
async function recoverFailedFold(deps, cause, startBranch, preFoldMainSha) {
|
|
10506
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
10507
|
+
const probe = await probeFoldFailureState(deps);
|
|
10508
|
+
if (probe.mergeInProgress) {
|
|
10509
|
+
try {
|
|
10510
|
+
await deps.run("git", ["merge", "--abort"]);
|
|
10511
|
+
} catch (e) {
|
|
10512
|
+
return new Error(
|
|
10513
|
+
`${causeMessage}
|
|
10514
|
+
|
|
10515
|
+
fold failed with an in-progress merge left on ${probe.branch || "(unknown branch)"}; automatic \`git merge --abort\` failed: ${e instanceof Error ? e.message : String(e)}. Nothing was pushed to origin and no tag was created.
|
|
10516
|
+
Recovery sequence:
|
|
10517
|
+
1. git merge --abort
|
|
10518
|
+
2. git checkout ${startBranch}`
|
|
10519
|
+
);
|
|
10520
|
+
}
|
|
10521
|
+
return finishFoldAutoRestore(deps, causeMessage, startBranch, "the in-progress merge was aborted");
|
|
10522
|
+
}
|
|
10523
|
+
if (probe.branch === "main" && preFoldMainSha) {
|
|
10524
|
+
const mainDescendsFromPreFold = await deps.run("git", ["merge-base", "--is-ancestor", preFoldMainSha, "main"]).then(() => true).catch(() => false);
|
|
10525
|
+
if (mainDescendsFromPreFold) {
|
|
10526
|
+
try {
|
|
10527
|
+
await deps.run("git", ["reset", "--hard", preFoldMainSha]);
|
|
10528
|
+
} catch (e) {
|
|
10529
|
+
return new Error(
|
|
10530
|
+
`${causeMessage}
|
|
10531
|
+
|
|
10532
|
+
fold failed after this run's merge/fold committed locally on main (now ${shaLabel(probe.mainSha)}, unpushed, built on the pre-fold main ${shaLabel(preFoldMainSha)}); automatic \`git reset --hard ${shaLabel(preFoldMainSha)}\` failed: ${e instanceof Error ? e.message : String(e)}. Nothing was pushed to origin and no tag was created.
|
|
10533
|
+
Recovery sequence:
|
|
10534
|
+
1. git reset --hard ${preFoldMainSha}
|
|
10535
|
+
2. git checkout ${startBranch}`
|
|
10536
|
+
);
|
|
10537
|
+
}
|
|
10538
|
+
const aheadNote = probe.originMainSha && preFoldMainSha !== probe.originMainSha ? `; local main carried commit(s) origin/main (${shaLabel(probe.originMainSha)}) does not have \u2014 they were preserved` : "";
|
|
10539
|
+
return finishFoldAutoRestore(
|
|
10540
|
+
deps,
|
|
10541
|
+
causeMessage,
|
|
10542
|
+
startBranch,
|
|
10543
|
+
`local main was reset to its pre-fold state ${shaLabel(preFoldMainSha)} (discarding only this run's merge/fold commit(s) at ${shaLabel(probe.mainSha)})${aheadNote}`
|
|
10544
|
+
);
|
|
10545
|
+
}
|
|
10546
|
+
}
|
|
10547
|
+
return foldFailureGuidance(causeMessage, probe, startBranch, preFoldMainSha);
|
|
10548
|
+
}
|
|
10549
|
+
async function runFoldStage(deps, startBranch, preFold, fn) {
|
|
10550
|
+
try {
|
|
10551
|
+
return await fn();
|
|
10552
|
+
} catch (e) {
|
|
10553
|
+
throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
|
|
10554
|
+
}
|
|
10386
10555
|
}
|
|
10387
10556
|
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
|
|
10388
10557
|
await ensureTagPushed(deps, tag, releaseSha);
|
|
@@ -10475,15 +10644,20 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
10475
10644
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
10476
10645
|
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
10477
10646
|
const rcShaAtRelease = !directTrack && hasRcBranch ? clean(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
10478
|
-
const { foldPaths: foldPaths2 } = await
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10647
|
+
const { foldPaths: foldPaths2, tolerated: tolerated2, predicted: predicted2 } = await preflightMergeToMain(
|
|
10648
|
+
deps,
|
|
10649
|
+
deployModel2,
|
|
10650
|
+
"origin/development",
|
|
10651
|
+
"development -> main merge would conflict on untolerated path(s)",
|
|
10652
|
+
"The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
|
|
10653
|
+
);
|
|
10654
|
+
const preFold2 = {};
|
|
10655
|
+
const { versionFold: versionFold2, releaseSha: releaseSha2 } = await runFoldStage(deps, "development", preFold2, async () => {
|
|
10656
|
+
await executeMergeToMain(deps, "development", "development -> main", tolerated2, predicted2, preFold2);
|
|
10657
|
+
const versionFold3 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
10658
|
+
const releaseSha3 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10659
|
+
return { versionFold: versionFold3, releaseSha: releaseSha3 };
|
|
10484
10660
|
});
|
|
10485
|
-
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
10486
|
-
const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10487
10661
|
const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2);
|
|
10488
10662
|
const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
|
|
10489
10663
|
if (directTrack) {
|
|
@@ -10560,10 +10734,14 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
10560
10734
|
);
|
|
10561
10735
|
}
|
|
10562
10736
|
const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
10563
|
-
|
|
10564
|
-
const tag =
|
|
10565
|
-
|
|
10566
|
-
|
|
10737
|
+
const preFold = {};
|
|
10738
|
+
const { tag, versionFold, releaseSha } = await runFoldStage(deps, "rc", preFold, async () => {
|
|
10739
|
+
await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted, preFold);
|
|
10740
|
+
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
10741
|
+
const versionFold2 = await foldReleaseVersion(deps, deployModel, tag2, foldPaths);
|
|
10742
|
+
const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10743
|
+
return { tag: tag2, versionFold: versionFold2, releaseSha: releaseSha2 };
|
|
10744
|
+
});
|
|
10567
10745
|
const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha);
|
|
10568
10746
|
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
10569
10747
|
const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
@@ -11165,7 +11343,11 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11165
11343
|
runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
|
|
11166
11344
|
}
|
|
11167
11345
|
deployNote = "watched release-triggered deploy.yml + publish.yml";
|
|
11168
|
-
} else if (deployModel === "
|
|
11346
|
+
} else if (deployModel === "registry-publish") {
|
|
11347
|
+
const run = await watchReleaseRun(deps, ctx, "publish.yml", mergedSha);
|
|
11348
|
+
runs.push(run);
|
|
11349
|
+
deployNote = "watched this repo's own release-triggered publish.yml (#2428 \u2014 no central dispatch)";
|
|
11350
|
+
} else if (deployModel === "tenant-container" || deployModel === "solo-container") {
|
|
11169
11351
|
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
11170
11352
|
const deploy = await dispatchDeploy(
|
|
11171
11353
|
deps,
|
|
@@ -11181,7 +11363,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11181
11363
|
);
|
|
11182
11364
|
const publish = deploy.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report") : null;
|
|
11183
11365
|
let dispatch = appendPublishDispatch(deploy, publish);
|
|
11184
|
-
if (!publish && deploy.deployStatus !== "success" && meta.publishRequired
|
|
11366
|
+
if (!publish && deploy.deployStatus !== "success" && meta.publishRequired) {
|
|
11185
11367
|
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
11368
|
dispatch = {
|
|
11187
11369
|
...dispatch,
|
|
@@ -11189,25 +11371,17 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
|
|
|
11189
11371
|
};
|
|
11190
11372
|
}
|
|
11191
11373
|
deployNote = dispatch.note;
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
});
|
|
11198
|
-
}
|
|
11374
|
+
runs.push({
|
|
11375
|
+
workflow: "tenant-deploy.yml",
|
|
11376
|
+
url: deploy.runUrl,
|
|
11377
|
+
conclusion: deploy.deployStatus === "success" ? "success" : deploy.deployStatus === "failure" ? "failure" : deploy.deployStatus ?? "pending"
|
|
11378
|
+
});
|
|
11199
11379
|
if (publish?.runUrl) {
|
|
11200
11380
|
runs.push({
|
|
11201
11381
|
workflow: "tenant-publish.yml",
|
|
11202
11382
|
url: publish.runUrl,
|
|
11203
11383
|
conclusion: publish.deployStatus === "success" ? "success" : publish.deployStatus === "failure" ? "failure" : publish.deployStatus ?? "pending"
|
|
11204
11384
|
});
|
|
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
11385
|
}
|
|
11212
11386
|
} else {
|
|
11213
11387
|
deployNote = `no hotfix deploy dispatch for deployModel=${deployModel} \u2014 prod deploy is repo-specific`;
|
|
@@ -14751,16 +14925,24 @@ function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
|
|
|
14751
14925
|
return false;
|
|
14752
14926
|
});
|
|
14753
14927
|
}
|
|
14928
|
+
function cursorPluginCacheSeedTargets(pins, releasedVersion, cacheRoot, cacheRootExists = false) {
|
|
14929
|
+
const needing = cursorPluginPinsNeedingSeed(pins, releasedVersion);
|
|
14930
|
+
if (needing.length > 0) return needing.map((pin) => pin.path);
|
|
14931
|
+
if (pins.length === 0 && cacheRoot && cacheRootExists && isSemverVersion(releasedVersion)) {
|
|
14932
|
+
return [(0, import_node_path15.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
|
|
14933
|
+
}
|
|
14934
|
+
return [];
|
|
14935
|
+
}
|
|
14754
14936
|
async function applyCursorPluginCacheSeed(input) {
|
|
14755
14937
|
if (!isSemverVersion(input.releasedVersion)) return false;
|
|
14756
|
-
const
|
|
14757
|
-
if (
|
|
14938
|
+
const targets = cursorPluginCacheSeedTargets(input.pins, input.releasedVersion, input.cacheRoot, input.cacheRootExists);
|
|
14939
|
+
if (targets.length === 0) return false;
|
|
14758
14940
|
const tmpRoot = await input.mkdtemp("mmi-cursor-seed-");
|
|
14759
14941
|
const source = await resolvePluginMmiSource(input.releasedVersion, input.hubCheckout, tmpRoot, input.execFileP);
|
|
14760
14942
|
if (!source) return false;
|
|
14761
14943
|
input.log(` \u21BB seeding Cursor MMI plugin cache \u2192 ${input.releasedVersion}\u2026`);
|
|
14762
|
-
for (const
|
|
14763
|
-
syncDirContents(source,
|
|
14944
|
+
for (const dest of targets) {
|
|
14945
|
+
syncDirContents(source, dest);
|
|
14764
14946
|
}
|
|
14765
14947
|
(0, import_node_fs14.rmSync)(tmpRoot, { recursive: true, force: true });
|
|
14766
14948
|
return true;
|
|
@@ -15040,6 +15222,33 @@ function buildMmiPluginCacheCleanupCheck(input) {
|
|
|
15040
15222
|
}))
|
|
15041
15223
|
};
|
|
15042
15224
|
}
|
|
15225
|
+
var CURSOR_PLUGIN_CACHE_CLEANUP_LABEL = "leftover older MMI plugin cache pins (Cursor)";
|
|
15226
|
+
var CURSOR_PLUGIN_CACHE_CLEANUP_FIX = "older Cursor MMI plugin cache pins can be removed manually after confirming in Cursor Settings \u2192 Plugins \u2192 Marketplace which pin is registered";
|
|
15227
|
+
function buildCursorPluginCacheCleanupCheck(input) {
|
|
15228
|
+
const base = {
|
|
15229
|
+
ok: true,
|
|
15230
|
+
label: CURSOR_PLUGIN_CACHE_CLEANUP_LABEL,
|
|
15231
|
+
fix: CURSOR_PLUGIN_CACHE_CLEANUP_FIX
|
|
15232
|
+
};
|
|
15233
|
+
if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
|
|
15234
|
+
const semverPins = input.pins.filter((pin) => isSemverVersion2(pin.version));
|
|
15235
|
+
if (semverPins.length === 0) return base;
|
|
15236
|
+
const activeVersion = semverPins.map((pin) => pin.version).reduce((a, b) => compareVersions(a, b) >= 0 ? a : b);
|
|
15237
|
+
const protectedVersions = new Set(
|
|
15238
|
+
[normalizeVersion(input.releasedVersion), normalizeVersion(activeVersion)].filter((v) => Boolean(v))
|
|
15239
|
+
);
|
|
15240
|
+
const leftoverPins = semverPins.filter((pin) => !protectedVersions.has(normalizeVersion(pin.version)));
|
|
15241
|
+
if (leftoverPins.length === 0) return base;
|
|
15242
|
+
const leftovers = leftoverPins.map((pin) => ({ surface: "cursor", root: input.cacheRoot, name: pin.name, path: pin.path }));
|
|
15243
|
+
const listed = leftoverPins.map((pin) => `${pin.name} (${pin.version})`).join(", ");
|
|
15244
|
+
return {
|
|
15245
|
+
...base,
|
|
15246
|
+
ok: false,
|
|
15247
|
+
severityOverride: "advisory",
|
|
15248
|
+
leftovers,
|
|
15249
|
+
fix: `${leftoverPins.length} older Cursor MMI plugin cache pin(s) left behind by marketplace re-clones: ${listed}. Not auto-removed: which pin Cursor's Team Marketplace registration is bound to cannot be read programmatically (no per-user CLI, #2409), and the registration may still point at an older pin \u2014 after confirming in Cursor Settings \u2192 Plugins \u2192 Marketplace that the newer pin is the registered one, the older dir(s) can be removed manually`
|
|
15250
|
+
};
|
|
15251
|
+
}
|
|
15043
15252
|
var NESTED_PLUGIN_TREE_LABEL = "self-nested MMI plugin cache tree (#1126)";
|
|
15044
15253
|
var NESTED_PLUGIN_TREE_FIX = "SessionStart and mmi-cli doctor auto-clear when possible; if this persists, run the MAX_PATH-safe robocopy cleanup for a self-nested MMI plugin cache tree (#1126)";
|
|
15045
15254
|
function nestedPluginTreeCleanupCommand(paths, isWindows) {
|
|
@@ -15062,7 +15271,21 @@ function buildNestedPluginTreeCheck(input) {
|
|
|
15062
15271
|
fix: `${nested.length} self-nested MMI plugin cache tree(s) (#1126) exceed MAX_PATH and can't self-clean \u2014 run: ${nestedPluginTreeCleanupCommand(nested.map((n) => n.path), input.isWindows)}`
|
|
15063
15272
|
};
|
|
15064
15273
|
}
|
|
15274
|
+
function nestedTreeReinstallGapFix(surfaces) {
|
|
15275
|
+
const recoveries = surfaces.map((s) => PLUGIN_SURFACE_HEAL[s].recovery).join(" and ");
|
|
15276
|
+
return `self-nested MMI plugin cache tree(s) cleared, but the plugin was NOT reinstalled for ${surfaces.join(" + ")} (host CLI unavailable or reinstall failed) \u2014 run: ${recoveries}`;
|
|
15277
|
+
}
|
|
15065
15278
|
var CODEX_ACTIVE_CACHE_LABEL = "Codex active plugin cache (vs latest release)";
|
|
15279
|
+
function codexEnabledPluginVersionFromList(jsonText, pluginId = MMI_PLUGIN_ID) {
|
|
15280
|
+
try {
|
|
15281
|
+
const parsed = JSON.parse(jsonText);
|
|
15282
|
+
const rows = Array.isArray(parsed?.installed) ? parsed.installed : [];
|
|
15283
|
+
const row = rows.find((r) => r?.pluginId === pluginId && r.installed === true && r.enabled === true);
|
|
15284
|
+
return isSemverVersion2(row?.version) ? row.version.trim().replace(/^v/, "") : void 0;
|
|
15285
|
+
} catch {
|
|
15286
|
+
return void 0;
|
|
15287
|
+
}
|
|
15288
|
+
}
|
|
15066
15289
|
function buildCodexActiveCacheCheck(input) {
|
|
15067
15290
|
const base = {
|
|
15068
15291
|
ok: true,
|
|
@@ -15070,6 +15293,18 @@ function buildCodexActiveCacheCheck(input) {
|
|
|
15070
15293
|
fix: CODEX_PLUGIN_RECOVERY
|
|
15071
15294
|
};
|
|
15072
15295
|
if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
|
|
15296
|
+
if (isSemverVersion2(input.codexActiveVersion)) {
|
|
15297
|
+
if (compareVersions(input.codexActiveVersion, input.releasedVersion) < 0) {
|
|
15298
|
+
return {
|
|
15299
|
+
...base,
|
|
15300
|
+
ok: false,
|
|
15301
|
+
activeCacheVersion: input.codexActiveVersion,
|
|
15302
|
+
releasedVersion: input.releasedVersion,
|
|
15303
|
+
fix: `\`codex plugin list\` reports mmi@mutmutco enabled at ${input.codexActiveVersion} < ${input.releasedVersion} (a newer cache dir or install record may also exist and mask it) \u2014 run \`mmi-cli doctor --apply\` (forces the Codex remove/re-add when \`codex\` is on PATH; restart Codex after), or manually: ${CODEX_PLUGIN_RECOVERY}`
|
|
15304
|
+
};
|
|
15305
|
+
}
|
|
15306
|
+
return { ...base, activeCacheVersion: input.codexActiveVersion, releasedVersion: input.releasedVersion };
|
|
15307
|
+
}
|
|
15073
15308
|
if (isSemverVersion2(input.codexRecordVersion) && compareVersions(input.codexRecordVersion, input.releasedVersion) < 0) {
|
|
15074
15309
|
return base;
|
|
15075
15310
|
}
|
|
@@ -15117,6 +15352,55 @@ function reloadAction(surface) {
|
|
|
15117
15352
|
return "restart Claude Code (or run /reload-plugins)";
|
|
15118
15353
|
}
|
|
15119
15354
|
}
|
|
15355
|
+
function restartAction(surface) {
|
|
15356
|
+
switch (surface) {
|
|
15357
|
+
case "claude-vscode":
|
|
15358
|
+
return "fully restart VS Code";
|
|
15359
|
+
case "codex":
|
|
15360
|
+
return "fully restart Codex";
|
|
15361
|
+
case "opencode":
|
|
15362
|
+
return "fully restart OpenCode";
|
|
15363
|
+
case "cursor":
|
|
15364
|
+
return "fully restart Cursor";
|
|
15365
|
+
case "claude-cli":
|
|
15366
|
+
case "shell":
|
|
15367
|
+
default:
|
|
15368
|
+
return "fully restart Claude Code";
|
|
15369
|
+
}
|
|
15370
|
+
}
|
|
15371
|
+
function parseSessionSkillRootVersion(pluginRoot) {
|
|
15372
|
+
const trimmed = pluginRoot?.trim();
|
|
15373
|
+
if (!trimmed) return void 0;
|
|
15374
|
+
const segments = trimmed.split(/[\\/]+/).filter(Boolean);
|
|
15375
|
+
const mmiIdx = segments.findIndex((seg, i) => seg === "mmi" && segments[i - 1] === "mutmutco");
|
|
15376
|
+
if (mmiIdx < 0) return void 0;
|
|
15377
|
+
const candidate = segments[mmiIdx + 1];
|
|
15378
|
+
return candidate && /^v?\d+\.\d+\.\d+/.test(candidate) ? candidate.replace(/^v/, "") : void 0;
|
|
15379
|
+
}
|
|
15380
|
+
var SESSION_SKILL_ROOT_LABEL = "Session-loaded MMI skill roots (vs on-disk plugin cache)";
|
|
15381
|
+
function buildSessionSkillRootCheck(input) {
|
|
15382
|
+
const base = {
|
|
15383
|
+
ok: true,
|
|
15384
|
+
label: SESSION_SKILL_ROOT_LABEL,
|
|
15385
|
+
fix: `fully restart the host to load the current MMI skill roots \u2014 a running session does not hot-refresh them, and a plugin reload does not re-register them`
|
|
15386
|
+
};
|
|
15387
|
+
if (!input.isOrgRepo) return base;
|
|
15388
|
+
const sessionVersion = parseSessionSkillRootVersion(input.pluginRoot);
|
|
15389
|
+
if (!sessionVersion) return base;
|
|
15390
|
+
const activeCacheVersion = highestSemver(input.cacheVersions ?? []);
|
|
15391
|
+
if (!isSemverVersion2(activeCacheVersion)) return { ...base, sessionVersion };
|
|
15392
|
+
if (compareVersions(sessionVersion, activeCacheVersion) >= 0) {
|
|
15393
|
+
return { ...base, sessionVersion, activeCacheVersion };
|
|
15394
|
+
}
|
|
15395
|
+
return {
|
|
15396
|
+
...base,
|
|
15397
|
+
ok: false,
|
|
15398
|
+
severityOverride: "advisory",
|
|
15399
|
+
sessionVersion,
|
|
15400
|
+
activeCacheVersion,
|
|
15401
|
+
fix: `This ${input.surface} session loaded MMI skill roots from ${sessionVersion} but the on-disk plugin cache is ${activeCacheVersion} \u2014 a running session does NOT hot-refresh skill roots and a plugin reload does not re-register them, so ${restartAction(input.surface)} to load ${activeCacheVersion}. Until then this session keeps executing the ${sessionVersion} skill bundle; halt autonomous /grind and /build first.`
|
|
15402
|
+
};
|
|
15403
|
+
}
|
|
15120
15404
|
var CLAUDE_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub --ref main && claude plugin install mmi@mutmutco`;
|
|
15121
15405
|
var CODEX_RECOVERY = `codex plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && codex plugin marketplace remove mutmutco && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mutmutco`;
|
|
15122
15406
|
var CURSOR_RECOVERY = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace";
|
|
@@ -15329,10 +15613,12 @@ function isSemverVersion2(v) {
|
|
|
15329
15613
|
function staleRecordCommand(surface) {
|
|
15330
15614
|
return surface === "codex" ? CODEX_PLUGIN_RECOVERY : CLAUDE_PLUGIN_RECOVERY;
|
|
15331
15615
|
}
|
|
15616
|
+
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
15617
|
function staleSurfacesFix(stale, releasedVersion) {
|
|
15333
15618
|
const parts = stale.map((s) => {
|
|
15334
15619
|
const at = s.recordPath ? ` (${s.recordPath})` : "";
|
|
15335
|
-
|
|
15620
|
+
const warning = s.surface === "claude" ? ` \u2014 ${CLAUDE_UPDATE_BUTTON_WARNING}` : "";
|
|
15621
|
+
return `${s.surface} record${at} is at ${s.installedVersion}${releasedVersion ? ` < ${releasedVersion}` : ""} \u2014 run: ${staleRecordCommand(s.surface)}${warning}`;
|
|
15336
15622
|
});
|
|
15337
15623
|
return `stale installed-plugin record on ${stale.map((s) => s.surface).join(" + ")}: ${parts.join("; ")}`;
|
|
15338
15624
|
}
|
|
@@ -15374,18 +15660,71 @@ function buildInstalledPluginVersionCheck(input) {
|
|
|
15374
15660
|
staleSurfaces: stale
|
|
15375
15661
|
};
|
|
15376
15662
|
}
|
|
15663
|
+
var MANAGED_PLUGINS = [
|
|
15664
|
+
{
|
|
15665
|
+
// Marketplace-qualified key exactly as installed_plugins.json stores it (same convention as
|
|
15666
|
+
// MMI_PLUGIN_ID = 'mmi@mutmutco') — a bare 'jervaise-powertools' would never match a real record
|
|
15667
|
+
// and the check would be a permanent no-op. The same `<marketplace>/<name>` segments also address its
|
|
15668
|
+
// Cursor Team Marketplace cache dir (~/.cursor/plugins/cache/jervaise/jervaise-powertools/<version>/).
|
|
15669
|
+
id: "jervaise-powertools@jervaise",
|
|
15670
|
+
label: "jervaise-powertools",
|
|
15671
|
+
healCommand: "jerv-cli doctor --apply",
|
|
15672
|
+
opencodePackage: "@jervaise/opencode-jerv"
|
|
15673
|
+
}
|
|
15674
|
+
];
|
|
15675
|
+
function parseManagedPluginId(id) {
|
|
15676
|
+
const at = id.lastIndexOf("@");
|
|
15677
|
+
if (at <= 0 || at === id.length - 1) return null;
|
|
15678
|
+
return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
|
|
15679
|
+
}
|
|
15680
|
+
var MANAGED_PLUGIN_DRIFT_LABEL = "managed org plugin version drift (cross-surface)";
|
|
15681
|
+
function managedPluginDriftFix(drifted) {
|
|
15682
|
+
return drifted.map((d) => {
|
|
15683
|
+
const versions = d.versions.map((v) => `${v.version} (${v.surface})`).join(" vs ");
|
|
15684
|
+
return `${d.label} drift: ${versions} \u2192 run \`${d.healCommand}\``;
|
|
15685
|
+
}).join(" ; ");
|
|
15686
|
+
}
|
|
15687
|
+
function buildManagedPluginDriftCheck(input) {
|
|
15688
|
+
const base = { ok: true, label: MANAGED_PLUGIN_DRIFT_LABEL, fix: "" };
|
|
15689
|
+
if (!input.isOrgRepo) return base;
|
|
15690
|
+
const registry2 = input.plugins ?? MANAGED_PLUGINS;
|
|
15691
|
+
const drifted = [];
|
|
15692
|
+
const normalize = (v) => v.replace(/^v/, "");
|
|
15693
|
+
for (const descriptor of registry2) {
|
|
15694
|
+
const versions = [];
|
|
15695
|
+
for (const source of input.sources) {
|
|
15696
|
+
if (source.directVersions && descriptor.id in source.directVersions) {
|
|
15697
|
+
const version2 = source.directVersions[descriptor.id];
|
|
15698
|
+
if (isSemverVersion2(version2)) versions.push({ surface: source.surface, version: normalize(version2) });
|
|
15699
|
+
continue;
|
|
15700
|
+
}
|
|
15701
|
+
const records = source.installed?.plugins?.[descriptor.id];
|
|
15702
|
+
if (!Array.isArray(records) || records.length === 0) continue;
|
|
15703
|
+
const recordVersion = bestRecord(records).version;
|
|
15704
|
+
const version = isSemverVersion2(recordVersion) ? recordVersion : highestSemver(source.cacheVersions?.[descriptor.id] ?? []);
|
|
15705
|
+
if (!isSemverVersion2(version)) continue;
|
|
15706
|
+
versions.push({ surface: source.surface, version: normalize(version) });
|
|
15707
|
+
}
|
|
15708
|
+
if (versions.length < 2) continue;
|
|
15709
|
+
const distinctVersions = new Set(versions.map((v) => v.version));
|
|
15710
|
+
if (distinctVersions.size < 2) continue;
|
|
15711
|
+
drifted.push({ pluginId: descriptor.id, label: descriptor.label, healCommand: descriptor.healCommand, versions });
|
|
15712
|
+
}
|
|
15713
|
+
if (drifted.length === 0) return base;
|
|
15714
|
+
return { ...base, ok: false, fix: managedPluginDriftFix(drifted), drifted };
|
|
15715
|
+
}
|
|
15377
15716
|
var OPENCODE_VERSION_LABEL = "installed OpenCode MMI adapter version (vs latest release)";
|
|
15378
15717
|
function buildOpencodeVersionCheck(input) {
|
|
15379
15718
|
const fix = pluginRecoveryFix("opencode");
|
|
15380
15719
|
const base = { ok: true, label: OPENCODE_VERSION_LABEL, fix };
|
|
15381
15720
|
if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
|
|
15382
15721
|
if (!isSemverVersion2(input.installedVersion)) {
|
|
15383
|
-
return { ...base, ok: false, releasedVersion: input.releasedVersion };
|
|
15722
|
+
return { ...base, ok: false, severityOverride: "hard", releasedVersion: input.releasedVersion };
|
|
15384
15723
|
}
|
|
15385
15724
|
if (compareVersions(input.installedVersion, input.releasedVersion) >= 0) {
|
|
15386
15725
|
return { ...base, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15387
15726
|
}
|
|
15388
|
-
return { ...base, ok: false, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15727
|
+
return { ...base, ok: false, severityOverride: "hard", installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15389
15728
|
}
|
|
15390
15729
|
function pickOpencodeActiveVersion(input) {
|
|
15391
15730
|
for (const value of [input.envStamp, input.cacheVersion, input.diskVersion]) {
|
|
@@ -15593,6 +15932,9 @@ function cursorPluginInstallFix(input) {
|
|
|
15593
15932
|
const localSeed = input.hubCheckout ? `temporary fallback: copy ${joinCachePath(input.hubCheckout, "plugins", "mmi")} to ${cacheDir}, then restart Cursor` : `temporary fallback: copy plugins/mmi from a local MMI-Hub checkout to ${cacheDir}, then restart Cursor`;
|
|
15594
15933
|
return `Cursor plugin cache at ${cacheDir} is empty or missing ${CURSOR_PLUGIN_JSON_REL}, ${CURSOR_HOOKS_JSON_REL}, or the cursor-hook dispatcher script \u2014 ${autoSeed}; ${marketplaceRefresh}; ${authSteps}; ${localSeed}; ${logHint}; ${guide}`;
|
|
15595
15934
|
}
|
|
15935
|
+
function cursorSeededFromNothingAdvisoryFix(releasedVersion) {
|
|
15936
|
+
return `Cursor MMI plugin cache seeded from nothing${releasedVersion ? ` at ${releasedVersion}` : ""} \u2014 the cache is healthy, but the Team Marketplace pin REGISTRATION could not be verified (no per-user CLI, #2409): if MMI commands do not load after restarting Cursor, a master-admin must register/refresh the MMI Team Marketplace in Cursor Settings \u2192 Plugins \u2192 Marketplace (\xA76.8); ${CURSOR_MARKETPLACE_INSTALL_GUIDE}`;
|
|
15937
|
+
}
|
|
15596
15938
|
function buildCursorPluginInstallCheck(input) {
|
|
15597
15939
|
const base = {
|
|
15598
15940
|
ok: true,
|
|
@@ -15634,9 +15976,10 @@ function buildCursorPluginInstallCheck(input) {
|
|
|
15634
15976
|
return {
|
|
15635
15977
|
...base,
|
|
15636
15978
|
ok: false,
|
|
15979
|
+
severityOverride: "advisory",
|
|
15637
15980
|
cacheRoot: input.cacheRoot,
|
|
15638
15981
|
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);
|
|
15982
|
+
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
15983
|
};
|
|
15641
15984
|
}
|
|
15642
15985
|
return { ...base, cacheRoot: input.cacheRoot, pins: input.pins };
|
|
@@ -15709,9 +16052,7 @@ function buildHubDeployFreshnessCheck(input) {
|
|
|
15709
16052
|
};
|
|
15710
16053
|
}
|
|
15711
16054
|
var PLAYWRIGHT_MCP_VISION_CAP_LABEL = "Playwright MCP vision caps (--caps=vision prohibited)";
|
|
15712
|
-
var PLAYWRIGHT_MCP_VISION_CAP_FIX = "remove --caps=vision (and vision-first defaults) from Playwright MCP args \u2014 use DOM-first tools; see skills/browser-automation/SKILL.md and bootstrap seed mcp-playwright.template.json";
|
|
15713
16055
|
var PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL = "Playwright MCP output dir (use tmp/playwright-mcp)";
|
|
15714
|
-
var PLAYWRIGHT_MCP_OUTPUT_DIR_FIX = "configure Playwright MCP args with --output-dir tmp/playwright-mcp; see skills/browser-automation/SKILL.md and bootstrap seed mcp-playwright.template.json";
|
|
15715
16056
|
function textHasPlaywrightMcp(content) {
|
|
15716
16057
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
15717
16058
|
return /@playwright\/mcp/.test(normalized) || /mcp_servers\.playwright/.test(normalized) || /"playwright"\s*:\s*\{/.test(normalized) || /\bmcpServers\b/.test(normalized);
|
|
@@ -15726,45 +16067,10 @@ function textHasPlaywrightVisionCap(content) {
|
|
|
15726
16067
|
if (/\bvision[-_]?(?:first|only|mode)\b/i.test(normalized)) return true;
|
|
15727
16068
|
return false;
|
|
15728
16069
|
}
|
|
15729
|
-
function buildPlaywrightMcpVisionCapCheck(input) {
|
|
15730
|
-
const base = {
|
|
15731
|
-
ok: true,
|
|
15732
|
-
label: PLAYWRIGHT_MCP_VISION_CAP_LABEL,
|
|
15733
|
-
fix: PLAYWRIGHT_MCP_VISION_CAP_FIX
|
|
15734
|
-
};
|
|
15735
|
-
if (!input.isOrgRepo) return base;
|
|
15736
|
-
const offending = input.configs.filter((c) => textHasPlaywrightVisionCap(c.content)).map((c) => c.path);
|
|
15737
|
-
if (offending.length === 0) return base;
|
|
15738
|
-
return {
|
|
15739
|
-
...base,
|
|
15740
|
-
ok: false,
|
|
15741
|
-
offendingPaths: offending,
|
|
15742
|
-
fix: `${PLAYWRIGHT_MCP_VISION_CAP_FIX} \u2014 found in: ${offending.join(", ")}`
|
|
15743
|
-
};
|
|
15744
|
-
}
|
|
15745
16070
|
function textHasCanonicalPlaywrightOutputDir(content) {
|
|
15746
16071
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
15747
16072
|
return /--output-dir(?:\s*=\s*|["'\s,]+)tmp\/playwright-mcp\b/.test(normalized);
|
|
15748
16073
|
}
|
|
15749
|
-
function textNeedsPlaywrightOutputDir(content) {
|
|
15750
|
-
return textHasPlaywrightMcp(content) && !textHasCanonicalPlaywrightOutputDir(content);
|
|
15751
|
-
}
|
|
15752
|
-
function buildPlaywrightMcpOutputDirCheck(input) {
|
|
15753
|
-
const base = {
|
|
15754
|
-
ok: true,
|
|
15755
|
-
label: PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL,
|
|
15756
|
-
fix: PLAYWRIGHT_MCP_OUTPUT_DIR_FIX
|
|
15757
|
-
};
|
|
15758
|
-
if (!input.isOrgRepo) return base;
|
|
15759
|
-
const offending = input.configs.filter((c) => textNeedsPlaywrightOutputDir(c.content)).map((c) => c.path);
|
|
15760
|
-
if (offending.length === 0) return base;
|
|
15761
|
-
return {
|
|
15762
|
-
...base,
|
|
15763
|
-
ok: false,
|
|
15764
|
-
offendingPaths: offending,
|
|
15765
|
-
fix: `${PLAYWRIGHT_MCP_OUTPUT_DIR_FIX} \u2014 missing in: ${offending.join(", ")}`
|
|
15766
|
-
};
|
|
15767
|
-
}
|
|
15768
16074
|
var STRAY_BROWSER_ARTIFACT_DIRS = [".playwright-mcp", "playwright-report", "test-results"];
|
|
15769
16075
|
var BROWSER_ARTIFACTS_LABEL = "browser MCP artifacts outside tmp/ (use tmp/playwright-mcp)";
|
|
15770
16076
|
var BROWSER_ARTIFACTS_FIX = "move or delete stray Playwright output at repo root; re-run MCP with --output-dir tmp/playwright-mcp \u2014 see skills/browser-automation/SKILL.md";
|
|
@@ -15804,6 +16110,12 @@ function buildSelfUpdateHaltPayload(input) {
|
|
|
15804
16110
|
checks: input.checks
|
|
15805
16111
|
};
|
|
15806
16112
|
}
|
|
16113
|
+
var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 3;
|
|
16114
|
+
var DOCTOR_SELF_UPDATE_HALT_SENTINEL = "MMI_DOCTOR_SELF_UPDATE_HALT";
|
|
16115
|
+
function selfUpdateHaltSentinelLine(input) {
|
|
16116
|
+
const to = input.report.releasedVersion ?? "latest";
|
|
16117
|
+
return `${DOCTOR_SELF_UPDATE_HALT_SENTINEL} rerunRequired=true updatedTo=${to} rerun="mmi-cli ${input.rerunArgs.join(" ")}"`;
|
|
16118
|
+
}
|
|
15807
16119
|
var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
|
|
15808
16120
|
function buildSelfUpdateReexecArgs(opts) {
|
|
15809
16121
|
const args = ["doctor"];
|
|
@@ -15830,6 +16142,30 @@ function preflightOutcome(input) {
|
|
|
15830
16142
|
function pluginAutonomousHaltLine(reloadHint) {
|
|
15831
16143
|
return `\u26A0 PLUGIN RELOAD REQUIRED \u2014 mmi:* skills and agent types are unavailable until you ${reloadHint}. Halt autonomous /grind and /build until then.`;
|
|
15832
16144
|
}
|
|
16145
|
+
var DOCTOR_EXTENDED_CHECK_LABELS = /* @__PURE__ */ new Set([
|
|
16146
|
+
AWS_CROSS_ACCOUNT_LABEL,
|
|
16147
|
+
HUB_DEPLOY_FRESHNESS_LABEL,
|
|
16148
|
+
PLAYWRIGHT_MCP_VISION_CAP_LABEL,
|
|
16149
|
+
PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL,
|
|
16150
|
+
BROWSER_ARTIFACTS_LABEL,
|
|
16151
|
+
SCRATCH_GC_LABEL,
|
|
16152
|
+
GIT_GC_LABEL,
|
|
16153
|
+
OPENCODE_VERSION_LABEL,
|
|
16154
|
+
OPENCODE_DESKTOP_BOOTSTRAP_LABEL
|
|
16155
|
+
]);
|
|
16156
|
+
function isDoctorExtendedCheck(label) {
|
|
16157
|
+
if (DOCTOR_EXTENDED_CHECK_LABELS.has(label)) return true;
|
|
16158
|
+
if (label.startsWith(HUB_DEPLOY_FRESHNESS_LABEL)) return true;
|
|
16159
|
+
if (label.startsWith("@mutmutco design-system") || label.startsWith("@mutmutco registry components")) return true;
|
|
16160
|
+
return false;
|
|
16161
|
+
}
|
|
16162
|
+
function isAdvisoryDoctorCheck(check) {
|
|
16163
|
+
if (check.severityOverride) return check.severityOverride === "advisory";
|
|
16164
|
+
return isDoctorExtendedCheck(check.label);
|
|
16165
|
+
}
|
|
16166
|
+
function doctorExitCode(checks) {
|
|
16167
|
+
return checks.some((c) => !c.ok && !isAdvisoryDoctorCheck(c)) ? 1 : 0;
|
|
16168
|
+
}
|
|
15833
16169
|
function renderPluginUpdateReportStaleOnly(report) {
|
|
15834
16170
|
const v = report.versions;
|
|
15835
16171
|
const released = v.released;
|
|
@@ -15844,24 +16180,97 @@ function renderPluginUpdateReportStaleOnly(report) {
|
|
|
15844
16180
|
return ["Update commands (stale surfaces):", ...blocks];
|
|
15845
16181
|
}
|
|
15846
16182
|
var DOCTOR_VERBOSE_HINT = "Run mmi-cli doctor --verbose for the full audit checklist + version report.";
|
|
15847
|
-
function
|
|
15848
|
-
|
|
15849
|
-
|
|
15850
|
-
|
|
15851
|
-
|
|
16183
|
+
function doctorCheckGlyph(check) {
|
|
16184
|
+
if (check.ok) return "\u2713";
|
|
16185
|
+
return isAdvisoryDoctorCheck(check) ? "\u26A0" : "\u2717";
|
|
16186
|
+
}
|
|
16187
|
+
function doctorVerdictLine(input) {
|
|
16188
|
+
const gaps = input.checks.filter((c) => !c.ok);
|
|
16189
|
+
const hardGaps = gaps.filter((c) => !isAdvisoryDoctorCheck(c));
|
|
16190
|
+
const softGaps = gaps.filter((c) => isAdvisoryDoctorCheck(c));
|
|
16191
|
+
if (input.shouldApply) {
|
|
16192
|
+
if (hardGaps.length > 0) {
|
|
16193
|
+
return `\u2717 ${hardGaps.length} repair${hardGaps.length === 1 ? "" : "s"} failed \u2014 see the items above.`;
|
|
16194
|
+
}
|
|
16195
|
+
if (softGaps.length > 0) {
|
|
16196
|
+
return `\u26A0 Healed what I could; ${softGaps.length} item${softGaps.length === 1 ? "" : "s"} still need${softGaps.length === 1 ? "s" : ""} attention (above).`;
|
|
16197
|
+
}
|
|
16198
|
+
const healed = input.healedCount ?? 0;
|
|
16199
|
+
if (healed > 0) return `\u2713 Healed ${healed} item${healed === 1 ? "" : "s"} \u2014 everything checks out now.`;
|
|
16200
|
+
return "\u2713 Everything healthy \u2014 nothing needed fixing.";
|
|
15852
16201
|
}
|
|
15853
|
-
const
|
|
15854
|
-
|
|
15855
|
-
|
|
15856
|
-
|
|
16202
|
+
const attention = gaps.length;
|
|
16203
|
+
if (attention > 0) {
|
|
16204
|
+
return `\u26A0 ${attention} item${attention === 1 ? " needs" : "s need"} attention \u2014 run \`mmi-cli doctor --apply\` to heal.`;
|
|
16205
|
+
}
|
|
16206
|
+
return "\u2713 Everything healthy.";
|
|
16207
|
+
}
|
|
16208
|
+
function doctorHumanLines(input) {
|
|
16209
|
+
const gaps = input.checks.filter((c) => !c.ok);
|
|
16210
|
+
const lines = [
|
|
16211
|
+
doctorVerdictLine({ checks: input.checks, shouldApply: input.shouldApply, healedCount: input.healedCount }),
|
|
16212
|
+
"",
|
|
16213
|
+
"Checks",
|
|
16214
|
+
...input.checks.map((c) => ` ${doctorCheckGlyph(c)} ${c.label}`)
|
|
16215
|
+
];
|
|
16216
|
+
if (gaps.length > 0) {
|
|
16217
|
+
lines.push("", input.shouldApply ? "Repairs" : "Will fix on --apply");
|
|
16218
|
+
for (const c of gaps) {
|
|
16219
|
+
const mark = input.shouldApply ? isAdvisoryDoctorCheck(c) ? "\u26A0" : "\u2717" : "\u2192";
|
|
16220
|
+
lines.push(` ${mark} ${c.label} \u2014 ${c.fix}`);
|
|
16221
|
+
}
|
|
16222
|
+
}
|
|
16223
|
+
if (input.shouldApply && input.pluginReloadRequired) {
|
|
16224
|
+
lines.push("", `\u21BB ${input.reloadHint ?? "reload"} so the healed plugin/MCP installs load.`);
|
|
15857
16225
|
}
|
|
15858
16226
|
const stale = renderPluginUpdateReportStaleOnly(input.updateReport);
|
|
15859
16227
|
if (stale.length) {
|
|
15860
|
-
lines.push("");
|
|
15861
|
-
|
|
16228
|
+
lines.push("", ...stale);
|
|
16229
|
+
} else if (!input.verbose) {
|
|
16230
|
+
lines.push("", DOCTOR_VERBOSE_HINT);
|
|
16231
|
+
}
|
|
16232
|
+
if (!input.verbose) return lines;
|
|
16233
|
+
lines.push("", ...renderPluginUpdateReport(input.updateReport));
|
|
16234
|
+
lines.push(
|
|
16235
|
+
"",
|
|
16236
|
+
doctorSummaryLine({ checks: input.checks, updateReport: input.updateReport, shouldApply: input.shouldApply, healedCount: input.healedCount }),
|
|
16237
|
+
...doctorAuditLines({
|
|
16238
|
+
checks: input.checks,
|
|
16239
|
+
shouldApply: input.shouldApply,
|
|
16240
|
+
healedCount: input.healedCount,
|
|
16241
|
+
cliVersion: input.updateReport.versions.cli,
|
|
16242
|
+
releasedVersion: input.updateReport.versions.released
|
|
16243
|
+
})
|
|
16244
|
+
);
|
|
16245
|
+
return lines;
|
|
16246
|
+
}
|
|
16247
|
+
function doctorSummaryLine(input) {
|
|
16248
|
+
const gaps = input.checks.filter((c) => !c.ok);
|
|
16249
|
+
const mode = input.shouldApply ? "apply" : "plan";
|
|
16250
|
+
const released = input.updateReport.versions.released ?? "unknown";
|
|
16251
|
+
const failed = gaps.filter((c) => !isAdvisoryDoctorCheck(c)).length;
|
|
16252
|
+
const countSummary = input.shouldApply ? `${input.healedCount ?? 0} healed` : `${gaps.length} planned`;
|
|
16253
|
+
return `summary: mode ${mode} \xB7 released ${released} \xB7 ${countSummary} \xB7 ${failed} failed`;
|
|
16254
|
+
}
|
|
16255
|
+
function doctorAuditLines(input) {
|
|
16256
|
+
const gaps = input.checks.filter((c) => !c.ok);
|
|
16257
|
+
const failed = gaps.filter((c) => !isAdvisoryDoctorCheck(c)).length;
|
|
16258
|
+
const lines = [
|
|
16259
|
+
"MMI doctor audit",
|
|
16260
|
+
`mode: ${input.shouldApply ? "apply" : "plan"}`,
|
|
16261
|
+
`cli: ${input.cliVersion ?? "unknown"}`,
|
|
16262
|
+
`released: ${input.releasedVersion ?? "unknown"}`,
|
|
16263
|
+
`checks: ${input.checks.length} (${input.checks.length - gaps.length} ok, ${gaps.length} gap)`,
|
|
16264
|
+
`planned actions: ${gaps.length}`,
|
|
16265
|
+
`healed actions: ${input.healedCount ?? 0}`,
|
|
16266
|
+
`failed actions: ${failed}`
|
|
16267
|
+
];
|
|
16268
|
+
if (gaps.length) {
|
|
16269
|
+
lines.push("actions:");
|
|
16270
|
+
for (const c of gaps) {
|
|
16271
|
+
lines.push(`[${isAdvisoryDoctorCheck(c) ? "advisory" : "auto"}] ${c.label}: ${c.fix}`);
|
|
16272
|
+
}
|
|
15862
16273
|
}
|
|
15863
|
-
lines.push("");
|
|
15864
|
-
lines.push(`\u26A0 ${input.gaps.length} item(s) need attention \u2014 ${DOCTOR_VERBOSE_HINT}`);
|
|
15865
16274
|
return lines;
|
|
15866
16275
|
}
|
|
15867
16276
|
var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
|
|
@@ -15894,6 +16303,262 @@ function buildPluginResolvabilityCheck(input) {
|
|
|
15894
16303
|
}
|
|
15895
16304
|
}
|
|
15896
16305
|
|
|
16306
|
+
// src/mcp-reconcile.ts
|
|
16307
|
+
var PLAYWRIGHT_MCP_SPEC = {
|
|
16308
|
+
name: "playwright",
|
|
16309
|
+
command: "npx",
|
|
16310
|
+
args: ["-y", "@playwright/mcp@latest", "--output-dir", "tmp/playwright-mcp"],
|
|
16311
|
+
legacyNames: []
|
|
16312
|
+
};
|
|
16313
|
+
var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
|
|
16314
|
+
var MCP_RECONCILE_FIX = "register/reconcile the org Playwright MCP server (npx -y @playwright/mcp@latest --output-dir tmp/playwright-mcp) \u2014 run `mmi-cli doctor --apply`; see skills/browser-automation/SKILL.md and bootstrap seed mcp-playwright.template.json";
|
|
16315
|
+
function extractServerBlock(content, format, spec) {
|
|
16316
|
+
return format === "json" ? extractJsonServer(content, spec) : extractTomlServer(content, spec);
|
|
16317
|
+
}
|
|
16318
|
+
function extractJsonServer(content, spec) {
|
|
16319
|
+
let parsed;
|
|
16320
|
+
try {
|
|
16321
|
+
parsed = JSON.parse(content);
|
|
16322
|
+
} catch {
|
|
16323
|
+
return { kind: "unparseable" };
|
|
16324
|
+
}
|
|
16325
|
+
if (parsed == null || typeof parsed !== "object") return { kind: "unparseable" };
|
|
16326
|
+
const servers = parsed.mcpServers;
|
|
16327
|
+
if (servers == null || typeof servers !== "object") return { kind: "absent" };
|
|
16328
|
+
const bag = servers;
|
|
16329
|
+
for (const name of [spec.name, ...spec.legacyNames]) {
|
|
16330
|
+
if (Object.prototype.hasOwnProperty.call(bag, name)) {
|
|
16331
|
+
return { kind: "found", matchedName: name, isLegacy: name !== spec.name, blockText: JSON.stringify(bag[name]) };
|
|
16332
|
+
}
|
|
16333
|
+
}
|
|
16334
|
+
return { kind: "absent" };
|
|
16335
|
+
}
|
|
16336
|
+
function extractTomlServer(content, spec) {
|
|
16337
|
+
const blocks = parseTomlBlocks(content.replace(/\r\n/g, "\n").split("\n"));
|
|
16338
|
+
for (const name of [spec.name, ...spec.legacyNames]) {
|
|
16339
|
+
const full = `mcp_servers.${name}`;
|
|
16340
|
+
const block = blocks.find((b) => b.name === full && !b.isArrayTable);
|
|
16341
|
+
if (block) {
|
|
16342
|
+
const blockText = [block.headerRaw, ...block.body].filter((l) => l != null).join("\n");
|
|
16343
|
+
return { kind: "found", matchedName: name, isLegacy: name !== spec.name, blockText };
|
|
16344
|
+
}
|
|
16345
|
+
}
|
|
16346
|
+
return { kind: "absent" };
|
|
16347
|
+
}
|
|
16348
|
+
function detectMcpState(target, spec) {
|
|
16349
|
+
if (!target.present) return { target, state: "skip", action: "skip", reasons: [] };
|
|
16350
|
+
if (target.content == null) {
|
|
16351
|
+
if (target.createWhenFileAbsent) return { target, state: "absent", action: "install", reasons: [] };
|
|
16352
|
+
return { target, state: "skip", action: "skip", reasons: [] };
|
|
16353
|
+
}
|
|
16354
|
+
const extract = extractServerBlock(target.content, target.format, spec);
|
|
16355
|
+
if (extract.kind === "unparseable") {
|
|
16356
|
+
return { target, state: "drifted", action: "reconcile", reasons: ["unparseable"] };
|
|
16357
|
+
}
|
|
16358
|
+
if (extract.kind === "absent") {
|
|
16359
|
+
return { target, state: "absent", action: "install", reasons: [] };
|
|
16360
|
+
}
|
|
16361
|
+
const reasons = [];
|
|
16362
|
+
if (extract.isLegacy) reasons.push("legacy-name");
|
|
16363
|
+
if (textHasPlaywrightVisionCap(extract.blockText)) reasons.push("vision-cap");
|
|
16364
|
+
if (!textHasCanonicalPlaywrightOutputDir(extract.blockText)) reasons.push("output-dir");
|
|
16365
|
+
if (reasons.length === 0) {
|
|
16366
|
+
return { target, state: "healthy", action: "none", reasons: [], matchedName: extract.matchedName };
|
|
16367
|
+
}
|
|
16368
|
+
return { target, state: "drifted", action: "reconcile", reasons, matchedName: extract.matchedName };
|
|
16369
|
+
}
|
|
16370
|
+
function planMcpReconcile(targets, spec) {
|
|
16371
|
+
return targets.map((t) => detectMcpState(t, spec));
|
|
16372
|
+
}
|
|
16373
|
+
function buildMcpReconcileCheck(plan) {
|
|
16374
|
+
const base = {
|
|
16375
|
+
ok: true,
|
|
16376
|
+
label: MCP_RECONCILE_LABEL,
|
|
16377
|
+
fix: MCP_RECONCILE_FIX,
|
|
16378
|
+
severityOverride: "advisory"
|
|
16379
|
+
};
|
|
16380
|
+
const gaps = plan.filter((p) => p.action === "install" || p.action === "reconcile");
|
|
16381
|
+
if (gaps.length === 0) return base;
|
|
16382
|
+
const detail = gaps.map((g) => `${g.target.path} (${g.action === "install" ? "missing" : g.reasons.join("+") || "drifted"})`).join(", ");
|
|
16383
|
+
return {
|
|
16384
|
+
...base,
|
|
16385
|
+
ok: false,
|
|
16386
|
+
fix: `${MCP_RECONCILE_FIX} \u2014 ${detail}`,
|
|
16387
|
+
mcpGaps: gaps.map((g) => ({ path: g.target.path, action: g.action, reasons: g.reasons }))
|
|
16388
|
+
};
|
|
16389
|
+
}
|
|
16390
|
+
function applyMcpReconcile(targets, spec, opts) {
|
|
16391
|
+
const results = [];
|
|
16392
|
+
for (const item of planMcpReconcile(targets, spec)) {
|
|
16393
|
+
if (item.action !== "install" && item.action !== "reconcile") continue;
|
|
16394
|
+
const t = item.target;
|
|
16395
|
+
if (t.isRepoFile && !opts.repoWritesAllowed) {
|
|
16396
|
+
results.push({ target: t, action: item.action, outcome: "skipped-repo-write" });
|
|
16397
|
+
continue;
|
|
16398
|
+
}
|
|
16399
|
+
const next = t.format === "json" ? renderJsonMcpConfig(t.content, spec) : renderTomlMcpConfig(t.content, spec);
|
|
16400
|
+
if (next == null) {
|
|
16401
|
+
results.push({ target: t, action: item.action, outcome: "unparseable" });
|
|
16402
|
+
continue;
|
|
16403
|
+
}
|
|
16404
|
+
results.push({ target: t, action: item.action, outcome: opts.write(t.path, next) ? "wrote" : "write-failed" });
|
|
16405
|
+
}
|
|
16406
|
+
return results;
|
|
16407
|
+
}
|
|
16408
|
+
function renderJsonMcpConfig(existing, spec) {
|
|
16409
|
+
const canonicalEntry = { command: spec.command, args: [...spec.args] };
|
|
16410
|
+
if (existing == null || existing.trim() === "") {
|
|
16411
|
+
return `${JSON.stringify({ mcpServers: { [spec.name]: canonicalEntry } }, null, 2)}
|
|
16412
|
+
`;
|
|
16413
|
+
}
|
|
16414
|
+
let parsed;
|
|
16415
|
+
try {
|
|
16416
|
+
parsed = JSON.parse(existing);
|
|
16417
|
+
} catch {
|
|
16418
|
+
return null;
|
|
16419
|
+
}
|
|
16420
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
16421
|
+
const obj = parsed;
|
|
16422
|
+
const rawServers = obj.mcpServers;
|
|
16423
|
+
if (rawServers != null && (typeof rawServers !== "object" || Array.isArray(rawServers))) return null;
|
|
16424
|
+
const servers = rawServers ?? {};
|
|
16425
|
+
const prior = servers[spec.name] ?? spec.legacyNames.map((n) => servers[n]).find((v) => v != null);
|
|
16426
|
+
for (const legacy of spec.legacyNames) delete servers[legacy];
|
|
16427
|
+
const priorObj = prior != null && typeof prior === "object" && !Array.isArray(prior) ? prior : {};
|
|
16428
|
+
servers[spec.name] = { ...priorObj, ...canonicalEntry };
|
|
16429
|
+
obj.mcpServers = servers;
|
|
16430
|
+
const text = `${JSON.stringify(obj, null, 2)}
|
|
16431
|
+
`;
|
|
16432
|
+
return existing.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
|
|
16433
|
+
}
|
|
16434
|
+
function isTomlEscaped(line, i) {
|
|
16435
|
+
let n = 0;
|
|
16436
|
+
for (let j = i - 1; j >= 0 && line[j] === "\\"; j--) n += 1;
|
|
16437
|
+
return n % 2 === 1;
|
|
16438
|
+
}
|
|
16439
|
+
function advanceTomlStringState(line, state) {
|
|
16440
|
+
let multi = state;
|
|
16441
|
+
let single = null;
|
|
16442
|
+
let i = 0;
|
|
16443
|
+
while (i < line.length) {
|
|
16444
|
+
if (multi != null) {
|
|
16445
|
+
if (line.startsWith(multi, i) && !(multi === '"""' && isTomlEscaped(line, i))) {
|
|
16446
|
+
multi = null;
|
|
16447
|
+
i += 3;
|
|
16448
|
+
} else {
|
|
16449
|
+
i += 1;
|
|
16450
|
+
}
|
|
16451
|
+
continue;
|
|
16452
|
+
}
|
|
16453
|
+
if (single != null) {
|
|
16454
|
+
if (line[i] === single && !(single === '"' && isTomlEscaped(line, i))) single = null;
|
|
16455
|
+
i += 1;
|
|
16456
|
+
continue;
|
|
16457
|
+
}
|
|
16458
|
+
if (line[i] === "#") break;
|
|
16459
|
+
if (line.startsWith('"""', i)) {
|
|
16460
|
+
multi = '"""';
|
|
16461
|
+
i += 3;
|
|
16462
|
+
continue;
|
|
16463
|
+
}
|
|
16464
|
+
if (line.startsWith("'''", i)) {
|
|
16465
|
+
multi = "'''";
|
|
16466
|
+
i += 3;
|
|
16467
|
+
continue;
|
|
16468
|
+
}
|
|
16469
|
+
if (line[i] === '"' || line[i] === "'") {
|
|
16470
|
+
single = line[i];
|
|
16471
|
+
i += 1;
|
|
16472
|
+
continue;
|
|
16473
|
+
}
|
|
16474
|
+
i += 1;
|
|
16475
|
+
}
|
|
16476
|
+
return multi;
|
|
16477
|
+
}
|
|
16478
|
+
function parseTomlBlocks(lines) {
|
|
16479
|
+
const blocks = [];
|
|
16480
|
+
let current = { headerRaw: null, name: null, isArrayTable: false, body: [] };
|
|
16481
|
+
let multi = null;
|
|
16482
|
+
const tableRe = /^\s*\[\s*([^[\]]+?)\s*\]\s*$/;
|
|
16483
|
+
const arrayTableRe = /^\s*\[\[\s*([^[\]]+?)\s*\]\]\s*$/;
|
|
16484
|
+
for (const line of lines) {
|
|
16485
|
+
if (multi == null) {
|
|
16486
|
+
const am = arrayTableRe.exec(line);
|
|
16487
|
+
const tm = am == null ? tableRe.exec(line) : null;
|
|
16488
|
+
if (am != null || tm != null) {
|
|
16489
|
+
blocks.push(current);
|
|
16490
|
+
current = { headerRaw: line, name: (am ?? tm)[1].trim(), isArrayTable: am != null, body: [] };
|
|
16491
|
+
continue;
|
|
16492
|
+
}
|
|
16493
|
+
}
|
|
16494
|
+
multi = advanceTomlStringState(line, multi);
|
|
16495
|
+
current.body.push(line);
|
|
16496
|
+
}
|
|
16497
|
+
blocks.push(current);
|
|
16498
|
+
return blocks;
|
|
16499
|
+
}
|
|
16500
|
+
function mergeCanonicalTomlBody(body, commandLine, argsLine) {
|
|
16501
|
+
const preserved = [];
|
|
16502
|
+
for (let i = 0; i < body.length; i++) {
|
|
16503
|
+
const line = body[i];
|
|
16504
|
+
const trimmed = line.trim();
|
|
16505
|
+
if (/^command\s*=/.test(trimmed)) continue;
|
|
16506
|
+
if (/^args\s*=/.test(trimmed)) {
|
|
16507
|
+
let open = (line.match(/\[/g) ?? []).length;
|
|
16508
|
+
let close = (line.match(/\]/g) ?? []).length;
|
|
16509
|
+
while (open > close && i + 1 < body.length) {
|
|
16510
|
+
i += 1;
|
|
16511
|
+
open += (body[i].match(/\[/g) ?? []).length;
|
|
16512
|
+
close += (body[i].match(/\]/g) ?? []).length;
|
|
16513
|
+
}
|
|
16514
|
+
continue;
|
|
16515
|
+
}
|
|
16516
|
+
preserved.push(line);
|
|
16517
|
+
}
|
|
16518
|
+
return [commandLine, argsLine, ...preserved];
|
|
16519
|
+
}
|
|
16520
|
+
function renderTomlMcpConfig(existing, spec) {
|
|
16521
|
+
const commandLine = `command = ${JSON.stringify(spec.command)}`;
|
|
16522
|
+
const argsLine = `args = [${spec.args.map((a) => JSON.stringify(a)).join(", ")}]`;
|
|
16523
|
+
const canonicalHeader = `[mcp_servers.${spec.name}]`;
|
|
16524
|
+
const canonicalName = `mcp_servers.${spec.name}`;
|
|
16525
|
+
if (existing == null || existing.trim() === "") {
|
|
16526
|
+
return `${canonicalHeader}
|
|
16527
|
+
${commandLine}
|
|
16528
|
+
${argsLine}
|
|
16529
|
+
`;
|
|
16530
|
+
}
|
|
16531
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
16532
|
+
const lines = existing.replace(/\r\n/g, "\n").split("\n");
|
|
16533
|
+
const legacyFull = new Set(spec.legacyNames.map((n) => `mcp_servers.${n}`));
|
|
16534
|
+
const all = parseTomlBlocks(lines);
|
|
16535
|
+
if (all.some((b) => b.isArrayTable && b.name != null && (b.name === canonicalName || legacyFull.has(b.name)))) return null;
|
|
16536
|
+
const targetBlock = all.find((b) => !b.isArrayTable && b.name === canonicalName) ?? all.find((b) => !b.isArrayTable && b.name != null && legacyFull.has(b.name));
|
|
16537
|
+
const out = [];
|
|
16538
|
+
for (const b of all) {
|
|
16539
|
+
if (b === targetBlock) {
|
|
16540
|
+
out.push({ headerRaw: canonicalHeader, name: canonicalName, isArrayTable: false, body: mergeCanonicalTomlBody(b.body, commandLine, argsLine) });
|
|
16541
|
+
continue;
|
|
16542
|
+
}
|
|
16543
|
+
if (targetBlock != null && !b.isArrayTable && b.name != null && legacyFull.has(b.name)) continue;
|
|
16544
|
+
out.push(b);
|
|
16545
|
+
}
|
|
16546
|
+
if (targetBlock != null) {
|
|
16547
|
+
const joined = out.flatMap((b) => b.headerRaw != null ? [b.headerRaw, ...b.body] : b.body).join("\n");
|
|
16548
|
+
return eol === "\r\n" ? joined.replace(/\n/g, "\r\n") : joined;
|
|
16549
|
+
}
|
|
16550
|
+
const base = out.flatMap((b) => b.headerRaw != null ? [b.headerRaw, ...b.body] : b.body).join("\n").replace(/\n+$/, "");
|
|
16551
|
+
const block = `${canonicalHeader}
|
|
16552
|
+
${commandLine}
|
|
16553
|
+
${argsLine}`;
|
|
16554
|
+
const combined = base === "" ? `${block}
|
|
16555
|
+
` : `${base}
|
|
16556
|
+
|
|
16557
|
+
${block}
|
|
16558
|
+
`;
|
|
16559
|
+
return eol === "\r\n" ? combined.replace(/\n/g, "\r\n") : combined;
|
|
16560
|
+
}
|
|
16561
|
+
|
|
15897
16562
|
// src/cli-doctor-shared.ts
|
|
15898
16563
|
var import_node_fs15 = require("node:fs");
|
|
15899
16564
|
var import_node_path16 = require("node:path");
|
|
@@ -16150,7 +16815,20 @@ var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
|
16150
16815
|
function runHostBin(bin, args, opts) {
|
|
16151
16816
|
return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
|
|
16152
16817
|
}
|
|
16153
|
-
function
|
|
16818
|
+
function reexecStrategies(input) {
|
|
16819
|
+
const strategies = [
|
|
16820
|
+
input.platform === "win32" ? { command: "cmd.exe", args: ["/c", "mmi-cli"], kind: "cmd-shim" } : { command: "mmi-cli", args: [], kind: "direct" }
|
|
16821
|
+
];
|
|
16822
|
+
if (input.argv1) strategies.push({ command: input.execPath, args: [input.argv1], kind: "direct" });
|
|
16823
|
+
return strategies;
|
|
16824
|
+
}
|
|
16825
|
+
var CMD_NOT_RECOGNIZED_EXIT_CODE = 9009;
|
|
16826
|
+
function reexecAttemptOutcome(strategy, code) {
|
|
16827
|
+
if (code < 0) return "strategy-failed";
|
|
16828
|
+
if (strategy.kind === "cmd-shim" && code === CMD_NOT_RECOGNIZED_EXIT_CODE) return "strategy-failed";
|
|
16829
|
+
return "result";
|
|
16830
|
+
}
|
|
16831
|
+
function spawnReexecAttempt(command, args, env) {
|
|
16154
16832
|
return new Promise((resolve6) => {
|
|
16155
16833
|
let settled = false;
|
|
16156
16834
|
const done = (code) => {
|
|
@@ -16159,12 +16837,20 @@ function reexecMmiCli(args) {
|
|
|
16159
16837
|
resolve6(code);
|
|
16160
16838
|
}
|
|
16161
16839
|
};
|
|
16162
|
-
const
|
|
16163
|
-
const child = isWin ? (0, import_node_child_process10.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", env }) : (0, import_node_child_process10.spawn)("mmi-cli", args, { stdio: "inherit", env });
|
|
16840
|
+
const child = (0, import_node_child_process10.spawn)(command, args, { stdio: "inherit", env });
|
|
16164
16841
|
child.on("error", () => done(-1));
|
|
16165
16842
|
child.on("exit", (code) => done(code ?? 0));
|
|
16166
16843
|
});
|
|
16167
16844
|
}
|
|
16845
|
+
async function reexecMmiCli(args) {
|
|
16846
|
+
const strategies = reexecStrategies({ platform: process.platform, argv1: process.argv[1], execPath: process.execPath });
|
|
16847
|
+
const env = { ...process.env, [DOCTOR_POST_SELF_UPDATE_ENV]: "1" };
|
|
16848
|
+
for (const strategy of strategies) {
|
|
16849
|
+
const code = await spawnReexecAttempt(strategy.command, [...strategy.args, ...args], env);
|
|
16850
|
+
if (reexecAttemptOutcome(strategy, code) === "result") return code;
|
|
16851
|
+
}
|
|
16852
|
+
return -1;
|
|
16853
|
+
}
|
|
16168
16854
|
function hostBinAvailable(bin) {
|
|
16169
16855
|
return execFileP2(isWin ? "where" : "which", [bin]).then(() => true).catch(() => false);
|
|
16170
16856
|
}
|
|
@@ -16184,6 +16870,15 @@ async function runCodexPlugin(args) {
|
|
|
16184
16870
|
return false;
|
|
16185
16871
|
}
|
|
16186
16872
|
}
|
|
16873
|
+
async function codexEnabledMmiVersion() {
|
|
16874
|
+
if (!await hostBinAvailable("codex")) return void 0;
|
|
16875
|
+
try {
|
|
16876
|
+
const { stdout } = await runHostBin("codex", ["plugin", "list", "--json"], { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
16877
|
+
return codexEnabledPluginVersionFromList(stdout, MMI_PLUGIN_ID);
|
|
16878
|
+
} catch {
|
|
16879
|
+
return void 0;
|
|
16880
|
+
}
|
|
16881
|
+
}
|
|
16187
16882
|
async function marketplaceAddRefSupported(bin) {
|
|
16188
16883
|
try {
|
|
16189
16884
|
const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
|
|
@@ -16246,6 +16941,77 @@ function installedPluginSources() {
|
|
|
16246
16941
|
}
|
|
16247
16942
|
});
|
|
16248
16943
|
}
|
|
16944
|
+
function managedPluginSurfaceSources() {
|
|
16945
|
+
const claudeCodex = installedPluginSources().map(({ surface, installed }) => {
|
|
16946
|
+
const cacheVersions = {};
|
|
16947
|
+
for (const descriptor of MANAGED_PLUGINS) {
|
|
16948
|
+
const segments = parseManagedPluginId(descriptor.id);
|
|
16949
|
+
if (!segments) continue;
|
|
16950
|
+
try {
|
|
16951
|
+
cacheVersions[descriptor.id] = (0, import_node_fs17.readdirSync)(
|
|
16952
|
+
(0, import_node_path17.join)((0, import_node_os6.homedir)(), `.${surface}`, "plugins", "cache", segments.marketplace, segments.name),
|
|
16953
|
+
{ withFileTypes: true }
|
|
16954
|
+
).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
16955
|
+
} catch {
|
|
16956
|
+
}
|
|
16957
|
+
}
|
|
16958
|
+
return { surface, installed, cacheVersions };
|
|
16959
|
+
});
|
|
16960
|
+
return [
|
|
16961
|
+
...claudeCodex,
|
|
16962
|
+
{ surface: "cursor", directVersions: managedPluginCursorDirectVersions() },
|
|
16963
|
+
{ surface: "opencode", directVersions: managedPluginOpencodeDirectVersions() }
|
|
16964
|
+
];
|
|
16965
|
+
}
|
|
16966
|
+
function managedPluginCursorDirectVersions() {
|
|
16967
|
+
const out = {};
|
|
16968
|
+
for (const descriptor of MANAGED_PLUGINS) {
|
|
16969
|
+
const segments = parseManagedPluginId(descriptor.id);
|
|
16970
|
+
if (!segments) continue;
|
|
16971
|
+
try {
|
|
16972
|
+
const versions = (0, import_node_fs17.readdirSync)(
|
|
16973
|
+
(0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cursor", "plugins", "cache", segments.marketplace, segments.name),
|
|
16974
|
+
{ withFileTypes: true }
|
|
16975
|
+
).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
16976
|
+
out[descriptor.id] = highestSemver(versions);
|
|
16977
|
+
} catch {
|
|
16978
|
+
}
|
|
16979
|
+
}
|
|
16980
|
+
return out;
|
|
16981
|
+
}
|
|
16982
|
+
function managedPluginOpencodeDirectVersions() {
|
|
16983
|
+
const out = {};
|
|
16984
|
+
for (const descriptor of MANAGED_PLUGINS) {
|
|
16985
|
+
if (!descriptor.opencodePackage) continue;
|
|
16986
|
+
out[descriptor.id] = readOpencodePackageVersion(descriptor.opencodePackage);
|
|
16987
|
+
}
|
|
16988
|
+
return out;
|
|
16989
|
+
}
|
|
16990
|
+
function readOpencodePackageVersionFrom(packageJsonPath) {
|
|
16991
|
+
try {
|
|
16992
|
+
const parsed = JSON.parse((0, import_node_fs17.readFileSync)(packageJsonPath, "utf8"));
|
|
16993
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
|
|
16994
|
+
} catch {
|
|
16995
|
+
return void 0;
|
|
16996
|
+
}
|
|
16997
|
+
}
|
|
16998
|
+
function readOpencodePackageVersion(packageName) {
|
|
16999
|
+
const [scope, name] = packageName.startsWith("@") ? packageName.slice(1).split("/", 2) : [void 0, packageName];
|
|
17000
|
+
if (!name) return void 0;
|
|
17001
|
+
const diskCandidates = [
|
|
17002
|
+
(0, import_node_path17.join)(opencodeConfigDir(), "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json"),
|
|
17003
|
+
(0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json")
|
|
17004
|
+
];
|
|
17005
|
+
const diskVersion = diskCandidates.map(readOpencodePackageVersionFrom).find((v) => v !== void 0);
|
|
17006
|
+
const packagesRoot = (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "packages", ...scope ? [`@${scope}`] : []);
|
|
17007
|
+
let cacheVersion;
|
|
17008
|
+
try {
|
|
17009
|
+
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));
|
|
17010
|
+
if (cacheVersions.length) cacheVersion = cacheVersions.reduce((lowest, v) => compareVersions(v, lowest) < 0 ? v : lowest);
|
|
17011
|
+
} catch {
|
|
17012
|
+
}
|
|
17013
|
+
return pickOpencodeActiveVersion({ cacheVersion, diskVersion });
|
|
17014
|
+
}
|
|
16249
17015
|
function readClaudeSettings() {
|
|
16250
17016
|
try {
|
|
16251
17017
|
return JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path17.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
|
|
@@ -16460,6 +17226,12 @@ function opencodeInstalledVersionForDoctor() {
|
|
|
16460
17226
|
diskVersion: readOpencodeAdapterDiskVersion()
|
|
16461
17227
|
});
|
|
16462
17228
|
}
|
|
17229
|
+
function opencodePersistedVersionForDoctor() {
|
|
17230
|
+
return pickOpencodeActiveVersion({
|
|
17231
|
+
cacheVersion: readOpencodeLoadedCacheVersion(),
|
|
17232
|
+
diskVersion: readOpencodeAdapterDiskVersion()
|
|
17233
|
+
});
|
|
17234
|
+
}
|
|
16463
17235
|
function opencodePluginVersionsForReport() {
|
|
16464
17236
|
return [process.env.MMI_OPENCODE_PLUGIN_VERSION, readOpencodeAdapterDiskVersion()].filter((v) => Boolean(v));
|
|
16465
17237
|
}
|
|
@@ -16577,8 +17349,8 @@ function hasNestedMmiChild(versionDir) {
|
|
|
16577
17349
|
}
|
|
16578
17350
|
}
|
|
16579
17351
|
function nestedPluginTreeSnapshot() {
|
|
16580
|
-
return mmiPluginCacheRootSnapshots().
|
|
16581
|
-
(root) => root.entries.filter((e) => e.isDirectory && /^v?\d+\.\d+\.\d+/.test(e.name)).map((e) => ({ surface:
|
|
17352
|
+
return mmiPluginCacheRootSnapshots().flatMap(
|
|
17353
|
+
(root) => root.entries.filter((e) => e.isDirectory && /^v?\d+\.\d+\.\d+/.test(e.name)).map((e) => ({ surface: root.surface, path: e.path, nested: hasNestedMmiChild(e.path) }))
|
|
16582
17354
|
);
|
|
16583
17355
|
}
|
|
16584
17356
|
function uniqueQuarantineTarget(path2) {
|
|
@@ -16654,21 +17426,95 @@ function readTextFile(path2) {
|
|
|
16654
17426
|
return null;
|
|
16655
17427
|
}
|
|
16656
17428
|
}
|
|
16657
|
-
function
|
|
17429
|
+
function mcpDirExists(path2) {
|
|
17430
|
+
try {
|
|
17431
|
+
return (0, import_node_fs17.existsSync)(path2);
|
|
17432
|
+
} catch {
|
|
17433
|
+
return false;
|
|
17434
|
+
}
|
|
17435
|
+
}
|
|
17436
|
+
function mcpConfigTargets() {
|
|
16658
17437
|
const cwd = process.cwd();
|
|
16659
17438
|
const home = (0, import_node_os6.homedir)();
|
|
16660
|
-
const
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
17439
|
+
const cursorProjectDir = (0, import_node_path17.join)(cwd, ".cursor");
|
|
17440
|
+
const cursorUserDir = (0, import_node_path17.join)(home, ".cursor");
|
|
17441
|
+
const codexDir = (0, import_node_path17.join)(home, ".codex");
|
|
17442
|
+
return [
|
|
17443
|
+
// Claude Code project MCP — reconciled if present, never conjured (org seeds .cursor/mcp.json, not this).
|
|
17444
|
+
{
|
|
17445
|
+
host: "claude-code",
|
|
17446
|
+
label: "Claude Code project MCP",
|
|
17447
|
+
path: (0, import_node_path17.join)(cwd, ".mcp.json"),
|
|
17448
|
+
format: "json",
|
|
17449
|
+
present: true,
|
|
17450
|
+
// parent is the repo root (cwd); the caller gates the whole reconcile on isOrgRepo
|
|
17451
|
+
isRepoFile: true,
|
|
17452
|
+
createWhenFileAbsent: false,
|
|
17453
|
+
content: readTextFile((0, import_node_path17.join)(cwd, ".mcp.json"))
|
|
17454
|
+
},
|
|
17455
|
+
// Cursor project MCP — the bootstrap-seeded surface; restored if its .cursor dir exists but the file is gone.
|
|
17456
|
+
{
|
|
17457
|
+
host: "cursor-project",
|
|
17458
|
+
label: "Cursor project MCP",
|
|
17459
|
+
path: (0, import_node_path17.join)(cursorProjectDir, "mcp.json"),
|
|
17460
|
+
format: "json",
|
|
17461
|
+
present: mcpDirExists(cursorProjectDir),
|
|
17462
|
+
isRepoFile: true,
|
|
17463
|
+
createWhenFileAbsent: true,
|
|
17464
|
+
content: readTextFile((0, import_node_path17.join)(cursorProjectDir, "mcp.json"))
|
|
17465
|
+
},
|
|
17466
|
+
// Cursor user MCP — global; written only when Cursor is installed (~/.cursor exists).
|
|
17467
|
+
{
|
|
17468
|
+
host: "cursor-user",
|
|
17469
|
+
label: "Cursor user MCP",
|
|
17470
|
+
path: (0, import_node_path17.join)(cursorUserDir, "mcp.json"),
|
|
17471
|
+
format: "json",
|
|
17472
|
+
present: mcpDirExists(cursorUserDir),
|
|
17473
|
+
isRepoFile: false,
|
|
17474
|
+
createWhenFileAbsent: true,
|
|
17475
|
+
content: readTextFile((0, import_node_path17.join)(cursorUserDir, "mcp.json"))
|
|
17476
|
+
},
|
|
17477
|
+
// Codex user config (TOML) — global; written only when Codex is installed (~/.codex exists).
|
|
17478
|
+
{
|
|
17479
|
+
host: "codex",
|
|
17480
|
+
label: "Codex user config",
|
|
17481
|
+
path: (0, import_node_path17.join)(codexDir, "config.toml"),
|
|
17482
|
+
format: "toml",
|
|
17483
|
+
present: mcpDirExists(codexDir),
|
|
17484
|
+
isRepoFile: false,
|
|
17485
|
+
createWhenFileAbsent: true,
|
|
17486
|
+
content: readTextFile((0, import_node_path17.join)(codexDir, "config.toml"))
|
|
17487
|
+
}
|
|
16665
17488
|
];
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
17489
|
+
}
|
|
17490
|
+
function writeMcpConfigFile(path2, content) {
|
|
17491
|
+
try {
|
|
17492
|
+
(0, import_node_fs17.writeFileSync)(path2, content, "utf8");
|
|
17493
|
+
return true;
|
|
17494
|
+
} catch {
|
|
17495
|
+
return false;
|
|
16670
17496
|
}
|
|
16671
|
-
|
|
17497
|
+
}
|
|
17498
|
+
async function reconcileMcpRegistrations(input) {
|
|
17499
|
+
if (!input.isOrgRepo) return buildMcpReconcileCheck([]);
|
|
17500
|
+
const targets = mcpConfigTargets();
|
|
17501
|
+
if (!input.apply) return buildMcpReconcileCheck(planMcpReconcile(targets, PLAYWRIGHT_MCP_SPEC));
|
|
17502
|
+
const results = applyMcpReconcile(targets, PLAYWRIGHT_MCP_SPEC, {
|
|
17503
|
+
repoWritesAllowed: input.repoWritesAllowed,
|
|
17504
|
+
write: writeMcpConfigFile
|
|
17505
|
+
});
|
|
17506
|
+
for (const r of results) {
|
|
17507
|
+
if (r.outcome === "wrote") {
|
|
17508
|
+
input.onHealed();
|
|
17509
|
+
const verb = r.action === "install" ? "registered" : "reconciled";
|
|
17510
|
+
input.log(` \u21BB ${verb} Playwright MCP in ${r.target.path} \u2014 restart the host to load it`);
|
|
17511
|
+
} else if (r.outcome === "unparseable") {
|
|
17512
|
+
input.log(` \u26A0 ${r.target.label}: could not safely patch ${r.target.path} (unparseable) \u2014 fix it by hand`);
|
|
17513
|
+
} else if (r.outcome === "write-failed") {
|
|
17514
|
+
input.log(` \u26A0 ${r.target.label}: failed to write ${r.target.path}`);
|
|
17515
|
+
}
|
|
17516
|
+
}
|
|
17517
|
+
return buildMcpReconcileCheck(planMcpReconcile(mcpConfigTargets(), PLAYWRIGHT_MCP_SPEC));
|
|
16672
17518
|
}
|
|
16673
17519
|
function strayBrowserArtifactPaths() {
|
|
16674
17520
|
const cwd = process.cwd();
|
|
@@ -16704,7 +17550,6 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16704
17550
|
const repairLocal = !opts.json || Boolean(opts.apply) || Boolean(opts.preflight);
|
|
16705
17551
|
const repoWritesAllowed = !opts.noRepoWrites;
|
|
16706
17552
|
const runExtended = Boolean(opts.verbose) || Boolean(opts.json);
|
|
16707
|
-
const terseOutput = !opts.verbose && !opts.json && !opts.banner && !opts.preflight;
|
|
16708
17553
|
const checks = [];
|
|
16709
17554
|
const REWRITE_KEY = "url.https://github.com/.insteadOf";
|
|
16710
17555
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
@@ -16774,6 +17619,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16774
17619
|
const markPluginReloadRequired = () => {
|
|
16775
17620
|
pluginReloadRequired = true;
|
|
16776
17621
|
};
|
|
17622
|
+
let healedCount = 0;
|
|
17623
|
+
const markHealed = () => {
|
|
17624
|
+
healedCount += 1;
|
|
17625
|
+
};
|
|
16777
17626
|
let versionReport = buildVersionLagReport({
|
|
16778
17627
|
currentVersion: resolveClientVersion(),
|
|
16779
17628
|
repoVersion: readRepoVersion(),
|
|
@@ -16789,13 +17638,19 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16789
17638
|
checks.push(versionReport);
|
|
16790
17639
|
if (selfUpdatedCli) {
|
|
16791
17640
|
if (!opts.json) io.err(selfUpdateReexecLine(versionReport));
|
|
16792
|
-
const
|
|
17641
|
+
const reexecArgs = buildSelfUpdateReexecArgs(opts);
|
|
17642
|
+
const code = await reexecMmiCli(reexecArgs);
|
|
16793
17643
|
if (code >= 0) {
|
|
16794
17644
|
if (code > 0) process.exitCode = code;
|
|
16795
17645
|
return;
|
|
16796
17646
|
}
|
|
16797
|
-
|
|
16798
|
-
|
|
17647
|
+
process.exitCode = DOCTOR_SELF_UPDATE_HALT_EXIT_CODE;
|
|
17648
|
+
if (opts.json) {
|
|
17649
|
+
io.log(JSON.stringify(buildSelfUpdateHaltPayload({ checks, updatedTo: versionReport.releasedVersion }), null, 2));
|
|
17650
|
+
} else {
|
|
17651
|
+
io.err(selfUpdateHaltLine(versionReport));
|
|
17652
|
+
io.err(selfUpdateHaltSentinelLine({ report: versionReport, rerunArgs: reexecArgs }));
|
|
17653
|
+
}
|
|
16799
17654
|
return;
|
|
16800
17655
|
}
|
|
16801
17656
|
checks.push({ ok: Boolean(cfg.sagaApiUrl), label: "Hub API URL configured", fix: "set MMI_HUB_URL or use a current MMI CLI/plugin build" });
|
|
@@ -16826,6 +17681,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16826
17681
|
try {
|
|
16827
17682
|
await execFileP2("git", ["config", "--global", "--add", REWRITE_KEY, "git@github.com:"]);
|
|
16828
17683
|
cloneOk = true;
|
|
17684
|
+
markHealed();
|
|
16829
17685
|
io.err(" \u21BB repaired: git insteadOf git@github.com \u2192 https (plugin clone over HTTPS)");
|
|
16830
17686
|
} catch {
|
|
16831
17687
|
}
|
|
@@ -16844,6 +17700,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16844
17700
|
if (!pluginCheck.ok && pluginCheck.recordToInsert && repairLocal) {
|
|
16845
17701
|
if (writeProjectInstallRecord(pluginCheck.recordToInsert)) {
|
|
16846
17702
|
pluginCheck = { ...pluginCheck, ok: true };
|
|
17703
|
+
markHealed();
|
|
16847
17704
|
io.err(` \u21BB repaired: registered mmi@mutmutco project install record \u2014 ${reloadHint} to load MMI commands`);
|
|
16848
17705
|
}
|
|
16849
17706
|
}
|
|
@@ -16863,6 +17720,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16863
17720
|
surface
|
|
16864
17721
|
});
|
|
16865
17722
|
if (legacyPluginCheck.ok) {
|
|
17723
|
+
markHealed();
|
|
16866
17724
|
io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via claude plugin \u2014 ${reloadHint} to load MMI commands`);
|
|
16867
17725
|
}
|
|
16868
17726
|
}
|
|
@@ -16873,6 +17731,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16873
17731
|
surface
|
|
16874
17732
|
});
|
|
16875
17733
|
if (legacyPluginCheck.ok) {
|
|
17734
|
+
markHealed();
|
|
16876
17735
|
io.err(` \u21BB migrated legacy mmi@mmi \u2192 mmi@mutmutco via codex plugin \u2014 ${reloadHint} to load MMI commands`);
|
|
16877
17736
|
}
|
|
16878
17737
|
}
|
|
@@ -16891,6 +17750,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16891
17750
|
gitignoreCheck.removed?.length ? `removed ${gitignoreCheck.removed.join(", ")}` : ""
|
|
16892
17751
|
].filter(Boolean).join("; ") || "normalized the block";
|
|
16893
17752
|
gitignoreCheck = { ...gitignoreCheck, ok: true };
|
|
17753
|
+
markHealed();
|
|
16894
17754
|
io.err(` \u21BB repaired: org-managed .gitignore block \u2014 ${drift}`);
|
|
16895
17755
|
io.err(" this is an org-managed update (not unrelated churn) \u2014 stage & commit .gitignore so it stops recurring");
|
|
16896
17756
|
}
|
|
@@ -16904,6 +17764,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16904
17764
|
if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
|
|
16905
17765
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
16906
17766
|
driftCheck = { ...driftCheck, ok: true };
|
|
17767
|
+
markHealed();
|
|
16907
17768
|
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
17769
|
}
|
|
16909
17770
|
}
|
|
@@ -16936,6 +17797,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16936
17797
|
installedVersionCheck = healed;
|
|
16937
17798
|
if (healed.ok) {
|
|
16938
17799
|
markPluginReloadRequired();
|
|
17800
|
+
markHealed();
|
|
16939
17801
|
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via claude plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
16940
17802
|
}
|
|
16941
17803
|
}
|
|
@@ -16945,11 +17807,13 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16945
17807
|
installedVersionCheck = healed;
|
|
16946
17808
|
if (healed.ok) {
|
|
16947
17809
|
markPluginReloadRequired();
|
|
17810
|
+
markHealed();
|
|
16948
17811
|
io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction(surface)} to load the new commands`);
|
|
16949
17812
|
}
|
|
16950
17813
|
}
|
|
16951
17814
|
}
|
|
16952
17815
|
checks.push(installedVersionCheck);
|
|
17816
|
+
checks.push(buildManagedPluginDriftCheck({ isOrgRepo, sources: managedPluginSurfaceSources() }));
|
|
16953
17817
|
let openCodeConfigSnapshot = opencodeConfigSnapshot();
|
|
16954
17818
|
const inspectOpenCode = surface === "opencode" || openCodeConfigSnapshot.hasConfig || runExtended;
|
|
16955
17819
|
if (inspectOpenCode) {
|
|
@@ -16974,6 +17838,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16974
17838
|
});
|
|
16975
17839
|
if (opencodeConfigCheck.ok) {
|
|
16976
17840
|
markPluginReloadRequired();
|
|
17841
|
+
markHealed();
|
|
16977
17842
|
io.err(` \u21BB repaired: wired ${OPENCODE_PLUGIN_PACKAGE} in OpenCode config \u2014 ${reloadAction("opencode")} to load MMI commands`);
|
|
16978
17843
|
}
|
|
16979
17844
|
}
|
|
@@ -16986,10 +17851,20 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16986
17851
|
releasedVersion
|
|
16987
17852
|
});
|
|
16988
17853
|
if (!opencodeVersionCheck.ok && repairFull) {
|
|
16989
|
-
const
|
|
16990
|
-
|
|
16991
|
-
|
|
16992
|
-
|
|
17854
|
+
const persistedCurrent = () => buildOpencodeVersionCheck({
|
|
17855
|
+
isOrgRepo,
|
|
17856
|
+
installedVersion: opencodePersistedVersionForDoctor(),
|
|
17857
|
+
releasedVersion
|
|
17858
|
+
}).ok;
|
|
17859
|
+
let refreshed = false;
|
|
17860
|
+
let quarantined = false;
|
|
17861
|
+
if (!persistedCurrent()) {
|
|
17862
|
+
refreshed = await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m));
|
|
17863
|
+
quarantined = quarantineStaleOpencodePluginCaches(releasedVersion, (m) => io.err(m));
|
|
17864
|
+
}
|
|
17865
|
+
if (persistedCurrent()) {
|
|
17866
|
+
const wasReinstalled = refreshed || quarantined;
|
|
17867
|
+
opencodeInstalledVersion = opencodePersistedVersionForDoctor() ?? opencodeInstalledVersion;
|
|
16993
17868
|
opencodeVersionCheck = buildOpencodeVersionCheck({
|
|
16994
17869
|
isOrgRepo,
|
|
16995
17870
|
installedVersion: opencodeInstalledVersion,
|
|
@@ -16997,7 +17872,9 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16997
17872
|
});
|
|
16998
17873
|
if (opencodeVersionCheck.ok) {
|
|
16999
17874
|
markPluginReloadRequired();
|
|
17000
|
-
|
|
17875
|
+
markHealed();
|
|
17876
|
+
const version = opencodeInstalledVersion ?? releasedVersion ?? "latest";
|
|
17877
|
+
io.err(wasReinstalled ? ` \u21BB force-refreshed OpenCode MMI plugin \u2192 ${version} \u2014 ${reloadAction("opencode")} to load it` : ` \u21BB OpenCode MMI plugin is current on disk (${version}); the running session is stale \u2014 ${reloadAction("opencode")} to load it`);
|
|
17001
17878
|
}
|
|
17002
17879
|
}
|
|
17003
17880
|
}
|
|
@@ -17035,6 +17912,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17035
17912
|
});
|
|
17036
17913
|
if (surfaceAssetsCheck.ok) {
|
|
17037
17914
|
markPluginReloadRequired();
|
|
17915
|
+
markHealed();
|
|
17038
17916
|
io.err(` \u21BB materialized OpenCode MMI commands + skills path \u2014 ${reloadAction("opencode")} to load them`);
|
|
17039
17917
|
}
|
|
17040
17918
|
}
|
|
@@ -17056,6 +17934,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17056
17934
|
if (!legacyOpenCodeCheck.ok && repairLocal && legacyOpenCodeConfig.legacyPath) {
|
|
17057
17935
|
if (quarantineOpencodeLegacyConfig(legacyOpenCodeConfig.legacyPath)) {
|
|
17058
17936
|
legacyOpenCodeCheck = buildOpencodeLegacyConfigCheck({ isOrgRepo: true });
|
|
17937
|
+
markHealed();
|
|
17059
17938
|
io.err(` \u21BB quarantined legacy OpenCode config \u2192 ${legacyOpenCodeConfig.legacyPath}.bak \u2014 restart OpenCode`);
|
|
17060
17939
|
}
|
|
17061
17940
|
}
|
|
@@ -17066,6 +17945,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17066
17945
|
return versions.sort((a, b) => compareVersions(b, a))[0];
|
|
17067
17946
|
};
|
|
17068
17947
|
const codexCacheVersions = () => mmiPluginCacheRootSnapshots().filter((r) => r.surface === "codex").flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
|
|
17948
|
+
const codexActiveVersion = () => isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
|
|
17069
17949
|
let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
|
|
17070
17950
|
isOrgRepo,
|
|
17071
17951
|
roots: mmiPluginCacheRootSnapshots(),
|
|
@@ -17080,6 +17960,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17080
17960
|
const surfaces = [...new Set(cacheCleanupCheck.leftovers?.map((entry) => entry.surface) ?? [])].join("/");
|
|
17081
17961
|
const names = cacheCleanupCheck.leftovers?.map((entry) => entry.name).join(", ");
|
|
17082
17962
|
markPluginReloadRequired();
|
|
17963
|
+
markHealed();
|
|
17083
17964
|
io.err(` \u21BB quarantined ${moved} stale MMI plugin cache dir(s) for ${surfaces || "agent surfaces"}: ${names} \u2014 ${reloadHint} to load MMI commands`);
|
|
17084
17965
|
}
|
|
17085
17966
|
cacheCleanupCheck = {
|
|
@@ -17101,18 +17982,21 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17101
17982
|
isOrgRepo,
|
|
17102
17983
|
releasedVersion,
|
|
17103
17984
|
codexCacheVersions: codexCacheVersions(),
|
|
17104
|
-
codexRecordVersion: codexRecordVersion()
|
|
17985
|
+
codexRecordVersion: codexRecordVersion(),
|
|
17986
|
+
codexActiveVersion: await codexActiveVersion()
|
|
17105
17987
|
});
|
|
17106
17988
|
if (!codexActiveCacheCheck.ok && repairFull) {
|
|
17107
17989
|
const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
|
|
17108
17990
|
if (canDriveCodex && await applyPluginHeal("codex", surface, (m) => io.err(m), { force: true })) {
|
|
17109
17991
|
markPluginReloadRequired();
|
|
17992
|
+
markHealed();
|
|
17110
17993
|
io.err(` \u21BB restored Codex MMI plugin cache \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction("codex")} to load the new commands`);
|
|
17111
17994
|
codexActiveCacheCheck = buildCodexActiveCacheCheck({
|
|
17112
17995
|
isOrgRepo,
|
|
17113
17996
|
releasedVersion,
|
|
17114
17997
|
codexCacheVersions: codexCacheVersions(),
|
|
17115
|
-
codexRecordVersion: codexRecordVersion()
|
|
17998
|
+
codexRecordVersion: codexRecordVersion(),
|
|
17999
|
+
codexActiveVersion: await codexActiveVersion()
|
|
17116
18000
|
});
|
|
17117
18001
|
}
|
|
17118
18002
|
}
|
|
@@ -17123,7 +18007,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17123
18007
|
entries: nestedPluginTreeSnapshot()
|
|
17124
18008
|
});
|
|
17125
18009
|
if (!nestedPluginTreeCheck.ok && nestedPluginTreeCheck.nested?.length && repairLocal) {
|
|
17126
|
-
const
|
|
18010
|
+
const nestedEntries = nestedPluginTreeCheck.nested;
|
|
18011
|
+
const nestedPaths = nestedEntries.map((n) => n.path);
|
|
17127
18012
|
if (await applyNestedPluginTreeCleanup(nestedPaths, (m) => io.err(m))) {
|
|
17128
18013
|
nestedPluginTreeCheck = buildNestedPluginTreeCheck({
|
|
17129
18014
|
isOrgRepo,
|
|
@@ -17131,35 +18016,62 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17131
18016
|
entries: nestedPluginTreeSnapshot()
|
|
17132
18017
|
});
|
|
17133
18018
|
if (nestedPluginTreeCheck.ok) {
|
|
17134
|
-
io.err(` \u21BB cleared self-nested MMI plugin cache tree(s)
|
|
18019
|
+
io.err(` \u21BB cleared self-nested MMI plugin cache tree(s)`);
|
|
18020
|
+
}
|
|
18021
|
+
let reinstalledAny = false;
|
|
18022
|
+
const unreinstalled = [];
|
|
18023
|
+
for (const token of [...new Set(nestedEntries.map((n) => n.surface))]) {
|
|
18024
|
+
const bin = token === "codex" ? "codex" : "claude";
|
|
18025
|
+
const canDrive = surfaceToken(surface) === token || await hostBinAvailable(bin);
|
|
18026
|
+
if (canDrive && await applyPluginHeal(token, surface, (m) => io.err(m), { force: true })) {
|
|
18027
|
+
reinstalledAny = true;
|
|
18028
|
+
io.err(` \u21BB reinstalled MMI plugin (${token}) after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
|
|
18029
|
+
} else {
|
|
18030
|
+
unreinstalled.push(token);
|
|
18031
|
+
}
|
|
17135
18032
|
}
|
|
17136
|
-
if (
|
|
18033
|
+
if (reinstalledAny) {
|
|
17137
18034
|
markPluginReloadRequired();
|
|
17138
|
-
|
|
18035
|
+
markHealed();
|
|
17139
18036
|
nestedPluginTreeCheck = buildNestedPluginTreeCheck({
|
|
17140
18037
|
isOrgRepo,
|
|
17141
18038
|
isWindows: isWin,
|
|
17142
18039
|
entries: nestedPluginTreeSnapshot()
|
|
17143
18040
|
});
|
|
17144
18041
|
}
|
|
18042
|
+
if (unreinstalled.length > 0 && nestedPluginTreeCheck.ok) {
|
|
18043
|
+
nestedPluginTreeCheck = { ...nestedPluginTreeCheck, ok: false, fix: nestedTreeReinstallGapFix(unreinstalled) };
|
|
18044
|
+
}
|
|
17145
18045
|
}
|
|
17146
18046
|
}
|
|
17147
18047
|
checks.push(nestedPluginTreeCheck);
|
|
17148
18048
|
const cursorCacheRoot = cursorPluginCacheRoot();
|
|
18049
|
+
const cursorCacheRootExists = (0, import_node_fs17.existsSync)(cursorCacheRoot);
|
|
17149
18050
|
let cursorPins = cursorPluginCachePinSnapshots() ?? [];
|
|
18051
|
+
checks.push(
|
|
18052
|
+
buildCursorPluginCacheCleanupCheck({
|
|
18053
|
+
isOrgRepo,
|
|
18054
|
+
cacheRoot: cursorCacheRoot,
|
|
18055
|
+
pins: cursorPins,
|
|
18056
|
+
releasedVersion
|
|
18057
|
+
})
|
|
18058
|
+
);
|
|
17150
18059
|
let cursorPluginCheck = buildCursorPluginInstallCheck({
|
|
17151
18060
|
isOrgRepo,
|
|
17152
18061
|
surface,
|
|
17153
18062
|
cacheRoot: cursorCacheRoot,
|
|
17154
|
-
cacheRootExists:
|
|
18063
|
+
cacheRootExists: cursorCacheRootExists,
|
|
17155
18064
|
pins: cursorPins,
|
|
17156
18065
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17157
18066
|
releasedVersion
|
|
17158
18067
|
});
|
|
17159
18068
|
if (!cursorPluginCheck.ok && repairLocal) {
|
|
18069
|
+
const seedingFromNothing = cursorPins.length === 0;
|
|
17160
18070
|
const seeded = await applyCursorPluginCacheSeed({
|
|
17161
18071
|
pins: cursorPins,
|
|
17162
18072
|
releasedVersion,
|
|
18073
|
+
cacheRoot: cursorCacheRoot,
|
|
18074
|
+
cacheRootExists: cursorCacheRootExists,
|
|
17163
18075
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17164
18076
|
execFileP: execFileP2,
|
|
17165
18077
|
mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
|
|
@@ -17171,13 +18083,26 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17171
18083
|
isOrgRepo,
|
|
17172
18084
|
surface,
|
|
17173
18085
|
cacheRoot: cursorCacheRoot,
|
|
17174
|
-
cacheRootExists:
|
|
18086
|
+
cacheRootExists: cursorCacheRootExists,
|
|
17175
18087
|
pins: cursorPins,
|
|
17176
18088
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17177
18089
|
releasedVersion
|
|
17178
18090
|
});
|
|
17179
18091
|
if (cursorPluginCheck.ok) {
|
|
17180
|
-
|
|
18092
|
+
markHealed();
|
|
18093
|
+
if (seedingFromNothing) {
|
|
18094
|
+
io.err(
|
|
18095
|
+
` \u21BB seeded Cursor MMI plugin cache from nothing \u2192 ${releasedVersion ?? "latest"} (cache only \u2014 Team Marketplace registration not verified, #2409) \u2014 ${reloadAction(surface)}`
|
|
18096
|
+
);
|
|
18097
|
+
cursorPluginCheck = {
|
|
18098
|
+
...cursorPluginCheck,
|
|
18099
|
+
ok: false,
|
|
18100
|
+
severityOverride: "advisory",
|
|
18101
|
+
fix: cursorSeededFromNothingAdvisoryFix(releasedVersion)
|
|
18102
|
+
};
|
|
18103
|
+
} else {
|
|
18104
|
+
io.err(` \u21BB seeded Cursor MMI plugin cache \u2192 ${releasedVersion ?? "latest"} \u2014 ${reloadAction(surface)}`);
|
|
18105
|
+
}
|
|
17181
18106
|
}
|
|
17182
18107
|
}
|
|
17183
18108
|
}
|
|
@@ -17198,20 +18123,29 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17198
18123
|
mmiCliOnPath: onPath
|
|
17199
18124
|
})
|
|
17200
18125
|
);
|
|
17201
|
-
if (
|
|
17202
|
-
const playwrightMcpConfigs = playwrightMcpConfigSnapshots();
|
|
17203
|
-
checks.push(
|
|
17204
|
-
buildPlaywrightMcpVisionCapCheck({
|
|
17205
|
-
isOrgRepo,
|
|
17206
|
-
configs: playwrightMcpConfigs
|
|
17207
|
-
})
|
|
17208
|
-
);
|
|
18126
|
+
if (!opts.banner) {
|
|
17209
18127
|
checks.push(
|
|
17210
|
-
|
|
18128
|
+
await reconcileMcpRegistrations({
|
|
17211
18129
|
isOrgRepo,
|
|
17212
|
-
|
|
18130
|
+
apply: Boolean(opts.apply),
|
|
18131
|
+
repoWritesAllowed,
|
|
18132
|
+
log: (m) => io.err(m),
|
|
18133
|
+
onHealed: markHealed
|
|
17213
18134
|
})
|
|
17214
18135
|
);
|
|
18136
|
+
}
|
|
18137
|
+
const sessionPluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
|
18138
|
+
const sessionCacheSurface = sessionPluginRoot?.includes(".codex") ? "codex" : sessionPluginRoot?.includes(".claude") ? "claude" : void 0;
|
|
18139
|
+
const sessionCacheVersions = sessionCacheSurface ? mmiPluginCacheRootSnapshots().filter((r) => r.surface === sessionCacheSurface).flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name)) : [];
|
|
18140
|
+
checks.push(
|
|
18141
|
+
buildSessionSkillRootCheck({
|
|
18142
|
+
isOrgRepo,
|
|
18143
|
+
surface,
|
|
18144
|
+
pluginRoot: sessionPluginRoot,
|
|
18145
|
+
cacheVersions: sessionCacheVersions
|
|
18146
|
+
})
|
|
18147
|
+
);
|
|
18148
|
+
if (runExtended) {
|
|
17215
18149
|
checks.push(
|
|
17216
18150
|
buildBrowserArtifactsCheck({
|
|
17217
18151
|
isOrgRepo,
|
|
@@ -17248,48 +18182,66 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17248
18182
|
} catch {
|
|
17249
18183
|
}
|
|
17250
18184
|
}
|
|
18185
|
+
emitDoctorReport(opts, io, {
|
|
18186
|
+
checks,
|
|
18187
|
+
surface,
|
|
18188
|
+
needsEagerHeal: healPlan.needsEagerHeal,
|
|
18189
|
+
healedCount,
|
|
18190
|
+
pluginReloadRequired,
|
|
18191
|
+
reloadHint,
|
|
18192
|
+
// Surface-aware update report (#865): the per-surface version snapshot + copy-paste update recipes, so
|
|
18193
|
+
// an agent told "make sure the CLI and plugin are up to date" can run the right command per surface and
|
|
18194
|
+
// echo back an unambiguous version line (CLI / Claude plugin / Codex marketplace / Codex active cache).
|
|
18195
|
+
// Passed lazily: emitDoctorReport calls it only past the --preflight/--banner early returns, keeping
|
|
18196
|
+
// the SessionStart hot path free of its synchronous fs I/O.
|
|
18197
|
+
buildUpdateReport: () => {
|
|
18198
|
+
const cacheRoots = mmiPluginCacheRootSnapshots();
|
|
18199
|
+
const cacheVersionsFor = (s) => cacheRoots.filter((r) => r.surface === s).flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
|
|
18200
|
+
const sourceVersions = (s) => installedPluginVersions(installedPluginSources().find((src) => src.surface === s)?.installed ?? null);
|
|
18201
|
+
return buildPluginUpdateReport({
|
|
18202
|
+
cliVersion: resolveClientVersion(),
|
|
18203
|
+
claudePluginVersions: sourceVersions("claude"),
|
|
18204
|
+
codexPluginVersions: sourceVersions("codex"),
|
|
18205
|
+
codexCacheVersions: cacheVersionsFor("codex"),
|
|
18206
|
+
opencodePluginVersions: opencodePluginVersionsForReport(),
|
|
18207
|
+
releasedVersion
|
|
18208
|
+
});
|
|
18209
|
+
}
|
|
18210
|
+
});
|
|
18211
|
+
}
|
|
18212
|
+
function emitDoctorReport(opts, io, ctx) {
|
|
18213
|
+
const { checks, surface, healedCount, pluginReloadRequired, reloadHint } = ctx;
|
|
17251
18214
|
const gaps = checks.filter((c) => !c.ok);
|
|
17252
18215
|
if (opts.preflight) {
|
|
17253
|
-
const outcome = preflightOutcome({ gaps, needsEagerHeal:
|
|
18216
|
+
const outcome = preflightOutcome({ gaps, needsEagerHeal: ctx.needsEagerHeal, surface });
|
|
17254
18217
|
if (outcome.line) io.err(outcome.line);
|
|
17255
18218
|
if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
|
|
17256
18219
|
return;
|
|
17257
18220
|
}
|
|
17258
18221
|
if (opts.banner) {
|
|
17259
|
-
if (
|
|
18222
|
+
if (ctx.needsEagerHeal) io.err(doctorPreflightDoneLine(surface));
|
|
17260
18223
|
if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
|
|
17261
18224
|
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
18225
|
return;
|
|
17263
18226
|
}
|
|
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
|
-
});
|
|
18227
|
+
const updateReport = ctx.buildUpdateReport();
|
|
17275
18228
|
const resources = doctorResourcesForGaps(gaps);
|
|
18229
|
+
process.exitCode = doctorExitCode(checks);
|
|
17276
18230
|
if (opts.json) {
|
|
17277
18231
|
io.log(JSON.stringify(buildDoctorJsonPayload({ checks, updateReport, resources }), null, 2));
|
|
17278
18232
|
return;
|
|
17279
18233
|
}
|
|
17280
|
-
if (
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
17285
|
-
|
|
17286
|
-
|
|
17287
|
-
|
|
18234
|
+
if (pluginReloadRequired) io.err(pluginAutonomousHaltLine(reloadHint));
|
|
18235
|
+
for (const line of doctorHumanLines({
|
|
18236
|
+
checks,
|
|
18237
|
+
updateReport,
|
|
18238
|
+
shouldApply: Boolean(opts.apply),
|
|
18239
|
+
healedCount,
|
|
18240
|
+
pluginReloadRequired,
|
|
18241
|
+
reloadHint,
|
|
18242
|
+
verbose: Boolean(opts.verbose)
|
|
18243
|
+
})) io.log(line);
|
|
17288
18244
|
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
18245
|
}
|
|
17294
18246
|
var USER_SCOPE_GUARD_MARKER = "mmi-guard:v1";
|
|
17295
18247
|
var USER_SCOPE_GUARD_COMMAND = `mmi-cli guard --session-start || true # ${USER_SCOPE_GUARD_MARKER}`;
|
|
@@ -18874,6 +19826,21 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
18874
19826
|
pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
|
|
18875
19827
|
sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms))
|
|
18876
19828
|
}),
|
|
19829
|
+
// #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
|
|
19830
|
+
// fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
|
|
19831
|
+
probeMergeReady: async (prNumber, repo) => {
|
|
19832
|
+
const args = repo ? ["--repo", repo] : [];
|
|
19833
|
+
const [state, checks] = await Promise.all([
|
|
19834
|
+
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(() => ["", ""]),
|
|
19835
|
+
pollGhPrChecks(prNumber, args).catch(() => "error")
|
|
19836
|
+
]);
|
|
19837
|
+
const [prState, mergeable] = state;
|
|
19838
|
+
return {
|
|
19839
|
+
open: prState === "OPEN",
|
|
19840
|
+
mergeable: mergeable === "MERGEABLE",
|
|
19841
|
+
checksPassing: checks === "success" || checks === "no-checks-reported"
|
|
19842
|
+
};
|
|
19843
|
+
},
|
|
18877
19844
|
mergeAuto: async (prNumber, repo) => {
|
|
18878
19845
|
const args = repo ? ["--repo", repo] : [];
|
|
18879
19846
|
const readMergeState = () => readGhPrStateWithRetry(async () => (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 })).stdout);
|
|
@@ -19274,7 +20241,7 @@ board.command("prune-priority-labels").description("remove retired priority:* la
|
|
|
19274
20241
|
});
|
|
19275
20242
|
board.command("done <issue>").description("set a board item's Status to Done (does not close the GitHub issue; use `gh issue close`)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return success JSON if the item resolves but the status move fails").action(async (issueRef, o) => {
|
|
19276
20243
|
try {
|
|
19277
|
-
const result = await moveBoardItem({ config: await
|
|
20244
|
+
const result = await moveBoardItem({ config: await loadConfigForBoardSelector(issueRef, o.repo), selector: issueRef, status: "Done", repo: o.repo, allowPartial: o.allowPartial });
|
|
19278
20245
|
if (o.json) return console.log(JSON.stringify(result));
|
|
19279
20246
|
console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> Done`);
|
|
19280
20247
|
} catch (e) {
|