@mutmutco/cli 3.11.0 → 3.12.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 +42 -12
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -4957,7 +4957,7 @@ function parseIssueSelector(selector, defaultRepo) {
|
|
|
4957
4957
|
throw new Error(`expected an issue selector like 123, #123, owner/repo#123, or a GitHub issue URL`);
|
|
4958
4958
|
}
|
|
4959
4959
|
function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
|
|
4960
|
-
const empty = () => ({ userOwned: [], claimable: [], taken: [] });
|
|
4960
|
+
const empty = () => ({ userOwned: [], claimable: [], taken: [], unownedInFlight: [] });
|
|
4961
4961
|
const groups = { primary: empty(), secondary: empty() };
|
|
4962
4962
|
const viewerKey = viewer.toLowerCase();
|
|
4963
4963
|
const repoKey = currentRepo.toLowerCase();
|
|
@@ -4972,6 +4972,8 @@ function partitionBoardItems(items, viewer, currentRepo, writableRepos) {
|
|
|
4972
4972
|
groups[scope].claimable.push(item);
|
|
4973
4973
|
} else if (item.assignees.length > 0) {
|
|
4974
4974
|
groups[scope].taken.push(item);
|
|
4975
|
+
} else {
|
|
4976
|
+
groups[scope].unownedInFlight.push(item);
|
|
4975
4977
|
}
|
|
4976
4978
|
}
|
|
4977
4979
|
sortBuckets(groups.primary);
|
|
@@ -5642,6 +5644,7 @@ function renderScope(lines, label, title, buckets) {
|
|
|
5642
5644
|
lines.push("", `${label} \xB7 ${title}`);
|
|
5643
5645
|
renderOwned(lines, buckets.userOwned);
|
|
5644
5646
|
renderClaimable(lines, buckets.claimable);
|
|
5647
|
+
renderUnowned(lines, buckets.unownedInFlight);
|
|
5645
5648
|
renderTaken(lines, buckets.taken);
|
|
5646
5649
|
}
|
|
5647
5650
|
function renderOwned(lines, items) {
|
|
@@ -5664,17 +5667,23 @@ function renderTaken(lines, items) {
|
|
|
5664
5667
|
lines.push("Taken");
|
|
5665
5668
|
for (const item of items) lines.push(` ${item.ref} \xB7 ${item.status} \xB7 @${item.assignees.join(", @")}`);
|
|
5666
5669
|
}
|
|
5670
|
+
function renderUnowned(lines, items) {
|
|
5671
|
+
if (!items.length) return;
|
|
5672
|
+
lines.push("Unowned in-flight (unassigned \u2014 needs an owner)");
|
|
5673
|
+
for (const item of items) lines.push(` ${item.status} \xB7 ${renderTitledItem(item)}`);
|
|
5674
|
+
}
|
|
5667
5675
|
function renderTitledItem(item) {
|
|
5668
5676
|
const pri = item.priority ? ` \xB7 ${item.priority}` : "";
|
|
5669
5677
|
return `${item.ref} - [${item.type ?? "item"}]${pri} ${item.title}`;
|
|
5670
5678
|
}
|
|
5671
5679
|
function hasItems(buckets) {
|
|
5672
|
-
return buckets.userOwned.length > 0 || buckets.claimable.length > 0 || buckets.taken.length > 0;
|
|
5680
|
+
return buckets.userOwned.length > 0 || buckets.claimable.length > 0 || buckets.taken.length > 0 || buckets.unownedInFlight.length > 0;
|
|
5673
5681
|
}
|
|
5674
5682
|
function sortBuckets(buckets) {
|
|
5675
5683
|
buckets.userOwned.sort(compareItems);
|
|
5676
5684
|
buckets.claimable.sort(compareItems);
|
|
5677
5685
|
buckets.taken.sort(compareItems);
|
|
5686
|
+
buckets.unownedInFlight.sort(compareItems);
|
|
5678
5687
|
}
|
|
5679
5688
|
function compareItems(a, b) {
|
|
5680
5689
|
return (STATUS_ORDER.get(a.status) ?? 99) - (STATUS_ORDER.get(b.status) ?? 99) || a.repository.localeCompare(b.repository) || a.number - b.number;
|
|
@@ -8783,6 +8792,7 @@ async function startStage(config = {}, opts = {}) {
|
|
|
8783
8792
|
if (!opts.envPrepared) await ensureStageRuntimeEnv(config, opts, cwd);
|
|
8784
8793
|
if (stagePort != null && portGuard) await ensureStagePortAvailable(stagePort, cwd, portGuard);
|
|
8785
8794
|
const extraEnv = stageExtraEnv(config, stagePort);
|
|
8795
|
+
const vaultProcessEnv = opts.vaultEnvMerge ?? {};
|
|
8786
8796
|
let up = sub(config.up.trim());
|
|
8787
8797
|
if (opts.forceRecreate) up = appendForceRecreate(up);
|
|
8788
8798
|
const identity = await resolveStageIdentity(cwd);
|
|
@@ -8795,7 +8805,8 @@ async function startStage(config = {}, opts = {}) {
|
|
|
8795
8805
|
detached: process.platform !== "win32",
|
|
8796
8806
|
windowsHide: true,
|
|
8797
8807
|
stdio: "ignore",
|
|
8798
|
-
|
|
8808
|
+
// Vault secrets first so the stage-selection contract (MMI_STAGE/MMI_PORT/…) always wins on any collision.
|
|
8809
|
+
env: { ...process.env, ...vaultProcessEnv, ...stageProcessEnv(stagePort, extraEnv) }
|
|
8799
8810
|
});
|
|
8800
8811
|
const state = {
|
|
8801
8812
|
pid: child.pid ?? 0,
|
|
@@ -9082,12 +9093,13 @@ function parseAddedItemId(stdout) {
|
|
|
9082
9093
|
function isAlreadyOnBoardError(stderr) {
|
|
9083
9094
|
return /already exists in (?:this|the) project/i.test(stderr);
|
|
9084
9095
|
}
|
|
9085
|
-
function buildPrArgs({ title, body, base, head, repo }) {
|
|
9096
|
+
function buildPrArgs({ title, body, base, head, repo, draft }) {
|
|
9086
9097
|
const args = ["pr", "create"];
|
|
9087
9098
|
if (repo) args.push("--repo", repo);
|
|
9088
9099
|
args.push("--title", title, "--body", body);
|
|
9089
9100
|
if (base) args.push("--base", base);
|
|
9090
9101
|
if (head) args.push("--head", head);
|
|
9102
|
+
if (draft) args.push("--draft");
|
|
9091
9103
|
return args;
|
|
9092
9104
|
}
|
|
9093
9105
|
|
|
@@ -9782,7 +9794,6 @@ function deriveStageGap(inputs) {
|
|
|
9782
9794
|
return `local stage default applies to central-container repos only (tenant-container/solo-container; registry deployModel = ${inputs.deployModel ?? "unset"})`;
|
|
9783
9795
|
}
|
|
9784
9796
|
if (!inputs.hasCompose) missing.push("docker-compose.yml");
|
|
9785
|
-
if (!inputs.hasEnvExample) missing.push(".env.example");
|
|
9786
9797
|
if (!inputs.portRange) missing.push("Hub registry portRange");
|
|
9787
9798
|
return missing.length ? `cannot derive a default local stage \u2014 missing: ${missing.join(", ")}` : null;
|
|
9788
9799
|
}
|
|
@@ -9798,7 +9809,9 @@ function deriveStage(inputs) {
|
|
|
9798
9809
|
// answered" — any HTTP status — is the health signal, not a 2xx on the bare root.
|
|
9799
9810
|
healthAnyStatus: true,
|
|
9800
9811
|
portRange: inputs.portRange,
|
|
9801
|
-
|
|
9812
|
+
// #2655: only bootstrap a `.env` when the repo ships `.env.example` (legacy). Without it, the stage is
|
|
9813
|
+
// vault-native — the runner injects the declared dev secrets into the compose process env (no disk file).
|
|
9814
|
+
...inputs.hasEnvExample ? { ensureEnv: { example: ".env.example", target: ".env" } } : {},
|
|
9802
9815
|
// MMI_PORT is the host-published port (the free STAGE_PORT); PORT is intentionally absent so compose
|
|
9803
9816
|
// reads the container's bind port from .env rather than this overriding it to the publish port (#1505).
|
|
9804
9817
|
env: {
|
|
@@ -9876,7 +9889,9 @@ async function runStageLiveUp(deps, t) {
|
|
|
9876
9889
|
ref: t.ref,
|
|
9877
9890
|
ip,
|
|
9878
9891
|
dispatched: ["tenant-deploy.yml", "tenant-control.yml"],
|
|
9879
|
-
|
|
9892
|
+
// #2656: the gate now writes a skip+block pair and self-verifies from the runner (a non-allowed vantage),
|
|
9893
|
+
// so a gate that failed to close fails the run RED. The stage is private only once that gate run is green.
|
|
9894
|
+
message: `dispatched the dev deploy of ${t.ref} and the Cloudflare edge gate for ${t.host} \u2192 ${ip}; watch the runs in ${STAGE_LIVE_HUB_REPO} Actions and treat the stage as private ONLY once the cf-gate-allow run is green (it self-verifies the host is blocked) \u2014 tear down with: mmi-cli stage --live --down --apply`
|
|
9880
9895
|
};
|
|
9881
9896
|
}
|
|
9882
9897
|
async function runStageLiveDown(deps, t) {
|
|
@@ -10545,7 +10560,16 @@ async function probeRemoteTag(deps, tag) {
|
|
|
10545
10560
|
const remoteOut = await runGitRemoteRead(deps, ["ls-remote", "origin", `refs/tags/${tag}`]);
|
|
10546
10561
|
return clean2(remoteOut).split(/\s+/)[0] || "";
|
|
10547
10562
|
}
|
|
10548
|
-
async function
|
|
10563
|
+
async function isStrayUnreleasedTag(deps, tag, remoteSha, repo) {
|
|
10564
|
+
try {
|
|
10565
|
+
await deps.run("git", ["merge-base", "--is-ancestor", remoteSha, "origin/main"]);
|
|
10566
|
+
return false;
|
|
10567
|
+
} catch (e) {
|
|
10568
|
+
if (e.code !== 1) return false;
|
|
10569
|
+
}
|
|
10570
|
+
return deps.run("gh", ["release", "view", tag, "--repo", repo, "--json", "tagName"]).then(() => false).catch((e) => /not found|HTTP 404/i.test(e instanceof Error ? e.message : String(e)));
|
|
10571
|
+
}
|
|
10572
|
+
async function ensureTagPushed(deps, tag, sha, probed, releaseRepo) {
|
|
10549
10573
|
const remoteSha = probed ? probed.remoteSha : await probeRemoteTag(deps, tag);
|
|
10550
10574
|
let localSha = "";
|
|
10551
10575
|
try {
|
|
@@ -10554,8 +10578,14 @@ async function ensureTagPushed(deps, tag, sha, probed) {
|
|
|
10554
10578
|
}
|
|
10555
10579
|
if (remoteSha) {
|
|
10556
10580
|
if (remoteSha !== sha) {
|
|
10581
|
+
const mismatch = `tag ${tag} already exists on origin at ${remoteSha}, but this run intends ${sha}`;
|
|
10582
|
+
if (releaseRepo && await isStrayUnreleasedTag(deps, tag, remoteSha, releaseRepo)) {
|
|
10583
|
+
throw new Error(
|
|
10584
|
+
`${mismatch}. The existing tag is not reachable from origin/main and has no GitHub Release \u2014 it was pushed outside the train, not by a completed release. Sanctioned recovery: delete the stray tag (git push origin --delete ${tag}; git tag -d ${tag} if it exists locally), check the repo's Actions for any workflow the stray tag already triggered, then rerun mmi-cli release --apply (if a publish already ran off the stray tag, mint the next version instead of reusing this one). Never complete the release by hand \u2014 a manual GitHub Release or branch push bypasses the train and leaves main behind.`
|
|
10585
|
+
);
|
|
10586
|
+
}
|
|
10557
10587
|
throw new Error(
|
|
10558
|
-
|
|
10588
|
+
`${mismatch} \u2014 refusing to touch a pushed tag. Repair manually: either release the existing tagged commit (reset the local branch to ${remoteSha} and rerun), or mint the next candidate version so a fresh tag is used. Never force-move or delete a pushed tag.`
|
|
10559
10589
|
);
|
|
10560
10590
|
}
|
|
10561
10591
|
if (localSha && localSha !== sha) {
|
|
@@ -10922,7 +10952,7 @@ async function runFoldStage(deps, startBranch, preFold, fn) {
|
|
|
10922
10952
|
async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha, startBranch, preFold, tagProbe) {
|
|
10923
10953
|
let tagPush;
|
|
10924
10954
|
try {
|
|
10925
|
-
tagPush = await ensureTagPushed(deps, tag, releaseSha, tagProbe);
|
|
10955
|
+
tagPush = await ensureTagPushed(deps, tag, releaseSha, tagProbe, ctx.repo);
|
|
10926
10956
|
} catch (e) {
|
|
10927
10957
|
throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha);
|
|
10928
10958
|
}
|
|
@@ -20963,7 +20993,7 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
20963
20993
|
console.log(JSON.stringify({ ...created, deduped: false, label: SKILL_LESSON_LABEL, skill, priority, projectItemId, onBoard }));
|
|
20964
20994
|
});
|
|
20965
20995
|
var pr = program2.command("pr").description("pull requests \u2014 reliable create with structured output");
|
|
20966
|
-
pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
|
|
20996
|
+
pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--draft", "open the PR in draft state (#2667)").action(async (o) => {
|
|
20967
20997
|
let body;
|
|
20968
20998
|
let title;
|
|
20969
20999
|
try {
|
|
@@ -20972,7 +21002,7 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
20972
21002
|
} catch (e) {
|
|
20973
21003
|
return fail(`pr create: ${e.message}`);
|
|
20974
21004
|
}
|
|
20975
|
-
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
21005
|
+
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
20976
21006
|
console.log(JSON.stringify(created));
|
|
20977
21007
|
});
|
|
20978
21008
|
pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').action(async (number, o) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.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",
|