@mutmutco/cli 4.5.102 → 4.5.104

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 +201 -61
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -6053,6 +6053,16 @@ function isRetryableGitHubWaitReadError(e) {
6053
6053
  }
6054
6054
  return /Something went wrong while executing your query/.test(text) || /^\s*unexpected end of JSON input\s*$/m.test(text);
6055
6055
  }
6056
+ function refusalWaitMs(e, nowMs) {
6057
+ const err = e;
6058
+ const text = [err?.stdout, err?.stderr, e instanceof Error ? e.message : ""].filter((t) => typeof t === "string").join("\n");
6059
+ const header = (name) => new RegExp(`^${name}:\\s*(\\d+)\\s*$`, "im").exec(text)?.[1];
6060
+ const retryAfter = header("retry-after");
6061
+ if (retryAfter !== void 0) return Number(retryAfter) * 1e3;
6062
+ const reset = header("x-ratelimit-remaining") === "0" ? Number(header("x-ratelimit-reset")) : typeof err?.rateLimitReset === "number" ? err.rateLimitReset : NaN;
6063
+ if (reset * 1e3 > nowMs) return reset * 1e3 - nowMs;
6064
+ return /secondary rate|abuse detection/i.test(text) ? 6e4 : void 0;
6065
+ }
6056
6066
  function waitReadFailureMessage(e) {
6057
6067
  const err = e;
6058
6068
  const stderr = typeof err?.stderr === "string" ? err.stderr.trim() : "";
@@ -6180,9 +6190,21 @@ async function waitForPrChecks(deps) {
6180
6190
  return { policy, status: "failure", reason: waitReadFailureMessage(e), detail: "github-read-failed", waitedMs: now() - started };
6181
6191
  }
6182
6192
  lastDetail = `github-read-transient (${waitReadFailureMessage(e)})`;
6183
- deps.log?.(`transient GitHub read failure \u2014 retrying within the wait budget: ${lastDetail}`);
6184
- report("pending");
6185
- await deps.sleep(PR_CHECKS_POLL_MS);
6193
+ const askedMs = refusalWaitMs(e, now());
6194
+ const jitter = deps.jitterMs ?? (() => Math.floor(Math.random() * PR_CHECKS_RATELIMIT_JITTER_MAX_MS));
6195
+ const waitMs = askedMs !== void 0 && askedMs > PR_CHECKS_POLL_MS ? askedMs + jitter() : PR_CHECKS_POLL_MS;
6196
+ if (askedMs !== void 0 && now() + waitMs >= deadline) {
6197
+ return {
6198
+ policy,
6199
+ status: "rate-limited",
6200
+ reason: `GitHub refused the read for rate limit and asked for ${Math.round(askedMs / 1e3)}s, past the wait deadline (${lastDetail})`,
6201
+ detail: "rate-limited",
6202
+ waitedMs: now() - started
6203
+ };
6204
+ }
6205
+ deps.log?.(`transient GitHub read failure \u2014 retrying in ${Math.round(waitMs / 1e3)}s within the wait budget: ${lastDetail}`);
6206
+ report(askedMs !== void 0 ? "rate-limited" : "pending");
6207
+ await deps.sleep(waitMs);
6186
6208
  continue;
6187
6209
  }
6188
6210
  report(state);
@@ -6300,6 +6322,7 @@ function setPollTokenProvider(provider) {
6300
6322
  pollTokenOnce = void 0;
6301
6323
  resetAppPollGuard();
6302
6324
  lastPollSource = void 0;
6325
+ observedPool.clear();
6303
6326
  }
