@mutmutco/cli 3.4.0 → 3.5.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 +57 -37
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -10904,7 +10904,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
10904
10904
  }
10905
10905
  async function runTenantRedeploy(deps, options) {
10906
10906
  const { stage: stage2 } = options;
10907
- const ref = options.ref ?? stage2;
10907
+ const ref = options.ref ?? (stage2 === "dev" ? "development" : stage2);
10908
10908
  const watch = options.watch ?? false;
10909
10909
  const repo = options.repo;
10910
10910
  const [owner, name] = repo.split("/");
@@ -12893,7 +12893,7 @@ function runtimeSecretStreamGap(stage2, meta, presentSecrets) {
12893
12893
  for (const name of stageRequiredSecrets(stage2, meta).map(materializedRuntimeSecretName)) {
12894
12894
  if (seen.has(name)) continue;
12895
12895
  seen.add(name);
12896
- if (!streamed.has(name) && presentSecrets.has(stageKey(stage2, name))) gap.push(name);
12896
+ if (!streamed.has(name) && (presentSecrets.has(stageKey(stage2, name)) || presentSecrets.has(name))) gap.push(name);
12897
12897
  }
12898
12898
  return gap;
12899
12899
  }
@@ -13137,9 +13137,11 @@ async function buildV2Doctor(repoOrSlug, deps) {
13137
13137
  return [stage2, { required, ok: required ? await deps.hasDeployState(slug, stage2) : true }];
13138
13138
  })));
13139
13139
  const secrets = Object.fromEntries(STAGES.map((stage2) => {
13140
- const required = stageInTrack(meta, stage2) ? stageRequiredSecrets(stage2, meta).map((key) => stageKey(stage2, key)) : [];
13141
- const present = required.filter((key) => presentSecrets.has(key));
13142
- const missing = required.filter((key) => !presentSecrets.has(key));
13140
+ const names = stageInTrack(meta, stage2) ? stageRequiredSecrets(stage2, meta) : [];
13141
+ const satisfied = (key) => presentSecrets.has(stageKey(stage2, key)) || !key.includes("/") && presentSecrets.has(key);
13142
+ const required = names.map((key) => stageKey(stage2, key));
13143
+ const present = names.filter(satisfied).map((key) => stageKey(stage2, key));
13144
+ const missing = names.filter((key) => !satisfied(key)).map((key) => stageKey(stage2, key));
13143
13145
  return [stage2, { required, present, missing }];
13144
13146
  }));
