@mutmutco/cli 3.5.0 → 3.7.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 +92 -147
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -4529,9 +4529,16 @@ function buildSessionStartPlan(verbs) {
4529
4529
  ]
4530
4530
  };
4531
4531
  }
4532
- function spawnDetachedSelf(args, deps) {
4532
+ var WIN32_TRAMPOLINE = "const{spawn}=require('node:child_process');const a=JSON.parse(process.argv[1]);const c=spawn(a.cmd,a.args,{stdio:'ignore',windowsHide:true,cwd:a.cwd});c.on('exit',x=>process.exit(x??0));c.on('error',()=>process.exit(1));";
4533
+ function spawnDetachedSelf(args, deps, opts = {}) {
4533
4534
  try {
4534
- deps.spawn(deps.execPath, [deps.scriptPath, ...args], { detached: true, stdio: "ignore", windowsHide: true }).unref();
4535
+ const platform2 = deps.platform ?? process.platform;
4536
+ if (platform2 === "win32") {
4537
+ const payload = JSON.stringify({ cmd: deps.execPath, args: [deps.scriptPath, ...args], cwd: opts.cwd });
4538
+ deps.spawn(deps.execPath, ["-e", WIN32_TRAMPOLINE, payload], { detached: true, stdio: "ignore", windowsHide: true }).unref();
4539
+ return;
4540
+ }
4541
+ deps.spawn(deps.execPath, [deps.scriptPath, ...args], { detached: true, stdio: "ignore", windowsHide: true, ...opts.cwd ? { cwd: opts.cwd } : {} }).unref();
4535
4542
  } catch {
4536
4543
  }
4537
4544
  }
@@ -5890,7 +5897,7 @@ function parseGhPrChecksOutput(stdout) {
5890
5897
  if (!lines.length) return "no-checks-reported";
5891
5898
  let anyPending = false;
5892
5899
  for (const line of lines) {
5893
- const parts = line.split(/\s+/);
5900
+ const parts = line.includes(" ") ? line.split(" ") : line.split(/\s+/);
5894
5901
  const state = (parts[1] ?? "").toLowerCase();
5895
5902
  if (state === "fail" || state === "failure" || state === "error") return "failure";
5896
5903
  if (state === "pass" || state === "success" || state === "skipping" || state === "skipped") continue;
@@ -5909,22 +5916,46 @@ var PR_CHECKS_POLL_MS = 3e4;
5909
5916
  var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
5910
5917
  var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
5911
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
+ }
5912
5935
  async function waitForPrChecks(deps) {
5913
- const { policy, reason } = await deps.resolvePolicy();
5936
+ let { policy, reason } = await deps.resolvePolicy();
5937
+ const queuedStates = [];
5914
5938
  if (policy === "no-ci") {
5915
- 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);
5916
5947
  }
5917
5948
  const now = deps.now ?? (() => Date.now());
5918
5949
  const deadline = now() + PR_CHECKS_TIMEOUT_MS;
5919
5950
  let lastDetail = "pending";
5920
5951
  let successStreak = 0;
5921
5952
  while (now() < deadline) {
5922
- const state = await deps.pollChecks();
5923
- 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 };
5924
5955
  if (state === "success") {
5925
5956
  successStreak += 1;
5926
5957
  if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
5927
- return { policy, status: "success", detail: "checks-success" };
5958
+ return { policy, status: "success", reason, detail: "checks-success" };
5928
5959
  }
5929
5960
  lastDetail = "confirming-success";
5930
5961
  await deps.sleep(PR_CHECKS_CONFIRM_MS);
@@ -5939,7 +5970,7 @@ async function waitForPrChecks(deps) {
5939
5970
  lastDetail = state;
5940
5971
  await deps.sleep(PR_CHECKS_POLL_MS);
5941
5972
  }
5942
- return { policy, status: "timeout", detail: lastDetail };
5973
+ return { policy, status: "timeout", reason, detail: lastDetail };
5943
5974
  }
5944
5975
 
5945
5976
  // src/bootstrap-ruleset.ts
@@ -6729,6 +6760,22 @@ async function auditRepoCi(repo, deps) {
6729
6760
  detail: info.delete_branch_on_merge === true ? void 0 : "false or unavailable"
6730
6761
  });
