@mutmutco/cli 3.1.0 → 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 +753 -118
- 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
|
}
|
|
@@ -10423,25 +10445,113 @@ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix
|
|
|
10423
10445
|
}
|
|
10424
10446
|
return { foldPaths, tolerated, predicted };
|
|
10425
10447
|
}
|
|
10426
|
-
async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted) {
|
|
10448
|
+
async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted, preFold) {
|
|
10427
10449
|
await deps.run("git", ["checkout", "main"]);
|
|
10428
10450
|
await ffOnlyPull(deps, "main");
|
|
10451
|
+
preFold.mainSha = clean(await deps.run("git", ["rev-parse", "main"]));
|
|
10429
10452
|
if (predicted.length === 0) {
|
|
10430
10453
|
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
10431
10454
|
} else {
|
|
10432
10455
|
await mergeWithToleratedResolution(deps, sourceRef, mergeLabel, "theirs", tolerated);
|
|
10433
10456
|
}
|
|
10434
10457
|
}
|
|
10435
|
-
async function
|
|
10436
|
-
const
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
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.`
|
|
10442
10484
|
);
|
|
10443
|
-
|
|
10444
|
-
|
|
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
|
+
}
|
|
10445
10555
|
}
|
|
10446
10556
|
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
|
|
10447
10557
|
await ensureTagPushed(deps, tag, releaseSha);
|
|
@@ -10534,15 +10644,20 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
10534
10644
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
10535
10645
|
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
10536
10646
|
const rcShaAtRelease = !directTrack && hasRcBranch ? clean(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
10537
|
-
const { foldPaths: foldPaths2 } = await
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
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 };
|
|
10543
10660
|
});
|
|
10544
|
-
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
10545
|
-
const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10546
10661
|
const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2);
|
|
10547
10662
|
const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
|
|
10548
10663
|
if (directTrack) {
|
|
@@ -10619,10 +10734,14 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
10619
10734
|
);
|
|
10620
10735
|
}
|
|
10621
10736
|
const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
10622
|
-
|
|
10623
|
-
const tag =
|
|
10624
|
-
|
|
10625
|
-
|
|
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
|
+
});
|
|
10626
10745
|
const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha);
|
|
10627
10746
|
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
10628
10747
|
const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
@@ -14806,16 +14925,24 @@ function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
|
|
|
14806
14925
|
return false;
|
|
14807
14926
|
});
|
|
14808
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
|
+
}
|
|
14809
14936
|
async function applyCursorPluginCacheSeed(input) {
|
|
14810
14937
|
if (!isSemverVersion(input.releasedVersion)) return false;
|
|
14811
|
-
const
|
|
14812
|
-
if (
|
|
14938
|
+
const targets = cursorPluginCacheSeedTargets(input.pins, input.releasedVersion, input.cacheRoot, input.cacheRootExists);
|
|
14939
|
+
if (targets.length === 0) return false;
|
|
14813
14940
|
const tmpRoot = await input.mkdtemp("mmi-cursor-seed-");
|
|
14814
14941
|
const source = await resolvePluginMmiSource(input.releasedVersion, input.hubCheckout, tmpRoot, input.execFileP);
|
|
14815
14942
|
if (!source) return false;
|
|
14816
14943
|
input.log(` \u21BB seeding Cursor MMI plugin cache \u2192 ${input.releasedVersion}\u2026`);
|
|
14817
|
-
for (const
|
|
14818
|
-
syncDirContents(source,
|
|
14944
|
+
for (const dest of targets) {
|
|
14945
|
+
syncDirContents(source, dest);
|
|
14819
14946
|
}
|
|
14820
14947
|
(0, import_node_fs14.rmSync)(tmpRoot, { recursive: true, force: true });
|
|
14821
14948
|
return true;
|
|
@@ -15095,6 +15222,33 @@ function buildMmiPluginCacheCleanupCheck(input) {
|
|
|
15095
15222
|
}))
|
|
15096
15223
|
};
|
|
15097
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
|
+
}
|
|
15098
15252
|
var NESTED_PLUGIN_TREE_LABEL = "self-nested MMI plugin cache tree (#1126)";
|
|
15099
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)";
|
|
15100
15254
|
function nestedPluginTreeCleanupCommand(paths, isWindows) {
|
|
@@ -15117,7 +15271,21 @@ function buildNestedPluginTreeCheck(input) {
|
|
|
15117
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)}`
|
|
15118
15272
|
};
|
|
15119
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
|
+
}
|
|
15120
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
|
+
}
|
|
15121
15289
|
function buildCodexActiveCacheCheck(input) {
|
|
15122
15290
|
const base = {
|
|
15123
15291
|
ok: true,
|
|
@@ -15125,6 +15293,18 @@ function buildCodexActiveCacheCheck(input) {
|
|
|
15125
15293
|
fix: CODEX_PLUGIN_RECOVERY
|
|
15126
15294
|
};
|
|
15127
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
|
+
}
|
|
15128
15308
|
if (isSemverVersion2(input.codexRecordVersion) && compareVersions(input.codexRecordVersion, input.releasedVersion) < 0) {
|
|
15129
15309
|
return base;
|
|
15130
15310
|
}
|
|
@@ -15172,6 +15352,55 @@ function reloadAction(surface) {
|
|
|
15172
15352
|
return "restart Claude Code (or run /reload-plugins)";
|
|
15173
15353
|
}
|
|
15174
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
|
+
}
|
|
15175
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`;
|
|
15176
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`;
|
|
15177
15406
|
var CURSOR_RECOVERY = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace";
|
|
@@ -15703,6 +15932,9 @@ function cursorPluginInstallFix(input) {
|
|
|
15703
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`;
|
|
15704
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}`;
|
|
15705
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
|
+
}
|
|
15706
15938
|
function buildCursorPluginInstallCheck(input) {
|
|
15707
15939
|
const base = {
|
|
15708
15940
|
ok: true,
|
|
@@ -15820,9 +16052,7 @@ function buildHubDeployFreshnessCheck(input) {
|
|
|
15820
16052
|
};
|
|
15821
16053
|
}
|
|
15822
16054
|
var PLAYWRIGHT_MCP_VISION_CAP_LABEL = "Playwright MCP vision caps (--caps=vision prohibited)";
|
|
15823
|
-
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";
|
|
15824
16055
|
var PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL = "Playwright MCP output dir (use tmp/playwright-mcp)";
|
|
15825
|
-
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";
|
|
15826
16056
|
function textHasPlaywrightMcp(content) {
|
|
15827
16057
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
15828
16058
|
return /@playwright\/mcp/.test(normalized) || /mcp_servers\.playwright/.test(normalized) || /"playwright"\s*:\s*\{/.test(normalized) || /\bmcpServers\b/.test(normalized);
|
|
@@ -15837,45 +16067,10 @@ function textHasPlaywrightVisionCap(content) {
|
|
|
15837
16067
|
if (/\bvision[-_]?(?:first|only|mode)\b/i.test(normalized)) return true;
|
|
15838
16068
|
return false;
|
|
15839
16069
|
}
|
|
15840
|
-
function buildPlaywrightMcpVisionCapCheck(input) {
|
|
15841
|
-
const base = {
|
|
15842
|
-
ok: true,
|
|
15843
|
-
label: PLAYWRIGHT_MCP_VISION_CAP_LABEL,
|
|
15844
|
-
fix: PLAYWRIGHT_MCP_VISION_CAP_FIX
|
|
15845
|
-
};
|
|
15846
|
-
if (!input.isOrgRepo) return base;
|
|
15847
|
-
const offending = input.configs.filter((c) => textHasPlaywrightVisionCap(c.content)).map((c) => c.path);
|
|
15848
|
-
if (offending.length === 0) return base;
|
|
15849
|
-
return {
|
|
15850
|
-
...base,
|
|
15851
|
-
ok: false,
|
|
15852
|
-
offendingPaths: offending,
|
|
15853
|
-
fix: `${PLAYWRIGHT_MCP_VISION_CAP_FIX} \u2014 found in: ${offending.join(", ")}`
|
|
15854
|
-
};
|
|
15855
|
-
}
|
|
15856
16070
|
function textHasCanonicalPlaywrightOutputDir(content) {
|
|
15857
16071
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
15858
16072
|
return /--output-dir(?:\s*=\s*|["'\s,]+)tmp\/playwright-mcp\b/.test(normalized);
|
|
15859
16073
|
}
|
|
15860
|
-
function textNeedsPlaywrightOutputDir(content) {
|
|
15861
|
-
return textHasPlaywrightMcp(content) && !textHasCanonicalPlaywrightOutputDir(content);
|
|
15862
|
-
}
|
|
15863
|
-
function buildPlaywrightMcpOutputDirCheck(input) {
|
|
15864
|
-
const base = {
|
|
15865
|
-
ok: true,
|
|
15866
|
-
label: PLAYWRIGHT_MCP_OUTPUT_DIR_LABEL,
|
|
15867
|
-
fix: PLAYWRIGHT_MCP_OUTPUT_DIR_FIX
|
|
15868
|
-
};
|
|
15869
|
-
if (!input.isOrgRepo) return base;
|
|
15870
|
-
const offending = input.configs.filter((c) => textNeedsPlaywrightOutputDir(c.content)).map((c) => c.path);
|
|
15871
|
-
if (offending.length === 0) return base;
|
|
15872
|
-
return {
|
|
15873
|
-
...base,
|
|
15874
|
-
ok: false,
|
|
15875
|
-
offendingPaths: offending,
|
|
15876
|
-
fix: `${PLAYWRIGHT_MCP_OUTPUT_DIR_FIX} \u2014 missing in: ${offending.join(", ")}`
|
|
15877
|
-
};
|
|
15878
|
-
}
|
|
15879
16074
|
var STRAY_BROWSER_ARTIFACT_DIRS = [".playwright-mcp", "playwright-report", "test-results"];
|
|
15880
16075
|
var BROWSER_ARTIFACTS_LABEL = "browser MCP artifacts outside tmp/ (use tmp/playwright-mcp)";
|
|
15881
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";
|
|
@@ -15915,6 +16110,12 @@ function buildSelfUpdateHaltPayload(input) {
|
|
|
15915
16110
|
checks: input.checks
|
|
15916
16111
|
};
|
|
15917
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
|
+
}
|
|
15918
16119
|
var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
|
|
15919
16120
|
function buildSelfUpdateReexecArgs(opts) {
|
|
15920
16121
|
const args = ["doctor"];
|
|
@@ -16102,6 +16303,262 @@ function buildPluginResolvabilityCheck(input) {
|
|
|
16102
16303
|
}
|
|
16103
16304
|
}
|
|
16104
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
|
+
|
|
16105
16562
|
// src/cli-doctor-shared.ts
|
|
16106
16563
|
var import_node_fs15 = require("node:fs");
|
|
16107
16564
|
var import_node_path16 = require("node:path");
|
|
@@ -16358,7 +16815,20 @@ var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
|
16358
16815
|
function runHostBin(bin, args, opts) {
|
|
16359
16816
|
return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
|
|
16360
16817
|
}
|
|
16361
|
-
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) {
|
|
16362
16832
|
return new Promise((resolve6) => {
|
|
16363
16833
|
let settled = false;
|
|
16364
16834
|
const done = (code) => {
|
|
@@ -16367,12 +16837,20 @@ function reexecMmiCli(args) {
|
|
|
16367
16837
|
resolve6(code);
|
|
16368
16838
|
}
|
|
16369
16839
|
};
|
|
16370
|
-
const
|
|
16371
|
-
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 });
|
|
16372
16841
|
child.on("error", () => done(-1));
|
|
16373
16842
|
child.on("exit", (code) => done(code ?? 0));
|
|
16374
16843
|
});
|
|
16375
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
|
+
}
|
|
16376
16854
|
function hostBinAvailable(bin) {
|
|
16377
16855
|
return execFileP2(isWin ? "where" : "which", [bin]).then(() => true).catch(() => false);
|
|
16378
16856
|
}
|
|
@@ -16392,6 +16870,15 @@ async function runCodexPlugin(args) {
|
|
|
16392
16870
|
return false;
|
|
16393
16871
|
}
|
|
16394
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
|
+
}
|
|
16395
16882
|
async function marketplaceAddRefSupported(bin) {
|
|
16396
16883
|
try {
|
|
16397
16884
|
const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
|
|
@@ -16739,6 +17226,12 @@ function opencodeInstalledVersionForDoctor() {
|
|
|
16739
17226
|
diskVersion: readOpencodeAdapterDiskVersion()
|
|
16740
17227
|
});
|
|
16741
17228
|
}
|
|
17229
|
+
function opencodePersistedVersionForDoctor() {
|
|
17230
|
+
return pickOpencodeActiveVersion({
|
|
17231
|
+
cacheVersion: readOpencodeLoadedCacheVersion(),
|
|
17232
|
+
diskVersion: readOpencodeAdapterDiskVersion()
|
|
17233
|
+
});
|
|
17234
|
+
}
|
|
16742
17235
|
function opencodePluginVersionsForReport() {
|
|
16743
17236
|
return [process.env.MMI_OPENCODE_PLUGIN_VERSION, readOpencodeAdapterDiskVersion()].filter((v) => Boolean(v));
|
|
16744
17237
|
}
|
|
@@ -16856,8 +17349,8 @@ function hasNestedMmiChild(versionDir) {
|
|
|
16856
17349
|
}
|
|
16857
17350
|
}
|
|
16858
17351
|
function nestedPluginTreeSnapshot() {
|
|
16859
|
-
return mmiPluginCacheRootSnapshots().
|
|
16860
|
-
(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) }))
|
|
16861
17354
|
);
|
|
16862
17355
|
}
|
|
16863
17356
|
function uniqueQuarantineTarget(path2) {
|
|
@@ -16933,21 +17426,95 @@ function readTextFile(path2) {
|
|
|
16933
17426
|
return null;
|
|
16934
17427
|
}
|
|
16935
17428
|
}
|
|
16936
|
-
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() {
|
|
16937
17437
|
const cwd = process.cwd();
|
|
16938
17438
|
const home = (0, import_node_os6.homedir)();
|
|
16939
|
-
const
|
|
16940
|
-
|
|
16941
|
-
|
|
16942
|
-
|
|
16943
|
-
|
|
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
|
+
}
|
|
16944
17488
|
];
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
|
|
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;
|
|
16949
17496
|
}
|
|
16950
|
-
|
|
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));
|
|
16951
17518
|
}
|
|
16952
17519
|
function strayBrowserArtifactPaths() {
|
|
16953
17520
|
const cwd = process.cwd();
|
|
@@ -17071,13 +17638,19 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17071
17638
|
checks.push(versionReport);
|
|
17072
17639
|
if (selfUpdatedCli) {
|
|
17073
17640
|
if (!opts.json) io.err(selfUpdateReexecLine(versionReport));
|
|
17074
|
-
const
|
|
17641
|
+
const reexecArgs = buildSelfUpdateReexecArgs(opts);
|
|
17642
|
+
const code = await reexecMmiCli(reexecArgs);
|
|
17075
17643
|
if (code >= 0) {
|
|
17076
17644
|
if (code > 0) process.exitCode = code;
|
|
17077
17645
|
return;
|
|
17078
17646
|
}
|
|
17079
|
-
|
|
17080
|
-
|
|
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
|
+
}
|
|
17081
17654
|
return;
|
|
17082
17655
|
}
|
|
17083
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" });
|
|
@@ -17278,10 +17851,20 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17278
17851
|
releasedVersion
|
|
17279
17852
|
});
|
|
17280
17853
|
if (!opencodeVersionCheck.ok && repairFull) {
|
|
17281
|
-
const
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
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;
|
|
17285
17868
|
opencodeVersionCheck = buildOpencodeVersionCheck({
|
|
17286
17869
|
isOrgRepo,
|
|
17287
17870
|
installedVersion: opencodeInstalledVersion,
|
|
@@ -17290,7 +17873,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17290
17873
|
if (opencodeVersionCheck.ok) {
|
|
17291
17874
|
markPluginReloadRequired();
|
|
17292
17875
|
markHealed();
|
|
17293
|
-
|
|
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`);
|
|
17294
17878
|
}
|
|
17295
17879
|
}
|
|
17296
17880
|
}
|
|
@@ -17361,6 +17945,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17361
17945
|
return versions.sort((a, b) => compareVersions(b, a))[0];
|
|
17362
17946
|
};
|
|
17363
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);
|
|
17364
17949
|
let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
|
|
17365
17950
|
isOrgRepo,
|
|
17366
17951
|
roots: mmiPluginCacheRootSnapshots(),
|
|
@@ -17397,7 +17982,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17397
17982
|
isOrgRepo,
|
|
17398
17983
|
releasedVersion,
|
|
17399
17984
|
codexCacheVersions: codexCacheVersions(),
|
|
17400
|
-
codexRecordVersion: codexRecordVersion()
|
|
17985
|
+
codexRecordVersion: codexRecordVersion(),
|
|
17986
|
+
codexActiveVersion: await codexActiveVersion()
|
|
17401
17987
|
});
|
|
17402
17988
|
if (!codexActiveCacheCheck.ok && repairFull) {
|
|
17403
17989
|
const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
|
|
@@ -17409,7 +17995,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17409
17995
|
isOrgRepo,
|
|
17410
17996
|
releasedVersion,
|
|
17411
17997
|
codexCacheVersions: codexCacheVersions(),
|
|
17412
|
-
codexRecordVersion: codexRecordVersion()
|
|
17998
|
+
codexRecordVersion: codexRecordVersion(),
|
|
17999
|
+
codexActiveVersion: await codexActiveVersion()
|
|
17413
18000
|
});
|
|
17414
18001
|
}
|
|
17415
18002
|
}
|
|
@@ -17420,7 +18007,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17420
18007
|
entries: nestedPluginTreeSnapshot()
|
|
17421
18008
|
});
|
|
17422
18009
|
if (!nestedPluginTreeCheck.ok && nestedPluginTreeCheck.nested?.length && repairLocal) {
|
|
17423
|
-
const
|
|
18010
|
+
const nestedEntries = nestedPluginTreeCheck.nested;
|
|
18011
|
+
const nestedPaths = nestedEntries.map((n) => n.path);
|
|
17424
18012
|
if (await applyNestedPluginTreeCleanup(nestedPaths, (m) => io.err(m))) {
|
|
17425
18013
|
nestedPluginTreeCheck = buildNestedPluginTreeCheck({
|
|
17426
18014
|
isOrgRepo,
|
|
@@ -17428,36 +18016,62 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17428
18016
|
entries: nestedPluginTreeSnapshot()
|
|
17429
18017
|
});
|
|
17430
18018
|
if (nestedPluginTreeCheck.ok) {
|
|
17431
|
-
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
|
+
}
|
|
17432
18032
|
}
|
|
17433
|
-
if (
|
|
18033
|
+
if (reinstalledAny) {
|
|
17434
18034
|
markPluginReloadRequired();
|
|
17435
18035
|
markHealed();
|
|
17436
|
-
io.err(` \u21BB reinstalled MMI plugin after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
|
|
17437
18036
|
nestedPluginTreeCheck = buildNestedPluginTreeCheck({
|
|
17438
18037
|
isOrgRepo,
|
|
17439
18038
|
isWindows: isWin,
|
|
17440
18039
|
entries: nestedPluginTreeSnapshot()
|
|
17441
18040
|
});
|
|
17442
18041
|
}
|
|
18042
|
+
if (unreinstalled.length > 0 && nestedPluginTreeCheck.ok) {
|
|
18043
|
+
nestedPluginTreeCheck = { ...nestedPluginTreeCheck, ok: false, fix: nestedTreeReinstallGapFix(unreinstalled) };
|
|
18044
|
+
}
|
|
17443
18045
|
}
|
|
17444
18046
|
}
|
|
17445
18047
|
checks.push(nestedPluginTreeCheck);
|
|
17446
18048
|
const cursorCacheRoot = cursorPluginCacheRoot();
|
|
18049
|
+
const cursorCacheRootExists = (0, import_node_fs17.existsSync)(cursorCacheRoot);
|
|
17447
18050
|
let cursorPins = cursorPluginCachePinSnapshots() ?? [];
|
|
18051
|
+
checks.push(
|
|
18052
|
+
buildCursorPluginCacheCleanupCheck({
|
|
18053
|
+
isOrgRepo,
|
|
18054
|
+
cacheRoot: cursorCacheRoot,
|
|
18055
|
+
pins: cursorPins,
|
|
18056
|
+
releasedVersion
|
|
18057
|
+
})
|
|
18058
|
+
);
|
|
17448
18059
|
let cursorPluginCheck = buildCursorPluginInstallCheck({
|
|
17449
18060
|
isOrgRepo,
|
|
17450
18061
|
surface,
|
|
17451
18062
|
cacheRoot: cursorCacheRoot,
|
|
17452
|
-
cacheRootExists:
|
|
18063
|
+
cacheRootExists: cursorCacheRootExists,
|
|
17453
18064
|
pins: cursorPins,
|
|
17454
18065
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17455
18066
|
releasedVersion
|
|
17456
18067
|
});
|
|
17457
18068
|
if (!cursorPluginCheck.ok && repairLocal) {
|
|
18069
|
+
const seedingFromNothing = cursorPins.length === 0;
|
|
17458
18070
|
const seeded = await applyCursorPluginCacheSeed({
|
|
17459
18071
|
pins: cursorPins,
|
|
17460
18072
|
releasedVersion,
|
|
18073
|
+
cacheRoot: cursorCacheRoot,
|
|
18074
|
+
cacheRootExists: cursorCacheRootExists,
|
|
17461
18075
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17462
18076
|
execFileP: execFileP2,
|
|
17463
18077
|
mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
|
|
@@ -17469,14 +18083,26 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17469
18083
|
isOrgRepo,
|
|
17470
18084
|
surface,
|
|
17471
18085
|
cacheRoot: cursorCacheRoot,
|
|
17472
|
-
cacheRootExists:
|
|
18086
|
+
cacheRootExists: cursorCacheRootExists,
|
|
17473
18087
|
pins: cursorPins,
|
|
17474
18088
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17475
18089
|
releasedVersion
|
|
17476
18090
|
});
|
|
17477
18091
|
if (cursorPluginCheck.ok) {
|
|
17478
18092
|
markHealed();
|
|
17479
|
-
|
|
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
|
+
}
|
|
17480
18106
|
}
|
|
17481
18107
|
}
|
|
17482
18108
|
}
|
|
@@ -17497,20 +18123,29 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17497
18123
|
mmiCliOnPath: onPath
|
|
17498
18124
|
})
|
|
17499
18125
|
);
|
|
17500
|
-
if (
|
|
17501
|
-
const playwrightMcpConfigs = playwrightMcpConfigSnapshots();
|
|
17502
|
-
checks.push(
|
|
17503
|
-
buildPlaywrightMcpVisionCapCheck({
|
|
17504
|
-
isOrgRepo,
|
|
17505
|
-
configs: playwrightMcpConfigs
|
|
17506
|
-
})
|
|
17507
|
-
);
|
|
18126
|
+
if (!opts.banner) {
|
|
17508
18127
|
checks.push(
|
|
17509
|
-
|
|
18128
|
+
await reconcileMcpRegistrations({
|
|
17510
18129
|
isOrgRepo,
|
|
17511
|
-
|
|
18130
|
+
apply: Boolean(opts.apply),
|
|
18131
|
+
repoWritesAllowed,
|
|
18132
|
+
log: (m) => io.err(m),
|
|
18133
|
+
onHealed: markHealed
|
|
17512
18134
|
})
|
|
17513
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) {
|
|
17514
18149
|
checks.push(
|
|
17515
18150
|
buildBrowserArtifactsCheck({
|
|
17516
18151
|
isOrgRepo,
|
|
@@ -19606,7 +20241,7 @@ board.command("prune-priority-labels").description("remove retired priority:* la
|
|
|
19606
20241
|
});
|
|
19607
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) => {
|
|
19608
20243
|
try {
|
|
19609
|
-
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 });
|
|
19610
20245
|
if (o.json) return console.log(JSON.stringify(result));
|
|
19611
20246
|
console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> Done`);
|
|
19612
20247
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|