@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.
Files changed (2) hide show
  1. package/dist/main.cjs +42 -12
  2. 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
- env: { ...process.env, ...stageProcessEnv(stagePort, extraEnv) }
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
- ensureEnv: { example: ".env.example", target: ".env" },
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
- 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 \u2014 tear down with: mmi-cli stage --live --down --apply`
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 ensureTagPushed(deps, tag, sha, probed) {
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
- `tag ${tag} already exists on origin at ${remoteSha}, but this run intends ${sha} \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.`
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.11.0",
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",