6731
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
+ }
6732
6779
  if (deployableGated) {
6733
6780
  checks.push({
6734
6781
  ok: hasGateWorkflow,
@@ -6763,7 +6810,7 @@ async function auditRepoCi(repo, deps) {
6763
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)`
6764
6811
  });
6765
6812
  if (hasGateWorkflow) {
6766
- const emitted = registryRequiredContexts(meta) ?? await resolveEmittedPrContexts(deps, repo, baseBranch);
6813
+ const emitted = registryRequiredContexts(meta) ?? await getEmittedPrContexts();
6767
6814
  if (emitted.length > 0) {
6768
6815
  const aligned = contextsMatchRuleset(statusChecks, new Set(emitted));
6769
6816
  checks.push({
@@ -13358,11 +13405,12 @@ function authorizeBodyHasMismatch(body) {
13358
13405
  }
13359
13406
 
13360
13407
  // src/project-set.ts
13361
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
13408
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
13362
13409
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
13363
13410
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
13364
- var SECRET_CONSUMERS = ["runtime", "lambda", "actions", "agent", "box"];
13411
+ var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
13365
13412
  var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
13413
+ 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)$/;
13366
13414
  function parseSecretsCatalogVar(raw) {
13367
13415
  let parsed;
13368
13416
  try {
@@ -13426,6 +13474,21 @@ function parseRuntimeSecretsVar(raw) {
13426
13474
  }
13427
13475
  return out;
13428
13476
  }
13477
+ function parseBuildSecretsVar(raw) {
13478
+ let parsed;
13479
+ try {
13480
+ parsed = JSON.parse(raw);
13481
+ } catch {
13482
+ throw new Error('project set: requiredBuildSecrets must be JSON, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]');
13483
+ }
13484
+ if (!Array.isArray(parsed)) {
13485
+ throw new Error("project set: requiredBuildSecrets must be a flat array (build secrets are not stage-mapped)");
13486
+ }
13487
+ if (parsed.some((n) => typeof n !== "string" || !BUILD_SECRET_REF_RE.test(n) || n === "@github-packages-token")) {
13488
+ throw new Error("project set: requiredBuildSecrets must be a flat array of ENVID=<ref> or bare <ref> strings");
13489
+ }
13490
+ return parsed;
13491
+ }
13429
13492
  function parseEdgeDomainsVar(raw) {
13430
13493
  let parsed;
13431
13494
  try {
@@ -13650,6 +13713,7 @@ var SETTABLE_VAR_KEYS = [
13650
13713
  "consumesDesignSystem",
13651
13714
  "requiredGcpApis",
13652
13715
  "requiredRuntimeSecrets",
13716
+ "requiredBuildSecrets",
13653
13717
  "edgeDomains",
13654
13718
  "statusFieldId",
13655
13719
  "statusOptions",
@@ -13673,7 +13737,8 @@ var SETTABLE_VAR_HINTS = {
13673
13737
  oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
13674
13738
  requiredGcpApis: "comma-string",
13675
13739
  requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
13676
- secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 prefer --secrets-file",
13740
+ requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
13741
+ secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; clear with --unset secrets; prefer --secrets-file",
13677
13742
  edgeDomains: "JSON {dev,rc,main} domain map",
13678
13743
  statusOptions: "JSON name\u2192id map",
13679
13744
  priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
@@ -13729,6 +13794,8 @@ function buildProjectSetPatch(input) {
13729
13794
  patch[key] = n;
13730
13795
  } else if (key === "requiredRuntimeSecrets") {
13731
13796
  patch[key] = parseRuntimeSecretsVar(raw);
13797
+ } else if (key === "requiredBuildSecrets") {
13798
+ patch[key] = parseBuildSecretsVar(raw);
13732
13799
  } else if (key === "secrets") {
13733
13800
  patch[key] = parseSecretsCatalogVar(raw);
13734
13801
  } else if (key === "edgeDomains") {
@@ -15708,59 +15775,6 @@ function buildInstalledPluginVersionCheck(input) {
15708
15775
  staleSurfaces: stale
15709
15776
  };
15710
15777
  }
15711
- var MANAGED_PLUGINS = [
15712
- {
15713
- // Marketplace-qualified key exactly as installed_plugins.json stores it (same convention as
15714
- // MMI_PLUGIN_ID = 'mmi@mutmutco') — a bare 'jervaise-powertools' would never match a real record
15715
- // and the check would be a permanent no-op. The same `<marketplace>/<name>` segments also address its
15716
- // Cursor Team Marketplace cache dir (~/.cursor/plugins/cache/jervaise/jervaise-powertools/<version>/).
15717
- id: "jervaise-powertools@jervaise",
15718
- label: "jervaise-powertools",
15719
- healCommand: "jerv-cli doctor --apply",
15720
- opencodePackage: "@jervaise/opencode-jerv"
15721
- }
15722
- ];
15723
- function parseManagedPluginId(id) {
15724
- const at = id.lastIndexOf("@");
15725
- if (at <= 0 || at === id.length - 1) return null;
15726
- return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
15727
- }
15728
- var MANAGED_PLUGIN_DRIFT_LABEL = "managed org plugin version drift (cross-surface)";
15729
- function managedPluginDriftFix(drifted) {
15730
- return drifted.map((d) => {
15731
- const versions = d.versions.map((v) => `${v.version} (${v.surface})`).join(" vs ");
15732
- return `${d.label} drift: ${versions} \u2192 run \`${d.healCommand}\``;
15733
- }).join(" ; ");
15734
- }
15735
- function buildManagedPluginDriftCheck(input) {
15736
- const base = { ok: true, label: MANAGED_PLUGIN_DRIFT_LABEL, fix: "" };
15737
- if (!input.isOrgRepo) return base;
15738
- const registry2 = input.plugins ?? MANAGED_PLUGINS;
15739
- const drifted = [];
15740
- const normalize = (v) => v.replace(/^v/, "");
15741
- for (const descriptor of registry2) {
15742
- const versions = [];
15743
- for (const source of input.sources) {
15744
- if (source.directVersions && descriptor.id in source.directVersions) {
15745
- const version2 = source.directVersions[descriptor.id];
15746
- if (isSemverVersion(version2)) versions.push({ surface: source.surface, version: normalize(version2) });
15747
- continue;
15748
- }
15749
- const records = source.installed?.plugins?.[descriptor.id];
15750
- if (!Array.isArray(records) || records.length === 0) continue;
15751
- const recordVersion = bestRecord(records).version;
15752
- const version = isSemverVersion(recordVersion) ? recordVersion : highestSemver(source.cacheVersions?.[descriptor.id] ?? []);
15753
- if (!isSemverVersion(version)) continue;
15754
- versions.push({ surface: source.surface, version: normalize(version) });
15755
- }
15756
- if (versions.length < 2) continue;
15757
- const distinctVersions = new Set(versions.map((v) => v.version));
15758
- if (distinctVersions.size < 2) continue;
15759
- drifted.push({ pluginId: descriptor.id, label: descriptor.label, healCommand: descriptor.healCommand, versions });
15760
- }
15761
- if (drifted.length === 0) return base;
15762
- return { ...base, ok: false, fix: managedPluginDriftFix(drifted), drifted };
15763
- }
15764
15778
  var OPENCODE_VERSION_LABEL = "installed OpenCode MMI adapter version (vs latest release)";
