@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.
- package/README.md +6 -4
- package/dist/main.cjs +47 -42
- 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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
|
12202
|
-
if (platform2 !== "win32") return message2;
|
|
12201
|
+
function withGatedWriteChainHint(message2) {
|
|
12203
12202
|
return `${message2}
|
|
12204
|
-
note:
|
|
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 :
|
|
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(
|
|
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.
|
|
15699
|
-
tag: "v4.3.
|
|
15700
|
-
commit: "
|
|
15701
|
-
npm: "@mutmutco/cli@4.3.
|
|
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.
|
|
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.
|
|
15725
|
-
v3Target: "v4.3.
|
|
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\
|
|
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
|
-
|
|
30332
|
+
const execution = execFileP3(stageBash(), ["-c", command], {
|
|
30342
30333
|
cwd,
|
|
30343
|
-
|
|
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(
|
|
36042
|
-
return
|
|
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