@mutmutco/cli 3.99.0 → 3.100.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 +197 -2
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -20923,7 +20923,7 @@ function authorizeBodyHasMismatch(body) {
20923
20923
  }
20924
20924
 
20925
20925
  // src/project-set.ts
20926
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
20926
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate", "seedCanary"];
20927
20927
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
20928
20928
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
20929
20929
  var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
@@ -21177,6 +21177,11 @@ function parseRuntimeVaultOnlyVar(raw) {
21177
21177
  if (raw === "false") return false;
21178
21178
  throw new Error("org project set: runtimeVaultOnly must be true or false");
21179
21179
  }
21180
+ function parseSeedCanaryVar(raw) {
21181
+ if (raw === "true") return true;
21182
+ if (raw === "false") return false;
21183
+ throw new Error("org project set: seedCanary must be true or false");
21184
+ }
21180
21185
  function parseConsumesDesignSystemVar(raw) {
21181
21186
  if (raw === "fofu") return raw;
21182
21187
  throw new Error('org project set: consumesDesignSystem must be "fofu"');
@@ -21266,7 +21271,8 @@ var SETTABLE_VAR_KEYS = [
21266
21271
  "ci",
21267
21272
  "requiredChecks",
21268
21273
  "gate",
21269
- "secrets"
21274
+ "secrets",
21275
+ "seedCanary"
21270
21276
  ];
21271
21277
  var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
21272
21278
  var SETTABLE_VAR_HINTS = {
@@ -21277,6 +21283,7 @@ var SETTABLE_VAR_HINTS = {
21277
21283
  fofuEnabled: "true|false",
21278
21284
  consumesDesignSystem: '"fofu"',
21279
21285
  runtimeVaultOnly: "true|false",
21286
+ seedCanary: "true|false",
21280
21287
  repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
21281
21288
  oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
21282
21289
  requiredGcpApis: "comma-string",
@@ -21373,6 +21380,8 @@ function buildProjectSetPatch(input) {
21373
21380
  patch[key] = parseFofuEnabledVar(raw);
21374
21381
  } else if (key === "runtimeVaultOnly") {
21375
21382
  patch[key] = parseRuntimeVaultOnlyVar(raw);
21383
+ } else if (key === "seedCanary") {
21384
+ patch[key] = parseSeedCanaryVar(raw);
21376
21385
  } else if (key === "consumesDesignSystem") {
21377
21386
  patch[key] = parseConsumesDesignSystemVar(raw);
21378
21387
  } else if (key === "publishDir") {
@@ -24017,6 +24026,62 @@ function renderPropagationReport(plan) {
24017
24026
  return lines.join("\n");
24018
24027
  }
24019
24028
 
24029
+ // src/bootstrap-rollback.ts
24030
+ function resolveRollbackRecord(records, repo, target) {
24031
+ const candidates = records.filter((r) => r.repo === repo && r.target === target && r.mergeSha);
24032
+ if (candidates.length === 0) {
24033
+ return { found: false, reason: `no propagation record resolves for ${repo} + ${target} \u2014 refusing to guess a commit to revert` };
24034
+ }
24035
+ const clean4 = candidates.filter((r) => r.files.length === 1 && r.files[0] === target);
24036
+ if (clean4.length === 0) {
24037
+ return {
24038
+ found: false,
24039
+ reason: `${candidates.length} merged seed-propagate PR(s) found for ${repo} + ${target}, but none is a clean single-file propagation of exactly this target \u2014 refusing to guess a commit to revert`
24040
+ };
24041
+ }
24042
+ const latest = clean4.slice().sort((a, b) => a.mergedAt < b.mergedAt ? 1 : a.mergedAt > b.mergedAt ? -1 : 0)[0];
24043
+ return { found: true, record: latest };
24044
+ }
24045
+ function planRollback(repo, target, slug, records) {
24046
+ const resolution = resolveRollbackRecord(records, repo, target);
24047
+ if (!resolution.found) return { repo, target, resolution };
24048
+ const branch = `seed-rollback-${slug}`;
24049
+ const title = `revert: rollback org-owned ${target} in ${repo} (seed PR #${resolution.record.number})`;
24050
+ const body = renderRollbackPrBody(resolution.record);
24051
+ return { repo, target, resolution, branch, title, body };
24052
+ }
24053
+ function renderRollbackPrBody(record) {
24054
+ return [
24055
+ `Per-repo emergency revert of \`${record.target}\`'s seed-propagate merge (\`mmi-cli bootstrap rollback\`, #4240).`,
24056
+ "",
24057
+ `Reverts ${record.url} (merge ${record.mergeSha}) \u2014 restores this repo's copy of \`${record.target}\` to its content immediately before that merge. Nothing else in this repo changes.`,
24058
+ "",
24059
+ "This is the emergency stop, not the fix: MMI-Hub is still the source of truth during a rollback. Closure is the Hub reverting (or fixing forward) the bad seed commit on `development` \u2014 once it does, this repo matches the Hub again and the drift alarm closes itself. A revert with no Hub-side follow-up re-alarms as drift within a week \u2014 deliberately, so an emergency divergence can never silently become permanent.",
24060
+ "",
24061
+ "Never re-run `bootstrap propagate` with the pre-revert bytes before the Hub itself is fixed \u2014 that reopens exactly what this PR closes."
24062
+ ].join("\n");
24063
+ }
24064
+ function renderRollbackReport(plan) {
24065
+ if (!plan.resolution.found) {
24066
+ return `bootstrap rollback \u2014 ${plan.repo} / ${plan.target}: REFUSED \u2014 ${plan.resolution.reason}`;
24067
+ }
24068
+ const r = plan.resolution.record;
24069
+ const opened = plan.prUrl ? ` \u2014 opened ${plan.prUrl}` : "";
24070
+ return `bootstrap rollback \u2014 ${plan.repo} / ${plan.target}: reverting PR#${r.number} (merge ${r.mergeSha}${r.mergedAt ? `, merged ${r.mergedAt}` : ""}) on branch ${plan.branch}${opened}`;
24071
+ }
24072
+ function seedPrRecordFromPropagationRecord(record) {
24073
+ if (!record.mergeSha || record.prNumber == null || !record.prUrl) return null;
24074
+ return {
24075
+ repo: record.repo,
24076
+ target: record.target,
24077
+ number: record.prNumber,
24078
+ url: record.prUrl,
24079
+ mergeSha: record.mergeSha,
24080
+ mergedAt: "",
24081
+ files: [record.target]
24082
+ };
24083
+ }
24084
+
24020
24085
  // src/bootstrap-verify.ts
24021
24086
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
24022
24087
  var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
@@ -25271,6 +25336,136 @@ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --exe
25271
25336
  else console.log(renderPropagationReport(plan));
25272
25337
  if (plan.halted) process.exitCode = 1;
25273
25338
  });
25339
+ bootstrap.command("rollback <repo>").description("#4240: open a per-repo revert PR of the recorded seed-propagate merge \u2014 never a fleet-wide overwrite; dry-run unless --execute").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).option("--target <path>", "the manifest target to roll back (an ownership:org + source:self seed, e.g. .github/workflows/agent-pr.yml)").option("--record <path>", "read propagation candidates from a persisted `bootstrap propagate --json` report instead of the live seed-propagate PR history").option("--execute", "LIVE revert via gh (master-gated) \u2014 opens/reuses the seed-rollback PR; dry-run prints the plan only").option("--json", "machine-readable output").action(async (repo) => {
25340
+ const o = {
25341
+ class: rawValue("--class", "deployable"),
25342
+ target: rawValue("--target", ""),
25343
+ record: rawValue("--record", ""),
25344
+ execute: rawFlag("--execute"),
25345
+ json: rawFlag("--json")
25346
+ };
25347
+ if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap rollback: --class must be deployable or content");
25348
+ let parsedRepo;
25349
+ try {
25350
+ parsedRepo = parseOwnerRepo(repo);
25351
+ } catch (e) {
25352
+ return fail(`bootstrap rollback: ${e.message}`);
25353
+ }
25354
+ const manifestPath = "skills/bootstrap/seeds/manifest.json";
25355
+ if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap rollback: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the manifest names which targets are org-owned and therefore propagated (and rollback-able)`);
25356
+ const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
25357
+ const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
25358
+ if (!o.target) {
25359
+ return fail(`bootstrap rollback: --target <path> is required \u2014 one of:
25360
+ ${propagatable.map((s) => s.target).join("\n ")}`);
25361
+ }
25362
+ const seed = propagatable.find((s) => s.target === o.target);
25363
+ if (!seed) return fail(`bootstrap rollback: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable (hence rollback-able) targets:
25364
+ ${propagatable.map((s) => s.target).join("\n ")}`);
25365
+ const slug = parsedRepo.slug;
25366
+ const baseBranch = o.class === "content" ? "main" : "development";
25367
+ const branchPrefix = "seed-propagate";
25368
+ const propagateBranch = `${branchPrefix}-${slug}`;
25369
+ const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
25370
+ const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
25371
+ let candidates;
25372
+ if (o.record) {
25373
+ if (!(0, import_node_fs31.existsSync)(o.record)) return fail(`bootstrap rollback: --record '${o.record}' not found`);
25374
+ let parsed;
25375
+ try {
25376
+ parsed = JSON.parse((0, import_node_fs31.readFileSync)(o.record, "utf8"));
25377
+ } catch (e) {
25378
+ return fail(`bootstrap rollback: --record '${o.record}' is not valid JSON: ${e.message}`);
25379
+ }
25380
+ const recs = Array.isArray(parsed?.records) ? parsed.records : Array.isArray(parsed) ? parsed : [];
25381
+ candidates = recs.map((r) => {
25382
+ try {
25383
+ return seedPrRecordFromPropagationRecord(r);
25384
+ } catch {
25385
+ return null;
25386
+ }
25387
+ }).filter((r) => r !== null);
25388
+ } else {
25389
+ candidates = [];
25390
+ try {
25391
+ const listed = await gh(["pr", "list", "--repo", repo, "--head", propagateBranch, "--base", baseBranch, "--state", "merged", "--json", "number,url,mergedAt,mergeCommit,files", "--limit", "20"]);
25392
+ const arr = JSON.parse(listed.stdout || "[]");
25393
+ for (const p of arr) {
25394
+ if (!p.mergeCommit?.oid) continue;
25395
+ candidates.push({
25396
+ repo,
25397
+ target: seed.target,
25398
+ number: p.number,
25399
+ url: p.url,
25400
+ mergeSha: p.mergeCommit.oid,
25401
+ mergedAt: p.mergedAt ?? "",
25402
+ files: (p.files ?? []).map((f) => f.path)
25403
+ });
25404
+ }
25405
+ } catch (e) {
25406
+ return fail(`bootstrap rollback: could not read ${repo}'s merged ${propagateBranch} PR history: ${e.message}`);
25407
+ }
25408
+ }
25409
+ const plan = planRollback(repo, seed.target, slug, candidates);
25410
+ if (!plan.resolution.found) {
25411
+ if (o.json) console.log(JSON.stringify(plan, null, 2));
25412
+ else console.log(renderRollbackReport(plan));
25413
+ return fail(`bootstrap rollback: ${plan.resolution.reason}`);
25414
+ }
25415
+ if (o.execute) {
25416
+ const record = plan.resolution.record;
25417
+ const parentResp = await gh(["api", `repos/${repo}/commits/${record.mergeSha}`, "--jq", ".parents[0].sha"]);
25418
+ const parentSha = parentResp.stdout.trim();
25419
+ if (!parentSha) return fail(`bootstrap rollback: could not resolve ${repo}@${record.mergeSha}'s parent commit \u2014 refusing to guess what to restore`);
25420
+ let preSeedContent = null;
25421
+ try {
25422
+ const resp = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${parentSha}`]);
25423
+ const parsed = JSON.parse(resp.stdout);
25424
+ preSeedContent = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
25425
+ } catch {
25426
+ preSeedContent = null;
25427
+ }
25428
+ if (preSeedContent == null) return fail(`bootstrap rollback: '${seed.target}' did not exist in ${repo} at ${parentSha} (the commit before the seed merge) \u2014 nothing to restore; refusing to guess`);
25429
+ const baseRef = await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
25430
+ const baseSha = baseRef.stdout.trim();
25431
+ let branchExists = true;
25432
+ try {
25433
+ await gh(["api", `repos/${repo}/git/ref/heads/${plan.branch}`]);
25434
+ } catch {
25435
+ branchExists = false;
25436
+ }
25437
+ if (!branchExists) await gh(["api", `repos/${repo}/git/refs`, "-f", `ref=refs/heads/${plan.branch}`, "-f", `sha=${baseSha}`]);
25438
+ let existingSha;
25439
+ try {
25440
+ const cur = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${plan.branch}`]);
25441
+ existingSha = JSON.parse(cur.stdout).sha;
25442
+ } catch {
25443
+ existingSha = void 0;
25444
+ }
25445
+ const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
25446
+ (0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
25447
+ try {
25448
+ await gh(contentPutInputArgs(repo, seed.target, tmp));
25449
+ } finally {
25450
+ try {
25451
+ (0, import_node_fs31.unlinkSync)(tmp);
25452
+ } catch {
25453
+ }
25454
+ }
25455
+ const openPrs = await gh(["pr", "list", "--repo", repo, "--head", plan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
25456
+ const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
25457
+ let prUrl;
25458
+ if (prDecision.action === "reuse") {
25459
+ prUrl = prDecision.url;
25460
+ } else {
25461
+ const created = await ghCreate(["pr", "create", "--repo", repo, "--base", baseBranch, "--head", plan.branch, "--title", plan.title, "--body", plan.body]);
25462
+ prUrl = created.url;
25463
+ }
25464
+ plan.prUrl = prUrl;
25465
+ }
25466
+ if (o.json) console.log(JSON.stringify(plan, null, 2));
25467
+ else console.log(renderRollbackReport(plan));
25468
+ });
25274
25469
  }
25275
25470
 
25276
25471
  // src/stage-commands.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.99.0",
3
+ "version": "3.100.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",