@mutmutco/cli 4.4.1 → 4.4.2

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 +258 -54
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -15870,10 +15870,10 @@ var rollout_plan_default = {
15870
15870
  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)."
15871
15871
  },
15872
15872
  baseline: {
15873
- version: "4.4.1",
15874
- tag: "v4.4.1",
15875
- commit: "a4bb2ac4bdb4",
15876
- npm: "@mutmutco/cli@4.4.1"
15873
+ version: "4.4.2",
15874
+ tag: "v4.4.2",
15875
+ commit: "678e071c6859",
15876
+ npm: "@mutmutco/cli@4.4.2"
15877
15877
  },
15878
15878
  exitCriterion: "fleet-n-of-n",
15879
15879
  hubOnlyShortcut: "forbidden",
@@ -15890,14 +15890,14 @@ var rollout_plan_default = {
15890
15890
  repo: "mutmutco/mmi-hub",
15891
15891
  role: "canary",
15892
15892
  schedule: "train",
15893
- v3Target: "v4.4.1"
15893
+ v3Target: "v4.4.2"
15894
15894
  }
15895
15895
  ],
15896
15896
  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.",
15897
15897
  rollback: {
15898
15898
  independent: true,
15899
- mechanism: "npm dist-tag latest -> 4.4.1 and redeploy the Hub Lambda from tag v4.4.1 (a4bb2ac4bdb4); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15900
- v3Target: "v4.4.1 (@mutmutco/cli@4.4.1, tag commit a4bb2ac4bdb4 \u2014 last known-good release carrying the repo-index v4-only contract)"
15899
+ mechanism: "npm dist-tag latest -> 4.4.2 and redeploy the Hub Lambda from tag v4.4.2 (678e071c6859); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15900
+ v3Target: "v4.4.2 (@mutmutco/cli@4.4.2, tag commit 678e071c6859 \u2014 last known-good release carrying the repo-index v4-only contract)"
15901
15901
  }
15902
15902
  },
