@mutmutco/cli 3.6.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +121 -153
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5897,7 +5897,7 @@ function parseGhPrChecksOutput(stdout) {
5897
5897
  if (!lines.length) return "no-checks-reported";
5898
5898
  let anyPending = false;
5899
5899
  for (const line of lines) {
5900
- const parts = line.split(/\s+/);
5900
+ const parts = line.includes(" ") ? line.split(" ") : line.split(/\s+/);
5901
5901
  const state = (parts[1] ?? "").toLowerCase();
5902
5902
  if (state === "fail" || state === "failure" || state === "error") return "failure";
5903
5903
  if (state === "pass" || state === "success" || state === "skipping" || state === "skipped") continue;
@@ -5916,22 +5916,46 @@ var PR_CHECKS_POLL_MS = 3e4;
5916
5916
  var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
5917
5917
  var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
5918
5918
  var PR_CHECKS_CONFIRM_MS = 5e3;
5919
+ var NO_CI_LIVE_CHECKS_CONTRADICTION_REASON = "ci:none contradicted by live check runs on PR head";
5920
+ function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none") {
5921
+ if (checks === "no-checks-reported") return { action: "proceed" };
5922
+ const staleNote = `registry says no-ci (${policyReason}), but live PR checks exist`;
5923
+ if (checks === "success") {
5924
+ return { action: "proceed", note: `${staleNote}; proceeding because they are green` };
5925
+ }
5926
+ return {
5927
+ action: "refuse",
5928
+ message: [
5929
+ `${staleNote} and are not green (${checks}).`,
5930
+ "Fix the checks or use `mmi-cli pr land` so the CLI waits.",
5931
+ "Then correct registry META: `mmi-cli project set <owner/repo> --var requiredChecks=<check-run-display-names>`."
5932
+ ].join(" ")
5933
+ };
5934
+ }
5919
5935
  async function waitForPrChecks(deps) {
5920
- const { policy, reason } = await deps.resolvePolicy();
5936
+ let { policy, reason } = await deps.resolvePolicy();
5937
+ const queuedStates = [];
5921
5938
  if (policy === "no-ci") {
5922
- return { policy, status: "skipped", reason };
5939
+ const firstState = await deps.pollChecks();
5940
+ if (firstState === "no-checks-reported") {
5941
+ return { policy, status: "skipped", reason };
5942
+ }
5943
+ reason = NO_CI_LIVE_CHECKS_CONTRADICTION_REASON;
5944
+ deps.log?.(`merge CI policy contradiction: ${reason}; waiting for live PR checks`);
5945
+ policy = "wait-for-checks";
5946
+ queuedStates.push(firstState);
5923
5947
  }
5924
5948
  const now = deps.now ?? (() => Date.now());
5925
5949
  const deadline = now() + PR_CHECKS_TIMEOUT_MS;
5926
5950
  let lastDetail = "pending";
5927
5951
  let successStreak = 0;
5928
5952
  while (now() < deadline) {
5929
- const state = await deps.pollChecks();
5930
- if (state === "failure") return { policy, status: "failure", detail: lastDetail };
5953
+ const state = queuedStates.shift() ?? await deps.pollChecks();
5954
+ if (state === "failure") return { policy, status: "failure", reason, detail: lastDetail };
5931
5955
  if (state === "success") {
5932
5956
  successStreak += 1;
5933
5957
  if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
5934
- return { policy, status: "success", detail: "checks-success" };
5958
+ return { policy, status: "success", reason, detail: "checks-success" };
5935
5959
  }
5936
5960
  lastDetail = "confirming-success";
5937
5961
  await deps.sleep(PR_CHECKS_CONFIRM_MS);
@@ -5946,7 +5970,7 @@ async function waitForPrChecks(deps) {
5946
5970
  lastDetail = state;
5947
5971
  await deps.sleep(PR_CHECKS_POLL_MS);
5948
5972
  }
5949
- return { policy, status: "timeout", detail: lastDetail };
5973
+ return { policy, status: "timeout", reason, detail: lastDetail };
5950
5974
  }
5951
5975
 
5952
5976
  // src/bootstrap-ruleset.ts
@@ -6736,6 +6760,22 @@ async function auditRepoCi(repo, deps) {
6736
6760
  detail: info.delete_branch_on_merge === true ? void 0 : "false or unavailable"
6737
6761
  });
6738
6762
  const hasGateWorkflow = repoClass === "hub" ? true : repoClass === "content" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
6763
+ let emittedPrContexts;
6764
+ const getEmittedPrContexts = async () => {
6765
+ emittedPrContexts ??= await resolveEmittedPrContexts(deps, repo, baseBranch);
6766
+ return emittedPrContexts;
6767
+ };
6768
+ if (explicitNoCi) {
6769
+ const emitted = await getEmittedPrContexts();
6770
+ if (emitted.length > 0) {
6771
+ checks.push({
6772
+ ok: false,
6773
+ label: "registry no-ci contradicts PR workflows",
6774
+ detail: `registry META declares no-ci but pull_request workflows emit [${emitted.join(", ")}]`,
6775
+ remediation: `Fix the workflows or correct registry META: mmi-cli project set ${repo} --var requiredChecks=${JSON.stringify(emitted)}`
6776
+ });
6777
+ }
6778
+ }
6739
6779
  if (deployableGated) {
6740
6780
  checks.push({
6741
6781
  ok: hasGateWorkflow,
@@ -6770,7 +6810,7 @@ async function auditRepoCi(repo, deps) {
6770
6810
  remediation: hasRequiredChecks ? void 0 : `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)`
6771
6811
  });
6772
6812
  if (hasGateWorkflow) {
6773
- const emitted = registryRequiredContexts(meta) ?? await resolveEmittedPrContexts(deps, repo, baseBranch);
6813
+ const emitted = registryRequiredContexts(meta) ?? await getEmittedPrContexts();
6774
6814
  if (emitted.length > 0) {
6775
6815
  const aligned = contextsMatchRuleset(statusChecks, new Set(emitted));
6776
6816
  checks.push({
@@ -9230,7 +9270,7 @@ function parseNpmVersion(stdout) {
9230
9270
  return /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v) ? v : void 0;
9231
9271
  }
9232
9272
  function staleTrainCliMessage(report, commandName) {
9233
- return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`mmi-cli doctor --apply --no-repo-writes\` to update to ${report.releasedVersion}, then rerun ${commandName} so it uses the current train path`;
9273
+ return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`mmi-cli doctor --apply --no-repo-writes\` to update to ${report.releasedVersion}, then rerun ${commandName} so it uses the current train path. This gate runs before any train mutation, so this invocation changed nothing; any partial train state (local merge, pushed tag) is from an earlier run and the rerun resumes it safely`;
9234
9274
  }
9235
9275
  async function resolveReleasedVersion(runners) {
9236
9276
  try {
@@ -9998,7 +10038,7 @@ var defaultSleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
9998
10038
  function resolveSleep(deps) {
9999
10039
  return deps.sleep ?? defaultSleep2;
10000
10040
  }
10001
- var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion}]";
10041
+ var TRAIN_CHECK_RUNS_JQ = "[.check_runs[]|{name:.name,status:.status,conclusion:.conclusion,startedAt:.started_at}]";
10002
10042
  var TRAIN_COMMIT_STATUS_JQ = "[.statuses[]|{context:.context,state:.state}]";
10003
10043
  var TRAIN_PROTECTION_CONTEXTS_JQ = "[.contexts[]]";
10004
10044
  var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]';
@@ -10006,6 +10046,7 @@ var TRAIN_CHECK_ATTEMPTS = 40;
10006
10046
  var TRAIN_CHECK_DELAY_MS = 15e3;
10007
10047
  var TRAIN_PR_ONLY_AUTOMATION_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done"]);
10008
10048
  var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
10049
+ var TRAIN_FRESH_RUN_GRACE_ATTEMPTS = 4;
10009
10050
  async function correlateRun(deps, args) {
10010
10051
  const sleep = resolveSleep(deps);
10011
10052
  const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
@@ -10063,12 +10104,19 @@ async function correlateWorkflowRun(deps, args) {
10063
10104
  }
10064
10105
  async function watchTenantRun(deps, runId, repo = HUB_REPO3) {
10065
10106
  if (runId == null) return "pending";
10107
+ let watchSaidSuccess = true;
10066
10108
  try {
10067
10109
  await deps.run("gh", ["run", "watch", String(runId), "--repo", repo, "--exit-status"]);
10068
- return "success";
10069
10110
  } catch {
10070
- return "failure";
10111
+ watchSaidSuccess = false;
10112
+ }
10113
+ try {
10114
+ const out = await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--json", "status,conclusion"]);
10115
+ const parsed = JSON.parse(out);
10116
+ if (parsed.status === "completed") return parsed.conclusion === "success" ? "success" : "failure";
10117
+ } catch {
10071
10118
  }
10119
+ return watchSaidSuccess ? "success" : "failure";
10072
10120
  }
10073
10121
  async function fetchControlRunLog(deps, runId) {
10074
10122
  try {
@@ -10180,10 +10228,16 @@ function resolveContextState(context, checkRuns, statuses) {
10180
10228
  if (sawSuccess) return "success";
10181
10229
  return sawFailure ? "failed" : "pending";
10182
10230
  }
10183
- async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
10231
+ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince) {
10184
10232
  if (required.length === 0) {
10185
10233
  return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
10186
10234
  }
10235
+ const freshThreshold = freshSince !== void 0 ? freshSince - CORRELATE_SKEW_SLACK_MS : void 0;
10236
+ const isFreshRun = (r) => {
10237
+ if (freshThreshold === void 0) return true;
10238
+ const t = Date.parse(String(r.startedAt ?? ""));
10239
+ return !Number.isFinite(t) || t >= freshThreshold;
10240
+ };
10187
10241
  const sleep = resolveSleep(deps);
10188
10242
  let lastStatus = "not checked";
10189
10243
  let lastError;
@@ -10216,7 +10270,14 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required) {
10216
10270
  }
10217
10271
  }
10218
10272
  const pending = required.filter((c) => !autoSatisfied.has(c));
10219
- const states = pending.map((c) => [c, resolveContextState(c, checkRuns, statuses)]);
10273
+ const holdStale = freshThreshold !== void 0 && attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS;
10274
+ const states = pending.map((c) => {
10275
+ if (holdStale) {
10276
+ const runsFor = checkRuns.filter((r) => r.name === c);
10277
+ if (runsFor.length > 0 && !runsFor.some(isFreshRun)) return [c, "pending"];
10278
+ }
10279
+ return [c, resolveContextState(c, checkRuns, statuses)];
10280
+ });
10220
10281
  const satisfiedNote = autoSatisfied.size ? `, auto-satisfied (never runs on a tag, see #2404): ${[...autoSatisfied].join(", ")}` : "";
10221
10282
  lastStatus = `${states.map(([c, s]) => `${c}=${s}`).join(", ")}${satisfiedNote}`;
10222
10283
  const failed = states.filter(([, s]) => s === "failed").map(([c]) => c);
@@ -10268,14 +10329,14 @@ async function ensureTagPushed(deps, tag, sha) {
10268
10329
  if (localSha && localSha !== sha) {
10269
10330
  throw new Error(`local tag ${tag} points at ${localSha} but origin has it at ${sha} \u2014 delete the stale local tag (git tag -d ${tag}) and rerun`);
10270
10331
  }
10271
- return `tag ${tag} already on origin at ${sha.slice(0, 7)} \u2014 resumed without re-pushing`;
10332
+ return { note: `tag ${tag} already on origin at ${sha.slice(0, 7)} \u2014 resumed without re-pushing`, pushed: false };
10272
10333
  }
10273
10334
  if (localSha && localSha !== sha) {
10274
10335
  throw new Error(`local tag ${tag} already points at ${localSha}, not the intended ${sha} \u2014 delete the stale local tag (git tag -d ${tag}) and rerun`);
10275
10336
  }
10276
10337
  if (!localSha) await deps.run("git", ["tag", tag, sha]);
10277
10338
  await deps.run("git", ["push", "origin", tag]);
10278
- return `tag ${tag} pushed at ${sha.slice(0, 7)}`;
10339
+ return { note: `tag ${tag} pushed at ${sha.slice(0, 7)}`, pushed: true };
10279
10340
  }
10280
10341
  async function resolveRcResumeTag(deps, base, sha) {
10281
10342
  const out = await deps.run("git", ["ls-remote", "--tags", "origin", `refs/tags/${base}-rc.*`]);
@@ -10581,11 +10642,12 @@ async function runFoldStage(deps, startBranch, preFold, fn) {
10581
10642
  }
10582
10643
  }
10583
10644
  async function completeMainRelease(deps, ctx, meta, deployModel, watch, options, tag, releaseSha) {
10584
- await ensureTagPushed(deps, tag, releaseSha);
10645
+ const tagPush = await ensureTagPushed(deps, tag, releaseSha);
10646
+ const tagPushSince = (deps.now ?? Date.now)();
10585
10647
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
10586
10648
  let checks;
10587
10649
  try {
10588
- checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
10650
+ checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
10589
10651
  } catch (e) {
10590
10652
  throw partialTrainRecoveryError(e, { repo: ctx.repo, tag, stage: "main" });
10591
10653
  }
@@ -10637,11 +10699,12 @@ async function runTrainApplyPipeline(mode, input) {
10637
10699
  const resume = await resolveRcResumeTag(deps, releaseBase, rcSha);
10638
10700
  const tag2 = resume.tag ?? requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "rc"])), "rc tag");
10639
10701
  const resumeNote = resume.tag ? resume.note : void 0;
10640
- await ensureTagPushed(deps, tag2, rcSha);
10702
+ const tagPush = await ensureTagPushed(deps, tag2, rcSha);
10703
+ const tagPushSince = (deps.now ?? Date.now)();
10641
10704
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "rc");
10642
10705
  let checks2;
10643
10706
  try {
10644
- checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks);
10707
+ checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks, tagPush.pushed ? tagPushSince : void 0);
10645
10708
  } catch (e) {
10646
10709
  throw partialTrainRecoveryError(e, { repo: ctx.repo, tag: tag2, stage: "rc" });
10647
10710
  }