6304
6327
  async function pollToken() {
6305
6328
  if (!pollTokenProvider) return void 0;
@@ -6351,6 +6374,19 @@ function appPoolDetail() {
6351
6374
  const counts = guard.remaining !== void 0 && guard.limit !== void 0 ? `App pool at ${guard.remaining}/${guard.limit}` : "App pool headroom unverified";
6352
6375
  return guard.resetAtMs !== void 0 ? `${counts}, resets ${new Date(guard.resetAtMs).toISOString()}` : counts;
6353
6376
  }
6377
+ var observedPool = /* @__PURE__ */ new Map();
6378
+ var OBSERVED_POOL_MAX_AGE_MS = 3 * 6e4;
6379
+ function notePoolHeaders(source, out, nowMs) {
6380
+ const { remaining, resetSec } = parseRateLimitHeaders(out);
6381
+ const resource = /^x-ratelimit-resource:\s*(\S+)\s*$/im.exec(out)?.[1];
6382
+ if (remaining !== void 0 && resetSec !== void 0 && (resource ?? "core") === "core") {
6383
+ observedPool.set(source, { remaining, reset: resetSec, atMs: nowMs });
6384
+ }
6385
+ }
6386
+ function observedCorePool(source, nowMs) {
6387
+ const seen = source ? observedPool.get(source) : void 0;
6388
+ return seen && nowMs - seen.atMs <= OBSERVED_POOL_MAX_AGE_MS && seen.reset * 1e3 > nowMs ? { remaining: seen.remaining, reset: seen.reset } : void 0;
6389
+ }
6354
6390
  async function defaultGhApi(args) {
6355
6391
  const appToken = await pollToken();
6356
6392
  let source = appToken && appPollAllowsApp(Date.now()) ? "app" : "user";
@@ -6372,6 +6408,7 @@ async function defaultGhApi(args) {
6372
6408
  const out = e.stdout;
6373
6409
  const errOut = typeof out === "string" ? out : void 0;
6374
6410
  const status = errOut !== void 0 ? parseIncludedOutput(errOut).status : void 0;
6411
+ if (errOut !== void 0) notePoolHeaders(source, errOut, Date.now());
6375
6412
  if (cached && status === 304) {
6376
6413
  lastPollSource = source;
6377
6414
  return cached.body;
@@ -6386,6 +6423,7 @@ async function defaultGhApi(args) {
6386
6423
  throw e;
6387
6424
  }
6388
6425
  const { body, etag, pages } = parseIncludedOutput(stdout);
6426
+ notePoolHeaders(source, stdout, Date.now());
6389
6427
  if (source === "app") noteAppPollResponse(parseRateLimitHeaders(stdout), false, Date.now());
6390
6428
  lastPollSource = source;
6391
6429
  if (etag && pages === 1) etagCache.set(key, { etag, body });
@@ -6858,7 +6896,9 @@ async function fetchRequiredCheckContexts(repo, branch, gh2 = defaultGhApi) {
6858
6896
  }
6859
6897
  return contexts;
6860
6898
  }
6861
- async function fetchRestCorePool(gh2 = defaultGhApi) {
6899
+ async function fetchRestCorePool(gh2 = defaultGhApi, nowMs = Date.now()) {
6900
+ const seen = gh2 === defaultGhApi ? observedCorePool(await pollToken() && appPollAllowsApp(nowMs) ? "app" : "user", nowMs) : void 0;
6901
+ if (seen) return { state: "ok", ...seen };
6862
6902
  let core;
6863
6903
  try {
6864
6904
  core = JSON.parse(await gh2(["rate_limit"])).resources?.core;
@@ -14868,7 +14908,7 @@ async function readDefaultBranchGate(repo, client, baseBranch, now = Date.now(),
14868
14908
  async function gateIsProvenGreen(repo, client, baseBranch, gateFiles = DEFAULT_GATE_FILES) {
14869
14909
  return (await readDefaultBranchGate(repo, client, baseBranch, Date.now(), gateFiles)).state === "green";
14870
14910
  }
14871
- async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active") {
14911
+ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active", dryRun = false) {
14872
14912
  const want = new Set(rulesetRequiredContexts({ rules: rulesetBody.rules }));
14873
14913
  const wantStrict = rulesetStrictPolicy({ rules: rulesetBody.rules });
14874
14914
  const wantBranches = rulesetBranchIncludes(rulesetBody);
@@ -14887,10 +14927,10 @@ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "
14887
14927
  if (detail.enforcement === effective && branchesMatch && strictMatch && have.size === want.size && [...want].every((c) => have.has(c))) {
14888
14928
  return { action: "skipped", enforcement: effective, detail: `${effective} ruleset already matches required contexts, branches and up-to-date policy${kept}` };
14889
14929
  }
14890
- await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: { ...rulesetBody, enforcement: effective }, timeoutMs: 2e4 });
14930
+ if (!dryRun) await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: { ...rulesetBody, enforcement: effective }, timeoutMs: 2e4 });
14891
14931
  return { action: "updated", enforcement: effective, detail: `ruleset ${existing.id}${kept}` };
14892
14932
  }
14893
- await client.rest("POST", `repos/${repo}/rulesets`, { body: { ...rulesetBody, enforcement }, timeoutMs: 2e4 });
14933
+ if (!dryRun) await client.rest("POST", `repos/${repo}/rulesets`, { body: { ...rulesetBody, enforcement }, timeoutMs: 2e4 });
14894
14934
  return { action: "created", enforcement };
14895
14935
  }
14896
14936
  async function gateContextEnforced(repo, rest) {
@@ -16126,6 +16166,39 @@ function gateConfigToVars(gate) {
16126
16166
  if (extras.steps?.length) out.GATE_EXTRA_STEPS_YAML = gateExtraStepsYaml(extras.steps);
16127
16167
  return out;
16128
16168
  }
16169
+ function suiteModuleSeeds(seeds, suite, deployModel) {
16170
+ if (suite !== "mm-work" || deployModel !== "tenant-container") return seeds;
16171
+ const replacements = {
16172
+ Dockerfile: "suite-Dockerfile.template",
16173
+ "docker-compose.yml": "suite-compose.template.yml",
16174
+ ".github/workflows/gate.yml": "suite-gate.template.yml"
16175
+ };
16176
+ const additions = {
16177
+ "src/index.ts": "suite-server.template.ts",
16178
+ "package.json": "suite-package.template.json",
16179
+ "package-lock.json": "suite-package-lock.template.json",
16180
+ "tsconfig.json": "suite-tsconfig.template.json",
16181
+ "module/package.json": "suite-module-package.template.json",
16182
+ "module/package-lock.json": "suite-module-package-lock.template.json",
16183
+ "module/.npmrc": "suite-npmrc.template",
16184
+ "module/src/module.jsx": "suite-module.template.jsx",
16185
+ "module/vite.config.mjs": "suite-vite.template.mjs",
16186
+ "module/screens/screens.json": "suite-screens.template.json",
16187
+ "module/screens/files.json": "suite-files.template.json",
16188
+ ".github/workflows/screens.yml": "suite-screens.template.yml",
16189
+ "docs/Guides/mm-work-module.md": "suite-guide.template.md"
16190
+ };
16191
+ return [
16192
+ ...seeds.filter((s) => s.target !== "docs/Guides/google-login.md").map((s) => replacements[s.target] ? { ...s, source: `seed:${replacements[s.target]}` } : s),
16193
+ ...Object.entries(additions).map(([target, source]) => ({
16194
+ target,
16195
+ source: `seed:${source}`,
16196
+ ownership: "repo",
16197
+ classes: ["deployable"],
16198
+ deployModels: ["tenant-container"]
16199
+ }))
16200
+ ];
16201
+ }
16129
16202
  function seedMatchesDeployModel(seed, deployModel) {
16130
16203
  if (!seed.deployModels?.length) return true;
16131
16204
  return deployModel != null && seed.deployModels.includes(deployModel);
@@ -16307,7 +16380,11 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
16307
16380
  // Identity. name/division default off the repo name when the skill didn't pass them.
16308
16381
  name: vars.NAME || parsedRepo.name,
16309
16382
  division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
16310
- suite: vars.SUITE || void 0,
16383
+ suite,
16384
+ ...suite === "mm-work" && deployModel === "tenant-container" ? {
16385
+ requiredBuildSecrets: ["NODE_AUTH_TOKEN=@github-packages-token"],
16386
+ requiredChecks: options.existing?.requiredChecks ?? ["gate"]
16387
+ } : {},
16311
16388
  repos: [`${parsedRepo.owner}/${slug}`],
16312
16389
  branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
16313
16390
  class: cls,
@@ -17622,10 +17699,10 @@ var rollout_plan_default = {
17622
17699
  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)."
17623
17700
  },
17624
17701
  baseline: {
17625
- version: "4.5.102",
17626
- tag: "v4.5.102",
17627
- commit: "69ad9c05771f",
17628
- npm: "@mutmutco/cli@4.5.102"
17702
+ version: "4.5.104",
17703
+ tag: "v4.5.104",
17704
+ commit: "6bf765dc986c",
17705
+ npm: "@mutmutco/cli@4.5.104"
17629
17706
  },
17630
17707
  exitCriterion: "fleet-n-of-n",
17631
17708
  hubOnlyShortcut: "forbidden",
@@ -17642,14 +17719,14 @@ var rollout_plan_default = {
17642
17719
  repo: "mutmutco/mmi-hub",
17643
17720
  role: "canary",
17644
17721
  schedule: "train",
17645
- v3Target: "v4.5.102"
17722
+ v3Target: "v4.5.104"
17646
17723
  }
17647
17724
  ],
17648
17725
  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.",
17649
17726
  rollback: {
17650
17727
  independent: true,
17651
- mechanism: "npm dist-tag latest -> 4.5.102 and redeploy the Hub Lambda from tag v4.5.102 (69ad9c05771f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17652
- v3Target: "v4.5.102 (@mutmutco/cli@4.5.102, tag commit 69ad9c05771f \u2014 last known-good release carrying the repo-index v4-only contract)"
17728
+ mechanism: "npm dist-tag latest -> 4.5.104 and redeploy the Hub Lambda from tag v4.5.104 (6bf765dc986c); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17729
+ v3Target: "v4.5.104 (@mutmutco/cli@4.5.104, tag commit 6bf765dc986c \u2014 last known-good release carrying the repo-index v4-only contract)"
17653
17730
  }
17654
17731
  },
