@mutmutco/cli 4.5.47 → 4.5.49

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 +75 -37
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -17043,10 +17043,10 @@ var rollout_plan_default = {
17043
17043
  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)."
17044
17044
  },
17045
17045
  baseline: {
17046
- version: "4.5.47",
17047
- tag: "v4.5.47",
17048
- commit: "d8722f771c7d",
17049
- npm: "@mutmutco/cli@4.5.47"
17046
+ version: "4.5.49",
17047
+ tag: "v4.5.49",
17048
+ commit: "b572aadbcfd3",
17049
+ npm: "@mutmutco/cli@4.5.49"
17050
17050
  },
17051
17051
  exitCriterion: "fleet-n-of-n",
17052
17052
  hubOnlyShortcut: "forbidden",
@@ -17063,14 +17063,14 @@ var rollout_plan_default = {
17063
17063
  repo: "mutmutco/mmi-hub",
17064
17064
  role: "canary",
17065
17065
  schedule: "train",
17066
- v3Target: "v4.5.47"
17066
+ v3Target: "v4.5.49"
17067
17067
  }
17068
17068
  ],
17069
17069
  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.",
17070
17070
  rollback: {
17071
17071
  independent: true,
17072
- mechanism: "npm dist-tag latest -> 4.5.47 and redeploy the Hub Lambda from tag v4.5.47 (d8722f771c7d); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17073
- v3Target: "v4.5.47 (@mutmutco/cli@4.5.47, tag commit d8722f771c7d \u2014 last known-good release carrying the repo-index v4-only contract)"
17072
+ mechanism: "npm dist-tag latest -> 4.5.49 and redeploy the Hub Lambda from tag v4.5.49 (b572aadbcfd3); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17073
+ v3Target: "v4.5.49 (@mutmutco/cli@4.5.49, tag commit b572aadbcfd3 \u2014 last known-good release carrying the repo-index v4-only contract)"
17074
17074
  }
17075
17075
  },
