@mutmutco/cli 2.40.3 → 2.41.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 +203 -17
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5678,7 +5678,16 @@ async function secretsCapabilities(deps, opts) {
5678
5678
  return;
5679
5679
  }
5680
5680
  if (!res.ok) {
5681
- deps.err(await upgradeMessage(res) ?? `access capabilities failed: HTTP ${res.status}${await readErr(res)}`);
5681
+ const upgrade = await upgradeMessage(res);
5682
+ if (upgrade) {
5683
+ deps.err(upgrade);
5684
+ } else if (res.status === 404) {
5685
+ deps.err(
5686
+ "access capabilities: the Hub API did not recognize /secrets/capabilities (HTTP 404). This endpoint ships in the Hub, so a 404 means the deployed Hub predates this command and should answer once a Hub release carrying this command is deployed \u2014 it is not an authorization or missing-credential error."
5687
+ );
5688
+ } else {
5689
+ deps.err(`access capabilities failed: HTTP ${res.status}${await readErr(res)}`);
5690
+ }
5682
5691
  return;
5683
5692
  }
5684
5693
  const report = await res.json();
@@ -6152,7 +6161,6 @@ function buildIngestPayload(args) {
6152
6161
  }
6153
6162
 
6154
6163
  // src/honcho-client.ts
