@mutmutco/cli 4.5.169 → 4.5.171

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 +142 -17
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -10145,6 +10145,19 @@ function retriedFetch(deps, url, init) {
10145
10145
  sleep: deps.retrySleep
10146
10146
  });
10147
10147
  }
10148
+ async function backfillDeployHistory(since, apply, deps) {
10149
+ if (!deps.baseUrl) throw new Error("Hub API URL not configured");
10150
+ const token = await deps.token();
10151
+ if (!token) throw new Error("no Hub session token (run `gh auth login`)");
10152
+ const res = await retriedFetch({ ...deps, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS }, `${deps.baseUrl.replace(/\/$/, "")}/deploy-history/backfill`, {
10153
+ method: "POST",
10154
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
10155
+ body: JSON.stringify({ since, dryRun: !apply })
10156
+ });
10157
+ const body = await res.json();
10158
+ if (!res.ok) throw new Error(`deploy history backfill HTTP ${res.status}: ${body.error ?? res.statusText}`);
10159
+ return body;
10160
+ }
10148
10161
  async function fetchTrainAuthority(repo, deps) {
10149
10162
  if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
10150
10163
  const token = await deps.token();
@@ -19074,10 +19087,10 @@ var rollout_plan_default = {
19074
19087
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
19075
19088
  },
19076
19089
  baseline: {
19077
- version: "4.5.169",
19078
- tag: "v4.5.169",
19079
- commit: "4732697958bc",
19080
- npm: "@mutmutco/cli@4.5.169"
19090
+ version: "4.5.171",
19091
+ tag: "v4.5.171",
19092
+ commit: "79faa07090b1",
19093
+ npm: "@mutmutco/cli@4.5.171"
19081
19094
  },
19082
19095
  exitCriterion: "fleet-n-of-n",
19083
19096
  hubOnlyShortcut: "forbidden",
@@ -19094,14 +19107,14 @@ var rollout_plan_default = {
19094
19107
  repo: "mutmutco/mmi-hub",
19095
19108
  role: "canary",
19096
19109
  schedule: "train",
19097
- v3Target: "v4.5.169"
19110
+ v3Target: "v4.5.171"
19098
19111
  }
19099
19112
  ],
19100
19113
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
19101
19114
  rollback: {
19102
19115
  independent: true,
19103
- mechanism: "npm dist-tag latest -> 4.5.169 and redeploy the Hub Lambda from tag v4.5.169 (4732697958bc); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
19104
- v3Target: "v4.5.169 (@mutmutco/cli@4.5.169, tag commit 4732697958bc \u2014 last known-good release carrying the repo-index v4-only contract)"
19116
+ mechanism: "npm dist-tag latest -> 4.5.171 and redeploy the Hub Lambda from tag v4.5.171 (79faa07090b1); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
19117
+ v3Target: "v4.5.171 (@mutmutco/cli@4.5.171, tag commit 79faa07090b1 \u2014 last known-good release carrying the repo-index v4-only contract)"
19105
19118
  }
19106
19119
  },