17076
17076
  {
@@ -35039,13 +35039,25 @@ var SCHEMA = "mmi-suite-cut/v1";
35039
35039
  var SHA2 = /^[a-f0-9]{40}$/i;
35040
35040
  var RC_TAG = /^v\d+\.\d+\.\d+-rc\.\d+$/;
35041
35041
  var RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
35042
- function suitePins(record) {
35042
+ function suitePins(record, bundles) {
35043
35043
  return record.members.map((m) => m.carried ? { module: m.id, rc: m.rcPin, main: m.mainPin } : {
35044
35044
  module: m.id,
35045
35045
  rc: `https://${m.id === "katip" ? m.rcHost ?? m.host : m.host}/${m.id === "katip" ? "" : "rc/"}module.js?v=${m.tag}`,
35046
- main: `https://${m.host}/module.js?v=${suiteReleaseTag(m.tag)}`
35046
+ main: mainPinFor(m.host, bundles[m.id], suiteReleaseTag(m.tag))
35047
35047
  });
35048
35048
  }
35049
+ function mainPinFor(host, bundles, tag) {
35050
+ const path2 = [bundles?.dev, bundles?.main].map(pathOf).find(Boolean) ?? "/module.js";
35051
+ return `https://${host}${path2}?v=${tag}`;
35052
+ }
35053
+ function pathOf(url) {
35054
+ try {
35055
+ return url ? new URL(url).pathname : void 0;
35056
+ } catch {
35057
+ return void 0;
35058
+ }
35059
+ }
35060
+ var bundlesOf = (members) => Object.fromEntries(members.map((m) => [m.id, m.bundles]));
35049
35061
  function suiteReleaseTag(rc) {
35050
35062
  if (!RC_TAG.test(rc)) throw new Error(`invalid rc tag ${rc}`);
35051
35063
  return rc.replace(/-rc\.\d+$/, "");
@@ -35091,19 +35103,24 @@ function pinVersion(url) {
35091
35103
  async function carriedPins(deps, member, bundles) {
35092
35104
  const main = bundles.main;
35093
35105
  if (!main || !RELEASE_TAG.test(pinVersion(main))) throw new Error(`${member.repo}: nothing new since rc, but its MM-Work main pin ${main ?? "(none)"} has no ?v=vX.Y.Z release; pin it before the suite cut`);
35106
+ if (!await deps.tagSha(member.repo, pinVersion(main))) throw new Error(`${member.repo}: nothing new since rc, but its MM-Work main pin names ${pinVersion(main)}, which was never released; release it or pin its live release before the suite cut`);
35107
+ const mainPin = mainPinFor(new URL(main).host, bundles, pinVersion(main));
35094
35108
  for (const url of [bundles.rc, main]) {
35095
35109
  const tag = pinVersion(url);
35096
35110
  if (!RC_TAG.test(tag) && !RELEASE_TAG.test(tag)) continue;
35097
35111
  const sha = await deps.tagSha(member.repo, tag);
35098
- if (sha && await deps.contains(member.repo, member.development, sha)) return { rcPin: url, mainPin: main };
35112
+ if (sha && await deps.contains(member.repo, member.development, sha)) return { rcPin: url === main ? mainPin : url, mainPin };
35099
35113
  }
35100
35114
  throw new Error(`${member.repo}: nothing new since rc, but no pinned MM-Work bundle carries development ${member.development.slice(0, 12)}; pin it before the suite cut`);
35101
35115
  }
35102
- function suiteAbandon(recordPath) {
35116
+ function suiteAbandon(recordPath, ownerGo = false) {
35103
35117
  const record = readSuiteCut(recordPath);
35104
35118
  if (!record) throw new Error("no suite cut record to abandon");
35105
- if (record.pinPr) {
35106
- throw new Error(`the cut already opened pin PR ${record.pinPr}; its pins may be visible, so resume it (suite rcand --owner-go) or release it (suite release) instead`);
35119
+ if (record.leadReleased || record.members.some((m) => m.released)) {
35120
+ throw new Error("the cut already released a member or the lead; finish it with suite release --owner-go, never abandon it");
35121
+ }
35122
+ if (record.pinPr && !ownerGo) {
35123
+ throw new Error(`the cut already opened pin PR ${record.pinPr}; its pins may be visible, so resume it (suite rcand --owner-go), release it (suite release), or retire it with suite abandon --owner-go`);
35107
35124
  }
35108
35125
  const archive = `${recordPath}.abandoned-${record.leadDevelopment.slice(0, 12)}.json`;
35109
35126
  if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`abandoned-cut archive already exists at ${archive}; inspect before abandoning again`);
@@ -35152,7 +35169,15 @@ async function suiteRcand(deps, opts) {
35152
35169
  throw new Error("suite roster changed since cut began; refuse to omit or add a member");
35153
35170
  const quiet = [];
35154
35171
  for (const member of record.members) {
35155
- if (!member.tag && !member.carried && await deps.contains(member.repo, member.development, member.previousRc)) quiet.push(member);
35172
+ if (member.tag || member.carried || !await deps.contains(member.repo, member.development, member.previousRc)) continue;
35173
+ const rcTag = pinVersion(modules.find((m) => m.repo === member.repo).bundles.rc);
35174
+ if (RC_TAG.test(rcTag) && (await deps.tagSha(member.repo, rcTag))?.toLowerCase() === member.previousRc.toLowerCase() && !await deps.tagSha(member.repo, suiteReleaseTag(rcTag))) {
35175
+ member.tag = rcTag;
35176
+ member.sha = member.previousRc;
35177
+ writeSuiteCut(deps.recordPath, record);
35178
+ continue;
35179
+ }
35180
+ quiet.push(member);
35156
35181
  }
35157
35182
  if (record.members.every((m) => m.carried || quiet.includes(m))) {
35158
35183
  (0, import_node_fs29.rmSync)(deps.recordPath, { force: true });
@@ -35164,7 +35189,7 @@ async function suiteRcand(deps, opts) {
35164
35189
  }
35165
35190
  const refusals = [];
35166
35191
  for (const member of record.members) {
35167
- if (member.carried || member.published || member.id === "katip") continue;
35192
+ if (member.published || member.id === "katip") continue;
35168
35193
  try {
35169
35194
  await deps.preflight(member.repo);
35170
35195
  } catch (e) {
@@ -35181,7 +35206,10 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35181
35206
  for (const member of record.members) {
35182
35207
  if ((await deps.head(member.repo, "development"))?.toLowerCase() !== member.development.toLowerCase())
35183
35208
  throw new Error(`${member.repo}: development moved after the cut snapshot; refuse to add untested code`);
35184
- if (member.carried) continue;
35209
+ if (member.carried) {
35210
+ if (member.id !== "katip") await deps.publish(member.repo, pinVersion(member.rcPin));
35211
+ continue;
35212
+ }
35185
35213
  if (!member.tag) {
35186
35214
  const cut = await deps.rcand(member.repo, member.previousRc, member.rcWasAbsent);
35187
35215
  if (!RC_TAG.test(cut.tag) || !SHA2.test(cut.sha) || cut.sha.toLowerCase() === member.previousRc.toLowerCase()) throw new Error(`${member.repo}: rcand did not return a new immutable rc tag/SHA`);
@@ -35205,7 +35233,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
35205
35233
  if (live.version !== member.backend || devHead?.toLowerCase() !== member.development.toLowerCase())
35206
35234
  throw new Error(`${member.repo}: live backend or development changed during the cut; pins unchanged`);
35207
35235
  }
35208
- const pins = suitePins(record);
35236
+ const pins = suitePins(record, bundlesOf(modules));
35209
35237
  await deps.verifyLeadCut(record.leadDevelopment);
35210
35238
  const pr = await deps.pin({ cut: record.leadDevelopment, pins });
35211
35239
  await deps.verifyLeadCut(record.leadDevelopment);
@@ -35230,7 +35258,7 @@ async function suiteRelease(deps, ownerGo) {
35230
35258
  if (!record?.pinMerged || !record.leadRcTag) throw new Error("no completed suite rc cut and merged pin PR; release refused");
35231
35259
  const members = suiteMembers(await deps.registry()).filter((m) => m.repo !== SUITE_REPO);
35232
35260
  if (members.length !== record.members.length || members.some((m) => !record.members.some((r) => r.repo === m.repo && r.id === m.id))) throw new Error("suite roster changed after the cut; release refused");
35233
- await deps.verifyPins(suitePins(record));
35261
+ await deps.verifyPins(suitePins(record, bundlesOf(members)), record.leadRcTag);
35234
35262
  for (const member of record.members) {
35235
35263
  if (member.carried) {
35236
35264
  const live2 = await deps.backend({ repo: member.repo, id: member.id, bundles: {} });
@@ -38586,8 +38614,8 @@ async function openSuiteCutPinPr(deps, input) {
38586
38614
  const ids = input.pins.map((p) => p.module);
38587
38615
  if (new Set(ids).size !== ids.length) throw new Error("suite pin PR contains a duplicate module");
38588
38616
  const branch = `suite-pin/cut-${input.cut.slice(0, 16)}`;
38589
- const registry2 = async () => {
38590
- const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`]));
38617
+ const registry2 = async (ref = "development") => {
38618
+ const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=${encodeURIComponent(ref)}`]));
38591
38619
  if (!get.content) throw new Error("MM-Work seed/modules.json is unreadable");
38592
38620
  return JSON.parse(Buffer.from(get.content, "base64").toString("utf8"));
38593
38621
  };
@@ -38611,25 +38639,35 @@ async function openSuiteCutPinPr(deps, input) {
38611
38639
  })) throw new Error(`${branch} merged but development pins differ from this cut`);
38612
38640
  return { status: "merged", pr: prs[0].url };
38613
38641
  }
38614
- if (prs.length) return { status: "pending", pr: prs[0].url };
38615
- if (input.pins.every((p) => {
38616
- const row = rows.find((r) => r.id === p.module);
38642
+ const matches = (r) => input.pins.every((p) => {
38643
+ const row = r.find((x) => x.id === p.module);
38617
38644
  return row?.bundles.rc === p.rc && row?.bundles.main === p.main;
38618
- })) throw new Error("pins already match without a merged cut PR \u2014 provenance unverified");
38645
+ });
38646
+ if (prs.length && matches(await registry2(branch))) return { status: "pending", pr: prs[0].url };
38647
+ if (!prs.length && matches(rows)) throw new Error("pins already match without a merged cut PR \u2014 provenance unverified");
38619
38648
  await (0, import_promises5.mkdir)((0, import_node_path34.join)(deps.root, "tmp"), { recursive: true });
38620
38649
  scratch = await (0, import_promises5.mkdtemp)((0, import_node_path34.join)(deps.root, "tmp", "suite-pin-"));
38621
38650
  const run = (file, args) => deps.run(file, args, scratch);
38622
- await run("git", ["clone", "--quiet", "--branch", "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
38623
- if ((await run("git", ["rev-parse", "HEAD"])).trim().toLowerCase() !== input.cut.toLowerCase())
38624
- throw new Error("MM-Work development moved after the suite snapshot; no pin PR opened");
38625
- await run("git", ["switch", "-c", branch]);
38651
+ await run("git", ["clone", "--quiet", "--branch", prs.length ? branch : "development", "--single-branch", `https://github.com/${SUITE_REPO}.git`, "."]);
38652
+ if (prs.length) {
38653
+ if ((await run("git", ["merge-base", "HEAD", `${input.cut}`]).catch(() => "")).trim().toLowerCase() !== input.cut.toLowerCase())
38654
+ throw new Error(`${branch} no longer builds on the cut ${input.cut.slice(0, 12)}; pin PR left unchanged`);
38655
+ } else {
38656
+ if ((await run("git", ["rev-parse", "HEAD"])).trim().toLowerCase() !== input.cut.toLowerCase())
38657
+ throw new Error("MM-Work development moved after the suite snapshot; no pin PR opened");
38658
+ await run("git", ["switch", "-c", branch]);
38659
+ }
38626
38660
  for (const stage of ["rc", "main"]) {
38627
38661
  const path2 = (0, import_node_path34.join)(scratch, `${stage}-pins.json`);
38628
38662
  await (0, import_promises5.writeFile)(path2, JSON.stringify(input.pins.map((p) => ({ module: p.module, url: p[stage] }))));
38629
38663
  await run("node", ["scripts/pin-modules.mjs", "--stage", stage, "--pins", path2]);
38630
38664
  }
38631
38665
  await run("git", ["add", "seed/modules.json"]);
38632
- await run("git", ["commit", "-m", `Pin MM-Work suite candidate ${input.cut.slice(0, 12)} (#7262)`]);
38666
+ await run("git", ["commit", "-m", prs.length ? `Correct MM-Work suite candidate pins ${input.cut.slice(0, 12)} (#7450)` : `Pin MM-Work suite candidate ${input.cut.slice(0, 12)} (#7262)`]);
38667
+ if (prs.length) {
38668
+ await run("git", ["push", "origin", branch]);
38669
+ return { status: "pending", pr: prs[0].url };
38670
+ }
38633
38671
  await run("git", ["push", "origin", branch]);
38634
38672
  const pr = (await run("gh", [
38635
38673
  "pr",
@@ -45303,8 +45341,8 @@ function suiteTrainIo(root, summaryFile) {
45303
45341
  return "";
45304
45342
  }
45305
45343
  };
45306
- const registry2 = async () => {
45307
- const file = await defaultGitHubClient().rest("GET", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`);
45344
+ const registry2 = async (ref = "development") => {
45345
+ const file = await defaultGitHubClient().rest("GET", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=${encodeURIComponent(ref)}`);
45308
45346
  if (!file?.content) throw new Error("MM-Work registry unreadable");
45309
45347
  const parsed = JSON.parse(decodeGitHubContents(file.content));
45310
45348
  suiteMembers(parsed);
@@ -45344,12 +45382,12 @@ function suiteTrainIo(root, summaryFile) {
45344
45382
  if (delta.ahead_by !== 1 || delta.behind_by !== 0 || delta.files?.length !== 1 || delta.files[0] !== "seed/modules.json")
45345
45383
  throw new Error("MM-Work development changed outside the one suite pin commit after the cut snapshot; refuse to cut an untested lead");
45346
45384
  },
45347
- verifyPins: async (pins) => {
45348
- const rows = await registry2();
45385
+ verifyPins: async (pins, ref) => {
45386
+ const rows = await registry2(ref);
45349
45387
  for (const pin of pins) {
45350
45388
  const row = rows.find((r) => r.id === pin.module);
45351
45389
  if (row?.bundles.rc !== pin.rc || row?.bundles.main !== pin.main)
45352
- throw new Error(`${pin.module}: MM-Work development pins no longer equal the tested cut; release refused`);
45390
+ throw new Error(`${pin.module}: MM-Work ${ref} pins do not equal the tested cut (${row?.bundles.main ?? "no main pin"} vs ${pin.main}); release refused`);
45353
45391
  }
45354
45392
  },
45355
45393
  release,
@@ -45392,19 +45430,19 @@ function registerSuiteCommands(program3) {
45392
45430
  return fail(`suite rcand: ${e.message}`);
45393
45431
  }
45394
45432
  });
45395
- suite.command("abandon").description("retire an unfinished cut that opened no pin PR: archive the record; member rc tags stay (#7396)").option("--json", "structured receipt").action(async (o) => {
45433
+ suite.command("abandon").description("retire an unfinished cut: archive the record; member rc tags stay (#7396). A cut whose pin PR landed but released nothing needs --owner-go (#7450)").option("--owner-go", "explicit owner approval to retire a cut whose pin PR already landed").option("--json", "structured receipt").action(async (o) => {
45396
45434
  try {
45397
45435
  const root = await leadRoot();
45398
45436
  const unlock = await acquireTrainWriterLock(root, "suite abandon");
45399
45437
  let result;
45400
45438
  try {
45401
- result = suiteAbandon(suiteTrainIo(root).recordPath);
45439
+ result = suiteAbandon(suiteTrainIo(root).recordPath, o.ownerGo === true);
45402
45440
  } finally {
45403
45441
  await unlock();
45404
45442
  }
45405
45443
  console.log(o.json ? JSON.stringify(result, null, 2) : [
45406
45444
  `suite abandon: archived the cut record to ${result.archive}`,
45407
- ...result.orphanTags.length ? [" member rc tags left in place (immutable; the next rc supersedes them):", ...result.orphanTags.map((t) => ` - ${t}`)] : [" no member rc tag was minted"],
45445
+ ...result.orphanTags.length ? [" member rc tags left in place (immutable; the next cut reuses one still unreleased on its rc head, #7450):", ...result.orphanTags.map((t) => ` - ${t}`)] : [" no member rc tag was minted"],
45408
45446
  " the next suite rcand --owner-go re-snapshots every member"
45409
45447
  ].join("\n"));
45410
45448
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.47",
3
+ "version": "4.5.49",
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",