@mutmutco/cli 3.52.0 → 3.54.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 +310 -157
  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;
@@ -9092,6 +9108,7 @@ function cachedReadNote(cachedAt, now = Date.now()) {
9092
9108
  // src/marketplace-autoupdate.ts
9093
9109
  var KNOWN_MARKETPLACES_RELATIVE = [".claude", "plugins", "known_marketplaces.json"];
9094
9110
  var MMI_MARKETPLACE_NAME = "mutmutco";
9111
+ var JERV_MARKETPLACE_NAME = "jervaise";
9095
9112
  function readFileSyncSafe(path2, read) {
9096
9113
  try {
9097
9114
  return read(path2, "utf8");
@@ -9128,6 +9145,49 @@ function resolveAutoUpdate(probe) {
9128
9145
  reason: "undeclared \u2014 third-party marketplaces are OFF by default, so nothing auto-updates and no update notice ever prints"
9129
9146
  };
9130
9147
  }
9148
+ function captureMarketplacePins(raw, names) {
9149
+ const out = /* @__PURE__ */ new Map();
9150
+ for (const name of names) {
9151
+ const { registered, declared, ref } = readKnownMarketplace(raw, name);
9152
+ if (!registered) continue;
9153
+ const pins = {
9154
+ ...typeof declared === "boolean" ? { autoUpdate: declared } : {},
9155
+ ...ref ? { ref } : {}
9156
+ };
9157
+ if (Object.keys(pins).length) out.set(name, pins);
9158
+ }
9159
+ return out;
9160
+ }
9161
+ function restoreMarketplacePins(raw, pins) {
9162
+ if (!raw || pins.size === 0) return null;
9163
+ let parsed;
9164
+ try {
9165
+ parsed = JSON.parse(raw);
9166
+ } catch {
9167
+ return null;
9168
+ }
9169
+ if (!parsed || typeof parsed !== "object") return null;
9170
+ const root = parsed;
9171
+ let changed = false;
9172
+ for (const [name, want] of pins) {
9173
+ const entry = root[name];
9174
+ if (!entry || typeof entry !== "object") continue;
9175
+ const e = entry;
9176
+ if (typeof want.autoUpdate === "boolean" && e.autoUpdate !== want.autoUpdate) {
9177
+ e.autoUpdate = want.autoUpdate;
9178
+ changed = true;
9179
+ }
9180
+ if (want.ref && e.source && typeof e.source === "object") {
9181
+ const source = e.source;
9182
+ if (source.ref !== want.ref) {
9183
+ source.ref = want.ref;
9184
+ changed = true;
9185
+ }
9186
+ }
9187
+ }
9188
+ return changed ? `${JSON.stringify(root, null, 2)}
9189
+ ` : null;
9190
+ }
9131
9191
  function readKnownMarketplace(raw, name) {
9132
9192
  if (!raw) return { registered: false };
9133
9193
  let parsed;
@@ -10018,6 +10078,112 @@ function planManagedGitignore(current) {
10018
10078
  return { changed, content, added, removed };
10019
10079
  }
10020
10080
 
10081
+ // src/docs-index-command.ts
10082
+ var import_node_fs15 = require("node:fs");
10083
+ var import_node_path13 = require("node:path");
10084
+ var DOCS_INDEX_PATH = "docs/index.md";
10085
+ function isRoutableDocsPath(relPath) {
10086
+ const normalized = relPath.replace(/\\/g, "/");
10087
+ return normalized !== "index.md" && normalized.split("/")[0]?.toLowerCase() !== "archive";
10088
+ }
10089
+ var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
10090
+ function plainInline(text) {
10091
+ 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();
10092
+ }
10093
+ function extractDocMeta(relPath, raw) {
10094
+ const lines = raw.split(/\r?\n/);
10095
+ let i = 0;
10096
+ if (lines[i]?.trim() === "---") {
10097
+ i++;
10098
+ while (i < lines.length && lines[i].trim() !== "---") i++;
10099
+ if (i < lines.length) i++;
10100
+ }
10101
+ let title = "";
10102
+ let scope = "";
10103
+ for (; i < lines.length; i++) {
10104
+ const line = lines[i];
10105
+ const trimmed = line.trim();
10106
+ if (trimmed === "") continue;
10107
+ if (trimmed.startsWith("<!--")) {
10108
+ while (i < lines.length && !lines[i].includes("-->")) i++;
10109
+ continue;
10110
+ }
10111
+ if (!title && trimmed.startsWith("# ")) {
10112
+ title = trimmed.slice(2).trim();
10113
+ continue;
10114
+ }
10115
+ if (!title) {
10116
+ break;
10117
+ }
10118
+ if (trimmed.startsWith("#")) continue;
10119
+ scope = plainInline(trimmed);
10120
+ break;
10121
+ }
10122
+ if (!title) {
10123
+ const base = relPath.split("/").pop() ?? relPath;
10124
+ title = base.replace(/\.md$/, "");
10125
+ }
10126
+ return { path: relPath, title, scope };
10127
+ }
10128
+ function renderDocsIndex(entries) {
10129
+ const lines = [
10130
+ GENERATED_HEADER,
10131
+ "",
10132
+ "# Documentation index",
10133
+ "",
10134
+ "Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
10135
+ "can never diverge from the records it lists.",
10136
+ ""
10137
+ ];
10138
+ for (const entry of entries) {
10139
+ const link = `[${plainInline(entry.title)}](${entry.path})`;
10140
+ lines.push(entry.scope ? `- ${link} \u2014 ${plainInline(entry.scope)}` : `- ${link}`);
10141
+ }
10142
+ return lines.join("\n") + "\n";
10143
+ }
10144
+ function sameContent(a, b) {
10145
+ const normalize = (text) => text.replace(/\r\n/g, "\n");
10146
+ return normalize(a) === normalize(b);
10147
+ }
10148
+ function docsIndex(deps, opts = {}) {
10149
+ const entries = deps.listDocs().slice().sort().map((relPath) => extractDocMeta(relPath, deps.readDoc(relPath)));
10150
+ const content = renderDocsIndex(entries);
10151
+ const current = deps.readIndex();
10152
+ const drift = current === null || !sameContent(current, content);
10153
+ let wrote = false;
10154
+ if (!opts.check && drift) {
10155
+ deps.writeIndex(content);
10156
+ wrote = true;
10157
+ }
10158
+ return { content, drift, wrote };
10159
+ }
10160
+ function walkMarkdown(dir) {
10161
+ const out = [];
10162
+ const stack = [dir];
10163
+ while (stack.length) {
10164
+ const current = stack.pop();
10165
+ for (const entry of (0, import_node_fs15.readdirSync)(current, { withFileTypes: true })) {
10166
+ const full = (0, import_node_path13.join)(current, entry.name);
10167
+ if (entry.isDirectory()) {
10168
+ stack.push(full);
10169
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
10170
+ out.push((0, import_node_path13.relative)(dir, full).split(import_node_path13.sep).join("/"));
10171
+ }
10172
+ }
10173
+ }
10174
+ return out;
10175
+ }
10176
+ function createDocsIndexDeps(repoRoot2) {
10177
+ const docsDir = (0, import_node_path13.join)(repoRoot2, "docs");
10178
+ const indexPath = (0, import_node_path13.join)(repoRoot2, DOCS_INDEX_PATH);
10179
+ return {
10180
+ listDocs: () => (0, import_node_fs15.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
10181
+ readDoc: (relPath) => (0, import_node_fs15.readFileSync)((0, import_node_path13.join)(docsDir, relPath), "utf8"),
10182
+ readIndex: () => (0, import_node_fs15.existsSync)(indexPath) ? (0, import_node_fs15.readFileSync)(indexPath, "utf8") : null,
10183
+ writeIndex: (content) => (0, import_node_fs15.writeFileSync)(indexPath, content, "utf8")
10184
+ };
10185
+ }
10186
+
10021
10187
  // src/gate-budget.ts
10022
10188
  var BLESSED_RUN_WITH_BUDGET_SHA = "ea2cf8710949dec53566c29836e29cd7515a4e23";
10023
10189
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
@@ -10264,6 +10430,7 @@ function withDerivedRepoVars(vars, parsed, cls, releaseTrack) {
10264
10430
  out.REPO_NAME ??= parsed.name;
10265
10431
  out.REPO_SLUG ??= parsed.slug;
10266
10432
  out.CLASS ??= cls;
10433
+ out.PROJECT_OWNER ??= parsed.owner;
10267
10434
  const track = releaseTrack ?? resolveBootstrapReleaseTrack(cls);
10268
10435
  const runtime = out.GATE_RUNTIME === "python" ? "python" : "node";
10269
10436
  for (const [key, value] of Object.entries(gateSeedVars(cls, track, runtime))) {
@@ -10464,6 +10631,26 @@ function boardFieldsQueryArgs(projectId) {
10464
10631
  const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
10465
10632
  return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
10466
10633
  }
10634
+ function seededDocsIndex(seeds) {
10635
+ 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));
10636
+ if (!docs2.length) return null;
10637
+ 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));
10638
+ return renderDocsIndex(entries);
10639
+ }
10640
+ function linkedProjectsQueryArgs(owner, name) {
10641
+ const query = "query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { projectsV2(first: 10) { nodes { id number title } } } }";
10642
+ return ["api", "graphql", "-f", `query=${query}`, "-f", `owner=${owner}`, "-f", `name=${name}`];
10643
+ }
10644
+ function pickLinkedProject(json) {
10645
+ const nodes = json?.data?.repository?.projectsV2?.nodes;
10646
+ if (!Array.isArray(nodes)) return void 0;
10647
+ const usable = nodes.filter(
10648
+ (n) => typeof n?.id === "string" && Boolean(n.id)
10649
+ );
10650
+ if (usable.length !== 1) return void 0;
10651
+ const only = usable[0];
10652
+ return typeof only.number === "number" && Number.isFinite(only.number) ? { id: only.id, number: only.number } : { id: only.id };
10653
+ }
10467
10654
  function decideSeedPrAction(openPrs) {
10468
10655
  const list = Array.isArray(openPrs) ? openPrs : [];
10469
10656
  for (const pr2 of list) {
@@ -11112,12 +11299,12 @@ async function runPrLand(prNumber, options, deps) {
11112
11299
  }
11113
11300
 
11114
11301
  // src/wave-status.ts
11115
- var import_node_fs16 = require("node:fs");
11302
+ var import_node_fs17 = require("node:fs");
11116
11303
 
11117
11304
  // src/stage-runner.ts
11118
11305
  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");
11306
+ var import_node_fs16 = require("node:fs");
11307
+ var import_node_path14 = require("node:path");
11121
11308
  var import_node_net = require("node:net");
11122
11309
  var import_node_util6 = require("node:util");
11123
11310
 
@@ -11256,11 +11443,11 @@ function appendForceRecreate(up) {
11256
11443
  return `${up.trimEnd()} --force-recreate`;
11257
11444
  }
11258
11445
  function stageStatePath(cwd = process.cwd()) {
11259
- return (0, import_node_path13.join)(cwd, "tmp", "stage", "state.json");
11446
+ return (0, import_node_path14.join)(cwd, "tmp", "stage", "state.json");
11260
11447
  }
11261
11448
  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");
11449
+ const dir = (0, import_node_path14.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path14.resolve)(cwd, gitCommonDir);
11450
+ return (0, import_node_path14.join)(dir, "mmi", "stage", "state.json");
11264
11451
  }
11265
11452
  function normPath2(path2) {
11266
11453
  return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -11484,14 +11671,14 @@ function stageProcessEnv(stagePort, extraEnv) {
11484
11671
  }
11485
11672
  async function ensureStageRuntimeEnv(config, opts, cwd) {
11486
11673
  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
11674
+ const target = (0, import_node_path14.join)(cwd, config.ensureEnv.target);
11675
+ const example = (0, import_node_path14.join)(cwd, config.ensureEnv.example);
11676
+ if (!(0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
11677
+ (0, import_node_fs16.copyFileSync)(example, target);
11678
+ } else if ((0, import_node_fs16.existsSync)(target) && (0, import_node_fs16.existsSync)(example)) {
11679
+ const stale = detectStaleEnvFile((0, import_node_fs16.readFileSync)(example, "utf8"), (0, import_node_fs16.readFileSync)(target, "utf8"), {
11680
+ exampleMtimeMs: (0, import_node_fs16.statSync)(example).mtimeMs,
11681
+ targetMtimeMs: (0, import_node_fs16.statSync)(target).mtimeMs
11495
11682
  });
11496
11683
  if (stale) {
11497
11684
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -11499,8 +11686,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
11499
11686
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
11500
11687
  }
11501
11688
  }
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");
11689
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs16.existsSync)(target)) {
11690
+ (0, import_node_fs16.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs16.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
11504
11691
  }
11505
11692
  }
11506
11693
  async function gitText(cwd, args) {
@@ -11530,20 +11717,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
11530
11717
  return void 0;
11531
11718
  }
11532
11719
  function readState(path2) {
11533
- if (!(0, import_node_fs15.existsSync)(path2)) return null;
11720
+ if (!(0, import_node_fs16.existsSync)(path2)) return null;
11534
11721
  try {
11535
- return JSON.parse((0, import_node_fs15.readFileSync)(path2, "utf8"));
11722
+ return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf8"));
11536
11723
  } catch {
11537
11724
  return null;
11538
11725
  }
11539
11726
  }
11540
11727
  function mkdirFor(path2) {
11541
11728
  const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
11542
- (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
11729
+ (0, import_node_fs16.mkdirSync)(dir, { recursive: true });
11543
11730
  }
11544
11731
  function writeState(path2, state) {
11545
11732
  mkdirFor(path2);
11546
- (0, import_node_fs15.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
11733
+ (0, import_node_fs16.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
11547
11734
  }
11548
11735
  function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
11549
11736
  const reservation = {
@@ -11563,7 +11750,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
11563
11750
  await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
11564
11751
  }
11565
11752
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
11566
- (0, import_node_fs15.rmSync)(path2, { force: true });
11753
+ (0, import_node_fs16.rmSync)(path2, { force: true });
11567
11754
  }
11568
11755
  }
11569
11756
  async function killTree(pid) {
@@ -11721,8 +11908,8 @@ async function runStage(config = {}, opts = {}) {
11721
11908
  await ensureStageRuntimeEnv(config, opts, cwd);
11722
11909
  if (build) await shell(sub(build), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
11723
11910
  } catch (e) {
11724
- (0, import_node_fs15.rmSync)(statePath, { force: true });
11725
- if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs15.rmSync)(globalStatePath, { force: true });
11911
+ (0, import_node_fs16.rmSync)(statePath, { force: true });
11912
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs16.rmSync)(globalStatePath, { force: true });
11726
11913
  throw e;
11727
11914
  }
11728
11915
  const started = await startStage(config, {
@@ -11743,9 +11930,9 @@ function parseNextFromHead(headText) {
11743
11930
  }
11744
11931
  function readStageSummary(worktreePath) {
11745
11932
  const statePath = stageStatePath(worktreePath);
11746
- if (!(0, import_node_fs16.existsSync)(statePath)) return void 0;
11933
+ if (!(0, import_node_fs17.existsSync)(statePath)) return void 0;
11747
11934
  try {
11748
- const state = JSON.parse((0, import_node_fs16.readFileSync)(statePath, "utf8"));
11935
+ const state = JSON.parse((0, import_node_fs17.readFileSync)(statePath, "utf8"));
11749
11936
  const port = typeof state.port === "number" ? state.port : void 0;
11750
11937
  if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
11751
11938
  return { port, url: typeof state.url === "string" ? state.url : void 0 };
@@ -11864,7 +12051,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
11864
12051
  // src/gh-create.ts
11865
12052
  var import_promises = require("node:fs/promises");
11866
12053
  var import_node_os3 = require("node:os");
11867
- var import_node_path14 = require("node:path");
12054
+ var import_node_path15 = require("node:path");
11868
12055
  var import_node_crypto3 = require("node:crypto");
11869
12056
  var ISSUE_TYPES = ["bug", "feature", "task"];
11870
12057
  var GH_MUTATION_TIMEOUT_MS = 12e4;
@@ -11918,8 +12105,8 @@ async function bodyArgsViaFile(args, deps = {}) {
11918
12105
  const remove2 = deps.remove ?? import_promises.unlink;
11919
12106
  const ensureDir = deps.ensureDir ?? import_promises.mkdir;
11920
12107
  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(() => {
12108
+ const file = (0, import_node_path15.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
12109
+ await ensureDir((0, import_node_path15.dirname)(file), { recursive: true }).catch(() => {
11923
12110
  });
11924
12111
  await write(file, args[i + 1], "utf8");
11925
12112
  return {
@@ -13194,8 +13381,8 @@ function parseVerifyBroker(stdout) {
13194
13381
  }
13195
13382
 
13196
13383
  // src/train-apply.ts
13197
- var import_node_fs17 = require("node:fs");
13198
- var import_node_path15 = require("node:path");
13384
+ var import_node_fs18 = require("node:fs");
13385
+ var import_node_path16 = require("node:path");
13199
13386
  function resolveDeployModel2(meta, repo) {
13200
13387
  const m = meta?.deployModel;
13201
13388
  if (isDeployModel(m)) return m;
@@ -14103,17 +14290,17 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
14103
14290
  return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
14104
14291
  }
14105
14292
  function readLocalGateWorkflows() {
14106
- const dir = (0, import_node_path15.join)(".github", "workflows");
14293
+ const dir = (0, import_node_path16.join)(".github", "workflows");
14107
14294
  let names;
14108
14295
  try {
14109
- names = (0, import_node_fs17.readdirSync)(dir);
14296
+ names = (0, import_node_fs18.readdirSync)(dir);
14110
14297
  } catch {
14111
14298
  return null;
14112
14299
  }
14113
14300
  const files = [];
14114
14301
  for (const name of names.filter(isGateWorkflowPath)) {
14115
14302
  try {
14116
- files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(dir, name), "utf8") });
14303
+ files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs18.readFileSync)((0, import_node_path16.join)(dir, name), "utf8") });
14117
14304
  } catch {
14118
14305
  }
14119
14306
  }
@@ -16009,112 +16196,6 @@ function renderAccessReport(report) {
16009
16196
  return lines.join("\n");
16010
16197
  }
16011
16198
 
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
16199
  // src/doc-refs-core.ts
16119
16200
  var import_node_child_process9 = require("node:child_process");
16120
16201
  var import_node_fs19 = require("node:fs");
@@ -19345,7 +19426,7 @@ var import_node_fs22 = require("node:fs");
19345
19426
 
19346
19427
  // src/bootstrap-verify.ts
19347
19428
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
19348
- var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md"];
19429
+ var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
19349
19430
  var requiredIssueTemplates = [
19350
19431
  ".github/ISSUE_TEMPLATE/bug.yml",
19351
19432
  ".github/ISSUE_TEMPLATE/feature.yml",
@@ -19502,6 +19583,11 @@ function unfilledDocPlaceholders(text) {
19502
19583
  for (const prompt of DOC_PLACEHOLDER_PROMPTS) if (text.includes(prompt)) found.add(prompt);
19503
19584
  return [...found];
19504
19585
  }
19586
+ function filledDocCheck(label, text, path2) {
19587
+ if (text === null) return { ok: false, label, detail: `${path2} not readable via API` };
19588
+ const unfilled = unfilledDocPlaceholders(text);
19589
+ return { ok: unfilled.length === 0, label, detail: unfilled.length ? `unfilled: ${unfilled.join(", ")}` : void 0 };
19590
+ }
19505
19591
  async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
19506
19592
  const branchesWanted = expectedBranches(repoClass, releaseTrack);
19507
19593
  const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
@@ -19575,19 +19661,9 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
19575
19661
  label: "README has Agent context section",
19576
19662
  detail: readme === null ? "README.md not readable via API" : void 0
19577
19663
  });
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
- });
19664
+ checks.push(filledDocCheck("README Agent context is filled (no template placeholders)", readme, "README.md"));
19584
19665
  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
- });
19666
+ checks.push(filledDocCheck("architecture.md is filled (no template placeholders)", architecture, "architecture.md"));
19591
19667
  const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
19592
19668
  const labelNames = new Set(labels.map((l) => l.name));
19593
19669
  for (const label of requiredLabels) {
@@ -19910,7 +19986,11 @@ function registerBootstrapCommands(program3) {
19910
19986
  return null;
19911
19987
  }
19912
19988
  }
19913
- }, resolveReleaseTrack(meta));
19989
+ // #3537: verify runs BEFORE registration by construction (skill Step 0a), so `meta` is null on every
19990
+ // fresh repo and `resolveReleaseTrack` falls through to `full` — which then outranks the operator's
19991
+ // own `--class content` inside expectedBranches, and the report demands a development and an rc the
19992
+ // track forbids. The declared class fills that gap only when the registry states nothing itself.
19993
+ }, resolveReleaseTrack(metaWithDeclaredClass(meta, o.class), void 0, repo));
19914
19994
  console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