19107
19120
  {
@@ -19291,7 +19304,7 @@ function formatTrainStatus(r) {
19291
19304
  "",
19292
19305
  `release track: ${r.releaseTrack} \u2014 stages: ${r.branches.join(" -> ")}`,
19293
19306
  `cli version: ${r.cliVersion}${r.repoVersion ? ` (repo: ${r.repoVersion})` : ""}${r.releasedVersion ? ` (released: ${r.releasedVersion})` : ""}`,
19294
- `release bump intent: ${r.bumpIntent ?? "not declared \u2014 an ordinary train defaults to patch (#4929); set MMI_BUMP_INTENT=major|minor to override"}`
19307
+ `release bump intent: ${r.bumpIntent ?? "not declared \u2014 an ordinary train defaults to patch (#4929)"} (proposed: patch; minor needs MMI_BUMP_REASON, major also MMI_MAJOR_APPROVED=<version> \u2014 docs/versioning.md)`
19295
19308
  ];
19296
19309
  if (r.versionLag.ok) {
19297
19310
  lines2.push("version lag: none \u2014 CLI is current");
@@ -26549,6 +26562,53 @@ async function selfConvergeTrainCli(input) {
26549
26562
  }
26550
26563
  }
26551
26564
 
26565
+ // src/bump-proposal.ts
26566
+ var BREAKING_BANG = /^[a-z]+(\([^)]*\))?!:/i;
26567
+ var BREAKING_TRAILER = /^BREAKING[ -]CHANGE:/m;
26568
+ var MAX_BUMP_REASON = 200;
26569
+ function proposeBump(commits, _opts = {}) {
26570
+ const breaking = [];
26571
+ for (const c of commits) {
26572
+ if (BREAKING_BANG.test(c.subject) || BREAKING_TRAILER.test(c.body)) breaking.push(`${c.sha.slice(0, 8)} ${c.subject}`);
26573
+ }
26574
+ return { proposed: "patch", breaking };
26575
+ }
26576
+ function parseRangeLog(raw) {
26577
+ return raw.split("").map((r) => r.replace(/^\n+/, "")).filter((r) => r.trim()).map((r) => {
26578
+ const [sha = "", subject = "", body = ""] = r.split("");
26579
+ return { sha: sha.trim(), subject: subject.trim(), body };
26580
+ });
26581
+ }
26582
+ var RANGE_LOG_FORMAT = "--format=%H%x1f%s%x1f%b%x1e";
26583
+ function enforceBumpRules(input) {
26584
+ const { env, proposal } = input;
26585
+ const effective = input.preOneZero && input.intent === "major" ? "minor" : input.intent;
26586
+ const reason = (env.MMI_BUMP_REASON ?? "").trim();
26587
+ const evidence = proposal.breaking.join("; ");
26588
+ const decision = () => ({ intent: effective, reason: reason || null, proposed: proposal.proposed, breaking: proposal.breaking });
26589
+ if (effective !== "patch") {
26590
+ if (!reason) throw new Error(`MMI_BUMP_INTENT=${input.intent} needs MMI_BUMP_REASON \u2014 a one-line reason (\u2264${MAX_BUMP_REASON} chars) for a real feature or significant change; patch is the default (#8499)`);
26591
+ if (/[\r\n]/.test(reason) || reason.length > MAX_BUMP_REASON) throw new Error(`MMI_BUMP_REASON must be one line of at most ${MAX_BUMP_REASON} chars (#8499)`);
26592
+ }
26593
+ if (effective === "major") {
26594
+ const approved = (env.MMI_MAJOR_APPROVED ?? "").trim().replace(/^v/, "");
26595
+ const target = (input.majorTarget ?? "").replace(/^v/, "");
26596
+ if (!approved) throw new Error(`a major release is owner-gated: set MMI_MAJOR_APPROVED=${target || "<target version>"} only after Jervaise's fresh approval in this turn (#8499)`);
26597
+ if (approved !== target) throw new Error(`MMI_MAJOR_APPROVED=${approved} does not match the major target ${target}; a stale approval cannot tag a different major (#8499)`);
26598
+ return decision();
26599
+ }
26600
+ if (proposal.breaking.length && effective === "patch") {
26601
+ throw new Error(input.preOneZero ? `range carries breaking changes (${evidence}): this repo is held on 0.x.y, so declare MMI_BUMP_INTENT=minor with MMI_BUMP_REASON, or ship a shim (patch) (#8499)` : `range carries breaking changes (${evidence}): ship a shim (patch) or declare an owner-gated major (MMI_BUMP_INTENT=major, MMI_BUMP_REASON, MMI_MAJOR_APPROVED=<version>) (#8499)`);
26602
+ }
26603
+ if (proposal.breaking.length && !input.preOneZero) {
26604
+ throw new Error(`range carries breaking changes (${evidence}): ship a shim (patch) or declare an owner-gated major (#8499)`);
26605
+ }
26606
+ return decision();
26607
+ }
26608
+ function bumpNotesLine(d) {
26609
+ return `Version bump: ${d.intent} \u2014 ${d.reason ?? "default patch"}`;
26610
+ }
26611
+
26552
26612
  // src/train-doctor.ts
26553
26613
  var TRAIN_LANES = ["release", "rcand", "hotfix"];
