@mutmutco/cli 3.90.0 → 3.92.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 +262 -10
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -9313,11 +9313,27 @@ function readKnownMarketplacesFile(path2) {
9313
9313
  return void 0;
9314
9314
  }
9315
9315
  }
9316
- function writeMarketplacePinsOnDisk(path2, pins, succeeded, failedVerb) {
9316
+ function claudeCodeIsRunning(env = process.env, listProcesses = defaultProcessList) {
9317
+ if (env.CLAUDE_CODE_SESSION_ID?.trim() || env.CLAUDECODE?.trim() || env.CLAUDE_PLUGIN_ROOT?.trim()) return true;
9318
+ let table;
9319
+ try {
9320
+ table = listProcesses();
9321
+ } catch {
9322
+ return true;
9323
+ }
9324
+ if (!table.trim()) return true;
9325
+ return table.split(/\r?\n/).some((line) => /(^|[/\\])claude(\.(exe|cmd|ps1))?\s/.test(`${line.trim()} `));
9326
+ }
9327
+ function defaultProcessList() {
9328
+ return isWin ? (0, import_node_child_process6.execFileSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.CommandLine }"], { encoding: "utf8", windowsHide: true, maxBuffer: 32 * 1024 * 1024, timeout: 15e3 }) : (0, import_node_child_process6.execFileSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
9329
+ }
9330
+ function writeMarketplacePinsOnDisk(path2, pins, succeeded, failedVerb, declineWhileHostLive) {
9317
9331
  if (pins.size === 0) return void 0;
9318
9332
  const after = readKnownMarketplacesFile(path2);
9319
9333
  const next = restoreMarketplacePins(after, pins);
9320
9334
  if (next === null) return void 0;
9335
+ const declined = declineWhileHostLive?.();
9336
+ if (declined) return declined;
9321
9337
  try {
9322
9338
  (0, import_node_fs15.writeFileSync)(path2, next, "utf8");
9323
9339
  } catch {
@@ -9330,16 +9346,27 @@ function writeMarketplacePinsOnDisk(path2, pins, succeeded, failedVerb) {
9330
9346
  });
9331
9347
  return failed.length ? `${failedVerb} did NOT take for ${failed.map(([n]) => n).join(", ")} \u2014 set it by hand` : succeeded(pins);
9332
9348
  }
9333
- function restoreMarketplacePinsOnDisk(path2, pins) {
9334
- return writeMarketplacePinsOnDisk(path2, pins, restoredPinsLine, "restore");
9335
- }
9336
- function applyOrgMarketplacePins(path2, names) {
9349
+ function restoreMarketplacePinsOnDisk(path2, pins, hostIsRunning = claudeCodeIsRunning) {
9337
9350
  return writeMarketplacePinsOnDisk(
9351
+ path2,
9352
+ pins,
9353
+ (restored) => hostIsRunning() ? `${restoredPinsLine(restored)}, but Claude Code is running and can rewrite this file from its own copy \u2014 restart it, then \`mmi-cli doctor --apply\` if the pins did not survive` : restoredPinsLine(restored),
9354
+ "restore"
9355
+ );
9356
+ }
9357
+ function applyOrgMarketplacePins(path2, names, hostIsRunning = claudeCodeIsRunning) {
9358
+ let landed = false;
9359
+ const detail = writeMarketplacePinsOnDisk(
9338
9360
  path2,
9339
9361
  new Map(names.map((name) => [name, ORG_MARKETPLACE_PINS])),
9340
- (pins) => `pinned ${[...pins.keys()].join(", ")} to ${ORG_MARKETPLACE_PINS.ref} with auto-update on`,
9341
- "pin"
9362
+ (pins) => {
9363
+ landed = true;
9364
+ return `pinned ${[...pins.keys()].join(", ")} to ${ORG_MARKETPLACE_PINS.ref} with auto-update on`;
9365
+ },
9366
+ "pin",
9367
+ () => hostIsRunning() ? "not pinned \u2014 Claude Code is running and rewrites this registration from its own copy; quit it, then run `mmi-cli doctor --apply`" : void 0
9342
9368
  );
9369
+ return detail === void 0 ? void 0 : { detail, wrote: landed };
9343
9370
  }
9344
9371
  function writeMarketplacePinPending(path2, names, now = Date.now()) {
9345
9372
  try {
@@ -22949,12 +22976,42 @@ var canonicalPriorityColors = { Urgent: "RED", High: "ORANGE", Medium: "YELLOW",
22949
22976
  function miscoloredOptions(options, canon) {
22950
22977
  return (options ?? []).filter((o) => canon[o.name] != null && o.color != null && o.color !== canon[o.name]).map((o) => `${o.name}=${o.color} (want ${canon[o.name]})`);
22951
22978
  }
22979
+ function missingBoardViews(views, required) {
22980
+ return required.filter((req) => !views.some((v) => v.name === req.name && v.layout === req.layout));
22981
+ }
22982
+ function boardGroupingDrift(board, wantColumn, wantSwimlane) {
22983
+ if (!board) return ["no Board view found"];
22984
+ const problems = [];
22985
+ if (!board.verticalGroupByFields.includes(wantColumn)) {
22986
+ problems.push(`columns: ${board.verticalGroupByFields.join(", ") || "none"} (want ${wantColumn})`);
22987
+ }
22988
+ if (!board.groupByFields.includes(wantSwimlane)) {
22989
+ problems.push(`swimlanes: ${board.groupByFields.join(", ") || "none"} (want ${wantSwimlane})`);
22990
+ }
22991
+ return problems;
22992
+ }
22993
+ function boardCardFieldDrift(visibleFields, canonical) {
22994
+ const have = new Set(visibleFields);
22995
+ const want = new Set(canonical);
22996
+ return {
22997
+ missing: canonical.filter((f) => !have.has(f)),
22998
+ extra: visibleFields.filter((f) => !want.has(f))
22999
+ };
23000
+ }
22952
23001
  var requiredProjectWorkflows = [
22953
23002
  "Auto-add sub-issues to project",
22954
23003
  "Auto-archive items",
22955
23004
  "Item added to project",
22956
23005
  "Item closed"
22957
23006
  ];
23007
+ var requiredBoardViews = [
23008
+ { name: "List", layout: "TABLE_LAYOUT" },
23009
+ { name: "Board", layout: "BOARD_LAYOUT" },
23010
+ { name: "Roadmap", layout: "ROADMAP_LAYOUT" }
23011
+ ];
23012
+ var requiredBoardColumnField = "Status";
23013
+ var requiredBoardSwimlaneField = "Repository";
23014
+ var requiredBoardCardFields = ["Title", "Assignees", "Status", "Labels", "Linked pull requests", "Parent issue", "Sub-issues progress", "Priority"];
22958
23015
  var requiredOrgRulesetTypes = ["pull_request", "non_fast_forward", "deletion"];
22959
23016
  var requiredHubStatusChecks = ["cli", "infra", "docs"];
22960
23017
  var requiredProductStatusChecks = ["gate"];
@@ -23303,6 +23360,48 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
23303
23360
  label: `Project workflow enabled: ${workflowName}`
23304
23361
  });
23305
23362
  }
23363
+ const viewsQuery = `query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { views(first: 10) { nodes { name layout groupByFields(first: 5) { nodes { ... on ProjectV2FieldCommon { name } } } verticalGroupByFields(first: 5) { nodes { ... on ProjectV2FieldCommon { name } } } visibleFields(first: 20) { nodes { ... on ProjectV2FieldCommon { name } } } } } } } }`;
23364
+ const boardViews = await (async () => {
23365
+ try {
23366
+ const data = await deps.client.graphql(viewsQuery, {
23367
+ login: config.projectOwner,
23368
+ number: config.projectNumber
23369
+ });
23370
+ const nodes = data.organization?.projectV2?.views?.nodes ?? [];
23371
+ const names = (conn) => (conn?.nodes ?? []).filter((f) => Boolean(f?.name)).map((f) => f.name);
23372
+ return nodes.filter((v) => Boolean(v)).map((v) => ({
23373
+ name: v.name,
23374
+ layout: v.layout,
23375
+ groupByFields: names(v.groupByFields),
23376
+ verticalGroupByFields: names(v.verticalGroupByFields),
23377
+ visibleFields: names(v.visibleFields)
23378
+ }));
23379
+ } catch {
23380
+ return [];
23381
+ }
23382
+ })();
23383
+ const missingViews = missingBoardViews(boardViews, requiredBoardViews);
23384
+ checks.push({
23385
+ ok: missingViews.length === 0,
23386
+ label: "Project view triple present: List/Board/Roadmap (#4093)",
23387
+ detail: missingViews.length ? `missing: ${missingViews.map((v) => `${v.name} (${v.layout})`).join(", ")} \u2014 createProjectV2View/updateProjectV2View is API-writable (name + layout)` : void 0
23388
+ });
23389
+ const boardView = boardViews.find((v) => v.layout === "BOARD_LAYOUT");
23390
+ const groupingDrift = boardGroupingDrift(boardView, requiredBoardColumnField, requiredBoardSwimlaneField);
23391
+ checks.push({
23392
+ ok: groupingDrift.length === 0,
23393
+ label: `Board view grouped by ${requiredBoardColumnField} columns / ${requiredBoardSwimlaneField} swimlanes (#4093)`,
23394
+ detail: groupingDrift.length ? `${groupingDrift.join("; ")} \u2014 GitHub exposes no create/update mutation for view grouping (ProjectV2ViewConfigurationInput carries only visibleFieldIds); fix in the UI: Board view \u2192 \u2699 (top-right) \u2192 Group by \u2192 ${requiredBoardColumnField}, Swimlanes \u2192 ${requiredBoardSwimlaneField} \u2192 Save view` : void 0
23395
+ });
23396
+ const cardDrift = boardCardFieldDrift(boardView?.visibleFields ?? [], requiredBoardCardFields);
23397
+ checks.push({
23398
+ ok: cardDrift.missing.length === 0 && cardDrift.extra.length === 0,
23399
+ label: "Board view card fields match the org standard (#4093)",
23400
+ detail: cardDrift.missing.length || cardDrift.extra.length ? `${[
23401
+ cardDrift.missing.length ? `missing: ${cardDrift.missing.join(", ")}` : null,
23402
+ cardDrift.extra.length ? `extra: ${cardDrift.extra.join(", ")}` : null
23403
+ ].filter(Boolean).join("; ")} \u2014 API-writable via updateProjectV2View(configuration:{visibleFieldIds:[...]}); fix in the UI (Board view \u2192 Fields) or that mutation` : void 0
23404
+ });
23306
23405
  }
23307
23406
  const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && projectRegistryIncludesRepo(json.projects, repo));
