@mutmutco/cli 3.17.0 → 3.18.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 +212 -83
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -12252,6 +12252,7 @@ async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predic
|
|
|
12252
12252
|
await deps.run("git", ["checkout", "main"]);
|
|
12253
12253
|
await ffOnlyPull(deps, "main");
|
|
12254
12254
|
preFold.mainSha = clean2(await deps.run("git", ["rev-parse", "main"]));
|
|
12255
|
+
preFold.isTrueMerge = !await deps.run("git", ["merge-base", "--is-ancestor", preFold.mainSha, sourceRef]).then(() => true).catch(() => false);
|
|
12255
12256
|
if (predicted.length === 0) {
|
|
12256
12257
|
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
12257
12258
|
} else {
|
|
@@ -12411,12 +12412,69 @@ async function verifyPublishDryRun(deps, ctx, meta, deployModel) {
|
|
|
12411
12412
|
);
|
|
12412
12413
|
}
|
|
12413
12414
|
}
|
|
12415
|
+
var TRUE_MERGE_GATE_BRANCH_PREFIX = "train/check/";
|
|
12416
|
+
function trueMergeGateBranchName(tag, sha) {
|
|
12417
|
+
return `${TRUE_MERGE_GATE_BRANCH_PREFIX}${tag.replace(/^v/, "")}-${sha.slice(0, 8)}`;
|
|
12418
|
+
}
|
|
12419
|
+
async function gateTrueMergeCommit(deps, ctx, tag, releaseSha, requiredChecks) {
|
|
12420
|
+
if (requiredChecks.length === 0) {
|
|
12421
|
+
return "true merge commit: target branch requires no status checks \u2014 throwaway-PR gate skipped";
|
|
12422
|
+
}
|
|
12423
|
+
const branch = trueMergeGateBranchName(tag, releaseSha);
|
|
12424
|
+
await deps.run("git", ["push", "origin", "--delete", branch]).catch(() => void 0);
|
|
12425
|
+
await deps.run("git", ["push", "origin", `${releaseSha}:refs/heads/${branch}`]);
|
|
12426
|
+
let prNumber;
|
|
12427
|
+
try {
|
|
12428
|
+
const url = clean2(await deps.run("gh", [
|
|
12429
|
+
"pr",
|
|
12430
|
+
"create",
|
|
12431
|
+
"--repo",
|
|
12432
|
+
ctx.repo,
|
|
12433
|
+
"--base",
|
|
12434
|
+
"main",
|
|
12435
|
+
"--head",
|
|
12436
|
+
branch,
|
|
12437
|
+
"--draft",
|
|
12438
|
+
"--title",
|
|
12439
|
+
`chore(train): gate ${tag} merge commit (auto, #2790)`,
|
|
12440
|
+
"--body",
|
|
12441
|
+
`Throwaway PR opened by the release train to earn required status checks (${requiredChecks.join(", ")}) on a true (non-fast-forward) merge commit before its tag is pushed (#2790). Auto-closed once the checks resolve \u2014 never merge this PR; the train pushes \`main\` directly once the gate is satisfied.`
|
|
12442
|
+
]));
|
|
12443
|
+
prNumber = parsePrNumber(url);
|
|
12444
|
+
const checksNote = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
|
|
12445
|
+
return `true merge commit: gated via throwaway PR ${url || "(url unavailable)"} \u2014 ${checksNote}`;
|
|
12446
|
+
} finally {
|
|
12447
|
+
if (prNumber !== void 0) {
|
|
12448
|
+
await deps.run("gh", ["pr", "close", String(prNumber), "--repo", ctx.repo, "--delete-branch"]).catch((e) => {
|
|
12449
|
+
deps.warn?.(`throwaway train-gate PR #${prNumber} close/branch-delete failed (${e instanceof Error ? e.message : String(e)}) \u2014 clean up '${branch}' manually`);
|
|
12450
|
+
});
|
|
12451
|
+
} else {
|
|
12452
|
+
await deps.run("git", ["push", "origin", "--delete", branch]).catch((e) => {
|
|
12453
|
+
deps.warn?.(`throwaway train-gate branch '${branch}' delete failed (${e instanceof Error ? e.message : String(e)}) \u2014 clean up manually`);
|
|
12454
|
+
});
|
|
12455
|
+
}
|
|
12456
|
+
}
|
|
12457
|
+
}
|
|
12414
12458
|
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe) {
|
|
12415
12459
|
try {
|
|
12416
12460
|
await verifyPublishDryRun(deps, ctx, meta, deployModel);
|
|
12417
12461
|
} catch (e) {
|
|
12418
12462
|
throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
|
|
12419
12463
|
}
|
|
12464
|
+
let requiredChecks;
|
|
12465
|
+
try {
|
|
12466
|
+
requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
12467
|
+
} catch (e) {
|
|
12468
|
+
throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
|
|
12469
|
+
}
|
|
12470
|
+
let trueMergeGateNote;
|
|
12471
|
+
if (preFold.isTrueMerge) {
|
|
12472
|
+
try {
|
|
12473
|
+
trueMergeGateNote = await gateTrueMergeCommit(deps, ctx, tag, releaseSha, requiredChecks);
|
|
12474
|
+
} catch (e) {
|
|
12475
|
+
throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
|
|
12476
|
+
}
|
|
12477
|
+
}
|
|
12420
12478
|
let tagPush;
|
|
12421
12479
|
try {
|
|
12422
12480
|
tagPush = await ensureTagPushed(deps, tag, releaseSha, tagProbe, ctx.repo);
|
|
@@ -12424,13 +12482,13 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
12424
12482
|
throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
|
|
12425
12483
|
}
|
|
12426
12484
|
const tagPushSince = (deps.now ?? Date.now)();
|
|
12427
|
-
const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
12428
12485
|
let checks;
|
|
12429
12486
|
try {
|
|
12430
12487
|
checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
|
|
12431
12488
|
} catch (e) {
|
|
12432
12489
|
throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
|
|
12433
12490
|
}
|
|
12491
|
+
if (trueMergeGateNote) checks = `${trueMergeGateNote}; ${checks}`;
|
|
12434
12492
|
await runGitPush(deps, ["push", "origin", "main"]);
|
|
12435
12493
|
const releaseUrl = clean2(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
12436
12494
|
await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
|
|
@@ -13688,6 +13746,7 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
|
|
|
13688
13746
|
// src/access.ts
|
|
13689
13747
|
var OWNER2 = "mutmutco";
|
|
13690
13748
|
var LOCKED_APP = "mmi-github-app";
|
|
13749
|
+
var PLUGIN_READ_REPO = "mutmutco/MMI-Hub";
|
|
13691
13750
|
var OVERGRANT_ROLES = /* @__PURE__ */ new Set(["admin", "maintain"]);
|
|
13692
13751
|
var REQUIRED_DATA_ACCESS = {};
|
|
13693
13752
|
function lockedBranches(repoClass, releaseTrack) {
|
|
@@ -13842,9 +13901,32 @@ async function auditOrgBasePermission(deps) {
|
|
|
13842
13901
|
}
|
|
13843
13902
|
return [];
|
|
13844
13903
|
}
|
|
13904
|
+
async function auditPluginReadAccess(owners, projectAdmins, deps) {
|
|
13905
|
+
const findings = [];
|
|
13906
|
+
for (const login of projectAdmins) {
|
|
13907
|
+
if (owners.has(login)) continue;
|
|
13908
|
+
const perm = (await restJson2(deps, `repos/${PLUGIN_READ_REPO}/collaborators/${login}/permission`, {})).permission;
|
|
13909
|
+
if (!perm || perm === "none") {
|
|
13910
|
+
findings.push({
|
|
13911
|
+
repo: PLUGIN_READ_REPO,
|
|
13912
|
+
kind: "plugin-read-missing",
|
|
13913
|
+
severity: "medium",
|
|
13914
|
+
actor: login,
|
|
13915
|
+
detail: `project-admin @${login} lacks read on ${PLUGIN_READ_REPO}; the MMI plugin marketplace add will 404 for them until granted`,
|
|
13916
|
+
remediation: `gh api -X PUT repos/${PLUGIN_READ_REPO}/collaborators/${login} -f permission=pull`
|
|
13917
|
+
});
|
|
13918
|
+
}
|
|
13919
|
+
}
|
|
13920
|
+
return findings;
|
|
13921
|
+
}
|
|
13845
13922
|
async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
|
|
13846
13923
|
const owners = new Set(await resolveOwners(deps));
|
|
13847
13924
|
const orgFindings = await auditOrgBasePermission(deps);
|
|
13925
|
+
const allProjectAdmins = /* @__PURE__ */ new Set();
|
|
13926
|
+
for (const target of targets) {
|
|
13927
|
+
for (const login of entriesValueByCanonicalRepo(matrix, target.repo) ?? []) allProjectAdmins.add(login);
|
|
13928
|
+
}
|
|
13929
|
+
orgFindings.push(...await auditPluginReadAccess(owners, allProjectAdmins, deps));
|
|
13848
13930
|
const repos = [];
|
|
13849
13931
|
for (const target of targets) {
|
|
13850
13932
|
repos.push(await auditRepoAccess(target.repo, target.class, owners, deps, new Set(entriesValueByCanonicalRepo(matrix, target.repo) ?? []), dataAccess, target.releaseTrack));
|
|
@@ -15929,6 +16011,62 @@ function requireProjectTarget(commandName, explicitTarget, currentRepo) {
|
|
|
15929
16011
|
return target;
|
|
15930
16012
|
}
|
|
15931
16013
|
|
|
16014
|
+
// src/deploy-guards.ts
|
|
16015
|
+
var GIT_SHOW_TIMEOUT_MS = 1e4;
|
|
16016
|
+
async function readStageBranchCompose(branch) {
|
|
16017
|
+
for (const ref of [`origin/${branch}`, branch]) {
|
|
16018
|
+
try {
|
|
16019
|
+
return (await execFileP2("git", ["show", `${ref}:docker-compose.yml`], { timeout: GIT_SHOW_TIMEOUT_MS })).stdout;
|
|
16020
|
+
} catch {
|
|
16021
|
+
}
|
|
16022
|
+
}
|
|
16023
|
+
return null;
|
|
16024
|
+
}
|
|
16025
|
+
function composeMarksRuntimeEnvTrue(composeText) {
|
|
16026
|
+
return composeText.split(/\r?\n/).some((raw) => {
|
|
16027
|
+
const line = raw.replace(/(^|\s)#.*$/, "$1");
|
|
16028
|
+
if (/(^|[\s"'])mmi\.runtime-env\s*:\s*["']?true["']?\s*$/.test(line)) return true;
|
|
16029
|
+
if (/mmi\.runtime-env\s*=\s*["']?true["']?\s*$/.test(line)) return true;
|
|
16030
|
+
return false;
|
|
16031
|
+
});
|
|
16032
|
+
}
|
|
16033
|
+
function promotionSourceBranch(stage2, releaseTrack) {
|
|
16034
|
+
if (stage2 === "main") return releaseTrack === "direct" ? "development" : "rc";
|
|
16035
|
+
return "development";
|
|
16036
|
+
}
|
|
16037
|
+
function evaluatePromotionFilelessGuard(input) {
|
|
16038
|
+
const { branch, stage: stage2 } = input;
|
|
16039
|
+
if (input.force) {
|
|
16040
|
+
return { ok: true, warn: `--skip-compose-guard: skipping the fileless-compose \u2194 DEPLOY#${stage2}.noEnvFile guard (#2813)` };
|
|
16041
|
+
}
|
|
16042
|
+
if (!input.sameRepo) {
|
|
16043
|
+
return { ok: true, warn: `fileless-compose guard skipped \u2014 run from the target repo checkout to verify the ${branch} compose against DEPLOY#${stage2}.noEnvFile (#2813).` };
|
|
16044
|
+
}
|
|
16045
|
+
if (input.composeText === null) {
|
|
16046
|
+
return { ok: true, warn: `could not read docker-compose.yml on ${branch} \u2014 skipping the fileless-compose guard (#2813).` };
|
|
16047
|
+
}
|
|
16048
|
+
if (input.fact === null || input.fact.present === false) {
|
|
16049
|
+
return { ok: true };
|
|
16050
|
+
}
|
|
16051
|
+
const noEnvFile = input.fact.noEnvFile === true;
|
|
16052
|
+
if (composeMarksRuntimeEnvTrue(input.composeText) && !noEnvFile) {
|
|
16053
|
+
return {
|
|
16054
|
+
ok: false,
|
|
16055
|
+
reason: `the ${branch} docker-compose.yml marks a service fileless (mmi.runtime-env: "true") but DEPLOY#${stage2}.noEnvFile is not true \u2014 the ${stage2} deploy would fail exit-78 ("compose is fileless but DEPLOY#${stage2} noEnvFile is not true"). Fix BEFORE promoting, then re-run:
|
|
16056
|
+
mmi-cli project set-deploy --stage ${stage2} --no-env-file true # register the fileless flag
|
|
16057
|
+
gh workflow run tenant-reconcile.yml --repo mutmutco/MMI-Hub # re-render the box control script (#1056)
|
|
16058
|
+
(or pass --skip-compose-guard to override this preflight)`
|
|
16059
|
+
};
|
|
16060
|
+
}
|
|
16061
|
+
if (noEnvFile && composeDeclaresEnvFile(input.composeText)) {
|
|
16062
|
+
return {
|
|
16063
|
+
ok: false,
|
|
16064
|
+
reason: `DEPLOY#${stage2}.noEnvFile is true but the ${branch} docker-compose.yml still declares env_file: \u2014 the box writes no .env, so docker compose dies on the missing file. Land the fileless compose (drop env_file:, add the mmi.runtime-env label) on ${branch}, or set-deploy --no-env-file false (or --skip-compose-guard to override).`
|
|
16065
|
+
};
|
|
16066
|
+
}
|
|
16067
|
+
return { ok: true };
|
|
16068
|
+
}
|
|
16069
|
+
|
|
15932
16070
|
// src/project-retire.ts
|
|
15933
16071
|
function retireSlugOf(repoOrSlug) {
|
|
15934
16072
|
return (repoOrSlug.includes("/") ? repoOrSlug.split("/").pop() : repoOrSlug).toLowerCase();
|
|
@@ -16215,7 +16353,7 @@ function registerSecretsCommands(program3) {
|
|
|
16215
16353
|
}
|
|
16216
16354
|
if (!await secretsOrgCatalogSet(d, body)) process.exitCode = 1;
|
|
16217
16355
|
}));
|
|
16218
|
-
secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--json", "machine-readable output").action(async (o) => {
|
|
16356
|
+
secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--skip-compose-guard", "skip the #2813 fileless-compose \u2194 DEPLOY#.noEnvFile promotion guard").option("--json", "machine-readable output").action(async (o) => {
|
|
16219
16357
|
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
16220
16358
|
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
16221
16359
|
}
|
|
@@ -16236,8 +16374,26 @@ function registerSecretsCommands(program3) {
|
|
|
16236
16374
|
if (!o.required?.length && centralContainer && meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
|
|
16237
16375
|
d.err('secrets preflight: requiredRuntimeSecrets is unset: treating as an empty contract (no required keys). Declare an explicit {"dev":[],"rc":[],"main":[]} in registry META to silence this warning.');
|
|
16238
16376
|
}
|
|
16377
|
+
let filelessOk = true;
|
|
16378
|
+
if (meta && centralContainer && !o.skipComposeGuard) {
|
|
16379
|
+
const stage2 = o.stage;
|
|
16380
|
+
const branch = promotionSourceBranch(stage2, resolveReleaseTrack(meta, void 0, repo));
|
|
16381
|
+
const cwdRepo = repoFromRemoteUrl((await execFileP2("git", ["remote", "get-url", "origin"]).catch(() => ({ stdout: "" }))).stdout);
|
|
16382
|
+
const targetRepo2 = o.repo ? o.repo.includes("/") ? o.repo : `mutmutco/${o.repo}` : cwdRepo;
|
|
16383
|
+
const sameRepo = !o.repo || Boolean(cwdRepo) && Boolean(targetRepo2) && cwdRepo.toLowerCase() === targetRepo2.toLowerCase();
|
|
16384
|
+
const facts = await fetchDeployFactsBySlug(slug, regDeps);
|
|
16385
|
+
const fact = facts?.stages?.[stage2] ?? null;
|
|
16386
|
+
const composeText = sameRepo ? await readStageBranchCompose(branch) : null;
|
|
16387
|
+
const verdict = evaluatePromotionFilelessGuard({ composeText, branch, stage: stage2, fact, sameRepo: Boolean(sameRepo), force: false });
|
|
16388
|
+
if (!verdict.ok) {
|
|
16389
|
+
d.err(`secrets preflight: ${verdict.reason}`);
|
|
16390
|
+
filelessOk = false;
|
|
16391
|
+
} else if (verdict.warn) {
|
|
16392
|
+
d.err(`secrets preflight: ${verdict.warn}`);
|
|
16393
|
+
}
|
|
16394
|
+
}
|
|
16239
16395
|
const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required, json: o.json, declaredSecrets: meta?.secrets });
|
|
16240
|
-
if (!ok) process.exitCode = 1;
|
|
16396
|
+
if (!ok || !filelessOk) process.exitCode = 1;
|
|
16241
16397
|
});
|
|
16242
16398
|
secrets.command("get <key>").description("print one secret value, RAW \u2014 MASTER-ONLY (WS6 #2184); non-master: `secrets use` to consume keyless or `secrets verify` to validate").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "vault slug override for org-infra namespaces (cloudflare, shared, \u2026) \u2014 master-gated").action((key, o) => withSecrets(async (d) => {
|
|
16243
16399
|
const ok = await secretsGet(d, key, o);
|
|
@@ -16674,7 +16830,7 @@ async function runIssueList(deps, opts) {
|
|
|
16674
16830
|
return shapeIssueList(rows);
|
|
16675
16831
|
}
|
|
16676
16832
|
function childrenGraphqlArgs(owner, name, number) {
|
|
16677
|
-
const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){issue(number:" + number + "){number subIssues(first:100){nodes{number title state url assignees(first:10){nodes{login}} repository{nameWithOwner} projectItems(first:5){nodes{project{id
|
|
16833
|
+
const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){issue(number:" + number + "){number subIssues(first:100){nodes{number title state url assignees(first:10){nodes{login}} repository{nameWithOwner} projectItems(first:5){nodes{project{id} fieldValues(first:20){nodes{... on ProjectV2ItemFieldSingleSelectValue{name field{...on ProjectV2SingleSelectField{name}}}}}}} timelineItems(first:30,itemTypes:[CROSS_REFERENCED_EVENT]){nodes{...on CrossReferencedEvent{source{...on PullRequest{number title state url}}}}}}}}}}";
|
|
16678
16834
|
return ["api", "graphql", "-f", `query=${query}`, "-f", `owner=${owner}`, "-f", `name=${name}`];
|
|
16679
16835
|
}
|
|
16680
16836
|
function shapeChildNode(node, depth, boardProjectId) {
|
|
@@ -17940,10 +18096,6 @@ function reloadAction(surface) {
|
|
|
17940
18096
|
}
|
|
17941
18097
|
}
|
|
17942
18098
|
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`;
|
|
17943
|
-
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`;
|
|
17944
|
-
var CURSOR_RECOVERY = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace";
|
|
17945
|
-
var OPENCODE_PLUGIN_INSTALL_COMMAND = "mmi-cli doctor --apply";
|
|
17946
|
-
var OPENCODE_RECOVERY = `${OPENCODE_PLUGIN_INSTALL_COMMAND} # then restart OpenCode to load MMI commands`;
|
|
17947
18099
|
var PLUGIN_SURFACE_HEAL = {
|
|
17948
18100
|
claude: {
|
|
17949
18101
|
delivery: "plugin-cli",
|
|
@@ -17955,43 +18107,15 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
17955
18107
|
{ args: ["plugin", "install", "mmi@mutmutco"], gated: true },
|
|
17956
18108
|
{ args: ["plugin", "enable", "mmi@mutmutco"], gated: false }
|
|
17957
18109
|
],
|
|
17958
|
-
pluginRunner: "claude",
|
|
17959
18110
|
fix: (surface) => `${CLAUDE_RECOVERY} # then ${reloadAction(surface)} to reload MMI commands`,
|
|
17960
18111
|
updateRecipe: [CLAUDE_RECOVERY]
|
|
17961
|
-
},
|
|
17962
|
-
codex: {
|
|
17963
|
-
delivery: "plugin-cli",
|
|
17964
|
-
recovery: CODEX_RECOVERY,
|
|
17965
|
-
healSteps: [
|
|
17966
|
-
{ args: ["plugin", "marketplace", "remove", LEGACY_MMI_MARKETPLACE], gated: false },
|
|
17967
|
-
{ args: ["plugin", "marketplace", "remove", "mutmutco"], gated: false },
|
|
17968
|
-
{ args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
|
|
17969
|
-
{ args: ["plugin", "add", "mmi@mutmutco"], gated: true }
|
|
17970
|
-
],
|
|
17971
|
-
pluginRunner: "codex",
|
|
17972
|
-
fix: (surface) => `${CODEX_RECOVERY} # then ${reloadAction(surface)}`,
|
|
17973
|
-
updateRecipe: [CODEX_RECOVERY, "codex plugin list # verify mmi@mutmutco shows the released version"],
|
|
17974
|
-
guardRecovery: "$env:MMI_AGENT_SURFACE='codex'; mmi-cli plugin-heal"
|
|
17975
|
-
},
|
|
17976
|
-
cursor: {
|
|
17977
|
-
delivery: "cursor-cache",
|
|
17978
|
-
recovery: CURSOR_RECOVERY,
|
|
17979
|
-
healSteps: null,
|
|
17980
|
-
fix: (surface) => `${CURSOR_RECOVERY}; then ${reloadAction(surface)} to reload MMI skills + hooks`,
|
|
17981
|
-
updateRecipe: [CURSOR_RECOVERY]
|
|
17982
|
-
},
|
|
17983
|
-
opencode: {
|
|
17984
|
-
delivery: "npm",
|
|
17985
|
-
recovery: OPENCODE_RECOVERY,
|
|
17986
|
-
healSteps: null,
|
|
17987
|
-
fix: () => OPENCODE_RECOVERY,
|
|
17988
|
-
updateRecipe: [OPENCODE_RECOVERY, "restart OpenCode, then run mmi-cli doctor to verify OpenCode plugin reports the released version"]
|
|
17989
18112
|
}
|
|
17990
18113
|
};
|
|
17991
18114
|
var CLAUDE_PLUGIN_RECOVERY = PLUGIN_SURFACE_HEAL.claude.recovery;
|
|
17992
18115
|
var CLAUDE_PLUGIN_HEAL_STEPS = PLUGIN_SURFACE_HEAL.claude.healSteps;
|
|
17993
|
-
|
|
17994
|
-
|
|
18116
|
+
function nonClaudeSurfaceHealMessage() {
|
|
18117
|
+
return "No Hub-shipped MMI plugin on this host \u2014 the Hub is Claude-only since #2741.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
|
|
18118
|
+
}
|
|
17995
18119
|
function healStepAborts(step, ok) {
|
|
17996
18120
|
return !ok && step.gated;
|
|
17997
18121
|
}
|
|
@@ -18070,6 +18194,9 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
18070
18194
|
pluginCachePresent: (0, import_node_fs21.existsSync)((0, import_node_path20.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
18071
18195
|
};
|
|
18072
18196
|
}
|
|
18197
|
+
function claudePluginGuardState(isOrgRepo) {
|
|
18198
|
+
return buildPluginGuardDecision(snapshotPluginGuardInput(detectSurface(process.env), isOrgRepo)).state;
|
|
18199
|
+
}
|
|
18073
18200
|
async function runClaudePlugin(args) {
|
|
18074
18201
|
try {
|
|
18075
18202
|
await runHostBin("claude", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
@@ -18078,14 +18205,6 @@ async function runClaudePlugin(args) {
|
|
|
18078
18205
|
return false;
|
|
18079
18206
|
}
|
|
18080
18207
|
}
|
|
18081
|
-
async function runCodexPlugin(args) {
|
|
18082
|
-
try {
|
|
18083
|
-
await runHostBin("codex", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
18084
|
-
return true;
|
|
18085
|
-
} catch {
|
|
18086
|
-
return false;
|
|
18087
|
-
}
|
|
18088
|
-
}
|
|
18089
18208
|
async function marketplaceAddRefSupported(bin) {
|
|
18090
18209
|
try {
|
|
18091
18210
|
const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
|
|
@@ -18105,33 +18224,36 @@ function refAbsenceNote(bin, refSupported) {
|
|
|
18105
18224
|
return refSupported ? "" : `
|
|
18106
18225
|
Note: \`${bin}\` has no \`--ref\` option; the marketplace manifest pins the released branch, so none is needed (#2516).`;
|
|
18107
18226
|
}
|
|
18108
|
-
|
|
18109
|
-
|
|
18110
|
-
|
|
18111
|
-
|
|
18227
|
+
var PLUGIN_READ_REPO2 = "mutmutco/MMI-Hub";
|
|
18228
|
+
function pluginReadGrantNote(login = "<your-github-login>") {
|
|
18229
|
+
return `
|
|
18230
|
+
If the reinstall 404s on ${PLUGIN_READ_REPO2}, you lack read on it. An org owner must grant it (idempotent):
|
|
18231
|
+
gh api -X PUT repos/${PLUGIN_READ_REPO2}/collaborators/${login} -f permission=pull`;
|
|
18232
|
+
}
|
|
18233
|
+
async function applyPluginHeal(surface, log, opts) {
|
|
18234
|
+
if (!opts?.force && surfaceToken(surface) !== "claude") return false;
|
|
18235
|
+
const tableSteps = PLUGIN_SURFACE_HEAL.claude.healSteps;
|
|
18112
18236
|
if (!tableSteps) return false;
|
|
18113
|
-
const
|
|
18114
|
-
const runner = descriptor.pluginRunner === "codex" ? runCodexPlugin : runClaudePlugin;
|
|
18115
|
-
const refSupported = await marketplaceAddRefSupported(bin);
|
|
18237
|
+
const refSupported = await marketplaceAddRefSupported("claude");
|
|
18116
18238
|
const { steps } = adaptHealStepsForRefSupport(tableSteps, refSupported);
|
|
18117
|
-
log(healBannerLine(
|
|
18239
|
+
log(healBannerLine("claude", "claude", refSupported));
|
|
18118
18240
|
for (const step of steps) {
|
|
18119
|
-
if (healStepAborts(step, await
|
|
18241
|
+
if (healStepAborts(step, await runClaudePlugin([...step.args]))) return false;
|
|
18120
18242
|
}
|
|
18121
18243
|
return true;
|
|
18122
18244
|
}
|
|
18123
|
-
function guardRecoveryCommand(surface) {
|
|
18124
|
-
const token = surfaceToken(surface);
|
|
18125
|
-
return token ? PLUGIN_SURFACE_HEAL[token]?.guardRecovery : void 0;
|
|
18126
|
-
}
|
|
18127
18245
|
async function runGuard(opts = {}, readOrigin) {
|
|
18128
18246
|
void opts;
|
|
18129
18247
|
try {
|
|
18130
18248
|
const surface = detectSurface(process.env);
|
|
18249
|
+
if (surfaceToken(surface) !== "claude") {
|
|
18250
|
+
process.exitCode = 0;
|
|
18251
|
+
return;
|
|
18252
|
+
}
|
|
18131
18253
|
const isOrgRepo = readOrigin ? await isOrgRepoRoot(readOrigin) : await isOrgRepoRoot();
|
|
18132
18254
|
const input = snapshotPluginGuardInput(surface, isOrgRepo);
|
|
18133
18255
|
const { state } = buildPluginGuardDecision(input);
|
|
18134
|
-
const { line, exitCode } = buildGuardSessionStartLine(state
|
|
18256
|
+
const { line, exitCode } = buildGuardSessionStartLine(state);
|
|
18135
18257
|
if (line) console.error(line);
|
|
18136
18258
|
process.exitCode = exitCode;
|
|
18137
18259
|
} catch {
|
|
@@ -18139,24 +18261,20 @@ async function runGuard(opts = {}, readOrigin) {
|
|
|
18139
18261
|
}
|
|
18140
18262
|
}
|
|
18141
18263
|
async function runPluginHeal(surface = detectSurface(process.env)) {
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
const descriptor = PLUGIN_SURFACE_HEAL[tableKey];
|
|
18145
|
-
if (!descriptor.healSteps) {
|
|
18146
|
-
console.log(descriptor.recovery);
|
|
18147
|
-
console.log(`Then: ${reloadAction(surface)}`);
|
|
18264
|
+
if (surfaceToken(surface) !== "claude") {
|
|
18265
|
+
console.log(nonClaudeSurfaceHealMessage());
|
|
18148
18266
|
return;
|
|
18149
18267
|
}
|
|
18150
|
-
const
|
|
18268
|
+
const descriptor = PLUGIN_SURFACE_HEAL.claude;
|
|
18269
|
+
const healed = await applyPluginHeal(surface, console.log, { force: true });
|
|
18151
18270
|
if (healed) {
|
|
18152
18271
|
console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI commands`);
|
|
18153
18272
|
} else {
|
|
18154
|
-
const
|
|
18155
|
-
const refSupported = await marketplaceAddRefSupported(bin);
|
|
18273
|
+
const refSupported = await marketplaceAddRefSupported("claude");
|
|
18156
18274
|
const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
|
|
18157
|
-
const note = refAbsenceNote(
|
|
18275
|
+
const note = refAbsenceNote("claude", refSupported);
|
|
18158
18276
|
console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
|
|
18159
|
-
${recovery}${note}`);
|
|
18277
|
+
${recovery}${note}${pluginReadGrantNote()}`);
|
|
18160
18278
|
}
|
|
18161
18279
|
}
|
|
18162
18280
|
var USER_SCOPE_GUARD_MARKER = "mmi-guard:v1";
|
|
@@ -19229,7 +19347,14 @@ function checkRepoWorktrees(probe) {
|
|
|
19229
19347
|
};
|
|
19230
19348
|
}
|
|
19231
19349
|
function checkClaudePlugin(probe) {
|
|
19232
|
-
const { installed, released } = probe;
|
|
19350
|
+
const { installed, released, guardState } = probe;
|
|
19351
|
+
if (guardState === "no-install" || guardState === "unresolved") {
|
|
19352
|
+
return {
|
|
19353
|
+
ok: false,
|
|
19354
|
+
label: guardState === "no-install" ? "Claude plugin \u2014 not installed" : "Claude plugin \u2014 unresolved (marketplace/cache missing)",
|
|
19355
|
+
fix: "run `mmi-cli plugin-heal` to reinstall the MMI marketplace + plugin, then restart Claude"
|
|
19356
|
+
};
|
|
19357
|
+
}
|
|
19233
19358
|
const behind = Boolean(installed && released) && compareVersions(installed, released) < 0;
|
|
19234
19359
|
if (behind) {
|
|
19235
19360
|
return {
|
|
@@ -19240,6 +19365,16 @@ function checkClaudePlugin(probe) {
|
|
|
19240
19365
|
}
|
|
19241
19366
|
return { ok: true, label: installed ? `Claude plugin ${installed}` : "Claude plugin" };
|
|
19242
19367
|
}
|
|
19368
|
+
function checkCliVersion(input) {
|
|
19369
|
+
const report = buildVersionLagReport(input);
|
|
19370
|
+
if (!report.releasedVersion) return { ok: true, label: `mmi-cli ${report.currentVersion}` };
|
|
19371
|
+
if (report.ok) return { ok: true, label: `mmi-cli ${report.currentVersion}` };
|
|
19372
|
+
return {
|
|
19373
|
+
ok: false,
|
|
19374
|
+
label: `mmi-cli ${report.currentVersion} \u2192 ${report.releasedVersion}`,
|
|
19375
|
+
fix: report.fix
|
|
19376
|
+
};
|
|
19377
|
+
}
|
|
19243
19378
|
function checkTrainSync(result) {
|
|
19244
19379
|
const problems = result.branches.filter((b) => b.status === "skipped" && (b.reason === "diverged" || b.reason === "ff-failed"));
|
|
19245
19380
|
if (problems.length) {
|
|
@@ -19286,9 +19421,10 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
19286
19421
|
checks.push({ ok: false, label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block" });
|
|
19287
19422
|
}
|
|
19288
19423
|
}
|
|
19289
|
-
const plugin = checkClaudePlugin({ installed, released });
|
|
19424
|
+
const plugin = checkClaudePlugin({ installed, released, guardState: deps.pluginGuardState(isOrgRepo) });
|
|
19290
19425
|
checks.push(plugin);
|
|
19291
19426
|
if (!plugin.ok) restartPending = true;
|
|
19427
|
+
checks.push(checkCliVersion({ currentVersion: deps.currentCliVersion(), releasedVersion: released }));
|
|
19292
19428
|
if (isOrgRepo && !opts.fast && !opts.banner) {
|
|
19293
19429
|
try {
|
|
19294
19430
|
checks.push(checkTrainSync(await deps.syncTrain()));
|
|
@@ -19566,7 +19702,9 @@ function mmiDoctorDeps() {
|
|
|
19566
19702
|
awsCallerArn,
|
|
19567
19703
|
isOrgRepo: () => isOrgRepoRoot(),
|
|
19568
19704
|
installedPluginVersion: installedClaudePluginVersion,
|
|
19705
|
+
pluginGuardState: claudePluginGuardState,
|
|
19569
19706
|
releasedVersion: fetchNpmReleasedVersion,
|
|
19707
|
+
currentCliVersion: resolveClientVersion,
|
|
19570
19708
|
readGitignore,
|
|
19571
19709
|
writeGitignore,
|
|
19572
19710
|
hasRepoLocalWorktrees,
|
|
@@ -20449,15 +20587,6 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
20449
20587
|
console.log(JSON.stringify(report, null, 2));
|
|
20450
20588
|
if (!report.rcand.canApply) process.exitCode = 1;
|
|
20451
20589
|
});
|
|
20452
|
-
async function readStageBranchCompose(branch) {
|
|
20453
|
-
for (const ref of [`origin/${branch}`, branch]) {
|
|
20454
|
-
try {
|
|
20455
|
-
return (await execFileP2("git", ["show", `${ref}:docker-compose.yml`], { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
20456
|
-
} catch {
|
|
20457
|
-
}
|
|
20458
|
-
}
|
|
20459
|
-
return null;
|
|
20460
|
-
}
|
|
20461
20590
|
project.command("set-deploy [owner/repo]").description("write the DEPLOY#<stage> Hetzner deploy coords for a tenant (master-only) \u2014 the explicit-coords path that seeds a freshly-bootstrapped tenant; defaults to the current repo").requiredOption("--stage <stage>", "dev | rc | main").option("--ssh-host <host>", "the box address the deploy ssh-es into (required for hetzner-ssh)").option("--ssh-user <user>", "ssh user (default root)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535)").option("--substrate <substrate>", "hetzner-ssh (default)").option("--deploy-path <path>", "on-box per-stage release root (default /opt/mmi/<slug>/<stage>)").option("--service <name>", "systemd/compose service name (default the slug)").option("--domain <domain>", "canonical serving host (default the project edgeDomains[stage])").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable)").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) \u2014 true = #2662 env passthrough, no .env symlink; omit to leave the row unchanged").option("--force", "skip the #2748 fileless-compose guard (flip noEnvFile=true even if the stage branch compose still declares env_file:)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
20462
20591
|
const cfg = await loadConfig();
|
|
20463
20592
|
let target;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.18.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",
|