@mutmutco/cli 3.4.0 → 3.6.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 +67 -45
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -4529,9 +4529,16 @@ function buildSessionStartPlan(verbs) {
4529
4529
  ]
4530
4530
  };
4531
4531
  }
4532
- function spawnDetachedSelf(args, deps) {
4532
+ var WIN32_TRAMPOLINE = "const{spawn}=require('node:child_process');const a=JSON.parse(process.argv[1]);const c=spawn(a.cmd,a.args,{stdio:'ignore',windowsHide:true,cwd:a.cwd});c.on('exit',x=>process.exit(x??0));c.on('error',()=>process.exit(1));";
4533
+ function spawnDetachedSelf(args, deps, opts = {}) {
4533
4534
  try {
4534
- deps.spawn(deps.execPath, [deps.scriptPath, ...args], { detached: true, stdio: "ignore", windowsHide: true }).unref();
4535
+ const platform2 = deps.platform ?? process.platform;
4536
+ if (platform2 === "win32") {
4537
+ const payload = JSON.stringify({ cmd: deps.execPath, args: [deps.scriptPath, ...args], cwd: opts.cwd });
4538
+ deps.spawn(deps.execPath, ["-e", WIN32_TRAMPOLINE, payload], { detached: true, stdio: "ignore", windowsHide: true }).unref();
4539
+ return;
4540
+ }
4541
+ deps.spawn(deps.execPath, [deps.scriptPath, ...args], { detached: true, stdio: "ignore", windowsHide: true, ...opts.cwd ? { cwd: opts.cwd } : {} }).unref();
4535
4542
  } catch {
4536
4543
  }
4537
4544
  }
@@ -10904,7 +10911,7 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
10904
10911
  }
10905
10912
  async function runTenantRedeploy(deps, options) {
10906
10913
  const { stage: stage2 } = options;
10907
- const ref = options.ref ?? stage2;
10914
+ const ref = options.ref ?? (stage2 === "dev" ? "development" : stage2);
10908
10915
  const watch = options.watch ?? false;
10909
10916
  const repo = options.repo;
10910
10917
  const [owner, name] = repo.split("/");
@@ -12893,7 +12900,7 @@ function runtimeSecretStreamGap(stage2, meta, presentSecrets) {
12893
12900
  for (const name of stageRequiredSecrets(stage2, meta).map(materializedRuntimeSecretName)) {
12894
12901
  if (seen.has(name)) continue;
12895
12902
  seen.add(name);
12896
- if (!streamed.has(name) && presentSecrets.has(stageKey(stage2, name))) gap.push(name);
12903
+ if (!streamed.has(name) && (presentSecrets.has(stageKey(stage2, name)) || presentSecrets.has(name))) gap.push(name);
12897
12904
  }
12898
12905
  return gap;
12899
12906
  }
@@ -13137,9 +13144,11 @@ async function buildV2Doctor(repoOrSlug, deps) {
13137
13144
  return [stage2, { required, ok: required ? await deps.hasDeployState(slug, stage2) : true }];
13138
13145
  })));
13139
13146
  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));
13147
+ const names = stageInTrack(meta, stage2) ? stageRequiredSecrets(stage2, meta) : [];
13148
+ const satisfied = (key) => presentSecrets.has(stageKey(stage2, key)) || !key.includes("/") && presentSecrets.has(key);
13149
+ const required = names.map((key) => stageKey(stage2, key));
13150
+ const present = names.filter(satisfied).map((key) => stageKey(stage2, key));
13151
+ const missing = names.filter((key) => !satisfied(key)).map((key) => stageKey(stage2, key));
13143
13152
  return [stage2, { required, present, missing }];
13144
13153
  }));
