@mutmutco/cli 0.12.0 → 2.1.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/README.md +12 -7
- package/dist/index.cjs +192 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
The command-line engine for MMI Future org tooling. It delivers the org spine, reads and claims GitHub Project work, records saga continuity notes, and exposes the model-agnostic commands used by the MMI plugin and non-Claude agents.
|
|
4
4
|
|
|
5
|
-
This package is published from [mutmutco/MMI-Hub](https://github.com/mutmutco/MMI-Hub) and its version matches the MMI Hub Claude Code and Codex plugin versions.
|
|
5
|
+
This package is published from [mutmutco/MMI-Hub](https://github.com/mutmutco/MMI-Hub) and its version matches the MMI Hub Claude Code and Codex plugin versions (the release train bumps all of them in lockstep).
|
|
6
|
+
|
|
7
|
+
The CLI carries the org **Hub endpoint** intrinsically (override with the `MMI_HUB_URL` env var), so a product repo needs **no committed `.mmi/config.json`** to reach the Hub — board coords, deploy coordinates, OAuth, and the secrets layout are all discovered from the Hub registry at runtime.
|
|
6
8
|
|
|
7
9
|
## Install
|
|
8
10
|
|
|
@@ -28,17 +30,20 @@ mmi-cli doctor --json
|
|
|
28
30
|
- `mmi-cli rules sync` delivers the org-owned `AGENTS.md`, `CLAUDE.md`, and Claude settings files.
|
|
29
31
|
- `mmi-cli docs sync` refreshes repo-owned `README.md` and `architecture.md` without clobbering dirty files.
|
|
30
32
|
- `mmi-cli saga note`, `saga show`, `saga health`, `saga session`, `saga capture`, and `saga head-update` write and inspect session continuity.
|
|
31
|
-
- `mmi-cli kb get
|
|
33
|
+
- `mmi-cli kb get` and `kb list` read the MM KB source (`kb list [prefix]` lists document paths, optionally under a prefix).
|
|
32
34
|
- `mmi-cli northstar push|pull|list|delete|graduate` manages North Star, the per-user plan/SSOT store.
|
|
33
35
|
`northstar graduate <slug> --merged-pr <url-or-number> --org-visible` marks a built-and-merged plan for
|
|
34
36
|
KB curation without echoing the plan body.
|
|
35
37
|
`mmi-cli plan` remains a compatibility alias.
|
|
36
|
-
- `mmi-cli secrets list|get|set|edit|rm|use|grant|revoke` manages two-tier project/org secrets without logging values.
|
|
37
|
-
- `mmi-cli
|
|
38
|
-
- `mmi-cli
|
|
38
|
+
- `mmi-cli secrets where|list|get|set|edit|rm|use|grant|revoke` manages two-tier project/org secrets without logging values; `where` prints the vault layout + well-known keys, and values move over TLS in the request body — never an argument.
|
|
39
|
+
- `mmi-cli project list|get|resolve|set` reads the DDB org registry — a project's identity + board coords + deploy coordinates (`resolve` reads deploy coords, which are OIDC-gated; `set` is master-only).
|
|
40
|
+
- `mmi-cli registry org` reads org-level constants from the registry (`ORG#config`).
|
|
41
|
+
- `mmi-cli oauth plan|verify` prints a repo's canonical Google OAuth URI set (read from the registry) and verifies the client is port-agnostic.
|
|
42
|
+
- `mmi-cli issue create` creates typed, prioritized GitHub issues (priority sets the board field, not a label) and queues related-issue discovery.
|
|
43
|
+
- `mmi-cli pr create` and `pr merge` create PRs and land them with branch/worktree cleanup; `mmi-cli gc` dry-runs cleanup of merged/closed PR branches + stale tracking refs.
|
|
39
44
|
- `mmi-cli board read|claim|show|move|done|backfill-priority` reads and moves GitHub Project work.
|
|
40
|
-
- `mmi-cli stage`, `stage start`, `stage stop`,
|
|
41
|
-
- `mmi-cli
|
|
45
|
+
- `mmi-cli stage`, `stage start`, `stage stop`, `stage run`, and `port-range <repo>` manage the local gitignored stage and its port block; `stage-live` explains that remote rc/live move only via `/rcand` · `/release` · `/hotfix`.
|
|
46
|
+
- `mmi-cli rcand`, `release`, and `hotfix` render guarded train plans; the train triggers the Hub's central tenant deployer, so product repos carry no deploy file.
|
|
42
47
|
- `mmi-cli bootstrap`, `bootstrap verify`, and `bootstrap apply` plan, audit, and seed repo onboarding.
|
|
43
48
|
- `mmi-cli access audit` checks collaborator roles and train-branch allowlists.
|
|
44
49
|
- `mmi-cli doctor` checks GitHub auth, repo config, CLI availability, the per-project plugin install record, and stale plugin/cache state, auto-repairing the safe gaps.
|
package/dist/index.cjs
CHANGED
|
@@ -4288,23 +4288,29 @@ function stagePlan(stage2 = {}) {
|
|
|
4288
4288
|
function stageLivePlan() {
|
|
4289
4289
|
return [
|
|
4290
4290
|
{ label: "stage-live is not an org command; /stage is local only", command: "mmi-cli stage run --apply" },
|
|
4291
|
-
{ label: "remote rc/live environments move through the gated promotion train", command: "mmi-cli
|
|
4291
|
+
{ label: "remote rc/live environments move through the gated promotion train", command: "mmi-cli rcand && mmi-cli release && mmi-cli hotfix", gated: true }
|
|
4292
4292
|
];
|
|
4293
4293
|
}
|
|
4294
4294
|
function trainPlan(command) {
|
|
4295
|
-
if (command === "
|
|
4295
|
+
if (command === "rcand") {
|
|
4296
4296
|
return [
|
|
4297
|
-
{ label: "verify
|
|
4297
|
+
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
4298
|
+
{ label: "verify current branch is development", gated: true },
|
|
4299
|
+
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
4300
|
+
{ label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
|
|
4298
4301
|
{ label: "merge development to rc", gated: true },
|
|
4299
|
-
{ label: "deploy rc", gated: true }
|
|
4302
|
+
{ label: "dispatch central tenant deploy for rc", command: "gh workflow run tenant-deploy.yml --repo mutmutco/MMI-Hub -f slug=<slug> -f repo=<owner/repo> -f ref=rc -f stage=rc", gated: true }
|
|
4300
4303
|
];
|
|
4301
4304
|
}
|
|
4302
4305
|
if (command === "release") {
|
|
4303
4306
|
return [
|
|
4304
|
-
{ label: "verify
|
|
4307
|
+
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
4308
|
+
{ label: "verify current branch is rc", gated: true },
|
|
4309
|
+
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
4310
|
+
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
4305
4311
|
{ label: "merge rc to main", gated: true },
|
|
4306
4312
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
4307
|
-
{ label: "deploy
|
|
4313
|
+
{ label: "dispatch central tenant deploy for main", command: "gh workflow run tenant-deploy.yml --repo mutmutco/MMI-Hub -f slug=<slug> -f repo=<owner/repo> -f ref=main -f stage=main", gated: true },
|
|
4308
4314
|
{ label: "roll development forward", gated: true }
|
|
4309
4315
|
];
|
|
4310
4316
|
}
|
|
@@ -4628,6 +4634,111 @@ async function runStage(config = {}, opts = {}) {
|
|
|
4628
4634
|
return { ...started, action: "run", message: `built and ${started.message}` };
|
|
4629
4635
|
}
|
|
4630
4636
|
|
|
4637
|
+
// src/train-apply.ts
|
|
4638
|
+
function clean(out) {
|
|
4639
|
+
return out.trim();
|
|
4640
|
+
}
|
|
4641
|
+
function requireValue(value, label) {
|
|
4642
|
+
if (!value) throw new Error(`${label} could not be resolved`);
|
|
4643
|
+
return value;
|
|
4644
|
+
}
|
|
4645
|
+
function ensurePositiveCount(out, emptyMessage) {
|
|
4646
|
+
const count = Number.parseInt(out.trim(), 10);
|
|
4647
|
+
if (!Number.isFinite(count)) throw new Error(`could not parse ahead count: ${out.trim() || "(empty)"}`);
|
|
4648
|
+
if (count <= 0) throw new Error(emptyMessage);
|
|
4649
|
+
}
|
|
4650
|
+
async function buildTrainApplyContext(deps) {
|
|
4651
|
+
const repo = requireValue(clean(await deps.run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])), "repo");
|
|
4652
|
+
const [owner, name] = repo.split("/");
|
|
4653
|
+
if (!owner || !name) throw new Error(`repo must be owner/name, got ${repo}`);
|
|
4654
|
+
const login = requireValue(clean(await deps.run("gh", ["api", "user", "--jq", ".login"])), "GitHub login");
|
|
4655
|
+
const role = clean(await deps.run("gh", ["api", `orgs/${owner}/memberships/${login}`, "--jq", ".role"]));
|
|
4656
|
+
if (role !== "admin") {
|
|
4657
|
+
throw new Error(`${commandAuthorityLabel(owner)} is master-admin only; @${login} is ${role || "not an org admin"}`);
|
|
4658
|
+
}
|
|
4659
|
+
return {
|
|
4660
|
+
repo,
|
|
4661
|
+
owner,
|
|
4662
|
+
slug: name.toLowerCase(),
|
|
4663
|
+
login
|
|
4664
|
+
};
|
|
4665
|
+
}
|
|
4666
|
+
function commandAuthorityLabel(owner) {
|
|
4667
|
+
return `${owner} release train`;
|
|
4668
|
+
}
|
|
4669
|
+
async function requireCleanTree(deps) {
|
|
4670
|
+
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
4671
|
+
if (status.trim()) throw new Error("working tree must be clean before train apply");
|
|
4672
|
+
}
|
|
4673
|
+
async function requireBranch(deps, branch) {
|
|
4674
|
+
const current = clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
4675
|
+
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
4676
|
+
}
|
|
4677
|
+
async function dispatchDeploy(deps, ctx, stage2, ref) {
|
|
4678
|
+
await deps.run("gh", [
|
|
4679
|
+
"workflow",
|
|
4680
|
+
"run",
|
|
4681
|
+
"tenant-deploy.yml",
|
|
4682
|
+
"--repo",
|
|
4683
|
+
"mutmutco/MMI-Hub",
|
|
4684
|
+
"-f",
|
|
4685
|
+
`slug=${ctx.slug}`,
|
|
4686
|
+
"-f",
|
|
4687
|
+
`repo=${ctx.repo}`,
|
|
4688
|
+
"-f",
|
|
4689
|
+
`ref=${ref}`,
|
|
4690
|
+
"-f",
|
|
4691
|
+
`stage=${stage2}`
|
|
4692
|
+
]);
|
|
4693
|
+
}
|
|
4694
|
+
async function preflight(deps, ctx, stage2) {
|
|
4695
|
+
await deps.runSelf(["project", "get", ctx.repo]);
|
|
4696
|
+
await deps.runSelf(["secrets", "preflight", "--stage", stage2, "--repo", ctx.repo]);
|
|
4697
|
+
}
|
|
4698
|
+
async function runTrainApply(command, deps) {
|
|
4699
|
+
const ctx = await buildTrainApplyContext(deps);
|
|
4700
|
+
await requireCleanTree(deps);
|
|
4701
|
+
await deps.run("git", ["fetch", "origin"]);
|
|
4702
|
+
if (command === "rcand") {
|
|
4703
|
+
await requireBranch(deps, "development");
|
|
4704
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
4705
|
+
ensurePositiveCount(
|
|
4706
|
+
await deps.run("git", ["rev-list", "--count", "origin/rc..origin/development"]),
|
|
4707
|
+
"nothing to promote: origin/development is not ahead of origin/rc"
|
|
4708
|
+
);
|
|
4709
|
+
await preflight(deps, ctx, "rc");
|
|
4710
|
+
await deps.run("git", ["checkout", "rc"]);
|
|
4711
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "rc"]);
|
|
4712
|
+
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
4713
|
+
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
|
|
4714
|
+
await deps.run("git", ["tag", tag2]);
|
|
4715
|
+
await deps.run("git", ["push", "origin", "rc"]);
|
|
4716
|
+
await deps.run("git", ["push", "origin", tag2]);
|
|
4717
|
+
await dispatchDeploy(deps, ctx, "rc", "rc");
|
|
4718
|
+
return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2 };
|
|
4719
|
+
}
|
|
4720
|
+
await requireBranch(deps, "rc");
|
|
4721
|
+
ensurePositiveCount(
|
|
4722
|
+
await deps.run("git", ["rev-list", "--count", "origin/main..origin/rc"]),
|
|
4723
|
+
"nothing to release: origin/rc is not ahead of origin/main"
|
|
4724
|
+
);
|
|
4725
|
+
await preflight(deps, ctx, "main");
|
|
4726
|
+
await deps.run("git", ["checkout", "main"]);
|
|
4727
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "main"]);
|
|
4728
|
+
await deps.run("git", ["merge", "rc", "--no-edit"]);
|
|
4729
|
+
const tag = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "release"])), "release tag");
|
|
4730
|
+
await deps.run("git", ["tag", tag]);
|
|
4731
|
+
await deps.run("git", ["push", "origin", "main"]);
|
|
4732
|
+
await deps.run("git", ["push", "origin", tag]);
|
|
4733
|
+
await deps.run("gh", ["release", "create", tag, "--generate-notes", "--latest", "--repo", ctx.repo]);
|
|
4734
|
+
await dispatchDeploy(deps, ctx, "main", "main");
|
|
4735
|
+
await deps.run("git", ["checkout", "development"]);
|
|
4736
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
4737
|
+
await deps.run("git", ["merge", "main", "--no-edit"]);
|
|
4738
|
+
await deps.run("git", ["push", "origin", "development"]);
|
|
4739
|
+
return { ...ctx, command, stage: "main", ref: "main", tag };
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4631
4742
|
// src/port-registry.ts
|
|
4632
4743
|
var import_node_fs3 = require("node:fs");
|
|
4633
4744
|
var BLOCK = 100;
|
|
@@ -4906,7 +5017,7 @@ var requiredIssueTemplates = [
|
|
|
4906
5017
|
".github/ISSUE_TEMPLATE/task.yml",
|
|
4907
5018
|
".github/ISSUE_TEMPLATE/config.yml"
|
|
4908
5019
|
];
|
|
4909
|
-
var requiredWorkflows = [
|
|
5020
|
+
var requiredWorkflows = [];
|
|
4910
5021
|
var requiredLabels = ["bug", "feature", "task", "priority:urgent", "priority:high", "priority:medium", "priority:low"];
|
|
4911
5022
|
var requiredPriorityOptions = ["Urgent", "High", "Medium", "Low"];
|
|
4912
5023
|
var strayDefaultLabels = ["documentation", "duplicate", "enhancement", "good first issue", "help wanted", "invalid", "question", "wontfix"];
|
|
@@ -5373,8 +5484,8 @@ function resolveKbSource(rawBase) {
|
|
|
5373
5484
|
return { owner: m[1], repo: m[2], ref: m[3] };
|
|
5374
5485
|
}
|
|
5375
5486
|
function buildKbGetArgs(src, path) {
|
|
5376
|
-
const
|
|
5377
|
-
return ["api", `repos/${src.owner}/${src.repo}/contents/${
|
|
5487
|
+
const clean2 = path.replace(/^\/+/, "");
|
|
5488
|
+
return ["api", `repos/${src.owner}/${src.repo}/contents/${clean2}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
|
|
5378
5489
|
}
|
|
5379
5490
|
function buildKbTreeArgs(src) {
|
|
5380
5491
|
return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
|
|
@@ -5686,6 +5797,46 @@ async function secretsList(deps, opts) {
|
|
|
5686
5797
|
const { secrets: secrets2 } = await res.json();
|
|
5687
5798
|
deps.log(formatSecretList(secrets2 ?? []));
|
|
5688
5799
|
}
|
|
5800
|
+
var DEFAULT_RUNTIME_SECRET_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
5801
|
+
function stringList(v) {
|
|
5802
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && isValidSecretKey(x)) : [];
|
|
5803
|
+
}
|
|
5804
|
+
function requiredRuntimeSecretNames(stage2, contract) {
|
|
5805
|
+
const extra = Array.isArray(contract) ? stringList(contract) : stringList(contract?.[stage2]);
|
|
5806
|
+
return [.../* @__PURE__ */ new Set([...DEFAULT_RUNTIME_SECRET_NAMES, ...extra])];
|
|
5807
|
+
}
|
|
5808
|
+
function stageKey(stage2, key) {
|
|
5809
|
+
return key.includes("/") ? key : `${stage2}/${key}`;
|
|
5810
|
+
}
|
|
5811
|
+
async function secretsPreflight(deps, opts) {
|
|
5812
|
+
const repo = await targetRepo(deps, opts);
|
|
5813
|
+
const qs = new URLSearchParams({ repo }).toString();
|
|
5814
|
+
let res;
|
|
5815
|
+
try {
|
|
5816
|
+
res = await deps.fetch(`${deps.apiUrl}/secrets/list?${qs}`, {
|
|
5817
|
+
method: "GET",
|
|
5818
|
+
headers: await deps.headers(),
|
|
5819
|
+
signal: AbortSignal.timeout(TIMEOUT_MS2)
|
|
5820
|
+
});
|
|
5821
|
+
} catch (e) {
|
|
5822
|
+
deps.err(`secrets preflight: ${e.message}`);
|
|
5823
|
+
return false;
|
|
5824
|
+
}
|
|
5825
|
+
if (!res.ok) {
|
|
5826
|
+
deps.err(`secrets preflight failed: HTTP ${res.status}${await readErr(res)}`);
|
|
5827
|
+
return false;
|
|
5828
|
+
}
|
|
5829
|
+
const { secrets: secrets2 } = await res.json();
|
|
5830
|
+
const present = new Set((secrets2 ?? []).map((s) => s.key));
|
|
5831
|
+
const required = opts.required.map((key) => stageKey(opts.stage, key));
|
|
5832
|
+
const missing = required.filter((key) => !present.has(key));
|
|
5833
|
+
if (missing.length) {
|
|
5834
|
+
deps.log(`missing ${missing.join(", ")}`);
|
|
5835
|
+
return false;
|
|
5836
|
+
}
|
|
5837
|
+
deps.log(`all required ${opts.stage} secret names are present`);
|
|
5838
|
+
return true;
|
|
5839
|
+
}
|
|
5689
5840
|
async function secretsGet(deps, key, opts) {
|
|
5690
5841
|
if (!isValidSecretKey(key)) return deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
5691
5842
|
const repo = await targetRepo(deps, opts);
|
|
@@ -6516,6 +6667,22 @@ async function withSecrets(run) {
|
|
|
6516
6667
|
var secrets = program2.command("secrets").description("two-tier project secrets \u2014 self-serve your repo dev/* tier; org tier is master-gated");
|
|
6517
6668
|
secrets.command("where").description("print where this repo\u2019s 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)));
|
|
6518
6669
|
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)));
|
|
6670
|
+
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) => {
|
|
6671
|
+
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
6672
|
+
return fail("secrets preflight: --stage must be dev, rc, or main");
|
|
6673
|
+
}
|
|
6674
|
+
const cfg = await loadConfig();
|
|
6675
|
+
if (!cfg.sagaApiUrl) {
|
|
6676
|
+
fail("secrets: sagaApiUrl not configured in .mmi/config.json (this repo is not bootstrapped)");
|
|
6677
|
+
return;
|
|
6678
|
+
}
|
|
6679
|
+
const d = makeSecretsDeps(cfg);
|
|
6680
|
+
const repo = o.repo ?? `mutmutco/${await d.slug()}`;
|
|
6681
|
+
const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(cfg));
|
|
6682
|
+
const required = o.required?.length ? o.required : requiredRuntimeSecretNames(o.stage, meta?.requiredRuntimeSecrets);
|
|
6683
|
+
const ok = await secretsPreflight(d, { repo: o.repo, stage: o.stage, required });
|
|
6684
|
+
if (!ok) process.exitCode = 1;
|
|
6685
|
+
});
|
|
6519
6686
|
secrets.command("get <key>").description("print one secret value over TLS (prints once, raw \u2014 do not log/paste it)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsGet(d, key, o)));
|
|
6520
6687
|
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((d) => secretsSet(d, key, o)));
|
|
6521
6688
|
secrets.command("edit <key>").description("alias for set \u2014 replace a secret value (read from stdin)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsEdit(d, key, o)));
|
|
@@ -6918,13 +7085,25 @@ stage.command("run").description("force-stop previous stage, build, start, and h
|
|
|
6918
7085
|
}
|
|
6919
7086
|
});
|
|
6920
7087
|
program2.command("stage-live").description("explain that remote rc/live environments use /rcand, /release, and /hotfix; /stage is local only").option("--json", "machine-readable output").option("--apply", "always refused; there is no stage-live mutation path").action((o) => {
|
|
6921
|
-
if (o.apply) return fail("stage-live: not an org command; use mmi-cli stage for local tests, or the gated
|
|
7088
|
+
if (o.apply) return fail("stage-live: not an org command; use mmi-cli stage for local tests, or the gated rcand/release/hotfix train for remote environments");
|
|
6922
7089
|
const steps = stageLivePlan();
|
|
6923
7090
|
console.log(o.json ? JSON.stringify({ command: "stage-live", steps }, null, 2) : renderSteps("mmi-cli stage-live: not an org command", steps));
|
|
6924
7091
|
});
|
|
6925
|
-
for (const commandName of ["
|
|
6926
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit approval`).option("--json", "machine-readable output").option("--apply", "reserved
|
|
6927
|
-
if (o.apply)
|
|
7092
|
+
for (const commandName of ["rcand", "release", "hotfix"]) {
|
|
7093
|
+
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--apply", commandName === "hotfix" ? "reserved; hotfix uses the /hotfix skill PR path" : "execute the guarded master-only train after explicit approval").action(async (o) => {
|
|
7094
|
+
if (o.apply) {
|
|
7095
|
+
if (commandName === "hotfix") return fail("hotfix: CLI apply is reserved; use the /hotfix skill PR path after explicit master-admin approval");
|
|
7096
|
+
try {
|
|
7097
|
+
const result = await runTrainApply(commandName, {
|
|
7098
|
+
run: async (file, args) => (await execFileP3(file, args, { timeout: file === "gh" ? 3e4 : GIT_TIMEOUT_MS })).stdout,
|
|
7099
|
+
runSelf: async (args) => (await execFileP3(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout
|
|
7100
|
+
});
|
|
7101
|
+
const message = `mmi-cli ${commandName}: applied ${result.repo} ${result.stage} train at ${result.tag}; dispatched ${result.ref} deploy`;
|
|
7102
|
+
return printLine(o.json ? JSON.stringify(result, null, 2) : message);
|
|
7103
|
+
} catch (e) {
|
|
7104
|
+
return fail(`${commandName}: ${e.message}`);
|
|
7105
|
+
}
|
|
7106
|
+
}
|
|
6928
7107
|
const steps = trainPlan(commandName);
|
|
6929
7108
|
console.log(o.json ? JSON.stringify({ command: commandName, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps));
|
|
6930
7109
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
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",
|