@mutmutco/cli 4.3.39 → 4.3.41

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 +72 -53
  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`, {
@@ -14100,7 +14099,7 @@ function expectedHosts(cfg) {
14100
14099
  if (cfg.fofuSubdomain !== void 0) {
14101
14100
  out.push(cfg.fofuSubdomain ? `${cfg.fofuSubdomain}.fofu.ai` : "fofu.ai");
14102
14101
  }
14103
- return uniq(out);
14102
+ return uniq([...out, ...cfg.edgeHosts ?? []]);
14104
14103
  }
14105
14104
  function expectedJsOrigins(cfg) {
14106
14105
  return uniq([...expectedHosts(cfg).map((h) => `https://${h}`), ...LOOPBACK]);
@@ -14167,7 +14166,7 @@ function parseOauthConfig(mmiConfig, slug) {
14167
14166
  const meta = mmiConfig ?? {};
14168
14167
  const rawFofuSub = raw.fofuSubdomain;
14169
14168
  const fofuSubdomain = meta.fofuEnabled === true ? typeof rawFofuSub === "string" ? rawFofuSub : defaultSubdomain(slug) : void 0;
14170
- return { subdomains, domains, callbackPath, extraCallbackPaths, fofuSubdomain };
14169
+ return { subdomains, domains, callbackPath, extraCallbackPaths, fofuSubdomain, edgeHosts: Object.values(edgeDomainsByStage(mmiConfig)) };
14171
14170
  }
14172
14171
  function probeRedirectUri(callbackPath, port = 9123) {
14173
14172
  return `http://localhost:${port}${callbackPath}`;
@@ -14186,6 +14185,20 @@ function buildAuthorizeProbeUrl(clientId, redirectUri) {
14186
14185
  function authorizeBodyHasMismatch(body) {
14187
14186
  return /redirect_uri_mismatch/i.test(body);
14188
14187
  }
14188
+ async function probeOauthRedirects(cfg, clientId, request = fetch) {
14189
+ const uris = uniq([probeRedirectUri(cfg.callbackPath), ...expectedRedirectUris(cfg)]);
14190
+ const signal = AbortSignal.timeout(1e4);
14191
+ const results = [];
14192
+ for (let i = 0; i < uris.length; i += 4) {
14193
+ results.push(...await Promise.all(uris.slice(i, i + 4).map(async (redirectUri) => {
14194
+ const response = await request(buildAuthorizeProbeUrl(clientId, redirectUri), { redirect: "follow", signal });
14195
+ const mismatch = authorizeBodyHasMismatch(await response.text());
14196
+ if (!mismatch && !response.ok) throw new Error(`authorize probe returned HTTP ${response.status}`);
14197
+ return { redirectUri, mismatch };
14198
+ })));
14199
+ }
14200
+ return results;
14201
+ }
14189
14202
 
14190
14203
  // src/project-runtime.ts
14191
14204
  function hasRuntimeSecretContract(contract) {
@@ -15695,10 +15708,10 @@ var rollout_plan_default = {
15695
15708
  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
15709
  },
15697
15710
  baseline: {
15698
- version: "4.3.39",
15699
- tag: "v4.3.39",
15700
- commit: "cc67ff24bbec",
15701
- npm: "@mutmutco/cli@4.3.39"
15711
+ version: "4.3.41",
15712
+ tag: "v4.3.41",
15713
+ commit: "02cc438cfbf4",
15714
+ npm: "@mutmutco/cli@4.3.41"
15702
15715
  },
15703
15716
  exitCriterion: "fleet-n-of-n",
15704
15717
  hubOnlyShortcut: "forbidden",
@@ -15715,14 +15728,14 @@ var rollout_plan_default = {
15715
15728
  repo: "mutmutco/mmi-hub",
15716
15729
  role: "canary",
15717
15730
  schedule: "train",
15718
- v3Target: "v4.3.39"
15731
+ v3Target: "v4.3.41"
15719
15732
  }
15720
15733
  ],
15721
15734
  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
15735
  rollback: {
15723
15736
  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)"
15737
+ mechanism: "npm dist-tag latest -> 4.3.41 and redeploy the Hub Lambda from tag v4.3.41 (02cc438cfbf4); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15738
+ v3Target: "v4.3.41 (@mutmutco/cli@4.3.41, tag commit 02cc438cfbf4 \u2014 last known-good release carrying the repo-index v4-only contract)"
15726
15739
  }
15727
15740
  },
15728
15741
  {
@@ -25957,7 +25970,7 @@ function registerBoardCommands(program3) {
25957
25970
  return `Claimed ${ref} for ${holder} - In Progress${reclaimed}`;
25958
25971
  }
25959
25972
  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));