15765
15779
  function buildOpencodeVersionCheck(input) {
15766
15780
  const fix = pluginRecoveryFix("opencode");
@@ -17228,77 +17242,6 @@ function installedPluginSources() {
17228
17242
  }
17229
17243
  });
17230
17244
  }
17231
- function managedPluginSurfaceSources() {
17232
- const claudeCodex = installedPluginSources().map(({ surface, installed }) => {
17233
- const cacheVersions = {};
17234
- for (const descriptor of MANAGED_PLUGINS) {
17235
- const segments = parseManagedPluginId(descriptor.id);
17236
- if (!segments) continue;
17237
- try {
17238
- cacheVersions[descriptor.id] = (0, import_node_fs18.readdirSync)(
17239
- (0, import_node_path18.join)((0, import_node_os7.homedir)(), `.${surface}`, "plugins", "cache", segments.marketplace, segments.name),
17240
- { withFileTypes: true }
17241
- ).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
17242
- } catch {
17243
- }
17244
- }
17245
- return { surface, installed, cacheVersions };
17246
- });
17247
- return [
17248
- ...claudeCodex,
17249
- { surface: "cursor", directVersions: managedPluginCursorDirectVersions() },
17250
- { surface: "opencode", directVersions: managedPluginOpencodeDirectVersions() }
17251
- ];
17252
- }
17253
- function managedPluginCursorDirectVersions() {
17254
- const out = {};
17255
- for (const descriptor of MANAGED_PLUGINS) {
17256
- const segments = parseManagedPluginId(descriptor.id);
17257
- if (!segments) continue;
17258
- try {
17259
- const versions = (0, import_node_fs18.readdirSync)(
17260
- (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cursor", "plugins", "cache", segments.marketplace, segments.name),
17261
- { withFileTypes: true }
17262
- ).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
17263
- out[descriptor.id] = highestSemver(versions);
17264
- } catch {
17265
- }
17266
- }
17267
- return out;
17268
- }
17269
- function managedPluginOpencodeDirectVersions() {
17270
- const out = {};
17271
- for (const descriptor of MANAGED_PLUGINS) {
17272
- if (!descriptor.opencodePackage) continue;
17273
- out[descriptor.id] = readOpencodePackageVersion(descriptor.opencodePackage);
17274
- }
17275
- return out;
17276
- }
17277
- function readOpencodePackageVersionFrom(packageJsonPath) {
17278
- try {
17279
- const parsed = JSON.parse((0, import_node_fs18.readFileSync)(packageJsonPath, "utf8"));
17280
- return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
17281
- } catch {
17282
- return void 0;
17283
- }
17284
- }
17285
- function readOpencodePackageVersion(packageName) {
17286
- const [scope, name] = packageName.startsWith("@") ? packageName.slice(1).split("/", 2) : [void 0, packageName];
17287
- if (!name) return void 0;
17288
- const diskCandidates = [
17289
- (0, import_node_path18.join)(opencodeConfigDir(), "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json"),
17290
- (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json")
17291
- ];
17292
- const diskVersion = diskCandidates.map(readOpencodePackageVersionFrom).find((v) => v !== void 0);
17293
- const packagesRoot = (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "packages", ...scope ? [`@${scope}`] : []);
17294
- let cacheVersion;
17295
- try {
17296
- 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));
17297
- if (cacheVersions.length) cacheVersion = cacheVersions.reduce((lowest, v) => compareVersions(v, lowest) < 0 ? v : lowest);
17298
- } catch {
17299
- }
17300
- return pickOpencodeActiveVersion({ cacheVersion, diskVersion });
17301
- }
17302
17245
  function readClaudeSettings() {
17303
17246
  try {
17304
17247
  return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path18.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
@@ -18185,7 +18128,6 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18185
18128
  }
18186
18129
  }