13145
13154
  const runtimeSecretStreamWarnings = Object.fromEntries(STAGES.map((stage2) => [
@@ -13268,7 +13277,6 @@ var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
13268
13277
  var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
13269
13278
  var ENV_PREFIXES = ["", "dev", "rc"];
13270
13279
  var LOOPBACK = ["http://localhost", "http://127.0.0.1"];
13271
- var SSM_ENVS = ["dev", "rc", "main"];
13272
13280
  var SSM_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
13273
13281
  var uniq = (xs) => [...new Set(xs)];
13274
13282
  function defaultSubdomain2(slug) {
@@ -13299,7 +13307,7 @@ function expectedRedirectUris(cfg) {
13299
13307
  ]);
13300
13308
  }
13301
13309
  function oauthSsmKeys() {
13302
- return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
13310
+ return [...SSM_NAMES];
13303
13311
  }
13304
13312
  function parseOauthClientJson(input) {
13305
13313
  let parsed;
@@ -13373,11 +13381,20 @@ function parseSecretsCatalogVar(raw) {
13373
13381
  throw new Error("project set: secrets must be a map keyed by canonical KEY");
13374
13382
  }
13375
13383
  const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
13384
+ const overrideMapKey = /^([A-Z_][A-Z0-9_]*)@(dev|rc|main)$/;
13376
13385
  for (const [mapKey, value] of Object.entries(parsed)) {
13377
13386
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`project set: secrets["${mapKey}"] must be an object`);
13378
13387
  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)`);
13388
+ if (typeof e.key !== "string" || !SECRET_ENV_NAME_RE.test(e.key)) {
13389
+ throw new Error(`project set: secrets["${mapKey}"].key must be an env-name (UPPER_SNAKE)`);
13390
+ }
13391
+ const override = overrideMapKey.exec(mapKey);
13392
+ if (override) {
13393
+ if (e.key !== override[1] || !Array.isArray(e.stages) || e.stages.length !== 1 || e.stages[0] !== override[2]) {
13394
+ throw new Error(`project set: secrets["${mapKey}"] is an override entry (#2528) \u2014 key must be ${override[1]} and stages exactly ["${override[2]}"]`);
13395
+ }
13396
+ } else if (e.key !== mapKey) {
13397
+ throw new Error(`project set: secrets["${mapKey}"].key must equal the map key (or use the KEY@<stage> override form, #2528)`);
13381
13398
  }
13382
13399
  if (!nonEmpty(e.purpose) || !nonEmpty(e.group) || !nonEmpty(e.owner)) throw new Error(`project set: secrets["${mapKey}"] needs non-empty purpose, group, owner`);
13383
13400
  if (e.provider !== void 0 && !nonEmpty(e.provider)) throw new Error(`project set: secrets["${mapKey}"].provider must be a non-empty string`);
@@ -13924,8 +13941,7 @@ function classifyTier(_slug, key) {
13924
13941
  return key.slice(0, slash) === PROJECT_TIER_SEGMENT ? "project" : "org";
13925
13942
  }
13926
13943
  function secretParamName(slug, key) {
13927
- const rel = key.includes("/") ? key : `${PROJECT_TIER_SEGMENT}/${key}`;
13928
- return `${SSM_ROOT}/${slug}/${rel}`;
13944
+ return `${SSM_ROOT}/${slug}/${key}`;
13929
13945
  }
13930
13946
  function formatSecretList(items) {
13931
13947
  if (!items.length) return "no secrets";
@@ -13944,10 +13960,10 @@ function vaultPointer(slug) {
13944
13960
  org: [`/mmi-future/{shared,cloudflare,mmi-hub,...}/* (org-infra, master-gated)`]
13945
13961
  },
13946
13962
  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.
13963
+ // Google OAuth is one client per repo = ONE stageless value pair at the slug root (#2244/#2528);
13964
+ // every stage (and local) reads the same pair. The staged {dev,rc,main}/GOOGLE_* shape is retired.
13949
13965
  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"]
13966
+ googleOAuth: ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"]
13951
13967
  }
13952
13968
  };
13953
13969
  }
@@ -14203,8 +14219,9 @@ function formatCatalog(r) {
14203
14219
  return lines.join("\n");
14204
14220
  }