19915
19995
  if (!report.ok) process.exitCode = 1;
19916
19996
  });
@@ -19958,6 +20038,17 @@ function registerBootstrapCommands(program3) {
19958
20038
  } catch {
19959
20039
  }
19960
20040
  const vars = withDerivedRepoVars(rawVars, parsedRepo, o.class, bootstrapReleaseTrack);
20041
+ if (!vars.PROJECT_ID) {
20042
+ try {
20043
+ const r = await gh(linkedProjectsQueryArgs(parsedRepo.owner, parsedRepo.name));
20044
+ const linked = pickLinkedProject(JSON.parse(r.stdout));
20045
+ if (linked) {
20046
+ vars.PROJECT_ID = linked.id;
20047
+ if (linked.number != null && vars.PROJECT_NUMBER == null) vars.PROJECT_NUMBER = String(linked.number);
20048
+ }
20049
+ } catch {
20050
+ }
20051
+ }
19961
20052
  if (vars.PROJECT_ID) {
19962
20053
  try {
19963
20054
  const r = await gh(boardFieldsQueryArgs(vars.PROJECT_ID));
@@ -19999,6 +20090,7 @@ function registerBootstrapCommands(program3) {
19999
20090
  }
20000
20091
  }
20001
20092
  }
20093
+ const docsForIndex = [];
20002
20094
  for (const seed of manifest.seeds) {
20003
20095
  if (!seed.classes.includes(o.class)) continue;
20004
20096
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
@@ -20026,12 +20118,38 @@ function registerBootstrapCommands(program3) {
20026
20118
  const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile7) : null;
20027
20119
  const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
20028
20120
  actions.push(action);
20121
+ const docBody = content ?? remoteContent;
20122
+ if (resolved.target.startsWith("docs/") && resolved.target.endsWith(".md") && docBody !== null) {
20123
+ docsForIndex.push({ path: resolved.target, content: docBody });
20124
+ }
20029
20125
  if (o.execute && (action.action === "create" || action.action === "update")) {
20030
20126
  await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
20031
20127
  applied.push(`${action.action} ${resolved.target}`);
20032
20128
  if (seedPlan.mode === "pr") seededToBranch++;
20033
20129
  }
20034
20130
  }
