@mutmutco/cli 2.13.0 → 2.14.1
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/index.cjs +221 -15
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -5945,7 +5945,10 @@ async function startStage(config = {}, opts = {}) {
|
|
|
5945
5945
|
const child = (0, import_node_child_process5.spawn)(up, {
|
|
5946
5946
|
cwd,
|
|
5947
5947
|
shell: true,
|
|
5948
|
-
|
|
5948
|
+
// POSIX-only: the process group exists for the group-kill in stopStage. On win32 teardown is
|
|
5949
|
+
// `taskkill /T /F` (no group needed), and detached+shell defeats windowsHide — every spawn would
|
|
5950
|
+
// flash a Windows Terminal window (0x800700e8) on dev machines.
|
|
5951
|
+
detached: process.platform !== "win32",
|
|
5949
5952
|
windowsHide: true,
|
|
5950
5953
|
stdio: "ignore",
|
|
5951
5954
|
env: { ...process.env, ...stagePort != null ? { STAGE_PORT: String(stagePort) } : {}, ...extraEnv }
|
|
@@ -6040,6 +6043,41 @@ async function verifyHubDistributionVersion(deps, model, releaseTag) {
|
|
|
6040
6043
|
if (model !== "hub-serverless") return;
|
|
6041
6044
|
await deps.run("node", ["scripts/release-distribution.mjs", "verify", releaseTag, "--skip-npm-view"]);
|
|
6042
6045
|
}
|
|
6046
|
+
var ORG_SPINE_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md", ".claude/settings.json"];
|
|
6047
|
+
function isSpinePath(path2) {
|
|
6048
|
+
return ORG_SPINE_FILES.includes(path2);
|
|
6049
|
+
}
|
|
6050
|
+
async function predictMergeConflicts(deps, ours, theirs) {
|
|
6051
|
+
try {
|
|
6052
|
+
await deps.run("git", ["merge-tree", "--write-tree", "--name-only", "--no-messages", ours, theirs]);
|
|
6053
|
+
return [];
|
|
6054
|
+
} catch (e) {
|
|
6055
|
+
const out = String(e.stdout ?? "");
|
|
6056
|
+
const files = out.split("\n").map((s) => s.trim()).filter(Boolean).slice(1);
|
|
6057
|
+
if (files.length === 0) {
|
|
6058
|
+
throw new Error(`could not preflight the ${theirs} -> ${ours} merge (git merge-tree failed: ${e.message ?? e})`);
|
|
6059
|
+
}
|
|
6060
|
+
return files;
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
6063
|
+
async function mergeRcWithSpineResolution(deps) {
|
|
6064
|
+
try {
|
|
6065
|
+
await deps.run("git", ["merge", "rc", "--no-edit"]);
|
|
6066
|
+
return;
|
|
6067
|
+
} catch {
|
|
6068
|
+
}
|
|
6069
|
+
const unmerged = (await deps.run("git", ["diff", "--name-only", "--diff-filter=U"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
6070
|
+
const nonSpine = unmerged.filter((f) => !isSpinePath(f));
|
|
6071
|
+
if (unmerged.length === 0 || nonSpine.length > 0) {
|
|
6072
|
+
await deps.run("git", ["merge", "--abort"]);
|
|
6073
|
+
throw new Error(
|
|
6074
|
+
unmerged.length === 0 ? "rc -> main merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun" : `rc -> main merge conflicts on non-spine path(s): ${nonSpine.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile main and rc via an approved alignment PR, then rerun release)`
|
|
6075
|
+
);
|
|
6076
|
+
}
|
|
6077
|
+
await deps.run("git", ["checkout", "--theirs", "--", ...unmerged]);
|
|
6078
|
+
await deps.run("git", ["add", "--", ...unmerged]);
|
|
6079
|
+
await deps.run("git", ["commit", "--no-edit"]);
|
|
6080
|
+
}
|
|
6043
6081
|
function ensurePositiveCount(out, emptyMessage) {
|
|
6044
6082
|
const count = Number.parseInt(out.trim(), 10);
|
|
6045
6083
|
if (!Number.isFinite(count)) throw new Error(`could not parse ahead count: ${out.trim() || "(empty)"}`);
|
|
@@ -6077,6 +6115,11 @@ var HUB_REPO2 = "mutmutco/MMI-Hub";
|
|
|
6077
6115
|
var CORRELATE_ATTEMPTS = 5;
|
|
6078
6116
|
var CORRELATE_DELAY_MS = 1500;
|
|
6079
6117
|
var CORRELATE_SKEW_SLACK_MS = 1e4;
|
|
6118
|
+
var TRAIN_REQUIRED_CHECKS = ["cli", "infra", "docs"];
|
|
6119
|
+
var TRAIN_REQUIRED_CHECK_SET = new Set(TRAIN_REQUIRED_CHECKS);
|
|
6120
|
+
var TRAIN_CHECKS_JQ = '[.check_runs[]|select(.name|test("^(cli|infra|docs)$"))|{(.name):.conclusion}]';
|
|
6121
|
+
var TRAIN_CHECK_ATTEMPTS = 40;
|
|
6122
|
+
var TRAIN_CHECK_DELAY_MS = 15e3;
|
|
6080
6123
|
async function correlateTenantRun(deps, since) {
|
|
6081
6124
|
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6082
6125
|
const threshold = since - CORRELATE_SKEW_SLACK_MS;
|
|
@@ -6114,6 +6157,51 @@ async function watchTenantRun(deps, runId) {
|
|
|
6114
6157
|
return "failure";
|
|
6115
6158
|
}
|
|
6116
6159
|
}
|
|
6160
|
+
function parseTrainCheckConclusions(out) {
|
|
6161
|
+
const parsed = JSON.parse(out);
|
|
6162
|
+
if (!Array.isArray(parsed)) throw new Error("check-runs response was not an array");
|
|
6163
|
+
const conclusions = /* @__PURE__ */ new Map();
|
|
6164
|
+
for (const row of parsed) {
|
|
6165
|
+
if (row == null || typeof row !== "object") continue;
|
|
6166
|
+
for (const [name, raw] of Object.entries(row)) {
|
|
6167
|
+
if (!TRAIN_REQUIRED_CHECK_SET.has(name)) continue;
|
|
6168
|
+
conclusions.set(name, raw == null ? null : String(raw));
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
return conclusions;
|
|
6172
|
+
}
|
|
6173
|
+
function describeTrainChecks(conclusions) {
|
|
6174
|
+
return TRAIN_REQUIRED_CHECKS.map((name) => `${name}=${conclusions.get(name) ?? "missing"}`).join(", ");
|
|
6175
|
+
}
|
|
6176
|
+
async function waitForRequiredTrainChecks(deps, ctx, sha) {
|
|
6177
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6178
|
+
let lastStatus = "not checked";
|
|
6179
|
+
let lastError;
|
|
6180
|
+
for (let attempt = 0; attempt < TRAIN_CHECK_ATTEMPTS; attempt++) {
|
|
6181
|
+
if (attempt > 0) await sleep(TRAIN_CHECK_DELAY_MS);
|
|
6182
|
+
let conclusions;
|
|
6183
|
+
try {
|
|
6184
|
+
const out = await deps.run("gh", ["api", `repos/${ctx.repo}/commits/${sha}/check-runs`, "--jq", TRAIN_CHECKS_JQ]);
|
|
6185
|
+
conclusions = parseTrainCheckConclusions(out);
|
|
6186
|
+
lastError = void 0;
|
|
6187
|
+
} catch (e) {
|
|
6188
|
+
lastError = e.message || String(e);
|
|
6189
|
+
continue;
|
|
6190
|
+
}
|
|
6191
|
+
lastStatus = describeTrainChecks(conclusions);
|
|
6192
|
+
const failed = TRAIN_REQUIRED_CHECKS.filter((name) => {
|
|
6193
|
+
const conclusion = conclusions.get(name);
|
|
6194
|
+
return conclusion != null && conclusion !== "success";
|
|
6195
|
+
});
|
|
6196
|
+
if (failed.length > 0) {
|
|
6197
|
+
throw new Error(`required train check failed: ${failed.join(", ")} (${lastStatus})`);
|
|
6198
|
+
}
|
|
6199
|
+
if (TRAIN_REQUIRED_CHECKS.every((name) => conclusions.get(name) === "success")) return;
|
|
6200
|
+
}
|
|
6201
|
+
throw new Error(
|
|
6202
|
+
`timed out waiting for required train checks on ${sha}: ${lastError ? `last error: ${lastError}` : lastStatus}`
|
|
6203
|
+
);
|
|
6204
|
+
}
|
|
6117
6205
|
async function dispatchDeploy(deps, ctx, stage2, ref, model, watch) {
|
|
6118
6206
|
if (model === "tenant-container") {
|
|
6119
6207
|
const since = (deps.now ?? Date.now)();
|
|
@@ -6180,8 +6268,10 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6180
6268
|
await deps.run("git", ["pull", "--ff-only", "origin", "rc"]);
|
|
6181
6269
|
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
6182
6270
|
await deps.run("git", ["tag", tag2]);
|
|
6183
|
-
await deps.run("git", ["push", "origin", "rc"]);
|
|
6184
6271
|
await deps.run("git", ["push", "origin", tag2]);
|
|
6272
|
+
const rcSha = requireValue(clean(await deps.run("git", ["rev-parse", "rc"])), "rc sha");
|
|
6273
|
+
await waitForRequiredTrainChecks(deps, ctx, rcSha);
|
|
6274
|
+
await deps.run("git", ["push", "origin", "rc"]);
|
|
6185
6275
|
const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch);
|
|
6186
6276
|
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, deployStatus: d2.deployStatus };
|
|
6187
6277
|
}
|
|
@@ -6191,21 +6281,86 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
6191
6281
|
"nothing to release: origin/rc is not ahead of origin/main"
|
|
6192
6282
|
);
|
|
6193
6283
|
const deployModel = await preflight(deps, ctx, "main");
|
|
6284
|
+
const predicted = await predictMergeConflicts(deps, "origin/main", "origin/rc");
|
|
6285
|
+
const predictedNonSpine = predicted.filter((f) => !isSpinePath(f));
|
|
6286
|
+
if (predictedNonSpine.length > 0) {
|
|
6287
|
+
throw new Error(
|
|
6288
|
+
`rc -> main merge would conflict on non-spine path(s): ${predictedNonSpine.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release.`
|
|
6289
|
+
);
|
|
6290
|
+
}
|
|
6291
|
+
const releasedRcSha = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
6194
6292
|
await deps.run("git", ["checkout", "main"]);
|
|
6195
6293
|
await deps.run("git", ["pull", "--ff-only", "origin", "main"]);
|
|
6196
|
-
|
|
6294
|
+
if (predicted.length === 0) {
|
|
6295
|
+
await deps.run("git", ["merge", "rc", "--no-edit"]);
|
|
6296
|
+
} else {
|
|
6297
|
+
await mergeRcWithSpineResolution(deps);
|
|
6298
|
+
}
|
|
6197
6299
|
const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
6198
6300
|
await verifyHubDistributionVersion(deps, deployModel, tag);
|
|
6199
6301
|
await deps.run("git", ["tag", tag]);
|
|
6200
|
-
await deps.run("git", ["push", "origin", "main"]);
|
|
6201
6302
|
await deps.run("git", ["push", "origin", tag]);
|
|
6303
|
+
const releaseSha = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
6304
|
+
await waitForRequiredTrainChecks(deps, ctx, releaseSha);
|
|
6305
|
+
await deps.run("git", ["push", "origin", "main"]);
|
|
6202
6306
|
await deps.run("gh", ["release", "create", tag, "--generate-notes", "--latest", "--repo", ctx.repo]);
|
|
6203
6307
|
const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch);
|
|
6308
|
+
const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
|
|
6204
6309
|
await deps.run("git", ["checkout", "development"]);
|
|
6205
6310
|
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
6206
6311
|
await deps.run("git", ["merge", "main", "--no-edit"]);
|
|
6207
6312
|
await deps.run("git", ["push", "origin", "development"]);
|
|
6208
|
-
return {
|
|
6313
|
+
return {
|
|
6314
|
+
...ctx,
|
|
6315
|
+
command,
|
|
6316
|
+
stage: "main",
|
|
6317
|
+
ref: "main",
|
|
6318
|
+
tag,
|
|
6319
|
+
deployModel,
|
|
6320
|
+
promoted: true,
|
|
6321
|
+
dispatch: d.note,
|
|
6322
|
+
runId: d.runId,
|
|
6323
|
+
runUrl: d.runUrl,
|
|
6324
|
+
deployStatus: d.deployStatus,
|
|
6325
|
+
rcRetirement: retirement.status,
|
|
6326
|
+
rcRetirementNote: retirement.note
|
|
6327
|
+
};
|
|
6328
|
+
}
|
|
6329
|
+
async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
6330
|
+
if (model !== "tenant-container") {
|
|
6331
|
+
return { status: "not-applicable", note: `${model} has no co-resident rc runtime to retire` };
|
|
6332
|
+
}
|
|
6333
|
+
if (deployStatus === "failure") {
|
|
6334
|
+
return { status: "skipped", note: "prod deploy failed \u2014 rc runtime left untouched" };
|
|
6335
|
+
}
|
|
6336
|
+
if (deployStatus !== "success") {
|
|
6337
|
+
return {
|
|
6338
|
+
status: "skipped",
|
|
6339
|
+
note: `prod deploy outcome unconfirmed (run without --watch) \u2014 rc runtime left untouched; after verifying prod, retire with: mmi-cli tenant control ${ctx.repo} rc retire`
|
|
6340
|
+
};
|
|
6341
|
+
}
|
|
6342
|
+
try {
|
|
6343
|
+
await deps.run("git", ["fetch", "origin", "rc"]);
|
|
6344
|
+
const rcNow = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
6345
|
+
if (rcNow !== releasedRcSha) {
|
|
6346
|
+
return {
|
|
6347
|
+
status: "skipped",
|
|
6348
|
+
note: `origin/rc moved past the released candidate (${releasedRcSha.slice(0, 7)} -> ${rcNow.slice(0, 7)}) \u2014 a new candidate is in flight; rc runtime left untouched`
|
|
6349
|
+
};
|
|
6350
|
+
}
|
|
6351
|
+
const out = await deps.runSelf(["tenant", "control", ctx.repo, "rc", "retire"]);
|
|
6352
|
+
let commandId = "";
|
|
6353
|
+
try {
|
|
6354
|
+
commandId = String(JSON.parse(out).commandId ?? "");
|
|
6355
|
+
} catch {
|
|
6356
|
+
}
|
|
6357
|
+
return {
|
|
6358
|
+
status: "retired",
|
|
6359
|
+
note: `rc runtime retired (tenant control retire${commandId ? `, command ${commandId}` : ""}) \u2014 registry coords kept; /rcand or tenant redeploy recreates rc next cycle`
|
|
6360
|
+
};
|
|
6361
|
+
} catch (e) {
|
|
6362
|
+
return { status: "failed", note: `rc retirement failed (the release itself succeeded): ${e.message}` };
|
|
6363
|
+
}
|
|
6209
6364
|
}
|
|
6210
6365
|
async function runTenantRedeploy(deps, options) {
|
|
6211
6366
|
const { stage: stage2 } = options;
|
|
@@ -8117,14 +8272,16 @@ async function secretsRequest(deps, key, opts) {
|
|
|
8117
8272
|
deps.log(body.notified === false ? "admin notification not sent" : "admin notification sent");
|
|
8118
8273
|
return true;
|
|
8119
8274
|
}
|
|
8120
|
-
async function
|
|
8121
|
-
if (!isValidSecretKey(key))
|
|
8122
|
-
|
|
8123
|
-
|
|
8275
|
+
async function putSecret(deps, key, value, opts) {
|
|
8276
|
+
if (!isValidSecretKey(key)) {
|
|
8277
|
+
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
8278
|
+
return false;
|
|
8279
|
+
}
|
|
8124
8280
|
if (!value) {
|
|
8125
|
-
deps.err(
|
|
8126
|
-
return;
|
|
8281
|
+
deps.err(`secrets set: empty value for ${key} \u2014 aborted (nothing written)`);
|
|
8282
|
+
return false;
|
|
8127
8283
|
}
|
|
8284
|
+
const repo = await targetRepo(deps, opts);
|
|
8128
8285
|
const res = await deps.fetch(`${deps.apiUrl}/secrets/set`, {
|
|
8129
8286
|
method: "POST",
|
|
8130
8287
|
headers: await deps.headers({ "content-type": "application/json" }),
|
|
@@ -8135,9 +8292,19 @@ async function secretsSet(deps, key, opts) {
|
|
|
8135
8292
|
deps.err(
|
|
8136
8293
|
res.status === 403 ? `secrets set: not authorized to write ${key} (HTTP 403)${await readErr(res)}` : `secrets set failed: HTTP ${res.status}${await readErr(res)}`
|
|
8137
8294
|
);
|
|
8138
|
-
return;
|
|
8295
|
+
return false;
|
|
8139
8296
|
}
|
|
8140
8297
|
deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier)`);
|
|
8298
|
+
return true;
|
|
8299
|
+
}
|
|
8300
|
+
async function secretsSet(deps, key, opts) {
|
|
8301
|
+
if (!isValidSecretKey(key)) return deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
8302
|
+
const value = await deps.readSecretValue(`value for ${key} (input hidden; will not be echoed): `);
|
|
8303
|
+
if (!value) {
|
|
8304
|
+
deps.err("secrets set: empty value \u2014 aborted (nothing written)");
|
|
8305
|
+
return;
|
|
8306
|
+
}
|
|
8307
|
+
await putSecret(deps, key, value, opts);
|
|
8141
8308
|
}
|
|
8142
8309
|
async function secretsEdit(deps, key, opts) {
|
|
8143
8310
|
return secretsSet(deps, key, opts);
|
|
@@ -8241,6 +8408,22 @@ function expectedRedirectUris(cfg) {
|
|
|
8241
8408
|
function oauthSsmKeys() {
|
|
8242
8409
|
return SSM_ENVS.flatMap((env) => SSM_NAMES.map((name) => `${env}/${name}`));
|
|
8243
8410
|
}
|
|
8411
|
+
function parseOauthClientJson(input) {
|
|
8412
|
+
let parsed;
|
|
8413
|
+
try {
|
|
8414
|
+
parsed = JSON.parse(input);
|
|
8415
|
+
} catch {
|
|
8416
|
+
throw new Error('not valid JSON \u2014 pipe the Google client JSON (the Console "Download JSON" file)');
|
|
8417
|
+
}
|
|
8418
|
+
const root = parsed ?? {};
|
|
8419
|
+
const obj = root.web ?? root.installed ?? parsed;
|
|
8420
|
+
const clientId = typeof obj?.client_id === "string" ? obj.client_id.trim() : "";
|
|
8421
|
+
const clientSecret = typeof obj?.client_secret === "string" ? obj.client_secret.trim() : "";
|
|
8422
|
+
if (!clientId || !clientSecret) {
|
|
8423
|
+
throw new Error("missing client_id or client_secret in the JSON");
|
|
8424
|
+
}
|
|
8425
|
+
return { clientId, clientSecret };
|
|
8426
|
+
}
|
|
8244
8427
|
function parseOauthConfig(mmiConfig, slug) {
|
|
8245
8428
|
const rawUnknown = mmiConfig?.oauth;
|
|
8246
8429
|
if (rawUnknown === void 0) throw new Error(`oauth is not configured for ${slug}`);
|
|
@@ -9112,7 +9295,7 @@ function reportWrite(label, res) {
|
|
|
9112
9295
|
fail(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
9113
9296
|
}
|
|
9114
9297
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
9115
|
-
tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart) for a tenant; project-admin dev/rc, master main").option("--json", "machine-readable output").action(async (repo, stage2, action) => {
|
|
9298
|
+
tenant.command("control <owner/repo> <stage> <action>").description("run bounded service control (status/start/stop/restart, plus rc-only retire) for a tenant; project-admin dev/rc, master main").option("--json", "machine-readable output").action(async (repo, stage2, action) => {
|
|
9116
9299
|
const cfg = await loadConfig();
|
|
9117
9300
|
const res = await tenantControl({ repo, stage: stage2, action }, registryClientDeps(cfg));
|
|
9118
9301
|
reportWrite("tenant control", res);
|
|
@@ -9344,7 +9527,29 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
|
|
|
9344
9527
|
console.log(`
|
|
9345
9528
|
SSM cred params (under /mmi-future/${slug}/):`);
|
|
9346
9529
|
ssm.forEach((k) => console.log(` ${k}`));
|
|
9347
|
-
console.log("\nProvision/repair the Console client per docs/Guides/oauth-provision.md; creds
|
|
9530
|
+
console.log("\nProvision/repair the Console client per docs/Guides/oauth-provision.md; store creds with `mmi-cli oauth set-creds`.");
|
|
9531
|
+
});
|
|
9532
|
+
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) => {
|
|
9533
|
+
const raw = await readStdin();
|
|
9534
|
+
if (!raw.trim()) {
|
|
9535
|
+
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");
|
|
9536
|
+
}
|
|
9537
|
+
let creds;
|
|
9538
|
+
try {
|
|
9539
|
+
creds = parseOauthClientJson(raw);
|
|
9540
|
+
} catch (e) {
|
|
9541
|
+
return fail(`oauth set-creds: ${e.message}`);
|
|
9542
|
+
}
|
|
9543
|
+
await withSecrets(async (d) => {
|
|
9544
|
+
for (const key of oauthSsmKeys()) {
|
|
9545
|
+
const value = key.endsWith("GOOGLE_CLIENT_ID") ? creds.clientId : creds.clientSecret;
|
|
9546
|
+
if (!await putSecret(d, key, value, { repo: o.repo })) {
|
|
9547
|
+
process.exitCode = 1;
|
|
9548
|
+
return;
|
|
9549
|
+
}
|
|
9550
|
+
}
|
|
9551
|
+
console.log(`OAuth client stored in all ${oauthSsmKeys().length} canonical keys. Run \`mmi-cli oauth verify\` to confirm the client is port-agnostic.`);
|
|
9552
|
+
});
|
|
9348
9553
|
});
|
|
9349
9554
|
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) => {
|
|
9350
9555
|
const cfg = await loadConfig();
|
|
@@ -9895,7 +10100,8 @@ function renderDeployLine(d) {
|
|
|
9895
10100
|
return parts.join("; ");
|
|
9896
10101
|
}
|
|
9897
10102
|
function renderTrainApply(commandName, r) {
|
|
9898
|
-
|
|
10103
|
+
const base = `mmi-cli ${commandName}: promoted ${r.repo} \u2192 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
10104
|
+
return r.rcRetirement ? `${base}; rc retirement: ${r.rcRetirement.toUpperCase()} (${r.rcRetirementNote ?? ""})` : base;
|
|
9899
10105
|
}
|
|
9900
10106
|
function renderTenantRedeploy(r) {
|
|
9901
10107
|
return `mmi-cli tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.1",
|
|
4
4
|
"description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|