@mutmutco/cli 3.104.0 → 3.105.2

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 +161 -1
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -12393,6 +12393,9 @@ async function preflight(deps, ctx, stage, meta) {
12393
12393
  }
12394
12394
  await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo]);
12395
12395
  enforceGateBudget(deps, ctx.repo);
12396
+ if (model === "hub-serverless") {
12397
+ await deps.run("node", ["scripts/release-distribution.mjs", "verify-deps"]);
12398
+ }
12396
12399
  return model;
12397
12400
  }
12398
12401
  async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix, realignMessage) {
@@ -20436,6 +20439,9 @@ async function runHotfixStart(deps, options) {
20436
20439
  };
20437
20440
  }
20438
20441
  const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
20442
+ if (deployModel === "hub-serverless") {
20443
+ await deps.run("node", ["scripts/release-distribution.mjs", "verify-deps"]);
20444
+ }
20439
20445
  const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
20440
20446
  if (remoteBranch) {
20441
20447
  await deps.run("git", ["checkout", branch]);
@@ -20456,7 +20462,8 @@ async function runHotfixStart(deps, options) {
20456
20462
  }
20457
20463
  notes.push(`cherry-picked ${label} onto ${branch} (from origin/main, -x trailer recorded)`);
20458
20464
  if (deployModel === "hub-serverless") {
20459
- await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", "HEAD"]);
20465
+ const durableSource = clean3(await deps.run("git", ["merge-base", "HEAD", "origin/main"]));
20466
+ await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", durableSource]);
20460
20467
  const changedFiles = (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
20461
20468
  await deps.run("git", ["add", "-f", "--", ...changedFiles]);
20462
20469
  const staged = await deps.run("git", ["diff", "--cached", "--name-only"]);
@@ -25433,6 +25440,148 @@ async function runIssueChildren(deps, epic, opts) {
25433
25440
  }
25434
25441
  return out;
25435
25442
  }
25443
+ var FRONTIER_BUCKET_ORDER = {
25444
+ in_flight: 0,
25445
+ partial: 1,
25446
+ ready: 2,
25447
+ landed: 3
25448
+ };
25449
+ function prStateIs(pr2, want) {
25450
+ return String(pr2.state ?? "").toUpperCase() === want;
25451
+ }
25452
+ function isBoardDone(boardStatus) {
25453
+ return /^done$/i.test(String(boardStatus ?? "").trim());
25454
+ }
25455
+ function isBoardInFlight(boardStatus) {
25456
+ return /^(in\s*progress|in\s*review|review|doing)$/i.test(String(boardStatus ?? "").trim());
25457
+ }
25458
+ function classifyFrontierChild(child2) {
25459
+ const mergedPrs = child2.linkedPrs.filter((p) => prStateIs(p, "MERGED"));
25460
+ const openPrs = child2.linkedPrs.filter((p) => prStateIs(p, "OPEN"));
25461
+ const otherPrs = child2.linkedPrs.filter((p) => !prStateIs(p, "MERGED") && !prStateIs(p, "OPEN"));
25462
+ const closed = String(child2.state ?? "").toUpperCase() === "CLOSED";
25463
+ let bucket;
25464
+ if (closed || isBoardDone(child2.boardStatus)) {
25465
+ bucket = "landed";
25466
+ } else if (openPrs.length > 0 || isBoardInFlight(child2.boardStatus)) {
25467
+ bucket = "in_flight";
25468
+ } else if (mergedPrs.length > 0) {
25469
+ bucket = "partial";
25470
+ } else {
25471
+ bucket = "ready";
25472
+ }
25473
+ return {
25474
+ number: child2.number,
25475
+ title: child2.title,
25476
+ state: child2.state,
25477
+ url: child2.url,
25478
+ repo: child2.repo,
25479
+ assignee: child2.assignee,
25480
+ boardStatus: child2.boardStatus,
25481
+ depth: child2.depth,
25482
+ bucket,
25483
+ mergedPrs,
25484
+ openPrs,
25485
+ otherPrs
25486
+ };
25487
+ }
25488
+ function sortFrontierChildren(rows) {
25489
+ return [...rows].sort((a, b) => {
25490
+ const byBucket = FRONTIER_BUCKET_ORDER[a.bucket] - FRONTIER_BUCKET_ORDER[b.bucket];
25491
+ if (byBucket !== 0) return byBucket;
25492
+ return a.number - b.number;
25493
+ });
25494
+ }
25495
+ function buildFrontierReport(epic, children) {
25496
+ const classified = children.map(classifyFrontierChild);
25497
+ const landed = classified.filter((c) => c.bucket === "landed").sort((a, b) => a.number - b.number);
25498
+ const frontier = sortFrontierChildren(classified.filter((c) => c.bucket !== "landed"));
25499
+ const inFlight = frontier.filter((c) => c.bucket === "in_flight").length;
25500
+ const partial = frontier.filter((c) => c.bucket === "partial").length;
25501
+ const ready = frontier.filter((c) => c.bucket === "ready").length;
25502
+ const report = {
25503
+ epic,
25504
+ counts: {
25505
+ total: classified.length,
25506
+ landed: landed.length,
25507
+ frontier: frontier.length,
25508
+ inFlight,
25509
+ partial,
25510
+ ready
25511
+ },
25512
+ landed,
25513
+ frontier,
25514
+ children: classified,
25515
+ summary: ""
25516
+ };
25517
+ report.summary = formatFrontierSummary(report);
25518
+ return report;
25519
+ }
25520
+ function fmtChildLine(c) {
25521
+ const board = c.boardStatus ? ` board=${c.boardStatus}` : "";
25522
+ const merged = c.mergedPrs.length ? ` merged=[${c.mergedPrs.map((p) => `#${p.number}`).join(",")}]` : "";
25523
+ const open2 = c.openPrs.length ? ` open=[${c.openPrs.map((p) => `#${p.number}`).join(",")}]` : "";
25524
+ return ` #${c.number} [${c.bucket}] ${c.title} (${c.state}${board}${merged}${open2})`;
25525
+ }
25526
+ function formatFrontierSummary(report) {
25527
+ const { epic, counts } = report;
25528
+ const lines = [
25529
+ `issue frontier \u2014 ${epic.repo}#${epic.number} ${epic.title} (${epic.state})`,
25530
+ ` ${counts.total} children \xB7 ${counts.landed} landed \xB7 ${counts.frontier} frontier (${counts.inFlight} in-flight, ${counts.partial} partial, ${counts.ready} ready)`
25531
+ ];
25532
+ if (report.frontier.length) {
25533
+ lines.push("frontier:");
25534
+ for (const c of report.frontier) lines.push(fmtChildLine(c));
25535
+ } else {
25536
+ lines.push("frontier: (empty \u2014 every walked child is landed)");
25537
+ }
25538
+ if (report.landed.length) {
25539
+ lines.push("landed:");
25540
+ for (const c of report.landed) lines.push(fmtChildLine(c));
25541
+ }
25542
+ return lines.join("\n");
25543
+ }
25544
+ function frontierEpicGraphqlArgs(owner, name, number) {
25545
+ const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){issue(number:" + number + "){number title state url}}}";
25546
+ return ["api", "graphql", "-f", `query=${query}`, "-f", `owner=${owner}`, "-f", `name=${name}`];
25547
+ }
25548
+ function extractFrontierEpicResponse(resp, repo) {
25549
+ const r = resp ?? {};
25550
+ const issue2 = r.data?.repository?.issue;
25551
+ if (!issue2 || (r.errors ?? []).some((e) => e?.type === "NOT_FOUND")) {
25552
+ throw new QueryReadError("NOT_FOUND", "issue not found");
25553
+ }
25554
+ return {
25555
+ number: Number(issue2.number),
25556
+ title: String(issue2.title ?? ""),
25557
+ state: String(issue2.state ?? ""),
25558
+ url: String(issue2.url ?? ""),
25559
+ repo
25560
+ };
25561
+ }
25562
+ async function runIssueFrontier(deps, epic, opts) {
25563
+ const ref = parseIssueRef(epic);
25564
+ const repo = ref.repo ?? await deps.resolveRepo(void 0);
25565
+ if (!repo) {
25566
+ throw new QueryReadError("BAD_INPUT", `could not resolve repo for #${ref.number} (pass --repo <owner/repo>)`);
25567
+ }
25568
+ const { owner, name } = splitRepo(repo);
25569
+ let headerResp;
25570
+ try {
25571
+ headerResp = await deps.ghJson(frontierEpicGraphqlArgs(owner, name, ref.number), GH_LIST_TIMEOUT_MS);
25572
+ } catch (e) {
25573
+ if (isGhNotFound(e)) {
25574
+ throw new QueryReadError("NOT_FOUND", `#${ref.number} not found in ${repo}`);
25575
+ }
25576
+ throw e;
25577
+ }
25578
+ const epicHeader = extractFrontierEpicResponse(headerResp, repo);
25579
+ const children = await runIssueChildren(deps, `${repo}#${ref.number}`, {
25580
+ recursive: opts.recursive,
25581
+ boardProjectId: opts.boardProjectId
25582
+ });
25583
+ return buildFrontierReport(epicHeader, children);
25584
+ }
25436
25585
  function buildPrListArgs(opts, repo, author) {
25437
25586
  const limit = clampLimit(opts.limit, DEFAULT_LIMIT);
25438
25587
  const state = opts.state ?? "open";
@@ -25609,6 +25758,17 @@ function registerQueryCommands(program3) {
25609
25758
  queryFail("issue children", e);
25610
25759
  }
25611
25760
  });