20131
+ const indexContent = seededDocsIndex(docsForIndex);
20132
+ if (indexContent) {
20133
+ let indexCurrent = null;
20134
+ try {
20135
+ const r = await gh(["api", `repos/${repo}/contents/${enc(DOCS_INDEX_PATH)}?ref=${seedPlan.ref}`]);
20136
+ const parsed = JSON.parse(r.stdout);
20137
+ indexCurrent = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : "";
20138
+ } catch {
20139
+ }
20140
+ const indexAction = indexCurrent === null ? "create" : "skip";
20141
+ actions.push({
20142
+ target: DOCS_INDEX_PATH,
20143
+ action: indexAction,
20144
+ ownership: "org",
20145
+ reason: indexCurrent === null ? "generated routing index (#3545)" : "routing index present \u2014 regenerate with `mmi-cli docs index --write`, which sees the whole tree"
20146
+ });
20147
+ if (o.execute && indexAction === "create") {
20148
+ await gh(contentPutArgs(repo, DOCS_INDEX_PATH, indexContent, seedPlan.ref, void 0));
20149
+ applied.push(`${indexAction} ${DOCS_INDEX_PATH}`);
20150
+ if (seedPlan.mode === "pr") seededToBranch++;
20151
+ }
20152
+ }
20035
20153
  let seedPrUrl;
