@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.
- package/dist/main.cjs +121 -153
- 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
|
-
|
|
5936
|
+
let { policy, reason } = await deps.resolvePolicy();
|
|
5937
|
+
const queuedStates = [];
|
|
5921
5938
|
if (policy === "no-ci") {
|
|
5922
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|