17655
17732
  {
@@ -28267,7 +28344,7 @@ function planBranchRequiredContexts(input) {
28267
28344
  })).filter((entry) => entry.contexts.length > 0);
28268
28345
  return { common, pending, waiting };
28269
28346
  }
28270
- async function branchContextPlan(deps, repo, authority, meta) {
28347
+ async function branchContextPlan(deps, repo, authority, meta, emittedBranchesOnly = false) {
28271
28348
  const branches = resolveRequiredCheckBranches(meta, repo);
28272
28349
  const emittedByBranch = {};
28273
28350
  const unreadable = [];
@@ -28277,7 +28354,10 @@ async function branchContextPlan(deps, repo, authority, meta) {
28277
28354
  emittedByBranch[branch] = await resolveEmittedPrContexts(deps, repo, branch);
28278
28355
  if (presence === void 0 || emittedByBranch[branch] === void 0) unreadable.push(branch);
28279
28356
  }
28280
- return { ...planBranchRequiredContexts({ contexts: authority.contexts, branches, emittedByBranch }), unreadable };
28357
+ const bindingBranches = emittedBranchesOnly ? branches.filter((branch) => authority.contexts.some((context) => emittedByBranch[branch]?.includes(context))) : branches;
28358
+ const plan = planBranchRequiredContexts({ contexts: authority.contexts, branches: bindingBranches, emittedByBranch });
28359
+ const waiting = planBranchRequiredContexts({ contexts: authority.contexts, branches, emittedByBranch }).waiting;
28360
+ return { ...plan, waiting, unreadable, bindingBranches };
28281
28361
  }
28282
28362
  function projectCommonAuthority(authority, common) {
28283
28363
  const apiPayload = patchRulesetRequiredContexts(authority.apiPayload, common);
@@ -28531,8 +28611,8 @@ async function auditRepoCi(repo, callerDeps) {
28531
28611
  checks.push({
28532
28612
  ok: pendingAligned,
28533
28613
  label: "branch-specific required checks match workflow emitters",
28534
- detail: pendingAligned ? void 0 : "pending ruleset contexts or branch conditions have not converged to the emitted registry requirements",
28535
- remediation: pendingAligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
28614
+ detail: pendingAligned ? void 0 : `pending ruleset contexts or branch conditions have not converged to the emitted registry requirements \u2014 run mmi-cli devops bootstrap apply ${repo} --only product-ruleset --execute`,
28615
+ remediation: pendingAligned ? void 0 : `mmi-cli devops bootstrap apply ${repo} --only product-ruleset --execute`
28536
28616
  });
28537
28617
  }
28538
28618
  }
@@ -28564,7 +28644,7 @@ async function auditRepoCi(repo, callerDeps) {
28564
28644
  checks.push({
28565
28645
  ok: aligned,
28566
28646
  label: RULESET_REFERENCE_MATCH_LABEL,
28567
- detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}], branches [${authoritativeRuleset.branchIncludes.join(", ")}] and the up-to-date policy match declared authority` : `${authoritativeRuleset.source} requires contexts [${authoritativeRuleset.contexts.join(", ")}]; ${authoritativeRuleset.branchSource} requires branches [${authoritativeRuleset.branchIncludes.join(", ")}]; ${PRODUCT_RULESET_REF} declares contexts [${authoritativeRuleset.committedContexts.join(", ")}] / branches [${authoritativeRuleset.committedBranchIncludes.join(", ")}] and live ${PRODUCT_RULESET_NAME} has contexts [${liveContexts.join(", ")}] / branches [${liveBranchIncludes.join(", ")}]` + (strictAligned ? "" : `; strict_required_status_checks_policy differs from committed policy \u2014 ${PRODUCT_RULESET_REF} declares ${authoritativeRuleset.committedStrict}, live ${PRODUCT_RULESET_NAME} has ${liveStrict}`),
28647
+ detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}], branches [${authoritativeRuleset.branchIncludes.join(", ")}] and the up-to-date policy match declared authority` : `${authoritativeRuleset.source} requires contexts [${authoritativeRuleset.contexts.join(", ")}]; ${authoritativeRuleset.branchSource} requires branches [${authoritativeRuleset.branchIncludes.join(", ")}]; ${PRODUCT_RULESET_REF} declares contexts [${authoritativeRuleset.committedContexts.join(", ")}] / branches [${authoritativeRuleset.committedBranchIncludes.join(", ")}] and live ${PRODUCT_RULESET_NAME} has contexts [${liveContexts.join(", ")}] / branches [${liveBranchIncludes.join(", ")}]` + (strictAligned ? "" : `; strict_required_status_checks_policy differs from committed policy \u2014 ${PRODUCT_RULESET_REF} declares ${authoritativeRuleset.committedStrict}, live ${PRODUCT_RULESET_NAME} has ${liveStrict}`) + ` \u2014 live repair: mmi-cli devops bootstrap apply ${repo} --only product-ruleset --execute`,
28568
28648
  remediation: aligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
28569
28649
  });
