@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.
- package/dist/main.cjs +57 -37
- 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
|
|
13141
|
-
const
|
|
13142
|
-
const
|
|
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
|
|
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)
|
|
13380
|
-
throw new Error(`project set: secrets["${mapKey}"].key must
|
|
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
|
-
|
|
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
|
|
13948
|
-
//
|
|
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: ["
|
|
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
|
-
|
|
14207
|
-
|
|
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
|
|
14321
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, "
|
|
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
|
|
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.
|
|
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",
|