14205
14221
  function formatDrift(d) {
14206
- if (d.ok) return "vault doctor: zero drift \u2014 SSM matches the catalog.";
14207
- const lines = ["vault doctor: drift found", ""];
14222
+ const shadows = d.stagedShadow ?? [];
14223
+ if (d.ok && !shadows.length) return "vault doctor: zero drift \u2014 SSM matches the catalog.";
14224
+ const lines = [d.ok ? "vault doctor: zero drift \u2014 SSM matches the catalog. Advisories:" : "vault doctor: drift found", ""];
14208
14225
  const section = (title, rows) => {
14209
14226
  if (!rows.length) return;
14210
14227
  lines.push(`${title} (${rows.length}):`, ...rows.map((r) => ` - ${r}`), "");
@@ -14213,6 +14230,10 @@ function formatDrift(d) {
14213
14230
  section("ORPHAN in SSM, undeclared \u2014 declare or delete", d.orphan.map((o) => o.path));
14214
14231
  section("OFF-SCHEME path", d.offScheme.map((o) => `${o.path} (${o.reason})`));
14215
14232
  section("STAGE-SPLIT \u2014 collapse to one shared key", (d.stageSplit ?? []).map((s) => `${s.slug}/${s.leaf} split across ${s.stages.join("/")}`));
14233
+ section(
14234
+ "STAGED SHADOW \u2014 the staged override WINS its stage over the stageless canonical (#2522)",
14235
+ shadows.map((s) => `${s.path} shadows ${s.canonicalPath}${s.declared ? "" : " (UNDECLARED residue \u2014 triage per #2526)"}`)
14236
+ );
14216
14237
  section("DUPLICATE org-infra secret", d.duplicate.map((dup) => `${dup.logical}: ${dup.paths.join(" , ")}`));
14217
14238
  return lines.join("\n").trimEnd();
14218
14239
  }
@@ -14317,10 +14338,11 @@ async function secretsPreflight(deps, opts) {
14317
14338
  }
14318
14339
  const { secrets } = await res.json();
14319
14340
  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));
14341
+ const missing = opts.required.filter(
14342
+ (key) => !present.has(stageKey2(opts.stage, key)) && !(key.includes("/") ? false : present.has(key))
14343
+ );
14322
14344
  if (missing.length) {
14323
- deps.log(`missing ${missing.join(", ")}`);
14345
+ deps.log(`missing ${missing.map((key) => key.includes("/") ? key : `${stageKey2(opts.stage, key)} (and no stageless ${key})`).join(", ")}`);
14324
14346
  return false;
14325
14347
  }
14326
14348
  deps.log(`all required ${opts.stage} secret names are present`);
@@ -14352,7 +14374,8 @@ async function secretsGet(deps, key, opts) {
14352
14374
  );
14353
14375
  return false;
14354
14376
  }
14355
- const { value } = await res.json();
14377
+ const { value, notes } = await res.json();
14378
+ for (const note of notes ?? []) deps.err(`note: ${note}`);
14356
14379
  deps.log(value ?? "");
14357
14380
  return true;
14358
14381
  }
@@ -14432,6 +14455,8 @@ async function putSecret(deps, key, value, opts) {
14432
14455
  );
14433
14456
  return false;
14434
14457
  }
14458
+ const { warnings } = await res.json();
14459
+ for (const warning of warnings ?? []) deps.err(`warning: ${warning}`);
14435
14460
  const provider = providerForSecretKey(key);
14436
14461
  if (provider) {
14437
14462
  const result = await verifySecretValue(deps, key, value, opts);
@@ -14827,7 +14852,7 @@ function registerSecretsCommands(program3) {
14827
14852
  const ok = await secretsVerify(d, key, o);
14828
14853
  if (!ok) process.exitCode = 1;
14829
14854
  }));
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) => {
14855
+ 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
14856
  const ok = await secretsSet(d, key, o);
14832
14857
  if (!ok) process.exitCode = 1;
14833
14858
  }));
@@ -17144,6 +17169,14 @@ ${stderr}`);
17144
17169
  return false;
17145
17170
  }
17146
17171
  }
17172
+ function healBannerLine(bin, token, refSupported) {
17173
+ const installVerb = token === "codex" ? "add" : "install";
17174
+ 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`;
17175
+ }
17176
+ function refAbsenceNote(bin, refSupported) {
17177
+ return refSupported ? "" : `
17178
+ Note: \`${bin}\` has no \`--ref\` option; the marketplace manifest pins the released branch, so none is needed (#2516).`;
17179
+ }
17147
17180
  async function applyPluginHeal(token, surface, log, opts) {
17148
17181
  if (!opts?.force && surfaceToken(surface) !== token) return false;
17149
17182
  const descriptor = PLUGIN_SURFACE_HEAL[token];
@@ -17152,13 +17185,8 @@ async function applyPluginHeal(token, surface, log, opts) {
17152
17185
  const bin = descriptor.pluginRunner === "codex" ? "codex" : "claude";
17153
17186
  const runner = descriptor.pluginRunner === "codex" ? runCodexPlugin : runClaudePlugin;
17154
17187
  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
- }
17188
+ const { steps } = adaptHealStepsForRefSupport(tableSteps, refSupported);
17189
+ log(healBannerLine(bin, token, refSupported));
17162
17190
  for (const step of steps) {
17163
17191
  if (healStepAborts(step, await runner([...step.args]))) return false;
17164
17192
  }
@@ -18679,8 +18707,7 @@ async function runPluginHeal(surface = detectSurface(process.env)) {
18679
18707
  const bin = descriptor.pluginRunner === "codex" ? "codex" : "claude";
18680
18708
  const refSupported = await marketplaceAddRefSupported(bin);
18681
18709
  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.`;
18710
+ const note = refAbsenceNote(bin, refSupported);
18684
18711
  console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
18685
18712
  ${recovery}${note}`);
18686
18713
  }