28570
28650
  }
@@ -29107,9 +29187,14 @@ async function authoritativeSafetyHold(authority, deps, repo, baseBranch, meta)
29107
29187
  }
29108
29188
  return null;
29109
29189
  }
29110
- async function applyCiReconcileRepo(repo, deps) {
29190
+ async function applyCiReconcileRepo(repo, deps, options = {}) {
29191
+ if (options.dryRun && !options.productRulesetOnly) throw new Error("dry-run requires productRulesetOnly");
29111
29192
  const meta = await deps.getProjectMeta(slugFromRepo(repo));
29193
+ if (options.productRulesetOnly && !meta?.requiredChecks?.length) {
29194
+ return { repo, applied: [], skipped: [], errors: ["product-ruleset requires registry META requiredChecks"] };
29195
+ }
29112
29196
  const report = await auditRepoCi(repo, deps);
29197
+ const finish = (result2, pendingReason) => options.productRulesetOnly ? Promise.resolve(result2) : finalizeCiReconcile(repo, deps, result2, report, pendingReason);
29113
29198
  const baseBranch = "development";
29114
29199
  let raw = report.class === "deployable" && !report.explicitNoCi ? await fetchRulesetSeedBody(deps, repo) : null;
29115
29200
  let authority = null;
@@ -29119,12 +29204,12 @@ async function applyCiReconcileRepo(repo, deps) {
29119
29204
  authority = parseAuthoritativeRuleset(raw, meta, repo);
29120
29205
  } catch (e) {
29121
29206
  empty.errors.push(`${PRODUCT_RULESET_REF} is not parseable: ${e.message}`);
29122
- return finalizeCiReconcile(repo, deps, empty, report);
29207
+ return finish(empty);
29123
29208
  }
29124
29209
  const hold = await authoritativeSafetyHold(authority, deps, repo, baseBranch, meta);
29125
29210
  if (hold) {
29126
29211
  empty.skipped.push(hold);
29127
- return finalizeCiReconcile(repo, deps, empty, report, hold);
29212
+ return finish(empty, hold);
29128
29213
  }
29129
29214
  } else if (report.class === "deployable" && !report.explicitNoCi) {
29130
29215
  const registryAuthority = registryAuthorityWithoutReference(meta, repo);
@@ -29132,59 +29217,64 @@ async function applyCiReconcileRepo(repo, deps) {
29132
29217
  const hold = await authoritativeSafetyHold(registryAuthority, deps, repo, baseBranch, meta);
29133
29218
  if (hold) {
29134
29219
  empty.skipped.push(hold);
29135
- return finalizeCiReconcile(repo, deps, empty, report, hold);
29220
+ return finish(empty, hold);
29136
29221
  }
29137
29222
  }
29138
29223
  }
29139
- const result = await applyCiReconcileMergeSettingsFromReport(repo, deps, report);
29224
+ const result = options.productRulesetOnly ? empty : await applyCiReconcileMergeSettingsFromReport(repo, deps, report);
29140
29225
  if (report.class !== "deployable" || report.explicitNoCi) {
29141
- return finalizeCiReconcile(repo, deps, result, report);
29226
+ return finish(result);
29142
29227
  }
29143
- const refDelivery = await seedGateYml(repo, deps, meta, result);
29228
+ const refDelivery = options.productRulesetOnly ? void 0 : await seedGateYml(repo, deps, meta, result);
29144
29229
  if (refDelivery === "pending") {
29145
29230
  const reason = `${PRODUCT_RULESET_REF} is pending on its protected-base source PR; live ruleset convergence waits for that source to merge`;
29146
29231
  result.skipped.push(reason);
29147
- return finalizeCiReconcile(repo, deps, result, report, reason);
29232
+ return finish(result, reason);
29148
29233
  }
29149
29234
  raw ??= await fetchRulesetSeedBody(deps, repo);
29150
29235
  if (!raw) {
29151
29236
  result.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
29152
- return finalizeCiReconcile(repo, deps, result, report);
29237
+ return finish(result);
29153
29238
  }
29154
29239
  if (authority == null) {
29155
29240
  try {
29156
29241
  authority = parseAuthoritativeRuleset(raw, meta, repo);
29157
29242
  } catch (e) {
29158
29243
  result.errors.push(`${PRODUCT_RULESET_REF} is not parseable: ${e.message}`);
29159
- return finalizeCiReconcile(repo, deps, result, report);
29244
+ return finish(result);
29160
29245
  }
29161
29246
  const hold = await authoritativeSafetyHold(authority, deps, repo, baseBranch, meta);
29162
29247
  if (hold) {
29163
29248
  result.skipped.push(hold);
29164
- return finalizeCiReconcile(repo, deps, result, report, hold);
29249
+ return finish(result, hold);
29165
29250
  }
29166
29251
  }
29167
29252
  if (authority.contexts.length === 0) {
29168
29253
  result.errors.push(`${authority.source} declares no required contexts`);
29169
- return finalizeCiReconcile(repo, deps, result, report);
29254
+ return finish(result);
29170
29255
  }
29171
29256
  let pendingPlan = null;
29172
29257
  const declaredContexts = authority.contexts;
29173
29258
  if (authority.source === "registry META requiredChecks") {
29174
- pendingPlan = await branchContextPlan(deps, repo, authority, meta);
29259
+ pendingPlan = await branchContextPlan(deps, repo, authority, meta, options.productRulesetOnly);
29175
29260
  if (pendingPlan.unreadable.length) {
29176
29261
  const reason = `required branch workflow emitters unreadable on [${pendingPlan.unreadable.join(", ")}] \u2014 ruleset convergence deferred`;
29177
29262
  result.skipped.push(reason);
29178
- return finalizeCiReconcile(repo, deps, result, report, reason);
29263
+ return finish(result, reason);
29179
29264
  }
29180
29265
  if (pendingPlan.common.length === 0) {
29181
29266
  result.errors.push("no common required context is emitted by every required branch \u2014 ruleset convergence deferred");
29182
- return finalizeCiReconcile(repo, deps, result, report);
29267
+ return finish(result);
29183
29268
  }
29184
29269
  authority = projectCommonAuthority(authority, pendingPlan.common);
29270
+ if (options.productRulesetOnly) {
29271
+ const apiPayload = patchRulesetBranchIncludes(authority.apiPayload, pendingPlan.bindingBranches);
29272
+ authority = { ...authority, apiPayload, branchIncludes: rulesetBranchIncludes(apiPayload) };
29273
+ for (const entry of pendingPlan.waiting) result.skipped.push(`${entry.branch} waits for workflow emitter(s) [${entry.contexts.join(", ")}]`);
29274
+ }
29185
29275
  }
29186
29276
  const sourceNeedsConvergence = !sameContexts(authority.committedContexts, authority.contexts) || !sameContexts(authority.committedBranchIncludes, authority.branchIncludes) || !authority.committedStrict;
29187
- if (sourceNeedsConvergence) {
29277
+ if (sourceNeedsConvergence && !options.productRulesetOnly) {
29188
29278
  try {
29189
29279
  const delivery = await deliverSeedFile(
29190
29280
  deps,
@@ -29201,15 +29291,15 @@ async function applyCiReconcileRepo(repo, deps) {
29201
29291
  `${PRODUCT_RULESET_REF} authority \u2192 contexts [${authority.contexts.join(", ")}], branches [${authority.branchIncludes.join(", ")}] delivered on ${delivery.plan.branch} (${delivery.plan.reason})${delivery.prUrl ? `: ${delivery.prUrl}` : ""}`
29202
29292
  );
29203
29293
  const reason = `${PRODUCT_RULESET_REF} authority is pending on its protected-base source PR; live ruleset convergence waits for that source to merge`;
29204
- return finalizeCiReconcile(repo, deps, result, report, reason);
29294
+ return finish(result, reason);
29205
29295
  } else {
29206
29296
  result.applied.push(`reconciled ${PRODUCT_RULESET_REF} authority \u2192 contexts [${authority.contexts.join(", ")}], branches [${authority.branchIncludes.join(", ")}]`);
29207
29297
  }
29208
29298
  } catch (e) {
29209
29299
  result.errors.push(`ruleset reference delivery failed: ${e.message}`);
29210
- return finalizeCiReconcile(repo, deps, result, report);
29300
+ return finish(result);
29211
29301
  }
29212
- } else {
29302
+ } else if (!options.productRulesetOnly) {
29213
29303
  result.skipped.push(`${PRODUCT_RULESET_REF} already matches declared context and branch authority`);
29214
29304
  }
29215
29305
  let live;
@@ -29217,7 +29307,7 @@ async function applyCiReconcileRepo(repo, deps) {
29217
29307
  live = await readLiveProductRuleset(deps, repo);
29218
29308
  } catch (e) {
29219
29309
  result.errors.push(`cannot read the live ${PRODUCT_RULESET_NAME} ruleset: ${e.message}`);
29220
- return finalizeCiReconcile(repo, deps, result, report);
29310
+ return finish(result);
29221
29311
  }
29222
29312
  const liveContexts = live == null ? [] : sortedUnique(rulesetRequiredContexts(live));
29223
29313
  const liveBranchIncludes = live == null ? [] : rulesetBranchIncludes(live);
@@ -29235,7 +29325,7 @@ async function applyCiReconcileRepo(repo, deps) {
29235
29325
  if (pendingPlan?.pending.some((entry) => entry.contexts.some((context) => unsafe.includes(context)))) {
29236
29326
  const reason = `pending required-check rulesets left non-enforcing \u2014 [${unsafe.join(", ")}] emitted only by path-filtered workflows`;
29237
29327
  result.skipped.push(reason);
29238
- return finalizeCiReconcile(repo, deps, result, report, reason);
29328
+ return finish(result, reason);
29239
29329
  }
29240
29330
  const gateProvenGreen = await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles);
29241
29331
  const plan = resolveProductRulesetReconcilePlan({
@@ -29252,25 +29342,25 @@ async function applyCiReconcileRepo(repo, deps) {
29252
29342
  if (!plan.shouldActivate) {
29253
29343
  if (plan.holdReason) {
29254
29344
  result.skipped.push(plan.holdReason);
29255
- return finalizeCiReconcile(repo, deps, result, report, plan.holdReason);
29345
+ return finish(result, plan.holdReason);
29256
29346
  }
29257
29347
  result.skipped.push(`live ${PRODUCT_RULESET_NAME} contexts, branches and up-to-date policy already match declared authority`);
29258
29348
  }
29259
29349
  if (plan.shouldActivate) {
29260
29350
  try {
29261
- const activation = await activateProductRuleset(repo, authority.apiPayload, deps.client, plan.targetEnforcement);
29351
+ const activation = await activateProductRuleset(repo, authority.apiPayload, deps.client, plan.targetEnforcement, options.dryRun);
29262
29352
  if (activation.action === "skipped") result.skipped.push(activation.detail ?? "product ruleset");
29263
- else result.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
29353
+ else result.applied.push(`product ruleset ${activation.action} (${activation.enforcement}; contexts [${authority.contexts.join(", ")}], branches [${authority.branchIncludes.join(", ")}])${activation.detail ? `: ${activation.detail}` : ""}`);
29264
29354
  } catch (e) {
29265
29355
  result.errors.push(e.message);
29266
- return finalizeCiReconcile(repo, deps, result, report);
29356
+ return finish(result);
29267
29357
  }
29268
29358
  }
29269
29359
  if (pendingPlan) {
29270
29360
  if (pendingPlan.pending.length > 0 && !gateProvenGreen) {
29271
29361
  const reason = "pending required-check rulesets left non-enforcing \u2014 the gate is not green on development";
29272
29362
  result.skipped.push(reason);
29273
- return finalizeCiReconcile(repo, deps, result, report, reason);
29363
+ return finish(result, reason);
29274
29364
  }
29275
29365
  try {
29276
29366
  const desired = /* @__PURE__ */ new Set();
@@ -29286,20 +29376,20 @@ async function applyCiReconcileRepo(repo, deps) {
29286
29376
  patchRulesetRequiredContexts(statusOnly, entry.contexts),
29287
29377
  [entry.branch]
29288
29378
  );
29289
- const activation = await activateProductRuleset(repo, payload, deps.client);
29290
- (activation.action === "skipped" ? result.skipped : result.applied).push(`${name}: ${activation.action}`);
29379
+ const activation = await activateProductRuleset(repo, payload, deps.client, "active", options.dryRun);
29380
+ (activation.action === "skipped" ? result.skipped : result.applied).push(`${name}: ${activation.action} \u2014 contexts [${entry.contexts.join(", ")}]`);
29291
29381
  }
29292
29382
  const list = await deps.client.rest("GET", `repos/${repo}/rulesets`);
29293
29383
  for (const stale of list ?? []) {
29294
29384
  if (!stale.name?.startsWith(PRODUCT_PENDING_RULESET_PREFIX) || desired.has(stale.name) || stale.id == null) continue;
29295
- await deps.client.rest("DELETE", `repos/${repo}/rulesets/${stale.id}`);
29385
+ if (!options.dryRun) await deps.client.rest("DELETE", `repos/${repo}/rulesets/${stale.id}`);
29296
29386
  result.applied.push(`deleted converged ${stale.name}`);
29297
29387
  }
29298
29388
  } catch (e) {
29299
29389
  result.errors.push(`pending ruleset convergence failed: ${e.message}`);
29300
29390
  }
29301
29391
  }
29302
- return finalizeCiReconcile(repo, deps, result, report);
29392
+ return finish(result);
29303
29393
  }
29304
29394
 
29305
29395
  // src/command-composition.ts
@@ -33937,14 +34027,15 @@ function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead, suit
33937
34027
  };
33938
34028
  });
