@mutmutco/cli 3.48.1 → 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 +100 -28
  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();
@@ -16872,7 +16900,7 @@ async function withSecrets(run) {
16872
16900
  function registerSecretsCommands(program3) {
16873
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");
16874
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)));
16875
- 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)));
16876
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) => {
16877
16905
  if (!await secretsFind(d, intent, o)) process.exitCode = 1;
16878
16906
  }));
@@ -16995,7 +17023,7 @@ function registerSecretsCommands(program3) {
16995
17023
  });
16996
17024
  if (!ok) process.exitCode = 1;
16997
17025
  }));
16998
- 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) => {
16999
17027
  const ok = await secretsVerify(d, key, o);
17000
17028
  if (!ok) process.exitCode = 1;
17001
17029
  }));
@@ -17045,7 +17073,7 @@ function registerSecretsCommands(program3) {
17045
17073
  if (!ok) process.exitCode = 1;
17046
17074
  }));
17047
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)));
17048
- 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) => {
17049
17077
  const ok = await secretsUse(d, key, { ...o, command });
17050
17078
  if (ok === false) process.exitCode = 1;
17051
17079
  }));
@@ -20748,14 +20776,30 @@ function parseRestPrSnapshot(json) {
20748
20776
  const pr2 = json ?? {};
20749
20777
  const headSha = pr2.head?.sha ?? "";
20750
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;
20751
20781
  return {
20752
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),
20753
20787
  mergeable: pr2.mergeable === true ? "MERGEABLE" : pr2.mergeable === false ? "CONFLICTING" : "UNKNOWN",
20754
20788
  baseRef: pr2.base?.ref ?? "development",
20755
20789
  merged: pr2.merged === true,
20756
20790
  state: pr2.state ?? ""
20757
20791
  };
20758
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
+ }
20759
20803
  async function fetchRestPrSnapshot(prNumber, repo, gh = defaultGhApi) {
20760
20804
  return parseRestPrSnapshot(JSON.parse(await gh([`repos/${repo}/pulls/${prNumber}`])));
20761
20805
  }
@@ -20790,36 +20834,54 @@ async function pollRestPrMerged(prNumber, repo, gh = defaultGhApi) {
20790
20834
  }
20791
20835
  }
20792
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
+ ]);
20793
20844
  function isErrorAnnotation(a) {
20794
20845
  const level = a.annotation_level?.toLowerCase();
20795
20846
  return level === "failure" || level === "error";
20796
20847
  }
20797
20848
  function classifyFailedChecks(failing) {
20798
- const budgetKilled = [];
20849
+ const infraFailures = [];
20799
20850
  const otherFailures = [];
20800
20851
  for (const check of failing) {
20801
20852
  const errors = check.annotations.filter(isErrorAnnotation);
20802
- const allBudget = errors.length > 0 && errors.every((a) => a.title?.trim() === CI_BUDGET_ANNOTATION_TITLE);
20803
- (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);
20804
20855
  }
20805
- if (budgetKilled.length && !otherFailures.length) {
20856
+ if (infraFailures.length && !otherFailures.length) {
20806
20857
  return {
20807
- cause: "ci-budget-kill",
20808
- budgetKilled,
20858
+ cause: "runner-infra",
20859
+ infraFailures,
20809
20860
  otherFailures,
20810
- 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.`
20811
20862
  };
20812
20863
  }
20813
20864
  return {
20814
20865
  cause: "checks-failure",
20815
- budgetKilled,
20866
+ infraFailures,
20816
20867
  otherFailures,
20817
- 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"}.`
20818
20869
  };
20819
20870
  }
20820
20871
  async function diagnoseFailedRestChecks(prNumber, repo, gh = defaultGhApi) {
20821
20872
  try {
20822
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
+ }
20823
20885
  const failing = (await fetchHeadCheckRuns(snapshot.headSha, repo, gh)).filter((run) => classifyCheckRun(run) === "fail" && typeof run.id === "number");
20824
20886
  if (!failing.length) return null;
20825
20887
  const annotated = await Promise.all(failing.map(async (run) => ({
@@ -24548,7 +24610,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
24548
24610
  const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
24549
24611
  console.log(JSON.stringify(payload));
24550
24612
  });
24551
- 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) => {
24552
24614
  const cfg = await loadConfig();
24553
24615
  let target;
24554
24616
  try {
@@ -24735,7 +24797,15 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
24735
24797
  try {
24736
24798
  oc = parseOauthConfig(meta ?? {}, slug);
24737
24799
  } catch (e) {
24738
- 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}`);
24739
24809
  }
24740
24810
  const origins = expectedJsOrigins(oc);
24741
24811
  const redirects = expectedRedirectUris(oc);
@@ -25292,8 +25362,10 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
25292
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>.`);
25293
25363
  } else if (result.status === "rate-limited") {
25294
25364
  printLine(`pr checks-wait: rate-limited \u2014 ${result.reason ?? "REST pool below floor"}. No check failed; re-run after the pool resets.`);
25295
- } else if (result.detail === "ci-budget-kill") {
25296
- 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}`);
25297
25369
  } else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
25298
25370
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
25299
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.1",
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",