6155
- var HONCHO_ASSISTANT_PEER = "assistant";
6156
6164
  function parseHonchoQueueStatus(json) {
6157
6165
  const o = json && typeof json === "object" ? json : {};
6158
6166
  const pendingRaw = o.pending_work_units ?? o.depth ?? o.queue_depth ?? o.pending;
@@ -6205,9 +6213,9 @@ async function ingestPost(cfg, body, fetchImpl = fetch, timeoutMs = 1e4) {
6205
6213
  const session = String(body.session ?? "");
6206
6214
  const meta = body.meta && typeof body.meta === "object" ? body.meta : {};
6207
6215
  const rawMessages = Array.isArray(body.messages) ? body.messages : [];
6208
- const messages = rawMessages.filter((m) => m && typeof m.content === "string" && m.content.trim().length > 0).map((m) => ({
6216
+ const messages = rawMessages.filter((m) => m && m.role === "user" && typeof m.content === "string" && m.content.trim().length > 0).map((m) => ({
6209
6217
  content: m.content,
6210
- peer_id: m.role === "user" ? peer : HONCHO_ASSISTANT_PEER,
6218
+ peer_id: peer,
6211
6219
  metadata: { role: m.role, ...meta }
6212
6220
  }));
6213
6221
  if (!peer || !session || !messages.length) return { ok: true, threw: false, status: 204 };
@@ -6216,7 +6224,8 @@ async function ingestPost(cfg, body, fetchImpl = fetch, timeoutMs = 1e4) {
6216
6224
  if (res.status === 404) {
6217
6225
  const ensured = await request(cfg, fetchImpl, "POST", honchoRoutes.sessions(cfg.workspace), {
6218
6226
  id: session,
6219
- peers: { [peer]: {}, [HONCHO_ASSISTANT_PEER]: {} }
6227
+ peers: { [peer]: {} }
6228
+ // only the human peer — assistant turns are no longer ingested (#1775)
6220
6229
  }, timeoutMs);
6221
6230
  if (!ensured.ok && ensured.status !== 409) {
6222
6231
  if (ensured.status >= 500) return { ok: false, threw: true, message: `honcho session-ensure ${ensured.status}` };
@@ -6366,7 +6375,7 @@ function spawnHonchoFlush() {
6366
6375
  } catch {
6367
6376
  }
6368
6377
  }
6369
- var DEFAULT_INGEST_MIN_INTERVAL_SEC = 60;
6378
+ var DEFAULT_INGEST_MIN_INTERVAL_SEC = 300;
6370
6379
  function honchoThrottlePath(key) {
6371
6380
  const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
6372
6381
  return `.mmi/head-ts/honcho/${safe(key.project)}/${safe(key.branch)}`;
@@ -6398,7 +6407,12 @@ async function resolveSummaryText(summary, messageFile) {
6398
6407
  }
6399
6408
  return "";
6400
6409
  }
6410
+ function isAutomatedSession(env = process.env) {
6411
+ const v = env.GITHUB_ACTIONS;
6412
+ return !!v && v !== "false" && v !== "0";
6413
+ }
6401
6414
  async function runHonchoIngest(opts) {
6415
+ if (isAutomatedSession()) return;
6402
6416
  const cfg = await loadConfig();
6403
6417
  const peer = honchoPeerId(await honchoLogin(cfg), cfg);
6404
6418
  if (!peer) return;
@@ -9672,6 +9686,44 @@ function buildPrArgs({ title, body, base: base2, head, repo }) {
9672
9686
  return args;
9673
9687
  }
9674
9688
 
9689
+ // src/issue-check.ts
9690
+ var CHECKLIST_RE = /^([ \t]*[-*+] \[)([ xX])(\] )(.*)$/gm;
9691
+ function findChecklistItems(body) {
9692
+ const items = [];
9693
+ for (const m of body.matchAll(CHECKLIST_RE)) {
9694
+ const prefix = m[1];
9695
+ const marker = m[2];
9696
+ const text = m[4].replace(/\r$/, "");
9697
+ items.push({
9698
+ markerIndex: (m.index ?? 0) + prefix.length,
9699
+ checked: marker.toLowerCase() === "x",
9700
+ text
9701
+ });
9702
+ }
9703
+ return items;
9704
+ }
9705
+ function selectChecklistItem(items, query) {
9706
+ const q = query.trim();
9707
+ if (!q) return { ok: false, reason: "not-found" };
9708
+ const exact = items.filter((it) => it.text.trim() === q);
9709
+ if (exact.length === 1) return { ok: true, item: exact[0] };
9710
+ if (exact.length > 1) return { ok: false, reason: "ambiguous", matches: exact };
9711
+ const sub = items.filter((it) => it.text.includes(q));
9712
+ if (sub.length === 1) return { ok: true, item: sub[0] };
9713
+ if (sub.length === 0) return { ok: false, reason: "not-found" };
9714
+ return { ok: false, reason: "ambiguous", matches: sub };
9715
+ }
9716
+ function setChecklistMarker(body, item, checked) {
9717
+ if (item.checked === checked) return { body, changed: false };
9718
+ const target = checked ? "x" : " ";
9719
+ return { body: body.slice(0, item.markerIndex) + target + body.slice(item.markerIndex + 1), changed: true };
9720
+ }
9721
+ function applyChecklistCheck(body, query, checked) {
9722
+ const sel = selectChecklistItem(findChecklistItems(body), query);
9723
+ if (!sel.ok) return sel;
9724
+ return { ok: true, edit: setChecklistMarker(body, sel.item, checked), item: sel.item };
9725
+ }
9726
+
9675
9727
  // src/command-manifest.ts
9676
9728
  function buildArgument(arg) {
9677
9729
  const out = { name: arg.name(), required: arg.required, variadic: arg.variadic };
@@ -11913,8 +11965,8 @@ function stageLiveUpSteps(t) {
11913
11965
  command: `gh ${ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref ?? "<branch>", stage: "dev" }).join(" ")}`
11914
11966
  },
11915
11967
  {
11916
- label: "record your IP as the dev allowlist (box writes /opt/mmi/<slug>/dev/allowlist + reloads Caddy)",
11917
- command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip: "<your ip>" }).join(" ")}`
11968
+ label: `gate ${t.host} to your IP at the Cloudflare edge (ephemeral firewall_custom skip rule)`,
11969
+ command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-allow", host: t.host, ip: "<your ip>" }).join(" ")}`
11918
11970
  },
11919
11971
  { label: "tear down when done", command: "mmi-cli stage --live --down --apply" }
11920
11972
  ];
@@ -11926,8 +11978,8 @@ function stageLiveDownSteps(t) {
11926
11978
  command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }).join(" ")}`
11927
11979
  },
11928
11980
  {
11929
- label: "clear the dev allowlist (the stage goes dark even if restarted)",
11930
- command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip: "clear" }).join(" ")}`
11981
+ label: "remove the Cloudflare edge gate (the stage goes dark even if restarted)",
11982
+ command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-clear", host: t.host }).join(" ")}`
11931
11983
  }
11932
11984
  ];
11933
11985
  }