23308
23407
  if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "project registry includes repo" });
@@ -25480,6 +25579,88 @@ async function fetchRestCorePool(gh = defaultGhApi) {
25480
25579
  }
25481
25580
  }
25482
25581
 
25582
+ // src/pr-create-docs-check.ts
25583
+ var GIT_TIMEOUT_MS2 = 15e3;
25584
+ function createPrCreateDocsIndexDeps() {
25585
+ return {
25586
+ worktreeRoot: async () => {
25587
+ try {
25588
+ const { stdout } = await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS2 });
25589
+ return stdout.trim() || void 0;
25590
+ } catch {
25591
+ return void 0;
25592
+ }
25593
+ },
25594
+ originRepo: async (root) => {
25595
+ try {
25596
+ const { stdout } = await execFileP2("git", ["-C", root, "remote", "get-url", "origin"], { timeout: GIT_TIMEOUT_MS2 });
25597
+ return repoFromRemoteUrl(stdout.trim());
25598
+ } catch {
25599
+ return void 0;
25600
+ }
25601
+ },
25602
+ refResolves: async (root, ref) => {
25603
+ try {
25604
+ await execFileP2("git", ["-C", root, "rev-parse", "--verify", "--quiet", `${ref}^{commit}`], { timeout: GIT_TIMEOUT_MS2 });
25605
+ return true;
25606
+ } catch {
25607
+ return false;
25608
+ }
25609
+ },
25610
+ listDocsAtRef: async (root, ref) => {
25611
+ try {
25612
+ const { stdout } = await execFileP2("git", ["-C", root, "ls-tree", "-r", "--name-only", ref, "--", "docs"], { timeout: GIT_TIMEOUT_MS2 });
25613
+ return stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
25614
+ } catch {
25615
+ return [];
25616
+ }
25617
+ },
25618
+ readAtRef: async (root, ref, path2) => {
25619
+ try {
25620
+ const { stdout } = await execFileP2("git", ["-C", root, "show", `${ref}:${path2}`], { timeout: GIT_TIMEOUT_MS2 });
25621
+ return stdout;
25622
+ } catch {
25623
+ return void 0;
25624
+ }
25625
+ }
25626
+ };
25627
+ }
25628
+ async function checkDocsIndexAtHead(opts, deps) {
25629
+ const root = await deps.worktreeRoot();
25630
+ if (!root) return void 0;
25631
+ if (opts.repo) {
25632
+ const origin = await deps.originRepo(root);
25633
+ if (!origin || origin.toLowerCase() !== opts.repo.toLowerCase()) return void 0;
25634
+ }
25635
+ const ref = opts.head || "HEAD";
25636
+ if (!await deps.refResolves(root, ref)) return void 0;
25637
+ const paths = await deps.listDocsAtRef(root, ref);
25638
+ if (!paths.includes(DOCS_INDEX_PATH)) return void 0;
25639
+ const relDocs = paths.filter((p) => p.startsWith("docs/") && p.endsWith(".md")).map((p) => p.slice("docs/".length)).filter(isRoutableDocsPath);
25640
+ const [indexContent, docContents] = await Promise.all([
25641
+ deps.readAtRef(root, ref, DOCS_INDEX_PATH),
25642
+ Promise.all(relDocs.map((rel) => deps.readAtRef(root, ref, `docs/${rel}`)))
25643
+ ]);
25644
+ const contentByPath = new Map(relDocs.map((rel, i) => [rel, docContents[i] ?? ""]));
25645
+ const atRefDeps = {
25646
+ listDocs: () => relDocs,
25647
+ readDoc: (rel) => contentByPath.get(rel) ?? "",
25648
+ readIndex: () => indexContent ?? null,
25649
+ writeIndex: () => {
25650
+ throw new Error("checkDocsIndexAtHead: read-only \u2014 a commit ref is never written to");
25651
+ }
25652
+ };
25653
+ const result = docsIndex(atRefDeps, { check: true });
25654
+ if (!result.drift) {
25655
+ return { ok: true, detail: `${DOCS_INDEX_PATH} matches the docs/ tree at ${ref === "HEAD" ? "HEAD" : ref}` };
25656
+ }
25657
+ return {
25658
+ ok: false,
25659
+ detail: `${DOCS_INDEX_PATH} is stale at the commit this PR would open from \u2014 it no longer matches the docs/ tree there`,
25660
+ fix: "run `mmi-cli docs index --write`, commit docs/index.md, and push again before retrying `pr create`"
25661
+ };
25662
+ }
25663
+
25483
25664
  // src/worktree-lifecycle-commands.ts
