@mutmutco/cli 2.21.0 → 2.22.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 +223 -6
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -6172,6 +6172,7 @@ function derivedStagePlan(derived, shell2, stops = true) {
|
|
|
6172
6172
|
function stageLivePlan() {
|
|
6173
6173
|
return [
|
|
6174
6174
|
{ label: "stage-live is not an org command; /stage is local only", command: "mmi-cli stage run --apply" },
|
|
6175
|
+
{ label: "personal IP-gated cloud dev stage (on-demand, #1060)", command: "mmi-cli stage --live --apply" },
|
|
6175
6176
|
{ label: "remote rc/live environments move through the gated promotion train", command: "mmi-cli rcand && mmi-cli release && mmi-cli hotfix", gated: true }
|
|
6176
6177
|
];
|
|
6177
6178
|
}
|
|
@@ -6218,6 +6219,21 @@ function trainPlan(command, options = {}) {
|
|
|
6218
6219
|
{ label: "roll development forward", gated: true }
|
|
6219
6220
|
];
|
|
6220
6221
|
}
|
|
6222
|
+
if (options.dev) {
|
|
6223
|
+
return [
|
|
6224
|
+
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
6225
|
+
{ label: "verify current branch is development", gated: true },
|
|
6226
|
+
{ label: "guard: refuse if origin/rc carries content not in development (a dev -> main release would drop it)", command: "git rev-list --count --right-only --cherry-pick --no-merges origin/development...origin/rc", gated: true },
|
|
6227
|
+
{ label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
|
|
6228
|
+
{ label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
6229
|
+
{ label: "merge development to main (rc skipped)", gated: true },
|
|
6230
|
+
{ label: "fold the version bump into the release commit \u2014 runs inside the apply step, no separate bump PR", gated: true },
|
|
6231
|
+
{ label: "tag release and publish GitHub Release", gated: true },
|
|
6232
|
+
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch", gated: true },
|
|
6233
|
+
{ label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli tenant control <owner/repo> rc retire", gated: true },
|
|
6234
|
+
{ label: "roll development forward and align rc to the released main", gated: true }
|
|
6235
|
+
];
|
|
6236
|
+
}
|
|
6221
6237
|
return [
|
|
6222
6238
|
{ label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
|
|
6223
6239
|
{ label: "verify current branch is rc", gated: true },
|
|
@@ -6862,11 +6878,92 @@ function buildHubCompatCheck(input) {
|
|
|
6862
6878
|
return { ok: versionAtLeast(input.installedVersion, min), label: `${label}: requires >= ${min}`, fix: HUB_COMPAT_FIX };
|
|
6863
6879
|
}
|
|
6864
6880
|
|
|
6881
|
+
// src/stage-live.ts
|
|
6882
|
+
var import_node_net = require("node:net");
|
|
6883
|
+
var STAGE_LIVE_HUB_REPO = "mutmutco/MMI-Hub";
|
|
6884
|
+
var IP_ECHO_URL = "https://api.ipify.org";
|
|
6885
|
+
var IP_DETECT_TIMEOUT_MS = 1e4;
|
|
6886
|
+
function validStageLiveIp(ip) {
|
|
6887
|
+
return (0, import_node_net.isIP)(ip.trim()) !== 0;
|
|
6888
|
+
}
|
|
6889
|
+
async function detectPublicIp(fetchImpl = fetch) {
|
|
6890
|
+
let res;
|
|
6891
|
+
try {
|
|
6892
|
+
res = await fetchImpl(IP_ECHO_URL, { signal: AbortSignal.timeout(IP_DETECT_TIMEOUT_MS) });
|
|
6893
|
+
} catch (e) {
|
|
6894
|
+
throw new Error(`public IP detection failed (${IP_ECHO_URL}): ${e.message}`);
|
|
6895
|
+
}
|
|
6896
|
+
if (!res.ok) throw new Error(`public IP detection failed: HTTP ${res.status} from ${IP_ECHO_URL}`);
|
|
6897
|
+
const ip = (await res.text()).trim();
|
|
6898
|
+
if (!validStageLiveIp(ip)) throw new Error(`public IP detection returned a non-IP body from ${IP_ECHO_URL}: "${ip.slice(0, 80)}"`);
|
|
6899
|
+
return ip;
|
|
6900
|
+
}
|
|
6901
|
+
function ghDispatchArgs(workflow, inputs) {
|
|
6902
|
+
const args = ["workflow", "run", workflow, "--repo", STAGE_LIVE_HUB_REPO];
|
|
6903
|
+
for (const [key, value] of Object.entries(inputs)) args.push("-f", `${key}=${value}`);
|
|
6904
|
+
return args;
|
|
6905
|
+
}
|
|
6906
|
+
function stageLiveUpSteps(t) {
|
|
6907
|
+
return [
|
|
6908
|
+
{ label: `detect your public IP (${IP_ECHO_URL}, bounded)` },
|
|
6909
|
+
{
|
|
6910
|
+
label: `deploy ${t.ref ?? "<current branch>"} to the ${t.slug} dev stage via the central deployer`,
|
|
6911
|
+
command: `gh ${ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref ?? "<branch>", stage: "dev" }).join(" ")}`
|
|
6912
|
+
},
|
|
6913
|
+
{
|
|
6914
|
+
label: "record your IP as the dev allowlist (box writes /opt/mmi/<slug>/dev/allowlist + reloads Caddy)",
|
|
6915
|
+
command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip: "<your ip>" }).join(" ")}`
|
|
6916
|
+
},
|
|
6917
|
+
{ label: "tear down when done", command: "mmi-cli stage --live --down --apply" }
|
|
6918
|
+
];
|
|
6919
|
+
}
|
|
6920
|
+
function stageLiveDownSteps(t) {
|
|
6921
|
+
return [
|
|
6922
|
+
{
|
|
6923
|
+
label: `stop the ${t.slug} dev runtime`,
|
|
6924
|
+
command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }).join(" ")}`
|
|
6925
|
+
},
|
|
6926
|
+
{
|
|
6927
|
+
label: "clear the dev allowlist (the stage goes dark even if restarted)",
|
|
6928
|
+
command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip: "clear" }).join(" ")}`
|
|
6929
|
+
}
|
|
6930
|
+
];
|
|
6931
|
+
}
|
|
6932
|
+
async function runStageLiveUp(deps, t) {
|
|
6933
|
+
if (!t.ref?.trim()) throw new Error("stage --live: cannot resolve the current branch to deploy");
|
|
6934
|
+
const ip = (await deps.detectIp()).trim();
|
|
6935
|
+
if (!validStageLiveIp(ip)) throw new Error(`stage --live: detected public IP is not a literal IPv4/IPv6 address: "${ip.slice(0, 80)}"`);
|
|
6936
|
+
await deps.run("gh", ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref, stage: "dev" }));
|
|
6937
|
+
await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip }));
|
|
6938
|
+
return {
|
|
6939
|
+
command: "stage --live",
|
|
6940
|
+
mode: "up",
|
|
6941
|
+
slug: t.slug,
|
|
6942
|
+
repo: t.repo,
|
|
6943
|
+
ref: t.ref,
|
|
6944
|
+
ip,
|
|
6945
|
+
dispatched: ["tenant-deploy.yml", "tenant-control.yml"],
|
|
6946
|
+
message: `dispatched the dev deploy of ${t.ref} and the allowlist update for ${ip}; watch the runs in ${STAGE_LIVE_HUB_REPO} Actions \u2014 tear down with: mmi-cli stage --live --down --apply`
|
|
6947
|
+
};
|
|
6948
|
+
}
|
|
6949
|
+
async function runStageLiveDown(deps, t) {
|
|
6950
|
+
await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }));
|
|
6951
|
+
await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip: "clear" }));
|
|
6952
|
+
return {
|
|
6953
|
+
command: "stage --live",
|
|
6954
|
+
mode: "down",
|
|
6955
|
+
slug: t.slug,
|
|
6956
|
+
repo: t.repo,
|
|
6957
|
+
dispatched: ["tenant-control.yml", "tenant-control.yml"],
|
|
6958
|
+
message: `dispatched the dev stop and allowlist clear for ${t.slug}; the dev stage is dark until the next mmi-cli stage --live --apply`
|
|
6959
|
+
};
|
|
6960
|
+
}
|
|
6961
|
+
|
|
6865
6962
|
// src/stage-runner.ts
|
|
6866
6963
|
var import_node_child_process6 = require("node:child_process");
|
|
6867
6964
|
var import_node_fs8 = require("node:fs");
|
|
6868
6965
|
var import_node_path8 = require("node:path");
|
|
6869
|
-
var
|
|
6966
|
+
var import_node_net2 = require("node:net");
|
|
6870
6967
|
var import_node_util6 = require("node:util");
|
|
6871
6968
|
var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process6.execFile);
|
|
6872
6969
|
function stageStatePath(cwd = process.cwd()) {
|
|
@@ -6916,7 +7013,7 @@ function pickStagePort(range, isFree) {
|
|
|
6916
7013
|
}
|
|
6917
7014
|
function isPortFree(port) {
|
|
6918
7015
|
return new Promise((resolve) => {
|
|
6919
|
-
const srv = (0,
|
|
7016
|
+
const srv = (0, import_node_net2.createServer)();
|
|
6920
7017
|
srv.once("error", () => resolve(false));
|
|
6921
7018
|
srv.once("listening", () => srv.close(() => resolve(true)));
|
|
6922
7019
|
srv.listen(port, "127.0.0.1");
|
|
@@ -7642,9 +7739,93 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
7642
7739
|
rcRetirement: "not-applicable",
|
|
7643
7740
|
rcRetirementNote: "direct-track release skips rc; no rc runtime to retire",
|
|
7644
7741
|
announceNote: announceNote2,
|
|
7742
|
+
// #1062: --dev on a direct-track repo is a friendly no-op — it already releases from development.
|
|
7743
|
+
devNote: options.dev ? "--dev is a no-op on a direct-track repo \u2014 it already releases development -> main" : void 0,
|
|
7645
7744
|
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
|
|
7646
7745
|
};
|
|
7647
7746
|
}
|
|
7747
|
+
if (command === "release" && options.dev) {
|
|
7748
|
+
await requireBranch(deps, "development");
|
|
7749
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
7750
|
+
const rcOnlyOut = (await deps.run("git", ["rev-list", "--count", "--right-only", "--cherry-pick", "--no-merges", "origin/development...origin/rc"])).trim();
|
|
7751
|
+
const rcOnly = Number.parseInt(rcOnlyOut, 10);
|
|
7752
|
+
if (!Number.isFinite(rcOnly)) throw new Error(`release --dev: could not count rc-only commits for the guard: ${rcOnlyOut || "(empty)"}`);
|
|
7753
|
+
if (rcOnly > 0) {
|
|
7754
|
+
throw new Error(
|
|
7755
|
+
`release --dev refused: origin/rc carries ${rcOnly} commit(s) not in origin/development \u2014 a development -> main release would drop that rc-only content. Land it on development first, or release the candidate via the default rc -> main path, then rerun.`
|
|
7756
|
+
);
|
|
7757
|
+
}
|
|
7758
|
+
ensurePositiveCount(
|
|
7759
|
+
await deps.run("git", ["rev-list", "--count", "origin/main..origin/development"]),
|
|
7760
|
+
"nothing to release: origin/development is not ahead of origin/main"
|
|
7761
|
+
);
|
|
7762
|
+
const deployModel2 = await preflight(deps, ctx, "main", meta);
|
|
7763
|
+
const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
|
|
7764
|
+
const rcShaAtRelease = clean(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
7765
|
+
const foldPaths2 = await resolveFoldPaths(deps, deployModel2);
|
|
7766
|
+
const tolerated2 = [...foldPaths2, ...RELEASE_TOLERATED_PATHS];
|
|
7767
|
+
const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
|
|
7768
|
+
const predictedBlocking2 = predicted2.filter((f) => !isSpinePath(f) && !tolerated2.includes(f));
|
|
7769
|
+
if (predictedBlocking2.length > 0) {
|
|
7770
|
+
throw new Error(
|
|
7771
|
+
`development -> main merge would conflict on non-spine path(s): ${predictedBlocking2.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release.`
|
|
7772
|
+
);
|
|
7773
|
+
}
|
|
7774
|
+
await deps.run("git", ["checkout", "main"]);
|
|
7775
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "main"]);
|
|
7776
|
+
if (predicted2.length === 0) {
|
|
7777
|
+
await deps.run("git", ["merge", "development", "--no-edit"]);
|
|
7778
|
+
} else {
|
|
7779
|
+
await mergeWithSpineResolution(deps, "development", "development -> main", "theirs", tolerated2);
|
|
7780
|
+
}
|
|
7781
|
+
const versionFold2 = await foldReleaseVersion(deps, deployModel2, tag2, foldPaths2);
|
|
7782
|
+
const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
|
|
7783
|
+
await ensureTagPushed(deps, tag2, releaseSha2);
|
|
7784
|
+
const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
|
|
7785
|
+
const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
|
|
7786
|
+
await deps.run("git", ["push", "origin", "main"]);
|
|
7787
|
+
const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
|
|
7788
|
+
const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
|
|
7789
|
+
const autoRunSince2 = (deps.now ?? Date.now)();
|
|
7790
|
+
const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2);
|
|
7791
|
+
const retirement2 = await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease);
|
|
7792
|
+
await deps.run("git", ["checkout", "development"]);
|
|
7793
|
+
await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
|
|
7794
|
+
await deps.run("git", ["merge", "main", "--no-edit"]);
|
|
7795
|
+
await deps.run("git", ["push", "origin", "development"]);
|
|
7796
|
+
let rcAlignment2;
|
|
7797
|
+
try {
|
|
7798
|
+
await deps.run("git", ["push", "origin", "main:rc"]);
|
|
7799
|
+
rcAlignment2 = "origin/rc aligned to the released main";
|
|
7800
|
+
} catch (e) {
|
|
7801
|
+
rcAlignment2 = `rc alignment push failed \u2014 align manually with \`git push origin main:rc\`: ${e instanceof Error ? e.message : String(e)}`;
|
|
7802
|
+
}
|
|
7803
|
+
const environments2 = await buildEnvironments(deps, ctx, deployModel2, d2.deployStatus, retirement2);
|
|
7804
|
+
return {
|
|
7805
|
+
...ctx,
|
|
7806
|
+
command,
|
|
7807
|
+
stage: "main",
|
|
7808
|
+
ref: "main",
|
|
7809
|
+
tag: tag2,
|
|
7810
|
+
deployModel: deployModel2,
|
|
7811
|
+
promoted: true,
|
|
7812
|
+
checks: checks2,
|
|
7813
|
+
versionFold: versionFold2,
|
|
7814
|
+
dispatch: d2.note,
|
|
7815
|
+
runId: d2.runId,
|
|
7816
|
+
runUrl: d2.runUrl,
|
|
7817
|
+
workflowRuns: d2.workflowRuns,
|
|
7818
|
+
deployStatus: d2.deployStatus,
|
|
7819
|
+
rcRetirement: retirement2.status,
|
|
7820
|
+
rcRetirementNote: retirement2.note,
|
|
7821
|
+
rcRetirementCategory: retirement2.category,
|
|
7822
|
+
rcAlignment: rcAlignment2,
|
|
7823
|
+
announceNote: announceNote2,
|
|
7824
|
+
devNote: "released development -> main (--dev), skipping the rc candidate",
|
|
7825
|
+
release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 },
|
|
7826
|
+
environments: environments2
|
|
7827
|
+
};
|
|
7828
|
+
}
|
|
7648
7829
|
await requireBranch(deps, "rc");
|
|
7649
7830
|
ensurePositiveCount(
|
|
7650
7831
|
await deps.run("git", ["rev-list", "--count", "origin/main..origin/rc"]),
|
|
@@ -12550,7 +12731,39 @@ program2.command("port-range <repo>").description("assign (idempotently) + print
|
|
|
12550
12731
|
printLine(`${repo}: stage.portRange [${start}, ${end}]${write.ok ? "" : ` (META not persisted: ${write.error ?? `HTTP ${write.status}`})`}`);
|
|
12551
12732
|
}
|
|
12552
12733
|
});
|
|
12553
|
-
|
|
12734
|
+
async function stageLiveTarget() {
|
|
12735
|
+
const repo = await resolveRepo();
|
|
12736
|
+
if (!repo) throw new Error("stage --live: cannot resolve the current repo (run inside a GitHub-remoted checkout)");
|
|
12737
|
+
const ref = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
|
|
12738
|
+
return { slug: slugOf(repo), repo, ref };
|
|
12739
|
+
}
|
|
12740
|
+
async function runStageLiveCommand(o) {
|
|
12741
|
+
let target;
|
|
12742
|
+
try {
|
|
12743
|
+
target = await stageLiveTarget();
|
|
12744
|
+
} catch (e) {
|
|
12745
|
+
return failGraceful(e.message);
|
|
12746
|
+
}
|
|
12747
|
+
const mode = o.down ? "down" : "up";
|
|
12748
|
+
if (!o.apply) {
|
|
12749
|
+
const steps = o.down ? stageLiveDownSteps(target) : stageLiveUpSteps(target);
|
|
12750
|
+
if (o.json) return console.log(JSON.stringify({ command: "stage --live", mode, slug: target.slug, repo: target.repo, ref: o.down ? void 0 : target.ref, steps }, null, 2));
|
|
12751
|
+
return console.log(renderSteps(`mmi-cli stage --live${o.down ? " --down" : ""}: dry-run plan`, steps));
|
|
12752
|
+
}
|
|
12753
|
+
const deps = {
|
|
12754
|
+
detectIp: () => detectPublicIp(),
|
|
12755
|
+
run: async (file, args) => (await execFileP2(file, args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout
|
|
12756
|
+
};
|
|
12757
|
+
try {
|
|
12758
|
+
const result = o.down ? await runStageLiveDown(deps, target) : await runStageLiveUp(deps, target);
|
|
12759
|
+
return printLine(o.json ? JSON.stringify(result) : `mmi-cli stage --live: ${result.message}`);
|
|
12760
|
+
} catch (e) {
|
|
12761
|
+
return failGraceful(`stage --live: ${e.message}`);
|
|
12762
|
+
}
|
|
12763
|
+
}
|
|
12764
|
+
var stage = program2.command("stage").description("plan or run the repo local stage environment; --live = personal IP-gated cloud dev stage").option("--json", "machine-readable output").option("--apply", "run the full local stage: stop previous, build, start, health-check").option("--live", "personal cloud dev stage: deploy the current branch to the dev runtime, served only to your public IP").option("--down", "with --live: stop the dev runtime and clear the IP allowlist").option("--timeout-ms <ms>", "bounded build/health timeout", "60000").action(async (o) => {
|
|
12765
|
+
if (o.down && !o.live) return fail("stage: --down applies to --live only (local teardown is `mmi-cli stage stop --apply`)");
|
|
12766
|
+
if (o.live) return runStageLiveCommand(o);
|
|
12554
12767
|
const res = await resolveStage();
|
|
12555
12768
|
if (o.apply) {
|
|
12556
12769
|
if (res.source === "none") return failGraceful(`stage: ${res.gap}`);
|
|
@@ -12718,6 +12931,7 @@ function renderTrainApply(commandName, r) {
|
|
|
12718
12931
|
let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2192 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
12719
12932
|
if (r.versionFold) base = `${base}; ${r.versionFold}`;
|
|
12720
12933
|
if (r.resumeNote) base = `${base}; ${r.resumeNote}`;
|
|
12934
|
+
if (r.devNote) base = `${base}; ${r.devNote}`;
|
|
12721
12935
|
if (r.rcRetirement) base = `${base}; rc retirement: ${r.rcRetirement.toUpperCase()} (${r.rcRetirementNote ?? ""})`;
|
|
12722
12936
|
return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
|
|
12723
12937
|
}
|
|
@@ -12733,7 +12947,7 @@ async function resolveRcandPlanTargets() {
|
|
|
12733
12947
|
}
|
|
12734
12948
|
}
|
|
12735
12949
|
for (const commandName of ["rcand", "release"]) {
|
|
12736
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").action(async (o) => {
|
|
12950
|
+
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").option("--ack <shas>", "release only: comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)").option("--dev", "release only: full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)").action(async (o) => {
|
|
12737
12951
|
try {
|
|
12738
12952
|
await requireFreshTrainCli(commandName);
|
|
12739
12953
|
} catch (e) {
|
|
@@ -12742,10 +12956,13 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
12742
12956
|
if (o.ack && commandName !== "release") {
|
|
12743
12957
|
return fail("--ack applies only to release: it overrides the rc -> main hotfix-coverage guard, which rcand does not run");
|
|
12744
12958
|
}
|
|
12959
|
+
if (o.dev && commandName !== "release") {
|
|
12960
|
+
return fail("--dev applies only to release: it ships development -> main skipping rc, which rcand cannot do");
|
|
12961
|
+
}
|
|
12745
12962
|
if (o.apply) {
|
|
12746
12963
|
try {
|
|
12747
12964
|
const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
12748
|
-
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack });
|
|
12965
|
+
const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev });
|
|
12749
12966
|
return printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainApply(commandName, result));
|
|
12750
12967
|
} catch (e) {
|
|
12751
12968
|
return failGraceful(`${commandName}: ${e.message}`);
|
|
@@ -12753,7 +12970,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
12753
12970
|
}
|
|
12754
12971
|
const repo = await resolveRepo();
|
|
12755
12972
|
const targets = commandName === "rcand" ? await resolveRcandPlanTargets() : void 0;
|
|
12756
|
-
const steps = trainPlan(commandName, { ...targets ?? {}, repo });
|
|
12973
|
+
const steps = trainPlan(commandName, { ...targets ?? {}, repo, dev: o.dev });
|
|
12757
12974
|
console.log(
|
|
12758
12975
|
o.json ? JSON.stringify({ command: commandName, ...targets ?? {}, repo, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps)
|
|
12759
12976
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.22.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",
|