@mutmutco/cli 3.48.0 → 3.49.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 +124 -29
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -6754,11 +6754,17 @@ async function secretsVerify(deps, key, opts) {
6754
6754
  deps.err(`invalid secret key ${JSON.stringify(key)}`);
6755
6755
  return false;
6756
6756
  }
6757
- if (!providerForSecretKey(key)) {
6758
- deps.err(`no provider verifier configured for ${secretLeafName(key)}`);
6759
- return false;
6760
- }
6757
+ const hasProviderVerifier = Boolean(providerForSecretKey(key));
6761
6758
  const value = await fetchSecretValue(deps, key, opts);
6759
+ if (!hasProviderVerifier) {
6760
+ if (!value) {
6761
+ deps.err(`${key}: ABSENT or unreachable at this coordinate \u2014 no value could be read`);
6762
+ deps.err(`Locate a key across every vault you can reach: mmi-cli org access capabilities${opts.repo ? ` --repo ${opts.repo}` : ""}`);
6763
+ return false;
6764
+ }
6765
+ deps.log(`${key}: PRESENT (${value.length} chars) \u2014 reachability only; no provider verifier is configured for ${secretLeafName(key)}, so the credential itself was not validated`);
6766
+ return true;
6767
+ }
6762
6768
  if (!value) {
6763
6769
  deps.err(`secrets verify: could not read ${key}; no value was printed`);
6764
6770
  return false;
@@ -6903,6 +6909,11 @@ async function secretsDeclare(deps, key, opts) {
6903
6909
  const body = await readJsonBody(res);
6904
6910
  if (!res.ok) {
6905
6911
  deps.err(await upgradeMessage(res, body) ?? `secrets declare failed: HTTP ${res.status}${errorDetail(body)}`);
6912
+ if (res.status === 404 && !errorDetail(body)) {
6913
+ deps.err(
6914
+ `The Hub did not recognize the declare route (no error body \u2014 an API-gateway 404, not an authority refusal). Fall back to the master-side catalog write: mmi-cli org project set ${repo} --secrets-file <catalog.json> (shape: keyed by env name; each entry {key,purpose,group,owner,stages[],consumers[]} \u2014 see docs/Guides/oauth-provision.md \xA7 Cred catalog shape).`
6915
+ );
6916
+ }
6906
6917
  return false;
6907
6918
  }
6908
6919
  if (opts.json) {
@@ -7115,7 +7126,12 @@ async function secretsUse(deps, key, opts) {
7115
7126
  " \u2022 Runtime: the central deploy injects the declared stage env; broker consumers use the workload's scoped runtime token. Never bake a value into an image or commit it.",
7116
7127
  ` \u2022 CI (GitHub Actions): the workflow assumes its OIDC role and runs \`aws ssm get-parameter --with-decryption --name ${path2}\` \u2014 no GitHub secret.`,
7117
7128
  " \u2022 Own project: the stageless canonical and any declared dev/rc/main overrides are project-admin self-service.",
7118
- " \u2022 Org-infra/cross-slug keys remain master-gated unless exactly granted."
7129
+ " \u2022 Org-infra/cross-slug keys remain master-gated unless exactly granted.",
7130
+ "",
7131
+ "Answer it without consuming it (#3436):",
7132
+ " \u2022 WHERE does a key live, across every vault you can reach: `mmi-cli org access capabilities` (names + tier + scope, never values). `secrets list` is this repo only and cannot see _org.",
7133
+ ` \u2022 DOES it exist here: \`mmi-cli secrets verify ${key}${opts.slug ? ` --slug ${opts.slug}` : ""}\` \u2014 PRESENT/ABSENT even with no provider verifier configured.`,
7134
+ " \u2022 PowerShell note: `mmi-cli` resolves to npm's mmi-cli.ps1, whose binder eats the `--` separator. The wrapped command works either way; if a flag still misbinds, call `mmi-cli.cmd` explicitly."
7119
7135
  ].join("\n")
7120
7136
  );
7121
7137
  return;
@@ -9179,8 +9195,11 @@ async function waitForPrChecks(deps) {
9179
9195
  failureStreak += 1;
9180
9196
  if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
9181
9197
  const diagnosis = deps.diagnoseFailure ? await deps.diagnoseFailure().catch(() => null) : null;
9182
- if (diagnosis?.cause === "ci-budget-kill") {
9183
- return { policy, status: "failure", reason: diagnosis.reason, detail: "ci-budget-kill" };
9198
+ if (diagnosis?.cause === "stale-head") {
9199
+ return { policy, status: "failure", reason: diagnosis.reason, detail: "stale-head" };
9200
+ }
9201
+ if (diagnosis?.cause === "runner-infra") {
9202
+ return { policy, status: "failure", reason: diagnosis.reason, detail: "runner-infra" };
9184
9203
  }
9185
9204
  return { policy, status: "failure", reason, detail: "checks-failure" };
9186
9205
  }
@@ -11808,27 +11827,36 @@ function setCommandMetadata(command, metadata) {
11808
11827
  function commandMetadata(command) {
11809
11828
  return command[COMMAND_METADATA];
11810
11829
  }
11811
- function classifyTree(command, path2, inherited) {
11812
- const metadata = PATH_OVERRIDES[path2] ?? inherited;
11830
+ var REQUIRED_OPTION_HELP = {
11831
+ optionDescription(option) {
11832
+ const described = Help.prototype.optionDescription.call(this, option);
11833
+ return option.mandatory ? `(required) ${described}` : described;
11834
+ }
11835
+ };
11836
+ function classifyTree(command, path2, inherited, hideFromParent) {
11837
+ const override = PATH_OVERRIDES[path2];
11838
+ const metadata = override ?? inherited;
11813
11839
  setCommandMetadata(command, metadata);
11814
- if (metadata.discovery !== "primary") {
11840
+ if ((override !== void 0 || hideFromParent) && metadata.discovery !== "primary") {
11815
11841
  command._hidden = true;
11816
11842
  }
11817
11843
  for (const option of command.options) {
11818
11844
  if ((HIDDEN_OPTIONS[path2] ?? []).includes(option.long ?? option.short ?? "")) option.hideHelp();
11819
11845
  }
11846
+ command.configureHelp(REQUIRED_OPTION_HELP);
11820
11847
  for (const child2 of command.commands) {
11821
11848
  const childPath = path2 ? `${path2} ${child2.name()}` : child2.name();
11822
- classifyTree(child2, childPath, PATH_OVERRIDES[childPath] ?? metadata);
11849
+ classifyTree(child2, childPath, metadata, false);
11823
11850
  }
11824
11851
  }
11825
11852
  function applyCommandTaxonomy(program3) {
11826
11853
  for (const command of program3.commands) {
11827
11854
  const metadata = topLevelMetadata(command.name());
11828
11855
  command.helpGroup(metadata.help_group);
11829
- classifyTree(command, command.name(), metadata);
11856
+ classifyTree(command, command.name(), metadata, true);
11830
11857
  }
11831
11858
  program3.configureHelp({
11859
+ ...REQUIRED_OPTION_HELP,
11832
11860
  visibleCommands(command) {
11833
11861
  const visible = command.commands.filter((child2) => !child2._hidden);
11834
11862
  const helpCommand = command._getHelpCommand();
@@ -14506,6 +14534,24 @@ async function deriveHotfixVersion(deps) {
14506
14534
  function hotfixBranch(tag) {
14507
14535
  return `hotfix/${tag}`;
14508
14536
  }
14537
+ async function restoreAfterFailedPort(deps, opts) {
14538
+ const { startBranch, branch, created } = opts;
14539
+ if (!startBranch || startBranch === "HEAD" || startBranch === branch) {
14540
+ return `Local state left as-is (could not determine the branch to return to).`;
14541
+ }
14542
+ try {
14543
+ await deps.run("git", ["checkout", startBranch]);
14544
+ } catch {
14545
+ return `Could NOT return to ${startBranch} \u2014 the checkout is still on ${branch} (origin/main content); switch back before editing anything.`;
14546
+ }
14547
+ if (!created) return `Returned to ${startBranch}; the pre-existing local ${branch} was left in place.`;
14548
+ try {
14549
+ await deps.run("git", ["branch", "-D", branch]);
14550
+ return `Returned to ${startBranch} and removed the local ${branch}.`;
14551
+ } catch {
14552
+ return `Returned to ${startBranch}; the local ${branch} could not be removed \u2014 delete it by hand.`;
14553
+ }
14554
+ }
14509
14555
  async function resolveHotfixDeployModel(deps, ctx) {
14510
14556
  const load = await loadProjectMeta(deps, ctx);
14511
14557
  const meta = requireProjectMetaForTrain(load, ctx.repo);
@@ -14604,12 +14650,17 @@ async function runHotfixStart(deps, options) {
14604
14650
  await deps.run("git", ["pull", "--ff-only", "origin", branch]);
14605
14651
  notes.push(`branch ${branch} already on origin \u2014 reused (cherry-pick/bump assumed present; PR step resumes)`);
14606
14652
  } else {
14653
+ const startBranch = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
14654
+ const preexistingLocal = clean3(await deps.run("git", ["branch", "--list", branch]));
14607
14655
  await deps.run("git", ["checkout", "-B", branch, "origin/main"]);
14608
14656
  try {
14609
14657
  await deps.run("git", ["cherry-pick", "-x", sha]);
14610
14658
  } catch (e) {
14611
14659
  await deps.run("git", ["cherry-pick", "--abort"]);
14612
- throw new Error(`cherry-pick of ${label} onto ${branch} conflicted \u2014 aborted; resolve by hand on a manual hotfix branch, keeping the -x trailer (${e.message ?? e})`);
14660
+ const recovery = await restoreAfterFailedPort(deps, { startBranch, branch, created: !preexistingLocal });
14661
+ throw new Error(
14662
+ `cherry-pick of ${label} onto ${branch} conflicted \u2014 aborted; nothing was pushed. Land the conflict resolution on development through a normal PR, then rerun \`mmi-cli hotfix start --from <that PR's merge sha>\` \u2014 never hand-resolve the port onto main. ${recovery} (${e.message ?? e})`
14663
+ );
14613
14664
  }
14614
14665
  notes.push(`cherry-picked ${label} onto ${branch} (from origin/main, -x trailer recorded)`);
14615
14666
  if (deployModel === "hub-serverless") {
@@ -16849,7 +16900,7 @@ async function withSecrets(run) {
16849
16900
  function registerSecretsCommands(program3) {
16850
16901
  const secrets = program3.command("secrets").description("project vault \u2014 project-admins self-serve their own repo's full tree (stageless + dev/rc/main); org-infra namespaces are master-gated");
16851
16902
  secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsWhere(d, o)));
16852
- secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
16903
+ secrets.command("list").description("list secret NAMES + tier for THIS repo (never values). To locate a key you cannot place \u2014 including org-infra (_org) \u2014 use the cross-slug view: `mmi-cli org access capabilities` (#3436)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
16853
16904
  secrets.command("find <intent>").description("resolve a plain-language intent to canonical secret names + the exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
16854
16905
  if (!await secretsFind(d, intent, o)) process.exitCode = 1;
16855
16906
  }));
@@ -16972,7 +17023,7 @@ function registerSecretsCommands(program3) {
16972
17023
  });
16973
17024
  if (!ok) process.exitCode = 1;
16974
17025
  }));
16975
- secrets.command("verify <key>").description("validate a known provider secret without printing its value").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
17026
+ secrets.command("verify <key>").description("validate a known provider secret without printing its value; a key with no configured provider verifier falls back to a value-free PRESENT/ABSENT reachability check (#3436)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").action((key, o) => withSecrets(async (d) => {
16976
17027
  const ok = await secretsVerify(d, key, o);
16977
17028
  if (!ok) process.exitCode = 1;
16978
17029
  }));
@@ -17022,7 +17073,7 @@ function registerSecretsCommands(program3) {
17022
17073
  if (!ok) process.exitCode = 1;
17023
17074
  }));
17024
17075
  secrets.command("rm <key>").description("remove a secret from your own full project vault; org-infra requires master or an exact grant. --slug _org removes an org-infra <provider>/<KEY> value (#3315)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for the value removal \u2014 master or an exact grant (#3315)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
17025
- secrets.command("use <key> [command...]").description("consume a secret KEYLESS: own-project full tree or an exactly granted org-infra key; injects into command env and never prints it").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
17076
+ secrets.command("use <key> [command...]").description("consume a secret KEYLESS: own-project full tree or an exactly granted org-infra key; injects into command env and never prints it. The wrapped command keeps its OWN flags (`-- node -e \u2026`, `-- curl -H \u2026`) with or without the `--`, because npm's PowerShell shim swallows `--` before the CLI sees it (#3436)").allowUnknownOption().option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
17026
17077
  const ok = await secretsUse(d, key, { ...o, command });
17027
17078
  if (ok === false) process.exitCode = 1;
17028
17079
  }));
@@ -20725,14 +20776,30 @@ function parseRestPrSnapshot(json) {
20725
20776
  const pr2 = json ?? {};
20726
20777
  const headSha = pr2.head?.sha ?? "";
20727
20778
  if (!headSha) throw new Error("pulls endpoint returned no head SHA");
20779
+ const headRepo = pr2.head?.repo?.full_name;
20780
+ const baseRepo = pr2.base?.repo?.full_name;
20728
20781
  return {
20729
20782
  headSha,
20783
+ headRef: pr2.head?.ref ?? "",
20784
+ // Only call it a fork when BOTH names are known and differ — an absent name (private/deleted fork)
20785
+ // must not read as same-repo and send a git-ref probe at a branch the base repo does not have.
20786
+ headIsFork: Boolean(headRepo && baseRepo && headRepo !== baseRepo),
20730
20787
  mergeable: pr2.mergeable === true ? "MERGEABLE" : pr2.mergeable === false ? "CONFLICTING" : "UNKNOWN",
20731
20788
  baseRef: pr2.base?.ref ?? "development",
20732
20789
  merged: pr2.merged === true,
20733
20790
  state: pr2.state ?? ""
20734
20791
  };
20735
20792
  }
20793
+ async function fetchBranchTipSha(repo, ref, gh = defaultGhApi) {
20794
+ if (!ref) return null;
20795
+ try {
20796
+ const parsed = JSON.parse(await gh([`repos/${repo}/git/ref/heads/${ref}`]));
20797
+ const sha = parsed.object?.sha;
20798
+ return typeof sha === "string" && sha ? sha : null;
20799
+ } catch {
20800
+ return null;
20801
+ }
20802
+ }
20736
20803
  async function fetchRestPrSnapshot(prNumber, repo, gh = defaultGhApi) {
20737
20804
  return parseRestPrSnapshot(JSON.parse(await gh([`repos/${repo}/pulls/${prNumber}`])));
20738
20805
  }
@@ -20767,36 +20834,54 @@ async function pollRestPrMerged(prNumber, repo, gh = defaultGhApi) {
20767
20834
  }
20768
20835
  }
20769
20836
  var CI_BUDGET_ANNOTATION_TITLE = "CI budget exceeded";
20837
+ var CI_DISK_ANNOTATION_TITLE = "CI runner disk";
20838
+ var CI_TOOLCHAIN_ANNOTATION_TITLE = "CI runner toolchain";
20839
+ var RUNNER_INFRA_ANNOTATION_TITLES = /* @__PURE__ */ new Set([
20840
+ CI_BUDGET_ANNOTATION_TITLE,
20841
+ CI_DISK_ANNOTATION_TITLE,
20842
+ CI_TOOLCHAIN_ANNOTATION_TITLE
20843
+ ]);
20770
20844
  function isErrorAnnotation(a) {
20771
20845
  const level = a.annotation_level?.toLowerCase();
20772
20846
  return level === "failure" || level === "error";
20773
20847
  }
20774
20848
  function classifyFailedChecks(failing) {
20775
- const budgetKilled = [];
20849
+ const infraFailures = [];
20776
20850
  const otherFailures = [];
20777
20851
  for (const check of failing) {
20778
20852
  const errors = check.annotations.filter(isErrorAnnotation);
20779
- const allBudget = errors.length > 0 && errors.every((a) => a.title?.trim() === CI_BUDGET_ANNOTATION_TITLE);
20780
- (allBudget ? budgetKilled : otherFailures).push(check.name);
20853
+ const allInfra = errors.length > 0 && errors.every((a) => RUNNER_INFRA_ANNOTATION_TITLES.has(a.title?.trim() ?? ""));
20854
+ (allInfra ? infraFailures : otherFailures).push(check.name);
20781
20855
  }
20782
- if (budgetKilled.length && !otherFailures.length) {
20856
+ if (infraFailures.length && !otherFailures.length) {
20783
20857
  return {
20784
- cause: "ci-budget-kill",
20785
- budgetKilled,
20858
+ cause: "runner-infra",
20859
+ infraFailures,
20786
20860
  otherFailures,
20787
- reason: `${budgetKilled.length} check(s) were KILLED for wall-clock, not failed: ${budgetKilled.join(", ")}. No test failed \u2014 this is runner contention (concurrent gates sharing one runner). Rerun once the other gates drain (gh run rerun --failed); do not debug the diff.`
20861
+ reason: `${infraFailures.length} check(s) failed on RUNNER INFRASTRUCTURE, not your diff: ${infraFailures.join(", ")}. No test failed \u2014 the shared runner hit a wall-clock budget kill, ran out of disk, or was missing a toolchain (the check annotation names which). Re-run once the runner drains/heals (gh run rerun --failed); do not debug the diff.`
20788
20862
  };
20789
20863
  }
20790
20864
  return {
20791
20865
  cause: "checks-failure",
20792
- budgetKilled,
20866
+ infraFailures,
20793
20867
  otherFailures,
20794
- reason: budgetKilled.length ? `checks failed: ${otherFailures.join(", ")}; separately, ${budgetKilled.join(", ")} was killed for wall-clock, not failed.` : `checks failed: ${otherFailures.join(", ") || "unknown"}.`
20868
+ reason: infraFailures.length ? `checks failed: ${otherFailures.join(", ")}; separately, ${infraFailures.join(", ")} failed on runner infrastructure, not a test.` : `checks failed: ${otherFailures.join(", ") || "unknown"}.`
20795
20869
  };
20796
20870
  }
20797
20871
  async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
20798
20872
  try {
20799
20873
  const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh);
20874
+ if (!snapshot.headIsFork) {
20875
+ const branchTip = await fetchBranchTipSha(repo, snapshot.headRef, gh);
20876
+ if (branchTip && branchTip !== snapshot.headSha) {
20877
+ return {
20878
+ cause: "stale-head",
20879
+ infraFailures: [],
20880
+ otherFailures: [],
20881
+ reason: `the PR head (${snapshot.headSha.slice(0, 7)}) is BEHIND the branch tip (${branchTip.slice(0, 7)}) \u2014 GitHub likely dropped a \`synchronize\` event, so the reported red is a superseded commit and NO run exists for your current push. This is not your diff failing. Re-push (an empty commit is enough) or close and reopen the PR to re-sync the head and trigger a fresh run.`
20882
+ };
20883
+ }
20884
+ }
20800
20885
  const failing = (await fetchHeadCheckRuns(snapshot.headSha, repo, gh)).filter((run) => classifyCheckRun(run) === "fail" && typeof run.id === "number");
20801
20886
  if (!failing.length) return null;
20802
20887
  const annotated = await Promise.all(failing.map(async (run) => ({
@@ -24525,7 +24610,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
24525
24610
  const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
24526
24611
  console.log(JSON.stringify(payload));
24527
24612
  });
24528
- project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", "read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
24613
+ project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|\u2026], "provider":"<optional>"}').option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
24529
24614
  const cfg = await loadConfig();
24530
24615
  let target;
24531
24616
  try {
@@ -24712,7 +24797,15 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
24712
24797
  try {
24713
24798
  oc = parseOauthConfig(meta ?? {}, slug);
24714
24799
  } catch (e) {
24715
- return failGraceful(`org oauth plan: ${e.message}`);
24800
+ const message = e.message;
24801
+ if (/^oauth is not configured for /.test(message)) {
24802
+ return failGraceful(
24803
+ `org oauth plan: ${message}. Declare it in the registry META first:
24804
+ mmi-cli org project set ${o.repo ?? `mutmutco/${slug}`} --var 'oauth={"subdomains":["${defaultSubdomain(slug)}"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}'
24805
+ New repo? The GCP project and its Google Auth Platform consent screen must exist too \u2014 docs/Guides/oauth-provision.md \xA7 New repo.`
24806
+ );
24807
+ }
24808
+ return failGraceful(`org oauth plan: ${message}`);
24716
24809
  }
24717
24810
  const origins = expectedJsOrigins(oc);
24718
24811
  const redirects = expectedRedirectUris(oc);
@@ -25269,8 +25362,10 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
25269
25362
  printLine(`pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.`);
25270
25363
  } else if (result.status === "rate-limited") {
25271
25364
  printLine(`pr checks-wait: rate-limited \u2014 ${result.reason ?? "REST pool below floor"}. No check failed; re-run after the pool resets.`);
25272
- } else if (result.detail === "ci-budget-kill") {
25273
- printLine(`pr checks-wait: failure (wall-clock budget kill, NOT a test failure) \u2014 ${result.reason}`);
25365
+ } else if (result.detail === "runner-infra") {
25366
+ printLine(`pr checks-wait: failure (runner infrastructure, NOT a test failure) \u2014 ${result.reason}`);
25367
+ } else if (result.detail === "stale-head") {
25368
+ printLine(`pr checks-wait: failure (stale PR head, NOT a test failure) \u2014 ${result.reason}`);
25274
25369
  } else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
25275
25370
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
25276
25371
  if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.48.0",
3
+ "version": "3.49.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",