@mutmutco/cli 3.26.0 → 3.27.1
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 +181 -44
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3616,13 +3616,29 @@ var CLI_EXIT_WATCHDOG_MS = 1e4;
|
|
|
3616
3616
|
async function finishCliRun(watchdogMs = CLI_EXIT_WATCHDOG_MS) {
|
|
3617
3617
|
const timer = setTimeout(() => {
|
|
3618
3618
|
console.error(
|
|
3619
|
-
`mmi-cli: command finished but the process did not exit within ${watchdogMs}ms \u2014 a handle leaked; forcing exit (#2904). Please report this.`
|
|
3619
|
+
`mmi-cli: command finished but the process did not exit within ${watchdogMs}ms \u2014 a handle leaked (${describeActiveHandles()}); forcing exit (#2904). Please report this.`
|
|
3620
3620
|
);
|
|
3621
3621
|
void flushStdio().finally(() => process.exit(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
3622
3622
|
}, watchdogMs);
|
|
3623
3623
|
timer.unref?.();
|
|
3624
3624
|
await closeHttpPool();
|
|
3625
3625
|
}
|
|
3626
|
+
function describeActiveHandles() {
|
|
3627
|
+
let summary;
|
|
3628
|
+
try {
|
|
3629
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3630
|
+
for (const type of process.getActiveResourcesInfo()) counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
3631
|
+
summary = [...counts.entries()].sort().map(([type, n]) => n > 1 ? `${type}\xD7${n}` : type).join(", ") || "none reported";
|
|
3632
|
+
} catch {
|
|
3633
|
+
summary = "unavailable";
|
|
3634
|
+
}
|
|
3635
|
+
try {
|
|
3636
|
+
const children = (process._getActiveHandles?.() ?? []).filter((h) => h?.constructor?.name === "ChildProcess").map((h) => `${h.spawnfile ?? "?"}(pid=${h.pid ?? "?"})`);
|
|
3637
|
+
if (children.length) summary += `; live children: ${children.join(", ")}`;
|
|
3638
|
+
} catch {
|
|
3639
|
+
}
|
|
3640
|
+
return summary;
|
|
3641
|
+
}
|
|
3626
3642
|
async function failGraceful(msg) {
|
|
3627
3643
|
console.error(`mmi-cli ${msg}`);
|
|
3628
3644
|
return cleanExit(1);
|
|
@@ -5453,6 +5469,7 @@ function buildPrMergeArgs(input) {
|
|
|
5453
5469
|
function basePolicyBlocksImmediateMerge(message) {
|
|
5454
5470
|
return /base branch policy prohibits|protected branch|required status check|merge queue/i.test(message);
|
|
5455
5471
|
}
|
|
5472
|
+
var PR_MERGE_ENQUEUED_EXIT_CODE = 2;
|
|
5456
5473
|
function ghPrMergeLocalBranchDeleteWarning(message) {
|
|
5457
5474
|
return /used by worktree|cannot delete branch/i.test(message);
|
|
5458
5475
|
}
|
|
@@ -9200,7 +9217,7 @@ function resolveMergeCiPolicy(input) {
|
|
|
9200
9217
|
(p) => /^\.github\/workflows\/[^/]+\.(ya?ml)$/i.test(p.replace(/\\/g, "/"))
|
|
9201
9218
|
);
|
|
9202
9219
|
if (!ciWorkflows.length) {
|
|
9203
|
-
return { policy: "no-ci", reason: "no .github/workflows CI" };
|
|
9220
|
+
return { policy: "no-ci", reason: "no pull_request-triggered .github/workflows CI" };
|
|
9204
9221
|
}
|
|
9205
9222
|
return { policy: "wait-for-checks", reason: ciWorkflows.join(", ") };
|
|
9206
9223
|
}
|
|
@@ -9211,13 +9228,18 @@ function parseGhPrChecksOutput(stdout) {
|
|
|
9211
9228
|
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
9212
9229
|
if (!lines.length) return "no-checks-reported";
|
|
9213
9230
|
let anyPending = false;
|
|
9231
|
+
let anyFailure = false;
|
|
9214
9232
|
for (const line of lines) {
|
|
9215
9233
|
const parts = line.includes(" ") ? line.split(" ") : line.split(/\s+/);
|
|
9216
9234
|
const state = (parts[1] ?? "").toLowerCase();
|
|
9217
|
-
if (state === "fail" || state === "failure" || state === "error")
|
|
9235
|
+
if (state === "fail" || state === "failure" || state === "error") {
|
|
9236
|
+
anyFailure = true;
|
|
9237
|
+
continue;
|
|
9238
|
+
}
|
|
9218
9239
|
if (state === "pass" || state === "success" || state === "skipping" || state === "skipped") continue;
|
|
9219
9240
|
anyPending = true;
|
|
9220
9241
|
}
|
|
9242
|
+
if (anyFailure) return anyPending ? "failing" : "failure";
|
|
9221
9243
|
return anyPending ? "pending" : "success";
|
|
9222
9244
|
}
|
|
9223
9245
|
function parseGhPrChecksResult(stdout, stderr) {
|
|
@@ -9230,14 +9252,16 @@ function parseGhPrChecksResult(stdout, stderr) {
|
|
|
9230
9252
|
function pollStateFromParsed(parsed) {
|
|
9231
9253
|
return parsed === "error" ? "failure" : parsed;
|
|
9232
9254
|
}
|
|
9255
|
+
var PR_CHECKS_TIMEOUT_EXIT_CODE = 2;
|
|
9233
9256
|
var PR_CHECKS_POLL_MS = 3e4;
|
|
9234
|
-
var PR_CHECKS_TIMEOUT_MS =
|
|
9257
|
+
var PR_CHECKS_TIMEOUT_MS = 30 * 6e4;
|
|
9235
9258
|
var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
|
|
9236
9259
|
var PR_CHECKS_CONFIRM_MS = 5e3;
|
|
9260
|
+
var PR_CHECKS_FAILURE_CONFIRMATIONS = 2;
|
|
9237
9261
|
var NO_CI_LIVE_CHECKS_CONTRADICTION_REASON = "ci:none contradicted by live check runs on PR head";
|
|
9238
9262
|
function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none") {
|
|
9239
9263
|
if (checks === "no-checks-reported") return { action: "proceed" };
|
|
9240
|
-
const staleNote = `
|
|
9264
|
+
const staleNote = `merge CI policy says no-ci (${policyReason}), but live PR checks exist`;
|
|
9241
9265
|
if (checks === "success") {
|
|
9242
9266
|
return { action: "proceed", note: `${staleNote}; proceeding because they are green` };
|
|
9243
9267
|
}
|
|
@@ -9264,12 +9288,39 @@ async function waitForPrChecks(deps) {
|
|
|
9264
9288
|
queuedStates.push(firstState);
|
|
9265
9289
|
}
|
|
9266
9290
|
const now = deps.now ?? (() => Date.now());
|
|
9267
|
-
const
|
|
9291
|
+
const timeoutMs = deps.timeoutMs ?? PR_CHECKS_TIMEOUT_MS;
|
|
9292
|
+
const started = now();
|
|
9293
|
+
const deadline = started + timeoutMs;
|
|
9294
|
+
const report = (state) => {
|
|
9295
|
+
try {
|
|
9296
|
+
const elapsedMs = now() - started;
|
|
9297
|
+
deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()) });
|
|
9298
|
+
} catch {
|
|
9299
|
+
}
|
|
9300
|
+
};
|
|
9268
9301
|
let lastDetail = "pending";
|
|
9269
9302
|
let successStreak = 0;
|
|
9303
|
+
let failureStreak = 0;
|
|
9304
|
+
report("starting");
|
|
9270
9305
|
while (now() < deadline) {
|
|
9271
9306
|
const state = queuedStates.shift() ?? await deps.pollChecks();
|
|
9272
|
-
|
|
9307
|
+
report(state);
|
|
9308
|
+
if (state !== "success") successStreak = 0;
|
|
9309
|
+
if (state !== "failure") failureStreak = 0;
|
|
9310
|
+
if (state === "failure") {
|
|
9311
|
+
failureStreak += 1;
|
|
9312
|
+
if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
|
|
9313
|
+
return { policy, status: "failure", reason, detail: "checks-failure" };
|
|
9314
|
+
}
|
|
9315
|
+
lastDetail = "confirming-failure";
|
|
9316
|
+
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
9317
|
+
continue;
|
|
9318
|
+
}
|
|
9319
|
+
if (state === "failing") {
|
|
9320
|
+
lastDetail = "failing (a check failed; others still running)";
|
|
9321
|
+
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
9322
|
+
continue;
|
|
9323
|
+
}
|
|
9273
9324
|
if (state === "success") {
|
|
9274
9325
|
successStreak += 1;
|
|
9275
9326
|
if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
|
|
@@ -9279,7 +9330,6 @@ async function waitForPrChecks(deps) {
|
|
|
9279
9330
|
await deps.sleep(PR_CHECKS_CONFIRM_MS);
|
|
9280
9331
|
continue;
|
|
9281
9332
|
}
|
|
9282
|
-
successStreak = 0;
|
|
9283
9333
|
if (state === "no-checks-reported") {
|
|
9284
9334
|
lastDetail = "no-checks-reported (waiting for workflow to queue)";
|
|
9285
9335
|
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
@@ -9288,7 +9338,7 @@ async function waitForPrChecks(deps) {
|
|
|
9288
9338
|
lastDetail = state;
|
|
9289
9339
|
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
9290
9340
|
}
|
|
9291
|
-
return { policy, status: "timeout", reason, detail: lastDetail };
|
|
9341
|
+
return { policy, status: "timeout", reason, detail: lastDetail, waitedMs: now() - started };
|
|
9292
9342
|
}
|
|
9293
9343
|
|
|
9294
9344
|
// src/bootstrap-ruleset.ts
|
|
@@ -9376,6 +9426,11 @@ function workflowTriggersPullRequest(yaml) {
|
|
|
9376
9426
|
if (!onBlock) return false;
|
|
9377
9427
|
return /\bpull_request\b/.test(onBlock);
|
|
9378
9428
|
}
|
|
9429
|
+
function workflowReportsPrChecks(yaml) {
|
|
9430
|
+
const onBlock = extractOnBlock(yaml);
|
|
9431
|
+
if (!onBlock) return false;
|
|
9432
|
+
return /\bpull_request(_target)?\b/.test(onBlock);
|
|
9433
|
+
}
|
|
9379
9434
|
function extractOnBlock(yaml) {
|
|
9380
9435
|
const lines = yaml.split(/\r?\n/);
|
|
9381
9436
|
let inOn = false;
|
|
@@ -9926,12 +9981,12 @@ async function listWorkflowPaths(deps, repo, branch) {
|
|
|
9926
9981
|
`repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`
|
|
9927
9982
|
);
|
|
9928
9983
|
return (entries ?? []).filter((e) => e.type === "file" && /\.ya?ml$/i.test(e.name ?? "")).map((e) => `.github/workflows/${e.name}`);
|
|
9929
|
-
} catch {
|
|
9930
|
-
return [];
|
|
9984
|
+
} catch (e) {
|
|
9985
|
+
return e?.status === 404 ? [] : void 0;
|
|
9931
9986
|
}
|
|
9932
9987
|
}
|
|
9933
9988
|
async function resolveEmittedPrContexts(deps, repo, branch) {
|
|
9934
|
-
const paths = await listWorkflowPaths(deps, repo, branch);
|
|
9989
|
+
const paths = await listWorkflowPaths(deps, repo, branch) ?? [];
|
|
9935
9990
|
const workflows = [];
|
|
9936
9991
|
for (const path2 of paths) {
|
|
9937
9992
|
const body = await fetchFileContent(deps, repo, branch, path2);
|
|
@@ -9984,12 +10039,25 @@ async function resolveRepoMergeCiPolicy(repo, deps) {
|
|
|
9984
10039
|
}
|
|
9985
10040
|
const baseBranch = "development";
|
|
9986
10041
|
const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
|
|
10042
|
+
const workflowPaths = repoClass === "hub" ? [".github/workflows/gate.yml"] : hasGate ? [PRODUCT_GATE_PATH] : await listWorkflowPaths(deps, repo, baseBranch);
|
|
10043
|
+
if (workflowPaths === void 0) {
|
|
10044
|
+
return { policy: "wait-for-checks", reason: "workflows listing unreadable (API error) \u2014 failing safe to wait-for-checks" };
|
|
10045
|
+
}
|
|
10046
|
+
const prTriggered = await filterPullRequestTriggered(deps, repo, baseBranch, workflowPaths);
|
|
9987
10047
|
return resolveMergeCiPolicy({
|
|
9988
|
-
workflowPaths:
|
|
10048
|
+
workflowPaths: prTriggered,
|
|
9989
10049
|
registryCi: meta?.ci,
|
|
9990
10050
|
registryRequiredChecks: meta?.requiredChecks
|
|
9991
10051
|
});
|
|
9992
10052
|
}
|
|
10053
|
+
async function filterPullRequestTriggered(deps, repo, branch, paths) {
|
|
10054
|
+
const kept = [];
|
|
10055
|
+
for (const path2 of paths) {
|
|
10056
|
+
const body = await fetchFileContent(deps, repo, branch, path2);
|
|
10057
|
+
if (body == null || workflowReportsPrChecks(body)) kept.push(path2);
|
|
10058
|
+
}
|
|
10059
|
+
return kept;
|
|
10060
|
+
}
|
|
9993
10061
|
async function auditRepoCi(repo, deps) {
|
|
9994
10062
|
const meta = await deps.getProjectMeta(slugFromRepo(repo));
|
|
9995
10063
|
const repoClass = classifyRepo(repo, meta);
|
|
@@ -16453,6 +16521,15 @@ function settableVarHelp() {
|
|
|
16453
16521
|
});
|
|
16454
16522
|
return `META field to set (repeatable): ${keys.join(", ")}`;
|
|
16455
16523
|
}
|
|
16524
|
+
function duplicateVarKeyAcrossFlags(vars, sets) {
|
|
16525
|
+
const keyOf = (v) => v.includes("=") ? v.slice(0, v.indexOf("=")) : v;
|
|
16526
|
+
const varKeys = new Set(vars.map(keyOf));
|
|
16527
|
+
for (const s of sets) {
|
|
16528
|
+
const key = keyOf(s);
|
|
16529
|
+
if (varKeys.has(key)) return key;
|
|
16530
|
+
}
|
|
16531
|
+
return null;
|
|
16532
|
+
}
|
|
16456
16533
|
function buildProjectSetPatch(input) {
|
|
16457
16534
|
const patch = {};
|
|
16458
16535
|
if (input.class) {
|
|
@@ -16481,7 +16558,11 @@ function buildProjectSetPatch(input) {
|
|
|
16481
16558
|
}
|
|
16482
16559
|
for (const value of input.vars) {
|
|
16483
16560
|
const eq = value.indexOf("=");
|
|
16484
|
-
if (eq <= 0)
|
|
16561
|
+
if (eq <= 0) {
|
|
16562
|
+
const looksPositional = value.includes("/") || !value.includes("=");
|
|
16563
|
+
const hint = looksPositional ? " \u2014 if that is the [owner/repo] argument, put it BEFORE --var; a variadic flag swallows the tokens after it" : "";
|
|
16564
|
+
throw new Error(`project set: --var must be KEY=VALUE (got "${value}")${hint}`);
|
|
16565
|
+
}
|
|
16485
16566
|
const key = value.slice(0, eq);
|
|
16486
16567
|
const raw = value.slice(eq + 1);
|
|
16487
16568
|
if (!SETTABLE_VAR_KEY_SET.has(key)) {
|
|
@@ -18206,6 +18287,28 @@ function validateBatchSpecs(specs) {
|
|
|
18206
18287
|
}
|
|
18207
18288
|
return { ok: errors.length === 0, errors, validated };
|
|
18208
18289
|
}
|
|
18290
|
+
var NORTH_STAR_LABEL_COLOR = "5319e7";
|
|
18291
|
+
function northStarLabelsForBatch(specs) {
|
|
18292
|
+
const labels = /* @__PURE__ */ new Set();
|
|
18293
|
+
for (const spec of specs) {
|
|
18294
|
+
if (spec.northStar) labels.add(northStarLabel(normalizeNorthStarSlug(spec.northStar)));
|
|
18295
|
+
for (const l of spec.labels ?? []) if (l.startsWith("northstar:")) labels.add(l);
|
|
18296
|
+
}
|
|
18297
|
+
return [...labels].sort((a, b) => a.localeCompare(b));
|
|
18298
|
+
}
|
|
18299
|
+
async function ensureNorthStarLabels(client, repo, specs) {
|
|
18300
|
+
const minted = [];
|
|
18301
|
+
for (const name of northStarLabelsForBatch(specs)) {
|
|
18302
|
+
try {
|
|
18303
|
+
await client.rest("POST", `repos/${repo}/labels`, {
|
|
18304
|
+
body: { name, color: NORTH_STAR_LABEL_COLOR, description: `North Star: ${name.slice("northstar:".length)}` }
|
|
18305
|
+
});
|
|
18306
|
+
minted.push(name);
|
|
18307
|
+
} catch {
|
|
18308
|
+
}
|
|
18309
|
+
}
|
|
18310
|
+
return minted;
|
|
18311
|
+
}
|
|
18209
18312
|
async function createIssuesBatch(specs, options, deps = {}) {
|
|
18210
18313
|
const client = deps.client ?? defaultGitHubClient();
|
|
18211
18314
|
const validation = validateBatchSpecs(specs);
|
|
@@ -18216,6 +18319,7 @@ ${lines}`);
|
|
|
18216
18319
|
}
|
|
18217
18320
|
const repo = await resolveRepo(options.repo);
|
|
18218
18321
|
if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
|
|
18322
|
+
await ensureNorthStarLabels(client, repo, validation.validated.map((v) => v.spec));
|
|
18219
18323
|
const created = [];
|
|
18220
18324
|
const failures = [];
|
|
18221
18325
|
for (const entry of validation.validated) {
|
|
@@ -19594,7 +19698,7 @@ function registerPrLifecycleCommands(program3) {
|
|
|
19594
19698
|
const { state, rows } = await snap();
|
|
19595
19699
|
const output = { state, checks: rows };
|
|
19596
19700
|
console.log(JSON.stringify(output));
|
|
19597
|
-
if (state === "failure") process.exitCode = 1;
|
|
19701
|
+
if (state === "failure" || state === "failing") process.exitCode = 1;
|
|
19598
19702
|
return;
|
|
19599
19703
|
}
|
|
19600
19704
|
const deadline = Date.now() + CHECKS_WATCH_TIMEOUT_MS;
|
|
@@ -20515,11 +20619,15 @@ function acquireWorktreeSetupLock(worktreeRoot) {
|
|
|
20515
20619
|
}
|
|
20516
20620
|
}
|
|
20517
20621
|
var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
|
|
20518
|
-
worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
|
|
20622
|
+
worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--base <ref>", "alias of --from (git's own word for the base ref)").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--json", "machine-readable output").action(async (branch, o, cmd) => {
|
|
20519
20623
|
try {
|
|
20624
|
+
if (o.base !== void 0 && cmd.getOptionValueSource("from") === "cli" && o.base !== o.from) {
|
|
20625
|
+
return fail(`worktree create: --from and --base are the same option (--base is an alias); got --from ${o.from} and --base ${o.base} \u2014 pass one`);
|
|
20626
|
+
}
|
|
20627
|
+
const fromRef = o.base ?? o.from;
|
|
20520
20628
|
const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
20521
20629
|
const wtPath = o.path ?? defaultWorktreePath(repoRoot2, branch);
|
|
20522
|
-
const { base, fetchBranch } = resolveWorktreeBase(
|
|
20630
|
+
const { base, fetchBranch } = resolveWorktreeBase(fromRef, o.remote);
|
|
20523
20631
|
if (fetchBranch) {
|
|
20524
20632
|
const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
|
|
20525
20633
|
if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
|
|
@@ -20975,7 +21083,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
|
|
|
20975
21083
|
const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
|
|
20976
21084
|
return reportWrite("project attest", res);
|
|
20977
21085
|
});
|
|
20978
|
-
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) => {
|
|
21086
|
+
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("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").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) => {
|
|
20979
21087
|
const cfg = await loadConfig();
|
|
20980
21088
|
let target;
|
|
20981
21089
|
try {
|
|
@@ -20985,7 +21093,10 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
20985
21093
|
}
|
|
20986
21094
|
const slug = slugOf(target);
|
|
20987
21095
|
const repo = target.includes("/") ? target : `mutmutco/${slug}`;
|
|
20988
|
-
const
|
|
21096
|
+
const setAlias = o.set ?? [];
|
|
21097
|
+
const vars = [...o.var ?? [], ...setAlias];
|
|
21098
|
+
const dupe = duplicateVarKeyAcrossFlags(o.var ?? [], setAlias);
|
|
21099
|
+
if (dupe) return fail(`project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
|
|
20989
21100
|
if (o.secretsFile) {
|
|
20990
21101
|
try {
|
|
20991
21102
|
vars.push(`secrets=${(0, import_node_fs24.readFileSync)(o.secretsFile, "utf8")}`);
|
|
@@ -21001,7 +21112,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
21001
21112
|
deployModel: o.deployModel,
|
|
21002
21113
|
releaseTrack: o.releaseTrack,
|
|
21003
21114
|
vars,
|
|
21004
|
-
unsets:
|
|
21115
|
+
unsets: o.unset ?? [],
|
|
21005
21116
|
clearWebProfile: Boolean(o.clearWebProfile)
|
|
21006
21117
|
});
|
|
21007
21118
|
} catch (e) {
|
|
@@ -21620,7 +21731,13 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
|
|
|
21620
21731
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
21621
21732
|
const wfDir = (0, import_node_path23.join)(cwd, ".github", "workflows");
|
|
21622
21733
|
if (!(0, import_node_fs24.existsSync)(wfDir)) return [];
|
|
21623
|
-
return (0, import_node_fs24.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).
|
|
21734
|
+
return (0, import_node_fs24.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
|
|
21735
|
+
try {
|
|
21736
|
+
return workflowReportsPrChecks((0, import_node_fs24.readFileSync)((0, import_node_path23.join)(wfDir, name), "utf8"));
|
|
21737
|
+
} catch {
|
|
21738
|
+
return true;
|
|
21739
|
+
}
|
|
21740
|
+
}).map((name) => `.github/workflows/${name}`);
|
|
21624
21741
|
}
|
|
21625
21742
|
async function resolveMergeCiPolicyForCheckout(repoOpt) {
|
|
21626
21743
|
const repo = repoOpt ?? await resolveRepo();
|
|
@@ -21677,17 +21794,30 @@ async function pollGhPrChecks(prNumber, repoArgs) {
|
|
|
21677
21794
|
}
|
|
21678
21795
|
return state;
|
|
21679
21796
|
}
|
|
21680
|
-
pr.command("checks-wait <number>").description(
|
|
21797
|
+
pr.command("checks-wait <number>").description(`bounded wait for PR checks; skips immediately on no-ci repos (#1432). Default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m; --timeout raises it. Exit 1 = a check FAILED, exit ${PR_CHECKS_TIMEOUT_EXIT_CODE} = the wait window expired (re-arm)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--timeout <minutes>", `wait budget in minutes (default ${PR_CHECKS_TIMEOUT_MS / 6e4}) \u2014 raise it for serial self-hosted e2e queues`).action(async (number, o) => {
|
|
21681
21798
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
21799
|
+
let timeoutMs;
|
|
21800
|
+
if (o.timeout !== void 0) {
|
|
21801
|
+
const minutes = Number(o.timeout);
|
|
21802
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return fail("pr checks-wait: --timeout must be a positive number of minutes");
|
|
21803
|
+
timeoutMs = Math.round(minutes * 6e4);
|
|
21804
|
+
}
|
|
21682
21805
|
const result = await waitForPrChecks({
|
|
21683
21806
|
resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
|
|
21684
21807
|
pollChecks: () => pollGhPrChecks(number, repoArgs),
|
|
21685
21808
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
21686
|
-
log: (message) => console.warn(message)
|
|
21809
|
+
log: (message) => console.warn(message),
|
|
21810
|
+
timeoutMs,
|
|
21811
|
+
// Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
|
|
21812
|
+
// an agent harness kills it on its own (shorter) deadline before the verdict ever prints (#2940).
|
|
21813
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
21687
21814
|
});
|
|
21688
21815
|
if (o.json) printLine(JSON.stringify(result));
|
|
21689
|
-
else
|
|
21690
|
-
|
|
21816
|
+
else if (result.status === "timeout") {
|
|
21817
|
+
printLine(`pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.`);
|
|
21818
|
+
} else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
21819
|
+
if (result.status === "failure") process.exitCode = 1;
|
|
21820
|
+
if (result.status === "timeout") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
21691
21821
|
});
|
|
21692
21822
|
async function ghMergeAutoEnqueue(prNumber, repo, method) {
|
|
21693
21823
|
const args = repo ? ["--repo", repo] : [];
|
|
@@ -21770,7 +21900,10 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
21770
21900
|
resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
21771
21901
|
pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
|
|
21772
21902
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
21773
|
-
log: (message) => console.warn(message)
|
|
21903
|
+
log: (message) => console.warn(message),
|
|
21904
|
+
// `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
|
|
21905
|
+
// 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
|
|
21906
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr land: waiting on checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
21774
21907
|
}),
|
|
21775
21908
|
// #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
|
|
21776
21909
|
// fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
|
|
@@ -21952,6 +22085,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
21952
22085
|
}
|
|
21953
22086
|
const remoteBefore = await remoteBranchExists2(headRef);
|
|
21954
22087
|
let remoteDeleteAttempted = false;
|
|
22088
|
+
let upgradedToAuto = false;
|
|
21955
22089
|
let remoteNotAttemptedReason = headIsProtected ? "protected-branch" : void 0;
|
|
21956
22090
|
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: !headIsProtected }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
|
|
21957
22091
|
const message = String(e.message || "");
|
|
@@ -21975,14 +22109,30 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
21975
22109
|
return;
|
|
21976
22110
|
}
|
|
21977
22111
|
if (!o.auto && basePolicyBlocksImmediateMerge(message)) {
|
|
21978
|
-
|
|
22112
|
+
console.warn(`pr merge: the base-branch policy blocks an immediate merge \u2014 upgrading to --auto (merges once required checks pass).`);
|
|
22113
|
+
upgradedToAuto = true;
|
|
22114
|
+
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: true, deleteBranch: !headIsProtected }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
|
|
22115
|
+
const m2 = String(e2.message || "");
|
|
22116
|
+
if (/already been merged/i.test(m2)) {
|
|
22117
|
+
remoteNotAttemptedReason = "pr-already-merged";
|
|
22118
|
+
return;
|
|
22119
|
+
}
|
|
22120
|
+
const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
|
|
22121
|
+
if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
|
|
22122
|
+
if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
|
|
22123
|
+
});
|
|
22124
|
+
return;
|
|
21979
22125
|
}
|
|
21980
22126
|
if (!ghPrMergeLocalBranchDeleteWarning(message)) throw e;
|
|
21981
22127
|
});
|
|
21982
|
-
if (o.auto) {
|
|
22128
|
+
if (o.auto || upgradedToAuto) {
|
|
21983
22129
|
const state = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS5 }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
21984
22130
|
if (state !== "MERGED") {
|
|
21985
|
-
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", pr: number, branch: headRef, state: state || "unknown" }));
|
|
22131
|
+
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
|
|
22132
|
+
if (upgradedToAuto && !o.auto) {
|
|
22133
|
+
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 auto-merge will land it once required checks pass. Exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
|
|
22134
|
+
process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
|
|
22135
|
+
}
|
|
21986
22136
|
return;
|
|
21987
22137
|
}
|
|
21988
22138
|
}
|
|
@@ -22316,19 +22466,6 @@ function rawValue(flag, fallback) {
|
|
|
22316
22466
|
const inline = process.argv.find((arg) => arg.startsWith(`${flag}=`));
|
|
22317
22467
|
return inline ? inline.slice(flag.length + 1) : fallback;
|
|
22318
22468
|
}
|
|
22319
|
-
function rawValues(flag) {
|
|
22320
|
-
const out = [];
|
|
22321
|
-
for (let i = 0; i < process.argv.length; i++) {
|
|
22322
|
-
const arg = process.argv[i];
|
|
22323
|
-
if (arg === flag && process.argv[i + 1]) {
|
|
22324
|
-
out.push(process.argv[i + 1]);
|
|
22325
|
-
i++;
|
|
22326
|
-
continue;
|
|
22327
|
-
}
|
|
22328
|
-
if (arg.startsWith(`${flag}=`)) out.push(arg.slice(flag.length + 1));
|
|
22329
|
-
}
|
|
22330
|
-
return out;
|
|
22331
|
-
}
|
|
22332
22469
|
function stagePortFromArgv() {
|
|
22333
22470
|
const raw = rawValue("--port", "");
|
|
22334
22471
|
if (!raw) return void 0;
|
|
@@ -22896,7 +23033,7 @@ bootstrap.command("org-ruleset").description("drift-check the codified org no-ag
|
|
|
22896
23033
|
else console.log(renderOrgRulesetDriftReport(plan));
|
|
22897
23034
|
if (plan.action !== "noop") process.exitCode = 1;
|
|
22898
23035
|
});
|
|
22899
|
-
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)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
|
|
23036
|
+
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)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
|
|
22900
23037
|
const o = {
|
|
22901
23038
|
class: rawValue("--class", "deployable"),
|
|
22902
23039
|
projectType: rawValue("--project-type", ""),
|
|
@@ -22922,7 +23059,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
|
|
|
22922
23059
|
const readFile4 = (p) => (0, import_node_fs24.existsSync)(p) ? (0, import_node_fs24.readFileSync)(p, "utf8") : null;
|
|
22923
23060
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
22924
23061
|
const rawVars = {};
|
|
22925
|
-
for (const value of
|
|
23062
|
+
for (const value of cmdOpts.var ?? []) {
|
|
22926
23063
|
const eq = value.indexOf("=");
|
|
22927
23064
|
if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
|
|
22928
23065
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.27.1",
|
|
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",
|