25484
25665
  var import_node_fs31 = require("node:fs");
25485
25666
  var import_promises9 = require("node:fs/promises");
@@ -28972,6 +29153,32 @@ function checkSessionPayload(probe) {
28972
29153
  verbose: evidence
28973
29154
  };
28974
29155
  }
29156
+ function checkDocsIndex(probe) {
29157
+ if (!probe) return null;
29158
+ const evidence = [
29159
+ // The WORKING TREE, said out loud. This row reads disk and never asks git what is staged or committed,
29160
+ // so it must not claim to have measured the commit: a developer who ran `--write` and has not committed
29161
+ // has a current tree and a stale HEAD, and a row that said "committed: current" there would be the one
29162
+ // failure this check exists to prevent, one layer down. Closing that gap belongs to the surface that
29163
+ // knows about commits (`pr create`, #4092), not to a local hygiene reading.
29164
+ `${DOCS_INDEX_PATH}: read from the working tree, not from HEAD`,
29165
+ `records indexed: ${probe.docCount}`,
29166
+ // Named because it is the one thing a Windows operator reproducing a CI failure needs to trust the
29167
+ // verdict: the comparison normalizes CRLF, so this row and the Linux gate agree on the same content (#3411).
29168
+ "comparison: eol-insensitive, against the index the generator renders now"
29169
+ ];
29170
+ if (!probe.drift) {
29171
+ return { ok: true, id: "docs-index", label: "docs index", detail: "current", verbose: evidence };
29172
+ }
29173
+ return {
29174
+ ok: false,
29175
+ id: "docs-index",
29176
+ label: "docs index",
29177
+ detail: "stale \u2014 docs/index.md no longer matches the docs/ tree",
29178
+ fix: "run `mmi-cli docs index --write` and commit docs/index.md",
29179
+ verbose: evidence
29180
+ };
29181
+ }
28975
29182
  function planGitignore(current) {
28976
29183
  const { content, changed } = upsertManagedGitignoreBlock(current);
28977
29184
  return changed ? { ok: false, content } : { ok: true };
@@ -29119,8 +29326,8 @@ async function runDoctorClean(opts, io, deps) {
29119
29326
  if (applyEnv) {
29120
29327
  const healed = deps.healMarketplacePins();
29121
29328
  if (healed) {
29122
- healIntent(`marketplace pins \u2014 ${healed}`);
29123
- restartPending = true;
29329
+ healIntent(`marketplace pins \u2014 ${healed.detail}`);
29330
+ if (healed.wrote) restartPending = true;
29124
29331
  }
29125
29332
  }
29126
29333
  for (const row of deps.marketplaceRows()) emitNow(row);
@@ -29139,6 +29346,28 @@ async function runDoctorClean(opts, io, deps) {
29139
29346
  });
29140
29347
  }