33939
34029
  }
33940
- function edgeCertificateChecks(results) {
34030
+ function edgeCertificateChecks(results, repo) {
33941
34031
  return results.flatMap((result) => {
33942
34032
  if (!result) return [];
33943
34033
  const { stage, status, error } = result;
34034
+ const fix = status?.missing && !status.pending ? ` \u2014 run: mmi-cli devops runtime edge cert ensure ${repo} ${stage}` : "";
33944
34035
  return [{
33945
34036
  ok: !error && (status?.required === false || status?.required === true && status.missing === false),
33946
34037
  label: `Cloudflare edge certificate: ${stage}`,
33947
- detail: error ? `certificate coverage unreadable (${error}); cannot verify TLS` : status?.hint
34038
+ detail: error ? `certificate coverage unreadable (${error}); cannot verify TLS` : status?.hint && `${status.hint}${fix}`
33948
34039
  }];
33949
34040
  });
33950
34041
  }
@@ -34008,6 +34099,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
34008
34099
  detail: exactAllowlist ? void 0 : ownersUnverified ? "org owner baseline is unreadable or empty; the exact restriction is unverified" : deps.pushAllowlistOwnersRead === true ? "restriction must contain exactly the org owners + mmi-github-app and no teams" : "restrictions.users is empty or unset"
34009
34100
  });