20036
20154
  if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
20037
20155
  await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
@@ -22668,12 +22786,40 @@ async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
22668
22786
  return { ok: false, detail: `not a Claude surface (${surface}) \u2014 no Hub-shipped plugin to reinstall` };
22669
22787
  }
22670
22788
  const steps = [];
22789
+ const pinsPath = (0, import_node_path26.join)((0, import_node_os6.homedir)(), ...KNOWN_MARKETPLACES_RELATIVE);
22790
+ const before = readKnownMarketplacesFile(pinsPath);
22791
+ const pins = captureMarketplacePins(before, [MMI_MARKETPLACE_NAME, JERV_MARKETPLACE_NAME]);
22671
22792
  const ok = await applyPluginHeal(surface, (msg) => steps.push(msg.trim()));
22793
+ const restored = ok ? restoreMarketplacePinsOnDisk(pinsPath, pins) : void 0;
22672
22794
  return {
22673
22795
  ok,
22674
- detail: ok ? "marketplace remove \u2192 add \u2192 install succeeded" : `\`claude plugin\` reinstall failed or was skipped${steps.length ? ` (${steps[steps.length - 1]})` : ""}`
22796
+ detail: ok ? `marketplace remove \u2192 add \u2192 install succeeded${restored ? `; ${restored}` : ""}` : `\`claude plugin\` reinstall failed or was skipped${steps.length ? ` (${steps[steps.length - 1]})` : ""}`
22675
22797
  };