13145
13147
  const runtimeSecretStreamWarnings = Object.fromEntries(STAGES.map((stage2) => [
@@ -13268,7 +13270,6 @@ var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
13268
13270
  var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
13269
13271
  var ENV_PREFIXES = ["", "dev", "rc"];
13270
13272
  var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
13271
- var SSM_ENVS = ["dev", "rc", "main"];
13272
13273
  var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
13273
13274
  var uniq = (xs) => [...new Set(xs)];
13274
13275
  function defaultSubdomain2(slug) {
@@ -13299,7 +13300,7 @@ function expectedRedirectUris(cfg) {
13299
13300
  ]);
13300
13301
  }
13301
13302
  function oauthSsmKeys() {
13302
- return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
13303
+ return [...SSM_NAMES];
13303
13304
  }
13304
13305
  function parseOauthClientJson(input) {
13305
13306
  let parsed;
@@ -13373,11 +13374,20 @@ function parseSecretsCatalogVar(raw) {
13373
13374
  throw new Error("project set: secrets must be a map keyed by canonical KEY");
13374
13375
  }
13375
13376
  const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
13377
+ const overrideMapKey = /^([A-Z_][A-Z0-9_]*)@(dev|rc|main)$/;
13376
13378
  for (const [mapKey, value] of Object.entries(parsed)) {
13377
13379
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`project set: secrets["${mapKey}"] must be an object`);
13378
13380
  const e = value;
13379
- if (typeof e.key !== "string" || !SECRET_ENV_NAME_RE.test(e.key) || e.key !== mapKey) {
13380
- throw new Error(`project set: secrets["${mapKey}"].key must equal the map key and be an env-name (UPPER_SNAKE)`);
13381
+ if (typeof e.key !== "string" || !SECRET_ENV_NAME_RE.test(e.key)) {
13382
+ throw new Error(`project set: secrets["${mapKey}"].key must be an env-name (UPPER_SNAKE)`);
13383
+ }
13384
+ const override = overrideMapKey.exec(mapKey);
13385
+ if (override) {
13386
+ if (e.key !== override[1] || !Array.isArray(e.stages) || e.stages.length !== 1 || e.stages[0] !== override[2]) {
13387
+ throw new Error(`project set: secrets["${mapKey}"] is an override entry (#2528) \u2014 key must be ${override[1]} and stages exactly ["${override[2]}"]`);
13388
+ }
13389
+ } else if (e.key !== mapKey) {
13390
+ throw new Error(`project set: secrets["${mapKey}"].key must equal the map key (or use the KEY@<stage> override form, #2528)`);
13381
13391
  }
13382
13392
  if (!nonEmpty(e.purpose) || !nonEmpty(e.group) || !nonEmpty(e.owner)) throw new Error(`project set: secrets["${mapKey}"] needs non-empty purpose, group, owner`);
13383
13393
  if (e.provider !== void 0 && !nonEmpty(e.provider)) throw new Error(`project set: secrets["${mapKey}"].provider must be a non-empty string`);
@@ -13924,8 +13934,7 @@ function classifyTier(_slug, key) {
13924
13934
  return key.slice(0, slash) === PROJECT_TIER_SEGMENT ? "project" : "org";
13925
13935
  }
13926
13936
  function secretParamName(slug, key) {
13927
- const rel = key.includes("/") ? key : `${PROJECT_TIER_SEGMENT}/${key}`;
13928
- return `${SSM_ROOT}/${slug}/${rel}`;
13937
+ return `${SSM_ROOT}/${slug}/${key}`;
13929
13938
  }
13930
13939
  function formatSecretList(items) {
13931
13940
  if (!items.length) return "no secrets";
@@ -13944,10 +13953,10 @@ function vaultPointer(slug) {
13944
13953
  org: [`/mmi-future/{shared,cloudflare,mmi-hub,...}/* (org-infra, master-gated)`]
13945
13954
  },
13946
13955
  stages: ["dev", "rc", "main"],
13947
- // Google OAuth is one client per repo; creds live at every stage under the standard key names
13948
- // (local is port-agnostic and reuses the dev tier). See the oauth-everywhere convention.
13956
+ // Google OAuth is one client per repo = ONE stageless value pair at the slug root (#2244/#2528);
13957
+ // every stage (and local) reads the same pair. The staged {dev,rc,main}/GOOGLE_* shape is retired.
13949
13958
  wellKnown: {
13950
- googleOAuth: ["dev/GOOGLE_CLIENT_ID", "dev/GOOGLE_CLIENT_SECRET", "rc/GOOGLE_CLIENT_ID", "rc/GOOGLE_CLIENT_SECRET", "main/GOOGLE_CLIENT_ID", "main/GOOGLE_CLIENT_SECRET"]
13959
+ googleOAuth: ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"]
13951
13960
  }
13952
13961
  };
13953
13962
  }
@@ -14203,8 +14212,9 @@ function formatCatalog(r) {
14203
14212
  return lines.join("\n");
14204
14213
  }
14205
14214
  function formatDrift(d) {
14206
- if (d.ok) return "vault doctor: zero drift \u2014 SSM matches the catalog.";
14207
- const lines = ["vault doctor: drift found", ""];
14215
+ const shadows = d.stagedShadow ?? [];
14216
+ if (d.ok && !shadows.length) return "vault doctor: zero drift \u2014 SSM matches the catalog.";
14217
+ const lines = [d.ok ? "vault doctor: zero drift \u2014 SSM matches the catalog. Advisories:" : "vault doctor: drift found", ""];
14208
14218
  const section = (title, rows) => {
14209
14219
  if (!rows.length) return;
14210
14220
  lines.push(`${title} (${rows.length}):`, ...rows.map((r) => ` - ${r}`), "");
@@ -14213,6 +14223,10 @@ function formatDrift(d) {
14213
14223
  section("ORPHAN in SSM, undeclared \u2014 declare or delete", d.orphan.map((o) => o.path));
14214
14224
  section("OFF-SCHEME path", d.offScheme.map((o) => `${o.path} (${o.reason})`));
14215
14225
  section("STAGE-SPLIT \u2014 collapse to one shared key", (d.stageSplit ?? []).map((s) => `${s.slug}/${s.leaf} split across ${s.stages.join("/")}`));
14226
+ section(
14227
+ "STAGED SHADOW \u2014 the staged override WINS its stage over the stageless canonical (#2522)",
14228
+ shadows.map((s) => `${s.path} shadows ${s.canonicalPath}${s.declared ? "" : " (UNDECLARED residue \u2014 triage per #2526)"}`)
14229
+ );
14216
14230
  section("DUPLICATE org-infra secret", d.duplicate.map((dup) => `${dup.logical}: ${dup.paths.join(" , ")}`));
14217
14231
  return lines.join("\n").trimEnd();
14218
14232
  }
@@ -14317,10 +14331,11 @@ async function secretsPreflight(deps, opts) {
14317
14331
  }
14318
14332
  const { secrets } = await res.json();
14319
14333
  const present = new Set((secrets ?? []).map((s) => s.key));
14320
- const required = opts.required.map((key) => stageKey2(opts.stage, key));
14321
- const missing = required.filter((key) => !present.has(key));
14334
+ const missing = opts.required.filter(
14335
+ (key) => !present.has(stageKey2(opts.stage, key)) && !(key.includes("/") ? false : present.has(key))
14336
+ );
14322
14337
  if (missing.length) {
14323
- deps.log(`missing ${missing.join(", ")}`);
14338
+ deps.log(`missing ${missing.map((key) => key.includes("/") ? key : `${stageKey2(opts.stage, key)} (and no stageless ${key})`).join(", ")}`);
14324
14339
  return false;
14325
14340
  }
14326
14341
  deps.log(`all required ${opts.stage} secret names are present`);
@@ -14352,7 +14367,8 @@ async function secretsGet(deps, key, opts) {
14352
14367
  );
14353
14368
  return false;
14354
14369
  }
14355
- const { value } = await res.json();
14370
+ const { value, notes } = await res.json();
14371
+ for (const note of notes ?? []) deps.err(`note: ${note}`);
14356
14372
  deps.log(value ?? "");
14357
14373
  return true;
14358
14374
  }
@@ -14432,6 +14448,8 @@ async function putSecret(deps, key, value, opts) {
14432
14448
  );
14433
14449
  return false;
14434
14450
  }
14451
+ const { warnings } = await res.json();
14452
+ for (const warning of warnings ?? []) deps.err(`warning: ${warning}`);
14435
14453
  const provider = providerForSecretKey(key);
14436
14454
  if (provider) {
14437
14455
  const result = await verifySecretValue(deps, key, value, opts);
@@ -14827,7 +14845,7 @@ function registerSecretsCommands(program3) {
14827
14845
  const ok = await secretsVerify(d, key, o);
14828
14846
  if (!ok) process.exitCode = 1;
14829
14847
  }));
14830
- secrets.command("set <key>").description("write/rotate a secret; value is read from stdin (never an argument)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
14848
+ secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets(async (d) => {
14831
14849
  const ok = await secretsSet(d, key, o);
14832
14850
  if (!ok) process.exitCode = 1;
14833
14851
  }));
@@ -17144,6 +17162,14 @@ ${stderr}`);
17144
17162
  return false;
17145
17163
  }
17146
17164
  }
17165
+ function healBannerLine(bin, token, refSupported) {
17166
+ const installVerb = token === "codex" ? "add" : "install";
17167
+ return refSupported ? ` \u21BB reinstalling the MMI plugin via \`${bin} plugin\` (marketplace remove \u2192 add --ref main \u2192 ${installVerb})\u2026` : ` \u21BB reinstalling the MMI plugin via \`${bin} plugin\` (marketplace remove \u2192 add \u2192 ${installVerb}; release ref pinned by the marketplace manifest)\u2026`;
17168
+ }
17169
+ function refAbsenceNote(bin, refSupported) {
17170
+ return refSupported ? "" : `
17171
+ Note: \`${bin}\` has no \`--ref\` option; the marketplace manifest pins the released branch, so none is needed (#2516).`;
17172
+ }
17147
17173
  async function applyPluginHeal(token, surface, log, opts) {
17148
17174
  if (!opts?.force && surfaceToken(surface) !== token) return false;
17149
17175
  const descriptor = PLUGIN_SURFACE_HEAL[token];
@@ -17152,13 +17178,8 @@ async function applyPluginHeal(token, surface, log, opts) {
17152
17178
  const bin = descriptor.pluginRunner === "codex" ? "codex" : "claude";
17153
17179
  const runner = descriptor.pluginRunner === "codex" ? runCodexPlugin : runClaudePlugin;
17154
17180
  const refSupported = await marketplaceAddRefSupported(bin);
17155
- const { steps, strippedRef } = adaptHealStepsForRefSupport(tableSteps, refSupported);
17156
- log(
17157
- refSupported ? ` \u21BB reinstalling the MMI plugin via \`${bin} plugin\` (marketplace remove \u2192 add --ref main \u2192 ${token === "codex" ? "add" : "install"})\u2026` : ` \u21BB reinstalling the MMI plugin via \`${bin} plugin\` (marketplace remove \u2192 add \u2192 ${token === "codex" ? "add" : "install"})\u2026`
17158
- );
17159
- if (strippedRef) {
17160
- log(` \u26A0 \`${bin}\` has no \`--ref\` option \u2014 cloning the default branch; update \`${bin}\` to pin the released \`main\` branch (#2080)`);
17161
- }
17181
+ const { steps } = adaptHealStepsForRefSupport(tableSteps, refSupported);
17182
+ log(healBannerLine(bin, token, refSupported));
17162
17183
  for (const step of steps) {
17163
17184
  if (healStepAborts(step, await runner([...step.args]))) return false;
17164
17185
  }
@@ -18679,8 +18700,7 @@ async function runPluginHeal(surface = detectSurface(process.env)) {
18679
18700
  const bin = descriptor.pluginRunner === "codex" ? "codex" : "claude";
18680
18701
  const refSupported = await marketplaceAddRefSupported(bin);
18681
18702
  const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
18682
- const note = refSupported ? "" : `
18683
- Note: \`${bin}\` has no \`--ref\` option here; update \`${bin}\` to pin the released \`main\` branch.`;
18703
+ const note = refAbsenceNote(bin, refSupported);
18684
18704
  console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
18685
18705
  ${recovery}${note}`);
18686
18706
  }
@@ -19242,8 +19262,8 @@ tenant.command("readiness <owner/repo> <stage>").description("alias for tenant s
19242
19262
  console.log(JSON.stringify(result, null, 2));
19243
19263
  if (result.publicProbe?.ok === false) process.exitCode = 1;
19244
19264
  });
19245
- tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main \u2014 the promoted ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
19246
- if (stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be rc or main");
19265
+ tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main, or `development` for dev \u2014 the promoted/staging ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage2, o) => {
19266
+ if (stage2 !== "dev" && stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be dev, rc, or main");
19247
19267
  try {
19248
19268
  const result = await runTenantRedeploy(trainApplyDeps(), { repo, stage: stage2, ref: o.ref, watch: o.watch });
19249
19269
  return printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantRedeploy(result));
@@ -19704,7 +19724,7 @@ SSM cred params (under /mmi-future/${slug}/):`);
19704
19724
  ssm.forEach((k) => console.log(` ${k}`));
19705
19725
  console.log("\nProvision/repair the Console client per docs/Guides/oauth-provision.md; store creds with `mmi-cli oauth set-creds`.");
19706
19726
  });
