@mutmutco/cli 3.52.0 → 3.53.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 +237 -156
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -6229,11 +6229,27 @@ function resolveReleaseTrack(meta, hints, repo) {
6229
6229
  if (inferred) return inferred;
6230
6230
  return "full";
6231
6231
  }
6232
+ function metaWithDeclaredClass(meta, declaredClass) {
6233
+ if (!declaredClass) return meta;
6234
+ const stated = typeof meta?.releaseTrack === "string" || typeof meta?.class === "string" || typeof meta?.deployModel === "string";
6235
+ if (stated) return meta;
6236
+ return { ...meta ?? {}, class: declaredClass };
6237
+ }
6232
6238
  function branchesForTrack(track) {
6233
6239
  if (track === "trunk") return ["main"];
6234
6240
  if (track === "direct") return ["development", "main"];
6235
6241
  return ["development", "rc", "main"];
6236
6242
  }
6243
+ function promotionBranchesForTrack(track) {
6244
+ if (track === "trunk") return [];
6245
+ if (track === "direct") return ["main"];
6246
+ return ["rc", "main"];
6247
+ }
6248
+ function isPromotionBase(base, track) {
6249
+ if (!base) return false;
6250
+ if (!track) return base !== "development";
6251
+ return promotionBranchesForTrack(track).includes(base);
6252
+ }
6237
6253
 
6238
6254
  // src/readiness-audit.ts
6239
6255
  var TENANT_DEPLOY_RUN_SCAN_LIMIT = 100;
@@ -10018,6 +10034,112 @@ function planManagedGitignore(current) {
10018
10034
  return { changed, content, added, removed };
10019
10035
  }
10020
10036
 
