@mutmutco/cli 3.68.0 → 3.70.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.
Files changed (2) hide show
  1. package/dist/main.cjs +267 -106
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -33,9 +33,11 @@ __export(index_exports, {
33
33
  DEFAULT_PRIORITY: () => DEFAULT_PRIORITY,
34
34
  awsCallerArn: () => awsCallerArn,
35
35
  classifyParseError: () => classifyParseError,
36
+ commandOwnLongFlags: () => commandOwnLongFlags,
36
37
  envHealLockPath: () => envHealLockPath,
37
38
  gcPlan: () => gcPlan,
38
39
  isOrgRegisteredRepo: () => isOrgRegisteredRepo,
40
+ positionalTargetForm: () => positionalTargetForm,
39
41
  registryClientDeps: () => registryClientDeps,
40
42
  repoSlug: () => repoSlug,
41
43
  shouldMarkWorktreeActivity: () => shouldMarkWorktreeActivity,
@@ -5003,6 +5005,7 @@ function buildPrMergeResultPayload(input) {
5003
5005
  function buildPrMergeArgs(input) {
5004
5006
  const args = ["pr", "merge", input.number, ...input.repoArgs, input.method];
5005
5007
  if (input.deleteBranch !== false) args.push("--delete-branch");
5008
+ if (input.bodyFile) args.push("--body-file", input.bodyFile);
5006
5009
  if (input.auto) args.push("--auto");
5007
5010
  return args;
5008
5011
  }
@@ -7235,7 +7238,7 @@ function checkGateBudget(files, opts = {}) {
7235
7238
  }
7236
7239
 
7237
7240
  // src/project-model.ts
7238
- var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-app", "desktop-game", "non-deployable", "cli-tool", "worker"];
7241
+ var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-app", "desktop-game", "mobile-app", "non-deployable", "cli-tool", "worker"];
7239
7242
  var DEPLOY_MODELS = ["hub-serverless", "serverless", "tenant-container", "solo-container", "registry-publish", "content", "none"];
7240
7243
  var RELEASE_TRACKS = ["full", "direct", "trunk"];
7241
7244
  var PROJECT_TYPE_SET = new Set(PROJECT_TYPES);
@@ -7272,7 +7275,7 @@ function resolveDeployModel(meta, repo) {
7272
7275
  const projectType = resolveProjectType(meta, repo);
7273
7276
  if (projectType === "content" || meta?.class === "content") return "content";
7274
7277
  if (projectType === "hub-service" || repoIsHub(repo)) return "hub-serverless";
7275
- if (projectType === "desktop-app" || projectType === "desktop-game" || projectType === "non-deployable") return "none";
7278
+ if (projectType === "desktop-app" || projectType === "desktop-game" || projectType === "mobile-app" || projectType === "non-deployable") return "none";
7276
7279
  if (projectType === "cli-tool") return "registry-publish";
7277
7280
  return "tenant-container";
7278
7281
  }
@@ -7751,7 +7754,13 @@ function collectRegistryRepos(projects) {
7751
7754
  if (![...seen].some((r) => r.toLowerCase() === HUB_REPO.toLowerCase())) seen.add(HUB_REPO);
7752
7755
  return [...seen].sort((a, b) => a.localeCompare(b));
7753
7756
  }
7754
- async function resolveRepoMergeCiPolicy(repo, deps) {
7757
+ async function prTriggeredWorkflowsOnRef(deps, repo, branch, repoClass) {
7758
+ const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, branch, PRODUCT_GATE_PATH);
7759
+ const workflowPaths = repoClass === "hub" ? [".github/workflows/gate.yml"] : hasGate ? [PRODUCT_GATE_PATH] : await listWorkflowPaths(deps, repo, branch);
7760
+ if (workflowPaths === void 0) return void 0;
7761
+ return filterPullRequestTriggered(deps, repo, branch, workflowPaths);
7762
+ }
7763
+ async function resolveRepoMergeCiPolicy(repo, deps, headRef) {
7755
7764
  const meta = await deps.getProjectMeta(slugFromRepo(repo));
7756
7765
  const repoClass = classifyRepo(repo, meta);
7757
7766
  if (repoClass === "content") {
@@ -7762,17 +7771,25 @@ async function resolveRepoMergeCiPolicy(repo, deps) {
7762
7771
  });
7763
7772
  }
7764
7773
  const baseBranch = "development";
7765
- const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
7766
- const workflowPaths = repoClass === "hub" ? [".github/workflows/gate.yml"] : hasGate ? [PRODUCT_GATE_PATH] : await listWorkflowPaths(deps, repo, baseBranch);
7767
- if (workflowPaths === void 0) {
7774
+ const prTriggered = await prTriggeredWorkflowsOnRef(deps, repo, baseBranch, repoClass);
7775
+ if (prTriggered === void 0) {
7768
7776
  return { policy: "wait-for-checks", reason: "workflows listing unreadable (API error) \u2014 failing safe to wait-for-checks" };
7769
7777
  }
7770
- const prTriggered = await filterPullRequestTriggered(deps, repo, baseBranch, workflowPaths);
7771
- return resolveMergeCiPolicy({
7772
- workflowPaths: prTriggered,
7778
+ let effective = prTriggered;
7779
+ let headReason = "";
7780
+ if (!effective.length && headRef && headRef !== baseBranch) {
7781
+ const onHead = await prTriggeredWorkflowsOnRef(deps, repo, headRef, repoClass);
7782
+ if (onHead?.length) {
7783
+ effective = onHead;
7784
+ headReason = ` (on PR head ${headRef}; absent on ${baseBranch})`;
7785
+ }
7786
+ }
7787
+ const resolved = resolveMergeCiPolicy({
7788
+ workflowPaths: effective,
7773
7789
  registryCi: meta?.ci,
7774
7790
  registryRequiredChecks: meta?.requiredChecks
7775
7791
  });
7792
+ return headReason ? { ...resolved, reason: `${resolved.reason}${headReason}` } : resolved;
7776
7793
  }
7777
7794
  async function filterPullRequestTriggered(deps, repo, branch, paths) {
7778
7795
  const kept = [];
@@ -8970,7 +8987,7 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
8970
8987
  }
8971
8988
 
8972
8989
  // src/index.ts
8973
- var import_node_os10 = require("node:os");
8990
+ var import_node_os11 = require("node:os");
8974
8991
 
8975
8992
  // src/board.ts
8976
8993
  var import_node_child_process7 = require("node:child_process");
@@ -9464,7 +9481,8 @@ var OWNER = "mutmutco";
9464
9481
  var SSM_ROOT = "/mmi-future";
9465
9482
  var PROJECT_TIER_SEGMENT = "dev";
9466
9483
  var ORG_INFRA_SLUG = "_org";
9467
- var KEY_RE = /^(?:[a-z][a-z0-9-]*\/)?[A-Za-z][A-Za-z0-9_]*$/;
9484
+ var KEY_MAX_PATH_SEGMENTS = 4;
9485
+ var KEY_RE = new RegExp(`^(?:[a-z0-9][a-z0-9-]*/){0,${KEY_MAX_PATH_SEGMENTS}}[A-Za-z][A-Za-z0-9_]*$`);
9468
9486
  function isValidSecretKey(key) {
9469
9487
  if (!key || key.length > 256) return false;
9470
9488
  if (key.includes("..") || key.startsWith("/") || key.includes("*")) return false;
@@ -9663,7 +9681,7 @@ function formatCapabilities(r) {
9663
9681
  no vault credentials visible`;
9664
9682
  const width = Math.max(...items.map((i) => i.scope.length));
9665
9683
  const lines = items.map(
9666
- (i) => `${i.accessible ? "+" : " "} ${i.scope.padEnd(width)} ${i.tier.padEnd(7)} ${i.reason}`
9684
+ (i) => `${i.accessible ? "+" : " "} ${i.scope.padEnd(width)} ${i.tier.padEnd(7)} ${i.grantScope ? `${i.reason} (${i.grantScope})` : i.reason}`
9667
9685
  );
9668
9686
  const reachable = items.filter((i) => i.accessible).length;
9669
9687
  return [
@@ -9674,6 +9692,7 @@ no vault credentials visible`;
9674
9692
  ...lines,
9675
9693
  "",
9676
9694
  "+ = usable now. Consume keyless: `mmi-cli secrets use <KEY> -- <cmd>` (inject, never printed); validate: `mmi-cli secrets verify <KEY>`. No command prints a raw value (#2844).",
9695
+ " `grant (read)` = keyless use only \u2014 rotating or removing that key still needs an rw grant (#3652).",
9677
9696
  ` Org-infra (#3463): \`--slug\` is ALWAYS \`${ORG_INFRA_SLUG}\`; the provider is the key's FIRST SEGMENT \u2014 \`mmi-cli secrets use cloudflare/GLOBAL_API_KEY --slug ${ORG_INFRA_SLUG} -- <cmd>\` (master-gated).`
9678
9697
  ].join("\n");
9679
9698
  }
@@ -10338,11 +10357,29 @@ async function secretsRemove(deps, key, opts) {
10338
10357
  }
10339
10358
  deps.log(`removed ${key}`);
10340
10359
  }
10341
- async function secretsGrant(deps, repo, login, key, _opts) {
10360
+ var WILDCARD_GRANT_KEY_RE = /^(?:[a-z][a-z0-9-]*\/)?\*$/;
10361
+ function isWildcardGrantKey(key) {
10362
+ return WILDCARD_GRANT_KEY_RE.test(key);
10363
+ }
10364
+ function isValidGrantKey(key) {
10365
+ if (!key || key.length > 256) return false;
10366
+ if (key.includes("..") || key.startsWith("/")) return false;
10367
+ return isWildcardGrantKey(key) || isValidSecretKey(key);
10368
+ }
10369
+ async function secretsGrant(deps, repo, login, key, opts) {
10370
+ if (!isValidGrantKey(key)) {
10371
+ deps.err(`invalid grant key ${JSON.stringify(key)} \u2014 expected <provider>/<KEY>, or a read-only wildcard \`*\` / \`<provider>/*\``);
10372
+ return;
10373
+ }
10374
+ if (isWildcardGrantKey(key) && !opts.read) {
10375
+ deps.err(`secrets grant: ${key} is a wildcard \u2014 pass --read. A wildcard never confers write, so granting it rw is refused rather than silently narrowed.`);
10376
+ return;
10377
+ }
10378
+ const scope = opts.read ? "read" : "rw";
10342
10379
  const res = await deps.fetch(`${deps.apiUrl}/secrets/grant`, {
10343
10380
  method: "POST",
10344
10381
  headers: await deps.headers({ "content-type": "application/json" }),
10345
- body: JSON.stringify({ repo, login, key }),
10382
+ body: JSON.stringify({ repo, login, key, scope }),
10346
10383
  signal: AbortSignal.timeout(TIMEOUT_MS)
10347
10384
  });
10348
10385
  if (!res.ok) {
@@ -10351,7 +10388,7 @@ async function secretsGrant(deps, repo, login, key, _opts) {
10351
10388
  );
10352
10389
  return;
10353
10390
  }
10354
- deps.log(`granted @${login} access to ${key} in ${repo}`);
10391
+ deps.log(`granted @${login} ${scope === "read" ? "read-only" : "read/write"} access to ${key} in ${repo}`);
10355
10392
  }
10356
10393
  async function secretsRevoke(deps, repo, login, key, _opts) {
10357
10394
  const res = await deps.fetch(`${deps.apiUrl}/secrets/revoke`, {
@@ -15943,12 +15980,15 @@ async function resolveOwners(deps) {
15943
15980
  function collaboratorRole(c) {
15944
15981
  return c.role_name ?? (c.permissions?.admin ? "admin" : c.permissions?.maintain ? "maintain" : "write");
15945
15982
  }
15946
- async function auditRepoCollaborators(repo, owners, deps) {
15983
+ async function auditRepoCollaborators(repo, owners, deps, projectAdmins = /* @__PURE__ */ new Set(), sanctionedAdmins = /* @__PURE__ */ new Set()) {
15947
15984
  const collabs = await restPagedJson(deps, `repos/${repo}/collaborators?affiliation=direct`, []);
15948
15985
  const findings = [];
15986
+ const writeOrBetter = /* @__PURE__ */ new Set();
15949
15987
  for (const c of collabs) {
15950
15988
  if (owners.has(c.login)) continue;
15951
15989
  const role = collaboratorRole(c);
15990
+ writeOrBetter.add(c.login);
15991
+ if (OVERGRANT_ROLES.has(role) && sanctionedAdmins.has(c.login)) continue;
15952
15992
  if (OVERGRANT_ROLES.has(role)) {
15953
15993
  findings.push({
15954
15994
  repo,
@@ -15960,6 +16000,17 @@ async function auditRepoCollaborators(repo, owners, deps) {
15960
16000
  });
15961
16001
  }
15962
16002
  }
16003
+ for (const login of projectAdmins) {
16004
+ if (owners.has(login) || writeOrBetter.has(login)) continue;
16005
+ findings.push({
16006
+ repo,
16007
+ kind: "collaborator-undergrant",
16008
+ severity: "medium",
16009
+ actor: login,
16010
+ detail: `@${login} is a declared project-admin of ${repo} in the registry but holds no direct collaborator role; secrets self-serve and train authority fail closed for them`,
16011
+ remediation: `gh api -X PUT repos/${repo}/collaborators/${login} -f permission=push`
16012
+ });
16013
+ }
15963
16014
  return findings;
15964
16015
  }
15965
16016
  async function auditTrainBranch(repo, branch, owners, deps, projectAdmins = /* @__PURE__ */ new Set()) {
@@ -16007,6 +16058,18 @@ async function auditTrainBranch(repo, branch, owners, deps, projectAdmins = /* @
16007
16058
  });
16008
16059
  }
16009
16060
  }
16061
+ for (const login of projectAdmins) {
16062
+ if (owners.has(login) || users.includes(login)) continue;
16063
+ findings.push({
16064
+ repo,
16065
+ branch,
16066
+ kind: "train-allowlist-missing",
16067
+ severity: "medium",
16068
+ actor: login,
16069
+ detail: `declared project-admin @${login} is missing from the ${branch} push allowlist \u2014 the registry grants them train authority the branch lock denies`,
16070
+ remediation: `gh api -X POST repos/${repo}/branches/${branch}/protection/restrictions/users -f users[]="${login}"`
16071
+ });
16072
+ }
16010
16073
  const apps = (restrictions.apps ?? []).map((a) => a.slug);
16011
16074
  if (!apps.includes(LOCKED_APP)) {
16012
16075
  findings.push({
@@ -16039,9 +16102,9 @@ function auditDataAccessContracts(repo, contracts = { consumers: {} }) {
16039
16102
  }
16040
16103
  return findings;
16041
16104
  }
16042
- async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /* @__PURE__ */ new Set(), dataAccess, releaseTrack) {
16105
+ async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /* @__PURE__ */ new Set(), dataAccess, releaseTrack, sanctionedAdmins = /* @__PURE__ */ new Set()) {
16043
16106
  const findings = [];
16044
- findings.push(...await auditRepoCollaborators(repo, owners, deps));
16107
+ findings.push(...await auditRepoCollaborators(repo, owners, deps, projectAdmins, sanctionedAdmins));
16045
16108
  if (dataAccess) findings.push(...auditDataAccessContracts(repo, dataAccess));
16046
16109
  const track = releaseTrack ?? (repoClass === "content" ? "trunk" : void 0);
16047
16110
  for (const branch of lockedBranches(repoClass, track)) {
@@ -16081,7 +16144,7 @@ async function auditPluginReadAccess(owners, projectAdmins, deps) {
16081
16144
  }
16082
16145
  return findings;
16083
16146
  }
16084
- async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
16147
+ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess, sanctioned = {}) {
16085
16148
  if (targets.length === 0) {
16086
16149
  return {
16087
16150
  ok: false,
@@ -16118,7 +16181,16 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
16118
16181
  orgFindings.push(...await auditPluginReadAccess(owners, allProjectAdmins, deps));
16119
16182
  const repos = [];
16120
16183
  for (const target of targets) {
16121
- repos.push(await auditRepoAccess(target.repo, target.class, owners, deps, new Set(entriesValueByCanonicalRepo(matrix, target.repo) ?? []), dataAccess, target.releaseTrack));
16184
+ repos.push(await auditRepoAccess(
16185
+ target.repo,
16186
+ target.class,
16187
+ owners,
16188
+ deps,
16189
+ new Set(entriesValueByCanonicalRepo(matrix, target.repo) ?? []),
16190
+ dataAccess,
16191
+ target.releaseTrack,
16192
+ new Set(entriesValueByCanonicalRepo(sanctioned, target.repo) ?? [])
16193
+ ));
16122
16194
  }
16123
16195
  const ok = orgFindings.every((f) => f.severity !== "high") && repos.every((r) => r.ok);
16124
16196
  return { ok, owners: [...owners], orgFindings, repos };
@@ -16157,6 +16229,10 @@ function loadAccessMatrix(matrixJson) {
16157
16229
  if (!matrixJson) return {};
16158
16230
  return safeJson(matrixJson, {}).projectAdmins ?? {};
16159
16231
  }
16232
+ function loadSanctionedAdmins(matrixJson) {
16233
+ if (!matrixJson) return {};
16234
+ return safeJson(matrixJson, {}).sanctionedAdmins ?? {};
16235
+ }
16160
16236
  function loadDataAccessContracts(dataAccessJson) {
16161
16237
  if (!dataAccessJson) return { consumers: {} };
16162
16238
  const parsed = safeJson(dataAccessJson, { consumers: {} });
@@ -16548,6 +16624,43 @@ function evaluate(changed, policy, present = () => false) {
16548
16624
  function git(args, cwd) {
16549
16625
  return (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
16550
16626
  }
16627
+ var COAUTHOR_KEY = "Co-authored-by";
16628
+ var GH_MESSAGE_SEPARATOR = /^-{5,}$/;
16629
+ var LIFTED_KEYS = new RegExp(`^(?:${TRAILER_KEY}|${COAUTHOR_KEY}):`, "i");
16630
+ function parseTrailers(message, cwd) {
16631
+ try {
16632
+ return (0, import_node_child_process10.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
16633
+ cwd,
16634
+ input: message,
16635
+ encoding: "utf8",
16636
+ maxBuffer: 4 * 1024 * 1024
16637
+ }).split("\n").map((l) => l.trim()).filter(Boolean);
16638
+ } catch {
16639
+ return [];
16640
+ }
16641
+ }
16642
+ function squashBodyWithOverride(commits, cwd) {
16643
+ const overrides = [];
16644
+ const coauthors = [];
16645
+ const bodies = [];
16646
+ for (const commit of commits) {
16647
+ const message = `${commit.headline}
16648
+
16649
+ ${commit.body}`.trim();
16650
+ for (const trailer of parseTrailers(message, cwd)) {
16651
+ if (trailer.toLowerCase().startsWith(`${TRAILER_KEY.toLowerCase()}:`)) {
16652
+ if (!overrides.includes(trailer)) overrides.push(trailer);
16653
+ } else if (trailer.toLowerCase().startsWith(`${COAUTHOR_KEY.toLowerCase()}:`)) {
16654
+ if (!coauthors.some((c) => c.toLowerCase() === trailer.toLowerCase())) coauthors.push(trailer);
16655
+ }
16656
+ }
16657
+ const source = commits.length > 1 ? message : commit.body;
16658
+ const kept = source.split("\n").filter((line) => !LIFTED_KEYS.test(line.trim()) && !GH_MESSAGE_SEPARATOR.test(line.trim())).join("\n").trim();
16659
+ if (kept) bodies.push(kept);
16660
+ }
16661
+ if (!overrides.length) return null;
16662
+ return [...bodies, [...overrides, ...coauthors].join("\n")].join("\n\n");
16663
+ }
16551
16664
  function resolveBase(cwd, explicit) {
16552
16665
  const candidates = [explicit, process.env.TEST_POLICY_BASE, "origin/development", "origin/main"].filter(Boolean);
16553
16666
  for (const ref of candidates) {
@@ -18035,8 +18148,8 @@ function registerSecretsCommands(program3) {
18035
18148
  if (ok === false) process.exitCode = 1;
18036
18149
  });
18037
18150
  });
18038
- secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to one specific org-infra secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
18039
- secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-infra secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
18151
+ secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to an org-infra secret. Default is read/write on one exact key; --read grants keyless USE only, and only --read accepts a wildcard key (`*` = the whole namespace, `<provider>/*` = one provider group) (#3652)").option("--read", "read-only: permits keyless `secrets use`, never `set`/`rm`. Required for a wildcard key.").action((repo, login, key, o) => withSecrets((d) => secretsGrant(d, repo, login, key, { read: o.read })));
18152
+ secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-infra secret access (pass the grant key exactly as granted, wildcard included)").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
18040
18153
  }
18041
18154
 
18042
18155
  // src/app-actor.ts
@@ -21350,6 +21463,7 @@ function registerBoardCommands(program3) {
21350
21463
  var import_node_fs27 = require("node:fs");
21351
21464
  var import_promises6 = require("node:fs/promises");
21352
21465
  var import_node_path26 = require("node:path");
21466
+ var import_node_os8 = require("node:os");
21353
21467
  var import_node_child_process12 = require("node:child_process");
21354
21468
 
21355
21469
  // src/board-advance.ts
@@ -22258,8 +22372,36 @@ ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).find((line) => line
22258
22372
  var defaultGhMergeAutoIo = {
22259
22373
  gh: async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout
22260
22374
  };
22375
+ async function composeOverrideBodyFile(prNumber, repoArgs, gh) {
22376
+ try {
22377
+ const raw = await gh(["pr", "view", prNumber, ...repoArgs, "--json", "commits"], GC_GH_TIMEOUT_MS2);
22378
+ const commits = JSON.parse(raw).commits ?? [];
22379
+ const body = squashBodyWithOverride(commits.map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" })), process.cwd());
22380
+ if (!body) return void 0;
22381
+ const dir = (0, import_node_fs27.mkdtempSync)((0, import_node_path26.join)((0, import_node_os8.tmpdir)(), "mmi-squash-body-"));
22382
+ const path2 = (0, import_node_path26.join)(dir, "body.txt");
22383
+ (0, import_node_fs27.writeFileSync)(path2, `${body}
22384
+ `, "utf8");
22385
+ return { path: path2, cleanup: () => {
22386
+ try {
22387
+ (0, import_node_fs27.rmSync)(dir, { recursive: true, force: true });
22388
+ } catch {
22389
+ }
22390
+ } };
22391
+ } catch {
22392
+ return void 0;
22393
+ }
22394
+ }
22261
22395
  async function ghMergeAutoEnqueue(prNumber, repo, method, io = defaultGhMergeAutoIo) {
22262
22396
  const args = repo ? ["--repo", repo] : [];
22397
+ const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh);
22398
+ try {
22399
+ return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path);
22400
+ } finally {
22401
+ overrideBody?.cleanup();
22402
+ }
22403
+ }
22404
+ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile) {
22263
22405
  const headRef = (await io.gh(["pr", "view", prNumber, ...args, "--json", "headRefName", "--jq", ".headRefName"], GC_GH_TIMEOUT_MS2).catch(() => "")).trim();
22264
22406
  const deleteBranch = !isProtectedBranch(headRef);
22265
22407
  const readMergeState = () => readGhPrStateWithRetry(
@@ -22278,7 +22420,7 @@ async function ghMergeAutoEnqueue(prNumber, repo, method, io = defaultGhMergeAut
22278
22420
  return s.ok && s.state === "MERGED";
22279
22421
  },
22280
22422
  reEnqueue: async () => {
22281
- await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch }), GH_MUTATION_TIMEOUT_MS);
22423
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch, bodyFile }), GH_MUTATION_TIMEOUT_MS);
22282
22424
  }
22283
22425
  });
22284
22426
  if (confirmation === "merged") return { mergeStatus: "merged" };
@@ -22286,7 +22428,7 @@ async function ghMergeAutoEnqueue(prNumber, repo, method, io = defaultGhMergeAut
22286
22428
  return { mergeStatus: "failed", error: "auto-merge did not stick \u2014 GitHub reported no autoMergeRequest after enqueue; retry once the PR has a pending check" };
22287
22429
  };
22288
22430
  try {
22289
- await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch }), GH_MUTATION_TIMEOUT_MS);
22431
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch, bodyFile }), GH_MUTATION_TIMEOUT_MS);
22290
22432
  } catch (e) {
22291
22433
  const message = String(e.message || "");
22292
22434
  if (/already been merged/i.test(message)) return { mergeStatus: "merged" };
@@ -22294,7 +22436,7 @@ async function ghMergeAutoEnqueue(prNumber, repo, method, io = defaultGhMergeAut
22294
22436
  if (note) return { mergeStatus: "failed", error: note };
22295
22437
  if (mergeAutoRejectedPrAlreadyClean(message)) {
22296
22438
  try {
22297
- await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: false, deleteBranch }), GH_MUTATION_TIMEOUT_MS);
22439
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: false, deleteBranch, bodyFile }), GH_MUTATION_TIMEOUT_MS);
22298
22440
  } catch (e2) {
22299
22441
  const m2 = String(e2.message || "");
22300
22442
  if (/already been merged/i.test(m2)) return { mergeStatus: "merged" };
@@ -22305,7 +22447,7 @@ async function ghMergeAutoEnqueue(prNumber, repo, method, io = defaultGhMergeAut
22305
22447
  return { mergeStatus: "failed", error: `could not confirm PR state after clean-status fallback: ${afterDirect.error}` };
22306
22448
  }
22307
22449
  try {
22308
- await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch }), GH_MUTATION_TIMEOUT_MS);
22450
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch, bodyFile }), GH_MUTATION_TIMEOUT_MS);
22309
22451
  } catch (e3) {
22310
22452
  const m3 = String(e3.message || "");
22311
22453
  if (/already been merged/i.test(m3)) return { mergeStatus: "merged" };
@@ -23829,7 +23971,7 @@ function registerDeployCommands(program3) {
23829
23971
 
23830
23972
  // src/discovery-commands.ts
23831
23973
  var import_node_fs31 = require("node:fs");
23832
- var import_node_os8 = require("node:os");
23974
+ var import_node_os9 = require("node:os");
23833
23975
  var import_node_path29 = require("node:path");
23834
23976
  var GC_GH_TIMEOUT_MS3 = 2e4;
23835
23977
  async function collectStatus() {
@@ -24005,7 +24147,7 @@ async function collectOnboardStatus() {
24005
24147
  else if (top) nextCommand = `mmi-cli board claim ${top.number} # ${top.title}`;
24006
24148
  else nextCommand = "mmi-cli board read \u2014 no claimable items found";
24007
24149
  }
24008
- const home = (0, import_node_os8.homedir)();
24150
+ const home = (0, import_node_os9.homedir)();
24009
24151
  const plugin = onboardPluginGate({
24010
24152
  readKnown: () => readFileSyncSafe((0, import_node_path29.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs31.readFileSync),
24011
24153
  readSettings: () => readFileSyncSafe((0, import_node_path29.join)(home, ".claude", "settings.json"), import_node_fs31.readFileSync)
@@ -25388,17 +25530,17 @@ function parseOriginRepo(remoteUrl) {
25388
25530
  }
25389
25531
  function ghHostsConfigPath(env, platform2) {
25390
25532
  const sep2 = platform2 === "win32" ? "\\" : "/";
25391
- const join26 = (...parts) => parts.join(sep2);
25533
+ const join27 = (...parts) => parts.join(sep2);
25392
25534
  const explicit = env.GH_CONFIG_DIR?.trim();
25393
- if (explicit) return join26(explicit, "hosts.yml");
25535
+ if (explicit) return join27(explicit, "hosts.yml");
25394
25536
  if (platform2 === "win32") {
25395
25537
  const appData = (env.AppData ?? env.APPDATA)?.trim();
25396
- return appData ? join26(appData, "GitHub CLI", "hosts.yml") : void 0;
25538
+ return appData ? join27(appData, "GitHub CLI", "hosts.yml") : void 0;
25397
25539
  }
25398
25540
  const xdg = env.XDG_CONFIG_HOME?.trim();
25399
- if (xdg) return join26(xdg, "gh", "hosts.yml");
25541
+ if (xdg) return join27(xdg, "gh", "hosts.yml");
25400
25542
  const home = env.HOME?.trim();
25401
- return home ? join26(home, ".config", "gh", "hosts.yml") : void 0;
25543
+ return home ? join27(home, ".config", "gh", "hosts.yml") : void 0;
25402
25544
  }
25403
25545
  function parseGhHostsAccounts(yaml, host = "github.com") {
25404
25546
  let hostIndent = null;
@@ -25449,7 +25591,7 @@ function ghAccountCaveat(announcedLogin, accounts) {
25449
25591
 
25450
25592
  // src/doctor-io.ts
25451
25593
  var import_node_fs32 = require("node:fs");
25452
- var import_node_os9 = require("node:os");
25594
+ var import_node_os10 = require("node:os");
25453
25595
  var import_node_path30 = require("node:path");
25454
25596
  var import_node_child_process13 = require("node:child_process");
25455
25597
  var import_node_util8 = require("node:util");
@@ -25458,7 +25600,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
25458
25600
  function installedClaudePluginVersion() {
25459
25601
  try {
25460
25602
  const file = JSON.parse(
25461
- (0, import_node_fs32.readFileSync)((0, import_node_path30.join)((0, import_node_os9.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
25603
+ (0, import_node_fs32.readFileSync)((0, import_node_path30.join)((0, import_node_os10.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
25462
25604
  );
25463
25605
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
25464
25606
  if (versions.length === 0) return void 0;
@@ -25567,7 +25709,7 @@ function envHealLockPath(home) {
25567
25709
  async function withEnvHealLock(what, run) {
25568
25710
  try {
25569
25711
  return await withFileLock(
25570
- envHealLockPath((0, import_node_os10.homedir)()),
25712
+ envHealLockPath((0, import_node_os11.homedir)()),
25571
25713
  { staleMs: ENV_HEAL_LOCK_STALE_MS, maxWaitMs: ENV_HEAL_LOCK_MAX_WAIT_MS, label: "mmi env-heal lock" },
25572
25714
  run
25573
25715
  );
@@ -25662,9 +25804,9 @@ function mmiDoctorDeps(opts = {}) {
25662
25804
  },
25663
25805
  pluginCache: () => {
25664
25806
  const plan = buildPluginCachePlan(
25665
- (0, import_node_os10.homedir)(),
25807
+ (0, import_node_os11.homedir)(),
25666
25808
  runningPluginVersion(process.env, resolveClientVersion()),
25667
- pluginCacheFsDeps((0, import_node_os10.homedir)(), () => 0)
25809
+ pluginCacheFsDeps((0, import_node_os11.homedir)(), () => 0)
25668
25810
  );
25669
25811
  return {
25670
25812
  stale: plan.prune,
@@ -25713,7 +25855,7 @@ function mmiDoctorDeps(opts = {}) {
25713
25855
  // which branch it would pick one up from. Two local file reads, no network, fail-soft to no rows.
25714
25856
  marketplaceRows: () => {
25715
25857
  try {
25716
- const home = (0, import_node_os10.homedir)();
25858
+ const home = (0, import_node_os11.homedir)();
25717
25859
  return marketplaceRows(
25718
25860
  MMI_MARKETPLACE_NAME,
25719
25861
  readFileSyncSafe((0, import_node_path31.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs33.readFileSync),
@@ -25734,23 +25876,6 @@ async function requireFreshTrainCli(commandName) {
25734
25876
  if (report.ok) return;
25735
25877
  throw new Error(staleTrainCliMessage(report, commandName));
25736
25878
  }
25737
- var cachedLongFlags;
25738
- function allLongFlags() {
25739
- if (cachedLongFlags) return cachedLongFlags;
25740
- const acc = /* @__PURE__ */ new Set();
25741
- const walk2 = (node) => {
25742
- if (!isCanonicalSuggestion(node)) return;
25743
- for (const opt of node.options) {
25744
- if (opt.discovery) continue;
25745
- const m = /--[\w-]+/.exec(opt.flags);
25746
- if (m) acc.add(m[0]);
25747
- }
25748
- for (const child2 of node.subcommands) walk2(child2);
25749
- };
25750
- walk2(buildCommandManifest(program2).tree);
25751
- cachedLongFlags = [...acc];
25752
- return cachedLongFlags;
25753
- }
25754
25879
  var cachedCommandPaths;
25755
25880
  function allCommandPaths() {
25756
25881
  if (cachedCommandPaths) return cachedCommandPaths;
@@ -25817,6 +25942,22 @@ function commandPath2(cmd) {
25817
25942
  }
25818
25943
  return parts.join(" ");
25819
25944
  }
25945
+ function commandOwnLongFlags(cmd) {
25946
+ if (!cmd) return [];
25947
+ const flags = /* @__PURE__ */ new Set();
25948
+ for (const opt of cmd.options) {
25949
+ if (opt.long) flags.add(opt.long);
25950
+ }
25951
+ return [...flags];
25952
+ }
25953
+ var TARGET_SELECTOR_FLAGS = /* @__PURE__ */ new Set(["--repo", "--ref", "--target", "--project"]);
25954
+ function positionalTargetForm(cmd) {
25955
+ if (!cmd) return void 0;
25956
+ const args = cmd.registeredArguments ?? [];
25957
+ const first = args[0];
25958
+ if (!first) return void 0;
25959
+ return `mmi-cli ${commandPath2(cmd)} <${first.name()}>`;
25960
+ }
25820
25961
  function resolveParseHint() {
25821
25962
  if (lastParseErrorKind === "unknown-command") {
25822
25963
  const path3 = lastUnknownCommand ? suggestCommandPath(lastUnknownCommand, allCommandPaths()) : void 0;
@@ -25845,10 +25986,13 @@ function envelopeAwareWriteErr(str) {
25845
25986
  if (match) {
25846
25987
  const flag = match[1];
25847
25988
  if (argvWantsJson2()) {
25848
- const suggestion = didYouMean(flag, allLongFlags());
25989
+ const invoked = resolveCommandFromArgv(program2, process.argv.slice(2));
25990
+ const suggestion = didYouMean(flag, commandOwnLongFlags(invoked));
25849
25991
  const corrected = suggestion ? `mmi-cli ${process.argv.slice(2).map((a) => a === flag ? suggestion : a).join(" ")}` : void 0;
25992
+ const positional = !suggestion && TARGET_SELECTOR_FLAGS.has(flag) ? positionalTargetForm(invoked) : void 0;
25993
+ const message = positional ? `unknown option '${flag}' \u2014 this command takes its target as a positional: ${positional}` : `unknown option '${flag}'`;
25850
25994
  process.stderr.write(
25851
- formatErrorEnvelope(`unknown option '${flag}'`, {
25995
+ formatErrorEnvelope(message, {
25852
25996
  code: ERROR_CODES.ERR_UNKNOWN_FLAG,
25853
25997
  offending_flag: flag,
25854
25998
  ...suggestion ? { did_you_mean: suggestion } : {},
@@ -27304,14 +27448,19 @@ async function listCiWorkflowPaths(cwd = process.cwd()) {
27304
27448
  }
27305
27449
  }).map((name) => `.github/workflows/${name}`);
27306
27450
  }
27307
- async function resolveMergeCiPolicyForCheckout(repoOpt) {
27451
+ async function resolveMergeCiPolicyForCheckout(repoOpt, headRef) {
27308
27452
  const repo = repoOpt ?? await resolveRepo();
27309
27453
  if (repo) {
27310
- return resolveRepoMergeCiPolicy(repo, ciAuditDeps());
27454
+ return resolveRepoMergeCiPolicy(repo, ciAuditDeps(), headRef);
27311
27455
  }
27312
27456
  const workflowPaths = await listCiWorkflowPaths();
27313
27457
  return resolveMergeCiPolicy({ workflowPaths });
27314
27458
  }
27459
+ async function prHeadRefForCiProbe(prNumber, repo) {
27460
+ const snapshot = await fetchRestPrSnapshot(prNumber, repo).catch(() => null);
27461
+ if (!snapshot || snapshot.headIsFork || !snapshot.headRef) return void 0;
27462
+ return snapshot.headRef;
27463
+ }
27315
27464
  function ciAuditDeps() {
27316
27465
  const cfgPromise = loadConfig();
27317
27466
  const root = hubRoot();
@@ -27350,9 +27499,11 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
27350
27499
  timeoutMs = Math.round(minutes * 6e4);
27351
27500
  }
27352
27501
  const repo = await requireRepo(o.repo);
27353
- const baseBranch = await fetchRestPrSnapshot(number, repo).then((s) => s.baseRef).catch(() => "development");
27502
+ const snapshot = await fetchRestPrSnapshot(number, repo).catch(() => null);
27503
+ const baseBranch = snapshot?.baseRef ?? "development";
27504
+ const ciHeadRef = snapshot && !snapshot.headIsFork && snapshot.headRef ? snapshot.headRef : void 0;
27354
27505
  const result = await waitForPrChecks({
27355
- resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
27506
+ resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef),
27356
27507
  pollChecks: () => pollRestPrChecks(number, repo),
27357
27508
  pollMergeable: () => pollRestPrMergeable(number, repo),
27358
27509
  pollRateLimit: () => fetchRestCorePool(),
@@ -27497,12 +27648,13 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
27497
27648
  const beforeWorktrees = parseWorktreePorcelain(
27498
27649
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
27499
27650
  );
27500
- const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo);
27651
+ const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
27652
+ const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
27501
27653
  if (o.wait) {
27502
27654
  const repo = await requireRepo(o.repo);
27503
27655
  const baseBranch = await fetchRestPrSnapshot(number, repo).then((s) => s.baseRef).catch(() => "development");
27504
27656
  const wait = await waitForPrChecks({
27505
- resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
27657
+ resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef),
27506
27658
  pollChecks: () => pollRestPrChecks(number, repo),
27507
27659
  pollMergeable: () => pollRestPrMergeable(number, repo),
27508
27660
  pollRateLimit: () => fetchRestCorePool(),
@@ -27527,44 +27679,50 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
27527
27679
  let remoteDeleteAttempted = false;
27528
27680
  let upgradedToAuto = false;
27529
27681
  let remoteNotAttemptedReason = headIsProtected ? "protected-branch" : void 0;
27530
- await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: !headIsProtected }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
27531
- const message = String(e.message || "");
27532
- if (/already been merged/i.test(message)) {
27533
- remoteNotAttemptedReason = "pr-already-merged";
27534
- return;
27535
- }
27536
- const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
27537
- if (note) throw new Error(`gh pr merge ${number}: ${note}`);
27538
- if (o.auto && mergeAutoRejectedPrAlreadyClean(message)) {
27539
- await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: !headIsProtected }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
27540
- const m2 = String(e2.message || "");
27541
- if (/already been merged/i.test(m2)) {
27542
- remoteNotAttemptedReason = "pr-already-merged";
27543
- return;
27544
- }
27545
- const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
27546
- if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
27547
- if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
27548
- });
27549
- return;
27550
- }
27551
- if (!o.auto && basePolicyBlocksImmediateMerge(message)) {
27552
- console.warn(`pr merge: the base-branch policy blocks an immediate merge \u2014 upgrading to --auto (merges once required checks pass).`);
27553
- upgradedToAuto = true;
27554
- await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: true, deleteBranch: !headIsProtected }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
27555
- const m2 = String(e2.message || "");
27556
- if (/already been merged/i.test(m2)) {
27557
- remoteNotAttemptedReason = "pr-already-merged";
27558
- return;
27559
- }
27560
- const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
27561
- if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
27562
- if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
27563
- });
27564
- return;
27565
- }
27566
- if (!ghPrMergeLocalBranchDeleteWarning(message)) throw e;
27567
- });
27682
+ const overrideBody = await composeOverrideBodyFile(number, repoArgs, async (a, t) => (await execFileP2("gh", a, { timeout: t })).stdout);
27683
+ const bodyFile = overrideBody?.path;
27684
+ try {
27685
+ await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: !headIsProtected, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
27686
+ const message = String(e.message || "");
27687
+ if (/already been merged/i.test(message)) {
27688
+ remoteNotAttemptedReason = "pr-already-merged";
27689
+ return;
27690
+ }
27691
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
27692
+ if (note) throw new Error(`gh pr merge ${number}: ${note}`);
27693
+ if (o.auto && mergeAutoRejectedPrAlreadyClean(message)) {
27694
+ await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: !headIsProtected, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
27695
+ const m2 = String(e2.message || "");
27696
+ if (/already been merged/i.test(m2)) {
27697
+ remoteNotAttemptedReason = "pr-already-merged";
27698
+ return;
27699
+ }
27700
+ const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
27701
+ if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
27702
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
27703
+ });
27704
+ return;
27705
+ }
27706
+ if (!o.auto && basePolicyBlocksImmediateMerge(message)) {
27707
+ console.warn(`pr merge: the base-branch policy blocks an immediate merge \u2014 upgrading to --auto (merges once required checks pass).`);
27708
+ upgradedToAuto = true;
27709
+ await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: true, deleteBranch: !headIsProtected, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
27710
+ const m2 = String(e2.message || "");
27711
+ if (/already been merged/i.test(m2)) {
27712
+ remoteNotAttemptedReason = "pr-already-merged";
27713
+ return;
27714
+ }
27715
+ const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
27716
+ if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
27717
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
27718
+ });
27719
+ return;
27720
+ }
27721
+ if (!ghPrMergeLocalBranchDeleteWarning(message)) throw e;
27722
+ });
27723
+ } finally {
27724
+ overrideBody?.cleanup();
27725
+ }
27568
27726
  const stateRead = await readGhPrStateWithRetry(
27569
27727
  () => execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => r.stdout)
27570
27728
  );
@@ -27958,7 +28116,8 @@ access.command("audit").description("audit collaborator roles + train-branch pus
27958
28116
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
27959
28117
  const fileContracts = (0, import_node_fs33.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs33.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
27960
28118
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
27961
- const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
28119
+ const sanctioned = (0, import_node_fs33.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs33.readFileSync)("access-matrix.json", "utf8")) : {};
28120
+ const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
27962
28121
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
27963
28122
  if (!report.ok) process.exitCode = 1;
27964
28123
  });
@@ -28052,13 +28211,13 @@ function stagingApplyFsGuard(home) {
28052
28211
  }
28053
28212
  program2.command("plugin-prune").description(`prune stale cached MMI plugin versions (keeps running + newest, ${PLUGIN_CACHE_KEEP} total) and orphaned temp_git_* staging dirs; dry-run unless --apply (#2903, #2990)`).option("--apply", "actually delete the stale version dirs + orphaned staging dirs (default: report only)").option("--json", "machine-readable output").action((o) => {
28054
28213
  const plan = buildPluginCachePlan(
28055
- (0, import_node_os10.homedir)(),
28214
+ (0, import_node_os11.homedir)(),
28056
28215
  runningPluginVersion(process.env, resolveClientVersion()),
28057
- pluginCacheFsDeps((0, import_node_os10.homedir)(), directoryBytes),
28216
+ pluginCacheFsDeps((0, import_node_os11.homedir)(), directoryBytes),
28058
28217
  { withBytes: true }
28059
28218
  );
28060
28219
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
28061
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs33.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os10.homedir)())) : void 0;
28220
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs33.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os11.homedir)())) : void 0;
28062
28221
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
28063
28222
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
28064
28223
  else console.log(renderPluginCachePlan(plan, result));
@@ -28141,9 +28300,11 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
28141
28300
  DEFAULT_PRIORITY,
28142
28301
  awsCallerArn,
28143
28302
  classifyParseError,
28303
+ commandOwnLongFlags,
28144
28304
  envHealLockPath,
28145
28305
  gcPlan,
28146
28306
  isOrgRegisteredRepo,
28307
+ positionalTargetForm,
28147
28308
  registryClientDeps,
28148
28309
  repoSlug,
28149
28310
  shouldMarkWorktreeActivity,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.68.0",
3
+ "version": "3.70.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",