29141
29348
  }
29349
+ async function runDocsIndexRow() {
29350
+ let probe;
29351
+ try {
29352
+ probe = deps.docsIndexState(await deps.repoRoot());
29353
+ } catch (e) {
29354
+ const message = e instanceof Error ? e.message : String(e);
29355
+ emitNow({
29356
+ ok: false,
29357
+ id: "docs-index",
29358
+ label: "docs index",
29359
+ // The measurement goes in `detail` and the one next action in `fix`, per docs/doctor-contract.md
29360
+ // § check model — never the error text stapled to the front of the advice, and never "re-run the
29361
+ // read that just failed" as the action.
29362
+ detail: `could not be read \u2014 ${message}`,
29363
+ fix: "repair the docs/ tree this repo cannot walk, then re-run doctor",
29364
+ verbose: [`probe threw: ${message}`]
29365
+ });
29366
+ return;
29367
+ }
29368
+ const docs2 = checkDocsIndex(probe);
29369
+ if (docs2) emitNow(docs2);
29370
+ }
29142
29371
  async function runHousekeeperRows() {
29143
29372
  const repoRoot2 = await deps.repoRoot();
29144
29373
  try {
@@ -29209,6 +29438,9 @@ async function runDoctorClean(opts, io, deps) {
29209
29438
  { id: "plugin-cache", when: true, run: runPluginCacheRow },
29210
29439
  { id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
29211
29440
  { id: "marketplace", when: true, run: runMarketplaceRows },
29441
+ // A `docs/` tree walk plus one batched `git check-ignore` — cheap next to the two rows below it, but a
29442
+ // disk walk and a subprocess all the same, so full lane only (#4091).
29443
+ { id: "docs-index", when: isOrgRepo && lane.full, run: runDocsIndexRow },
29212
29444
  // The two most expensive things in this file — a real `git fetch` plus train-branch fast-forward,
29213
29445
  // and a `gh`-backed gc sweep with a 20s timeout — so org repos on the full lane only (#3485).
29214
29446
  { id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
@@ -29650,13 +29882,29 @@ function mmiDoctorDeps(opts = {}) {
29650
29882
  const home = (0, import_node_os14.homedir)();
29651
29883
  const names = [MMI_MARKETPLACE_NAME];
29652
29884
  const result = applyOrgMarketplacePins((0, import_node_path35.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), names);
29653
- if (result?.startsWith("pinned ")) {
29885
+ if (result?.wrote) {
29654
29886
  writeMarketplacePinPending((0, import_node_path35.join)(home, ".claude", "plugins", ".mmi-marketplace-pin-pending.json"), names);
29655
29887
  }
29656
29888
  return result;
29657
29889
  } catch {
29658
29890
  return void 0;
29659
29891
  }
29892
+ },
29893
+ // #4091: the same `docs index --check` comparison the required CI gate runs, in-process. Rooted at the
29894
+ // repo root doctor already resolved — never `process.cwd()`, which would have `mmi-cli doctor` run from
29895
+ // `cli/` measure a `cli/docs/` tree that does not exist.
29896
+ //
29897
+ // `existsSync` on `docs/index.md` is the adoption gate, and it sits UNDER the table's org-repo gate
29898
+ // rather than replacing it: a missing index is drift by construction, so without this a repo that never
29899
+ // adopted the generated routing index (MMG-Unlive: `docs/Archive/**` only, no index, no gate.yml) would
29900
+ // get a permanent ✗ demanding an artifact it never asked for.
29901
+ docsIndexState: (root) => {
29902
+ if (!(0, import_node_fs36.existsSync)((0, import_node_path35.join)(root, DOCS_INDEX_PATH))) return void 0;
29903
+ const real = createDocsIndexDeps(root);
29904
+ let docs2;
29905
+ const listDocs = () => docs2 ??= real.listDocs();
29906
+ const drift = docsIndex({ ...real, listDocs }, { check: true }).drift;
29907
+ return { drift, docCount: listDocs().length };
29660
29908
  }
29661
29909
  };
29662
29910
  }
@@ -31501,6 +31749,10 @@ withExamples(pr.command("create").description("create a PR and print {number,url
31501
31749
  return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
31502
31750
  }
31503
31751
  body = normalizeClosingDirectives(body);
31752
+ const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
31753
+ if (docsCheck && !docsCheck.ok) {
31754
+ return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
31755
+ }
31504
31756
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
31505
31757
  console.log(JSON.stringify(created));
31506
31758
  }), [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.90.0",
3
+ "version": "3.92.0",
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",