10037
+ // src/docs-index-command.ts
10038
+ var import_node_fs15 = require("node:fs");
10039
+ var import_node_path13 = require("node:path");
10040
+ var DOCS_INDEX_PATH = "docs/index.md";
10041
+ function isRoutableDocsPath(relPath) {
10042
+ const normalized = relPath.replace(/\\/g, "/");
10043
+ return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
10044
+ }
10045
+ var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
10046
+ function plainInline(text) {
10047
+ return text.replace(/\r?\n/g, " ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`/g, "").replace(/\*+/g, "").replace(/~~/g, "").replace(/__([^_]+)__/g, "$1").replace(/(^|[\s(])_([^_]+)_($|[\s).,!?:;])/g, "$1$2$3").replace(/^>\s*/, "").replace(/\\([\\`*_\[\]{}()#+.!|>-])/g, "$1").replace(/\s+/g, " ").trim();
10048
+ }
10049
+ function extractDocMeta(relPath, raw) {
10050
+ const lines = raw.split(/\r?\n/);
10051
+ let i = 0;
10052
+ if (lines[i]?.trim() === "---") {
10053
+ i++;
10054
+ while (i < lines.length && lines[i].trim() !== "---") i++;
10055
+ if (i < lines.length) i++;
10056
+ }
10057
+ let title = "";
10058
+ let scope = "";
10059
+ for (; i < lines.length; i++) {
10060
+ const line = lines[i];
10061
+ const trimmed = line.trim();
10062
+ if (trimmed === "") continue;
10063
+ if (trimmed.startsWith("<!--")) {
10064
+ while (i < lines.length && !lines[i].includes("-->")) i++;
10065
+ continue;
10066
+ }
10067
+ if (!title && trimmed.startsWith("# ")) {
10068
+ title = trimmed.slice(2).trim();
10069
+ continue;
10070
+ }
10071
+ if (!title) {
10072
+ break;
10073
+ }
10074
+ if (trimmed.startsWith("#")) continue;
10075
+ scope = plainInline(trimmed);
10076
+ break;
10077
+ }
10078
+ if (!title) {
10079
+ const base = relPath.split("/").pop() ?? relPath;
10080
+ title = base.replace(/\.md$/, "");
10081
+ }
10082
+ return { path: relPath, title, scope };
10083
+ }
10084
+ function renderDocsIndex(entries) {
10085
+ const lines = [
10086
+ GENERATED_HEADER,
10087
+ "",
10088
+ "# Documentation index",
10089
+ "",
10090
+ "Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
10091
+ "can never diverge from the records it lists.",
10092
+ ""
10093
+ ];
10094
+ for (const entry of entries) {
10095
+ const link = `[${plainInline(entry.title)}](${entry.path})`;
10096
+ lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
10097
+ }
10098
+ return lines.join("\n") + "\n";
10099
+ }
10100
+ function sameContent(a, b) {
10101
+ const normalize = (text) => text.replace(/\r\n/g, "\n");
10102
+ return normalize(a) === normalize(b);
10103
+ }
10104
+ function docsIndex(deps, opts = {}) {
10105
+ const entries = deps.listDocs().slice().sort().map((relPath) => extractDocMeta(relPath, deps.readDoc(relPath)));
10106
+ const content = renderDocsIndex(entries);
10107
+ const current = deps.readIndex();
10108
+ const drift = current === null || !sameContent(current, content);
10109
+ let wrote = false;
10110
+ if (!opts.check && drift) {
10111
+ deps.writeIndex(content);
10112
+ wrote = true;
10113
+ }
10114
+ return { content, drift, wrote };
10115
+ }
10116
+ function walkMarkdown(dir) {
10117
+ const out = [];
10118
+ const stack = [dir];
10119
+ while (stack.length) {
10120
+ const current = stack.pop();
10121
+ for (const entry of (0, import_node_fs15.readdirSync)(current, { withFileTypes: true })) {
10122
+ const full = (0, import_node_path13.join)(current, entry.name);
10123
+ if (entry.isDirectory()) {
10124
+ stack.push(full);
10125
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
10126
+ out.push((0, import_node_path13.relative)(dir, full).split(import_node_path13.sep).join("/"));
10127
+ }
10128
+ }
10129
+ }
10130
+ return out;
10131
+ }
10132
+ function createDocsIndexDeps(repoRoot2) {
10133
+ const docsDir = (0, import_node_path13.join)(repoRoot2, "docs");
10134
+ const indexPath = (0, import_node_path13.join)(repoRoot2, DOCS_INDEX_PATH);
10135
+ return {
10136
+ listDocs: () => (0, import_node_fs15.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
10137
+ readDoc: (relPath) => (0, import_node_fs15.readFileSync)((0, import_node_path13.join)(docsDir, relPath), "utf8"),
10138
+ readIndex: () => (0, import_node_fs15.existsSync)(indexPath) ? (0, import_node_fs15.readFileSync)(indexPath, "utf8") : null,
10139
+ writeIndex: (content) => (0, import_node_fs15.writeFileSync)(indexPath, content, "utf8")
10140
+ };
10141
+ }
10142
+
10021
10143
  // src/gate-budget.ts
10022
10144
  var BLESSED_RUN_WITH_BUDGET_SHA = "ea2cf8710949dec53566c29836e29cd7515a4e23";
10023
10145
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
@@ -10264,6 +10386,7 @@ function withDerivedRepoVars(vars, parsed, cls, releaseTrack) {
10264
10386
  out.REPO_NAME ??= parsed.name;
10265
10387
  out.REPO_SLUG ??= parsed.slug;
10266
10388
  out.CLASS ??= cls;
10389
+ out.PROJECT_OWNER ??= parsed.owner;
10267
10390
  const track = releaseTrack ?? resolveBootstrapReleaseTrack(cls);
10268
10391
  const runtime = out.GATE_RUNTIME === "python" ? "python" : "node";
10269
10392
  for (const [key, value] of Object.entries(gateSeedVars(cls, track, runtime))) {
@@ -10464,6 +10587,26 @@ function boardFieldsQueryArgs(projectId) {
10464
10587
  const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
10465
10588
  return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
10466
10589
  }
10590
+ function seededDocsIndex(seeds) {
10591
+ const docs2 = seeds.map((s) => ({ rel: s.path.replace(/\\/g, "/"), content: s.content })).filter((s) => s.rel.startsWith("docs/") && s.rel.endsWith(".md")).map((s) => ({ rel: s.rel.slice("docs/".length), content: s.content })).filter((s) => isRoutableDocsPath(s.rel));
10592
+ if (!docs2.length) return null;
10593
+ const entries = docs2.slice().sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0).map((s) => extractDocMeta(s.rel, s.content));
10594
+ return renderDocsIndex(entries);
10595
+ }
10596
+ function linkedProjectsQueryArgs(owner, name) {
10597
+ const query = "query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { projectsV2(first: 10) { nodes { id number title } } } }";
10598
+ return ["api", "graphql", "-f", `query=${query}`, "-f", `owner=${owner}`, "-f", `name=${name}`];
10599
+ }
10600
+ function pickLinkedProject(json) {
10601
+ const nodes = json?.data?.repository?.projectsV2?.nodes;
10602
+ if (!Array.isArray(nodes)) return void 0;
10603
+ const usable = nodes.filter(
10604
+ (n) => typeof n?.id === "string" && Boolean(n.id)
10605
+ );
10606
+ if (usable.length !== 1) return void 0;
10607
+ const only = usable[0];
10608
+ return typeof only.number === "number" && Number.isFinite(only.number) ? { id: only.id, number: only.number } : { id: only.id };
10609
+ }
10467
10610
  function decideSeedPrAction(openPrs) {
10468
10611
  const list = Array.isArray(openPrs) ? openPrs : [];
10469
10612
  for (const pr2 of list) {
@@ -11112,12 +11255,12 @@ async function runPrLand(prNumber, options, deps) {
11112
11255
  }
11113
11256
 
11114
11257
  // src/wave-status.ts
11115
- var import_node_fs16 = require("node:fs");
11258
+ var import_node_fs17 = require("node:fs");
11116
11259
 
11117
11260
  // src/stage-runner.ts
11118
11261
  var import_node_child_process7 = require("node:child_process");
11119
- var import_node_fs15 = require("node:fs");
11120
- var import_node_path13 = require("node:path");
11262
+ var import_node_fs16 = require("node:fs");
11263
+ var import_node_path14 = require("node:path");
11121
11264
  var import_node_net = require("node:net");
11122
11265
  var import_node_util6 = require("node:util");
11123
11266
 
@@ -11256,11 +11399,11 @@ function appendForceRecreate(up) {
11256
11399
  return `${up.trimEnd()} --force-recreate`;
11257
11400
  }
11258
11401
  function stageStatePath(cwd = process.cwd()) {
11259
- return (0, import_node_path13.join)(cwd, "tmp", "stage", "state.json");
11402
+ return (0, import_node_path14.join)(cwd, "tmp", "stage", "state.json");
11260
11403
  }
11261
11404
  function stageGlobalStatePath(cwd = process.cwd(), gitCommonDir = ".git") {
11262
- const dir = (0, import_node_path13.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path13.resolve)(cwd, gitCommonDir);
11263
- return (0, import_node_path13.join)(dir, "mmi", "stage", "state.json");
11405
+ const dir = (0, import_node_path14.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path14.resolve)(cwd, gitCommonDir);
11406
+ return (0, import_node_path14.join)(dir, "mmi", "stage", "state.json");
11264
11407
  }
11265
11408
  function normPath2(path2) {
11266
11409
  return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -11484,14 +11627,14 @@ function stageProcessEnv(stagePort, extraEnv) {
11484
11627
  }
11485
11628
  async function ensureStageRuntimeEnv(config, opts, cwd) {
11486
11629
  if (!config.ensureEnv) return;
11487
- const target = (0, import_node_path13.join)(cwd, config.ensureEnv.target);
11488
- const example = (0, import_node_path13.join)(cwd, config.ensureEnv.example);
11489
- if (!(0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
11490
- (0, import_node_fs15.copyFileSync)(example, target);
11491
- } else if ((0, import_node_fs15.existsSync)(target) && (0, import_node_fs15.existsSync)(example)) {
11492
- const stale = detectStaleEnvFile((0, import_node_fs15.readFileSync)(example, "utf8"), (0, import_node_fs15.readFileSync)(target, "utf8"), {
11493
- exampleMtimeMs: (0, import_node_fs15.statSync)(example).mtimeMs,
11494
- targetMtimeMs: (0, import_node_fs15.statSync)(target).mtimeMs
11630
+ const target = (0, import_node_path14.join)(cwd, config.ensureEnv.target);
11631
+ const example = (0, import_node_path14.join)(cwd, config.ensureEnv.example);
11632
+ if (!(0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
11633
+ (0, import_node_fs16.copyFileSync)(example, target);
11634
+ } else if ((0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
11635
+ const stale = detectStaleEnvFile((0, import_node_fs16.readFileSync)(example, "utf8"), (0, import_node_fs16.readFileSync)(target, "utf8"), {
11636
+ exampleMtimeMs: (0, import_node_fs16.statSync)(example).mtimeMs,
11637
+ targetMtimeMs: (0, import_node_fs16.statSync)(target).mtimeMs
11495
11638
  });
11496
11639
  if (stale) {
11497
11640
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -11499,8 +11642,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
11499
11642
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
11500
11643
  }
11501
11644
  }
11502
- if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs15.existsSync)(target)) {
11503
- (0, import_node_fs15.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs15.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
11645
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs16.existsSync)(target)) {
11646
+ (0, import_node_fs16.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs16.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
11504
11647
  }
11505
11648
  }
11506
11649
  async function gitText(cwd, args) {
@@ -11530,20 +11673,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
11530
11673
  return void 0;
11531
11674
  }
11532
11675
  function readState(path2) {
11533
- if (!(0, import_node_fs15.existsSync)(path2)) return null;
11676
+ if (!(0, import_node_fs16.existsSync)(path2)) return null;
11534
11677
  try {
11535
- return JSON.parse((0, import_node_fs15.readFileSync)(path2, "utf8"));
11678
+ return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
11536
11679
  } catch {
11537
11680
  return null;
11538
11681
  }
11539
11682
  }
11540
11683
  function mkdirFor(path2) {
11541
11684
  const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
11542
- (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
11685
+ (0, import_node_fs16.mkdirSync)(dir, { recursive: true });
11543
11686
  }
11544
11687
  function writeState(path2, state) {
11545
11688
  mkdirFor(path2);
11546
- (0, import_node_fs15.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
11689
+ (0, import_node_fs16.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
11547
11690
  }
11548
11691
  function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
11549
11692
  const reservation = {
@@ -11563,7 +11706,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
11563
11706
  await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
11564
11707
  }
11565
11708
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
11566
- (0, import_node_fs15.rmSync)(path2, { force: true });
11709
+ (0, import_node_fs16.rmSync)(path2, { force: true });
11567
11710
  }
11568
11711
  }
11569
11712
  async function killTree(pid) {
@@ -11721,8 +11864,8 @@ async function runStage(config = {}, opts = {}) {
11721
11864
  await ensureStageRuntimeEnv(config, opts, cwd);
11722
11865
  if (build) await shell(sub(build), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
11723
11866
  } catch (e) {
11724
- (0, import_node_fs15.rmSync)(statePath, { force: true });
11725
- if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs15.rmSync)(globalStatePath, { force: true });
11867
+ (0, import_node_fs16.rmSync)(statePath, { force: true });
11868
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs16.rmSync)(globalStatePath, { force: true });
11726
11869
  throw e;
11727
11870
  }
11728
11871
  const started = await startStage(config, {
@@ -11743,9 +11886,9 @@ function parseNextFromHead(headText) {
11743
11886
  }
11744
11887
  function readStageSummary(worktreePath) {
11745
11888
  const statePath = stageStatePath(worktreePath);
11746
- if (!(0, import_node_fs16.existsSync)(statePath)) return void 0;
11889
+ if (!(0, import_node_fs17.existsSync)(statePath)) return void 0;
11747
11890
  try {
11748
- const state = JSON.parse((0, import_node_fs16.readFileSync)(statePath, "utf8"));
11891
+ const state = JSON.parse((0, import_node_fs17.readFileSync)(statePath, "utf8"));
11749
11892
  const port = typeof state.port === "number" ? state.port : void 0;
11750
11893
  if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
11751
11894
  return { port, url: typeof state.url === "string" ? state.url : void 0 };
@@ -11864,7 +12007,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
11864
12007
  // src/gh-create.ts
11865
12008
  var import_promises = require("node:fs/promises");
11866
12009
  var import_node_os3 = require("node:os");
11867
- var import_node_path14 = require("node:path");
12010
+ var import_node_path15 = require("node:path");
11868
12011
  var import_node_crypto3 = require("node:crypto");
11869
12012
  var ISSUE_TYPES = ["bug", "feature", "task"];
11870
12013
  var GH_MUTATION_TIMEOUT_MS = 12e4;
@@ -11918,8 +12061,8 @@ async function bodyArgsViaFile(args, deps = {}) {
11918
12061
  const remove2 = deps.remove ?? import_promises.unlink;
11919
12062
  const ensureDir = deps.ensureDir ?? import_promises.mkdir;
11920
12063
  const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
11921
- const file = (0, import_node_path14.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
11922
- await ensureDir((0, import_node_path14.dirname)(file), { recursive: true }).catch(() => {
12064
+ const file = (0, import_node_path15.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
12065
+ await ensureDir((0, import_node_path15.dirname)(file), { recursive: true }).catch(() => {
11923
12066
  });
11924
12067
  await write(file, args[i + 1], "utf8");
11925
12068
  return {
@@ -13194,8 +13337,8 @@ function parseVerifyBroker(stdout) {
13194
13337
  }
13195
13338
 
13196
13339
  // src/train-apply.ts
13197
- var import_node_fs17 = require("node:fs");
13198
- var import_node_path15 = require("node:path");
13340
+ var import_node_fs18 = require("node:fs");
13341
+ var import_node_path16 = require("node:path");
13199
13342
  function resolveDeployModel2(meta, repo) {
13200
13343
  const m = meta?.deployModel;
13201
13344
  if (isDeployModel(m)) return m;
@@ -14103,17 +14246,17 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
14103
14246
  return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
14104
14247
  }
14105
14248
  function readLocalGateWorkflows() {
14106
- const dir = (0, import_node_path15.join)(".github", "workflows");
14249
+ const dir = (0, import_node_path16.join)(".github", "workflows");
14107
14250
  let names;
14108
14251
  try {
14109
- names = (0, import_node_fs17.readdirSync)(dir);
14252
+ names = (0, import_node_fs18.readdirSync)(dir);
14110
14253
  } catch {
14111
14254
  return null;
14112
14255
  }
14113
14256
  const files = [];
14114
14257
  for (const name of names.filter(isGateWorkflowPath)) {
14115
14258
  try {
14116
- files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(dir, name), "utf8") });
14259
+ files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs18.readFileSync)((0, import_node_path16.join)(dir, name), "utf8") });
14117
14260
  } catch {
14118
14261
  }
14119
14262
  }
@@ -16009,112 +16152,6 @@ function renderAccessReport(report) {
16009
16152
  return lines.join("\n");
16010
16153
  }
16011
16154
 
16012
- // src/docs-index-command.ts
16013
- var import_node_fs18 = require("node:fs");
16014
- var import_node_path16 = require("node:path");
16015
- var DOCS_INDEX_PATH = "docs/index.md";
16016
- function isRoutableDocsPath(relPath) {
16017
- const normalized = relPath.replace(/\\/g, "/");
16018
- return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
16019
- }
16020
- var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
16021
- function plainInline(text) {
16022
- return text.replace(/\r?\n/g, " ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`/g, "").replace(/\*+/g, "").replace(/~~/g, "").replace(/__([^_]+)__/g, "$1").replace(/(^|[\s(])_([^_]+)_($|[\s).,!?:;])/g, "$1$2$3").replace(/^>\s*/, "").replace(/\\([\\`*_\[\]{}()#+.!|>-])/g, "$1").replace(/\s+/g, " ").trim();
16023
- }
16024
- function extractDocMeta(relPath, raw) {
16025
- const lines = raw.split(/\r?\n/);
16026
- let i = 0;
16027
- if (lines[i]?.trim() === "---") {
16028
- i++;
16029
- while (i < lines.length && lines[i].trim() !== "---") i++;
16030
- if (i < lines.length) i++;
16031
- }
16032
- let title = "";
16033
- let scope = "";
16034
- for (; i < lines.length; i++) {
16035
- const line = lines[i];
16036
- const trimmed = line.trim();
16037
- if (trimmed === "") continue;
16038
- if (trimmed.startsWith("<!--")) {
16039
- while (i < lines.length && !lines[i].includes("-->")) i++;
16040
- continue;
16041
- }
16042
- if (!title && trimmed.startsWith("# ")) {
16043
- title = trimmed.slice(2).trim();
16044
- continue;
16045
- }
16046
- if (!title) {
16047
- break;
16048
- }
16049
- if (trimmed.startsWith("#")) continue;
16050
- scope = plainInline(trimmed);
16051
- break;
16052
- }
16053
- if (!title) {
16054
- const base = relPath.split("/").pop() ?? relPath;
16055
- title = base.replace(/\.md$/, "");
16056
- }
16057
- return { path: relPath, title, scope };
16058
- }
16059
- function renderDocsIndex(entries) {
16060
- const lines = [
16061
- GENERATED_HEADER,
16062
- "",
16063
- "# Documentation index",
16064
- "",
16065
- "Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
16066
- "can never diverge from the records it lists.",
16067
- ""
16068
- ];
16069
- for (const entry of entries) {
16070
- const link = `[${plainInline(entry.title)}](${entry.path})`;
16071
- lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
16072
- }
16073
- return lines.join("\n") + "\n";
16074
- }
16075
- function sameContent(a, b) {
16076
- const normalize = (text) => text.replace(/\r\n/g, "\n");
16077
- return normalize(a) === normalize(b);
16078
- }
16079
- function docsIndex(deps, opts = {}) {
16080
- const entries = deps.listDocs().slice().sort().map((relPath) => extractDocMeta(relPath, deps.readDoc(relPath)));
16081
- const content = renderDocsIndex(entries);
16082
- const current = deps.readIndex();
16083
- const drift = current === null || !sameContent(current, content);
16084
- let wrote = false;
16085
- if (!opts.check && drift) {
16086
- deps.writeIndex(content);
16087
- wrote = true;
16088
- }
16089
- return { content, drift, wrote };
16090
- }
16091
- function walkMarkdown(dir) {
16092
- const out = [];
16093
- const stack = [dir];
16094
- while (stack.length) {
16095
- const current = stack.pop();
16096
- for (const entry of (0, import_node_fs18.readdirSync)(current, { withFileTypes: true })) {
16097
- const full = (0, import_node_path16.join)(current, entry.name);
16098
- if (entry.isDirectory()) {
16099
- stack.push(full);
16100
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
16101
- out.push((0, import_node_path16.relative)(dir, full).split(import_node_path16.sep).join("/"));
16102
- }
16103
- }
16104
- }
16105
- return out;
16106
- }
16107
- function createDocsIndexDeps(repoRoot2) {
16108
- const docsDir = (0, import_node_path16.join)(repoRoot2, "docs");
16109
- const indexPath = (0, import_node_path16.join)(repoRoot2, DOCS_INDEX_PATH);
16110
- return {
16111
- listDocs: () => (0, import_node_fs18.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
16112
- readDoc: (relPath) => (0, import_node_fs18.readFileSync)((0, import_node_path16.join)(docsDir, relPath), "utf8"),
16113
- readIndex: () => (0, import_node_fs18.existsSync)(indexPath) ? (0, import_node_fs18.readFileSync)(indexPath, "utf8") : null,
16114
- writeIndex: (content) => (0, import_node_fs18.writeFileSync)(indexPath, content, "utf8")
16115
- };
16116
- }
16117
-
16118
16155
  // src/doc-refs-core.ts
16119
16156
  var import_node_child_process9 = require("node:child_process");
16120
16157
  var import_node_fs19 = require("node:fs");
@@ -19345,7 +19382,7 @@ var import_node_fs22 = require("node:fs");
19345
19382
 
19346
19383
  // src/bootstrap-verify.ts
19347
19384
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
19348
- var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md"];
19385
+ var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
19349
19386
  var requiredIssueTemplates = [
19350
19387
  ".github/ISSUE_TEMPLATE/bug.yml",
19351
19388
  ".github/ISSUE_TEMPLATE/feature.yml",
@@ -19502,6 +19539,11 @@ function unfilledDocPlaceholders(text) {
19502
19539
  for (const prompt of DOC_PLACEHOLDER_PROMPTS) if (text.includes(prompt)) found.add(prompt);
19503
19540
  return [...found];
19504
19541
  }
19542
+ function filledDocCheck(label, text, path2) {
19543
+ if (text === null) return { ok: false, label, detail: `${path2} not readable via API` };
19544
+ const unfilled = unfilledDocPlaceholders(text);
19545
+ return { ok: unfilled.length === 0, label, detail: unfilled.length ? `unfilled: ${unfilled.join(", ")}` : void 0 };
19546
+ }
19505
19547
  async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
19506
19548
  const branchesWanted = expectedBranches(repoClass, releaseTrack);
19507
19549
  const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
@@ -19575,19 +19617,9 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
19575
19617
  label: "README has Agent context section",
19576
19618
  detail: readme === null ? "README.md not readable via API" : void 0
19577
19619
  });
19578
- const readmePlaceholders = unfilledDocPlaceholders(readme);
19579
- checks.push({
19580
- ok: readmePlaceholders.length === 0,
19581
- label: "README Agent context is filled (no template placeholders)",
19582
- detail: readmePlaceholders.length ? `unfilled: ${readmePlaceholders.join(", ")}` : void 0
19583
- });
19620
+ checks.push(filledDocCheck("README Agent context is filled (no template placeholders)", readme, "README.md"));
19584
19621
  const architecture = await contentText(deps, repo, baseBranch, "architecture.md");
19585
- const archPlaceholders = unfilledDocPlaceholders(architecture);
19586
- checks.push({
19587
- ok: archPlaceholders.length === 0,
19588
- label: "architecture.md is filled (no template placeholders)",
19589
- detail: archPlaceholders.length ? `unfilled: ${archPlaceholders.join(", ")}` : void 0
19590
- });
19622
+ checks.push(filledDocCheck("architecture.md is filled (no template placeholders)", architecture, "architecture.md"));
19591
19623
  const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
19592
19624
  const labelNames = new Set(labels.map((l) => l.name));
19593
19625
  for (const label of requiredLabels) {
@@ -19910,7 +19942,11 @@ function registerBootstrapCommands(program3) {
19910
19942
  return null;
19911
19943
  }
19912
19944
  }
19913
- }, resolveReleaseTrack(meta));
19945
+ // #3537: verify runs BEFORE registration by construction (skill Step 0a), so `meta` is null on every
19946
+ // fresh repo and `resolveReleaseTrack` falls through to `full` — which then outranks the operator's
19947
+ // own `--class content` inside expectedBranches, and the report demands a development and an rc the
19948
+ // track forbids. The declared class fills that gap only when the registry states nothing itself.
19949
+ }, resolveReleaseTrack(metaWithDeclaredClass(meta, o.class), void 0, repo));
19914
19950
  console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
19915
19951
  if (!report.ok) process.exitCode = 1;
19916
19952
  });
@@ -19958,6 +19994,17 @@ function registerBootstrapCommands(program3) {
19958
19994
  } catch {
19959
19995
  }
19960
19996
  const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class, bootstrapReleaseTrack);
19997
+ if (!vars.PROJECT_ID) {
19998
+ try {
19999
+ const r = await gh(linkedProjectsQueryArgs(parsedRepo.owner, parsedRepo.name));
20000
+ const linked = pickLinkedProject(JSON.parse(r.stdout));
20001
+ if (linked) {
20002
+ vars.PROJECT_ID = linked.id;
20003
+ if (linked.number != null && vars.PROJECT_NUMBER == null) vars.PROJECT_NUMBER = String(linked.number);
20004
+ }
20005
+ } catch {
20006
+ }
20007
+ }
19961
20008
  if (vars.PROJECT_ID) {
19962
20009
  try {
19963
20010
  const r = await gh(boardFieldsQueryArgs(vars.PROJECT_ID));
@@ -19999,6 +20046,7 @@ function registerBootstrapCommands(program3) {
19999
20046
  }
20000
20047
  }
20001
20048
  }
20049
+ const docsForIndex = [];
20002
20050
  for (const seed of manifest.seeds) {
20003
20051
  if (!seed.classes.includes(o.class)) continue;
20004
20052
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
@@ -20026,12 +20074,38 @@ function registerBootstrapCommands(program3) {
20026
20074
  const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile7) : null;
20027
20075
  const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
20028
20076
  actions.push(action);
20077
+ const docBody = content ?? remoteContent;
20078
+ if (resolved.target.startsWith("docs/") && resolved.target.endsWith(".md") && docBody !== null) {
20079
+ docsForIndex.push({ path: resolved.target, content: docBody });
20080
+ }
20029
20081
  if (o.execute && (action.action === "create" || action.action === "update")) {
20030
20082
  await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
20031
20083
  applied.push(`${action.action} ${resolved.target}`);
20032
20084
  if (seedPlan.mode === "pr") seededToBranch++;
20033
20085
  }
20034
20086
  }
20087
+ const indexContent = seededDocsIndex(docsForIndex);
20088
+ if (indexContent) {
20089
+ let indexCurrent = null;
20090
+ try {
20091
+ const r = await gh(["api", `repos/${repo}/contents/${enc(DOCS_INDEX_PATH)}?ref=${seedPlan.ref}`]);
20092
+ const parsed = JSON.parse(r.stdout);
20093
+ indexCurrent = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : "";
20094
+ } catch {
20095
+ }
20096
+ const indexAction = indexCurrent === null ? "create" : "skip";
20097
+ actions.push({
20098
+ target: DOCS_INDEX_PATH,
20099
+ action: indexAction,
20100
+ ownership: "org",
20101
+ reason: indexCurrent === null ? "generated routing index (#3545)" : "routing index present \u2014 regenerate with `mmi-cli docs index --write`, which sees the whole tree"
20102
+ });
20103
+ if (o.execute && indexAction === "create") {
20104
+ await gh(contentPutArgs(repo, DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0));
20105
+ applied.push(`${indexAction} ${DOCS_INDEX_PATH}`);
20106
+ if (seedPlan.mode === "pr") seededToBranch++;
20107
+ }
20108
+ }
20035
20109
  let seedPrUrl;
20036
20110
  if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
20037
20111
  await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
@@ -26323,11 +26397,18 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
26323
26397
  const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
26324
26398
  const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim();
26325
26399
  const [repoFromGh, base] = viewed.split(/\s+/);
26326
- if (base && base !== "development") {
26327
- throw new Error(`pr land: base branch must be development (got ${base}) \u2014 promotion merges stay human-only`);
26328
- }
26329
26400
  const repo = repoOpt ?? repoFromGh;
26330
26401
  if (!repo) throw new Error("pr land: could not resolve PR repo");
26402
+ let track;
26403
+ try {
26404
+ const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(await loadConfig()));
26405
+ if (meta) track = resolveReleaseTrack(meta, void 0, repo);
26406
+ } catch {
26407
+ }
26408
+ if (isPromotionBase(base, track)) {
26409
+ const shape = track ? `${track} track` : "unresolved track \u2014 strict reading";
26410
+ throw new Error(`pr land: base branch ${base} is a promotion target (${shape}) \u2014 promotion merges stay human-only`);
26411
+ }
26331
26412
  return repo;
26332
26413
  },
26333
26414
  fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.52.0",
3
+ "version": "3.53.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",