34010
34101
  }
34102
+ const packageAccess = suiteModulePackageAccessCheck(deps.projectMeta?.suite, deps.deployModel);
34103
+ if (packageAccess) checks.push(packageAccess);
34011
34104
  let environmentDrift2 = [];
34012
34105
  try {
34013
34106
  environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client, deps.projectMeta?.suite);
@@ -34091,7 +34184,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
34091
34184
  const check = npmrcGitHubPackagesAuthCheck(path2, await contentText(deps, repo, baseBranch, path2));
34092
34185
  if (check) checks.push(check);
34093
34186
  }
34094
- checks.push(...edgeCertificateChecks(deps.edgeCertificates ?? []));
34187
+ checks.push(...edgeCertificateChecks(deps.edgeCertificates ?? [], repo));
34095
34188
  checks.push(...edgeDnsChecks(deps.edgeDns ?? [], repo));
34096
34189
  const portRangeCheck = centralContainerPortRangeCheck(deps.deployModel, deps.projectMeta?.portRange, repo);
34097
34190
  if (portRangeCheck) checks.push(portRangeCheck);
@@ -34349,6 +34442,14 @@ function laneNotEvaluatedFromThisBranch(doctor) {
34349
34442
  const only = reasons.length === 1 ? reasons[0] : void 0;
34350
34443
  return only?.code === "branch-mismatch" ? only.title : void 0;
34351
34444
  }