25973
+ 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
25974
  withExamples(mutating(
25962
25975
  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
25976
  (_opts, args) => ({ command: "oracle board claim", issues: args[0] ?? [] })
@@ -30097,6 +30110,18 @@ function stageComposeFileEnv(files) {
30097
30110
  // src/stage-runner.ts
30098
30111
  var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process11.execFile);
30099
30112
  var DOCKER_TIMEOUT_MS = 15e3;
30113
+ function stageBash() {
30114
+ if (process.platform !== "win32") return "bash";
30115
+ const configured = process.env.SHELL;
30116
+ 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;
30117
+ const candidates = [
30118
+ (0, import_node_path27.join)(process.env.ProgramFiles || "C:\\Program Files", "Git", "bin", "bash.exe"),
30119
+ ...process.env.LOCALAPPDATA ? [(0, import_node_path27.join)(process.env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe")] : []
30120
+ ];
30121
+ const bash = candidates.find((path2) => (0, import_node_fs28.existsSync)(path2));
30122
+ 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.");
30123
+ return bash;
30124
+ }
30100
30125
  var EARLY_EXIT_GRACE_MS = 2e3;
30101
30126
  function earlyExitGraceMs() {
30102
30127
  if (process.env.NODE_ENV === "test") {
@@ -30287,24 +30312,6 @@ function mergeEnvSecretsIntoFile(content, secrets) {
30287
30312
  return body.endsWith("\n") ? body : `${body}
30288
30313
  `;
30289
30314
  }
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
30315
  function validateStageConfig(config = {}, action) {
30309
30316
  const problems = [];
30310
30317
  if (action === "run" && !config.build?.trim() && !config.ensureEnv) problems.push("stage.build is required for stage run");
@@ -30312,8 +30319,6 @@ function validateStageConfig(config = {}, action) {
30312
30319
  if (config.healthUrl != null && config.healthUrl.trim() && !/^https?:\/\//.test(config.healthUrl.trim())) {
30313
30320
  problems.push("stage.healthUrl must be an http(s) URL");
30314
30321
  }
30315
- if (action === "run") problems.push(...posixOnlyShellProblems(config.build, "build"));
30316
- problems.push(...posixOnlyShellProblems(config.up, "up"));
30317
30322
  if (config.portRange != null) {
30318
30323
  const r = config.portRange;
30319
30324
  const ok = Array.isArray(r) && r.length === 2 && r.every((n) => Number.isInteger(n) && n >= 1024 && n <= 65535) && r[0] <= r[1];
@@ -30338,14 +30343,27 @@ function isPortFree(port) {
30338
30343
  });
30339
30344
  }
30340
30345
  async function shell(command, cwd, timeoutMs, env) {
30341
- await execFileP3(command, [], {
30346
+ const execution = execFileP3(stageBash(), ["-c", command], {
30342
30347
  cwd,
30343
- shell: true,
30344
- timeout: timeoutMs,
30348
+ timeout: process.platform === "win32" ? timeoutMs + 5e3 : timeoutMs,
30345
30349
  windowsHide: true,
30346
30350
  maxBuffer: 1024 * 1024 * 4,
30347
30351
  ...env ? { env: { ...process.env, ...env } } : {}
30348
30352
  });
30353
+ let cleanup;
30354
+ const timer = process.platform === "win32" ? setTimeout(() => {
30355
+ cleanup = killTree(execution.child.pid ?? 0);
30356
+ }, timeoutMs) : void 0;
30357
+ try {
30358
+ await execution;
30359
+ if (cleanup) throw new Error(`stage command timed out after ${timeoutMs}ms`);
30360
+ } catch (error) {
30361
+ if (cleanup) throw new Error(`stage command timed out after ${timeoutMs}ms`, { cause: error });
30362
+ throw error;
30363
+ } finally {
30364
+ clearTimeout(timer);
30365
+ await cleanup;
30366
+ }
30349
30367
  }
30350
30368
  async function listDockerContainers() {
30351
30369
  const { stdout } = await execFileP3("docker", ["container", "ls", "--format", "{{json .}}"], {
@@ -30585,7 +30603,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd, currentEn
30585
30603
  async function killTree(pid) {
30586
30604
  if (!Number.isInteger(pid) || pid <= 0) return;
30587
30605
  if (process.platform === "win32") {
30588
- await execFileP3("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }).catch(() => void 0);
30606
+ await execFileP3("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true, timeout: 5e3 }).catch(() => void 0);
30589
30607
  return;
30590
30608
  }
30591
30609
  try {
@@ -30651,6 +30669,7 @@ async function stopStage(opts = {}) {
30651
30669
  };
30652
30670
  }
30653
30671
  async function startStage(config = {}, opts = {}) {
30672
+ const bash = stageBash();
30654
30673
  const problems = validateStageConfig(config, "start");
30655
30674
  if (problems.length) throw new Error(problems.join("; "));
30656
30675
  const cwd = opts.cwd ?? process.cwd();
@@ -30672,9 +30691,8 @@ async function startStage(config = {}, opts = {}) {
30672
30691
  let up = sub(config.up.trim());
30673
30692
  if (opts.forceRecreate) up = appendForceRecreate(up);
30674
30693
  const identity = await resolveStageIdentity(cwd);
30675
- const child2 = (0, import_node_child_process11.spawn)(up, {
30694
+ const child2 = (0, import_node_child_process11.spawn)(bash, ["-c", up], {
30676
30695
  cwd,
30677
- shell: true,
30678
30696
  // POSIX-only: the process group exists for the group-kill in stopStage. On win32 teardown is
30679
30697
  // `taskkill /T /F` (no group needed), and detached+shell defeats windowsHide — every spawn would
30680
30698
  // flash a Windows Terminal window (0x800700e8) on dev machines.
@@ -30728,6 +30746,7 @@ function stageEnvRefusal(cwd) {
30728
30746
  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
30747
  }
30730
30748
  async function runStage(config = {}, opts = {}) {
30749
+ stageBash();
30731
30750
  const problems = validateStageConfig(config, "run");
30732
30751
  if (problems.length) throw new Error(problems.join("; "));
30733
30752
  const cwd = opts.cwd ?? process.cwd();
@@ -36038,8 +36057,8 @@ var import_node_fs34 = require("node:fs");
36038
36057
  var import_node_path31 = require("node:path");
36039
36058
 
36040
36059
  // src/stage-default.ts
36041
- function shellFor(platform2 = process.platform) {
36042
- return platform2 === "win32" ? "powershell" : "bash";
36060
+ function shellFor() {
36061
+ return "bash";
36043
36062
  }
36044
36063
  function isCentralContainerModel(model) {
36045
36064
  return model === "tenant-container" || model === "solo-container";
@@ -36510,7 +36529,7 @@ function registerStageCommands(program3) {
36510
36529
  fail(`stage stop: ${e.message}`);
36511
36530
  }
36512
36531
  });
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 () => {
36532
+ 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
36533
  const o = { json: rawFlag("--json"), apply: rawFlag("--apply"), timeoutMs: rawValue("--timeout-ms", "60000"), allowStaleEnv: rawFlag("--allow-stale-env") };
36515
36534
  const { resolution: res, project: project2, cfg: stageCfg } = await resolveStage();
36516
36535
  if (!o.apply) {
@@ -45554,22 +45573,22 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
45554
45573
  return failGraceful("org oauth verify: no client_id (pass --client-id, or provision the repo so GOOGLE_CLIENT_ID exists)");
45555
45574
  }
45556
45575
  const redirectUri = probeRedirectUri(oc.callbackPath);
45557
- let body = "";
45576
+ let probes;
45558
45577
  try {
45559
- const res = await fetch(buildAuthorizeProbeUrl(clientId, redirectUri), { redirect: "follow", signal: AbortSignal.timeout(1e4) });
45560
- body = await res.text();
45578
+ probes = await probeOauthRedirects(oc, clientId);
45561
45579
  } catch (e) {
45562
45580
  return failGraceful(`org oauth verify: probe request failed: ${e.message}`);
45563
45581
  }
45564
- const mismatch = authorizeBodyHasMismatch(body);
45582
+ const portAgnostic = !probes[0].mismatch;
45583
+ const mismatches = probes.filter((probe) => probe.mismatch);
45565
45584
  if (o.json) {
45566
- console.log(JSON.stringify({ slug, redirectUri, portAgnostic: !mismatch }));
45567
- } else if (mismatch) {
45568
- console.error(`FAIL ${slug}: redirect_uri_mismatch for ${redirectUri} \u2014 client is not port-agnostic (run /oauth-provision)`);
45585
+ console.log(JSON.stringify({ slug, redirectUri, portAgnostic, probes }));
45586
+ } else if (mismatches.length) {
45587
+ for (const probe of mismatches) console.error(`FAIL ${slug}: redirect_uri_mismatch for ${probe.redirectUri} \u2014 run /oauth-provision`);
45569
45588
  } else {
45570
- console.log(`PASS ${slug}: ${redirectUri} accepted \u2014 port-agnostic OAuth is live`);
45589
+ console.log(`PASS ${slug}: no redirect_uri_mismatch across ${probes.length} callbacks, including the arbitrary-port loopback`);
45571
45590
  }
45572
- if (mismatch) process.exitCode = 1;
45591
+ if (mismatches.length) process.exitCode = 1;
45573
45592
  });
45574
45593
  registerCollaborationCommands(program2);
45575
45594
  function ciAuditDeps2() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.3.39",
3
+ "version": "4.3.41",
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",