@@ -19186,12 +19213,7 @@ function scheduleRelatedDiscovery(o) {
19186
19213
  try {
19187
19214
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
19188
19215
  if (o.repo) args.push("--repo", o.repo);
19189
- (0, import_node_child_process12.spawn)(process.execPath, [process.argv[1], ...args], {
19190
- detached: true,
19191
- stdio: "ignore",
19192
- windowsHide: true,
19193
- cwd: process.cwd()
19194
- }).unref();
19216
+ spawnDetachedSelf(args, { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
19195
19217
  } catch {
19196
19218
  }
19197
19219
  }
@@ -19242,8 +19264,8 @@ tenant.command("readiness <owner/repo> <stage>").description("alias for tenant s
19242
19264
  console.log(JSON.stringify(result, null, 2));
19243
19265
  if (result.publicProbe?.ok === false) process.exitCode = 1;
19244
19266
  });
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");
19267
+ 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) => {
19268
+ if (stage2 !== "dev" && stage2 !== "rc" && stage2 !== "main") return fail("tenant redeploy: <stage> must be dev, rc, or main");
19247
19269
  try {
19248
19270
  const result = await runTenantRedeploy(trainApplyDeps(), { repo, stage: stage2, ref: o.ref, watch: o.watch });
19249
19271
  return printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantRedeploy(result));
@@ -19704,7 +19726,7 @@ SSM cred params (under /mmi-future/${slug}/):`);
19704
19726
  ssm.forEach((k) => console.log(` ${k}`));
19705
19727
  console.log("\nProvision/repair the Console client per docs/Guides/oauth-provision.md; store creds with `mmi-cli oauth set-creds`.");
19706
19728
  });
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) => {
19729
+ 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
19730
  const raw = await readStdin();
19709
19731
  if (!raw.trim()) {
19710
19732
  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 +19745,10 @@ oauth.command("set-creds").description('store the OAuth client into the canonica
19723
19745
  return;
19724
19746
  }
19725
19747
  }
19726
- console.log(`OAuth client stored in all ${oauthSsmKeys().length} canonical keys. Run \`mmi-cli oauth verify\` to confirm the client is port-agnostic.`);
19748
+ 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
19749
  });
19728
19750
  });
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) => {
19751
+ 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
19752
  const cfg = await loadConfig();
19731
19753
  const slug = (o.repo ? o.repo.split("/").pop() : cfg.project ?? await repoSlug()).toLowerCase();
19732
19754
  const meta = await fetchProjectBySlug(slug, registryClientDeps(cfg));
@@ -19739,11 +19761,11 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
19739
19761
  let clientId = o.clientId;
19740
19762
  if (!clientId) {
19741
19763
  await withSecrets(async (d) => {
19742
- clientId = await fetchSecretValue(d, "dev/GOOGLE_CLIENT_ID", { repo: o.repo }) ?? void 0;
19764
+ clientId = await fetchSecretValue(d, "GOOGLE_CLIENT_ID", { repo: o.repo }) ?? void 0;
19743
19765
  });
19744
19766
  }
19745
19767
  if (!clientId) {
19746
- return failGraceful("oauth verify: no client_id (pass --client-id, or provision the repo so dev/GOOGLE_CLIENT_ID exists)");
19768
+ return failGraceful("oauth verify: no client_id (pass --client-id, or provision the repo so GOOGLE_CLIENT_ID exists)");
19747
19769
  }
19748
19770
  const redirectUri = probeRedirectUri(oc.callbackPath);
19749
19771
  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.6.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",