34445
+ function suiteModulePackageAccessCheck(suite, deployModel) {
34446
+ if (suite !== "mm-work" || deployModel !== "tenant-container") return void 0;
34447
+ return {
34448
+ ok: true,
34449
+ label: "mmwork-ds + mmwork-screens package access (manual org-admin step; unverified)",
34450
+ detail: `An org admin must add this repository with Read under BOTH @mutmutco/mmwork-ds and @mutmutco/mmwork-screens \u2192 Package settings \u2192 Manage Actions access (#7984). No GitHub REST API can verify or grant this; without it the image build or the screens job fails with 403 read_package. Once screens is green, require it: mmi-cli oracle org project set <owner/repo> --var 'requiredChecks=["gate","screens / screens"]'.`
34451
+ };
34452
+ }
34352
34453
  function attachTrainDoctorVerdicts(report, trainDoctors) {
34353
34454
  const marked = trainDoctors.map((doctor) => {
34354
34455
  const notEvaluatedFromThisBranch = laneNotEvaluatedFromThisBranch(doctor);
@@ -34742,7 +34843,7 @@ function registerBootstrapCommands(program3) {
34742
34843
  }
34743
34844
  if (findings.some((f) => f.state !== "waived")) process.exitCode = 1;
34744
34845
  });
34745
- bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").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)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--box <name>", "resolve this live box and plan free registry DEPLOY# ports (container deploy models)").option("--domains <stages>", "optional comma-separated dev=host,rc=host,main=host deploy domains (requires --box)").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes; or the special target environments (#5035) to run the deployment-environment reconcile alone (dry-run classifies, --execute writes)").option("--from-config", "with --only gate.yml: adopt an equal legacy gate with a render stamp, then update from registry config via review PR; refuse hand edits").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
34846
+ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").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)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--box <name>", "resolve this live box and plan free registry DEPLOY# ports (container deploy models)").option("--domains <stages>", "optional comma-separated dev=host,rc=host,main=host deploy domains (requires --box)").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes; or the special targets environments (#5035) / product-ruleset (#7986) to reconcile only that live surface (dry-run classifies, --execute writes)").option("--from-config", "with --only gate.yml: adopt an equal legacy gate with a render stamp, then update from registry config via review PR; refuse hand edits").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
34746
34847
  const o = {
34747
34848
  class: rawValue("--class", "deployable"),
34748
34849
  projectType: rawValue("--project-type", ""),
@@ -34829,6 +34930,26 @@ function registerBootstrapCommands(program3) {
34829
34930
  }
34830
34931
  return;
34831
34932
  }
34933
+ if (o.only.trim() === "product-ruleset") {
34934
+ if (cmdOpts.var?.length) return fail("bootstrap apply: --only product-ruleset accepts no --var overrides; set registry META instead");
34935
+ const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
34936
+ if (!read.ok) return failGraceful(`bootstrap apply: registry META unreadable (${read.error}); refusing product ruleset reconciliation`);
34937
+ if (!read.project?.requiredChecks?.length) return failGraceful("bootstrap apply: --only product-ruleset requires registry META requiredChecks");
34938
+ const result = await applyCiReconcileRepo(repo, {
34939
+ client: defaultGitHubClient(),
34940
+ listProjects: async () => [],
34941
+ getProjectMeta: async () => read.project
34942
+ }, { productRulesetOnly: true, dryRun: !o.execute });
34943
+ if (o.json) console.log(JSON.stringify(result, null, 2));
34944
+ else {
34945
+ console.log(`bootstrap apply --only product-ruleset \u2014 ${o.execute ? "LIVE" : "dry-run; no writes"} (${repo}):`);
34946
+ result.applied.forEach((line) => console.log(` ${o.execute ? "APPLY" : "PLAN"} ${line}`));
34947
+ result.skipped.forEach((line) => console.log(` SKIP ${line}`));
34948
+ result.errors.forEach((line) => console.error(` REFUSED ${line}`));
34949
+ }
34950
+ if (result.errors.length) return failGraceful("bootstrap apply: product ruleset reconciliation failed");
34951
+ return;
34952
+ }
34832
34953
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
34833
34954
  if (!(0, import_node_fs28.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
34834
34955
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
@@ -34837,13 +34958,6 @@ function registerBootstrapCommands(program3) {
34837
34958
  const baseBranch = o.class === "content" ? "main" : "development";
34838
34959
  const slug = parsedRepo.slug;
34839
34960
  const onlyTarget = o.fromConfig && o.only.trim() === "gate.yml" ? ".github/workflows/gate.yml" : o.only.trim();
34840
- const seedsToApply = onlyTarget ? manifest.seeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : manifest.seeds;
34841
- if (onlyTarget && !seedsToApply.length) {
34842
- const known = manifest.seeds.map((s) => s.target).join("\n ");
34843
- return fail(`bootstrap apply: --only '${onlyTarget}' names no seed in ${manifestPath}. Declared targets:
34844
- ${known}`);
34845
- }
34846
- const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
34847
34961
  const gh2 = async (args) => execFileP("gh", args, { timeout: 2e4 });
34848
34962
  const readFile10 = (p) => (0, import_node_fs28.existsSync)(p) ? (0, import_node_fs28.readFileSync)(p, "utf8") : null;
34849
34963
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
@@ -34913,6 +35027,21 @@ function registerBootstrapCommands(program3) {
34913
35027
  applyProjectType = o.projectType || registryMeta?.projectType || payload.projectType;
34914
35028
  } catch {
34915
35029
  }
35030
+ const availableSeeds = suiteModuleSeeds(manifest.seeds, vars.SUITE || registryMeta?.suite, applyDeployModel);
35031
+ const seedsToApply = onlyTarget ? availableSeeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : availableSeeds;
35032
+ if (onlyTarget && !seedsToApply.length) {
35033
+ return fail(`bootstrap apply: --only '${onlyTarget}' names no seed for this project. Declared targets:
35034
+ ${availableSeeds.map((s) => s.target).join("\n ")}`);
35035
+ }
35036
+ const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
35037
+ if (vars.SUITE === "mm-work" || registryMeta?.suite === "mm-work") {
35038
+ if (applyDeployModel === "tenant-container") {
35039
+ const requiredChecks = registryMeta?.requiredChecks ?? ["gate"];
35040
+ const moduleGate = withDerivedRepoVars({}, parsedRepo, o.class, effectiveTrack, registryRequiredCheckBranches, requiredChecks);
35041
+ vars.GATE_RULESET_CONTEXTS_JSON = moduleGate.GATE_RULESET_CONTEXTS_JSON;
35042
+ if (registryMeta) registryMeta = { ...registryMeta, requiredChecks };
35043
+ }
35044
+ }
34916
35045
  const controlClient = defaultGitHubClient();
34917
35046
  let controlOwners = [];
34918
35047
  let controlDeployFacts = null;
@@ -53292,6 +53421,17 @@ function registerEdgeCommands(program3) {
53292
53421
  fail(`runtime edge dns: ${e.message}`);
53293
53422
  }
53294
53423
  });
53424
+ edge.command("cert <action> <owner/repo> <stage>").description("read (`status`) or order-when-missing (`ensure`, master-only) the Cloudflare advanced edge certificate pack (<x> + *.<x>) a dev/rc host needs").action(async (action, repo, stage) => {
53425
+ if (action !== "status" && action !== "ensure") return fail("runtime edge cert: <action> must be status or ensure");
53426
+ if (stage !== "dev" && stage !== "rc") return fail("runtime edge cert: <stage> must be dev or rc (main is covered by the zone certificate)");
53427
+ try {
53428
+ const result = await edgeCertificateForStage(slugOf(repo), stage, registryClientDeps(await loadConfig()), action === "ensure");
53429
+ console.log(JSON.stringify(result, null, 2));
53430
+ if (result.required && result.missing) process.exitCode = 1;
53431
+ } catch (e) {
53432
+ fail(`runtime edge cert: ${e.message}`);
53433
+ }
53434
+ });
53295
53435
  }
53296
53436
 
53297
53437
  // src/command-register-developer.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.102",
3
+ "version": "4.5.104",
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",