@@ -11935,8 +11987,9 @@ async function runStageLiveUp(deps, t) {
11935
11987
  if (!t.ref?.trim()) throw new Error("stage --live: cannot resolve the current branch to deploy");
11936
11988
  const ip = (await deps.detectIp()).trim();
11937
11989
  if (!validStageLiveIp(ip)) throw new Error(`stage --live: detected public IP is not a literal IPv4/IPv6 address: "${ip.slice(0, 80)}"`);
11990
+ if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
11938
11991
  await deps.run("gh", ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref, stage: "dev" }));
11939
- await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip }));
11992
+ await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-allow", host: t.host, ip }));
11940
11993
  return {
11941
11994
  command: "stage --live",
11942
11995
  mode: "up",
@@ -11945,19 +11998,20 @@ async function runStageLiveUp(deps, t) {
11945
11998
  ref: t.ref,
11946
11999
  ip,
11947
12000
  dispatched: ["tenant-deploy.yml", "tenant-control.yml"],
11948
- message: `dispatched the dev deploy of ${t.ref} and the allowlist update for ${ip}; watch the runs in ${STAGE_LIVE_HUB_REPO} Actions \u2014 tear down with: mmi-cli stage --live --down --apply`
12001
+ 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`
11949
12002
  };
11950
12003
  }
11951
12004
  async function runStageLiveDown(deps, t) {
12005
+ if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
11952
12006
  await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }));
11953
- await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "allow-ip", ip: "clear" }));
12007
+ await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-clear", host: t.host }));
11954
12008
  return {
11955
12009
  command: "stage --live",
11956
12010
  mode: "down",
11957
12011
  slug: t.slug,
11958
12012
  repo: t.repo,
11959
12013
  dispatched: ["tenant-control.yml", "tenant-control.yml"],
11960
- message: `dispatched the dev stop and allowlist clear for ${t.slug}; the dev stage is dark until the next mmi-cli stage --live --apply`
12014
+ message: `dispatched the dev stop and the Cloudflare edge gate clear for ${t.host}; the dev stage is dark until the next mmi-cli stage --live --apply`
11961
12015
  };
11962
12016
  }
11963
12017
 
@@ -15240,6 +15294,9 @@ async function upsertProject(slug, patch, deps) {
15240
15294
  async function attestAppGaps(slug, repo, deps) {
15241
15295
  return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
15242
15296
  }
15297
+ async function setDeployCoords(slug, payload, deps) {
15298
+ return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
15299
+ }
15243
15300
  async function tenantControl(payload, deps) {
15244
15301
  return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
15245
15302
  }
@@ -15634,7 +15691,7 @@ ${section}`.trim();
15634
15691
  }
15635
15692
 
15636
15693
  // src/project-set.ts
15637
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dashboard", "ci", "requiredChecks", "gate"];
15694
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dashboard", "fofuEnabled", "ci", "requiredChecks", "gate"];
15638
15695
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
15639
15696
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
15640
15697
  function parseRuntimeSecretsVar(raw) {
@@ -15798,6 +15855,11 @@ function parseDashboardVar(raw) {
15798
15855
  if (raw === "false") return false;
15799
15856
  throw new Error("project set: dashboard must be true or false");
15800
15857
  }
15858
+ function parseFofuEnabledVar(raw) {
15859
+ if (raw === "true") return true;
15860
+ if (raw === "false") return false;
15861
+ throw new Error("project set: fofuEnabled must be true or false");
15862
+ }
15801
15863
  function parsePublishDirVar(raw) {
15802
15864
  const v = raw.trim();
15803
15865
  if (v === "" || v === ".") {
@@ -15860,6 +15922,7 @@ var SETTABLE_VAR_KEYS = [
15860
15922
  "publishRequired",
15861
15923
  "publishDir",
15862
15924
  "dashboard",
15925
+ "fofuEnabled",
15863
15926
  "requiredGcpApis",
15864
15927
  "requiredRuntimeSecrets",
15865
15928
  "edgeDomains",
@@ -15878,6 +15941,7 @@ var SETTABLE_VAR_HINTS = {
15878
15941
  publishRequired: "true|false",
15879
15942
  publishDir: "relative subpath, e.g. packages/ui",
15880
15943
  dashboard: "true|false",
15944
+ fofuEnabled: "true|false",
15881
15945
  repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
15882
15946
  oauth: "JSON {subdomains,domains,callbackPath}",
15883
15947
  requiredGcpApis: "comma-string",
@@ -15953,6 +16017,8 @@ function buildProjectSetPatch(input) {
15953
16017
  patch[key] = parsePublishRequiredVar(raw);
15954
16018
  } else if (key === "dashboard") {
15955
16019
  patch[key] = parseDashboardVar(raw);
16020
+ } else if (key === "fofuEnabled") {
16021
+ patch[key] = parseFofuEnabledVar(raw);
15956
16022
  } else if (key === "publishDir") {
15957
16023
  patch[key] = parsePublishDirVar(raw);
15958
16024
  } else if (key === "ci") {
@@ -15981,6 +16047,54 @@ function buildProjectSetPatch(input) {
15981
16047
  }
15982
16048
  return patch;
15983
16049
  }
16050
+ var DEPLOY_SUBSTRATES = ["hetzner-ssh"];
16051
+ var DEPLOY_STAGES = ["dev", "rc", "main"];
16052
+ var DEPLOY_DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
16053
+ var DEPLOY_SHELL_SAFE_RE = /^[A-Za-z0-9./:_?&=%-]*$/;
16054
+ function buildSetDeployPatch(slug, input) {
16055
+ const clean4 = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
16056
+ const stage2 = clean4(input.stage);
16057
+ if (!stage2 || !DEPLOY_STAGES.includes(stage2)) {
16058
+ throw new Error(`project set-deploy: --stage must be one of: ${DEPLOY_STAGES.join(", ")}`);
16059
+ }
16060
+ const substrate = clean4(input.substrate) ?? "hetzner-ssh";
16061
+ if (!DEPLOY_SUBSTRATES.includes(substrate)) {
16062
+ throw new Error(`project set-deploy: --substrate must be one of: ${DEPLOY_SUBSTRATES.join(", ")}`);
16063
+ }
16064
+ const sshHost = clean4(input.sshHost);
16065
+ if (substrate === "hetzner-ssh" && !sshHost) {
16066
+ throw new Error("project set-deploy: hetzner-ssh requires --ssh-host");
16067
+ }
16068
+ const sshUser = clean4(input.sshUser) ?? "root";
16069
+ const deployPath = clean4(input.deployPath) ?? `/opt/mmi/${slug}/${stage2}`;
16070
+ const serviceName = clean4(input.service) ?? slug;
16071
+ for (const [label, v] of [["--ssh-host", sshHost], ["--ssh-user", sshUser], ["--deploy-path", deployPath], ["--service", serviceName]]) {
16072
+ if (v !== void 0 && !DEPLOY_SHELL_SAFE_RE.test(v)) throw new Error(`project set-deploy: ${label} contains unsafe characters`);
16073
+ }
16074
+ let port;
16075
+ if (input.port !== void 0 && input.port !== null && `${input.port}`.trim() !== "") {
16076
+ port = Number(input.port);
16077
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("project set-deploy: --port must be an integer 1..65535");
16078
+ }
16079
+ const domain = clean4(input.domain);
16080
+ if (domain !== void 0 && !DEPLOY_DOMAIN_RE.test(domain)) throw new Error(`project set-deploy: --domain must be a DNS hostname, got ${JSON.stringify(input.domain)}`);
16081
+ const aliases = (Array.isArray(input.aliases) ? input.aliases : []).map((a) => clean4(a)).filter((a) => a !== void 0);
16082
+ for (const a of aliases) {
16083
+ if (!DEPLOY_DOMAIN_RE.test(a)) throw new Error(`project set-deploy: --alias must be a DNS hostname, got ${JSON.stringify(a)}`);
16084
+ }
16085
+ const uniqueAliases = [...new Set(aliases)];
16086
+ return {
16087
+ stage: stage2,
16088
+ substrate,
16089
+ sshHost,
16090
+ sshUser,
16091
+ deployPath,
16092
+ serviceName,
16093
+ ...domain !== void 0 ? { domain } : {},
16094
+ ...port !== void 0 ? { port } : {},
16095
+ ...uniqueAliases.length ? { aliases: uniqueAliases } : {}
16096
+ };
16097
+ }
15984
16098
  function repoFromRemoteUrl(remoteUrl) {
15985
16099
  const m = remoteUrl.trim().match(/^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@\s/]+@)?github\.com[:/]([^/\s:]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
15986
16100
  return m ? `${m[1]}/${m[2]}` : void 0;
@@ -17729,7 +17843,7 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
17729
17843
  }
17730
17844
  });
17731
17845
  project.command("resolve <owner/repo>").description("deploy coords for a stage \u2014 for diagnosis. NOTE: /deploy-coords is OIDC-gated (a deploy job\u2019s id-token), so a gh-token CLI cannot read it from a dev machine").option("--stage <main|rc>", "deploy stage", "main").option("--json", "machine-readable output").action((_repoOrRepo, o) => {
17732
- const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect the DEPLOY# item via the AWS console / a master DDB read instead.";
17846
+ const msg = "project resolve: deploy coords are served only to a deploy workflow (GitHub OIDC id-token, repo-scoped). A gh-token CLI on a dev machine cannot read /deploy-coords; inspect the DEPLOY# item via a master registry (DDB) read instead.";
17733
17847
  if (o.json) {
17734
17848
  console.log(JSON.stringify({ ok: false, stage: o.stage, error: msg }));
17735
17849
  process.exitCode = 1;
@@ -17829,6 +17943,34 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
17829
17943
  const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
17830
17944
  return reportWrite("project set", res);
17831
17945
  });
17946
+ project.command("set-deploy [owner/repo]").description("write the DEPLOY#<stage> Hetzner deploy coords for a tenant (master-only) \u2014 the explicit-coords path that seeds a freshly-bootstrapped tenant; defaults to the current repo").requiredOption("--stage <stage>", "dev | rc | main").option("--ssh-host <host>", "the box address the deploy ssh-es into (required for hetzner-ssh)").option("--ssh-user <user>", "ssh user (default root)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535)").option("--substrate <substrate>", "hetzner-ssh (default)").option("--deploy-path <path>", "on-box per-stage release root (default /opt/mmi/<slug>/<stage>)").option("--service <name>", "systemd/compose service name (default the slug)").option("--domain <domain>", "canonical serving host (default the project edgeDomains[stage])").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
17947
+ const cfg = await loadConfig();
17948
+ let target;
17949
+ try {
17950
+ target = await projectTarget("project set-deploy", repoOrSlug);
17951
+ } catch (e) {
17952
+ return fail(e.message);
17953
+ }
17954
+ const slug = slugOf(target);
17955
+ let body;
17956
+ try {
17957
+ body = buildSetDeployPatch(slug, {
17958
+ stage: o.stage,
17959
+ sshHost: o.sshHost,
17960
+ sshUser: o.sshUser,
17961
+ port: o.port,
17962
+ substrate: o.substrate,
17963
+ deployPath: o.deployPath,
17964
+ service: o.service,
17965
+ domain: o.domain,
17966
+ aliases: o.alias
17967
+ });
17968
+ } catch (e) {
17969
+ return fail(e.message);
17970
+ }
17971
+ const res = await setDeployCoords(slug, body, registryClientDeps(cfg));
17972
+ return reportWrite("project set-deploy", res);
17973
+ });
17832
17974
  var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
17833
17975
  registry.command("org").description("the org config (account id, region, orgProjectId, sagaApiUrl)").option("--json", "machine-readable output").action(async (_o) => {
17834
17976
  const cfg = await loadConfig();
@@ -18011,6 +18153,46 @@ issue.command("link-child <parent> <child>").description("link an existing issue
18011
18153
  return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
18012
18154
  }
18013
18155
  });
18156
+ issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2192 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)").action(async (ref, o) => {
18157
+ let parsed;
18158
+ try {
18159
+ parsed = parseIssueRef(ref);
18160
+ } catch (e) {
18161
+ return fail(`issue check: ${e.message}`);
18162
+ }
18163
+ const repo = await resolveRepo(parsed.repo ?? o.repo);
18164
+ if (!repo) return fail("issue check: could not resolve repo \u2014 pass --repo owner/repo");
18165
+ const checked = o.off !== true;
18166
+ let body;
18167
+ try {
18168
+ const viewed = await ghJson(["issue", "view", String(parsed.number), "--repo", repo, "--json", "body"]);
18169
+ body = viewed.body ?? "";
18170
+ } catch (e) {
18171
+ return fail(`issue check: could not read ${repo}#${parsed.number}: ${e.message}`);
18172
+ }
18173
+ const result = applyChecklistCheck(body, o.item, checked);
18174
+ if (!result.ok) {
18175
+ if (result.reason === "ambiguous") {
18176
+ const list = result.matches.map((m) => ` - ${m.text}`).join("\n");
18177
+ return fail(`issue check: "${o.item}" matches ${result.matches.length} checklist items in ${repo}#${parsed.number} \u2014 narrow the text:
18178
+ ${list}`);
18179
+ }
18180
+ return fail(`issue check: no checklist item matching "${o.item}" in ${repo}#${parsed.number}`);
18181
+ }
18182
+ if (!result.edit.changed) {
18183
+ return console.log(JSON.stringify({ number: parsed.number, repo, item: result.item.text, checked, changed: false }));
18184
+ }
18185
+ const edit = await bodyArgsViaFile(["issue", "edit", String(parsed.number), "--repo", repo, "--body", result.edit.body]);
18186
+ try {
18187
+ await execFileP2("gh", edit.args, { timeout: GH_MUTATION_TIMEOUT_MS });
18188
+ } catch (e) {
18189
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
18190
+ return fail(`issue check: edit failed for ${repo}#${parsed.number}: ${e.message}${note ? ` (${note})` : ""}`);
18191
+ } finally {
18192
+ await edit.cleanup();
18193
+ }
18194
+ console.log(JSON.stringify({ number: parsed.number, repo, item: result.item.text, checked, changed: true }));
18195
+ });
18014
18196
  program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only, #416)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
18015
18197
  let body;
18016
18198
  let priority;
@@ -18831,7 +19013,11 @@ async function stageLiveTarget() {
18831
19013
  const repo = await resolveRepo();
18832
19014
  if (!repo) throw new Error("stage --live: cannot resolve the current repo (run inside a GitHub-remoted checkout)");
18833
19015
  const ref = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
18834
- return { slug: slugOf(repo), repo, ref };
19016
+ const slug = slugOf(repo);
19017
+ const meta = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig())).catch(() => null);
19018
+ const host = edgeDomainsByStage(meta).dev ?? `dev.${defaultSubdomain(slug)}.mutatismutandis.co`;
19019
+ if (!host.trim()) throw new Error(`stage --live: no dev edge host for ${slug} (registry edgeDomains.dev)`);
19020
+ return { slug, repo, host, ref };
18835
19021
  }
18836
19022
  async function runStageLiveCommand(o) {
18837
19023
  let target;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.40.3",
3
+ "version": "2.41.0",
4
4
  "description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",