25761
+ issue2.command("frontier <epic>").description("compute an epic's landed-vs-open frontier from merged PRs + board Status (read-only; never mutates the epic body) \u2014 human summary by default, --json for the structured report").option("--recursive", "walk the full sub-issue tree (bounded depth + total cap)").option("--repo <owner/repo>", "repo for a bare epic ref (defaults to the current repo)").option("--json", "print the structured FrontierReport JSON (includes summary)").action(async (epic, o) => {
25762
+ try {
25763
+ const frontierDeps = { ...deps, resolveRepo: async (r) => deps.resolveRepo(r ?? o.repo) };
25764
+ const boardProjectId = await resolveBoardProjectId(o.repo);
25765
+ const report = await runIssueFrontier(frontierDeps, epic, { recursive: o.recursive, boardProjectId });
25766
+ if (o.json) console.log(JSON.stringify(report, null, 2));
25767
+ else console.log(report.summary);
25768
+ } catch (e) {
25769
+ queryFail("issue frontier", e);
25770
+ }
25771
+ });
25612
25772
  pr2.command("list").description("bounded PR read \u2014 --mine scopes to the viewer's own PRs (resolves the viewer via gh); always prints JSON").option("--mine", "only the caller's own PRs (resolves the viewer via `gh api user`)").addOption(new Option("--state <state>", "open | all").default("open").choices(PR_STATES)).option("--limit <n>", "max PRs to return (capped at 100)", (v) => Number(v), DEFAULT_LIMIT).option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output (already the default \u2014 accepted for contract uniformity)").action(async (o) => {
25613
25773
  try {
25614
25774
  const state = validateEnum("--state", PR_STATES, o.state, "pr list");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.104.0",
3
+ "version": "3.105.2",
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",