18187
18130
  checks.push(installedVersionCheck);
18188
- checks.push(buildManagedPluginDriftCheck({ isOrgRepo, sources: managedPluginSurfaceSources() }));
18189
18131
  let openCodeConfigSnapshot = opencodeConfigSnapshot();
18190
18132
  const inspectOpenCode = surface === "opencode" || openCodeConfigSnapshot.hasConfig || runExtended;
18191
18133
  if (inspectOpenCode) {
@@ -19206,12 +19148,7 @@ function scheduleRelatedDiscovery(o) {
19206
19148
  try {
19207
19149
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
19208
19150
  if (o.repo) args.push("--repo", o.repo);
19209
- (0, import_node_child_process12.spawn)(process.execPath, [process.argv[1], ...args], {
19210
- detached: true,
19211
- stdio: "ignore",
19212
- windowsHide: true,
19213
- cwd: process.cwd()
19214
- }).unref();
19151
+ spawnDetachedSelf(args, { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
19215
19152
  } catch {
19216
19153
  }
19217
19154
  }
@@ -19549,7 +19486,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
19549
19486
  const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
19550
19487
  return reportWrite("project attest", res);
19551
19488
  });
19552
- 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) => {
19489
+ 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) => {
19553
19490
  const cfg = await loadConfig();
19554
19491
  let target;
19555
19492
  try {
@@ -20183,7 +20120,8 @@ pr.command("checks-wait <number>").description("bounded wait for PR checks; skip
20183
20120
  const result = await waitForPrChecks({
20184
20121
  resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
20185
20122
  pollChecks: () => pollGhPrChecks(number, repoArgs),
20186
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
20123
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
20124
+ log: (message) => console.warn(message)
20187
20125
  });
20188
20126
  if (o.json) printLine(JSON.stringify(result));
20189
20127
  else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
@@ -20210,7 +20148,8 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
20210
20148
  waitForChecks: (prNumber, repo) => waitForPrChecks({
20211
20149
  resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
20212
20150
  pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
20213
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
20151
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
20152
+ log: (message) => console.warn(message)
20214
20153
  }),
20215
20154
  // #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
20216
20155
  // fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
@@ -20421,6 +20360,12 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
20421
20360
  const beforeWorktrees = parseWorktreePorcelain(
20422
20361
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
20423
20362
  );
20363
+ const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo);
20364
+ if (ciPolicy.policy === "no-ci") {
20365
+ const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
20366
+ if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
20367
+ if (guard.note) console.warn(`pr merge: ${guard.note}`);
20368
+ }
20424
20369
  const remoteBefore = await remoteBranchExists2(headRef);
20425
20370
  let remoteDeleteAttempted = false;
20426
20371
  let remoteNotAttemptedReason;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.5.0",
3
+ "version": "3.7.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",