@mutmutco/cli 2.60.0 → 2.61.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/main.cjs +118 -50
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -14143,8 +14143,8 @@ function bootstrapPlan(repo, repoClass) {
|
|
|
14143
14143
|
{ label: `provision branch model: ${branchModel}`, gated: true },
|
|
14144
14144
|
{ label: `apply branch protection / allowlist: ${protectedBranches}`, gated: true },
|
|
14145
14145
|
{ label: "attach GitHub Project v2 and register saga-ready Hub registry META", gated: true },
|
|
14146
|
-
{ label: "seed README.md
|
|
14147
|
-
{ label: "commit .
|
|
14146
|
+
{ label: "seed README.md and architecture.md", gated: true },
|
|
14147
|
+
{ label: "commit .cursor/rules/<repo>.mdc and .cursor/environment.json", gated: true },
|
|
14148
14148
|
{ label: `register fanout target on ${repoClass === "content" ? "main" : "development"}`, gated: true }
|
|
14149
14149
|
];
|
|
14150
14150
|
}
|
|
@@ -14329,28 +14329,6 @@ async function runStageLiveDown(deps, t) {
|
|
|
14329
14329
|
};
|
|
14330
14330
|
}
|
|
14331
14331
|
|
|
14332
|
-
// src/org-spine-pull.ts
|
|
14333
|
-
async function reconcileDirtyOrgSpineBeforePull(deps, branch, options = {}) {
|
|
14334
|
-
await deps.run("git", ["fetch", "origin", branch]).catch(() => "");
|
|
14335
|
-
const reconciled = [];
|
|
14336
|
-
for (const file of ORG_SPINE_FILES) {
|
|
14337
|
-
const status = (await deps.run("git", ["status", "--porcelain", "--", file])).trim();
|
|
14338
|
-
if (!status) continue;
|
|
14339
|
-
if (options.isRulesSource) {
|
|
14340
|
-
options.warn?.(
|
|
14341
|
-
`org spine file ${file} has local edits \u2014 skipped reconcile (rules source repo; commit or stash before train)`
|
|
14342
|
-
);
|
|
14343
|
-
continue;
|
|
14344
|
-
}
|
|
14345
|
-
try {
|
|
14346
|
-
await deps.run("git", ["checkout", `origin/${branch}`, "--", file]);
|
|
14347
|
-
reconciled.push(file);
|
|
14348
|
-
} catch {
|
|
14349
|
-
}
|
|
14350
|
-
}
|
|
14351
|
-
return reconciled;
|
|
14352
|
-
}
|
|
14353
|
-
|
|
14354
14332
|
// src/git-clean-tree.ts
|
|
14355
14333
|
function isAgentScratchPath(path2) {
|
|
14356
14334
|
const normalized = path2.replace(/\\/g, "/").trim();
|
|
@@ -14469,22 +14447,7 @@ async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedS
|
|
|
14469
14447
|
throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
|
|
14470
14448
|
}
|
|
14471
14449
|
}
|
|
14472
|
-
var ORG_SPINE_FILES = [
|
|
14473
|
-
"AGENTS.md",
|
|
14474
|
-
"CLAUDE.md",
|
|
14475
|
-
"GEMINI.md",
|
|
14476
|
-
".claude/settings.json",
|
|
14477
|
-
".claude/output-styles/mmi-plain.md",
|
|
14478
|
-
".cursor/rules/mmi-plain-language.mdc",
|
|
14479
|
-
".cursor/rules/mmi-tool-economy.mdc",
|
|
14480
|
-
".cursor/rules/mmi-code-economy.mdc"
|
|
14481
|
-
];
|
|
14482
|
-
function isSpinePath(path2) {
|
|
14483
|
-
return ORG_SPINE_FILES.includes(path2);
|
|
14484
|
-
}
|
|
14485
14450
|
async function ffOnlyPull(deps, branch) {
|
|
14486
|
-
const isRulesSource2 = deps.isRulesSource ? await deps.isRulesSource() : false;
|
|
14487
|
-
await reconcileDirtyOrgSpineBeforePull(deps, branch, { isRulesSource: isRulesSource2, warn: deps.warn });
|
|
14488
14451
|
await deps.run("git", ["pull", "--ff-only", "origin", branch]);
|
|
14489
14452
|
}
|
|
14490
14453
|
var RELEASE_TOLERATED_PATHS = [".gitignore"];
|
|
@@ -14515,18 +14478,18 @@ async function runMergeTreePreflight(deps, ours, theirs) {
|
|
|
14515
14478
|
async function predictMergeConflicts(deps, ours, theirs) {
|
|
14516
14479
|
return runMergeTreePreflight(deps, ours, theirs);
|
|
14517
14480
|
}
|
|
14518
|
-
async function
|
|
14481
|
+
async function mergeWithToleratedResolution(deps, sourceRef, label, resolve6, extraTolerated = []) {
|
|
14519
14482
|
try {
|
|
14520
14483
|
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
14521
14484
|
return;
|
|
14522
14485
|
} catch {
|
|
14523
14486
|
}
|
|
14524
14487
|
const unmerged = (await deps.run("git", ["diff", "--name-only", "--diff-filter=U"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
14525
|
-
const blocking = unmerged.filter((f) => !
|
|
14488
|
+
const blocking = unmerged.filter((f) => !extraTolerated.includes(f));
|
|
14526
14489
|
if (unmerged.length === 0 || blocking.length > 0) {
|
|
14527
14490
|
await deps.run("git", ["merge", "--abort"]);
|
|
14528
14491
|
throw new Error(
|
|
14529
|
-
unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on
|
|
14492
|
+
unmerged.length === 0 ? `${label} merge failed without conflicted paths \u2014 merge aborted; inspect the repo state and rerun` : `${label} merge conflicts on untolerated path(s): ${blocking.join(", ")} \u2014 merge aborted (the train is misaligned; reconcile the branches via an approved alignment PR, then rerun)`
|
|
14530
14493
|
);
|
|
14531
14494
|
}
|
|
14532
14495
|
await deps.run("git", ["checkout", `--${resolve6}`, "--", ...unmerged]);
|
|
@@ -15071,7 +15034,7 @@ async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix
|
|
|
15071
15034
|
const foldPaths = await resolveFoldPaths(deps, deployModel);
|
|
15072
15035
|
const tolerated = [...foldPaths, ...RELEASE_TOLERATED_PATHS];
|
|
15073
15036
|
const predicted = await predictMergeConflicts(deps, "origin/main", remoteRef);
|
|
15074
|
-
const predictedBlocking = predicted.filter((f) => !
|
|
15037
|
+
const predictedBlocking = predicted.filter((f) => !tolerated.includes(f));
|
|
15075
15038
|
if (predictedBlocking.length > 0) {
|
|
15076
15039
|
throw new Error(`${blockingPrefix}: ${predictedBlocking.join(", ")} \u2014 no merge was started. ${realignMessage}`);
|
|
15077
15040
|
}
|
|
@@ -15083,7 +15046,7 @@ async function executeMergeToMain(deps, sourceRef, mergeLabel, tolerated, predic
|
|
|
15083
15046
|
if (predicted.length === 0) {
|
|
15084
15047
|
await deps.run("git", ["merge", sourceRef, "--no-edit"]);
|
|
15085
15048
|
} else {
|
|
15086
|
-
await
|
|
15049
|
+
await mergeWithToleratedResolution(deps, sourceRef, mergeLabel, "theirs", tolerated);
|
|
15087
15050
|
}
|
|
15088
15051
|
}
|
|
15089
15052
|
async function mergeSourceToMain(deps, deployModel, args) {
|
|
@@ -15192,7 +15155,7 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
15192
15155
|
sourceRef: "development",
|
|
15193
15156
|
remoteRef: "origin/development",
|
|
15194
15157
|
mergeLabel: "development -> main",
|
|
15195
|
-
blockingPrefix: "development -> main merge would conflict on
|
|
15158
|
+
blockingPrefix: "development -> main merge would conflict on untolerated path(s)",
|
|
15196
15159
|
realignMessage: "The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release."
|
|
15197
15160
|
});
|
|
15198
15161
|
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
@@ -15262,7 +15225,7 @@ async function runTrainApplyPipeline(mode, input) {
|
|
|
15262
15225
|
deps,
|
|
15263
15226
|
deployModel,
|
|
15264
15227
|
"origin/rc",
|
|
15265
|
-
"rc -> main merge would conflict on
|
|
15228
|
+
"rc -> main merge would conflict on untolerated path(s)",
|
|
15266
15229
|
"The train is misaligned: reconcile main and rc via an approved alignment PR (do not hand-resolve on main), then rerun release."
|
|
15267
15230
|
);
|
|
15268
15231
|
const coverage = deps.hotfixCoverage({ mainRef: "origin/main", rcRef: "origin/rc", ack: options.ack ?? [] });
|
|
@@ -15741,8 +15704,6 @@ async function runHotfixStart(deps, options) {
|
|
|
15741
15704
|
const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
|
|
15742
15705
|
if (remoteBranch) {
|
|
15743
15706
|
await deps.run("git", ["checkout", branch]);
|
|
15744
|
-
const isRulesSource2 = deps.isRulesSource ? await deps.isRulesSource() : false;
|
|
15745
|
-
await reconcileDirtyOrgSpineBeforePull(deps, branch, { isRulesSource: isRulesSource2, warn: deps.warn });
|
|
15746
15707
|
await deps.run("git", ["pull", "--ff-only", "origin", branch]);
|
|
15747
15708
|
notes.push(`branch ${branch} already on origin \u2014 reused (cherry-pick/bump assumed present; PR step resumes)`);
|
|
15748
15709
|
} else {
|
|
@@ -16564,7 +16525,7 @@ function renderAccessReport(report) {
|
|
|
16564
16525
|
|
|
16565
16526
|
// src/bootstrap-verify.ts
|
|
16566
16527
|
var TRAIN_BRANCHES = ["development", "rc", "main"];
|
|
16567
|
-
var requiredDocs = ["README.md", "architecture.md"
|
|
16528
|
+
var requiredDocs = ["README.md", "architecture.md"];
|
|
16568
16529
|
var requiredIssueTemplates = [
|
|
16569
16530
|
".github/ISSUE_TEMPLATE/bug.yml",
|
|
16570
16531
|
".github/ISSUE_TEMPLATE/feature.yml",
|
|
@@ -18684,6 +18645,107 @@ async function secretsCapabilities(deps, opts) {
|
|
|
18684
18645
|
}
|
|
18685
18646
|
deps.log(formatCapabilities(report));
|
|
18686
18647
|
}
|
|
18648
|
+
async function fetchCatalog(deps, label, repo, extra) {
|
|
18649
|
+
const qs = new URLSearchParams({ repo, ...extra }).toString();
|
|
18650
|
+
let res;
|
|
18651
|
+
try {
|
|
18652
|
+
res = await deps.fetch(`${deps.apiUrl}/secrets/catalog?${qs}`, {
|
|
18653
|
+
method: "GET",
|
|
18654
|
+
headers: await deps.headers(),
|
|
18655
|
+
signal: AbortSignal.timeout(CAPABILITIES_TIMEOUT_MS)
|
|
18656
|
+
});
|
|
18657
|
+
} catch (e) {
|
|
18658
|
+
if (isTimeoutError(e)) {
|
|
18659
|
+
deps.err(`${label}: timed out after ${CAPABILITIES_TIMEOUT_MS}ms aggregating the catalog for ${repo}. No conclusion made; retry with a warm Hub.`);
|
|
18660
|
+
return null;
|
|
18661
|
+
}
|
|
18662
|
+
deps.err(`${label}: ${e.message}`);
|
|
18663
|
+
return null;
|
|
18664
|
+
}
|
|
18665
|
+
if (!res.ok) {
|
|
18666
|
+
const upgrade = await upgradeMessage(res);
|
|
18667
|
+
if (upgrade) deps.err(upgrade);
|
|
18668
|
+
else if (res.status === 404)
|
|
18669
|
+
deps.err(`${label}: the Hub API did not recognize /secrets/catalog (HTTP 404) \u2014 the deployed Hub predates this command. Not an authorization error; it answers once a Hub release carrying it is deployed.`);
|
|
18670
|
+
else if (res.status === 403)
|
|
18671
|
+
deps.err(`${label}: master-admin only (HTTP 403) \u2014 the catalog surface is master-only (#2220).`);
|
|
18672
|
+
else deps.err(`${label} failed: HTTP ${res.status}${await readErr(res)}`);
|
|
18673
|
+
return null;
|
|
18674
|
+
}
|
|
18675
|
+
return await res.json();
|
|
18676
|
+
}
|
|
18677
|
+
function whereLabel(v) {
|
|
18678
|
+
return v.slug === "_org" ? `_org/${v.provider ?? v.group}` : v.slug;
|
|
18679
|
+
}
|
|
18680
|
+
function formatFindHits(intent, hits) {
|
|
18681
|
+
if (!hits.length) return `no catalog match for "${intent}". Browse with \`mmi-cli secrets catalog\`, or run \`mmi-cli secrets doctor\` for drift.`;
|
|
18682
|
+
const blocks = hits.map((h) => [`${h.key} (${whereLabel(h)}) \u2014 ${h.purpose}`, ` ${h.command}`].join("\n"));
|
|
18683
|
+
return [`${hits.length} match(es) for "${intent}" (only \`get\` prints a value):`, "", ...blocks].join("\n");
|
|
18684
|
+
}
|
|
18685
|
+
function formatCatalog(r) {
|
|
18686
|
+
const lines = [`catalog \u2014 ${r.count} secret(s) across ${r.groups.length} group(s):`, ""];
|
|
18687
|
+
for (const g of r.groups) {
|
|
18688
|
+
lines.push(`\u25B8 ${g.group}`);
|
|
18689
|
+
const width = Math.max(...g.secrets.map((s) => s.key.length), 1);
|
|
18690
|
+
for (const s of g.secrets) lines.push(` ${s.key.padEnd(width)} ${s.command}`);
|
|
18691
|
+
}
|
|
18692
|
+
lines.push("", "Names + meaning only \u2014 values are never shown. `secrets doctor` reports drift.");
|
|
18693
|
+
return lines.join("\n");
|
|
18694
|
+
}
|
|
18695
|
+
function formatDrift(d) {
|
|
18696
|
+
if (d.ok) return "vault doctor: zero drift \u2014 SSM matches the catalog.";
|
|
18697
|
+
const lines = ["vault doctor: drift found", ""];
|
|
18698
|
+
const section = (title, rows) => {
|
|
18699
|
+
if (!rows.length) return;
|
|
18700
|
+
lines.push(`${title} (${rows.length}):`, ...rows.map((r) => ` - ${r}`), "");
|
|
18701
|
+
};
|
|
18702
|
+
section("declared but MISSING in SSM", d.declaredMissing.map((m) => m.path));
|
|
18703
|
+
section("ORPHAN in SSM, undeclared \u2014 declare or delete", d.orphan.map((o) => o.path));
|
|
18704
|
+
section("OFF-SCHEME path", d.offScheme.map((o) => `${o.path} (${o.reason})`));
|
|
18705
|
+
section("DUPLICATE org-infra secret", d.duplicate.map((dup) => `${dup.logical}: ${dup.paths.join(" , ")}`));
|
|
18706
|
+
return lines.join("\n").trimEnd();
|
|
18707
|
+
}
|
|
18708
|
+
async function secretsFind(deps, intent, opts) {
|
|
18709
|
+
const repo = await targetRepo(deps, opts);
|
|
18710
|
+
const data = await fetchCatalog(deps, "secrets find", repo, { intent });
|
|
18711
|
+
if (!data) return false;
|
|
18712
|
+
if (data.mode !== "intent") {
|
|
18713
|
+
deps.err("secrets find: unexpected response mode");
|
|
18714
|
+
return false;
|
|
18715
|
+
}
|
|
18716
|
+
if (opts.json) {
|
|
18717
|
+
deps.log(JSON.stringify(data, null, 2));
|
|
18718
|
+
return true;
|
|
18719
|
+
}
|
|
18720
|
+
deps.log(formatFindHits(intent, data.hits));
|
|
18721
|
+
return true;
|
|
18722
|
+
}
|
|
18723
|
+
async function secretsCatalog(deps, opts) {
|
|
18724
|
+
const repo = await targetRepo(deps, opts);
|
|
18725
|
+
const data = await fetchCatalog(deps, "secrets catalog", repo, opts.drift ? { drift: "true" } : {});
|
|
18726
|
+
if (!data) return;
|
|
18727
|
+
if (opts.json) {
|
|
18728
|
+
deps.log(JSON.stringify(data, null, 2));
|
|
18729
|
+
return;
|
|
18730
|
+
}
|
|
18731
|
+
if (data.mode === "drift") deps.log(formatDrift(data.drift));
|
|
18732
|
+
else if (data.mode === "catalog") deps.log(formatCatalog(data));
|
|
18733
|
+
}
|
|
18734
|
+
async function secretsDoctor(deps, opts) {
|
|
18735
|
+
const repo = await targetRepo(deps, opts);
|
|
18736
|
+
const data = await fetchCatalog(deps, "secrets doctor", repo, { drift: "true" });
|
|
18737
|
+
if (!data) return false;
|
|
18738
|
+
if (data.mode !== "drift") {
|
|
18739
|
+
deps.err("secrets doctor: unexpected response mode");
|
|
18740
|
+
return false;
|
|
18741
|
+
}
|
|
18742
|
+
if (opts.json) {
|
|
18743
|
+
deps.log(JSON.stringify(data.drift, null, 2));
|
|
18744
|
+
return data.drift.ok;
|
|
18745
|
+
}
|
|
18746
|
+
deps.log(formatDrift(data.drift));
|
|
18747
|
+
return data.drift.ok;
|
|
18748
|
+
}
|
|
18687
18749
|
var DEFAULT_RUNTIME_SECRET_NAMES2 = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
18688
18750
|
function stringList(v) {
|
|
18689
18751
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && isValidSecretKey(x)) : [];
|
|
@@ -19131,6 +19193,13 @@ function registerSecretsCommands(program3) {
|
|
|
19131
19193
|
const secrets = program3.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
|
|
19132
19194
|
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)").action((o) => withSecrets((d) => secretsWhere(d, o)));
|
|
19133
19195
|
secrets.command("list").description("list secret NAMES + tier for this repo (never values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets((d) => secretsList(d, o)));
|
|
19196
|
+
secrets.command("find <intent>").description("resolve a plain-language intent to the canonical secret(s) + the exact get 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) => {
|
|
19197
|
+
if (!await secretsFind(d, intent, o)) process.exitCode = 1;
|
|
19198
|
+
}));
|
|
19199
|
+
secrets.command("catalog").description("the secret catalog \u2014 grouped, with each secret's exact get command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--drift", "include the bidirectional reconciler drift report").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCatalog(d, o)));
|
|
19200
|
+
secrets.command("doctor").description("vault drift report \u2014 declared-missing / orphan / off-scheme / duplicate (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets(async (d) => {
|
|
19201
|
+
if (!await secretsDoctor(d, o)) process.exitCode = 1;
|
|
19202
|
+
}));
|
|
19134
19203
|
secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").action(async (o) => {
|
|
19135
19204
|
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
19136
19205
|
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
@@ -24435,7 +24504,6 @@ function trainApplyDeps() {
|
|
|
24435
24504
|
const toArr = (v) => v ? [v] : void 0;
|
|
24436
24505
|
return { rc: toArr(ed.rc), main: toArr(ed.main) };
|
|
24437
24506
|
},
|
|
24438
|
-
isRulesSource: async () => isRulesSource((await loadConfig()).orgRulesSource),
|
|
24439
24507
|
warn: (message) => consoleIo.err(`mmi-cli: ${message}`)
|
|
24440
24508
|
};
|
|
24441
24509
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.61.1",
|
|
4
4
|
"description": "MMI Future CLI — delivers the org rules (whole-file), Jervaise-only continuity, and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|