22676
22798
  }
22799
+ function readKnownMarketplacesFile(path2) {
22800
+ try {
22801
+ return (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : void 0;
22802
+ } catch {
22803
+ return void 0;
22804
+ }
22805
+ }
22806
+ function restoreMarketplacePinsOnDisk(path2, pins) {
22807
+ if (pins.size === 0) return void 0;
22808
+ const after = readKnownMarketplacesFile(path2);
22809
+ const next = restoreMarketplacePins(after, pins);
22810
+ if (next === null) return void 0;
22811
+ try {
22812
+ (0, import_node_fs28.writeFileSync)(path2, next, "utf8");
22813
+ } catch {
22814
+ return `could NOT restore ${[...pins.keys()].join(", ")} \u2014 re-pin by hand`;
22815
+ }
22816
+ const verify = readKnownMarketplacesFile(path2);
22817
+ const failed = [...pins].filter(([name, want]) => {
22818
+ const got = readKnownMarketplace(verify, name);
22819
+ return typeof want.autoUpdate === "boolean" && got.declared !== want.autoUpdate || want.ref !== void 0 && got.ref !== want.ref;
22820
+ });
22821
+ return failed.length ? `restore did NOT take for ${failed.map(([n]) => n).join(", ")} \u2014 re-pin by hand` : `re-pinned ${[...pins.keys()].join(", ")} (auto-update + catalog ref survive the reinstall)`;
22822
+ }
22677
22823
  async function runGuard(readOrigin) {
22678
22824
  try {
22679
22825
  const surface = detectSurface(process.env);
@@ -26323,11 +26469,18 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
26323
26469
  const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
26324
26470
  const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim();
26325
26471
  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
26472
  const repo = repoOpt ?? repoFromGh;
26330
26473
  if (!repo) throw new Error("pr land: could not resolve PR repo");
26474
+ let track;
26475
+ try {
26476
+ const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(await loadConfig()));
26477
+ if (meta) track = resolveReleaseTrack(meta, void 0, repo);
26478
+ } catch {
26479
+ }
26480
+ if (isPromotionBase(base, track)) {
26481
+ const shape = track ? `${track} track` : "unresolved track \u2014 strict reading";
26482
+ throw new Error(`pr land: base branch ${base} is a promotion target (${shape}) \u2014 promotion merges stay human-only`);
26483
+ }
26331
26484
  return repo;
26332
26485
  },
26333
26486
  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.54.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",