@mutmutco/cli 3.1.0 → 3.3.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 +960 -144
- 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
|
}
|
|
@@ -9197,9 +9219,23 @@ function parseNpmVersion(stdout) {
|
|
|
9197
9219
|
function staleTrainCliMessage(report, commandName) {
|
|
9198
9220
|
return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`mmi-cli doctor --apply --no-repo-writes\` to update to ${report.releasedVersion}, then rerun ${commandName} so it uses the current train path`;
|
|
9199
9221
|
}
|
|
9200
|
-
function
|
|
9222
|
+
async function resolveReleasedVersion(runners) {
|
|
9223
|
+
try {
|
|
9224
|
+
const npmVersion = parseNpmVersion(await runners.npm());
|
|
9225
|
+
if (npmVersion) return { version: npmVersion, source: "npm" };
|
|
9226
|
+
} catch {
|
|
9227
|
+
}
|
|
9228
|
+
try {
|
|
9229
|
+
const ghVersion = parseManifestVersion(await runners.gh());
|
|
9230
|
+
if (ghVersion) return { version: ghVersion, source: "gh" };
|
|
9231
|
+
} catch {
|
|
9232
|
+
}
|
|
9233
|
+
return { version: void 0, source: "unavailable" };
|
|
9234
|
+
}
|
|
9235
|
+
function versionAutoUpdateAction(report, hasPluginRoot, releasedSource) {
|
|
9201
9236
|
if (report.ok || report.staleAgainst !== "released") return "none";
|
|
9202
|
-
|
|
9237
|
+
if (hasPluginRoot) return "plugin-pull";
|
|
9238
|
+
return releasedSource === "gh" ? "npm-unreachable" : "npm";
|
|
9203
9239
|
}
|
|
9204
9240
|
|
|
9205
9241
|
// src/issue-related.ts
|
|
@@ -10423,25 +10459,113 @@ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix
|
|
|
10423
10459
|
}
|
|
10424
10460
|
return { foldPaths, tolerated, predicted };
|
|
10425
10461
|
}
|
|
10426
|
-
async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted) {
|
|
10462
|
+
async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predicted, preFold) {
|
|
10427
10463
|
await deps.run("git", ["checkout", "main"]);
|
|
10428
10464
|
await ffOnlyPull(deps, "main");
|
|
10465
|
+
preFold.mainSha = clean(await deps.run("git", ["rev-parse", "main"]));
|
|
10429
10466
|
if (predicted.length === 0) {
|
|
10430
10467
|
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
10431
10468
|
} else {
|
|
10432
10469
|
await mergeWithToleratedResolution(deps, sourceRef, mergeLabel, "theirs", tolerated);
|
|
10433
10470
|
}
|
|
10434
10471
|
}
|
|
10435
|
-
async function
|
|
10436
|
-
const
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10472
|
+
async function probeFoldFailureState(deps) {
|
|
10473
|
+
const branch = await currentBranch(deps);
|
|
10474
|
+
const mergeInProgress = await deps.run("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).then(() => true).catch(() => false);
|
|
10475
|
+
const dirty = porcelainHasBlockingChanges(await deps.run("git", ["status", "--porcelain"]).catch(() => ""));
|
|
10476
|
+
const mainSha = await deps.run("git", ["rev-parse", "main"]).then(clean).catch(() => "");
|
|
10477
|
+
const originMainSha = await deps.run("git", ["rev-parse", "origin/main"]).then(clean).catch(() => "");
|
|
10478
|
+
const originIsAncestorOfMain = Boolean(mainSha) && Boolean(originMainSha) ? await deps.run("git", ["merge-base", "--is-ancestor", "origin/main", "main"]).then(() => true).catch(() => false) : false;
|
|
10479
|
+
return { branch, mergeInProgress, dirty, mainSha, originMainSha, originIsAncestorOfMain };
|
|
10480
|
+
}
|
|
10481
|
+
function shaLabel(sha) {
|
|
10482
|
+
return sha ? sha.slice(0, 7) : "(unknown)";
|
|
10483
|
+
}
|
|
10484
|
+
async function finishFoldAutoRestore(deps, causeMessage, startBranch, what) {
|
|
10485
|
+
try {
|
|
10486
|
+
await deps.run("git", ["checkout", startBranch]);
|
|
10487
|
+
} catch (e) {
|
|
10488
|
+
return new Error(
|
|
10489
|
+
`${causeMessage}
|
|
10490
|
+
|
|
10491
|
+
fold failed; ${what}, but returning to ${startBranch} afterwards failed: ${e instanceof Error ? e.message : String(e)}. Finish manually: git checkout ${startBranch}`
|
|
10492
|
+
);
|
|
10493
|
+
}
|
|
10494
|
+
return new Error(
|
|
10495
|
+
`${causeMessage}
|
|
10496
|
+
|
|
10497
|
+
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
10498
|
);
|
|
10443
|
-
|
|
10444
|
-
|
|
10499
|
+
}
|
|
10500
|
+
function foldFailureGuidance(causeMessage, probe, startBranch, preFoldMainSha) {
|
|
10501
|
+
const steps = preFoldMainSha ? [
|
|
10502
|
+
` 1. git status # inspect what is actually there`,
|
|
10503
|
+
` 2. git log --oneline ${preFoldMainSha}..main # commits beyond the pre-fold main \u2014 confirm they are only this run's merge/fold attempt`,
|
|
10504
|
+
` 3. git checkout main && git reset --hard ${preFoldMainSha} # back to the exact commit the fold built on`,
|
|
10505
|
+
` 4. git checkout ${startBranch}`
|
|
10506
|
+
] : [
|
|
10507
|
+
` 1. git status # inspect what is actually there`,
|
|
10508
|
+
` 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`,
|
|
10509
|
+
` 3. git checkout ${startBranch}`
|
|
10510
|
+
];
|
|
10511
|
+
return new Error(
|
|
10512
|
+
`${causeMessage}
|
|
10513
|
+
|
|
10514
|
+
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.
|
|
10515
|
+
Recovery sequence:
|
|
10516
|
+
${steps.join("\n")}`
|
|
10517
|
+
);
|
|
10518
|
+
}
|
|
10519
|
+
async function recoverFailedFold(deps, cause, startBranch, preFoldMainSha) {
|
|
10520
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
10521
|
+
const probe = await probeFoldFailureState(deps);
|
|
10522
|
+
if (probe.mergeInProgress) {
|
|
10523
|
+
try {
|
|
10524
|
+
await deps.run("git", ["merge", "--abort"]);
|
|
10525
|
+
} catch (e) {
|
|
10526
|
+
return new Error(
|
|
10527
|
+
`${causeMessage}
|
|
10528
|
+
|
|
10529
|
+
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.
|
|
10530
|
+
Recovery sequence:
|
|
10531
|
+
1. git merge --abort
|
|
10532
|
+
2. git checkout ${startBranch}`
|
|
10533
|
+
);
|
|
10534
|
+
}
|
|
10535
|
+
return finishFoldAutoRestore(deps, causeMessage, startBranch, "the in-progress merge was aborted");
|
|
10536
|
+
}
|
|
10537
|
+
if (probe.branch === "main" && preFoldMainSha) {
|
|
10538
|
+
const mainDescendsFromPreFold = await deps.run("git", ["merge-base", "--is-ancestor", preFoldMainSha, "main"]).then(() => true).catch(() => false);
|
|
10539
|
+
if (mainDescendsFromPreFold) {
|
|
10540
|
+
try {
|
|
10541
|
+
await deps.run("git", ["reset", "--hard", preFoldMainSha]);
|
|
10542
|
+
} catch (e) {
|
|
10543
|
+
return new Error(
|
|
10544
|
+
`${causeMessage}
|
|
10545
|
+
|
|
10546
|
+
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.
|
|
10547
|
+
Recovery sequence:
|
|
10548
|
+
1. git reset --hard ${preFoldMainSha}
|
|
10549
|
+
2. git checkout ${startBranch}`
|
|
10550
|
+
);
|
|
10551
|
+
}
|
|
10552
|
+
const aheadNote = probe.originMainSha && preFoldMainSha !== probe.originMainSha ? `; local main carried commit(s) origin/main (${shaLabel(probe.originMainSha)}) does not have \u2014 they were preserved` : "";
|
|
10553
|
+
return finishFoldAutoRestore(
|
|
10554
|
+
deps,
|
|
10555
|
+
causeMessage,
|
|
10556
|
+
startBranch,
|
|
10557
|
+
`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}`
|
|
10558
|
+
);
|
|
10559
|
+
}
|
|
10560
|
+
}
|
|
10561
|
+
return foldFailureGuidance(causeMessage, probe, startBranch, preFoldMainSha);
|
|
10562
|
+
}
|
|
10563
|
+
async function runFoldStage(deps, startBranch, preFold, fn) {
|
|
10564
|
+
try {
|
|
10565
|
+
return await fn();
|
|
10566
|
+
} catch (e) {
|
|
10567
|
+
throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
|
|
10568
|
+
}
|
|
10445
10569
|
}
|
|
10446
10570
|
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
|
|
10447
10571
|
await ensureTagPushed(deps, tag, releaseSha);
|
|
@@ -10534,15 +10658,20 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
10534
10658
|
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
10535
10659
|
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
10536
10660
|
const rcShaAtRelease = !directTrack && hasRcBranch ? clean(await deps.run("git", ["rev-parse", "origin/rc"])) : "";
|
|
10537
|
-
const { foldPaths: foldPaths2 } = await
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10661
|
+
const { foldPaths: foldPaths2, tolerated: tolerated2, predicted: predicted2 } = await preflightMergeToMain(
|
|
10662
|
+
deps,
|
|
10663
|
+
deployModel2,
|
|
10664
|
+
"origin/development",
|
|
10665
|
+
"development -> main merge would conflict on untolerated path(s)",
|
|
10666
|
+
"The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
|
|
10667
|
+
);
|
|
10668
|
+
const preFold2 = {};
|
|
10669
|
+
const { versionFold: versionFold2, releaseSha: releaseSha2 } = await runFoldStage(deps, "development", preFold2, async () => {
|
|
10670
|
+
await executeMergeToMain(deps, "development", "development -> main", tolerated2, predicted2, preFold2);
|
|
10671
|
+
const versionFold3 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
10672
|
+
const releaseSha3 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10673
|
+
return { versionFold: versionFold3, releaseSha: releaseSha3 };
|
|
10543
10674
|
});
|
|
10544
|
-
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
10545
|
-
const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10546
10675
|
const { checks: checks2, releaseUrl: releaseUrl2, announceNote: announceNote2, dispatch: d2 } = await completeMainRelease(deps, ctx, meta, deployModel2, watch, options, tag2, releaseSha2);
|
|
10547
10676
|
const devRollForward2 = await rollDevelopmentForward(deps, ctx, tag2);
|
|
10548
10677
|
if (directTrack) {
|
|
@@ -10619,10 +10748,14 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
10619
10748
|
);
|
|
10620
10749
|
}
|
|
10621
10750
|
const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
10622
|
-
|
|
10623
|
-
const tag =
|
|
10624
|
-
|
|
10625
|
-
|
|
10751
|
+
const preFold = {};
|
|
10752
|
+
const { tag, versionFold, releaseSha } = await runFoldStage(deps, "rc", preFold, async () => {
|
|
10753
|
+
await executeMergeToMain(deps, "rc", "rc -> main", tolerated, predicted, preFold);
|
|
10754
|
+
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
10755
|
+
const versionFold2 = await foldReleaseVersion(deps, deployModel, tag2, foldPaths);
|
|
10756
|
+
const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
10757
|
+
return { tag: tag2, versionFold: versionFold2, releaseSha: releaseSha2 };
|
|
10758
|
+
});
|
|
10626
10759
|
const { checks, releaseUrl, announceNote, dispatch: d } = await completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha);
|
|
10627
10760
|
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
10628
10761
|
const devRollForward = await rollDevelopmentForward(deps, ctx, tag);
|
|
@@ -12389,6 +12522,19 @@ async function fetchTrainAuthority(repo, deps) {
|
|
|
12389
12522
|
return { ok: false, error: e.message };
|
|
12390
12523
|
}
|
|
12391
12524
|
}
|
|
12525
|
+
async function mintWikiToken(repo, deps) {
|
|
12526
|
+
const res = await postJson("/wiki-mint", { repo }, deps, "POST", { noRetry: true });
|
|
12527
|
+
if (res.error) return { ok: false, error: res.error };
|
|
12528
|
+
const body = res.body ?? {};
|
|
12529
|
+
if (res.status === 426) return { ok: false, error: upgradeRequiredError({ status: 426 }, res.body) };
|
|
12530
|
+
if (res.status === 403) return { ok: false, error: `master-admin only (HTTP 403)${body.error ? ` \u2014 ${body.error}` : ""}` };
|
|
12531
|
+
if (res.status === 404) {
|
|
12532
|
+
return { ok: false, error: "the Hub API did not recognize /wiki-mint (HTTP 404) \u2014 the deployed Hub predates this command; it answers once a Hub release carrying it is deployed" };
|
|
12533
|
+
}
|
|
12534
|
+
if (!res.ok) return { ok: false, error: `wiki-mint HTTP ${res.status}${body.error ? ` \u2014 ${body.error}` : ""}` };
|
|
12535
|
+
if (!body.token || !body.expiresAt) return { ok: false, error: "malformed wiki-mint response (no token)" };
|
|
12536
|
+
return { ok: true, mint: { token: body.token, expiresAt: body.expiresAt, repository: body.repository ?? repo, permissions: body.permissions ?? {} } };
|
|
12537
|
+
}
|
|
12392
12538
|
async function fetchProjectsList(deps) {
|
|
12393
12539
|
if (!deps.baseUrl) return null;
|
|
12394
12540
|
const token = await deps.token();
|
|
@@ -12526,6 +12672,32 @@ async function tenantDeploy(payload, deps) {
|
|
|
12526
12672
|
return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS });
|
|
12527
12673
|
}
|
|
12528
12674
|
|
|
12675
|
+
// src/wiki-publish-command.ts
|
|
12676
|
+
async function wikiPublish(deps, opts) {
|
|
12677
|
+
if (!opts.pagesDir) {
|
|
12678
|
+
deps.err("wiki publish: <pages-dir> is required");
|
|
12679
|
+
return false;
|
|
12680
|
+
}
|
|
12681
|
+
const script = deps.scriptPath();
|
|
12682
|
+
if (!deps.scriptExists(script)) {
|
|
12683
|
+
deps.err(`wiki publish: publish script not found at ${script} \u2014 run from the repo checkout root`);
|
|
12684
|
+
return false;
|
|
12685
|
+
}
|
|
12686
|
+
const minted = await deps.mint(opts.repo);
|
|
12687
|
+
if (!minted.ok) {
|
|
12688
|
+
deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
|
|
12689
|
+
return false;
|
|
12690
|
+
}
|
|
12691
|
+
const env = { ...process.env, WIKI_REPO: opts.repo, WIKI_TOKEN: minted.mint.token };
|
|
12692
|
+
const code = deps.spawn("node", [script, opts.pagesDir], env);
|
|
12693
|
+
if (code !== 0) {
|
|
12694
|
+
deps.err(`wiki publish failed: publish script exited ${code} (token was scoped to ${opts.repo}, contents:write, expires ${minted.mint.expiresAt})`);
|
|
12695
|
+
return false;
|
|
12696
|
+
}
|
|
12697
|
+
deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
|
|
12698
|
+
return true;
|
|
12699
|
+
}
|
|
12700
|
+
|
|
12529
12701
|
// src/project-readiness.ts
|
|
12530
12702
|
function stagesForTrack(meta) {
|
|
12531
12703
|
return branchesForTrack(resolveReleaseTrack(meta)).map((b) => b === "development" ? "dev" : b);
|
|
@@ -13669,7 +13841,9 @@ function vaultPointer(slug) {
|
|
|
13669
13841
|
slug,
|
|
13670
13842
|
root,
|
|
13671
13843
|
tiers: {
|
|
13672
|
-
|
|
13844
|
+
// #2482: a stageless catalog entry (stages: []) lives at the slug ROOT — one value per repo —
|
|
13845
|
+
// alongside the staged tree for per-stage entries. Both are the same project-admin wall (#2032).
|
|
13846
|
+
project: `${root}/<KEY> (stageless, one value per repo) + ${root}/{dev,rc,main}/* (staged entries) (project-admin self-serve for this repo)`,
|
|
13673
13847
|
org: [`/mmi-future/{shared,cloudflare,mmi-hub,...}/* (org-infra, master-gated)`]
|
|
13674
13848
|
},
|
|
13675
13849
|
stages: ["dev", "rc", "main"],
|
|
@@ -14806,16 +14980,24 @@ function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
|
|
|
14806
14980
|
return false;
|
|
14807
14981
|
});
|
|
14808
14982
|
}
|
|
14983
|
+
function cursorPluginCacheSeedTargets(pins, releasedVersion, cacheRoot, cacheRootExists = false) {
|
|
14984
|
+
const needing = cursorPluginPinsNeedingSeed(pins, releasedVersion);
|
|
14985
|
+
if (needing.length > 0) return needing.map((pin) => pin.path);
|
|
14986
|
+
if (pins.length === 0 && cacheRoot && cacheRootExists && isSemverVersion(releasedVersion)) {
|
|
14987
|
+
return [(0, import_node_path15.join)(cacheRoot, `v${releasedVersion.replace(/^v/, "")}`)];
|
|
14988
|
+
}
|
|
14989
|
+
return [];
|
|
14990
|
+
}
|
|
14809
14991
|
async function applyCursorPluginCacheSeed(input) {
|
|
14810
14992
|
if (!isSemverVersion(input.releasedVersion)) return false;
|
|
14811
|
-
const
|
|
14812
|
-
if (
|
|
14993
|
+
const targets = cursorPluginCacheSeedTargets(input.pins, input.releasedVersion, input.cacheRoot, input.cacheRootExists);
|
|
14994
|
+
if (targets.length === 0) return false;
|
|
14813
14995
|
const tmpRoot = await input.mkdtemp("mmi-cursor-seed-");
|
|
14814
14996
|
const source = await resolvePluginMmiSource(input.releasedVersion, input.hubCheckout, tmpRoot, input.execFileP);
|
|
14815
14997
|
if (!source) return false;
|
|
14816
14998
|
input.log(` \u21BB seeding Cursor MMI plugin cache \u2192 ${input.releasedVersion}\u2026`);
|
|
14817
|
-
for (const
|
|
14818
|
-
syncDirContents(source,
|
|
14999
|
+
for (const dest of targets) {
|
|
15000
|
+
syncDirContents(source, dest);
|
|
14819
15001
|
}
|
|
14820
15002
|
(0, import_node_fs14.rmSync)(tmpRoot, { recursive: true, force: true });
|
|
14821
15003
|
return true;
|
|
@@ -15095,6 +15277,33 @@ function buildMmiPluginCacheCleanupCheck(input) {
|
|
|
15095
15277
|
}))
|
|
15096
15278
|
};
|
|
15097
15279
|
}
|
|
15280
|
+
var CURSOR_PLUGIN_CACHE_CLEANUP_LABEL = "leftover older MMI plugin cache pins (Cursor)";
|
|
15281
|
+
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";
|
|
15282
|
+
function buildCursorPluginCacheCleanupCheck(input) {
|
|
15283
|
+
const base = {
|
|
15284
|
+
ok: true,
|
|
15285
|
+
label: CURSOR_PLUGIN_CACHE_CLEANUP_LABEL,
|
|
15286
|
+
fix: CURSOR_PLUGIN_CACHE_CLEANUP_FIX
|
|
15287
|
+
};
|
|
15288
|
+
if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
|
|
15289
|
+
const semverPins = input.pins.filter((pin) => isSemverVersion2(pin.version));
|
|
15290
|
+
if (semverPins.length === 0) return base;
|
|
15291
|
+
const activeVersion = semverPins.map((pin) => pin.version).reduce((a, b) => compareVersions(a, b) >= 0 ? a : b);
|
|
15292
|
+
const protectedVersions = new Set(
|
|
15293
|
+
[normalizeVersion(input.releasedVersion), normalizeVersion(activeVersion)].filter((v) => Boolean(v))
|
|
15294
|
+
);
|
|
15295
|
+
const leftoverPins = semverPins.filter((pin) => !protectedVersions.has(normalizeVersion(pin.version)));
|
|
15296
|
+
if (leftoverPins.length === 0) return base;
|
|
15297
|
+
const leftovers = leftoverPins.map((pin) => ({ surface: "cursor", root: input.cacheRoot, name: pin.name, path: pin.path }));
|
|
15298
|
+
const listed = leftoverPins.map((pin) => `${pin.name} (${pin.version})`).join(", ");
|
|
15299
|
+
return {
|
|
15300
|
+
...base,
|
|
15301
|
+
ok: false,
|
|
15302
|
+
severityOverride: "advisory",
|
|
15303
|
+
leftovers,
|
|
15304
|
+
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`
|
|
15305
|
+
};
|
|
15306
|
+
}
|
|
15098
15307
|
var NESTED_PLUGIN_TREE_LABEL = "self-nested MMI plugin cache tree (#1126)";
|
|
15099
15308
|
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
15309
|
function nestedPluginTreeCleanupCommand(paths, isWindows) {
|
|
@@ -15117,7 +15326,21 @@ function buildNestedPluginTreeCheck(input) {
|
|
|
15117
15326
|
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
15327
|
};
|
|
15119
15328
|
}
|
|
15329
|
+
function nestedTreeReinstallGapFix(surfaces) {
|
|
15330
|
+
const recoveries = surfaces.map((s) => PLUGIN_SURFACE_HEAL[s].recovery).join(" and ");
|
|
15331
|
+
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}`;
|
|
15332
|
+
}
|
|
15120
15333
|
var CODEX_ACTIVE_CACHE_LABEL = "Codex active plugin cache (vs latest release)";
|
|
15334
|
+
function codexEnabledPluginVersionFromList(jsonText, pluginId = MMI_PLUGIN_ID) {
|
|
15335
|
+
try {
|
|
15336
|
+
const parsed = JSON.parse(jsonText);
|
|
15337
|
+
const rows = Array.isArray(parsed?.installed) ? parsed.installed : [];
|
|
15338
|
+
const row = rows.find((r) => r?.pluginId === pluginId && r.installed === true && r.enabled === true);
|
|
15339
|
+
return isSemverVersion2(row?.version) ? row.version.trim().replace(/^v/, "") : void 0;
|
|
15340
|
+
} catch {
|
|
15341
|
+
return void 0;
|
|
15342
|
+
}
|
|
15343
|
+
}
|
|
15121
15344
|
function buildCodexActiveCacheCheck(input) {
|
|
15122
15345
|
const base = {
|
|
15123
15346
|
ok: true,
|
|
@@ -15125,6 +15348,19 @@ function buildCodexActiveCacheCheck(input) {
|
|
|
15125
15348
|
fix: CODEX_PLUGIN_RECOVERY
|
|
15126
15349
|
};
|
|
15127
15350
|
if (!input.isOrgRepo || !isSemverVersion2(input.releasedVersion)) return base;
|
|
15351
|
+
if (input.codexPresent === false) return base;
|
|
15352
|
+
if (isSemverVersion2(input.codexActiveVersion)) {
|
|
15353
|
+
if (compareVersions(input.codexActiveVersion, input.releasedVersion) < 0) {
|
|
15354
|
+
return {
|
|
15355
|
+
...base,
|
|
15356
|
+
ok: false,
|
|
15357
|
+
activeCacheVersion: input.codexActiveVersion,
|
|
15358
|
+
releasedVersion: input.releasedVersion,
|
|
15359
|
+
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}`
|
|
15360
|
+
};
|
|
15361
|
+
}
|
|
15362
|
+
return { ...base, activeCacheVersion: input.codexActiveVersion, releasedVersion: input.releasedVersion };
|
|
15363
|
+
}
|
|
15128
15364
|
if (isSemverVersion2(input.codexRecordVersion) && compareVersions(input.codexRecordVersion, input.releasedVersion) < 0) {
|
|
15129
15365
|
return base;
|
|
15130
15366
|
}
|
|
@@ -15172,6 +15408,55 @@ function reloadAction(surface) {
|
|
|
15172
15408
|
return "restart Claude Code (or run /reload-plugins)";
|
|
15173
15409
|
}
|
|
15174
15410
|
}
|
|
15411
|
+
function restartAction(surface) {
|
|
15412
|
+
switch (surface) {
|
|
15413
|
+
case "claude-vscode":
|
|
15414
|
+
return "fully restart VS Code";
|
|
15415
|
+
case "codex":
|
|
15416
|
+
return "fully restart Codex";
|
|
15417
|
+
case "opencode":
|
|
15418
|
+
return "fully restart OpenCode";
|
|
15419
|
+
case "cursor":
|
|
15420
|
+
return "fully restart Cursor";
|
|
15421
|
+
case "claude-cli":
|
|
15422
|
+
case "shell":
|
|
15423
|
+
default:
|
|
15424
|
+
return "fully restart Claude Code";
|
|
15425
|
+
}
|
|
15426
|
+
}
|
|
15427
|
+
function parseSessionSkillRootVersion(pluginRoot) {
|
|
15428
|
+
const trimmed = pluginRoot?.trim();
|
|
15429
|
+
if (!trimmed) return void 0;
|
|
15430
|
+
const segments = trimmed.split(/[\\/]+/).filter(Boolean);
|
|
15431
|
+
const mmiIdx = segments.findIndex((seg, i) => seg === "mmi" && segments[i - 1] === "mutmutco");
|
|
15432
|
+
if (mmiIdx < 0) return void 0;
|
|
15433
|
+
const candidate = segments[mmiIdx + 1];
|
|
15434
|
+
return candidate && /^v?\d+\.\d+\.\d+/.test(candidate) ? candidate.replace(/^v/, "") : void 0;
|
|
15435
|
+
}
|
|
15436
|
+
var SESSION_SKILL_ROOT_LABEL = "Session-loaded MMI skill roots (vs on-disk plugin cache)";
|
|
15437
|
+
function buildSessionSkillRootCheck(input) {
|
|
15438
|
+
const base = {
|
|
15439
|
+
ok: true,
|
|
15440
|
+
label: SESSION_SKILL_ROOT_LABEL,
|
|
15441
|
+
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`
|
|
15442
|
+
};
|
|
15443
|
+
if (!input.isOrgRepo) return base;
|
|
15444
|
+
const sessionVersion = parseSessionSkillRootVersion(input.pluginRoot);
|
|
15445
|
+
if (!sessionVersion) return base;
|
|
15446
|
+
const activeCacheVersion = highestSemver(input.cacheVersions ?? []);
|
|
15447
|
+
if (!isSemverVersion2(activeCacheVersion)) return { ...base, sessionVersion };
|
|
15448
|
+
if (compareVersions(sessionVersion, activeCacheVersion) >= 0) {
|
|
15449
|
+
return { ...base, sessionVersion, activeCacheVersion };
|
|
15450
|
+
}
|
|
15451
|
+
return {
|
|
15452
|
+
...base,
|
|
15453
|
+
ok: false,
|
|
15454
|
+
severityOverride: "advisory",
|
|
15455
|
+
sessionVersion,
|
|
15456
|
+
activeCacheVersion,
|
|
15457
|
+
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.`
|
|
15458
|
+
};
|
|
15459
|
+
}
|
|
15175
15460
|
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
15461
|
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
15462
|
var CURSOR_RECOVERY = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace";
|
|
@@ -15342,12 +15627,13 @@ function buildPluginUpdateReport(input) {
|
|
|
15342
15627
|
...codexMarketplace ? { codexMarketplace } : {},
|
|
15343
15628
|
...codexActiveCache ? { codexActiveCache } : {},
|
|
15344
15629
|
...opencodePlugin ? { opencodePlugin } : {},
|
|
15345
|
-
...isSemverVersion2(input.releasedVersion) ? { released: input.releasedVersion } : {}
|
|
15630
|
+
...isSemverVersion2(input.releasedVersion) ? { released: input.releasedVersion } : {},
|
|
15631
|
+
...input.releasedVersionSource ? { releasedSource: input.releasedVersionSource } : {}
|
|
15346
15632
|
},
|
|
15347
15633
|
recipes: PLUGIN_UPDATE_RECIPES
|
|
15348
15634
|
};
|
|
15349
15635
|
}
|
|
15350
|
-
function
|
|
15636
|
+
function renderPluginVersionBlock(report) {
|
|
15351
15637
|
const v = report.versions;
|
|
15352
15638
|
const show = (x) => x ?? "unknown";
|
|
15353
15639
|
const versionRows = [
|
|
@@ -15358,16 +15644,10 @@ function renderPluginUpdateReport(report) {
|
|
|
15358
15644
|
["OpenCode plugin", show(v.opencodePlugin)]
|
|
15359
15645
|
];
|
|
15360
15646
|
const pad = Math.max(...versionRows.map(([label]) => label.length));
|
|
15361
|
-
|
|
15647
|
+
return [
|
|
15362
15648
|
`MMI versions (target release: ${show(v.released)})`,
|
|
15363
|
-
...versionRows.map(([label, value]) => ` ${label.padEnd(pad)} ${value}`)
|
|
15364
|
-
"",
|
|
15365
|
-
"Update commands by surface"
|
|
15649
|
+
...versionRows.map(([label, value]) => ` ${label.padEnd(pad)} ${value}`)
|
|
15366
15650
|
];
|
|
15367
|
-
for (const surface of PLUGIN_GUIDE_SURFACES) {
|
|
15368
|
-
lines.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
|
|
15369
|
-
}
|
|
15370
|
-
return lines;
|
|
15371
15651
|
}
|
|
15372
15652
|
function buildDoctorJsonPayload(input) {
|
|
15373
15653
|
return {
|
|
@@ -15703,6 +15983,9 @@ function cursorPluginInstallFix(input) {
|
|
|
15703
15983
|
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
15984
|
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
15985
|
}
|
|
15986
|
+
function cursorSeededFromNothingAdvisoryFix(releasedVersion) {
|
|
15987
|
+
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}`;
|
|
15988
|
+
}
|
|
15706
15989
|
function buildCursorPluginInstallCheck(input) {
|
|
15707
15990
|
const base = {
|
|
15708
15991
|
ok: true,
|
|
@@ -15820,9 +16103,7 @@ function buildHubDeployFreshnessCheck(input) {
|
|
|
15820
16103
|
};
|
|
15821
16104
|
}
|
|
15822
16105
|
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
16106
|
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
16107
|
function textHasPlaywrightMcp(content) {
|
|
15827
16108
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
15828
16109
|
return /@playwright\/mcp/.test(normalized) || /mcp_servers\.playwright/.test(normalized) || /"playwright"\s*:\s*\{/.test(normalized) || /\bmcpServers\b/.test(normalized);
|
|
@@ -15837,45 +16118,10 @@ function textHasPlaywrightVisionCap(content) {
|
|
|
15837
16118
|
if (/\bvision[-_]?(?:first|only|mode)\b/i.test(normalized)) return true;
|
|
15838
16119
|
return false;
|
|
15839
16120
|
}
|
|
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
16121
|
function textHasCanonicalPlaywrightOutputDir(content) {
|
|
15857
16122
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
15858
16123
|
return /--output-dir(?:\s*=\s*|["'\s,]+)tmp\/playwright-mcp\b/.test(normalized);
|
|
15859
16124
|
}
|
|
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
16125
|
var STRAY_BROWSER_ARTIFACT_DIRS = [".playwright-mcp", "playwright-report", "test-results"];
|
|
15880
16126
|
var BROWSER_ARTIFACTS_LABEL = "browser MCP artifacts outside tmp/ (use tmp/playwright-mcp)";
|
|
15881
16127
|
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 +16161,12 @@ function buildSelfUpdateHaltPayload(input) {
|
|
|
15915
16161
|
checks: input.checks
|
|
15916
16162
|
};
|
|
15917
16163
|
}
|
|
16164
|
+
var DOCTOR_SELF_UPDATE_HALT_EXIT_CODE = 3;
|
|
16165
|
+
var DOCTOR_SELF_UPDATE_HALT_SENTINEL = "MMI_DOCTOR_SELF_UPDATE_HALT";
|
|
16166
|
+
function selfUpdateHaltSentinelLine(input) {
|
|
16167
|
+
const to = input.report.releasedVersion ?? "latest";
|
|
16168
|
+
return `${DOCTOR_SELF_UPDATE_HALT_SENTINEL} rerunRequired=true updatedTo=${to} rerun="mmi-cli ${input.rerunArgs.join(" ")}"`;
|
|
16169
|
+
}
|
|
15918
16170
|
var DOCTOR_POST_SELF_UPDATE_ENV = "MMI_DOCTOR_POST_SELF_UPDATE";
|
|
15919
16171
|
function buildSelfUpdateReexecArgs(opts) {
|
|
15920
16172
|
const args = ["doctor"];
|
|
@@ -16029,7 +16281,7 @@ function doctorHumanLines(input) {
|
|
|
16029
16281
|
lines.push("", DOCTOR_VERBOSE_HINT);
|
|
16030
16282
|
}
|
|
16031
16283
|
if (!input.verbose) return lines;
|
|
16032
|
-
lines.push("", ...
|
|
16284
|
+
lines.push("", ...renderPluginVersionBlock(input.updateReport));
|
|
16033
16285
|
lines.push(
|
|
16034
16286
|
"",
|
|
16035
16287
|
doctorSummaryLine({ checks: input.checks, updateReport: input.updateReport, shouldApply: input.shouldApply, healedCount: input.healedCount }),
|
|
@@ -16102,6 +16354,313 @@ function buildPluginResolvabilityCheck(input) {
|
|
|
16102
16354
|
}
|
|
16103
16355
|
}
|
|
16104
16356
|
|
|
16357
|
+
// src/mcp-reconcile.ts
|
|
16358
|
+
var PLAYWRIGHT_MCP_SPEC = {
|
|
16359
|
+
name: "playwright",
|
|
16360
|
+
command: "npx",
|
|
16361
|
+
args: ["-y", "@playwright/mcp@latest", "--output-dir", "tmp/playwright-mcp"],
|
|
16362
|
+
legacyNames: []
|
|
16363
|
+
};
|
|
16364
|
+
var MCP_RECONCILE_LABEL = "MCP server registrations (org-managed Playwright)";
|
|
16365
|
+
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";
|
|
16366
|
+
function extractServerBlock(content, format, spec) {
|
|
16367
|
+
return format === "json" ? extractJsonServer(content, spec) : extractTomlServer(content, spec);
|
|
16368
|
+
}
|
|
16369
|
+
function extractJsonServer(content, spec) {
|
|
16370
|
+
let parsed;
|
|
16371
|
+
try {
|
|
16372
|
+
parsed = JSON.parse(content);
|
|
16373
|
+
} catch {
|
|
16374
|
+
return { kind: "unparseable" };
|
|
16375
|
+
}
|
|
16376
|
+
if (parsed == null || typeof parsed !== "object") return { kind: "unparseable" };
|
|
16377
|
+
const servers = parsed.mcpServers;
|
|
16378
|
+
if (servers == null || typeof servers !== "object") return { kind: "absent" };
|
|
16379
|
+
const bag = servers;
|
|
16380
|
+
for (const name of [spec.name, ...spec.legacyNames]) {
|
|
16381
|
+
if (Object.prototype.hasOwnProperty.call(bag, name)) {
|
|
16382
|
+
return { kind: "found", matchedName: name, isLegacy: name !== spec.name, blockText: JSON.stringify(bag[name]) };
|
|
16383
|
+
}
|
|
16384
|
+
}
|
|
16385
|
+
return { kind: "absent" };
|
|
16386
|
+
}
|
|
16387
|
+
function extractTomlServer(content, spec) {
|
|
16388
|
+
const blocks = parseTomlBlocks(content.replace(/\r\n/g, "\n").split("\n"));
|
|
16389
|
+
for (const name of [spec.name, ...spec.legacyNames]) {
|
|
16390
|
+
const full = `mcp_servers.${name}`;
|
|
16391
|
+
const block = blocks.find((b) => b.name === full && !b.isArrayTable);
|
|
16392
|
+
if (block) {
|
|
16393
|
+
const blockText = [block.headerRaw, ...block.body].filter((l) => l != null).join("\n");
|
|
16394
|
+
return { kind: "found", matchedName: name, isLegacy: name !== spec.name, blockText };
|
|
16395
|
+
}
|
|
16396
|
+
}
|
|
16397
|
+
return { kind: "absent" };
|
|
16398
|
+
}
|
|
16399
|
+
function detectMcpState(target, spec) {
|
|
16400
|
+
if (!target.present) return { target, state: "skip", action: "skip", reasons: [] };
|
|
16401
|
+
if (target.format === "claude-cli") return detectClaudeCliMcpState(target, spec);
|
|
16402
|
+
if (target.content == null) {
|
|
16403
|
+
if (target.createWhenFileAbsent) return { target, state: "absent", action: "install", reasons: [] };
|
|
16404
|
+
return { target, state: "skip", action: "skip", reasons: [] };
|
|
16405
|
+
}
|
|
16406
|
+
const extract = extractServerBlock(target.content, target.format, spec);
|
|
16407
|
+
if (extract.kind === "unparseable") {
|
|
16408
|
+
return { target, state: "drifted", action: "reconcile", reasons: ["unparseable"] };
|
|
16409
|
+
}
|
|
16410
|
+
if (extract.kind === "absent") {
|
|
16411
|
+
return { target, state: "absent", action: "install", reasons: [] };
|
|
16412
|
+
}
|
|
16413
|
+
const reasons = [];
|
|
16414
|
+
if (extract.isLegacy) reasons.push("legacy-name");
|
|
16415
|
+
if (textHasPlaywrightVisionCap(extract.blockText)) reasons.push("vision-cap");
|
|
16416
|
+
if (!textHasCanonicalPlaywrightOutputDir(extract.blockText)) reasons.push("output-dir");
|
|
16417
|
+
if (reasons.length === 0) {
|
|
16418
|
+
return { target, state: "healthy", action: "none", reasons: [], matchedName: extract.matchedName };
|
|
16419
|
+
}
|
|
16420
|
+
return { target, state: "drifted", action: "reconcile", reasons, matchedName: extract.matchedName };
|
|
16421
|
+
}
|
|
16422
|
+
function planMcpReconcile(targets, spec) {
|
|
16423
|
+
return targets.map((t) => detectMcpState(t, spec));
|
|
16424
|
+
}
|
|
16425
|
+
function parseClaudeMcpGetOutput(stdout) {
|
|
16426
|
+
const commandMatch = /^\s*Command:\s*(.+)$/m.exec(stdout);
|
|
16427
|
+
if (commandMatch == null) return null;
|
|
16428
|
+
const argsMatch = /^\s*Args:\s*(.*)$/m.exec(stdout);
|
|
16429
|
+
const env = {};
|
|
16430
|
+
const lines = stdout.replace(/\r\n/g, "\n").split("\n");
|
|
16431
|
+
const headerIdx = lines.findIndex((l) => /^\s*Environment:\s*$/.test(l));
|
|
16432
|
+
if (headerIdx !== -1) {
|
|
16433
|
+
for (const line of lines.slice(headerIdx + 1)) {
|
|
16434
|
+
if (line.trim() === "") continue;
|
|
16435
|
+
if (!/^[ \t]/.test(line)) break;
|
|
16436
|
+
const kv = /^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
|
|
16437
|
+
if (kv != null) env[kv[1]] = kv[2];
|
|
16438
|
+
}
|
|
16439
|
+
}
|
|
16440
|
+
return { command: commandMatch[1].trim(), args: (argsMatch?.[1] ?? "").trim(), env };
|
|
16441
|
+
}
|
|
16442
|
+
function detectClaudeCliMcpState(target, spec) {
|
|
16443
|
+
if (target.content == null) {
|
|
16444
|
+
return { target, state: "absent", action: "install", reasons: [] };
|
|
16445
|
+
}
|
|
16446
|
+
const matchedName = target.claudeCliMatchedName ?? spec.name;
|
|
16447
|
+
const isLegacy = matchedName !== spec.name;
|
|
16448
|
+
const parsed = parseClaudeMcpGetOutput(target.content);
|
|
16449
|
+
if (parsed == null) {
|
|
16450
|
+
const reasons2 = isLegacy ? ["legacy-name", "unparseable"] : ["unparseable"];
|
|
16451
|
+
return { target, state: "drifted", action: "reconcile", reasons: reasons2, matchedName };
|
|
16452
|
+
}
|
|
16453
|
+
const reasons = [];
|
|
16454
|
+
if (isLegacy) reasons.push("legacy-name");
|
|
16455
|
+
if (textHasPlaywrightVisionCap(parsed.args)) reasons.push("vision-cap");
|
|
16456
|
+
if (!textHasCanonicalPlaywrightOutputDir(parsed.args)) reasons.push("output-dir");
|
|
16457
|
+
if (reasons.length === 0) return { target, state: "healthy", action: "none", reasons: [], matchedName };
|
|
16458
|
+
return { target, state: "drifted", action: "reconcile", reasons, matchedName };
|
|
16459
|
+
}
|
|
16460
|
+
var CLAUDE_MCP_USER_SCOPE = "user";
|
|
16461
|
+
function planClaudeCliMcpHeal(item, spec) {
|
|
16462
|
+
if (item.action !== "install" && item.action !== "reconcile") return [];
|
|
16463
|
+
const commands = [];
|
|
16464
|
+
if (item.action === "reconcile" && item.matchedName != null) {
|
|
16465
|
+
commands.push({ op: "remove", args: ["remove", item.matchedName, "-s", CLAUDE_MCP_USER_SCOPE] });
|
|
16466
|
+
}
|
|
16467
|
+
const parsed = item.target.content != null ? parseClaudeMcpGetOutput(item.target.content) : null;
|
|
16468
|
+
const envFlags = Object.entries(parsed?.env ?? {}).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
|
|
16469
|
+
commands.push({
|
|
16470
|
+
op: "add",
|
|
16471
|
+
args: ["add", spec.name, "-s", CLAUDE_MCP_USER_SCOPE, ...envFlags, "--", spec.command, ...spec.args]
|
|
16472
|
+
});
|
|
16473
|
+
return commands;
|
|
16474
|
+
}
|
|
16475
|
+
function buildMcpReconcileCheck(plan) {
|
|
16476
|
+
const base = {
|
|
16477
|
+
ok: true,
|
|
16478
|
+
label: MCP_RECONCILE_LABEL,
|
|
16479
|
+
fix: MCP_RECONCILE_FIX,
|
|
16480
|
+
severityOverride: "advisory"
|
|
16481
|
+
};
|
|
16482
|
+
const gaps = plan.filter((p) => p.action === "install" || p.action === "reconcile");
|
|
16483
|
+
if (gaps.length === 0) return base;
|
|
16484
|
+
const detail = gaps.map((g) => `${g.target.path} (${g.action === "install" ? "missing" : g.reasons.join("+") || "drifted"})`).join(", ");
|
|
16485
|
+
return {
|
|
16486
|
+
...base,
|
|
16487
|
+
ok: false,
|
|
16488
|
+
fix: `${MCP_RECONCILE_FIX} \u2014 ${detail}`,
|
|
16489
|
+
mcpGaps: gaps.map((g) => ({ path: g.target.path, action: g.action, reasons: g.reasons }))
|
|
16490
|
+
};
|
|
16491
|
+
}
|
|
16492
|
+
function applyMcpReconcile(targets, spec, opts) {
|
|
16493
|
+
const results = [];
|
|
16494
|
+
for (const item of planMcpReconcile(targets, spec)) {
|
|
16495
|
+
if (item.action !== "install" && item.action !== "reconcile") continue;
|
|
16496
|
+
const t = item.target;
|
|
16497
|
+
if (t.isRepoFile && !opts.repoWritesAllowed) {
|
|
16498
|
+
results.push({ target: t, action: item.action, outcome: "skipped-repo-write" });
|
|
16499
|
+
continue;
|
|
16500
|
+
}
|
|
16501
|
+
const next = t.format === "json" ? renderJsonMcpConfig(t.content, spec) : renderTomlMcpConfig(t.content, spec);
|
|
16502
|
+
if (next == null) {
|
|
16503
|
+
results.push({ target: t, action: item.action, outcome: "unparseable" });
|
|
16504
|
+
continue;
|
|
16505
|
+
}
|
|
16506
|
+
results.push({ target: t, action: item.action, outcome: opts.write(t.path, next) ? "wrote" : "write-failed" });
|
|
16507
|
+
}
|
|
16508
|
+
return results;
|
|
16509
|
+
}
|
|
16510
|
+
function renderJsonMcpConfig(existing, spec) {
|
|
16511
|
+
const canonicalEntry = { command: spec.command, args: [...spec.args] };
|
|
16512
|
+
if (existing == null || existing.trim() === "") {
|
|
16513
|
+
return `${JSON.stringify({ mcpServers: { [spec.name]: canonicalEntry } }, null, 2)}
|
|
16514
|
+
`;
|
|
16515
|
+
}
|
|
16516
|
+
let parsed;
|
|
16517
|
+
try {
|
|
16518
|
+
parsed = JSON.parse(existing);
|
|
16519
|
+
} catch {
|
|
16520
|
+
return null;
|
|
16521
|
+
}
|
|
16522
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
16523
|
+
const obj = parsed;
|
|
16524
|
+
const rawServers = obj.mcpServers;
|
|
16525
|
+
if (rawServers != null && (typeof rawServers !== "object" || Array.isArray(rawServers))) return null;
|
|
16526
|
+
const servers = rawServers ?? {};
|
|
16527
|
+
const prior = servers[spec.name] ?? spec.legacyNames.map((n) => servers[n]).find((v) => v != null);
|
|
16528
|
+
for (const legacy of spec.legacyNames) delete servers[legacy];
|
|
16529
|
+
const priorObj = prior != null && typeof prior === "object" && !Array.isArray(prior) ? prior : {};
|
|
16530
|
+
servers[spec.name] = { ...priorObj, ...canonicalEntry };
|
|
16531
|
+
obj.mcpServers = servers;
|
|
16532
|
+
const text = `${JSON.stringify(obj, null, 2)}
|
|
16533
|
+
`;
|
|
16534
|
+
return existing.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
|
|
16535
|
+
}
|
|
16536
|
+
function isTomlEscaped(line, i) {
|
|
16537
|
+
let n = 0;
|
|
16538
|
+
for (let j = i - 1; j >= 0 && line[j] === "\\"; j--) n += 1;
|
|
16539
|
+
return n % 2 === 1;
|
|
16540
|
+
}
|
|
16541
|
+
function advanceTomlStringState(line, state) {
|
|
16542
|
+
let multi = state;
|
|
16543
|
+
let single = null;
|
|
16544
|
+
let i = 0;
|
|
16545
|
+
while (i < line.length) {
|
|
16546
|
+
if (multi != null) {
|
|
16547
|
+
if (line.startsWith(multi, i) && !(multi === '"""' && isTomlEscaped(line, i))) {
|
|
16548
|
+
multi = null;
|
|
16549
|
+
i += 3;
|
|
16550
|
+
} else {
|
|
16551
|
+
i += 1;
|
|
16552
|
+
}
|
|
16553
|
+
continue;
|
|
16554
|
+
}
|
|
16555
|
+
if (single != null) {
|
|
16556
|
+
if (line[i] === single && !(single === '"' && isTomlEscaped(line, i))) single = null;
|
|
16557
|
+
i += 1;
|
|
16558
|
+
continue;
|
|
16559
|
+
}
|
|
16560
|
+
if (line[i] === "#") break;
|
|
16561
|
+
if (line.startsWith('"""', i)) {
|
|
16562
|
+
multi = '"""';
|
|
16563
|
+
i += 3;
|
|
16564
|
+
continue;
|
|
16565
|
+
}
|
|
16566
|
+
if (line.startsWith("'''", i)) {
|
|
16567
|
+
multi = "'''";
|
|
16568
|
+
i += 3;
|
|
16569
|
+
continue;
|
|
16570
|
+
}
|
|
16571
|
+
if (line[i] === '"' || line[i] === "'") {
|
|
16572
|
+
single = line[i];
|
|
16573
|
+
i += 1;
|
|
16574
|
+
continue;
|
|
16575
|
+
}
|
|
16576
|
+
i += 1;
|
|
16577
|
+
}
|
|
16578
|
+
return multi;
|
|
16579
|
+
}
|
|
16580
|
+
function parseTomlBlocks(lines) {
|
|
16581
|
+
const blocks = [];
|
|
16582
|
+
let current = { headerRaw: null, name: null, isArrayTable: false, body: [] };
|
|
16583
|
+
let multi = null;
|
|
16584
|
+
const tableRe = /^\s*\[\s*([^[\]]+?)\s*\]\s*$/;
|
|
16585
|
+
const arrayTableRe = /^\s*\[\[\s*([^[\]]+?)\s*\]\]\s*$/;
|
|
16586
|
+
for (const line of lines) {
|
|
16587
|
+
if (multi == null) {
|
|
16588
|
+
const am = arrayTableRe.exec(line);
|
|
16589
|
+
const tm = am == null ? tableRe.exec(line) : null;
|
|
16590
|
+
if (am != null || tm != null) {
|
|
16591
|
+
blocks.push(current);
|
|
16592
|
+
current = { headerRaw: line, name: (am ?? tm)[1].trim(), isArrayTable: am != null, body: [] };
|
|
16593
|
+
continue;
|
|
16594
|
+
}
|
|
16595
|
+
}
|
|
16596
|
+
multi = advanceTomlStringState(line, multi);
|
|
16597
|
+
current.body.push(line);
|
|
16598
|
+
}
|
|
16599
|
+
blocks.push(current);
|
|
16600
|
+
return blocks;
|
|
16601
|
+
}
|
|
16602
|
+
function mergeCanonicalTomlBody(body, commandLine, argsLine) {
|
|
16603
|
+
const preserved = [];
|
|
16604
|
+
for (let i = 0; i < body.length; i++) {
|
|
16605
|
+
const line = body[i];
|
|
16606
|
+
const trimmed = line.trim();
|
|
16607
|
+
if (/^command\s*=/.test(trimmed)) continue;
|
|
16608
|
+
if (/^args\s*=/.test(trimmed)) {
|
|
16609
|
+
let open = (line.match(/\[/g) ?? []).length;
|
|
16610
|
+
let close = (line.match(/\]/g) ?? []).length;
|
|
16611
|
+
while (open > close && i + 1 < body.length) {
|
|
16612
|
+
i += 1;
|
|
16613
|
+
open += (body[i].match(/\[/g) ?? []).length;
|
|
16614
|
+
close += (body[i].match(/\]/g) ?? []).length;
|
|
16615
|
+
}
|
|
16616
|
+
continue;
|
|
16617
|
+
}
|
|
16618
|
+
preserved.push(line);
|
|
16619
|
+
}
|
|
16620
|
+
return [commandLine, argsLine, ...preserved];
|
|
16621
|
+
}
|
|
16622
|
+
function renderTomlMcpConfig(existing, spec) {
|
|
16623
|
+
const commandLine = `command = ${JSON.stringify(spec.command)}`;
|
|
16624
|
+
const argsLine = `args = [${spec.args.map((a) => JSON.stringify(a)).join(", ")}]`;
|
|
16625
|
+
const canonicalHeader = `[mcp_servers.${spec.name}]`;
|
|
16626
|
+
const canonicalName = `mcp_servers.${spec.name}`;
|
|
16627
|
+
if (existing == null || existing.trim() === "") {
|
|
16628
|
+
return `${canonicalHeader}
|
|
16629
|
+
${commandLine}
|
|
16630
|
+
${argsLine}
|
|
16631
|
+
`;
|
|
16632
|
+
}
|
|
16633
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
16634
|
+
const lines = existing.replace(/\r\n/g, "\n").split("\n");
|
|
16635
|
+
const legacyFull = new Set(spec.legacyNames.map((n) => `mcp_servers.${n}`));
|
|
16636
|
+
const all = parseTomlBlocks(lines);
|
|
16637
|
+
if (all.some((b) => b.isArrayTable && b.name != null && (b.name === canonicalName || legacyFull.has(b.name)))) return null;
|
|
16638
|
+
const targetBlock = all.find((b) => !b.isArrayTable && b.name === canonicalName) ?? all.find((b) => !b.isArrayTable && b.name != null && legacyFull.has(b.name));
|
|
16639
|
+
const out = [];
|
|
16640
|
+
for (const b of all) {
|
|
16641
|
+
if (b === targetBlock) {
|
|
16642
|
+
out.push({ headerRaw: canonicalHeader, name: canonicalName, isArrayTable: false, body: mergeCanonicalTomlBody(b.body, commandLine, argsLine) });
|
|
16643
|
+
continue;
|
|
16644
|
+
}
|
|
16645
|
+
if (targetBlock != null && !b.isArrayTable && b.name != null && legacyFull.has(b.name)) continue;
|
|
16646
|
+
out.push(b);
|
|
16647
|
+
}
|
|
16648
|
+
if (targetBlock != null) {
|
|
16649
|
+
const joined = out.flatMap((b) => b.headerRaw != null ? [b.headerRaw, ...b.body] : b.body).join("\n");
|
|
16650
|
+
return eol === "\r\n" ? joined.replace(/\n/g, "\r\n") : joined;
|
|
16651
|
+
}
|
|
16652
|
+
const base = out.flatMap((b) => b.headerRaw != null ? [b.headerRaw, ...b.body] : b.body).join("\n").replace(/\n+$/, "");
|
|
16653
|
+
const block = `${canonicalHeader}
|
|
16654
|
+
${commandLine}
|
|
16655
|
+
${argsLine}`;
|
|
16656
|
+
const combined = base === "" ? `${block}
|
|
16657
|
+
` : `${base}
|
|
16658
|
+
|
|
16659
|
+
${block}
|
|
16660
|
+
`;
|
|
16661
|
+
return eol === "\r\n" ? combined.replace(/\n/g, "\r\n") : combined;
|
|
16662
|
+
}
|
|
16663
|
+
|
|
16105
16664
|
// src/cli-doctor-shared.ts
|
|
16106
16665
|
var import_node_fs15 = require("node:fs");
|
|
16107
16666
|
var import_node_path16 = require("node:path");
|
|
@@ -16314,20 +16873,22 @@ function readRepoVersion() {
|
|
|
16314
16873
|
}
|
|
16315
16874
|
}
|
|
16316
16875
|
async function fetchReleasedVersion() {
|
|
16317
|
-
|
|
16318
|
-
|
|
16319
|
-
|
|
16320
|
-
}
|
|
16321
|
-
return void 0;
|
|
16322
|
-
}
|
|
16876
|
+
return resolveReleasedVersion({
|
|
16877
|
+
npm: async () => (await runHostBin("npm", npmReleasedVersionArgs(), { timeout: NPM_VIEW_TIMEOUT_MS })).stdout,
|
|
16878
|
+
gh: async () => (await execFileP2("gh", pluginManifestVersionArgs(), { timeout: 5e3 })).stdout
|
|
16879
|
+
});
|
|
16323
16880
|
}
|
|
16324
16881
|
var NPM_UPDATE_TIMEOUT_MS = 12e4;
|
|
16325
16882
|
var NPM_VIEW_TIMEOUT_MS = 15e3;
|
|
16326
16883
|
var PLUGIN_PULL_TIMEOUT_MS = 3e4;
|
|
16327
|
-
async function applyVersionAutoUpdate(report, log) {
|
|
16328
|
-
const action = versionAutoUpdateAction(report, Boolean(process.env.CLAUDE_PLUGIN_ROOT));
|
|
16884
|
+
async function applyVersionAutoUpdate(report, log, releasedSource) {
|
|
16885
|
+
const action = versionAutoUpdateAction(report, Boolean(process.env.CLAUDE_PLUGIN_ROOT), releasedSource);
|
|
16329
16886
|
if (action === "none") return { report, applied: "none" };
|
|
16330
16887
|
const target = report.releasedVersion ?? "latest";
|
|
16888
|
+
if (action === "npm-unreachable") {
|
|
16889
|
+
log(` \u2717 mmi-cli ${report.currentVersion} is stale (released ${target}, resolved via gh) \u2014 npm registry unreachable this pass; when npm is back: npm install -g @mutmutco/cli@latest`);
|
|
16890
|
+
return { report, applied: "none" };
|
|
16891
|
+
}
|
|
16331
16892
|
if (action === "plugin-pull") {
|
|
16332
16893
|
try {
|
|
16333
16894
|
const root = (await execFileP2("git", ["-C", process.env.CLAUDE_PLUGIN_ROOT, "rev-parse", "--show-toplevel"], { timeout: PLUGIN_PULL_TIMEOUT_MS })).stdout.trim();
|
|
@@ -16358,7 +16919,20 @@ var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
|
|
|
16358
16919
|
function runHostBin(bin, args, opts) {
|
|
16359
16920
|
return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
|
|
16360
16921
|
}
|
|
16361
|
-
function
|
|
16922
|
+
function reexecStrategies(input) {
|
|
16923
|
+
const strategies = [
|
|
16924
|
+
input.platform === "win32" ? { command: "cmd.exe", args: ["/c", "mmi-cli"], kind: "cmd-shim" } : { command: "mmi-cli", args: [], kind: "direct" }
|
|
16925
|
+
];
|
|
16926
|
+
if (input.argv1) strategies.push({ command: input.execPath, args: [input.argv1], kind: "direct" });
|
|
16927
|
+
return strategies;
|
|
16928
|
+
}
|
|
16929
|
+
var CMD_NOT_RECOGNIZED_EXIT_CODE = 9009;
|
|
16930
|
+
function reexecAttemptOutcome(strategy, code) {
|
|
16931
|
+
if (code < 0) return "strategy-failed";
|
|
16932
|
+
if (strategy.kind === "cmd-shim" && code === CMD_NOT_RECOGNIZED_EXIT_CODE) return "strategy-failed";
|
|
16933
|
+
return "result";
|
|
16934
|
+
}
|
|
16935
|
+
function spawnReexecAttempt(command, args, env) {
|
|
16362
16936
|
return new Promise((resolve6) => {
|
|
16363
16937
|
let settled = false;
|
|
16364
16938
|
const done = (code) => {
|
|
@@ -16367,12 +16941,20 @@ function reexecMmiCli(args) {
|
|
|
16367
16941
|
resolve6(code);
|
|
16368
16942
|
}
|
|
16369
16943
|
};
|
|
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 });
|
|
16944
|
+
const child = (0, import_node_child_process10.spawn)(command, args, { stdio: "inherit", env });
|
|
16372
16945
|
child.on("error", () => done(-1));
|
|
16373
16946
|
child.on("exit", (code) => done(code ?? 0));
|
|
16374
16947
|
});
|
|
16375
16948
|
}
|
|
16949
|
+
async function reexecMmiCli(args) {
|
|
16950
|
+
const strategies = reexecStrategies({ platform: process.platform, argv1: process.argv[1], execPath: process.execPath });
|
|
16951
|
+
const env = { ...process.env, [DOCTOR_POST_SELF_UPDATE_ENV]: "1" };
|
|
16952
|
+
for (const strategy of strategies) {
|
|
16953
|
+
const code = await spawnReexecAttempt(strategy.command, [...strategy.args, ...args], env);
|
|
16954
|
+
if (reexecAttemptOutcome(strategy, code) === "result") return code;
|
|
16955
|
+
}
|
|
16956
|
+
return -1;
|
|
16957
|
+
}
|
|
16376
16958
|
function hostBinAvailable(bin) {
|
|
16377
16959
|
return execFileP2(isWin ? "where" : "which", [bin]).then(() => true).catch(() => false);
|
|
16378
16960
|
}
|
|
@@ -16392,6 +16974,15 @@ async function runCodexPlugin(args) {
|
|
|
16392
16974
|
return false;
|
|
16393
16975
|
}
|
|
16394
16976
|
}
|
|
16977
|
+
async function codexEnabledMmiVersion() {
|
|
16978
|
+
if (!await hostBinAvailable("codex")) return void 0;
|
|
16979
|
+
try {
|
|
16980
|
+
const { stdout } = await runHostBin("codex", ["plugin", "list", "--json"], { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
16981
|
+
return codexEnabledPluginVersionFromList(stdout, MMI_PLUGIN_ID);
|
|
16982
|
+
} catch {
|
|
16983
|
+
return void 0;
|
|
16984
|
+
}
|
|
16985
|
+
}
|
|
16395
16986
|
async function marketplaceAddRefSupported(bin) {
|
|
16396
16987
|
try {
|
|
16397
16988
|
const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
|
|
@@ -16739,6 +17330,12 @@ function opencodeInstalledVersionForDoctor() {
|
|
|
16739
17330
|
diskVersion: readOpencodeAdapterDiskVersion()
|
|
16740
17331
|
});
|
|
16741
17332
|
}
|
|
17333
|
+
function opencodePersistedVersionForDoctor() {
|
|
17334
|
+
return pickOpencodeActiveVersion({
|
|
17335
|
+
cacheVersion: readOpencodeLoadedCacheVersion(),
|
|
17336
|
+
diskVersion: readOpencodeAdapterDiskVersion()
|
|
17337
|
+
});
|
|
17338
|
+
}
|
|
16742
17339
|
function opencodePluginVersionsForReport() {
|
|
16743
17340
|
return [process.env.MMI_OPENCODE_PLUGIN_VERSION, readOpencodeAdapterDiskVersion()].filter((v) => Boolean(v));
|
|
16744
17341
|
}
|
|
@@ -16856,8 +17453,8 @@ function hasNestedMmiChild(versionDir) {
|
|
|
16856
17453
|
}
|
|
16857
17454
|
}
|
|
16858
17455
|
function nestedPluginTreeSnapshot() {
|
|
16859
|
-
return mmiPluginCacheRootSnapshots().
|
|
16860
|
-
(root) => root.entries.filter((e) => e.isDirectory && /^v?\d+\.\d+\.\d+/.test(e.name)).map((e) => ({ surface:
|
|
17456
|
+
return mmiPluginCacheRootSnapshots().flatMap(
|
|
17457
|
+
(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
17458
|
);
|
|
16862
17459
|
}
|
|
16863
17460
|
function uniqueQuarantineTarget(path2) {
|
|
@@ -16933,21 +17530,144 @@ function readTextFile(path2) {
|
|
|
16933
17530
|
return null;
|
|
16934
17531
|
}
|
|
16935
17532
|
}
|
|
16936
|
-
function
|
|
17533
|
+
function mcpDirExists(path2) {
|
|
17534
|
+
try {
|
|
17535
|
+
return (0, import_node_fs17.existsSync)(path2);
|
|
17536
|
+
} catch {
|
|
17537
|
+
return false;
|
|
17538
|
+
}
|
|
17539
|
+
}
|
|
17540
|
+
function mcpConfigTargets() {
|
|
16937
17541
|
const cwd = process.cwd();
|
|
16938
17542
|
const home = (0, import_node_os6.homedir)();
|
|
16939
|
-
const
|
|
16940
|
-
|
|
16941
|
-
|
|
16942
|
-
|
|
16943
|
-
|
|
17543
|
+
const cursorProjectDir = (0, import_node_path17.join)(cwd, ".cursor");
|
|
17544
|
+
const cursorUserDir = (0, import_node_path17.join)(home, ".cursor");
|
|
17545
|
+
const codexDir = (0, import_node_path17.join)(home, ".codex");
|
|
17546
|
+
return [
|
|
17547
|
+
// Claude Code project MCP — reconciled if present, never conjured (org seeds .cursor/mcp.json, not this).
|
|
17548
|
+
{
|
|
17549
|
+
host: "claude-code",
|
|
17550
|
+
label: "Claude Code project MCP",
|
|
17551
|
+
path: (0, import_node_path17.join)(cwd, ".mcp.json"),
|
|
17552
|
+
format: "json",
|
|
17553
|
+
present: true,
|
|
17554
|
+
// parent is the repo root (cwd); the caller gates the whole reconcile on isOrgRepo
|
|
17555
|
+
isRepoFile: true,
|
|
17556
|
+
createWhenFileAbsent: false,
|
|
17557
|
+
content: readTextFile((0, import_node_path17.join)(cwd, ".mcp.json"))
|
|
17558
|
+
},
|
|
17559
|
+
// Cursor project MCP — the bootstrap-seeded surface; restored if its .cursor dir exists but the file is gone.
|
|
17560
|
+
{
|
|
17561
|
+
host: "cursor-project",
|
|
17562
|
+
label: "Cursor project MCP",
|
|
17563
|
+
path: (0, import_node_path17.join)(cursorProjectDir, "mcp.json"),
|
|
17564
|
+
format: "json",
|
|
17565
|
+
present: mcpDirExists(cursorProjectDir),
|
|
17566
|
+
isRepoFile: true,
|
|
17567
|
+
createWhenFileAbsent: true,
|
|
17568
|
+
content: readTextFile((0, import_node_path17.join)(cursorProjectDir, "mcp.json"))
|
|
17569
|
+
},
|
|
17570
|
+
// Cursor user MCP — global; written only when Cursor is installed (~/.cursor exists).
|
|
17571
|
+
{
|
|
17572
|
+
host: "cursor-user",
|
|
17573
|
+
label: "Cursor user MCP",
|
|
17574
|
+
path: (0, import_node_path17.join)(cursorUserDir, "mcp.json"),
|
|
17575
|
+
format: "json",
|
|
17576
|
+
present: mcpDirExists(cursorUserDir),
|
|
17577
|
+
isRepoFile: false,
|
|
17578
|
+
createWhenFileAbsent: true,
|
|
17579
|
+
content: readTextFile((0, import_node_path17.join)(cursorUserDir, "mcp.json"))
|
|
17580
|
+
},
|
|
17581
|
+
// Codex user config (TOML) — global; written only when Codex is installed (~/.codex exists).
|
|
17582
|
+
{
|
|
17583
|
+
host: "codex",
|
|
17584
|
+
label: "Codex user config",
|
|
17585
|
+
path: (0, import_node_path17.join)(codexDir, "config.toml"),
|
|
17586
|
+
format: "toml",
|
|
17587
|
+
present: mcpDirExists(codexDir),
|
|
17588
|
+
isRepoFile: false,
|
|
17589
|
+
createWhenFileAbsent: true,
|
|
17590
|
+
content: readTextFile((0, import_node_path17.join)(codexDir, "config.toml"))
|
|
17591
|
+
}
|
|
16944
17592
|
];
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
|
|
17593
|
+
}
|
|
17594
|
+
function writeMcpConfigFile(path2, content) {
|
|
17595
|
+
try {
|
|
17596
|
+
(0, import_node_fs17.writeFileSync)(path2, content, "utf8");
|
|
17597
|
+
return true;
|
|
17598
|
+
} catch {
|
|
17599
|
+
return false;
|
|
16949
17600
|
}
|
|
16950
|
-
|
|
17601
|
+
}
|
|
17602
|
+
var CLAUDE_CLI_MCP_LABEL = "Claude Code MCP store (user scope)";
|
|
17603
|
+
var CLAUDE_CLI_MCP_PATH = "claude mcp (user scope)";
|
|
17604
|
+
async function claudeCliMcpTarget(spec) {
|
|
17605
|
+
const base = {
|
|
17606
|
+
host: "claude-cli",
|
|
17607
|
+
label: CLAUDE_CLI_MCP_LABEL,
|
|
17608
|
+
path: CLAUDE_CLI_MCP_PATH,
|
|
17609
|
+
format: "claude-cli",
|
|
17610
|
+
isRepoFile: false,
|
|
17611
|
+
createWhenFileAbsent: true
|
|
17612
|
+
};
|
|
17613
|
+
if (!await hostBinAvailable("claude")) return { ...base, present: false, content: null };
|
|
17614
|
+
for (const name of [spec.name, ...spec.legacyNames]) {
|
|
17615
|
+
try {
|
|
17616
|
+
const { stdout } = await runHostBin("claude", ["mcp", "get", name], { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
17617
|
+
return { ...base, present: true, content: stdout, claudeCliMatchedName: name };
|
|
17618
|
+
} catch {
|
|
17619
|
+
}
|
|
17620
|
+
}
|
|
17621
|
+
return { ...base, present: true, content: null };
|
|
17622
|
+
}
|
|
17623
|
+
async function runClaudeCliMcpCommands(commands) {
|
|
17624
|
+
for (const cmd of commands) {
|
|
17625
|
+
try {
|
|
17626
|
+
await runHostBin("claude", ["mcp", ...cmd.args], { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
17627
|
+
} catch {
|
|
17628
|
+
return false;
|
|
17629
|
+
}
|
|
17630
|
+
}
|
|
17631
|
+
return true;
|
|
17632
|
+
}
|
|
17633
|
+
async function reconcileMcpRegistrations(input) {
|
|
17634
|
+
if (!input.isOrgRepo) return buildMcpReconcileCheck([]);
|
|
17635
|
+
const snapshot = async () => [...mcpConfigTargets(), await claudeCliMcpTarget(PLAYWRIGHT_MCP_SPEC)];
|
|
17636
|
+
const targets = await snapshot();
|
|
17637
|
+
if (!input.apply) return buildMcpReconcileCheck(planMcpReconcile(targets, PLAYWRIGHT_MCP_SPEC));
|
|
17638
|
+
const fileTargets = targets.filter((t) => t.format !== "claude-cli");
|
|
17639
|
+
const results = applyMcpReconcile(fileTargets, PLAYWRIGHT_MCP_SPEC, {
|
|
17640
|
+
repoWritesAllowed: input.repoWritesAllowed,
|
|
17641
|
+
write: writeMcpConfigFile
|
|
17642
|
+
});
|
|
17643
|
+
for (const r of results) {
|
|
17644
|
+
if (r.outcome === "wrote") {
|
|
17645
|
+
input.onHealed();
|
|
17646
|
+
input.onReloadRequired();
|
|
17647
|
+
const verb = r.action === "install" ? "registered" : "reconciled";
|
|
17648
|
+
input.log(` \u21BB ${verb} Playwright MCP in ${r.target.path} \u2014 restart the host to load it`);
|
|
17649
|
+
} else if (r.outcome === "unparseable") {
|
|
17650
|
+
input.log(` \u26A0 ${r.target.label}: could not safely patch ${r.target.path} (unparseable) \u2014 fix it by hand`);
|
|
17651
|
+
} else if (r.outcome === "write-failed") {
|
|
17652
|
+
input.log(` \u26A0 ${r.target.label}: failed to write ${r.target.path}`);
|
|
17653
|
+
}
|
|
17654
|
+
}
|
|
17655
|
+
const claudeTarget = targets.find((t) => t.format === "claude-cli");
|
|
17656
|
+
if (claudeTarget != null) {
|
|
17657
|
+
const item = detectMcpState(claudeTarget, PLAYWRIGHT_MCP_SPEC);
|
|
17658
|
+
if (item.action === "install" || item.action === "reconcile") {
|
|
17659
|
+
const commands = planClaudeCliMcpHeal(item, PLAYWRIGHT_MCP_SPEC);
|
|
17660
|
+
if (await runClaudeCliMcpCommands(commands)) {
|
|
17661
|
+
input.onHealed();
|
|
17662
|
+
input.onReloadRequired();
|
|
17663
|
+
const verb = item.action === "install" ? "registered" : "reconciled";
|
|
17664
|
+
input.log(` \u21BB ${verb} Playwright MCP via \`claude mcp add\` (${CLAUDE_CLI_MCP_PATH}) \u2014 restart Claude Code to load it`);
|
|
17665
|
+
} else {
|
|
17666
|
+
input.log(` \u26A0 ${CLAUDE_CLI_MCP_LABEL}: \`claude mcp add\` failed for ${PLAYWRIGHT_MCP_SPEC.name} \u2014 run it by hand`);
|
|
17667
|
+
}
|
|
17668
|
+
}
|
|
17669
|
+
}
|
|
17670
|
+
return buildMcpReconcileCheck(planMcpReconcile(await snapshot(), PLAYWRIGHT_MCP_SPEC));
|
|
16951
17671
|
}
|
|
16952
17672
|
function strayBrowserArtifactPaths() {
|
|
16953
17673
|
const cwd = process.cwd();
|
|
@@ -16986,7 +17706,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16986
17706
|
const checks = [];
|
|
16987
17707
|
const REWRITE_KEY = "url.https://github.com/.insteadOf";
|
|
16988
17708
|
const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
|
|
16989
|
-
const [login, pathProbe,
|
|
17709
|
+
const [login, pathProbe, releasedVersionResolution, cfg, callerArn, cloneProbe, isOrgRepo] = await Promise.all([
|
|
16990
17710
|
githubLogin(),
|
|
16991
17711
|
execFileP2(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
|
|
16992
17712
|
fetchReleasedVersion(),
|
|
@@ -16998,6 +17718,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16998
17718
|
// not the now-always-defaulted `cfg.sagaApiUrl`. Independent git read — resolved concurrently here.
|
|
16999
17719
|
readOrigin ? isOrgRepoRoot(readOrigin) : isOrgRepoRoot()
|
|
17000
17720
|
]);
|
|
17721
|
+
const releasedVersion = releasedVersionResolution.version;
|
|
17001
17722
|
const surface = detectSurface(process.env);
|
|
17002
17723
|
const versionReportProbe = buildVersionLagReport({
|
|
17003
17724
|
currentVersion: resolveClientVersion(),
|
|
@@ -17063,7 +17784,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17063
17784
|
});
|
|
17064
17785
|
let selfUpdatedCli = false;
|
|
17065
17786
|
if (repairFull && !process.env[DOCTOR_POST_SELF_UPDATE_ENV]) {
|
|
17066
|
-
const updated = await applyVersionAutoUpdate(versionReport, (m) => io.err(m));
|
|
17787
|
+
const updated = await applyVersionAutoUpdate(versionReport, (m) => io.err(m), releasedVersionResolution.source);
|
|
17067
17788
|
versionReport = updated.report;
|
|
17068
17789
|
selfUpdatedCli = updated.applied === "npm";
|
|
17069
17790
|
}
|
|
@@ -17071,13 +17792,19 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17071
17792
|
checks.push(versionReport);
|
|
17072
17793
|
if (selfUpdatedCli) {
|
|
17073
17794
|
if (!opts.json) io.err(selfUpdateReexecLine(versionReport));
|
|
17074
|
-
const
|
|
17795
|
+
const reexecArgs = buildSelfUpdateReexecArgs(opts);
|
|
17796
|
+
const code = await reexecMmiCli(reexecArgs);
|
|
17075
17797
|
if (code >= 0) {
|
|
17076
17798
|
if (code > 0) process.exitCode = code;
|
|
17077
17799
|
return;
|
|
17078
17800
|
}
|
|
17079
|
-
|
|
17080
|
-
|
|
17801
|
+
process.exitCode = DOCTOR_SELF_UPDATE_HALT_EXIT_CODE;
|
|
17802
|
+
if (opts.json) {
|
|
17803
|
+
io.log(JSON.stringify(buildSelfUpdateHaltPayload({ checks, updatedTo: versionReport.releasedVersion }), null, 2));
|
|
17804
|
+
} else {
|
|
17805
|
+
io.err(selfUpdateHaltLine(versionReport));
|
|
17806
|
+
io.err(selfUpdateHaltSentinelLine({ report: versionReport, rerunArgs: reexecArgs }));
|
|
17807
|
+
}
|
|
17081
17808
|
return;
|
|
17082
17809
|
}
|
|
17083
17810
|
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 +18005,20 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17278
18005
|
releasedVersion
|
|
17279
18006
|
});
|
|
17280
18007
|
if (!opencodeVersionCheck.ok && repairFull) {
|
|
17281
|
-
const
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
18008
|
+
const persistedCurrent = () => buildOpencodeVersionCheck({
|
|
18009
|
+
isOrgRepo,
|
|
18010
|
+
installedVersion: opencodePersistedVersionForDoctor(),
|
|
18011
|
+
releasedVersion
|
|
18012
|
+
}).ok;
|
|
18013
|
+
let refreshed = false;
|
|
18014
|
+
let quarantined = false;
|
|
18015
|
+
if (!persistedCurrent()) {
|
|
18016
|
+
refreshed = await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m));
|
|
18017
|
+
quarantined = quarantineStaleOpencodePluginCaches(releasedVersion, (m) => io.err(m));
|
|
18018
|
+
}
|
|
18019
|
+
if (persistedCurrent()) {
|
|
18020
|
+
const wasReinstalled = refreshed || quarantined;
|
|
18021
|
+
opencodeInstalledVersion = opencodePersistedVersionForDoctor() ?? opencodeInstalledVersion;
|
|
17285
18022
|
opencodeVersionCheck = buildOpencodeVersionCheck({
|
|
17286
18023
|
isOrgRepo,
|
|
17287
18024
|
installedVersion: opencodeInstalledVersion,
|
|
@@ -17290,7 +18027,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17290
18027
|
if (opencodeVersionCheck.ok) {
|
|
17291
18028
|
markPluginReloadRequired();
|
|
17292
18029
|
markHealed();
|
|
17293
|
-
|
|
18030
|
+
const version = opencodeInstalledVersion ?? releasedVersion ?? "latest";
|
|
18031
|
+
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
18032
|
}
|
|
17295
18033
|
}
|
|
17296
18034
|
}
|
|
@@ -17361,6 +18099,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17361
18099
|
return versions.sort((a, b) => compareVersions(b, a))[0];
|
|
17362
18100
|
};
|
|
17363
18101
|
const codexCacheVersions = () => mmiPluginCacheRootSnapshots().filter((r) => r.surface === "codex").flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name));
|
|
18102
|
+
const codexActiveVersion = () => isOrgRepo && releasedVersion ? codexEnabledMmiVersion() : Promise.resolve(void 0);
|
|
17364
18103
|
let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
|
|
17365
18104
|
isOrgRepo,
|
|
17366
18105
|
roots: mmiPluginCacheRootSnapshots(),
|
|
@@ -17393,14 +18132,16 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17393
18132
|
};
|
|
17394
18133
|
}
|
|
17395
18134
|
checks.push(cacheCleanupCheck);
|
|
18135
|
+
const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
|
|
17396
18136
|
let codexActiveCacheCheck = buildCodexActiveCacheCheck({
|
|
17397
18137
|
isOrgRepo,
|
|
17398
18138
|
releasedVersion,
|
|
17399
18139
|
codexCacheVersions: codexCacheVersions(),
|
|
17400
|
-
codexRecordVersion: codexRecordVersion()
|
|
18140
|
+
codexRecordVersion: codexRecordVersion(),
|
|
18141
|
+
codexActiveVersion: await codexActiveVersion(),
|
|
18142
|
+
codexPresent: canDriveCodex
|
|
17401
18143
|
});
|
|
17402
18144
|
if (!codexActiveCacheCheck.ok && repairFull) {
|
|
17403
|
-
const canDriveCodex = surfaceToken(surface) === "codex" || await hostBinAvailable("codex");
|
|
17404
18145
|
if (canDriveCodex && await applyPluginHeal("codex", surface, (m) => io.err(m), { force: true })) {
|
|
17405
18146
|
markPluginReloadRequired();
|
|
17406
18147
|
markHealed();
|
|
@@ -17409,7 +18150,9 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17409
18150
|
isOrgRepo,
|
|
17410
18151
|
releasedVersion,
|
|
17411
18152
|
codexCacheVersions: codexCacheVersions(),
|
|
17412
|
-
codexRecordVersion: codexRecordVersion()
|
|
18153
|
+
codexRecordVersion: codexRecordVersion(),
|
|
18154
|
+
codexActiveVersion: await codexActiveVersion(),
|
|
18155
|
+
codexPresent: canDriveCodex
|
|
17413
18156
|
});
|
|
17414
18157
|
}
|
|
17415
18158
|
}
|
|
@@ -17420,7 +18163,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17420
18163
|
entries: nestedPluginTreeSnapshot()
|
|
17421
18164
|
});
|
|
17422
18165
|
if (!nestedPluginTreeCheck.ok && nestedPluginTreeCheck.nested?.length && repairLocal) {
|
|
17423
|
-
const
|
|
18166
|
+
const nestedEntries = nestedPluginTreeCheck.nested;
|
|
18167
|
+
const nestedPaths = nestedEntries.map((n) => n.path);
|
|
17424
18168
|
if (await applyNestedPluginTreeCleanup(nestedPaths, (m) => io.err(m))) {
|
|
17425
18169
|
nestedPluginTreeCheck = buildNestedPluginTreeCheck({
|
|
17426
18170
|
isOrgRepo,
|
|
@@ -17428,36 +18172,62 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17428
18172
|
entries: nestedPluginTreeSnapshot()
|
|
17429
18173
|
});
|
|
17430
18174
|
if (nestedPluginTreeCheck.ok) {
|
|
17431
|
-
io.err(` \u21BB cleared self-nested MMI plugin cache tree(s)
|
|
18175
|
+
io.err(` \u21BB cleared self-nested MMI plugin cache tree(s)`);
|
|
17432
18176
|
}
|
|
17433
|
-
|
|
18177
|
+
let reinstalledAny = false;
|
|
18178
|
+
const unreinstalled = [];
|
|
18179
|
+
for (const token of [...new Set(nestedEntries.map((n) => n.surface))]) {
|
|
18180
|
+
const bin = token === "codex" ? "codex" : "claude";
|
|
18181
|
+
const canDrive = surfaceToken(surface) === token || await hostBinAvailable(bin);
|
|
18182
|
+
if (canDrive && await applyPluginHeal(token, surface, (m) => io.err(m), { force: true })) {
|
|
18183
|
+
reinstalledAny = true;
|
|
18184
|
+
io.err(` \u21BB reinstalled MMI plugin (${token}) after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
|
|
18185
|
+
} else {
|
|
18186
|
+
unreinstalled.push(token);
|
|
18187
|
+
}
|
|
18188
|
+
}
|
|
18189
|
+
if (reinstalledAny) {
|
|
17434
18190
|
markPluginReloadRequired();
|
|
17435
18191
|
markHealed();
|
|
17436
|
-
io.err(` \u21BB reinstalled MMI plugin after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
|
|
17437
18192
|
nestedPluginTreeCheck = buildNestedPluginTreeCheck({
|
|
17438
18193
|
isOrgRepo,
|
|
17439
18194
|
isWindows: isWin,
|
|
17440
18195
|
entries: nestedPluginTreeSnapshot()
|
|
17441
18196
|
});
|
|
17442
18197
|
}
|
|
18198
|
+
if (unreinstalled.length > 0 && nestedPluginTreeCheck.ok) {
|
|
18199
|
+
nestedPluginTreeCheck = { ...nestedPluginTreeCheck, ok: false, fix: nestedTreeReinstallGapFix(unreinstalled) };
|
|
18200
|
+
}
|
|
17443
18201
|
}
|
|
17444
18202
|
}
|
|
17445
18203
|
checks.push(nestedPluginTreeCheck);
|
|
17446
18204
|
const cursorCacheRoot = cursorPluginCacheRoot();
|
|
18205
|
+
const cursorCacheRootExists = (0, import_node_fs17.existsSync)(cursorCacheRoot);
|
|
17447
18206
|
let cursorPins = cursorPluginCachePinSnapshots() ?? [];
|
|
18207
|
+
checks.push(
|
|
18208
|
+
buildCursorPluginCacheCleanupCheck({
|
|
18209
|
+
isOrgRepo,
|
|
18210
|
+
cacheRoot: cursorCacheRoot,
|
|
18211
|
+
pins: cursorPins,
|
|
18212
|
+
releasedVersion
|
|
18213
|
+
})
|
|
18214
|
+
);
|
|
17448
18215
|
let cursorPluginCheck = buildCursorPluginInstallCheck({
|
|
17449
18216
|
isOrgRepo,
|
|
17450
18217
|
surface,
|
|
17451
18218
|
cacheRoot: cursorCacheRoot,
|
|
17452
|
-
cacheRootExists:
|
|
18219
|
+
cacheRootExists: cursorCacheRootExists,
|
|
17453
18220
|
pins: cursorPins,
|
|
17454
18221
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17455
18222
|
releasedVersion
|
|
17456
18223
|
});
|
|
17457
18224
|
if (!cursorPluginCheck.ok && repairLocal) {
|
|
18225
|
+
const seedingFromNothing = cursorPins.length === 0;
|
|
17458
18226
|
const seeded = await applyCursorPluginCacheSeed({
|
|
17459
18227
|
pins: cursorPins,
|
|
17460
18228
|
releasedVersion,
|
|
18229
|
+
cacheRoot: cursorCacheRoot,
|
|
18230
|
+
cacheRootExists: cursorCacheRootExists,
|
|
17461
18231
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17462
18232
|
execFileP: execFileP2,
|
|
17463
18233
|
mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
|
|
@@ -17469,14 +18239,26 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17469
18239
|
isOrgRepo,
|
|
17470
18240
|
surface,
|
|
17471
18241
|
cacheRoot: cursorCacheRoot,
|
|
17472
|
-
cacheRootExists:
|
|
18242
|
+
cacheRootExists: cursorCacheRootExists,
|
|
17473
18243
|
pins: cursorPins,
|
|
17474
18244
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17475
18245
|
releasedVersion
|
|
17476
18246
|
});
|
|
17477
18247
|
if (cursorPluginCheck.ok) {
|
|
17478
18248
|
markHealed();
|
|
17479
|
-
|
|
18249
|
+
if (seedingFromNothing) {
|
|
18250
|
+
io.err(
|
|
18251
|
+
` \u21BB seeded Cursor MMI plugin cache from nothing \u2192 ${releasedVersion ?? "latest"} (cache only \u2014 Team Marketplace registration not verified, #2409) \u2014 ${reloadAction(surface)}`
|
|
18252
|
+
);
|
|
18253
|
+
cursorPluginCheck = {
|
|
18254
|
+
...cursorPluginCheck,
|
|
18255
|
+
ok: false,
|
|
18256
|
+
severityOverride: "advisory",
|
|
18257
|
+
fix: cursorSeededFromNothingAdvisoryFix(releasedVersion)
|
|
18258
|
+
};
|
|
18259
|
+
} else {
|
|
18260
|
+
io.err(` \u21BB seeded Cursor MMI plugin cache \u2192 ${releasedVersion ?? "latest"} \u2014 ${reloadAction(surface)}`);
|
|
18261
|
+
}
|
|
17480
18262
|
}
|
|
17481
18263
|
}
|
|
17482
18264
|
}
|
|
@@ -17497,20 +18279,30 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17497
18279
|
mmiCliOnPath: onPath
|
|
17498
18280
|
})
|
|
17499
18281
|
);
|
|
17500
|
-
if (
|
|
17501
|
-
const playwrightMcpConfigs = playwrightMcpConfigSnapshots();
|
|
17502
|
-
checks.push(
|
|
17503
|
-
buildPlaywrightMcpVisionCapCheck({
|
|
17504
|
-
isOrgRepo,
|
|
17505
|
-
configs: playwrightMcpConfigs
|
|
17506
|
-
})
|
|
17507
|
-
);
|
|
18282
|
+
if (!opts.banner) {
|
|
17508
18283
|
checks.push(
|
|
17509
|
-
|
|
18284
|
+
await reconcileMcpRegistrations({
|
|
17510
18285
|
isOrgRepo,
|
|
17511
|
-
|
|
18286
|
+
apply: Boolean(opts.apply),
|
|
18287
|
+
repoWritesAllowed,
|
|
18288
|
+
log: (m) => io.err(m),
|
|
18289
|
+
onHealed: markHealed,
|
|
18290
|
+
onReloadRequired: markPluginReloadRequired
|
|
17512
18291
|
})
|
|
17513
18292
|
);
|
|
18293
|
+
}
|
|
18294
|
+
const sessionPluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
|
18295
|
+
const sessionCacheSurface = sessionPluginRoot?.includes(".codex") ? "codex" : sessionPluginRoot?.includes(".claude") ? "claude" : void 0;
|
|
18296
|
+
const sessionCacheVersions = sessionCacheSurface ? mmiPluginCacheRootSnapshots().filter((r) => r.surface === sessionCacheSurface).flatMap((r) => r.entries.filter((e) => e.isDirectory).map((e) => e.name)) : [];
|
|
18297
|
+
checks.push(
|
|
18298
|
+
buildSessionSkillRootCheck({
|
|
18299
|
+
isOrgRepo,
|
|
18300
|
+
surface,
|
|
18301
|
+
pluginRoot: sessionPluginRoot,
|
|
18302
|
+
cacheVersions: sessionCacheVersions
|
|
18303
|
+
})
|
|
18304
|
+
);
|
|
18305
|
+
if (runExtended) {
|
|
17514
18306
|
checks.push(
|
|
17515
18307
|
buildBrowserArtifactsCheck({
|
|
17516
18308
|
isOrgRepo,
|
|
@@ -17569,7 +18361,8 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17569
18361
|
codexPluginVersions: sourceVersions("codex"),
|
|
17570
18362
|
codexCacheVersions: cacheVersionsFor("codex"),
|
|
17571
18363
|
opencodePluginVersions: opencodePluginVersionsForReport(),
|
|
17572
|
-
releasedVersion
|
|
18364
|
+
releasedVersion,
|
|
18365
|
+
releasedVersionSource: releasedVersionResolution.source
|
|
17573
18366
|
});
|
|
17574
18367
|
}
|
|
17575
18368
|
});
|
|
@@ -19606,7 +20399,7 @@ board.command("prune-priority-labels").description("remove retired priority:* la
|
|
|
19606
20399
|
});
|
|
19607
20400
|
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
20401
|
try {
|
|
19609
|
-
const result = await moveBoardItem({ config: await
|
|
20402
|
+
const result = await moveBoardItem({ config: await loadConfigForBoardSelector(issueRef, o.repo), selector: issueRef, status: "Done", repo: o.repo, allowPartial: o.allowPartial });
|
|
19610
20403
|
if (o.json) return console.log(JSON.stringify(result));
|
|
19611
20404
|
console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> Done`);
|
|
19612
20405
|
} catch (e) {
|
|
@@ -20106,6 +20899,29 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
|
|
|
20106
20899
|
}
|
|
20107
20900
|
if (!audit.ok) process.exitCode = 1;
|
|
20108
20901
|
});
|
|
20902
|
+
var wiki = program2.command("wiki").description("release wiki lane \u2014 publish generated pages via a short-lived, repo-scoped minted token");
|
|
20903
|
+
wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:write token (master-gated) and publish the generated .md pages to <repo>.wiki.git via scripts/wiki-publish.mjs; the token stays in memory (never printed/committed). Fails loud on the credential gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").action(async (pagesDir, o) => {
|
|
20904
|
+
const repo = await resolveRepo(o.repo);
|
|
20905
|
+
if (!repo) return fail("wiki publish: could not determine the target repo (pass --repo owner/name)");
|
|
20906
|
+
const cfg = await loadConfig();
|
|
20907
|
+
const ok = await wikiPublish(
|
|
20908
|
+
{
|
|
20909
|
+
mint: (r) => mintWikiToken(r, registryClientDeps(cfg)),
|
|
20910
|
+
// scripts/wiki-publish.mjs lives at the repo root; Lane B runs there. Resolve against cwd.
|
|
20911
|
+
scriptPath: () => (0, import_node_path18.resolve)(process.cwd(), "scripts", "wiki-publish.mjs"),
|
|
20912
|
+
scriptExists: (p) => (0, import_node_fs18.existsSync)(p),
|
|
20913
|
+
spawn: (command, args, env) => {
|
|
20914
|
+
const r = (0, import_node_child_process11.spawnSync)(command, args, { stdio: "inherit", env });
|
|
20915
|
+
if (r.error) throw r.error;
|
|
20916
|
+
return r.status ?? 1;
|
|
20917
|
+
},
|
|
20918
|
+
log: (msg) => console.log(msg),
|
|
20919
|
+
err: (msg) => console.error(msg)
|
|
20920
|
+
},
|
|
20921
|
+
{ repo, pagesDir }
|
|
20922
|
+
);
|
|
20923
|
+
if (!ok) process.exitCode = 1;
|
|
20924
|
+
});
|
|
20109
20925
|
var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
|
|
20110
20926
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
20111
20927
|
if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
|