@mutmutco/cli 4.3.39 → 4.3.40

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 (3) hide show
  1. package/README.md +6 -4
  2. package/dist/main.cjs +47 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,21 +6,23 @@ This package is published from [mutmutco/MMI-Hub](https://github.com/mutmutco/MM
6
6
 
7
7
  The CLI carries the org **Hub endpoint** intrinsically (override with the `MMI_HUB_URL` env var), so a product repo needs **no committed control-plane config** to reach the Hub — board coords, deploy coordinates, OAuth, and the secrets layout are all discovered from the Hub registry at runtime.
8
8
 
9
+ Local stage recipes and printed plans use Bash on every platform. Windows requires Git Bash in its standard system or user installation, or `SHELL` set to the absolute `bash.exe` path. A missing Bash installation fails explicitly. Native Windows administration helpers remain independent of the stage shell.
10
+
9
11
  ## Install
10
12
 
11
- ```powershell
13
+ ```bash
12
14
  npm install -g @mutmutco/cli
13
15
  ```
14
16
 
15
17
  Authenticate GitHub once for Hub session issuance and Project board operations:
16
18
 
17
- ```powershell
19
+ ```bash
18
20
  gh auth login --hostname github.com --git-protocol https --web --scopes "project"
19
21
  ```
20
22
 
21
23
  Then verify the installed command:
22
24
 
23
- ```powershell
25
+ ```bash
24
26
  mmi-cli --version
25
27
  mmi-cli doctor --json
26
28
  ```
@@ -79,7 +81,7 @@ they don't get re-derived or re-litigated per session:
79
81
 
80
82
  When working inside an `MMI-Hub` checkout before npm is available, use the committed bundle directly:
81
83
 
82
- ```powershell
84
+ ```bash
83
85
  node cli/dist/index.cjs --version
84
86
  node cli/dist/index.cjs doctor --json
85
87
  ```
package/dist/main.cjs CHANGED
@@ -12198,10 +12198,9 @@ async function postIssueComment(client, input) {
12198
12198
 
12199
12199
  // src/board-claim-move.ts
12200
12200
  var CLAIM_CONCURRENCY = 5;
12201
- function withPowerShellChainHint(message2, platform2 = process.platform) {
12202
- if (platform2 !== "win32") return message2;
12201
+ function withGatedWriteChainHint(message2) {
12203
12202
  return `${message2}
12204
- note: in PowerShell a ';' chain runs every statement regardless of the previous exit code, so the chain ran past this refusal \u2014 rerun the refused write solo or chain gated writes with '&&'`;
12203
+ note: run refused writes separately or join dependent commands with '&&'; a ';' chain continues after failure`;
12205
12204
  }
12206
12205
  function isArchivedItemRefusal(message2) {
12207
12206
  return /The item is archived and cannot be updated/i.test(message2);
@@ -12387,7 +12386,7 @@ async function claimOneBoardItem(ctx, selector, options) {
12387
12386
  }
12388
12387
  if (!options.force) {
12389
12388
  const refusal = laneContestMessage(item.ref, contest, "claim");
12390
- throw new Error(options.bulk ? refusal : withPowerShellChainHint(refusal));
12389
+ throw new Error(options.bulk ? refusal : withGatedWriteChainHint(refusal));
12391
12390
  }
12392
12391
  previousHolder = displaced();
12393
12392
  };
@@ -12571,7 +12570,7 @@ async function unclaimBoardIssue(options, deps = {}) {
12571
12570
  const toStatus = options.toStatus ?? "Todo";
12572
12571
  if (!options.force && item.contentType === "Issue") {
12573
12572
  const contest = await checkLaneContest(client, item);
12574
- if (contest.contested) throw new Error(withPowerShellChainHint(laneContestMessage(item.ref, contest, "unclaim")));
12573
+ if (contest.contested) throw new Error(withGatedWriteChainHint(laneContestMessage(item.ref, contest, "unclaim")));
12575
12574
  }
12576
12575
  try {
12577
12576
  await client.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
@@ -15695,10 +15694,10 @@ var rollout_plan_default = {
15695
15694
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
15696
15695
  },
15697
15696
  baseline: {
15698
- version: "4.3.39",
15699
- tag: "v4.3.39",
15700
- commit: "cc67ff24bbec",
15701
- npm: "@mutmutco/cli@4.3.39"
15697
+ version: "4.3.40",
15698
+ tag: "v4.3.40",
15699
+ commit: "487948fabf28",
15700
+ npm: "@mutmutco/cli@4.3.40"
15702
15701
  },
15703
15702
  exitCriterion: "fleet-n-of-n",
15704
15703
  hubOnlyShortcut: "forbidden",
@@ -15715,14 +15714,14 @@ var rollout_plan_default = {
15715
15714
  repo: "mutmutco/mmi-hub",
15716
15715
  role: "canary",
15717
15716
  schedule: "train",
15718
- v3Target: "v4.3.39"
15717
+ v3Target: "v4.3.40"
15719
15718
  }
15720
15719
  ],
15721
15720
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
15722
15721
  rollback: {
15723
15722
  independent: true,
15724
- mechanism: "npm dist-tag latest -> 4.3.39 and redeploy the Hub Lambda from tag v4.3.39 (cc67ff24bbec); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15725
- v3Target: "v4.3.39 (@mutmutco/cli@4.3.39, tag commit cc67ff24bbec \u2014 last known-good release carrying the repo-index v4-only contract)"
15723
+ mechanism: "npm dist-tag latest -> 4.3.40 and redeploy the Hub Lambda from tag v4.3.40 (487948fabf28); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15724
+ v3Target: "v4.3.40 (@mutmutco/cli@4.3.40, tag commit 487948fabf28 \u2014 last known-good release carrying the repo-index v4-only contract)"
15726
15725
  }
15727
15726
  },
15728
15727
  {
@@ -25957,7 +25956,7 @@ function registerBoardCommands(program3) {
25957
25956
  return `Claimed ${ref} for ${holder} - In Progress${reclaimed}`;
25958
25957
  }
25959
25958
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
25960
- board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nNever capture the JSON with a shell redirect on Windows: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
25959
+ board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
25961
25960
  withExamples(mutating(
25962
25961
  board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n\nreclaim from In Review (#6339): an In Review item with no holder, or one held by the claiming\nlogin, is claimable \u2014 it moves back to In Progress and the receipt names the status it came from.\nAn In Review item another login holds is refused (ask the holder, or wait for the review to land),\nand Done stays refused. The board status is the authority; no PR state is consulted.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
25963
25962
  (_opts, args) => ({ command: "oracle board claim", issues: args[0] ?? [] })
@@ -30097,6 +30096,18 @@ function stageComposeFileEnv(files) {
30097
30096
  // src/stage-runner.ts
30098
30097
  var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process11.execFile);
30099
30098
  var DOCKER_TIMEOUT_MS = 15e3;
30099
+ function stageBash() {
30100
+ if (process.platform !== "win32") return "bash";
30101
+ const configured = process.env.SHELL;
30102
+ if (configured && (0, import_node_path27.isAbsolute)(configured) && /[/\\]bash(?:\.exe)?$/i.test(configured) && !/[/\\](?:System32|Sysnative)[/\\]bash(?:\.exe)?$/i.test(configured) && (0, import_node_fs28.existsSync)(configured)) return configured;
30103
+ const candidates = [
30104
+ (0, import_node_path27.join)(process.env.ProgramFiles || "C:\\Program Files", "Git", "bin", "bash.exe"),
30105
+ ...process.env.LOCALAPPDATA ? [(0, import_node_path27.join)(process.env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe")] : []
30106
+ ];
30107
+ const bash = candidates.find((path2) => (0, import_node_fs28.existsSync)(path2));
30108
+ if (!bash) throw new Error("Local stages require Git Bash on Windows. Install Git for Windows or set SHELL to its absolute bash.exe path.");
30109
+ return bash;
30110
+ }
30100
30111
  var EARLY_EXIT_GRACE_MS = 2e3;
30101
30112
  function earlyExitGraceMs() {
30102
30113
  if (process.env.NODE_ENV === "test") {
@@ -30287,24 +30298,6 @@ function mergeEnvSecretsIntoFile(content, secrets) {
30287
30298
  return body.endsWith("\n") ? body : `${body}
30288
30299
  `;
30289
30300
  }
30290
- var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
30291
- function posixOnlyShellProblems(command, field, platform2 = process.platform) {
30292
- if (platform2 !== "win32" || !command?.trim()) return [];
30293
- const problems = [];
30294
- if (/(^|&&|\||;)\s*[A-Za-z_][A-Za-z0-9_]*=\S/.test(command)) {
30295
- problems.push(
30296
- `stage.${field} uses POSIX inline env assignment (VAR=value command) which fails in cmd.exe on Windows; use 'set VAR=value && command' or a cross-platform launcher`
30297
- );
30298
- }
30299
- for (const verb of POSIX_ONLY_VERBS) {
30300
- if (new RegExp(`(^|&&|\\||;|\\()\\s*${verb}\\b`).test(command)) {
30301
- problems.push(
30302
- `stage.${field} calls POSIX '${verb}' which does not exist in cmd.exe on Windows; use the cmd/PowerShell equivalent or a cross-platform script`
30303
- );
30304
- }
30305
- }
30306
- return problems;
30307
- }
30308
30301
  function validateStageConfig(config = {}, action) {
30309
30302
  const problems = [];
30310
30303
  if (action === "run" && !config.build?.trim() && !config.ensureEnv) problems.push("stage.build is required for stage run");
@@ -30312,8 +30305,6 @@ function validateStageConfig(config = {}, action) {
30312
30305
  if (config.healthUrl != null && config.healthUrl.trim() && !/^https?:\/\//.test(config.healthUrl.trim())) {
30313
30306
  problems.push("stage.healthUrl must be an http(s) URL");
30314
30307
  }
30315
- if (action === "run") problems.push(...posixOnlyShellProblems(config.build, "build"));
30316
- problems.push(...posixOnlyShellProblems(config.up, "up"));
30317
30308
  if (config.portRange != null) {
30318
30309
  const r = config.portRange;
30319
30310
  const ok = Array.isArray(r) && r.length === 2 && r.every((n) => Number.isInteger(n) && n >= 1024 && n <= 65535) && r[0] <= r[1];
@@ -30338,14 +30329,27 @@ function isPortFree(port) {
30338
30329
  });
30339
30330
  }
30340
30331
  async function shell(command, cwd, timeoutMs, env) {
30341
- await execFileP3(command, [], {
30332
+ const execution = execFileP3(stageBash(), ["-c", command], {
30342
30333
  cwd,
30343
- shell: true,
30344
- timeout: timeoutMs,
30334
+ timeout: process.platform === "win32" ? timeoutMs + 5e3 : timeoutMs,
30345
30335
  windowsHide: true,
30346
30336
  maxBuffer: 1024 * 1024 * 4,
30347
30337
  ...env ? { env: { ...process.env, ...env } } : {}
30348
30338
  });
30339
+ let cleanup;
30340
+ const timer = process.platform === "win32" ? setTimeout(() => {
30341
+ cleanup = killTree(execution.child.pid ?? 0);
30342
+ }, timeoutMs) : void 0;
30343
+ try {
30344
+ await execution;
30345
+ if (cleanup) throw new Error(`stage command timed out after ${timeoutMs}ms`);
30346
+ } catch (error) {
30347
+ if (cleanup) throw new Error(`stage command timed out after ${timeoutMs}ms`, { cause: error });
30348
+ throw error;
30349
+ } finally {
30350
+ clearTimeout(timer);
30351
+ await cleanup;
30352
+ }
30349
30353
  }
30350
30354
  async function listDockerContainers() {
30351
30355
  const { stdout } = await execFileP3("docker", ["container", "ls", "--format", "{{json .}}"], {
@@ -30585,7 +30589,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd, currentEn
30585
30589
  async function killTree(pid) {
30586
30590
  if (!Number.isInteger(pid) || pid <= 0) return;
30587
30591
  if (process.platform === "win32") {
30588
- await execFileP3("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }).catch(() => void 0);
30592
+ await execFileP3("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true, timeout: 5e3 }).catch(() => void 0);
30589
30593
  return;
30590
30594
  }
30591
30595
  try {
@@ -30651,6 +30655,7 @@ async function stopStage(opts = {}) {
30651
30655
  };
30652
30656
  }
30653
30657
  async function startStage(config = {}, opts = {}) {
30658
+ const bash = stageBash();
30654
30659
  const problems = validateStageConfig(config, "start");
30655
30660
  if (problems.length) throw new Error(problems.join("; "));
30656
30661
  const cwd = opts.cwd ?? process.cwd();
@@ -30672,9 +30677,8 @@ async function startStage(config = {}, opts = {}) {
30672
30677
  let up = sub(config.up.trim());
30673
30678
  if (opts.forceRecreate) up = appendForceRecreate(up);
30674
30679
  const identity = await resolveStageIdentity(cwd);
30675
- const child2 = (0, import_node_child_process11.spawn)(up, {
30680
+ const child2 = (0, import_node_child_process11.spawn)(bash, ["-c", up], {
30676
30681
  cwd,
30677
- shell: true,
30678
30682
  // POSIX-only: the process group exists for the group-kill in stopStage. On win32 teardown is
30679
30683
  // `taskkill /T /F` (no group needed), and detached+shell defeats windowsHide — every spawn would
30680
30684
  // flash a Windows Terminal window (0x800700e8) on dev machines.
@@ -30728,6 +30732,7 @@ function stageEnvRefusal(cwd) {
30728
30732
  return `stage refuses to run: .env file(s) present in the repo (${envFiles.join(", ")}) \u2014 secrets belong in the Hub vault; delete the file(s) and re-run (#5908)`;
30729
30733
  }
30730
30734
  async function runStage(config = {}, opts = {}) {
30735
+ stageBash();
30731
30736
  const problems = validateStageConfig(config, "run");
30732
30737
  if (problems.length) throw new Error(problems.join("; "));
30733
30738
  const cwd = opts.cwd ?? process.cwd();
@@ -36038,8 +36043,8 @@ var import_node_fs34 = require("node:fs");
36038
36043
  var import_node_path31 = require("node:path");
36039
36044
 
36040
36045
  // src/stage-default.ts
36041
- function shellFor(platform2 = process.platform) {
36042
- return platform2 === "win32" ? "powershell" : "bash";
36046
+ function shellFor() {
36047
+ return "bash";
36043
36048
  }
36044
36049
  function isCentralContainerModel(model) {
36045
36050
  return model === "tenant-container" || model === "solo-container";
@@ -36510,7 +36515,7 @@ function registerStageCommands(program3) {
36510
36515
  fail(`stage stop: ${e.message}`);
36511
36516
  }
36512
36517
  });
36513
- stage.command("start").description("start the configured local stage process and optionally wait for health").option("--json", "machine-readable output").option("--apply", "start the configured stage.up process").option("--port <port>", "loopback port for this worktree stage (1024..65535)").option("--timeout-ms <ms>", "bounded health timeout", "60000").option("--allow-stale-env", "start despite a stale ensureEnv target file").action(async () => {
36518
+ stage.command("start").description("start the configured local stage process and optionally wait for health").option("--json", "machine-readable output").option("--apply", "start the configured stage.up process in Bash (Git Bash on Windows)").option("--port <port>", "loopback port for this worktree stage (1024..65535)").option("--timeout-ms <ms>", "bounded health timeout", "60000").option("--allow-stale-env", "start despite a stale ensureEnv target file").action(async () => {
36514
36519
  const o = { json: rawFlag("--json"), apply: rawFlag("--apply"), timeoutMs: rawValue("--timeout-ms", "60000"), allowStaleEnv: rawFlag("--allow-stale-env") };
36515
36520
  const { resolution: res, project: project2, cfg: stageCfg } = await resolveStage();
36516
36521
  if (!o.apply) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.3.39",
3
+ "version": "4.3.40",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",