19707
- oauth.command("set-creds").description('store the OAuth client into the canonical {dev,rc,main}/GOOGLE_CLIENT_* SSM keys (pipe the Console "Download JSON" on stdin)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
19727
+ oauth.command("set-creds").description('store the OAuth client into the canonical stageless GOOGLE_CLIENT_ID/SECRET pair (pipe the Console "Download JSON" on stdin)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
19708
19728
  const raw = await readStdin();
19709
19729
  if (!raw.trim()) {
19710
19730
  return fail("oauth set-creds: pipe the Google client JSON on stdin \u2014 e.g.\n mmi-cli oauth set-creds --repo <owner/repo> < client.json");
@@ -19723,10 +19743,10 @@ oauth.command("set-creds").description('store the OAuth client into the canonica
19723
19743
  return;
19724
19744
  }
19725
19745
  }
19726
- console.log(`OAuth client stored in all ${oauthSsmKeys().length} canonical keys. Run \`mmi-cli oauth verify\` to confirm the client is port-agnostic.`);
19746
+ console.log(`OAuth client stored in the ${oauthSsmKeys().length} canonical stageless keys. Run \`mmi-cli oauth verify\` to confirm the client is port-agnostic.`);
19727
19747
  });
19728
19748
  });
19729
- oauth.command("verify").description("probe Google authorize with an arbitrary port (:9123) to confirm the client is port-agnostic").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--client-id <id>", "OAuth client_id (else read dev/GOOGLE_CLIENT_ID from SSM)").option("--json", "machine-readable output").action(async (o) => {
19749
+ oauth.command("verify").description("probe Google authorize with an arbitrary port (:9123) to confirm the client is port-agnostic").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--client-id <id>", "OAuth client_id (else read the stageless GOOGLE_CLIENT_ID from SSM)").option("--json", "machine-readable output").action(async (o) => {
19730
19750
  const cfg = await loadConfig();
19731
19751
  const slug = (o.repo ? o.repo.split("/").pop() : cfg.project ?? await repoSlug()).toLowerCase();
19732
19752
  const meta = await fetchProjectBySlug(slug, registryClientDeps(cfg));
@@ -19739,11 +19759,11 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
19739
19759
  let clientId = o.clientId;
19740
19760
  if (!clientId) {
19741
19761
  await withSecrets(async (d) => {
19742
- clientId = await fetchSecretValue(d, "dev/GOOGLE_CLIENT_ID", { repo: o.repo }) ?? void 0;
19762
+ clientId = await fetchSecretValue(d, "GOOGLE_CLIENT_ID", { repo: o.repo }) ?? void 0;
19743
19763
  });
19744
19764
  }
19745
19765
  if (!clientId) {
19746
- return failGraceful("oauth verify: no client_id (pass --client-id, or provision the repo so dev/GOOGLE_CLIENT_ID exists)");
19766
+ return failGraceful("oauth verify: no client_id (pass --client-id, or provision the repo so GOOGLE_CLIENT_ID exists)");
19747
19767
  }
19748
19768
  const redirectUri = probeRedirectUri(oc.callbackPath);
19749
19769
  let body = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.4.0",
3
+ "version": "3.5.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",