26554
26614
  var TROUBLESHOOTING_GUIDE = "docs/Guides/train-troubleshooting.md";
@@ -26844,6 +26904,17 @@ async function runTrainDoctor(input) {
26844
26904
  if (workflows === null && !startBranchMissing) {
26845
26905
  add({ code: "workflows-unreadable", severity: "blocker", source: "local", title: `.github/workflows at ${workflowsRef} could not be read from ${cwd} \u2014 the tag-addressability (#5428) and npm-major (#5666) preflights did not run`, remedy: `run from a checkout whose git can read ${workflowsRef} (fetch origin first), then rerun; an unread workflow set is unverified, not green` });
26846
26906
  }
26907
+ if (lane === "release" && !startBranchMissing && startBranch) {
26908
+ try {
26909
+ const proposal = proposeBump(parseRangeLog(await git3(["log", `origin/main..origin/${startBranch}`, RANGE_LOG_FORMAT])));
26910
+ add({ code: "bump-proposal", severity: "info", source: "origin", title: `bump proposal: ${proposal.proposed}`, remedy: "nothing \u2014 patch is the default; minor needs MMI_BUMP_REASON, major also MMI_MAJOR_APPROVED=<version> (docs/versioning.md)" });
26911
+ for (const evidence of proposal.breaking) {
26912
+ add({ code: "bump-breaking", severity: "warning", source: "origin", title: `breaking signal in the release range: ${evidence}`, remedy: "ship a shim (patch) or declare an owner-gated major; the train refuses otherwise" });
26913
+ }
26914
+ } catch (e) {
26915
+ void e;
26916
+ }
26917
+ }
26847
26918
  if (lane === "release" && !startBranchMissing) {
26848
26919
  const required = meta?.requiredChecks?.length ? meta.requiredChecks : ["gate"];
26849
26920
  const emitters = (workflows ?? []).filter((f) => parseWorkflowJobIds(f.body).some((id) => required.some((context) => contextMatchesJobId(id, context))));
@@ -29610,6 +29681,17 @@ async function resolveTrainBumpIntent(deps, env = process.env, latestRelease, pr
29610
29681
  }
29611
29682
  return "patch";
29612
29683
  }
29684
+ async function enforceTrainBump(deps, env, intent, preOneZero, source) {
29685
+ const raw = await deps.run("git", ["log", `origin/main..${source}`, RANGE_LOG_FORMAT]);
29686
+ const proposal = proposeBump(parseRangeLog(raw), { preOneZero });
29687
+ let majorTarget;
29688
+ if (intent === "major") {
29689
+ const explicit = (env.MMI_RELEASE_VERSION ?? "").trim().replace(/^v/, "");
29690
+ const latest = await latestLocalReleaseTag(deps, void 0, preOneZero);
29691
+ majorTarget = explicit || `${latest.major + 1}.0.0`;
29692
+ }
29693
+ return enforceBumpRules({ intent, env, proposal, preOneZero, majorTarget });
29694
+ }
29613
29695
  async function resolveHotfixVersion(deps, env = process.env, preOneZero = false) {
29614
29696
  const latest = await latestLocalReleaseTag(deps, "origin/main", preOneZero);
29615
29697
  if (latest.major === 0 && latest.minor === 0 && latest.patch === 0) {
@@ -29655,9 +29737,19 @@ async function runTrainApplyLocked(command, deps, options = {}) {
29655
29737
  await requireCleanTree(deps);
29656
29738
  await runGitRemoteRead(deps, ["fetch", "origin"]);
29657
29739
  const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
29658
- const bumpIntent = await resolveTrainBumpIntent(deps, process.env, void 0, preOneZeroInForce(meta));
29740
+ const declaredIntent = await resolveTrainBumpIntent(deps, process.env, void 0, preOneZeroInForce(meta));
29659
29741
  const branchHints = await loadReleaseTrackBranchHints(deps);
29660
29742
  const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
29743
+ const bump = await enforceTrainBump(
29744
+ deps,
29745
+ process.env,
29746
+ declaredIntent,
29747
+ preOneZeroInForce(meta),
29748
+ command === "release" && !directTrack && options.dev !== true ? "origin/rc" : "origin/development"
29749
+ );
29750
+ const bumpIntent = bump.intent;
29751
+ const bumpReceipt = { intent: bump.intent, reason: bump.reason, proposed: bump.proposed, breaking: bump.breaking };
29752
+ options = { ...options, bumpLine: bumpNotesLine(bump) };
29661
29753
  assertSuiteAutoVersion(ctx.repo, meta, process.env, command === "release" && !directTrack);
29662
29754
  if (command === "release" && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile, options.announceSummaryFileTr);
29663
29755
  const pipelineInput = { deps, ctx, command, meta, branchHints, watch, options, bumpIntent };
@@ -29665,28 +29757,31 @@ async function runTrainApplyLocked(command, deps, options = {}) {
29665
29757
  if (directTrack) {
29666
29758
  throw new Error("direct-track repos release straight to main (no rc); run `mmi-cli devops release --apply` from development instead of rcand");
29667
29759
  }
29668
- return withCheckoutRestoredAfterRelease(deps, await runTrainApplyPipeline("rcand", pipelineInput), "development");
29760
+ return withCheckoutRestoredAfterRelease(deps, await runPipelineWithBump(bumpReceipt, "rcand", pipelineInput), "development");
29669
29761
  }
29670
29762
  if (directTrack) {
29671
29763
  return withCheckoutRestoredAfterRelease(
29672
29764
  deps,
29673
- await runTrainApplyPipeline("release-dev", { ...pipelineInput, directTrack: true }),
29765
+ await runPipelineWithBump(bumpReceipt, "release-dev", { ...pipelineInput, directTrack: true }),
29674
29766
  "development"
29675
29767
  );
29676
29768
  }
29677
29769
  if (command === "release" && options.dev) {
29678
29770
  return withCheckoutRestoredAfterRelease(
29679
29771
  deps,
29680
- await runTrainApplyPipeline("release-dev", pipelineInput),
29772
+ await runPipelineWithBump(bumpReceipt, "release-dev", pipelineInput),
29681
29773
  "development"
29682
29774
  );
29683
29775
  }
29684
29776
  return withCheckoutRestoredAfterRelease(
29685
29777
  deps,
29686
- await runTrainApplyPipeline("release-full", pipelineInput),
29778
+ await runPipelineWithBump(bumpReceipt, "release-full", pipelineInput),
29687
29779
  "rc"
29688
29780
  );
29689
29781
  }
29782
+ async function runPipelineWithBump(bump, mode, input) {
29783
+ return { ...await runTrainApplyPipeline(mode, input), bump };
29784
+ }
29690
29785
 
29691
29786
  // src/ci-audit.ts
29692
29787
  var HUB_REPO4 = "mutmutco/MMI-Hub";
@@ -31076,6 +31171,7 @@ function consolidateCommandNamespaces(program3) {
31076
31171
  const runtimeDeploy = runtime.command("deploy").description("runtime deployment status");
31077
31172
  const deploy = child(program3, "deploy");
31078
31173
  move(deploy, runtimeDeploy, "status");
31174
+ move(deploy, runtimeDeploy, "history-backfill");
31079
31175
  remove(program3, "deploy");
31080
31176
  move(program3, runtime, "box");
31081
31177
  move(program3, runtime, "edge");
@@ -32476,7 +32572,7 @@ function parseTrainTag(tag) {
32476
32572
  function cmpTrainRelease(a, b) {
32477
32573
  return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
32478
32574
  }
32479
- function trainPlanTargetsFromTags(tags, explicitRaw) {
32575
+ function trainPlanTargetsFromTags(tags, explicitRaw, intent = process.env.MMI_BUMP_INTENT) {
32480
32576
  const all = tags.map(parseTrainTag).filter((v) => Boolean(v));
32481
32577
  const release = all.find((v) => v.rc === null) ?? { major: 0, minor: 0, patch: 0, rc: null };
32482
32578
  const targets = {};
@@ -32493,7 +32589,8 @@ function trainPlanTargetsFromTags(tags, explicitRaw) {
32493
32589
  }
32494
32590
  }
32495
32591
  if (!targets.plannedReleaseError) {
32496
- const planned = explicit ?? { major: release.major, minor: release.minor + 1, patch: 0, rc: null };
32592
+ const bumped = intent === "major" ? { major: release.major + 1, minor: 0, patch: 0 } : intent === "minor" ? { major: release.major, minor: release.minor + 1, patch: 0 } : { major: release.major, minor: release.minor, patch: release.patch + 1 };
32593
+ const planned = explicit ?? { ...bumped, rc: null };
32497
32594
  targets.plannedRelease = `v${planned.major}.${planned.minor}.${planned.patch}`;
32498
32595
  }
32499
32596
  let rcs = all.filter((v) => v.rc !== null);
@@ -32983,6 +33080,9 @@ function parseSecretsCatalogVar(raw) {
32983
33080
  if (e.aliases !== void 0 && (!Array.isArray(e.aliases) || e.aliases.some((a) => typeof a !== "string"))) {
32984
33081
  throw new Error(`org project set: secrets["${mapKey}"].aliases must be a string array`);
32985
33082
  }
33083
+ if (e.branchReadable !== void 0 && typeof e.branchReadable !== "boolean") {
33084
+ throw new Error(`org project set: secrets["${mapKey}"].branchReadable must be a boolean`);
33085
+ }
32986
33086
  }
32987
33087
  return parsed;
32988
33088
  }
@@ -33481,7 +33581,7 @@ var SETTABLE_VAR_HINTS = {
33481
33581
  requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
33482
33582
  tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",output?:"file",secretOutput?:{path,vault:{repo,key},overwrite?}}} \u2014 merged per entry, at most 20 stored in total; retire ONE task with --unset-task NAME; use {artifact} in argv when required; a secretOutput is master-only',
33483
33583
  internalPeers: "JSON {owner/repo:[dev|rc|main]} \u2014 same-box suite peers via the box Caddy; clear with --unset internalPeers (#7886)",
33484
- secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets; prefer --secrets-file",
33584
+ secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[],branchReadable?} \u2014 merged per entry; drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets; prefer --secrets-file",
33485
33585
  edgeDomains: "JSON {dev,rc,main} domain map",
33486
33586
  statusOptions: "JSON name\u2192id map",
33487
33587
  priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
@@ -37883,6 +37983,19 @@ function formatDeployStatus(r) {
37883
37983
  var STAGES2 = ["dev", "rc", "main"];
37884
37984
  function registerDeployCommands(program3) {
37885
37985
  const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
37986
+ deploy.command("history-backfill").description("master-only registry history backfill from Hub tenant-deploy runs (dry-run by default; no dispatch)").requiredOption("--since <ISO>", "include runs created at or after this timestamp").option("--apply", "write successful runs to Hub deploy history").option("--json", "machine-readable counts").action(async (o) => {
37987
+ try {
37988
+ const report = await backfillDeployHistory(o.since, o.apply === true, registryClientDeps(await loadConfig()));
37989
+ if (o.json) console.log(JSON.stringify(report, null, 2));
37990
+ else {
37991
+ console.log(`${report.dryRun ? "Dry-run" : "Applied"} since ${report.since}: ${report.totalRuns} runs, ${report.written} written`);
37992
+ for (const [repo, counts] of Object.entries(report.perRepo)) console.log(`${repo}: ${counts.success} success, ${counts.failed} failed, ${counts.cancelled} cancelled, ${counts.other} other`);
37993
+ console.log(`Skipped: ${report.skipped.nonSha} non-SHA, ${report.skipped.unregistered} unregistered/ambiguous, ${report.skipped.badTitle} bad title`);
37994
+ }
37995
+ } catch (e) {
37996
+ return failGraceful(`deploy history-backfill: ${e.message}`);
37997
+ }
37998
+ });
37886
37999
  deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo); a root answering 401/403/404 is re-probed at /health and the `url` field names the endpoint that answered (#6137)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
37887
38000
  if (!STAGES2.includes(stage)) {
37888
38001
  return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
@@ -42995,6 +43108,15 @@ async function openSuitePinPr(deps, input) {
42995
43108
  }
42996
43109
  }
42997
43110
 
43111
+ // src/pr-title-check.ts
43112
+ var CONVENTIONAL_PR_TITLE = /^(feat|fix|docs|chore|refactor|perf|test|build|ci|revert|style)(\([^)]+\))?!?: .+/;
43113
+ var EXEMPT_PR_TITLE = /^Merge pull request /;
43114
+ function prTitleRefusal(title) {
43115
+ const t = title.trim();
43116
+ if (CONVENTIONAL_PR_TITLE.test(t) || EXEMPT_PR_TITLE.test(t)) return null;
43117
+ return `pr land: PR title "${t}" is not a Conventional type. Expected \`<type>(<scope>)?(!)?: <summary>\` with type one of feat|fix|docs|chore|refactor|perf|test|build|ci|revert|style (e.g. \`fix(cli): refuse a stale tag\`); retitle with \`gh pr edit <n> --title \u2026\` and land again (#8499)`;
43118
+ }
43119
+
42998
43120
  // src/command-register-collaboration.ts
42999
43121
  var import_node_child_process17 = require("node:child_process");
43000
43122
  var import_node_fs47 = require("node:fs");
@@ -47935,6 +48057,9 @@ ${list}`);
47935
48057
  return typeof viewed.state === "string" ? viewed.state : void 0;
47936
48058
  }
47937
48059
  );
48060
+ const landTitle = await execFileP("gh", ["pr", "view", number, ...repoArgs, "--json", "title", "--jq", ".title"], { timeout: GC_GH_TIMEOUT_MS }).then((r) => r.stdout.trim(), () => "");
48061
+ const landTitleRefusal = landTitle ? prTitleRefusal(landTitle) : null;
48062
+ if (landTitleRefusal) return refused(landTitleRefusal);
47938
48063
  const landSquashBody = squashBodyTextForMerge(landClosingGuardInput, true);
47939
48064
  warnSquashBodyClosingStrip("pr land", landClosingGuardInput, landSquashBody);
47940
48065
  const landClosingGuardVerdict = evaluateClosingGuard(landClosingGuardInput, {
@@ -57656,7 +57781,7 @@ projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one
57656
57781
  }
57657
57782
  if (!result.ok) return failGraceful(`org project deploy retire: ${result.error ?? "read failed"}`);
57658
57783
  });
57659
- project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset-secret <KEY...>", "remove exactly these #2244 catalog entries (repeatable; KEY or KEY@dev|rc|main). Refuses while a stage still requires the key \u2014 drop it from requiredRuntimeSecrets/requiredBuildSecrets first").option("--unset-task <NAME...>", "retire exactly these tenant tasks (repeatable; lowercase kebab-case names). Refuses a name that is not declared. tenantTasks merges per entry and is capped at 20 stored in total").option("--confirm-clear-secrets", "required to let --unset secrets wipe the WHOLE catalog (the refusal names how many entries that removes)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets (the WHOLE catalog \u2014 needs --confirm-clear-secrets), edgeDomains, requiredGcpApis, publishRequired, releaseChannel, releaseLanguage").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
57784
+ project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>", "branchReadable":<optional boolean, master-only #8497>}').option("--unset-secret <KEY...>", "remove exactly these #2244 catalog entries (repeatable; KEY or KEY@dev|rc|main). Refuses while a stage still requires the key \u2014 drop it from requiredRuntimeSecrets/requiredBuildSecrets first").option("--unset-task <NAME...>", "retire exactly these tenant tasks (repeatable; lowercase kebab-case names). Refuses a name that is not declared. tenantTasks merges per entry and is capped at 20 stored in total").option("--confirm-clear-secrets", "required to let --unset secrets wipe the WHOLE catalog (the refusal names how many entries that removes)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets (the WHOLE catalog \u2014 needs --confirm-clear-secrets), edgeDomains, requiredGcpApis, publishRequired, releaseChannel, releaseLanguage").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
57660
57785
  const cfg = await loadConfig();
57661
57786
  let target;
57662
57787
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.169",
3
+ "version": "4.5.171",
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",