@@ -11343,9 +11406,11 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
11343
11406
  await deps.run("git", ["merge-base", "--is-ancestor", mergedSha, "origin/main"]).catch(() => {
11344
11407
  throw new Error(`merged hotfix SHA ${mergedSha.slice(0, 7)} is not on origin/main \u2014 refusing to tag`);
11345
11408
  });
11346
- const tagNote = await ensureTagPushed(deps, tag, mergedSha);
11409
+ const tagPush = await ensureTagPushed(deps, tag, mergedSha);
11410
+ const tagPushSince = Date.now();
11411
+ const tagNote = tagPush.note;
11347
11412
  const required = await discoverRequiredCheckContexts(deps, ctx, "main");
11348
- const checks = await waitForRequiredTrainChecks(deps, ctx, mergedSha, required);
11413
+ const checks = await waitForRequiredTrainChecks(deps, ctx, mergedSha, required, tagPush.pushed ? tagPushSince : void 0);
11349
11414
  let releaseNote;
11350
11415
  let releaseExists = false;
11351
11416
  try {
@@ -13365,11 +13430,12 @@ function authorizeBodyHasMismatch(body) {
13365
13430
  }
13366
13431
 
13367
13432
  // src/project-set.ts
13368
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
13433
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
13369
13434
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
13370
13435
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
13371
- var SECRET_CONSUMERS = ["runtime", "lambda", "actions", "agent", "box"];
13436
+ var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
13372
13437
  var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
13438
+ var BUILD_SECRET_REF_RE = /^(?:[A-Z_][A-Z0-9_]*=)?(?:[A-Z_][A-Z0-9_]*|(?:mm-fofu|_org\/[a-z0-9][a-z0-9-]*):[A-Z_][A-Z0-9_]*|@github-packages-token)$/;
13373
13439
  function parseSecretsCatalogVar(raw) {
13374
13440
  let parsed;
13375
13441
  try {
@@ -13433,6 +13499,21 @@ function parseRuntimeSecretsVar(raw) {
13433
13499
  }
13434
13500
  return out;
13435
13501
  }
13502
+ function parseBuildSecretsVar(raw) {
13503
+ let parsed;
13504
+ try {
13505
+ parsed = JSON.parse(raw);
13506
+ } catch {
13507
+ throw new Error('project set: requiredBuildSecrets must be JSON, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]');
13508
+ }
13509
+ if (!Array.isArray(parsed)) {
13510
+ throw new Error("project set: requiredBuildSecrets must be a flat array (build secrets are not stage-mapped)");
13511
+ }
13512
+ if (parsed.some((n) => typeof n !== "string" || !BUILD_SECRET_REF_RE.test(n) || n === "@github-packages-token")) {
13513
+ throw new Error("project set: requiredBuildSecrets must be a flat array of ENVID=<ref> or bare <ref> strings");
13514
+ }
13515
+ return parsed;
13516
+ }
13436
13517
  function parseEdgeDomainsVar(raw) {
13437
13518
  let parsed;
13438
13519
  try {
@@ -13657,6 +13738,7 @@ var SETTABLE_VAR_KEYS = [
13657
13738
  "consumesDesignSystem",
13658
13739
  "requiredGcpApis",
13659
13740
  "requiredRuntimeSecrets",
13741
+ "requiredBuildSecrets",
13660
13742
  "edgeDomains",
13661
13743
  "statusFieldId",
13662
13744
  "statusOptions",
@@ -13680,7 +13762,8 @@ var SETTABLE_VAR_HINTS = {
13680
13762
  oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
13681
13763
  requiredGcpApis: "comma-string",
13682
13764
  requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
13683
- secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 prefer --secrets-file",
13765
+ requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
13766
+ secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; clear with --unset secrets; prefer --secrets-file",
13684
13767
  edgeDomains: "JSON {dev,rc,main} domain map",
13685
13768
  statusOptions: "JSON name\u2192id map",
13686
13769
  priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
@@ -13736,6 +13819,8 @@ function buildProjectSetPatch(input) {
13736
13819
  patch[key] = n;
13737
13820
  } else if (key === "requiredRuntimeSecrets") {
13738
13821
  patch[key] = parseRuntimeSecretsVar(raw);
13822
+ } else if (key === "requiredBuildSecrets") {
13823
+ patch[key] = parseBuildSecretsVar(raw);
13739
13824
  } else if (key === "secrets") {
13740
13825
  patch[key] = parseSecretsCatalogVar(raw);
13741
13826
  } else if (key === "edgeDomains") {
@@ -15715,59 +15800,6 @@ function buildInstalledPluginVersionCheck(input) {
15715
15800
  staleSurfaces: stale
15716
15801
  };
15717
15802
  }
15718
- var MANAGED_PLUGINS = [
15719
- {
15720
- // Marketplace-qualified key exactly as installed_plugins.json stores it (same convention as
15721
- // MMI_PLUGIN_ID = 'mmi@mutmutco') — a bare 'jervaise-powertools' would never match a real record
15722
- // and the check would be a permanent no-op. The same `<marketplace>/<name>` segments also address its
15723
- // Cursor Team Marketplace cache dir (~/.cursor/plugins/cache/jervaise/jervaise-powertools/<version>/).
15724
- id: "jervaise-powertools@jervaise",
15725
- label: "jervaise-powertools",
15726
- healCommand: "jerv-cli doctor --apply",
15727
- opencodePackage: "@jervaise/opencode-jerv"
15728
- }
15729
- ];
15730
- function parseManagedPluginId(id) {
15731
- const at = id.lastIndexOf("@");
15732
- if (at <= 0 || at === id.length - 1) return null;
15733
- return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
15734
- }
15735
- var MANAGED_PLUGIN_DRIFT_LABEL = "managed org plugin version drift (cross-surface)";
15736
- function managedPluginDriftFix(drifted) {
15737
- return drifted.map((d) => {
15738
- const versions = d.versions.map((v) => `${v.version} (${v.surface})`).join(" vs ");
15739
- return `${d.label} drift: ${versions} \u2192 run \`${d.healCommand}\``;
15740
- }).join(" ; ");
15741
- }
15742
- function buildManagedPluginDriftCheck(input) {
15743
- const base = { ok: true, label: MANAGED_PLUGIN_DRIFT_LABEL, fix: "" };
15744
- if (!input.isOrgRepo) return base;
15745
- const registry2 = input.plugins ?? MANAGED_PLUGINS;
15746
- const drifted = [];
15747
- const normalize = (v) => v.replace(/^v/, "");
15748
- for (const descriptor of registry2) {
15749
- const versions = [];
15750
- for (const source of input.sources) {
15751
- if (source.directVersions && descriptor.id in source.directVersions) {
15752
- const version2 = source.directVersions[descriptor.id];
15753
- if (isSemverVersion(version2)) versions.push({ surface: source.surface, version: normalize(version2) });
15754
- continue;
15755
- }
15756
- const records = source.installed?.plugins?.[descriptor.id];
15757
- if (!Array.isArray(records) || records.length === 0) continue;
15758
- const recordVersion = bestRecord(records).version;
15759
- const version = isSemverVersion(recordVersion) ? recordVersion : highestSemver(source.cacheVersions?.[descriptor.id] ?? []);
15760
- if (!isSemverVersion(version)) continue;
15761
- versions.push({ surface: source.surface, version: normalize(version) });
15762
- }
15763
- if (versions.length < 2) continue;
15764
- const distinctVersions = new Set(versions.map((v) => v.version));
15765
- if (distinctVersions.size < 2) continue;
15766
- drifted.push({ pluginId: descriptor.id, label: descriptor.label, healCommand: descriptor.healCommand, versions });
15767
- }
15768
- if (drifted.length === 0) return base;
15769
- return { ...base, ok: false, fix: managedPluginDriftFix(drifted), drifted };
15770
- }
15771
15803
  var OPENCODE_VERSION_LABEL = "installed OpenCode MMI adapter version (vs latest release)";
15772
15804
  function buildOpencodeVersionCheck(input) {
15773
15805
  const fix = pluginRecoveryFix("opencode");
@@ -17235,77 +17267,6 @@ function installedPluginSources() {
17235
17267
  }
17236
17268
  });
17237
17269
  }
17238
- function managedPluginSurfaceSources() {
17239
- const claudeCodex = installedPluginSources().map(({ surface, installed }) => {
17240
- const cacheVersions = {};
17241
- for (const descriptor of MANAGED_PLUGINS) {
17242
- const segments = parseManagedPluginId(descriptor.id);
17243
- if (!segments) continue;
17244
- try {
17245
- cacheVersions[descriptor.id] = (0, import_node_fs18.readdirSync)(
17246
- (0, import_node_path18.join)((0, import_node_os7.homedir)(), `.${surface}`, "plugins", "cache", segments.marketplace, segments.name),
17247
- { withFileTypes: true }
17248
- ).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
17249
- } catch {
17250
- }
17251
- }
17252
- return { surface, installed, cacheVersions };
17253
- });
17254
- return [
17255
- ...claudeCodex,
17256
- { surface: "cursor", directVersions: managedPluginCursorDirectVersions() },
17257
- { surface: "opencode", directVersions: managedPluginOpencodeDirectVersions() }
17258
- ];
17259
- }
17260
- function managedPluginCursorDirectVersions() {
17261
- const out = {};
17262
- for (const descriptor of MANAGED_PLUGINS) {
17263
- const segments = parseManagedPluginId(descriptor.id);
17264
- if (!segments) continue;
17265
- try {
17266
- const versions = (0, import_node_fs18.readdirSync)(
17267
- (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cursor", "plugins", "cache", segments.marketplace, segments.name),
17268
- { withFileTypes: true }
17269
- ).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
17270
- out[descriptor.id] = highestSemver(versions);
17271
- } catch {
17272
- }
17273
- }
17274
- return out;
17275
- }
17276
- function managedPluginOpencodeDirectVersions() {
17277
- const out = {};
17278
- for (const descriptor of MANAGED_PLUGINS) {
17279
- if (!descriptor.opencodePackage) continue;
17280
- out[descriptor.id] = readOpencodePackageVersion(descriptor.opencodePackage);
17281
- }
17282
- return out;
17283
- }
17284
- function readOpencodePackageVersionFrom(packageJsonPath) {
17285
- try {
17286
- const parsed = JSON.parse((0, import_node_fs18.readFileSync)(packageJsonPath, "utf8"));
17287
- return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
17288
- } catch {
17289
- return void 0;
17290
- }
17291
- }
17292
- function readOpencodePackageVersion(packageName) {
17293
- const [scope, name] = packageName.startsWith("@") ? packageName.slice(1).split("/", 2) : [void 0, packageName];
17294
- if (!name) return void 0;
17295
- const diskCandidates = [
17296
- (0, import_node_path18.join)(opencodeConfigDir(), "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json"),
17297
- (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json")
17298
- ];
17299
- const diskVersion = diskCandidates.map(readOpencodePackageVersionFrom).find((v) => v !== void 0);
17300
- const packagesRoot = (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "packages", ...scope ? [`@${scope}`] : []);
17301
- let cacheVersion;
17302
- try {
17303
- const cacheVersions = (0, import_node_fs18.readdirSync)(packagesRoot, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith(`${name}@`) && !e.name.startsWith(".")).map((e) => readOpencodePackageVersionFrom((0, import_node_path18.join)(packagesRoot, e.name, "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json"))).filter((v) => Boolean(v));
17304
- if (cacheVersions.length) cacheVersion = cacheVersions.reduce((lowest, v) => compareVersions(v, lowest) < 0 ? v : lowest);
17305
- } catch {
17306
- }
17307
- return pickOpencodeActiveVersion({ cacheVersion, diskVersion });
17308
- }
17309
17270
  function readClaudeSettings() {
17310
17271
  try {
17311
17272
  return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path18.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
@@ -18192,7 +18153,6 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18192
18153
  }
18193
18154
  }
18194
18155
  checks.push(installedVersionCheck);
18195
- checks.push(buildManagedPluginDriftCheck({ isOrgRepo, sources: managedPluginSurfaceSources() }));
18196
18156
  let openCodeConfigSnapshot = opencodeConfigSnapshot();
18197
18157
  const inspectOpenCode = surface === "opencode" || openCodeConfigSnapshot.hasConfig || runExtended;
18198
18158
  if (inspectOpenCode) {
@@ -19551,7 +19511,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
19551
19511
  const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
19552
19512
  return reportWrite("project attest", res);
19553
19513
  });
19554
- project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--secrets-file <path>", "read the #2244 secrets catalog map (JSON) from a file and set it as --var secrets (robust for large maps)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, edgeDomains, requiredGcpApis, publishRequired").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) => {
19514
+ project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).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)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").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) => {
19555
19515
  const cfg = await loadConfig();
19556
19516
  let target;
19557
19517
  try {
@@ -20185,7 +20145,8 @@ pr.command("checks-wait <number>").description("bounded wait for PR checks; skip
20185
20145
  const result = await waitForPrChecks({
20186
20146
  resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
20187
20147
  pollChecks: () => pollGhPrChecks(number, repoArgs),
20188
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
20148
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
20149
+ log: (message) => console.warn(message)
20189
20150
  });
20190
20151
  if (o.json) printLine(JSON.stringify(result));
20191
20152
  else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
@@ -20212,7 +20173,8 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
20212
20173
  waitForChecks: (prNumber, repo) => waitForPrChecks({
20213
20174
  resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
20214
20175
  pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
20215
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
20176
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
20177
+ log: (message) => console.warn(message)
20216
20178
  }),
20217
20179
  // #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
20218
20180
  // fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
@@ -20423,6 +20385,12 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
20423
20385
  const beforeWorktrees = parseWorktreePorcelain(
20424
20386
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
20425
20387
  );
20388
+ const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo);
20389
+ if (ciPolicy.policy === "no-ci") {
20390
+ const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
20391
+ if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
20392
+ if (guard.note) console.warn(`pr merge: ${guard.note}`);
20393
+ }
20426
20394
  const remoteBefore = await remoteBranchExists2(headRef);
20427
20395
  let remoteDeleteAttempted = false;
20428
20396
  let remoteNotAttemptedReason;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",