@mutmutco/cli 3.71.0 → 3.72.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 +455 -249
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -37,6 +37,7 @@ __export(index_exports, {
|
|
|
37
37
|
envHealLockPath: () => envHealLockPath,
|
|
38
38
|
gcPlan: () => gcPlan,
|
|
39
39
|
isOrgRegisteredRepo: () => isOrgRegisteredRepo,
|
|
40
|
+
parseInvalidChoiceError: () => parseInvalidChoiceError,
|
|
40
41
|
positionalTargetForm: () => positionalTargetForm,
|
|
41
42
|
registryClientDeps: () => registryClientDeps,
|
|
42
43
|
repoSlug: () => repoSlug,
|
|
@@ -4052,7 +4053,9 @@ function argvWantsMachineFailure() {
|
|
|
4052
4053
|
}
|
|
4053
4054
|
function commandFromFailMessage(msg) {
|
|
4054
4055
|
const head = msg.split(":", 1)[0].trim();
|
|
4055
|
-
|
|
4056
|
+
if (!head || head.startsWith("-")) return "mmi-cli";
|
|
4057
|
+
if (!/^[a-z][a-z0-9-]*(?: [a-z][a-z0-9-]*){0,3}$/.test(head)) return "mmi-cli";
|
|
4058
|
+
return head;
|
|
4056
4059
|
}
|
|
4057
4060
|
function fail(msg, payload) {
|
|
4058
4061
|
const json = argvWantsMachineFailure();
|
|
@@ -6686,21 +6689,39 @@ function patchRulesetRequiredContexts(body, contexts) {
|
|
|
6686
6689
|
function findProductRuleset(rulesets) {
|
|
6687
6690
|
return rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
|
|
6688
6691
|
}
|
|
6689
|
-
async function
|
|
6690
|
-
const
|
|
6692
|
+
async function latestCompletedGateRun(repo, client, query) {
|
|
6693
|
+
const res = await client.rest(
|
|
6694
|
+
"GET",
|
|
6695
|
+
`repos/${repo}/actions/workflows/gate.yml/runs?status=completed&per_page=1${query}`,
|
|
6696
|
+
{ timeoutMs: 2e4 }
|
|
6697
|
+
);
|
|
6698
|
+
return res?.workflow_runs?.[0];
|
|
6699
|
+
}
|
|
6700
|
+
async function gateIsProvenGreen(repo, client, baseBranch) {
|
|
6701
|
+
try {
|
|
6702
|
+
const onBase = await latestCompletedGateRun(repo, client, `&branch=${encodeURIComponent(baseBranch)}`);
|
|
6703
|
+
const latest = onBase ?? await latestCompletedGateRun(repo, client, "");
|
|
6704
|
+
return latest?.conclusion === "success";
|
|
6705
|
+
} catch {
|
|
6706
|
+
return false;
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active") {
|
|
6710
|
+
const body = { ...rulesetBody, enforcement };
|
|
6711
|
+
const want = new Set(rulesetRequiredContexts({ rules: body.rules }));
|
|
6691
6712
|
const list = await client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
|
|
6692
6713
|
const existing = findProductRuleset(list ?? []);
|
|
6693
6714
|
if (existing?.id != null) {
|
|
6694
6715
|
const detail = await client.rest("GET", `repos/${repo}/rulesets/${existing.id}`, { timeoutMs: 2e4 });
|
|
6695
6716
|
const have = new Set(rulesetRequiredContexts(detail));
|
|
6696
|
-
if (detail.enforcement ===
|
|
6697
|
-
return { action: "skipped", detail:
|
|
6717
|
+
if (detail.enforcement === enforcement && have.size === want.size && [...want].every((c) => have.has(c))) {
|
|
6718
|
+
return { action: "skipped", enforcement, detail: `${enforcement} ruleset already matches required contexts` };
|
|
6698
6719
|
}
|
|
6699
|
-
await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body
|
|
6700
|
-
return { action: "updated", detail: `ruleset ${existing.id}` };
|
|
6720
|
+
await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body, timeoutMs: 2e4 });
|
|
6721
|
+
return { action: "updated", enforcement, detail: `ruleset ${existing.id}` };
|
|
6701
6722
|
}
|
|
6702
|
-
await client.rest("POST", `repos/${repo}/rulesets`, { body
|
|
6703
|
-
return { action: "created" };
|
|
6723
|
+
await client.rest("POST", `repos/${repo}/rulesets`, { body, timeoutMs: 2e4 });
|
|
6724
|
+
return { action: "created", enforcement };
|
|
6704
6725
|
}
|
|
6705
6726
|
|
|
6706
6727
|
// src/workflow-context.ts
|
|
@@ -8109,6 +8130,32 @@ async function seedRulesetRefIfMissing(repo, deps, derivedVars, baseBranch, resu
|
|
|
8109
8130
|
result.errors.push(`ruleset reference seed failed: ${e.message}`);
|
|
8110
8131
|
}
|
|
8111
8132
|
}
|
|
8133
|
+
async function parkProductRuleset(repo, deps) {
|
|
8134
|
+
const result = { repo, applied: [], skipped: [], errors: [] };
|
|
8135
|
+
try {
|
|
8136
|
+
const list = await deps.client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
|
|
8137
|
+
const existing = findProductRuleset(list ?? []);
|
|
8138
|
+
if (existing?.id == null) {
|
|
8139
|
+
result.skipped.push(`no ${PRODUCT_RULESET_NAME} ruleset on ${repo} \u2014 nothing to park`);
|
|
8140
|
+
return result;
|
|
8141
|
+
}
|
|
8142
|
+
const detail = await deps.client.rest("GET", `repos/${repo}/rulesets/${existing.id}`, { timeoutMs: 2e4 });
|
|
8143
|
+
if (detail.enforcement !== "active") {
|
|
8144
|
+
result.skipped.push(`${PRODUCT_RULESET_NAME} is already ${detail.enforcement ?? "non-enforcing"}`);
|
|
8145
|
+
return result;
|
|
8146
|
+
}
|
|
8147
|
+
await deps.client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, {
|
|
8148
|
+
body: { ...detail, enforcement: "disabled" },
|
|
8149
|
+
timeoutMs: 2e4
|
|
8150
|
+
});
|
|
8151
|
+
result.applied.push(
|
|
8152
|
+
`${PRODUCT_RULESET_NAME} parked (disabled; contexts [${rulesetRequiredContexts(detail).join(", ") || "none"}] kept)`
|
|
8153
|
+
);
|
|
8154
|
+
} catch (e) {
|
|
8155
|
+
result.errors.push(`park failed: ${e.message}`);
|
|
8156
|
+
}
|
|
8157
|
+
return result;
|
|
8158
|
+
}
|
|
8112
8159
|
async function applyCiReconcileRepo(repo, deps) {
|
|
8113
8160
|
const merge = await applyCiReconcileMergeSettings(repo, deps);
|
|
8114
8161
|
const meta = await deps.getProjectMeta(slugFromRepo(repo));
|
|
@@ -8126,6 +8173,12 @@ async function applyCiReconcileRepo(repo, deps) {
|
|
|
8126
8173
|
merge.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
|
|
8127
8174
|
return merge;
|
|
8128
8175
|
}
|
|
8176
|
+
if (!await gateIsProvenGreen(repo, deps.client, "development")) {
|
|
8177
|
+
merge.skipped.push(
|
|
8178
|
+
"product ruleset left non-enforcing \u2014 the gate is not green on development; activating it would block every PR (#3694)"
|
|
8179
|
+
);
|
|
8180
|
+
return merge;
|
|
8181
|
+
}
|
|
8129
8182
|
try {
|
|
8130
8183
|
let body = stripRulesetComment(raw);
|
|
8131
8184
|
if (!driftCheck?.ok) {
|
|
@@ -9359,7 +9412,7 @@ function boardConfigFromProject(meta, floor = {}) {
|
|
|
9359
9412
|
|
|
9360
9413
|
// src/cli-doctor-shared.ts
|
|
9361
9414
|
var import_node_fs15 = require("node:fs");
|
|
9362
|
-
var
|
|
9415
|
+
var import_node_path14 = require("node:path");
|
|
9363
9416
|
var import_node_fs16 = require("node:fs");
|
|
9364
9417
|
|
|
9365
9418
|
// src/readiness-audit.ts
|
|
@@ -9477,6 +9530,193 @@ ${lines.join("\n")}`;
|
|
|
9477
9530
|
|
|
9478
9531
|
// src/secrets.ts
|
|
9479
9532
|
var import_node_child_process6 = require("node:child_process");
|
|
9533
|
+
|
|
9534
|
+
// src/gh-create.ts
|
|
9535
|
+
var import_promises = require("node:fs/promises");
|
|
9536
|
+
var import_node_os3 = require("node:os");
|
|
9537
|
+
var import_node_path13 = require("node:path");
|
|
9538
|
+
var import_node_crypto3 = require("node:crypto");
|
|
9539
|
+
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
9540
|
+
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
9541
|
+
function timeoutKillNote(err, timeoutMs) {
|
|
9542
|
+
if (typeof err !== "object" || err === null || !err.killed) return void 0;
|
|
9543
|
+
return `killed at the ${timeoutMs}ms timeout \u2014 the write may have completed server-side; verify before retrying`;
|
|
9544
|
+
}
|
|
9545
|
+
function normalizePriority(priority) {
|
|
9546
|
+
const p = priority.trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
9547
|
+
if (!CLI_PRIORITIES.includes(p)) {
|
|
9548
|
+
throw new Error(`unknown priority "${priority}" \u2014 expected one of: ${CLI_PRIORITIES.join(", ")}`);
|
|
9549
|
+
}
|
|
9550
|
+
return p;
|
|
9551
|
+
}
|
|
9552
|
+
function parseCreatedUrl(stdout) {
|
|
9553
|
+
const re = /https:\/\/github\.com\/[^\s]+\/(?:issues|pull)\/(\d+)/g;
|
|
9554
|
+
let match;
|
|
9555
|
+
let last;
|
|
9556
|
+
while ((match = re.exec(stdout)) !== null) {
|
|
9557
|
+
last = { number: Number(match[1]), url: match[0] };
|
|
9558
|
+
}
|
|
9559
|
+
if (!last) throw new Error(`could not find a github issue/PR URL in gh output:
|
|
9560
|
+
${stdout.trim() || "(empty)"}`);
|
|
9561
|
+
return last;
|
|
9562
|
+
}
|
|
9563
|
+
function buildIssueArgs({ type, title, body, priority, repo, labels }) {
|
|
9564
|
+
if (!ISSUE_TYPES.includes(type)) throw new Error(`unknown issue type "${type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
9565
|
+
normalizePriority(priority);
|
|
9566
|
+
const args = ["issue", "create"];
|
|
9567
|
+
if (repo) args.push("--repo", repo);
|
|
9568
|
+
args.push("--title", title, "--body", body, "--label", type);
|
|
9569
|
+
for (const label of labels ?? []) args.push("--label", label);
|
|
9570
|
+
return args;
|
|
9571
|
+
}
|
|
9572
|
+
async function ensureLabelsExist(labels, repo, deps = {}) {
|
|
9573
|
+
const run = deps.run ?? execFileP2;
|
|
9574
|
+
for (const label of new Set(labels)) {
|
|
9575
|
+
const args = ["label", "create", label, "--color", "ededed"];
|
|
9576
|
+
if (repo) args.push("--repo", repo);
|
|
9577
|
+
try {
|
|
9578
|
+
await run("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
9579
|
+
} catch {
|
|
9580
|
+
}
|
|
9581
|
+
}
|
|
9582
|
+
}
|
|
9583
|
+
async function bodyArgsViaFile(args, deps = {}) {
|
|
9584
|
+
const i = args.indexOf("--body");
|
|
9585
|
+
if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
|
|
9586
|
+
} };
|
|
9587
|
+
const write = deps.write ?? import_promises.writeFile;
|
|
9588
|
+
const remove2 = deps.remove ?? import_promises.unlink;
|
|
9589
|
+
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
9590
|
+
const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
|
|
9591
|
+
const file = (0, import_node_path13.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
|
|
9592
|
+
await ensureDir((0, import_node_path13.dirname)(file), { recursive: true }).catch(() => {
|
|
9593
|
+
});
|
|
9594
|
+
await write(file, args[i + 1], "utf8");
|
|
9595
|
+
return {
|
|
9596
|
+
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
9597
|
+
cleanup: async () => {
|
|
9598
|
+
try {
|
|
9599
|
+
await remove2(file);
|
|
9600
|
+
} catch {
|
|
9601
|
+
}
|
|
9602
|
+
}
|
|
9603
|
+
};
|
|
9604
|
+
}
|
|
9605
|
+
function buildAddToProjectArgs(projectId, contentId) {
|
|
9606
|
+
if (!projectId) throw new Error("addToProject: projectId is required");
|
|
9607
|
+
if (!contentId) throw new Error("addToProject: contentId is required");
|
|
9608
|
+
return [
|
|
9609
|
+
"api",
|
|
9610
|
+
"graphql",
|
|
9611
|
+
"-f",
|
|
9612
|
+
"query=mutation($p:ID!,$c:ID!){addProjectV2ItemById(input:{projectId:$p,contentId:$c}){item{id}}}",
|
|
9613
|
+
"-f",
|
|
9614
|
+
`p=${projectId}`,
|
|
9615
|
+
"-f",
|
|
9616
|
+
`c=${contentId}`
|
|
9617
|
+
];
|
|
9618
|
+
}
|
|
9619
|
+
function parseAddedItemId(stdout) {
|
|
9620
|
+
try {
|
|
9621
|
+
return JSON.parse(stdout)?.data?.addProjectV2ItemById?.item?.id || void 0;
|
|
9622
|
+
} catch {
|
|
9623
|
+
return void 0;
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9626
|
+
function isAlreadyOnBoardError(stderr) {
|
|
9627
|
+
return /already exists in (?:this|the) project/i.test(stderr);
|
|
9628
|
+
}
|
|
9629
|
+
var CLOSING_KEYWORD = "clos(?:e|es|ed)|fix(?:es|ed)?|resolve(?:s|d)?";
|
|
9630
|
+
var CLOSING_CHAIN = new RegExp(
|
|
9631
|
+
`\\b(${CLOSING_KEYWORD})(\\s+)#(\\d+)((?:\\s*(?:,|,?\\s+and)\\s*#\\d+)+)`,
|
|
9632
|
+
"gi"
|
|
9633
|
+
);
|
|
9634
|
+
function normalizeClosingDirectives(body) {
|
|
9635
|
+
if (!body.includes("#")) return body;
|
|
9636
|
+
const segments = body.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g);
|
|
9637
|
+
return segments.map(
|
|
9638
|
+
(seg, i) => i % 2 === 1 ? seg : seg.replace(
|
|
9639
|
+
CLOSING_CHAIN,
|
|
9640
|
+
(_m, kw, ws, first, tail) => `${kw}${ws}#${first}${tail.replace(/#(\d+)/g, "closes #$1")}`
|
|
9641
|
+
)
|
|
9642
|
+
).join("");
|
|
9643
|
+
}
|
|
9644
|
+
function buildPrArgs({ title, body, base, head, repo, draft }) {
|
|
9645
|
+
const args = ["pr", "create"];
|
|
9646
|
+
if (repo) args.push("--repo", repo);
|
|
9647
|
+
args.push("--title", title, "--body", body);
|
|
9648
|
+
if (base) args.push("--base", base);
|
|
9649
|
+
if (head) args.push("--head", head);
|
|
9650
|
+
if (draft) args.push("--draft");
|
|
9651
|
+
return args;
|
|
9652
|
+
}
|
|
9653
|
+
function parseExistingPr(stderr) {
|
|
9654
|
+
if (!/already exists/i.test(stderr)) return void 0;
|
|
9655
|
+
const match = /https:\/\/\S*\/pull\/(\d+)/.exec(stderr);
|
|
9656
|
+
if (!match) return void 0;
|
|
9657
|
+
return { number: Number(match[1]), url: match[0] };
|
|
9658
|
+
}
|
|
9659
|
+
var GH_CREATE_UPSTREAM_RETRIES = 3;
|
|
9660
|
+
var GH_CREATE_RETRY_BACKOFF_MS = [2e3, 5e3];
|
|
9661
|
+
function httpStatusCodes(stderr) {
|
|
9662
|
+
const out = [];
|
|
9663
|
+
for (const m of stderr.matchAll(/\bHTTP\/?\d*(?:\.\d)?\s+(\d{3})\b/g)) out.push(Number(m[1]));
|
|
9664
|
+
for (const m of stderr.matchAll(/\b(\d{3})\s+Internal Server Error\b/g)) out.push(Number(m[1]));
|
|
9665
|
+
return out;
|
|
9666
|
+
}
|
|
9667
|
+
function isUpstreamGitHubFault(stderr) {
|
|
9668
|
+
const codes = httpStatusCodes(stderr);
|
|
9669
|
+
if (codes.some((c) => c >= 400 && c < 500)) return false;
|
|
9670
|
+
if (codes.some((c) => c >= 500 && c < 600)) return true;
|
|
9671
|
+
return /Something went wrong while executing your query/.test(stderr) || /^\s*unexpected end of JSON input\s*$/m.test(stderr);
|
|
9672
|
+
}
|
|
9673
|
+
function mayRetryCreate(verb) {
|
|
9674
|
+
return verb === "pr";
|
|
9675
|
+
}
|
|
9676
|
+
function upstreamFaultMessage(verb, stderr) {
|
|
9677
|
+
const requestId = /\b([0-9A-F]{4}:[0-9A-F]{4,6}:[0-9A-F]+:[0-9A-F]+:[0-9A-F]+)\b/.exec(stderr)?.[1];
|
|
9678
|
+
const advice = mayRetryCreate(verb) ? "Retrying is the right action" : "The write may have completed server-side before the error \u2014 VERIFY before retrying, or a retry will duplicate it";
|
|
9679
|
+
return `gh ${verb} create failed: GitHub's API is failing (server-side 5xx) \u2014 this is NOT a problem with your diff or arguments. GitHub request ID: ${requestId ?? "unavailable"}. ${advice}; note githubstatus.com may still read green while this is happening.`;
|
|
9680
|
+
}
|
|
9681
|
+
async function ghCreate(args, deps = {}) {
|
|
9682
|
+
const exec = deps.exec ?? execFileP2;
|
|
9683
|
+
const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
9684
|
+
const swapped = await bodyArgsViaFile(args);
|
|
9685
|
+
try {
|
|
9686
|
+
for (let attempt = 1; attempt <= GH_CREATE_UPSTREAM_RETRIES; attempt++) {
|
|
9687
|
+
try {
|
|
9688
|
+
const { stdout } = await exec("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
9689
|
+
return parseCreatedUrl(stdout);
|
|
9690
|
+
} catch (e) {
|
|
9691
|
+
const err = e;
|
|
9692
|
+
const errText = `${err.stderr ?? ""}
|
|
9693
|
+
${err.message ?? ""}`;
|
|
9694
|
+
const faultText = (err.stderr ?? "").trim() ? err.stderr : err.message ?? "";
|
|
9695
|
+
const existing = args[0] === "pr" ? parseExistingPr(errText) : void 0;
|
|
9696
|
+
if (existing) {
|
|
9697
|
+
await swapped.cleanup();
|
|
9698
|
+
console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
|
|
9699
|
+
return { ...existing, existing: true };
|
|
9700
|
+
}
|
|
9701
|
+
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
9702
|
+
if (isUpstreamGitHubFault(faultText) && mayRetryCreate(args[0]) && attempt < GH_CREATE_UPSTREAM_RETRIES) {
|
|
9703
|
+
const waitMs = GH_CREATE_RETRY_BACKOFF_MS[attempt - 1];
|
|
9704
|
+
console.warn(`gh ${args[0]} create: GitHub API server fault (attempt ${attempt}/${GH_CREATE_UPSTREAM_RETRIES}) \u2014 retrying in ${waitMs}ms`);
|
|
9705
|
+
await sleep3(waitMs);
|
|
9706
|
+
continue;
|
|
9707
|
+
}
|
|
9708
|
+
await swapped.cleanup();
|
|
9709
|
+
if (isUpstreamGitHubFault(faultText)) return fail(upstreamFaultMessage(args[0], faultText));
|
|
9710
|
+
return fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
9711
|
+
}
|
|
9712
|
+
}
|
|
9713
|
+
throw new Error("ghCreate: retry loop exhausted without a verdict");
|
|
9714
|
+
} finally {
|
|
9715
|
+
await swapped.cleanup();
|
|
9716
|
+
}
|
|
9717
|
+
}
|
|
9718
|
+
|
|
9719
|
+
// src/secrets.ts
|
|
9480
9720
|
var OWNER = "mutmutco";
|
|
9481
9721
|
var SSM_ROOT = "/mmi-future";
|
|
9482
9722
|
var PROJECT_TIER_SEGMENT = "dev";
|
|
@@ -10105,6 +10345,15 @@ async function secretsRequest(deps, key, opts) {
|
|
|
10105
10345
|
deps.err(`invalid secret key ${JSON.stringify(key)}`);
|
|
10106
10346
|
return false;
|
|
10107
10347
|
}
|
|
10348
|
+
let priority = "medium";
|
|
10349
|
+
if (opts.priority !== void 0) {
|
|
10350
|
+
try {
|
|
10351
|
+
priority = normalizePriority(opts.priority);
|
|
10352
|
+
} catch (e) {
|
|
10353
|
+
deps.err(`secrets request: ${e.message}`);
|
|
10354
|
+
return false;
|
|
10355
|
+
}
|
|
10356
|
+
}
|
|
10108
10357
|
const repo = await targetRepo(deps, opts);
|
|
10109
10358
|
const requester = await probeCapabilities(deps, repo);
|
|
10110
10359
|
if (requester?.role === "master") {
|
|
@@ -10122,7 +10371,7 @@ async function secretsRequest(deps, key, opts) {
|
|
|
10122
10371
|
body: JSON.stringify({
|
|
10123
10372
|
repo,
|
|
10124
10373
|
key,
|
|
10125
|
-
priority
|
|
10374
|
+
priority,
|
|
10126
10375
|
reason: opts.reason,
|
|
10127
10376
|
context: opts.context
|
|
10128
10377
|
}),
|
|
@@ -10749,7 +10998,7 @@ async function localBranchHeads() {
|
|
|
10749
10998
|
}
|
|
10750
10999
|
async function currentRepoWorktreeGitRoot(repoRoot2) {
|
|
10751
11000
|
const gitCommonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
10752
|
-
return gitCommonDir ? (0,
|
|
11001
|
+
return gitCommonDir ? (0, import_node_path14.resolve)(repoRoot2, gitCommonDir, "worktrees") : "";
|
|
10753
11002
|
}
|
|
10754
11003
|
async function worktreeBranches() {
|
|
10755
11004
|
const { stdout } = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
@@ -10769,7 +11018,7 @@ function resolveGitdirForWorktreeFile(worktreePath, content) {
|
|
|
10769
11018
|
const match = /^gitdir:\s*(.+)\s*$/im.exec(content);
|
|
10770
11019
|
if (!match?.[1]) return void 0;
|
|
10771
11020
|
const raw = match[1].trim();
|
|
10772
|
-
return (0,
|
|
11021
|
+
return (0, import_node_path14.isAbsolute)(raw) ? raw : (0, import_node_path14.resolve)(worktreePath, raw);
|
|
10773
11022
|
}
|
|
10774
11023
|
function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
|
|
10775
11024
|
if (!worktreeGitRoot) return false;
|
|
@@ -10778,9 +11027,9 @@ function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
|
|
|
10778
11027
|
for (const ent of entries) {
|
|
10779
11028
|
if (!ent.isDirectory()) continue;
|
|
10780
11029
|
try {
|
|
10781
|
-
const gitdirPath = (0, import_node_fs15.readFileSync)((0,
|
|
10782
|
-
const resolvedGitdir = (0,
|
|
10783
|
-
if (sameWorktreeMetadataPath((0,
|
|
11030
|
+
const gitdirPath = (0, import_node_fs15.readFileSync)((0, import_node_path14.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
|
|
11031
|
+
const resolvedGitdir = (0, import_node_path14.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path14.resolve)(worktreeGitRoot, ent.name, gitdirPath);
|
|
11032
|
+
if (sameWorktreeMetadataPath((0, import_node_path14.dirname)(resolvedGitdir), worktreePath)) return true;
|
|
10784
11033
|
} catch {
|
|
10785
11034
|
}
|
|
10786
11035
|
}
|
|
@@ -10799,7 +11048,7 @@ function pathExistsKnown(path2) {
|
|
|
10799
11048
|
}
|
|
10800
11049
|
}
|
|
10801
11050
|
function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
|
|
10802
|
-
const gitPath = (0,
|
|
11051
|
+
const gitPath = (0, import_node_path14.join)(path2, ".git");
|
|
10803
11052
|
let st;
|
|
10804
11053
|
try {
|
|
10805
11054
|
st = (0, import_node_fs16.lstatSync)(gitPath);
|
|
@@ -10853,7 +11102,7 @@ async function preservedBranches() {
|
|
|
10853
11102
|
async function siblingWorktreeDirs(explicitRoot) {
|
|
10854
11103
|
const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
10855
11104
|
const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
|
|
10856
|
-
const primaryRepoRoot = worktreeGitRoot ? (0,
|
|
11105
|
+
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path14.dirname)((0, import_node_path14.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
10857
11106
|
try {
|
|
10858
11107
|
const dirs = explicitRoot ? listDirsIn(resolveExplicitScanRoot(explicitRoot, primaryRepoRoot)) : worktreeScanDirs(siblingMmiWorktreesRoot(primaryRepoRoot), primaryRepoRoot, listDirsIn, isRepoCheckoutDir);
|
|
10859
11108
|
return dirs.map((dir) => inspectSiblingWorktreeDir(dir, worktreeGitRoot)).filter((entry) => Boolean(entry));
|
|
@@ -10863,13 +11112,13 @@ async function siblingWorktreeDirs(explicitRoot) {
|
|
|
10863
11112
|
}
|
|
10864
11113
|
function listDirsIn(dir) {
|
|
10865
11114
|
try {
|
|
10866
|
-
return (0, import_node_fs16.readdirSync)(dir, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => (0,
|
|
11115
|
+
return (0, import_node_fs16.readdirSync)(dir, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => (0, import_node_path14.join)(dir, ent.name));
|
|
10867
11116
|
} catch {
|
|
10868
11117
|
return [];
|
|
10869
11118
|
}
|
|
10870
11119
|
}
|
|
10871
11120
|
function isRepoCheckoutDir(dir) {
|
|
10872
|
-
return (0, import_node_fs16.existsSync)((0,
|
|
11121
|
+
return (0, import_node_fs16.existsSync)((0, import_node_path14.join)(dir, ".git"));
|
|
10873
11122
|
}
|
|
10874
11123
|
function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
|
|
10875
11124
|
let rootDirs;
|
|
@@ -10940,7 +11189,7 @@ function measureWorktreeRoot(root, deadline) {
|
|
|
10940
11189
|
continue;
|
|
10941
11190
|
}
|
|
10942
11191
|
for (const ent of entries) {
|
|
10943
|
-
const child2 = (0,
|
|
11192
|
+
const child2 = (0, import_node_path14.join)(current, ent.name);
|
|
10944
11193
|
if (ent.isDirectory()) {
|
|
10945
11194
|
if (depth0) dirs++;
|
|
10946
11195
|
let isLink = false;
|
|
@@ -10963,7 +11212,7 @@ function measureWorktreeRoot(root, deadline) {
|
|
|
10963
11212
|
}
|
|
10964
11213
|
function worktreeRootsProbe(repoRoot2) {
|
|
10965
11214
|
const authoritative = siblingMmiWorktreesRoot(repoRoot2);
|
|
10966
|
-
const container = (0,
|
|
11215
|
+
const container = (0, import_node_path14.dirname)(authoritative);
|
|
10967
11216
|
let names;
|
|
10968
11217
|
try {
|
|
10969
11218
|
names = (0, import_node_fs16.readdirSync)(container, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => ent.name);
|
|
@@ -12094,191 +12343,6 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
|
|
|
12094
12343
|
return { projectItemId };
|
|
12095
12344
|
}
|
|
12096
12345
|
|
|
12097
|
-
// src/gh-create.ts
|
|
12098
|
-
var import_promises = require("node:fs/promises");
|
|
12099
|
-
var import_node_os3 = require("node:os");
|
|
12100
|
-
var import_node_path14 = require("node:path");
|
|
12101
|
-
var import_node_crypto3 = require("node:crypto");
|
|
12102
|
-
var ISSUE_TYPES = ["bug", "feature", "task"];
|
|
12103
|
-
var GH_MUTATION_TIMEOUT_MS = 12e4;
|
|
12104
|
-
function timeoutKillNote(err, timeoutMs) {
|
|
12105
|
-
if (typeof err !== "object" || err === null || !err.killed) return void 0;
|
|
12106
|
-
return `killed at the ${timeoutMs}ms timeout \u2014 the write may have completed server-side; verify before retrying`;
|
|
12107
|
-
}
|
|
12108
|
-
function normalizePriority(priority) {
|
|
12109
|
-
const p = priority.trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
12110
|
-
if (!CLI_PRIORITIES.includes(p)) {
|
|
12111
|
-
throw new Error(`unknown priority "${priority}" \u2014 expected one of: ${CLI_PRIORITIES.join(", ")}`);
|
|
12112
|
-
}
|
|
12113
|
-
return p;
|
|
12114
|
-
}
|
|
12115
|
-
function parseCreatedUrl(stdout) {
|
|
12116
|
-
const re = /https:\/\/github\.com\/[^\s]+\/(?:issues|pull)\/(\d+)/g;
|
|
12117
|
-
let match;
|
|
12118
|
-
let last;
|
|
12119
|
-
while ((match = re.exec(stdout)) !== null) {
|
|
12120
|
-
last = { number: Number(match[1]), url: match[0] };
|
|
12121
|
-
}
|
|
12122
|
-
if (!last) throw new Error(`could not find a github issue/PR URL in gh output:
|
|
12123
|
-
${stdout.trim() || "(empty)"}`);
|
|
12124
|
-
return last;
|
|
12125
|
-
}
|
|
12126
|
-
function buildIssueArgs({ type, title, body, priority, repo, labels }) {
|
|
12127
|
-
if (!ISSUE_TYPES.includes(type)) throw new Error(`unknown issue type "${type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
12128
|
-
normalizePriority(priority);
|
|
12129
|
-
const args = ["issue", "create"];
|
|
12130
|
-
if (repo) args.push("--repo", repo);
|
|
12131
|
-
args.push("--title", title, "--body", body, "--label", type);
|
|
12132
|
-
for (const label of labels ?? []) args.push("--label", label);
|
|
12133
|
-
return args;
|
|
12134
|
-
}
|
|
12135
|
-
async function ensureLabelsExist(labels, repo, deps = {}) {
|
|
12136
|
-
const run = deps.run ?? execFileP2;
|
|
12137
|
-
for (const label of new Set(labels)) {
|
|
12138
|
-
const args = ["label", "create", label, "--color", "ededed"];
|
|
12139
|
-
if (repo) args.push("--repo", repo);
|
|
12140
|
-
try {
|
|
12141
|
-
await run("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
12142
|
-
} catch {
|
|
12143
|
-
}
|
|
12144
|
-
}
|
|
12145
|
-
}
|
|
12146
|
-
async function bodyArgsViaFile(args, deps = {}) {
|
|
12147
|
-
const i = args.indexOf("--body");
|
|
12148
|
-
if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
|
|
12149
|
-
} };
|
|
12150
|
-
const write = deps.write ?? import_promises.writeFile;
|
|
12151
|
-
const remove2 = deps.remove ?? import_promises.unlink;
|
|
12152
|
-
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
12153
|
-
const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
|
|
12154
|
-
const file = (0, import_node_path14.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
|
|
12155
|
-
await ensureDir((0, import_node_path14.dirname)(file), { recursive: true }).catch(() => {
|
|
12156
|
-
});
|
|
12157
|
-
await write(file, args[i + 1], "utf8");
|
|
12158
|
-
return {
|
|
12159
|
-
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
12160
|
-
cleanup: async () => {
|
|
12161
|
-
try {
|
|
12162
|
-
await remove2(file);
|
|
12163
|
-
} catch {
|
|
12164
|
-
}
|
|
12165
|
-
}
|
|
12166
|
-
};
|
|
12167
|
-
}
|
|
12168
|
-
function buildAddToProjectArgs(projectId, contentId) {
|
|
12169
|
-
if (!projectId) throw new Error("addToProject: projectId is required");
|
|
12170
|
-
if (!contentId) throw new Error("addToProject: contentId is required");
|
|
12171
|
-
return [
|
|
12172
|
-
"api",
|
|
12173
|
-
"graphql",
|
|
12174
|
-
"-f",
|
|
12175
|
-
"query=mutation($p:ID!,$c:ID!){addProjectV2ItemById(input:{projectId:$p,contentId:$c}){item{id}}}",
|
|
12176
|
-
"-f",
|
|
12177
|
-
`p=${projectId}`,
|
|
12178
|
-
"-f",
|
|
12179
|
-
`c=${contentId}`
|
|
12180
|
-
];
|
|
12181
|
-
}
|
|
12182
|
-
function parseAddedItemId(stdout) {
|
|
12183
|
-
try {
|
|
12184
|
-
return JSON.parse(stdout)?.data?.addProjectV2ItemById?.item?.id || void 0;
|
|
12185
|
-
} catch {
|
|
12186
|
-
return void 0;
|
|
12187
|
-
}
|
|
12188
|
-
}
|
|
12189
|
-
function isAlreadyOnBoardError(stderr) {
|
|
12190
|
-
return /already exists in (?:this|the) project/i.test(stderr);
|
|
12191
|
-
}
|
|
12192
|
-
var CLOSING_KEYWORD = "clos(?:e|es|ed)|fix(?:es|ed)?|resolve(?:s|d)?";
|
|
12193
|
-
var CLOSING_CHAIN = new RegExp(
|
|
12194
|
-
`\\b(${CLOSING_KEYWORD})(\\s+)#(\\d+)((?:\\s*(?:,|,?\\s+and)\\s*#\\d+)+)`,
|
|
12195
|
-
"gi"
|
|
12196
|
-
);
|
|
12197
|
-
function normalizeClosingDirectives(body) {
|
|
12198
|
-
if (!body.includes("#")) return body;
|
|
12199
|
-
const segments = body.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g);
|
|
12200
|
-
return segments.map(
|
|
12201
|
-
(seg, i) => i % 2 === 1 ? seg : seg.replace(
|
|
12202
|
-
CLOSING_CHAIN,
|
|
12203
|
-
(_m, kw, ws, first, tail) => `${kw}${ws}#${first}${tail.replace(/#(\d+)/g, "closes #$1")}`
|
|
12204
|
-
)
|
|
12205
|
-
).join("");
|
|
12206
|
-
}
|
|
12207
|
-
function buildPrArgs({ title, body, base, head, repo, draft }) {
|
|
12208
|
-
const args = ["pr", "create"];
|
|
12209
|
-
if (repo) args.push("--repo", repo);
|
|
12210
|
-
args.push("--title", title, "--body", body);
|
|
12211
|
-
if (base) args.push("--base", base);
|
|
12212
|
-
if (head) args.push("--head", head);
|
|
12213
|
-
if (draft) args.push("--draft");
|
|
12214
|
-
return args;
|
|
12215
|
-
}
|
|
12216
|
-
function parseExistingPr(stderr) {
|
|
12217
|
-
if (!/already exists/i.test(stderr)) return void 0;
|
|
12218
|
-
const match = /https:\/\/\S*\/pull\/(\d+)/.exec(stderr);
|
|
12219
|
-
if (!match) return void 0;
|
|
12220
|
-
return { number: Number(match[1]), url: match[0] };
|
|
12221
|
-
}
|
|
12222
|
-
var GH_CREATE_UPSTREAM_RETRIES = 3;
|
|
12223
|
-
var GH_CREATE_RETRY_BACKOFF_MS = [2e3, 5e3];
|
|
12224
|
-
function httpStatusCodes(stderr) {
|
|
12225
|
-
const out = [];
|
|
12226
|
-
for (const m of stderr.matchAll(/\bHTTP\/?\d*(?:\.\d)?\s+(\d{3})\b/g)) out.push(Number(m[1]));
|
|
12227
|
-
for (const m of stderr.matchAll(/\b(\d{3})\s+Internal Server Error\b/g)) out.push(Number(m[1]));
|
|
12228
|
-
return out;
|
|
12229
|
-
}
|
|
12230
|
-
function isUpstreamGitHubFault(stderr) {
|
|
12231
|
-
const codes = httpStatusCodes(stderr);
|
|
12232
|
-
if (codes.some((c) => c >= 400 && c < 500)) return false;
|
|
12233
|
-
if (codes.some((c) => c >= 500 && c < 600)) return true;
|
|
12234
|
-
return /Something went wrong while executing your query/.test(stderr) || /^\s*unexpected end of JSON input\s*$/m.test(stderr);
|
|
12235
|
-
}
|
|
12236
|
-
function mayRetryCreate(verb) {
|
|
12237
|
-
return verb === "pr";
|
|
12238
|
-
}
|
|
12239
|
-
function upstreamFaultMessage(verb, stderr) {
|
|
12240
|
-
const requestId = /\b([0-9A-F]{4}:[0-9A-F]{4,6}:[0-9A-F]+:[0-9A-F]+:[0-9A-F]+)\b/.exec(stderr)?.[1];
|
|
12241
|
-
const advice = mayRetryCreate(verb) ? "Retrying is the right action" : "The write may have completed server-side before the error \u2014 VERIFY before retrying, or a retry will duplicate it";
|
|
12242
|
-
return `gh ${verb} create failed: GitHub's API is failing (server-side 5xx) \u2014 this is NOT a problem with your diff or arguments. GitHub request ID: ${requestId ?? "unavailable"}. ${advice}; note githubstatus.com may still read green while this is happening.`;
|
|
12243
|
-
}
|
|
12244
|
-
async function ghCreate(args, deps = {}) {
|
|
12245
|
-
const exec = deps.exec ?? execFileP2;
|
|
12246
|
-
const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
12247
|
-
const swapped = await bodyArgsViaFile(args);
|
|
12248
|
-
try {
|
|
12249
|
-
for (let attempt = 1; attempt <= GH_CREATE_UPSTREAM_RETRIES; attempt++) {
|
|
12250
|
-
try {
|
|
12251
|
-
const { stdout } = await exec("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
12252
|
-
return parseCreatedUrl(stdout);
|
|
12253
|
-
} catch (e) {
|
|
12254
|
-
const err = e;
|
|
12255
|
-
const errText = `${err.stderr ?? ""}
|
|
12256
|
-
${err.message ?? ""}`;
|
|
12257
|
-
const faultText = (err.stderr ?? "").trim() ? err.stderr : err.message ?? "";
|
|
12258
|
-
const existing = args[0] === "pr" ? parseExistingPr(errText) : void 0;
|
|
12259
|
-
if (existing) {
|
|
12260
|
-
await swapped.cleanup();
|
|
12261
|
-
console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
|
|
12262
|
-
return { ...existing, existing: true };
|
|
12263
|
-
}
|
|
12264
|
-
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
12265
|
-
if (isUpstreamGitHubFault(faultText) && mayRetryCreate(args[0]) && attempt < GH_CREATE_UPSTREAM_RETRIES) {
|
|
12266
|
-
const waitMs = GH_CREATE_RETRY_BACKOFF_MS[attempt - 1];
|
|
12267
|
-
console.warn(`gh ${args[0]} create: GitHub API server fault (attempt ${attempt}/${GH_CREATE_UPSTREAM_RETRIES}) \u2014 retrying in ${waitMs}ms`);
|
|
12268
|
-
await sleep3(waitMs);
|
|
12269
|
-
continue;
|
|
12270
|
-
}
|
|
12271
|
-
await swapped.cleanup();
|
|
12272
|
-
if (isUpstreamGitHubFault(faultText)) return fail(upstreamFaultMessage(args[0], faultText));
|
|
12273
|
-
return fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
12274
|
-
}
|
|
12275
|
-
}
|
|
12276
|
-
throw new Error("ghCreate: retry loop exhausted without a verdict");
|
|
12277
|
-
} finally {
|
|
12278
|
-
await swapped.cleanup();
|
|
12279
|
-
}
|
|
12280
|
-
}
|
|
12281
|
-
|
|
12282
12346
|
// src/issue-body.ts
|
|
12283
12347
|
var import_node_os4 = require("node:os");
|
|
12284
12348
|
var TextArgError = class extends Error {
|
|
@@ -12684,6 +12748,32 @@ function withExamples(cmd, examples, note) {
|
|
|
12684
12748
|
cmd.addHelpText("after", lines.join("\n") + "\n");
|
|
12685
12749
|
return cmd;
|
|
12686
12750
|
}
|
|
12751
|
+
var DECLARED_OPTIONS = /* @__PURE__ */ Symbol.for("mmi.commandManifest.declaredOptions");
|
|
12752
|
+
function withDeclaredOptions(cmd, options, footer) {
|
|
12753
|
+
cmd[DECLARED_OPTIONS] = options;
|
|
12754
|
+
const width = Math.max(...options.map((o) => o.flags.length));
|
|
12755
|
+
const lines = ["", "Options:", ...options.map((o) => ` ${o.flags.padEnd(width)} ${o.description}`)];
|
|
12756
|
+
if (footer?.length) lines.push("", ...footer);
|
|
12757
|
+
cmd.addHelpText("after", lines.join("\n") + "\n");
|
|
12758
|
+
return cmd;
|
|
12759
|
+
}
|
|
12760
|
+
function readDeclaredOptions(cmd) {
|
|
12761
|
+
const declared = cmd[DECLARED_OPTIONS];
|
|
12762
|
+
if (!Array.isArray(declared)) return [];
|
|
12763
|
+
return declared.map((o) => {
|
|
12764
|
+
const takesValue = /[<[]/.test(o.flags);
|
|
12765
|
+
return {
|
|
12766
|
+
flags: o.flags,
|
|
12767
|
+
description: o.description,
|
|
12768
|
+
mandatory: false,
|
|
12769
|
+
valueRequired: /</.test(o.flags),
|
|
12770
|
+
optional: /\[/.test(o.flags),
|
|
12771
|
+
takesValue,
|
|
12772
|
+
variadic: o.flags.includes("..."),
|
|
12773
|
+
negate: o.flags.includes("--no-")
|
|
12774
|
+
};
|
|
12775
|
+
});
|
|
12776
|
+
}
|
|
12687
12777
|
function readExamples(cmd) {
|
|
12688
12778
|
const ex = cmd[EXAMPLES];
|
|
12689
12779
|
return Array.isArray(ex) && ex.length ? ex : void 0;
|
|
@@ -12723,12 +12813,14 @@ function buildCommand(cmd, path2) {
|
|
|
12723
12813
|
name: cmd.name(),
|
|
12724
12814
|
path: path2,
|
|
12725
12815
|
arguments: cmd.registeredArguments.map(buildArgument),
|
|
12726
|
-
options
|
|
12816
|
+
// A hand-parsed command registers no Commander options, so its declared set is merged in (#3682).
|
|
12817
|
+
options: [...cmd.options.map(buildOption), ...readDeclaredOptions(cmd)],
|
|
12727
12818
|
subcommands: cmd.commands.map(
|
|
12728
12819
|
(child2) => buildCommand(child2, path2 ? `${path2} ${child2.name()}` : child2.name())
|
|
12729
12820
|
),
|
|
12730
12821
|
...metadata
|
|
12731
12822
|
};
|
|
12823
|
+
if (cmd._allowUnknownOption) out.parses_own_argv = true;
|
|
12732
12824
|
const description = cmd.description();
|
|
12733
12825
|
if (description) out.description = description;
|
|
12734
12826
|
const examples = readExamples(cmd);
|
|
@@ -16021,6 +16113,22 @@ async function auditTrainBranch(repo, branch, owners, deps, projectAdmins = /* @
|
|
|
16021
16113
|
restrictions = null;
|
|
16022
16114
|
}
|
|
16023
16115
|
if (!restrictions) {
|
|
16116
|
+
let branchExists = true;
|
|
16117
|
+
try {
|
|
16118
|
+
branchExists = Boolean(await deps.client.rest("GET", `repos/${repo}/branches/${branch}`));
|
|
16119
|
+
} catch {
|
|
16120
|
+
branchExists = false;
|
|
16121
|
+
}
|
|
16122
|
+
if (!branchExists) {
|
|
16123
|
+
return [{
|
|
16124
|
+
repo,
|
|
16125
|
+
branch,
|
|
16126
|
+
kind: "train-branch-missing",
|
|
16127
|
+
severity: "medium",
|
|
16128
|
+
detail: `${branch} is in this repo's locked train set but does not exist \u2014 either the branch was never created, or the repo's releaseTrack does not actually include it`,
|
|
16129
|
+
remediation: `create it: gh api -X POST repos/${repo}/git/refs -f ref=refs/heads/${branch} -f sha=<development sha>, then initialize the lock (docs/Guides/repo-access.md). If the repo has no ${branch} stage, set its real track instead: mmi-cli org project set --var releaseTrack=<direct|trunk>`
|
|
16130
|
+
}];
|
|
16131
|
+
}
|
|
16024
16132
|
return [{
|
|
16025
16133
|
repo,
|
|
16026
16134
|
branch,
|
|
@@ -18124,17 +18232,7 @@ function registerSecretsCommands(program3) {
|
|
|
18124
18232
|
if (!ok) process.exitCode = 1;
|
|
18125
18233
|
}));
|
|
18126
18234
|
secrets.command("rm <key>").description("remove a secret from your own full project vault; org-infra requires master or an exact grant. --slug _org removes an org-infra <provider>/<KEY> value (#3315)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for the value removal \u2014 master or an exact grant (#3315)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
|
|
18127
|
-
secrets.command("use").usage("<key> | --key <key> [--key <key>\u2026] [-- <command>] [--repo <owner/repo>] [--slug <slug>] [--name <ENVVAR>]").argument("[args...]", "one secret key (or use --key), then `--` and the command to run").description("consume secrets KEYLESS: own-project full tree or exactly granted org-infra keys; injects each into the command env and never prints one. ONE positional key, or several via repeated `--key` (#3561). The wrapped command keeps its OWN flags (`-- node -e \u2026`, `-- curl -H \u2026`) with or without the `--`, because npm's PowerShell shim swallows `--` before the CLI sees it (#3436)").allowUnknownOption().helpOption(false).
|
|
18128
|
-
"",
|
|
18129
|
-
"Options:",
|
|
18130
|
-
" --key <key> a secret key; repeat for several. The only form no shell can misread \u2014",
|
|
18131
|
-
" `npm` and `slack/token` are BOTH valid key names and plausible commands.",
|
|
18132
|
-
" --repo <owner/repo> target repo (defaults to the current repo)",
|
|
18133
|
-
" --slug <slug> org-infra namespace (e.g. _org) for a granted org secret",
|
|
18134
|
-
" --name <ENVVAR> env var to inject under, single key only (default: the key leaf, UPPER_SNAKE)",
|
|
18135
|
-
"",
|
|
18136
|
-
"These belong BEFORE the `--`. Everything after it is the wrapped command, untouched."
|
|
18137
|
-
].join("\n")).action(function() {
|
|
18235
|
+
secrets.command("use").usage("<key> | --key <key> [--key <key>\u2026] [-- <command>] [--repo <owner/repo>] [--slug <slug>] [--name <ENVVAR>]").argument("[args...]", "one secret key (or use --key), then `--` and the command to run").description("consume secrets KEYLESS: own-project full tree or exactly granted org-infra keys; injects each into the command env and never prints one. ONE positional key, or several via repeated `--key` (#3561). The wrapped command keeps its OWN flags (`-- node -e \u2026`, `-- curl -H \u2026`) with or without the `--`, because npm's PowerShell shim swallows `--` before the CLI sees it (#3436)").allowUnknownOption().helpOption(false).action(function() {
|
|
18138
18236
|
const parsed = parseSecretsUseArgv(secretsUseTail(process.argv));
|
|
18139
18237
|
if (parsed.kind === "help") return this.outputHelp();
|
|
18140
18238
|
return withSecrets(async (d) => {
|
|
@@ -18148,6 +18246,16 @@ function registerSecretsCommands(program3) {
|
|
|
18148
18246
|
if (ok === false) process.exitCode = 1;
|
|
18149
18247
|
});
|
|
18150
18248
|
});
|
|
18249
|
+
withDeclaredOptions(
|
|
18250
|
+
secrets.commands.find((c) => c.name() === "use"),
|
|
18251
|
+
[
|
|
18252
|
+
{ flags: "--key <key>", description: "a secret key; repeat for several. The only form no shell can misread \u2014 `npm` and `slack/token` are BOTH valid key names and plausible commands." },
|
|
18253
|
+
{ flags: "--repo <owner/repo>", description: "target repo (defaults to the current repo)" },
|
|
18254
|
+
{ flags: "--slug <slug>", description: "org-infra namespace (e.g. _org) for a granted org secret" },
|
|
18255
|
+
{ flags: "--name <ENVVAR>", description: "env var to inject under, single key only (default: the key leaf, UPPER_SNAKE)" }
|
|
18256
|
+
],
|
|
18257
|
+
["These belong BEFORE the `--`. Everything after it is the wrapped command, untouched."]
|
|
18258
|
+
);
|
|
18151
18259
|
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to an org-infra secret. Default is read/write on one exact key; --read grants keyless USE only, and only --read accepts a wildcard key (`*` = the whole namespace, `<provider>/*` = one provider group) (#3652)").option("--read", "read-only: permits keyless `secrets use`, never `set`/`rm`. Required for a wildcard key.").action((repo, login, key, o) => withSecrets((d) => secretsGrant(d, repo, login, key, { read: o.read })));
|
|
18152
18260
|
secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-infra secret access (pass the grant key exactly as granted, wildcard included)").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
|
|
18153
18261
|
}
|
|
@@ -19901,7 +20009,7 @@ function registerQueryCommands(program3) {
|
|
|
19901
20009
|
const pr2 = program3.commands.find((c) => c.name() === "pr");
|
|
19902
20010
|
if (!issue2 || !pr2) return;
|
|
19903
20011
|
const deps = queryDeps();
|
|
19904
|
-
issue2.command("list").description("bounded issue read \u2014 filter by label/state/assignee (NO free-text search); always prints JSON").option("--label <label>", "filter by label").
|
|
20012
|
+
issue2.command("list").description("bounded issue read \u2014 filter by label/state/assignee (NO free-text search); always prints JSON").option("--label <label>", "filter by label").addOption(new Option("--state <state>", "open | closed | all").default("open").choices(ISSUE_STATES)).option("--assignee <login>", "filter by assignee login").option("--limit <n>", "max issues to return (capped at 100)", (v) => Number(v), DEFAULT_LIMIT).option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output (already the default \u2014 accepted for contract uniformity)").action(async (o) => {
|
|
19905
20013
|
try {
|
|
19906
20014
|
const state = validateEnum("--state", ISSUE_STATES, o.state, "issue list");
|
|
19907
20015
|
const rows = await runIssueList(deps, { label: o.label, state, assignee: o.assignee, limit: o.limit, repo: o.repo });
|
|
@@ -19920,7 +20028,7 @@ function registerQueryCommands(program3) {
|
|
|
19920
20028
|
queryFail("issue children", e);
|
|
19921
20029
|
}
|
|
19922
20030
|
});
|
|
19923
|
-
pr2.command("list").description("bounded PR read \u2014 --mine scopes to the viewer's own PRs (resolves the viewer via gh); always prints JSON").option("--mine", "only the caller's own PRs (resolves the viewer via `gh api user`)").
|
|
20031
|
+
pr2.command("list").description("bounded PR read \u2014 --mine scopes to the viewer's own PRs (resolves the viewer via gh); always prints JSON").option("--mine", "only the caller's own PRs (resolves the viewer via `gh api user`)").addOption(new Option("--state <state>", "open | all").default("open").choices(PR_STATES)).option("--limit <n>", "max PRs to return (capped at 100)", (v) => Number(v), DEFAULT_LIMIT).option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output (already the default \u2014 accepted for contract uniformity)").action(async (o) => {
|
|
19924
20032
|
try {
|
|
19925
20033
|
const state = validateEnum("--state", PR_STATES, o.state, "pr list");
|
|
19926
20034
|
const rows = await runPrList(deps, { mine: o.mine, state, limit: o.limit, repo: o.repo });
|
|
@@ -20071,6 +20179,21 @@ async function rulesetDetails2(deps, repo, list) {
|
|
|
20071
20179
|
}
|
|
20072
20180
|
return details;
|
|
20073
20181
|
}
|
|
20182
|
+
async function orgRulesetExclusion(deps, repo) {
|
|
20183
|
+
const [owner, name] = repo.split("/");
|
|
20184
|
+
if (!owner || !name) return null;
|
|
20185
|
+
const list = await restJson3(deps, `orgs/${owner}/rulesets`, []);
|
|
20186
|
+
const excluding = [];
|
|
20187
|
+
for (const ruleset of list) {
|
|
20188
|
+
if (ruleset.id == null || ruleset.target !== "branch" || ruleset.enforcement !== "active") continue;
|
|
20189
|
+
const full = await restJson3(deps, `orgs/${owner}/rulesets/${ruleset.id}`, null);
|
|
20190
|
+
const excluded = full?.conditions?.repository_name?.exclude ?? [];
|
|
20191
|
+
if (excluded.some((entry) => entry.toLowerCase() === name.toLowerCase())) {
|
|
20192
|
+
excluding.push(full?.name ?? String(ruleset.id));
|
|
20193
|
+
}
|
|
20194
|
+
}
|
|
20195
|
+
return excluding.length ? excluding.join(", ") : null;
|
|
20196
|
+
}
|
|
20074
20197
|
function repoRefsMatch(a, b) {
|
|
20075
20198
|
return a.toLowerCase() === b.toLowerCase();
|
|
20076
20199
|
}
|
|
@@ -20134,11 +20257,12 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
20134
20257
|
});
|
|
20135
20258
|
try {
|
|
20136
20259
|
const owners = new Set(await resolveOwners(deps));
|
|
20137
|
-
const
|
|
20260
|
+
const sanctioned = new Set(deps.sanctionedAdmins ?? []);
|
|
20261
|
+
const overgrants = (await auditRepoCollaborators(repo, owners, deps, /* @__PURE__ */ new Set(), sanctioned)).filter((f) => f.kind === "collaborator-overgrant");
|
|
20138
20262
|
checks.push({
|
|
20139
20263
|
ok: overgrants.length === 0,
|
|
20140
20264
|
label: "collaborator roles are master-only (no admin/maintain over-grant)",
|
|
20141
|
-
detail: overgrants.length ? `over-granted: ${overgrants.map((f) => f.actor).join(", ")}` : void 0
|
|
20265
|
+
detail: overgrants.length ? `over-granted: ${overgrants.map((f) => f.actor).join(", ")}` : sanctioned.size ? `sanctioned admin: ${[...sanctioned].join(", ")} (access-matrix.json, an owner decision)` : void 0
|
|
20142
20266
|
});
|
|
20143
20267
|
} catch {
|
|
20144
20268
|
}
|
|
@@ -20314,10 +20438,11 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
|
|
|
20314
20438
|
);
|
|
20315
20439
|
const orgRuleset = activeOrgRulesets.find((ruleset) => missingRuleTypes(ruleset, requiredOrgRulesetTypes).length === 0);
|
|
20316
20440
|
const missingOrgRuleTypes = activeOrgRulesets.length === 0 ? requiredOrgRulesetTypes : missingRuleTypes(activeOrgRulesets[0], requiredOrgRulesetTypes);
|
|
20441
|
+
const excludedByOrgRuleset = orgRuleset ? null : await orgRulesetExclusion(deps, repo);
|
|
20317
20442
|
checks.push({
|
|
20318
|
-
ok: Boolean(orgRuleset),
|
|
20443
|
+
ok: Boolean(orgRuleset) || Boolean(excludedByOrgRuleset),
|
|
20319
20444
|
label: "covered by an active org ruleset",
|
|
20320
|
-
detail: orgRuleset ? void 0 : activeOrgRulesets.length === 0 ? "no active Organization-sourced branch ruleset targets this repo" : `missing rule types: ${missingOrgRuleTypes.join(", ")}`
|
|
20445
|
+
detail: orgRuleset ? void 0 : excludedByOrgRuleset ? `deliberately excluded by name from ${excludedByOrgRuleset} \u2014 an owner decision, not drift (org-architecture.md \xA74)` : activeOrgRulesets.length === 0 ? "no active Organization-sourced branch ruleset targets this repo" : `missing rule types: ${missingOrgRuleTypes.join(", ")}`
|
|
20321
20446
|
});
|
|
20322
20447
|
if (repo === HUB_REPO4) {
|
|
20323
20448
|
const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
|
|
@@ -20466,13 +20591,13 @@ async function fetchOrgNoAgentFilesRuleset(client, org = ORG_LOGIN) {
|
|
|
20466
20591
|
|
|
20467
20592
|
// src/bootstrap-commands.ts
|
|
20468
20593
|
function registerBootstrapCommands(program3) {
|
|
20469
|
-
const bootstrap = program3.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").
|
|
20594
|
+
const bootstrap = program3.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).option("--json", "machine-readable output").action((o) => {
|
|
20470
20595
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
20471
20596
|
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap: --class must be deployable or content");
|
|
20472
20597
|
const steps = bootstrapPlan(o.repo, o.class);
|
|
20473
20598
|
console.log(o.json ? JSON.stringify({ command: "bootstrap", repo: o.repo, class: o.class, steps }, null, 2) : renderSteps(`mmi-cli bootstrap: dry-run plan for ${o.repo}`, steps));
|
|
20474
20599
|
});
|
|
20475
|
-
bootstrap.command("verify <repo>").description("audit whether an existing repo is bootstrapped correctly; no mutations").
|
|
20600
|
+
bootstrap.command("verify <repo>").description("audit whether an existing repo is bootstrapped correctly; no mutations").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).option("--json", "machine-readable output").action(async (repo) => {
|
|
20476
20601
|
const o = { class: rawValue("--class", "deployable"), json: rawFlag("--json") };
|
|
20477
20602
|
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap verify: --class must be deployable or content");
|
|
20478
20603
|
const cfg = await loadConfig();
|
|
@@ -20487,6 +20612,10 @@ function registerBootstrapCommands(program3) {
|
|
|
20487
20612
|
readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs22.existsSync)(path2) ? (0, import_node_fs22.readFileSync)(path2, "utf8") : null,
|
|
20488
20613
|
// requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
|
|
20489
20614
|
// comma-string — accept either so the seeded value verifies regardless of how it was written.
|
|
20615
|
+
// #3689: the same committed map the org access audit reads (#3664), so a sanctioned admin is not a
|
|
20616
|
+
// permanent bootstrap failure on one surface and an intended state on the other. Absent file → no
|
|
20617
|
+
// sanction, which is the pre-#3664 behaviour.
|
|
20618
|
+
sanctionedAdmins: (0, import_node_fs22.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs22.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
|
|
20490
20619
|
requiredGcpApis: (() => {
|
|
20491
20620
|
const v = meta?.requiredGcpApis;
|
|
20492
20621
|
if (Array.isArray(v)) return v;
|
|
@@ -20521,7 +20650,7 @@ function registerBootstrapCommands(program3) {
|
|
|
20521
20650
|
else console.log(renderOrgRulesetDriftReport(plan));
|
|
20522
20651
|
if (plan.action !== "noop") process.exitCode = 1;
|
|
20523
20652
|
});
|
|
20524
|
-
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)").
|
|
20653
|
+
bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
|
|
20525
20654
|
const o = {
|
|
20526
20655
|
class: rawValue("--class", "deployable"),
|
|
20527
20656
|
projectType: rawValue("--project-type", ""),
|
|
@@ -20531,6 +20660,9 @@ function registerBootstrapCommands(program3) {
|
|
|
20531
20660
|
json: rawFlag("--json")
|
|
20532
20661
|
};
|
|
20533
20662
|
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap apply: --class must be deployable or content");
|
|
20663
|
+
if (o.releaseTrack && !isReleaseTrack(o.releaseTrack)) {
|
|
20664
|
+
return fail(`bootstrap apply: --release-track must be one of: ${RELEASE_TRACKS.join(", ")}`);
|
|
20665
|
+
}
|
|
20534
20666
|
const bootstrapReleaseTrack = resolveBootstrapReleaseTrack(o.class, o.releaseTrack || void 0);
|
|
20535
20667
|
let parsedRepo;
|
|
20536
20668
|
try {
|
|
@@ -20718,8 +20850,17 @@ function registerBootstrapCommands(program3) {
|
|
|
20718
20850
|
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile7);
|
|
20719
20851
|
if (rulesetContent) {
|
|
20720
20852
|
try {
|
|
20721
|
-
const
|
|
20722
|
-
|
|
20853
|
+
const client = defaultGitHubClient();
|
|
20854
|
+
const proven = await gateIsProvenGreen(repo, client, baseBranch);
|
|
20855
|
+
const activation = await activateProductRuleset(
|
|
20856
|
+
repo,
|
|
20857
|
+
stripRulesetComment(rulesetContent),
|
|
20858
|
+
client,
|
|
20859
|
+
proven ? "active" : "disabled"
|
|
20860
|
+
);
|
|
20861
|
+
applied.push(
|
|
20862
|
+
`product ruleset: ${activation.action} (${activation.enforcement}${activation.detail ? `; ${activation.detail}` : ""})` + (proven ? "" : ` \u2014 the gate is not green on ${baseBranch}, so it is NOT enforcing. Activate once it is: mmi-cli ci reconcile --apply --repo ${repo}`)
|
|
20863
|
+
);
|
|
20723
20864
|
} catch (e) {
|
|
20724
20865
|
return failGraceful(`bootstrap apply: product ruleset activation failed: ${e.message}`);
|
|
20725
20866
|
}
|
|
@@ -24330,7 +24471,7 @@ function findCommandInManifest(manifest, commandPath3) {
|
|
|
24330
24471
|
return visit(manifest.tree);
|
|
24331
24472
|
}
|
|
24332
24473
|
function registerExplainCommand(program3) {
|
|
24333
|
-
program3.command("explain").description("print a command's purpose, flags, and examples from the live schema \u2014 one grounding call replaces trial-and-error").argument("[command...]", 'the command path to explain (e.g. "issue create")').
|
|
24474
|
+
program3.command("explain").description("print a command's purpose, flags, and examples from the live schema \u2014 one grounding call replaces trial-and-error").argument("[command...]", 'the command path to explain (e.g. "issue create")').addOption(new Option("--loop <name>", "print the canonical command sequence: agent | start-work | ship-pr | hotfix").choices(Object.keys(LOOP_PLAYBOOKS))).action((commandArgs, opts) => {
|
|
24334
24475
|
if (opts.loop) {
|
|
24335
24476
|
if (!LOOP_PLAYBOOKS[opts.loop]) {
|
|
24336
24477
|
const valid = Object.keys(LOOP_PLAYBOOKS).join(", ");
|
|
@@ -25069,7 +25210,17 @@ function checkClaudePlugin(probe) {
|
|
|
25069
25210
|
verbose: evidence
|
|
25070
25211
|
};
|
|
25071
25212
|
}
|
|
25072
|
-
if (!released)
|
|
25213
|
+
if (!released) {
|
|
25214
|
+
return {
|
|
25215
|
+
id: "claude-plugin",
|
|
25216
|
+
ok: false,
|
|
25217
|
+
reportOnly: true,
|
|
25218
|
+
label: "Claude plugin",
|
|
25219
|
+
detail: `${installed ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
|
|
25220
|
+
fix: "check it directly: `npm view @mutmutco/cli version`, then `mmi-cli plugin heal` if behind",
|
|
25221
|
+
verbose: evidence
|
|
25222
|
+
};
|
|
25223
|
+
}
|
|
25073
25224
|
return { id: "claude-plugin", ok: true, label: "Claude plugin", ...installed ? { detail: installed } : {}, verbose: evidence };
|
|
25074
25225
|
}
|
|
25075
25226
|
function checkCliVersion(input, releasedNote) {
|
|
@@ -25078,7 +25229,17 @@ function checkCliVersion(input, releasedNote) {
|
|
|
25078
25229
|
`running: ${report.currentVersion}`,
|
|
25079
25230
|
`published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}${report.releasedVersion && releasedNote ? ` ${releasedNote}` : ""}`
|
|
25080
25231
|
];
|
|
25081
|
-
if (!report.releasedVersion)
|
|
25232
|
+
if (!report.releasedVersion) {
|
|
25233
|
+
return {
|
|
25234
|
+
id: "cli-version",
|
|
25235
|
+
ok: false,
|
|
25236
|
+
reportOnly: true,
|
|
25237
|
+
label: "mmi-cli",
|
|
25238
|
+
detail: `${report.currentVersion} \u2014 freshness UNKNOWN, the published version could not be read`,
|
|
25239
|
+
fix: "check it directly: `mmi-cli --version` against `npm view @mutmutco/cli version`; update with `npm install -g @mutmutco/cli@<released>`",
|
|
25240
|
+
verbose: evidence
|
|
25241
|
+
};
|
|
25242
|
+
}
|
|
25082
25243
|
if (report.ok) return { id: "cli-version", ok: true, label: "mmi-cli", detail: report.currentVersion, verbose: evidence };
|
|
25083
25244
|
return {
|
|
25084
25245
|
id: "cli-version",
|
|
@@ -25916,11 +26077,20 @@ var lastParseErrorKind = "other";
|
|
|
25916
26077
|
var lastUnknownCommand;
|
|
25917
26078
|
function classifyParseError(plain) {
|
|
25918
26079
|
if (/unknown command/i.test(plain)) return "unknown-command";
|
|
25919
|
-
if (/unknown option|missing required argument|too many arguments|argument missing/i.test(plain)) {
|
|
26080
|
+
if (/unknown option|missing required argument|too many arguments|argument missing|is invalid\. Allowed choices are/i.test(plain)) {
|
|
25920
26081
|
return "bad-arguments";
|
|
25921
26082
|
}
|
|
25922
26083
|
return "other";
|
|
25923
26084
|
}
|
|
26085
|
+
function parseInvalidChoiceError(plain) {
|
|
26086
|
+
const m = /option '([^']+)' argument '([^']*)' is invalid\. Allowed choices are ([^.]+)\./.exec(plain);
|
|
26087
|
+
if (!m) return void 0;
|
|
26088
|
+
return {
|
|
26089
|
+
flag: m[1].split(/[ ,|]/)[0],
|
|
26090
|
+
value: m[2],
|
|
26091
|
+
expected: m[3].split(",").map((s) => s.trim()).filter(Boolean)
|
|
26092
|
+
};
|
|
26093
|
+
}
|
|
25924
26094
|
function resolveCommandFromArgv(root, argv) {
|
|
25925
26095
|
let current = root;
|
|
25926
26096
|
for (const token of argv) {
|
|
@@ -25982,6 +26152,22 @@ function envelopeAwareWriteErr(str) {
|
|
|
25982
26152
|
}
|
|
25983
26153
|
lastParseErrorKind = classifyParseError(plain);
|
|
25984
26154
|
lastUnknownCommand = /unknown command '?([\w:-]+)'?/.exec(plain)?.[1];
|
|
26155
|
+
const badChoice = parseInvalidChoiceError(plain);
|
|
26156
|
+
if (badChoice) {
|
|
26157
|
+
if (!argvWantsJson2()) {
|
|
26158
|
+
process.stderr.write(str);
|
|
26159
|
+
return;
|
|
26160
|
+
}
|
|
26161
|
+
process.stderr.write(
|
|
26162
|
+
formatErrorEnvelope(`${badChoice.flag}: unknown value "${badChoice.value}"`, {
|
|
26163
|
+
code: ERROR_CODES.ERR_BAD_ENUM,
|
|
26164
|
+
offending_flag: badChoice.flag,
|
|
26165
|
+
expected: badChoice.expected
|
|
26166
|
+
}) + "\n"
|
|
26167
|
+
);
|
|
26168
|
+
unknownFlagJsonHandled = true;
|
|
26169
|
+
return;
|
|
26170
|
+
}
|
|
25985
26171
|
const match = /unknown option '([^']+)'/.exec(plain);
|
|
25986
26172
|
if (match) {
|
|
25987
26173
|
const flag = match[1];
|
|
@@ -26568,7 +26754,7 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
|
|
|
26568
26754
|
}
|
|
26569
26755
|
});
|
|
26570
26756
|
var docsAudit = program2.command("docs-audit").description("the docs janitor verdict ledger \u2014 record a dated run verdict, or read the dead-man status back");
|
|
26571
|
-
docsAudit.command("record").description("write a dated janitor verdict for one repo to the registry ledger (master-only server-side)").option("--repo <owner/name>", "the repo the verdict is for (default: the current repo)").option("--date <YYYY-MM-DD>", "the ISO day the run examined (default: today)").requiredOption("--sha-range <a..b>", "the git range the janitor read (e.g. <lastVerdictSha>..HEAD)").
|
|
26757
|
+
docsAudit.command("record").description("write a dated janitor verdict for one repo to the registry ledger (master-only server-side)").option("--repo <owner/name>", "the repo the verdict is for (default: the current repo)").option("--date <YYYY-MM-DD>", "the ISO day the run examined (default: today)").requiredOption("--sha-range <a..b>", "the git range the janitor read (e.g. <lastVerdictSha>..HEAD)").addOption(new Option("--outcome <kind>", "clean | refreshed | failed").makeOptionMandatory().choices(["clean", "refreshed", "failed"])).option("--count <n>", "docs refreshed (required when --outcome refreshed)").option("--reason <text>", "why the run failed (required when --outcome failed)").requiredOption("--checker-vendor <vendor>", "which vendor's model actually ran the check").action(async (o) => {
|
|
26572
26758
|
try {
|
|
26573
26759
|
const repo = o.repo ?? await currentRepoFullName();
|
|
26574
26760
|
const date = o.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -26751,7 +26937,7 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
|
|
|
26751
26937
|
}
|
|
26752
26938
|
});
|
|
26753
26939
|
var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)");
|
|
26754
|
-
projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").
|
|
26940
|
+
projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
26755
26941
|
const cfg = await loadConfig();
|
|
26756
26942
|
let target;
|
|
26757
26943
|
try {
|
|
@@ -26766,7 +26952,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
|
|
|
26766
26952
|
const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
|
|
26767
26953
|
console.log(JSON.stringify(payload));
|
|
26768
26954
|
});
|
|
26769
|
-
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").
|
|
26955
|
+
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).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). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|\u2026], "provider":"<optional>"}').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) => {
|
|
26770
26956
|
const cfg = await loadConfig();
|
|
26771
26957
|
let target;
|
|
26772
26958
|
try {
|
|
@@ -26880,7 +27066,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
26880
27066
|
console.log(JSON.stringify(report, null, 2));
|
|
26881
27067
|
if (!report.rcand.canApply) process.exitCode = 1;
|
|
26882
27068
|
});
|
|
26883
|
-
project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row \u2014 project-admin may set only --no-env-file on their own existing dev/rc row; master may seed/change all coords and main; defaults to the current repo").
|
|
27069
|
+
project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row \u2014 project-admin may set only --no-env-file on their own existing dev/rc row; master may seed/change all coords and main; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").makeOptionMandatory().choices(["dev", "rc", "main"])).option("--ssh-host <host>", "the box address the deploy ssh-es into; omit to keep the stored value (required only for a NEW hetzner-ssh row \u2014 `mmi-cli runtime box list` finds it)").option("--ssh-user <user>", "ssh user; omit to leave the row unchanged (default root on a new row)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535); omit to leave the row unchanged").option("--substrate <substrate>", "hetzner-ssh; omit to leave the row unchanged").option("--deploy-path <path>", "on-box per-stage release root; omit to leave the row unchanged (default /opt/mmi/<slug>/<stage> on a new row)").option("--service <name>", "systemd/compose service name; omit to leave the row unchanged (default the slug on a new row)").option("--domain <domain>", "canonical serving host; omit to leave the row unchanged").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable); omit to leave the stored aliases unchanged").option("--clear-aliases", "remove EVERY serving alias from the row (#2986) \u2014 omitting --alias preserves them, so clearing needs saying out loud").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) \u2014 own-repo project-admin dev/rc; master main; true = env passthrough with no .env symlink").option("--force", "explicit recovery override: skip fileless-compose verification only after independent proof").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
26884
27070
|
const cfg = await loadConfig();
|
|
26885
27071
|
let target;
|
|
26886
27072
|
try {
|
|
@@ -27077,7 +27263,7 @@ function resolveCreateType(raw, command, labels) {
|
|
|
27077
27263
|
}
|
|
27078
27264
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
27079
27265
|
withExamples(mutating(
|
|
27080
|
-
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").
|
|
27266
|
+
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks or newlines needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
|
|
27081
27267
|
// --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
|
|
27082
27268
|
// ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
|
|
27083
27269
|
// (`--parent`) and title-source are validated by the action on a real run.
|
|
@@ -27294,7 +27480,7 @@ ${list}`);
|
|
|
27294
27480
|
}
|
|
27295
27481
|
console.log(JSON.stringify({ number: parsed.number, repo, item: result.item.text, checked, changed: true }));
|
|
27296
27482
|
});
|
|
27297
|
-
program2.command("report").description("file a friction report on the Hub board (Hub session auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").
|
|
27483
|
+
program2.command("report").description("file a friction report on the Hub board (Hub session auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label)").default("task").choices([...ISSUE_TYPES])).option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only, #416)").option("--repo <owner/repo>", 'attribute the report to a different source repo than the current checkout for the "Filed via..." footer (rare \u2014 usually auto-detected; every report always lands on the org Hub, never an alternate target, #263)').option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
|
|
27298
27484
|
let body;
|
|
27299
27485
|
let priority;
|
|
27300
27486
|
let title;
|
|
@@ -27338,7 +27524,7 @@ async function resolvePluginSha() {
|
|
|
27338
27524
|
return void 0;
|
|
27339
27525
|
}
|
|
27340
27526
|
}
|
|
27341
|
-
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").
|
|
27527
|
+
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").addOption(new Option("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).makeOptionMandatory().choices([...SKILL_NAMES])).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only, #416)").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
|
|
27342
27528
|
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
27343
27529
|
const sourceRepo = await resolveRepo(void 0);
|
|
27344
27530
|
const pluginSha = await resolvePluginSha();
|
|
@@ -27940,17 +28126,33 @@ async function resolveRcandPlanTargets() {
|
|
|
27940
28126
|
}
|
|
27941
28127
|
}
|
|
27942
28128
|
for (const commandName of ["rcand", "release"]) {
|
|
27943
|
-
program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval")
|
|
28129
|
+
const trainCmd = program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval");
|
|
28130
|
+
const RELEASE_ONLY_FLAGS = [
|
|
28131
|
+
{ flags: "--announce-summary-file <path>", description: "agent-curated summary lines for the Hub Slack announcement (#883)" },
|
|
28132
|
+
{ flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
|
|
28133
|
+
{ flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" }
|
|
28134
|
+
];
|
|
28135
|
+
for (const f of RELEASE_ONLY_FLAGS) {
|
|
28136
|
+
if (commandName === "release") {
|
|
28137
|
+
trainCmd.option(f.flags, f.description);
|
|
28138
|
+
} else {
|
|
28139
|
+
trainCmd.addOption(new Option(f.flags, f.description).hideHelp());
|
|
28140
|
+
}
|
|
28141
|
+
}
|
|
28142
|
+
trainCmd.option("--repo <owner/repo>", "dry-run plan for a target repo without relying on the current checkout; --apply still uses the current checkout").action(async (o) => {
|
|
27944
28143
|
try {
|
|
27945
28144
|
await requireFreshTrainCli(commandName);
|
|
27946
28145
|
} catch (e) {
|
|
27947
28146
|
return fail(`${commandName}: ${e.message}`);
|
|
27948
28147
|
}
|
|
27949
28148
|
if (o.ack && commandName !== "release") {
|
|
27950
|
-
return fail(
|
|
28149
|
+
return fail(`${commandName}: --ack applies only to release \u2014 it overrides the rc -> main hotfix-coverage guard, which rcand does not run. Run: mmi-cli release --ack <shas>`);
|
|
27951
28150
|
}
|
|
27952
28151
|
if (o.dev && commandName !== "release") {
|
|
27953
|
-
return fail(
|
|
28152
|
+
return fail(`${commandName}: --dev applies only to release \u2014 it ships development -> main skipping rc, which rcand cannot do. Run: mmi-cli release --dev`);
|
|
28153
|
+
}
|
|
28154
|
+
if (o.announceSummaryFile && commandName !== "release") {
|
|
28155
|
+
return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no Hub Slack announcement. Run: mmi-cli release --announce-summary-file <path>`);
|
|
27954
28156
|
}
|
|
27955
28157
|
if (o.apply && o.repo) {
|
|
27956
28158
|
const rerun = `mmi-cli ${commandName} --apply${o.watch ? " --watch" : ""}${o.dev ? " --dev" : ""}${o.json ? " --json" : ""}`;
|
|
@@ -28039,21 +28241,24 @@ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset co
|
|
|
28039
28241
|
else console.log(renderCiAuditText(report));
|
|
28040
28242
|
if (!report.ok) process.exitCode = 1;
|
|
28041
28243
|
});
|
|
28042
|
-
ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required)").action(async (o) => {
|
|
28043
|
-
if (o.apply) {
|
|
28244
|
+
ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required); activation is skipped while the repo's gate has never passed (#3694)").option("--park-ruleset", "stop the product ruleset enforcing without deleting it \u2014 keeps its required contexts (master role required)").action(async (o) => {
|
|
28245
|
+
if (o.apply && o.parkRuleset) {
|
|
28246
|
+
return fail("ci reconcile: --apply and --park-ruleset ask for opposite things; pass one");
|
|
28247
|
+
}
|
|
28248
|
+
if (o.apply || o.parkRuleset) {
|
|
28044
28249
|
const verdict = await fetchTrainAuthority(HUB_REPO2, registryClientDeps(await loadConfig()));
|
|
28045
28250
|
if (!verdict.ok || verdict.authority.role !== "master") {
|
|
28046
|
-
return fail(
|
|
28251
|
+
return fail(`ci reconcile ${o.apply ? "--apply" : "--park-ruleset"}: master-admin required`);
|
|
28047
28252
|
}
|
|
28048
28253
|
}
|
|
28049
28254
|
const deps = ciAuditDeps();
|
|
28050
28255
|
const audit = await auditOrgCi(deps, o.repo);
|
|
28051
|
-
const applyResults = o.apply ? await Promise.all(audit.repos.map((r) => applyCiReconcileRepo(r.repo, deps))) : [];
|
|
28256
|
+
const applyResults = o.apply ? await Promise.all(audit.repos.map((r) => applyCiReconcileRepo(r.repo, deps))) : o.parkRuleset ? await Promise.all(audit.repos.map((r) => parkProductRuleset(r.repo, deps))) : [];
|
|
28052
28257
|
const payload = { audit, apply: applyResults };
|
|
28053
28258
|
if (o.json) console.log(JSON.stringify(payload, null, 2));
|
|
28054
28259
|
else {
|
|
28055
28260
|
console.log(renderCiAuditText(audit));
|
|
28056
|
-
if (
|
|
28261
|
+
if (applyResults.length) {
|
|
28057
28262
|
for (const r of applyResults) {
|
|
28058
28263
|
console.log(`
|
|
28059
28264
|
${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${r.errors.length ? ` errors=[${r.errors.join("; ")}]` : ""}`);
|
|
@@ -28304,6 +28509,7 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
|
|
|
28304
28509
|
envHealLockPath,
|
|
28305
28510
|
gcPlan,
|
|
28306
28511
|
isOrgRegisteredRepo,
|
|
28512
|
+
parseInvalidChoiceError,
|
|
28307
28513
|
positionalTargetForm,
|
|
28308
28514
|
registryClientDeps,
|
|
28309
28515
|
repoSlug,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.72.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",
|