15903
15903
  {
@@ -21631,6 +21631,48 @@ function supersedeablePublishLegShape(rec) {
21631
21631
  function deferredGatewayDeployLegShape(rec) {
21632
21632
  return rec.state === "pending" && rec.runId == null && rec.workflow === "jerv-gateway";
21633
21633
  }
21634
+ async function proveOriginPromotionOrArchived(deps, prior, path2, label, ref) {
21635
+ let liveSha = "";
21636
+ try {
21637
+ liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
21638
+ } catch (e) {
21639
+ throw new Error(`${label}: could not prove ${prior.tag} live on origin (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to close the leg on an unproven state; retry when origin answers (${ref})`);
21640
+ }
21641
+ if (liveSha.toLowerCase() === prior.tagSha.toLowerCase()) return "live";
21642
+ if (liveSha) {
21643
+ throw new Error(
21644
+ `${label}: origin reports ${prior.tag} at ${liveSha.slice(0, 12)} but the ledger anchors ${prior.tagSha.slice(0, 12)} \u2014 a divergent release is NOT supersedeable; nothing was written (${ref})`
21645
+ );
21646
+ }
21647
+ let repository;
21648
+ try {
21649
+ repository = JSON.parse(await deps.run("gh", ["repo", "view", prior.repo, "--json", "nameWithOwner"]));
21650
+ } catch (e) {
21651
+ throw new Error(`${label}: could not prove repository access for ${prior.repo} (${e instanceof Error ? e.message.split("\n")[0] : String(e)}) \u2014 a Release 404 is untrusted without that proof; nothing was written (${ref})`);
21652
+ }
21653
+ if (repository.nameWithOwner !== prior.repo) {
21654
+ throw new Error(`${label}: repository access proof named ${String(repository.nameWithOwner || "(missing)")}, not ${prior.repo} \u2014 nothing was written (${ref})`);
21655
+ }
21656
+ let releaseAbsent = false;
21657
+ try {
21658
+ await deps.run("gh", ["release", "view", prior.tag, "--repo", prior.repo, "--json", "tagName"]);
21659
+ } catch (e) {
21660
+ const detail = `${e instanceof Error ? e.message : String(e)} ${String(e.stderr ?? "")}`;
21661
+ releaseAbsent = /release not found|HTTP 404|\(404\)/i.test(detail);
21662
+ if (!releaseAbsent) {
21663
+ throw new Error(`${label}: could not prove the GitHub Release for ${prior.tag} absent (${detail.split("\n")[0]}) \u2014 refusing to close on an unproven state; nothing was written (${ref})`);
21664
+ }
21665
+ }
21666
+ if (!releaseAbsent) {
21667
+ throw new Error(`${label}: origin no longer has ${prior.tag} but its GitHub Release still exists \u2014 resolve the mixed external state before closing the ledger; nothing was written (${ref})`);
21668
+ }
21669
+ const archivePath = (0, import_node_path21.join)((0, import_node_path21.dirname)(path2), `phases.archive-${prior.tag}.json`);
21670
+ if ((0, import_node_fs21.existsSync)(archivePath)) {
21671
+ throw new Error(`${label}: archive already exists at ${archivePath} \u2014 preserving both ledger records; resolve the collision by hand; nothing was written (${ref})`);
21672
+ }
21673
+ (0, import_node_fs21.renameSync)(path2, archivePath);
21674
+ return "deleted";
21675
+ }
21634
21676
  async function supersedeContentCausedPublishLeg(deps, cwd) {
21635
21677
  const path2 = releaseLedgerPath(cwd);
21636
21678
  return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
@@ -21664,45 +21706,7 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
21664
21706
  `release --supersede-publish: ${prior.tag} has unresolved legs besides publish (${unresolved.map((p) => `${p}=${phases[p].state}`).join(", ")}) \u2014 the supersession closes only the publish leg of an otherwise-complete release; resolve those first, nothing was written (#6638)`
21665
21707
  );
21666
21708
  }
21667
- let liveSha = "";
21668
- try {
21669
- liveSha = (await deps.run("git", ["ls-remote", "origin", `refs/tags/${prior.tag}`])).trim().split(/\s+/)[0] ?? "";
21670
- } catch (e) {
21671
- throw new Error(`release --supersede-publish: could not prove ${prior.tag} live on origin (${e instanceof Error ? e.message : String(e)}) \u2014 refusing to close the leg on an unproven state; retry when origin answers (#6638)`);
21672
- }
21673
- if (liveSha.toLowerCase() !== prior.tagSha.toLowerCase()) {
21674
- if (liveSha) {
21675
- throw new Error(
21676
- `release --supersede-publish: origin reports ${prior.tag} at ${liveSha.slice(0, 12)} but the ledger anchors ${prior.tagSha.slice(0, 12)} \u2014 a divergent release is NOT supersedeable; nothing was written (#6638)`
21677
- );
21678
- }
21679
- let repository;
21680
- try {
21681
- repository = JSON.parse(await deps.run("gh", ["repo", "view", prior.repo, "--json", "nameWithOwner"]));
21682
- } catch (e) {
21683
- throw new Error(`release --supersede-publish: could not prove repository access for ${prior.repo} (${e instanceof Error ? e.message.split("\n")[0] : String(e)}) \u2014 a Release 404 is untrusted without that proof; nothing was written (#6638)`);
21684
- }
21685
- if (repository.nameWithOwner !== prior.repo) {
21686
- throw new Error(`release --supersede-publish: repository access proof named ${String(repository.nameWithOwner || "(missing)")}, not ${prior.repo} \u2014 nothing was written (#6638)`);
21687
- }
21688
- let releaseAbsent = false;
21689
- try {
21690
- await deps.run("gh", ["release", "view", prior.tag, "--repo", prior.repo, "--json", "tagName"]);
21691
- } catch (e) {
21692
- const detail = `${e instanceof Error ? e.message : String(e)} ${String(e.stderr ?? "")}`;
21693
- releaseAbsent = /release not found|HTTP 404|\(404\)/i.test(detail);
21694
- if (!releaseAbsent) {
21695
- throw new Error(`release --supersede-publish: could not prove the GitHub Release for ${prior.tag} absent (${detail.split("\n")[0]}) \u2014 refusing to close on an unproven state; nothing was written (#6638)`);
21696
- }
21697
- }
21698
- if (!releaseAbsent) {
21699
- throw new Error(`release --supersede-publish: origin no longer has ${prior.tag} but its GitHub Release still exists \u2014 resolve the mixed external state before closing the ledger; nothing was written (#6638)`);
21700
- }
21701
- const archivePath = (0, import_node_path21.join)((0, import_node_path21.dirname)(path2), `phases.archive-${prior.tag}.json`);
21702
- if ((0, import_node_fs21.existsSync)(archivePath)) {
21703
- throw new Error(`release --supersede-publish: archive already exists at ${archivePath} \u2014 preserving both ledger records; resolve the collision by hand; nothing was written (#6638)`);
21704
- }
21705
- (0, import_node_fs21.renameSync)(path2, archivePath);
21709
+ if (await proveOriginPromotionOrArchived(deps, prior, path2, "release --supersede-publish", "#6638") === "deleted") {
21706
21710
  return {
21707
21711
  command: "release-supersede-publish",
21708
21712
  repo: prior.repo,
@@ -21749,6 +21753,115 @@ async function supersedeContentCausedPublishLeg(deps, cwd) {
21749
21753
  };
21750
21754
  });
21751
21755
  }
21756
+ async function supersedeContentCausedDeployAndPublishLegs(deps, cwd) {
21757
+ const label = "release --supersede-deploy --supersede-publish";
21758
+ const ref = "#6752";
21759
+ const path2 = releaseLedgerPath(cwd);
21760
+ return withFileLock(`${path2}.lock`, { label: "release ledger" }, async () => {
21761
+ let prior;
21762
+ try {
21763
+ prior = parseReleaseLedger((0, import_node_fs21.readFileSync)(path2, "utf8"));
21764
+ } catch (e) {
21765
+ throw new ReleaseLedgerError(
21766
+ "unreadable",
21767
+ `${label}: the active release ledger at ${path2} could not be read (${e instanceof Error ? e.message : String(e)}) \u2014 resolve it by hand before any train; nothing was written (${ref})`
21768
+ );
21769
+ }
21770
+ const deploy = prior.phases.deploy;
21771
+ const publish = prior.phases.publish;
21772
+ const unresolvedLeg = (rec) => rec.superseded !== true && (rec.state === "pending" || rec.state === "failed");
21773
+ const deployUnresolved = unresolvedLeg(deploy);
21774
+ const publishUnresolved = unresolvedLeg(publish);
21775
+ if (!deployUnresolved && !publishUnresolved) {
21776
+ throw new Error(
21777
+ deploy.superseded === true && publish.superseded === true ? `${label}: the ${prior.tag} deploy and publish legs are already superseded \u2014 nothing to do; the next train runs normally` : `${label}: the ${prior.tag} deploy and publish legs are both already resolved (deploy=${deploy.state}, publish=${publish.state}) \u2014 nothing to close; nothing was written (${ref})`
21778
+ );
21779
+ }
21780
+ if (!deployUnresolved) {
21781
+ throw new Error(`${label}: only the ${prior.tag} publish leg is unresolved (deploy=${deploy.state}) \u2014 use --supersede-publish for a single content-caused leg; nothing was written (${ref})`);
21782
+ }
21783
+ if (!publishUnresolved) {
21784
+ throw new Error(`${label}: only the ${prior.tag} deploy leg is unresolved (publish=${publish.state}) \u2014 use --supersede-deploy for a single content-caused leg; nothing was written (${ref})`);
21785
+ }
21786
+ if (!supersedeablePublishLegShape(publish)) {
21787
+ throw new Error(
21788
+ `${label}: the ${prior.tag} publish leg is ${publish.state}` + (publish.runId != null ? ` with run ${publish.runId} \u2014 a run-backed leg is resolved by watching/reverifying its run, never superseded` : "") + `; the combined closure closes only a FAILED publish leg, or a PENDING one no run correlates (the uncorrelatable historical shape); nothing was written (${ref})`
21789
+ );
21790
+ }
21791
+ const deferredGateway = isJervHubRepo(prior.repo) && deferredGatewayDeployLegShape(deploy);
21792
+ if (!(deploy.state === "failed" && deploy.runId != null || deferredGateway)) {
21793
+ throw new Error(
21794
+ `${label}: the ${prior.tag} deploy leg is ${deploy.state}` + (deploy.runId != null ? ` with run ${deploy.runId} \u2014 a run-backed leg is resolved by watching/reverifying its run, never superseded` : "") + `; the combined closure closes only a content-caused FAILED deploy leg with its run id, or the deferred Jerv-Hub operator-host Gateway leg (#6742); nothing was written (${ref})`
21795
+ );
21796
+ }
21797
+ const phases = { ...prior.phases };
21798
+ if (phases.alignment.state === "pending") {
21799
+ const closed = await alignmentLegClosedLive(deps, prior);
21800
+ if (closed) phases.alignment = closed;
21801
+ }
21802
+ const unresolved = ["promotion", "ordinaryCi", "githubRelease", "alignment"].filter((phase) => phases[phase].state === "pending" || phases[phase].state === "failed");
21803
+ if (unresolved.length > 0) {
21804
+ throw new Error(
21805
+ `${label}: ${prior.tag} has unresolved legs besides deploy and publish (${unresolved.map((p) => `${p}=${phases[p].state}`).join(", ")}) \u2014 the combined closure closes only the two content-caused legs of an otherwise-complete release; resolve those first, nothing was written (${ref})`
21806
+ );
21807
+ }
21808
+ if (await proveOriginPromotionOrArchived(deps, prior, path2, label, ref) === "deleted") {
21809
+ return {
21810
+ command: "release-supersede-deploy-and-publish",
21811
+ repo: prior.repo,
21812
+ tag: prior.tag,
21813
+ tagSha: prior.tagSha,
21814
+ note: `closed the deleted ${prior.tag} deploy and publish failures by archiving its ledger after origin and GitHub both proved absent; start a fresh release train to carry the cause fix`
21815
+ };
21816
+ }
21817
+ let deployEntry;
21818
+ let gatewayDeploy;
21819
+ if (deferredGateway) {
21820
+ const dispatch = await appendJervGatewayReleaseDeploy(
21821
+ deps,
21822
+ prior.repo,
21823
+ prior.tag,
21824
+ prior.tagSha,
21825
+ { note: `Jerv Gateway operator-host deploy leg for ${prior.tag} run during the deploy-and-publish supersession`, deployStatus: "success" }
21826
+ );
21827
+ deployEntry = {
21828
+ ...phaseEntry(deployPhaseInput("registry-publish", dispatch, { releaseSha: prior.tagSha })),
21829
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
21830
+ };
21831
+ if (dispatch.deployStatus !== "success") {
21832
+ throw new Error(
21833
+ `${label}: the deferred ${prior.tag} Jerv Gateway operator-host deploy leg did not verify (${dispatch.note}) \u2014 run \`scripts/jerv-gateway-release-deploy.sh --tag ${prior.tag}\` to a healthy receipt naming the released tag and commit, then retry; nothing was written (#6742)`
21834
+ );
21835
+ }
21836
+ gatewayDeploy = { workflow: "jerv-gateway", state: "complete", note: dispatch.note };
21837
+ } else {
21838
+ deployEntry = {
21839
+ state: "skipped",
21840
+ sha: prior.tagSha,
21841
+ superseded: true,
21842
+ note: `superseded: content-caused deploy failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, ${ref})`,
21843
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
21844
+ };
21845
+ }
21846
+ phases.deploy = deployEntry;
21847
+ phases.publish = {
21848
+ state: "skipped",
21849
+ sha: prior.tagSha,
21850
+ superseded: true,
21851
+ note: `superseded: content-caused publish failure on the immutable promoted ${prior.tag} \u2014 the cause fix rides the next patch train (master-approved closure, ${ref})`,
21852
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
21853
+ };
21854
+ atomicWriteJson(path2, { ...prior, phases, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
21855
+ return {
21856
+ command: "release-supersede-deploy-and-publish",
21857
+ repo: prior.repo,
21858
+ tag: prior.tag,
21859
+ tagSha: prior.tagSha,
21860
+ note: `closed the ${prior.tag} deploy and publish legs as superseded (skipped) \u2014 the ledger is phases-green; rerun the train doctor to confirm, then run the next patch train (hotfix or release) to carry the cause fix`,
21861
+ ...gatewayDeploy ? { gatewayDeploy } : {}
21862
+ };
21863
+ });
21864
+ }
21752
21865
  async function reverifyPriorRunLegs(deps, path2, targetTag) {
21753
21866
  let prior;
21754
21867
  try {
@@ -23835,6 +23948,17 @@ async function runReleaseSupersedePublish(deps, options = {}) {
23835
23948
  throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
23836
23949
  }
23837
23950
  }
23951
+ async function runReleaseSupersedeDeployAndPublish(deps, options = {}) {
23952
+ if (!options.approved) {
23953
+ throw new Error("release --supersede-deploy --supersede-publish requires --apply after explicit master approval; nothing was written");
23954
+ }
23955
+ try {
23956
+ return await supersedeContentCausedDeployAndPublishLegs(deps, process.cwd());
23957
+ } catch (e) {
23958
+ const message2 = e instanceof Error ? e.message : String(e);
23959
+ throw new Error(message2.includes("nothing was written") || message2.includes("nothing to do") ? message2 : `${message2}; nothing was written`);
23960
+ }
23961
+ }
23838
23962
  async function runReleasePublishRetry(deps, runId, options = {}) {
23839
23963
  if (!options.approved) {
23840
23964
  throw new Error("release --retry-publish requires --apply after explicit approval; nothing was written");
@@ -29699,6 +29823,7 @@ var UNSET_KEY_SET = new Set(UNSET_KEYS);
29699
29823
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
29700
29824
  var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
29701
29825
  var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
29826
+ var SECRET_CATALOG_MAP_KEY_RE = /^[A-Za-z][A-Za-z0-9_]*(?:@(?:dev|rc|main))?$/;
29702
29827
  var BUILD_SECRET_REF_RE = /^(?:[A-Z_][A-Z0-9_]*=)?(?:[A-Z_][A-Z0-9_]*|(?:_org\/[a-z0-9][a-z0-9-]*):[A-Z_][A-Z0-9_]*|@github-packages-token)$/;
29703
29828
  function previewRegistryMetaMerge(existing, patch) {
29704
29829
  const out = { ...existing ?? {} };
@@ -30140,7 +30265,7 @@ var SETTABLE_VAR_HINTS = {
30140
30265
  requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
30141
30266
  requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
30142
30267
  tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required"}}; use {artifact} in argv when required',
30143
- secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; clear with --unset secrets; prefer --secrets-file",
30268
+ 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",
30144
30269
  edgeDomains: "JSON {dev,rc,main} domain map",
30145
30270
  statusOptions: "JSON name\u2192id map",
30146
30271
  priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
@@ -30167,6 +30292,40 @@ function duplicateVarKeyAcrossFlags(vars, sets) {
30167
30292
  }
30168
30293
  return null;
30169
30294
  }
30295
+ function secretRemovalRefusal(existing, patch) {
30296
+ const remove2 = Array.isArray(patch.secretsRemove) ? patch.secretsRemove : [];
30297
+ if (remove2.length === 0) return null;
30298
+ const declared = new Set(Object.keys(existing?.secrets ?? {}));
30299
+ const runtime = patch.requiredRuntimeSecrets ?? existing?.requiredRuntimeSecrets ?? {};
30300
+ const build = patch.requiredBuildSecrets ?? existing?.requiredBuildSecrets ?? [];
30301
+ for (const mapKey of remove2) {
30302
+ if (!declared.has(mapKey)) {
30303
+ return `org project set: --unset-secret ${mapKey} is not declared in ${existing?.slug ?? "this project"}'s secrets catalog \u2014 nothing to remove`;
30304
+ }
30305
+ const at = mapKey.indexOf("@");
30306
+ const canonical = at === -1 ? mapKey : mapKey.slice(0, at);
30307
+ for (const [stage, names] of Object.entries(runtime)) {
30308
+ if (Array.isArray(names) && names.includes(canonical)) {
30309
+ return `org project set: ${canonical} is still required by stage ${stage} \u2014 drop it there first (--var requiredRuntimeSecrets=...)`;
30310
+ }
30311
+ }
30312
+ for (const raw of build) {
30313
+ if (typeof raw !== "string") continue;
30314
+ const eq = raw.indexOf("=");
30315
+ const ref = eq === -1 ? raw : raw.slice(eq + 1);
30316
+ if (ref === canonical) {
30317
+ return `org project set: ${canonical} is still required by the build secret contract \u2014 drop it there first (--var requiredBuildSecrets=...)`;
30318
+ }
30319
+ }
30320
+ }
30321
+ return null;
30322
+ }
30323
+ function clearSecretsConfirmationError(unsets, existing, confirmed) {
30324
+ if (!unsets.includes("secrets")) return null;
30325
+ const count = Object.keys(existing?.secrets ?? {}).length;
30326
+ if (confirmed) return null;
30327
+ return `org project set: --unset secrets clears the WHOLE catalog for ${existing?.slug ?? "this project"} (${count} declared entr${count === 1 ? "y" : "ies"} would be removed). To drop ONE entry use --unset-secret <KEY>; re-run with --confirm-clear-secrets to wipe all ${count}.`;
30328
+ }
30170
30329
  function buildProjectSetPatch(input) {
30171
30330
  const patch = {};
30172
30331
  if (input.class) {
@@ -30266,12 +30425,26 @@ function buildProjectSetPatch(input) {
30266
30425
  }
30267
30426
  patch[key] = null;
30268
30427
  }
30428
+ if (input.unsetSecrets?.length) {
30429
+ if (patch.secrets === null) {
30430
+ throw new Error("org project set: --unset secrets clears the WHOLE catalog and cannot be combined with --unset-secret \u2014 pass one or the other");
30431
+ }
30432
+ const keys = [];
30433
+ for (const raw of input.unsetSecrets) {
30434
+ const key = raw.trim();
30435
+ if (!SECRET_CATALOG_MAP_KEY_RE.test(key)) {
30436
+ throw new Error(`org project set: --unset-secret must be a catalog KEY (UPPER_SNAKE, optionally KEY@dev|rc|main) \u2014 got ${JSON.stringify(raw)}`);
30437
+ }
30438
+ if (!keys.includes(key)) keys.push(key);
30439
+ }
30440
+ patch.secretsRemove = keys;
30441
+ }
30269
30442
  if (input.clearWebProfile) {
30270
30443
  patch.oauth = null;
30271
30444
  patch.edgeDomains = null;
30272
30445
  }
30273
30446
  if (Object.keys(patch).length === 0) {
30274
- throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, and/or --clear-web-profile");
30447
+ throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, --unset-secret KEY, and/or --clear-web-profile");
30275
30448
  }
30276
30449
  return patch;
30277
30450
  }
@@ -44641,6 +44814,10 @@ function renderReleaseSupersedePublish(r) {
44641
44814
  const base = `mmi-cli devops release --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
44642
44815
  return r.gatewayDeploy ? `${base}; the deferred Jerv Gateway operator-host deploy leg was run and verified during the supersession (jerv-gateway=complete): ${r.gatewayDeploy.note}` : base;
44643
44816
  }
44817
+ function renderReleaseSupersedeDeployAndPublish(r) {
44818
+ const base = `mmi-cli devops release --supersede-deploy --supersede-publish --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
44819
+ return r.gatewayDeploy ? `${base}; the deferred Jerv Gateway operator-host deploy leg was run and verified during the closure (jerv-gateway=complete): ${r.gatewayDeploy.note}` : base;
44820
+ }
44644
44821
  function renderReleasePublishRetry(r) {
44645
44822
  return `mmi-cli devops release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
44646
44823
  }
@@ -44870,8 +45047,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44870
45047
  { flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
44871
45048
  { flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
44872
45049
  { flags: "--abort", description: "with --apply, delete only a proven unpublished failed release candidate on any registered deploy model and restore local main for a same-version recut (#3944/#5650/#5657)" },
44873
- { flags: "--supersede-deploy", description: "with --apply, master-approved closure of a content-caused failed deploy leg on a PUBLISHED release whose immutable tag can never deploy green \u2014 the next patch train then carries the fix (#6553)" },
44874
- { flags: "--supersede-publish", description: "with --apply, master-approved closure of a content-caused failed publish leg on a fully PROMOTED release whose immutable tag can never publish green \u2014 a deferred Jerv-Hub operator-host Gateway deploy leg is run and verified as part of the closure \u2014 the next patch train then carries the fix (#6638/#6742)" },
45050
+ { flags: "--supersede-deploy", description: "with --apply, master-approved closure of a content-caused failed deploy leg on a PUBLISHED release whose immutable tag can never deploy green \u2014 the next patch train then carries the fix (#6553); combined with --supersede-publish it closes BOTH content-caused legs in one approved closure (#6752)" },
45051
+ { flags: "--supersede-publish", description: "with --apply, master-approved closure of a content-caused failed publish leg on a fully PROMOTED release whose immutable tag can never publish green \u2014 a deferred Jerv-Hub operator-host Gateway deploy leg is run and verified as part of the closure \u2014 the next patch train then carries the fix (#6638/#6742); combined with --supersede-deploy it closes BOTH content-caused legs in one approved closure (#6752)" },
44875
45052
  { flags: "--retry-publish <run-id>", description: "with --apply, retry one proven failed or cancelled Hub publish release run (#3949/#5797)" }
44876
45053
  ];
44877
45054
  for (const f of RELEASE_ONLY_FLAGS) {
@@ -44911,6 +45088,25 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44911
45088
  if (o.out && !o.apply && !o.resume) {
44912
45089
  return fail(`${commandName}: --out writes a terminal result receipt (--apply, --resume, --abort, --retry-publish) \u2014 the dry-run plan keeps its stdout/JSON output. Drop --out or rerun with --apply`);
44913
45090
  }
45091
+ if (o.supersedeDeploy && o.supersedePublish) {
45092
+ if (o.resume) return fail("release: --supersede-deploy --supersede-publish and --resume are mutually exclusive \u2014 the closure settles the ledger so a NEW train can run, resume re-derives the very legs it closes");
45093
+ if (o.abort) return fail("release: --supersede-deploy --supersede-publish and --abort are mutually exclusive \u2014 the closure is for a PUBLISHED release, abort only ever deletes a proven unpublished candidate");
45094
+ if (o.retryPublish) return fail("release: --supersede-deploy --supersede-publish cannot be combined with --retry-publish \u2014 a content-caused failure on the immutable tag cannot be retried green");
45095
+ if (!o.apply) return fail("release: --supersede-deploy --supersede-publish requires --apply after explicit master approval; nothing was written");
45096
+ if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
45097
+ return fail("release: --supersede-deploy --supersede-publish accepts only --apply, --repo, --json and --out; it closes ledger legs and dispatches nothing");
45098
+ }
45099
+ if (o.repo) {
45100
+ const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), "mmi-cli devops release --supersede-deploy --supersede-publish --apply");
45101
+ if (!guard.ok) return fail(`release: ${guard.message}`);
45102
+ }
45103
+ try {
45104
+ const result = await runReleaseSupersedeDeployAndPublish(trainApplyDeps(), { approved: true });
45105
+ return emitTrainResult("release --supersede-deploy --supersede-publish", o.json ? JSON.stringify(result, null, 2) : renderReleaseSupersedeDeployAndPublish(result), o.out);
45106
+ } catch (e) {
45107
+ return failGraceful(`release --supersede-deploy --supersede-publish: ${e.message}`);
45108
+ }
45109
+ }
44914
45110
  if (o.retryPublish) {
44915
45111
  if (o.resume || o.abort) return fail("release: --retry-publish cannot be combined with --resume or --abort");
44916
45112
  if (!o.apply) return fail("release: --retry-publish requires --apply after explicit approval; nothing was written");
@@ -44952,9 +45148,6 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
44952
45148
  return failGraceful(`release --abort: ${e.message}`);
44953
45149
  }
44954
45150
  }
44955
- if (o.supersedeDeploy && o.supersedePublish) {
44956
- return fail("release: --supersede-deploy and --supersede-publish are mutually exclusive \u2014 close the one leg that failed content-caused; the other leg has its own resolution paths");
44957
- }
44958
45151
  if (o.supersedeDeploy) {
44959
45152
  if (o.resume) return fail("release: --supersede-deploy and --resume are mutually exclusive \u2014 the closure settles the ledger so a NEW train can run, resume re-derives the very leg it closes");
44960
45153
  if (o.abort) return fail("release: --supersede-deploy and --abort are mutually exclusive \u2014 the closure is for a PUBLISHED release, abort only ever deletes a proven unpublished candidate");
@@ -46052,7 +46245,7 @@ projectDeploy.command("doctor").description("read-only estate scan for duplicate
46052
46245
  if (report.error && report.status !== 403) return failGraceful(`org project deploy doctor: ${report.error}`);
46053
46246
  if (!report.ok && !report.error) process.exitCode = 1;
46054
46247
  });
46055
- 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 (clear all with --unset 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 <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, 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) => {
46248
+ 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("--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) => {
46056
46249
  const cfg = await loadConfig();
46057
46250
  let target;
46058
46251
  try {
@@ -46082,15 +46275,26 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
46082
46275
  releaseTrack: o.releaseTrack,
46083
46276
  vars,
46084
46277
  unsets: o.unset ?? [],
46278
+ unsetSecrets: o.unsetSecret ?? [],
46085
46279
  clearWebProfile: Boolean(o.clearWebProfile)
46086
46280
  });
46087
46281
  } catch (e) {
46088
46282
  return fail(e.message.replace(/^org project set: /, "org project set: "));
46089
46283
  }
46090
46284
  const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
46285
+ const clearRefusal = clearSecretsConfirmationError(o.unset ?? [], existing, Boolean(o.confirmClearSecrets));
46286
+ if (clearRefusal) return fail(clearRefusal);
46287
+ const removalRefusal = secretRemovalRefusal(existing, patch);
46288
+ if (removalRefusal) return fail(removalRefusal);
46091
46289
  const boardError = boardLinkWriteError(patch, existing);
46092
46290
  if (boardError) return fail(`org project set: ${boardError}`);
46093
46291
  const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
46292
+ const removedKeys = Array.isArray(patch.secretsRemove) ? patch.secretsRemove : [];
46293
+ if (removedKeys.length && res.ok) {
46294
+ const body = res.body;
46295
+ const remaining = Object.keys(body?.project?.secrets ?? {}).sort();
46296
+ printLine(`org project set: removed ${removedKeys.join(", ")} \u2014 catalog now declares ${remaining.length}: ${remaining.join(", ") || "(none)"}`);
46297
+ }
46094
46298
  return reportWrite("org project set", res);
46095
46299
  });
46096
46300
  project.command("retire [owner/repo]").description("retire an orphaned registry slug (master-only): soft-delete the PROJECT# META row + drop the repo's board items; secrets/vault left alone. DRY-RUN by default \u2014 pass --apply to actually delete; defaults to the current repo").option("--apply", "actually delete (default is a dry-run preview of what would be removed)").option("--skip-board", "retire the registry META only \u2014 leave board items in place").option("--json", "machine-readable {slug, removedMeta, removedBoardItem} output").action(async (repoOrSlug, o) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.4.1",
3
+ "version": "4.4.2",
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",