@mutmutco/cli 3.71.0 → 3.73.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/README.md +1 -1
- package/dist/main.cjs +476 -254
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The command-line engine for MMI Future org tooling. It delivers the org-managed `.gitignore` block, reads and claims GitHub Project work, and exposes the model-agnostic commands used by the MMI plugin and non-Claude agents. (Personal agent guides — `AGENTS.md` / `CLAUDE.md` — are developer-owned and gitignored; the CLI does not deliver, overwrite, or fan them out — the whole-spine fanout is retired.)
|
|
4
4
|
|
|
5
|
-
This package is published from [mutmutco/MMI-Hub](https://github.com/mutmutco/MMI-Hub) and its version matches the MMI Hub
|
|
5
|
+
This package is published from [mutmutco/MMI-Hub](https://github.com/mutmutco/MMI-Hub) and its version matches the MMI Hub plugin distribution version — the release train bumps the CLI and the Claude, Codex, and Kimi plugin manifests in lockstep.
|
|
6
6
|
|
|
7
7
|
The CLI carries the org **Hub endpoint** intrinsically (override with the `MMI_HUB_URL` env var), so a product repo needs **no committed control-plane config** to reach the Hub — board coords, deploy coordinates, OAuth, and the secrets layout are all discovered from the Hub registry at runtime.
|
|
8
8
|
|
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
|
}
|
|
@@ -21661,6 +21802,9 @@ function detectSurface(env) {
|
|
|
21661
21802
|
if (env.MMI_AGENT_SURFACE === "codex" || has("CODEX_HOME") || (env.CLAUDE_PLUGIN_ROOT ?? "").includes(".codex")) {
|
|
21662
21803
|
return "codex";
|
|
21663
21804
|
}
|
|
21805
|
+
if (env.MMI_AGENT_SURFACE === "kimi" || has("KIMI_PLUGIN_ROOT") || has("KIMI_CODE_HOME")) {
|
|
21806
|
+
return "kimi";
|
|
21807
|
+
}
|
|
21664
21808
|
if (env.MMI_AGENT_SURFACE === "cursor" || has("CURSOR_TRACE_ID") || has("CURSOR_USER") || has("CURSOR_SESSION_ID") || env.CURSOR_AGENT === "1" || has("CURSOR_EXTENSION_HOST_ROLE")) {
|
|
21665
21809
|
return "cursor";
|
|
21666
21810
|
}
|
|
@@ -21678,6 +21822,8 @@ function surfaceToken(surface) {
|
|
|
21678
21822
|
return "claude";
|
|
21679
21823
|
case "codex":
|
|
21680
21824
|
return "codex";
|
|
21825
|
+
case "kimi":
|
|
21826
|
+
return "kimi";
|
|
21681
21827
|
case "cursor":
|
|
21682
21828
|
return "cursor";
|
|
21683
21829
|
case "opencode":
|
|
@@ -21693,6 +21839,8 @@ function reloadAction(surface) {
|
|
|
21693
21839
|
return "restart VS Code";
|
|
21694
21840
|
case "codex":
|
|
21695
21841
|
return "restart Codex";
|
|
21842
|
+
case "kimi":
|
|
21843
|
+
return "run /reload (or start a new session) in Kimi Code";
|
|
21696
21844
|
case "opencode":
|
|
21697
21845
|
return "restart OpenCode";
|
|
21698
21846
|
case "cursor":
|
|
@@ -21723,7 +21871,10 @@ function nonClaudeSurfaceHealMessage(surface) {
|
|
|
21723
21871
|
if (surface === "codex") {
|
|
21724
21872
|
return "Codex ships an MMI plugin (#3563). Install or repair it with:\n codex plugin marketplace add mutmutco/MMI-Hub && codex plugin add mmi@mutmutco\n Then run /hooks and TRUST the MMI hooks \u2014 bundled hooks are skipped until reviewed.\n Update the CLI too: npm i -g @mutmutco/cli";
|
|
21725
21873
|
}
|
|
21726
|
-
|
|
21874
|
+
if (surface === "kimi") {
|
|
21875
|
+
return "Kimi Code CLI ships an MMI plugin (kimi-k3). Install or repair it from the Kimi TUI with:\n /plugins install https://github.com/mutmutco/MMI-Hub\n Then run /reload (plugin hooks start only after a reload), and TRUST the install when prompted.\n Update the CLI too: npm i -g @mutmutco/cli";
|
|
21876
|
+
}
|
|
21877
|
+
return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude, Codex, and Kimi only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
|
|
21727
21878
|
}
|
|
21728
21879
|
function healStepAborts(step, ok) {
|
|
21729
21880
|
return !ok && step.gated;
|
|
@@ -21762,9 +21913,13 @@ var NPM_VIEW_TIMEOUT_MS = 15e3;
|
|
|
21762
21913
|
function runHostBin(bin, args, opts) {
|
|
21763
21914
|
return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
|
|
21764
21915
|
}
|
|
21916
|
+
function surfaceHomeDir(surface) {
|
|
21917
|
+
if (surface === "codex") return ".codex";
|
|
21918
|
+
if (surface === "kimi") return ".kimi-code";
|
|
21919
|
+
return ".claude";
|
|
21920
|
+
}
|
|
21765
21921
|
var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
|
|
21766
|
-
|
|
21767
|
-
return (0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
|
|
21922
|
+
return (0, import_node_path24.join)((0, import_node_os6.homedir)(), surfaceHomeDir(surface), "plugins", "installed_plugins.json");
|
|
21768
21923
|
};
|
|
21769
21924
|
function readInstalledPlugins(surface = detectSurface(process.env)) {
|
|
21770
21925
|
try {
|
|
@@ -21780,6 +21935,7 @@ function marketplaceCloneCandidates(surface, home) {
|
|
|
21780
21935
|
(0, import_node_path24.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
|
|
21781
21936
|
];
|
|
21782
21937
|
}
|
|
21938
|
+
if (surface === "kimi") return [];
|
|
21783
21939
|
return [(0, import_node_path24.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
|
|
21784
21940
|
}
|
|
21785
21941
|
function marketplaceClonePresent(surface, home, exists = import_node_fs25.existsSync) {
|
|
@@ -21804,13 +21960,14 @@ async function npmSelfUpdateCli(target) {
|
|
|
21804
21960
|
}
|
|
21805
21961
|
}
|
|
21806
21962
|
function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
|
|
21807
|
-
const homeDir = surface
|
|
21963
|
+
const homeDir = surfaceHomeDir(surface);
|
|
21808
21964
|
const installed = readInstalledPlugins(surface);
|
|
21809
21965
|
return {
|
|
21810
21966
|
isOrgRepo,
|
|
21811
21967
|
installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
|
|
21812
21968
|
marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
|
|
21813
|
-
|
|
21969
|
+
// Kimi keeps no plugin cache dir — installs are copied to plugins/managed/<id> and run from there.
|
|
21970
|
+
pluginCachePresent: surface === "kimi" ? (0, import_node_fs25.existsSync)((0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "managed", "mmi")) : (0, import_node_fs25.existsSync)((0, import_node_path24.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
|
|
21814
21971
|
};
|
|
21815
21972
|
}
|
|
21816
21973
|
function claudePluginGuardState(isOrgRepo) {
|
|
@@ -24330,7 +24487,7 @@ function findCommandInManifest(manifest, commandPath3) {
|
|
|
24330
24487
|
return visit(manifest.tree);
|
|
24331
24488
|
}
|
|
24332
24489
|
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")').
|
|
24490
|
+
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
24491
|
if (opts.loop) {
|
|
24335
24492
|
if (!LOOP_PLAYBOOKS[opts.loop]) {
|
|
24336
24493
|
const valid = Object.keys(LOOP_PLAYBOOKS).join(", ");
|
|
@@ -25069,7 +25226,17 @@ function checkClaudePlugin(probe) {
|
|
|
25069
25226
|
verbose: evidence
|
|
25070
25227
|
};
|
|
25071
25228
|
}
|
|
25072
|
-
if (!released)
|
|
25229
|
+
if (!released) {
|
|
25230
|
+
return {
|
|
25231
|
+
id: "claude-plugin",
|
|
25232
|
+
ok: false,
|
|
25233
|
+
reportOnly: true,
|
|
25234
|
+
label: "Claude plugin",
|
|
25235
|
+
detail: `${installed ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
|
|
25236
|
+
fix: "check it directly: `npm view @mutmutco/cli version`, then `mmi-cli plugin heal` if behind",
|
|
25237
|
+
verbose: evidence
|
|
25238
|
+
};
|
|
25239
|
+
}
|
|
25073
25240
|
return { id: "claude-plugin", ok: true, label: "Claude plugin", ...installed ? { detail: installed } : {}, verbose: evidence };
|
|
25074
25241
|
}
|
|
25075
25242
|
function checkCliVersion(input, releasedNote) {
|
|
@@ -25078,7 +25245,17 @@ function checkCliVersion(input, releasedNote) {
|
|
|
25078
25245
|
`running: ${report.currentVersion}`,
|
|
25079
25246
|
`published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}${report.releasedVersion && releasedNote ? ` ${releasedNote}` : ""}`
|
|
25080
25247
|
];
|
|
25081
|
-
if (!report.releasedVersion)
|
|
25248
|
+
if (!report.releasedVersion) {
|
|
25249
|
+
return {
|
|
25250
|
+
id: "cli-version",
|
|
25251
|
+
ok: false,
|
|
25252
|
+
reportOnly: true,
|
|
25253
|
+
label: "mmi-cli",
|
|
25254
|
+
detail: `${report.currentVersion} \u2014 freshness UNKNOWN, the published version could not be read`,
|
|
25255
|
+
fix: "check it directly: `mmi-cli --version` against `npm view @mutmutco/cli version`; update with `npm install -g @mutmutco/cli@<released>`",
|
|
25256
|
+
verbose: evidence
|
|
25257
|
+
};
|
|
25258
|
+
}
|
|
25082
25259
|
if (report.ok) return { id: "cli-version", ok: true, label: "mmi-cli", detail: report.currentVersion, verbose: evidence };
|
|
25083
25260
|
return {
|
|
25084
25261
|
id: "cli-version",
|
|
@@ -25916,11 +26093,20 @@ var lastParseErrorKind = "other";
|
|
|
25916
26093
|
var lastUnknownCommand;
|
|
25917
26094
|
function classifyParseError(plain) {
|
|
25918
26095
|
if (/unknown command/i.test(plain)) return "unknown-command";
|
|
25919
|
-
if (/unknown option|missing required argument|too many arguments|argument missing/i.test(plain)) {
|
|
26096
|
+
if (/unknown option|missing required argument|too many arguments|argument missing|is invalid\. Allowed choices are/i.test(plain)) {
|
|
25920
26097
|
return "bad-arguments";
|
|
25921
26098
|
}
|
|
25922
26099
|
return "other";
|
|
25923
26100
|
}
|
|
26101
|
+
function parseInvalidChoiceError(plain) {
|
|
26102
|
+
const m = /option '([^']+)' argument '([^']*)' is invalid\. Allowed choices are ([^.]+)\./.exec(plain);
|
|
26103
|
+
if (!m) return void 0;
|
|
26104
|
+
return {
|
|
26105
|
+
flag: m[1].split(/[ ,|]/)[0],
|
|
26106
|
+
value: m[2],
|
|
26107
|
+
expected: m[3].split(",").map((s) => s.trim()).filter(Boolean)
|
|
26108
|
+
};
|
|
26109
|
+
}
|
|
25924
26110
|
function resolveCommandFromArgv(root, argv) {
|
|
25925
26111
|
let current = root;
|
|
25926
26112
|
for (const token of argv) {
|
|
@@ -25982,6 +26168,22 @@ function envelopeAwareWriteErr(str) {
|
|
|
25982
26168
|
}
|
|
25983
26169
|
lastParseErrorKind = classifyParseError(plain);
|
|
25984
26170
|
lastUnknownCommand = /unknown command '?([\w:-]+)'?/.exec(plain)?.[1];
|
|
26171
|
+
const badChoice = parseInvalidChoiceError(plain);
|
|
26172
|
+
if (badChoice) {
|
|
26173
|
+
if (!argvWantsJson2()) {
|
|
26174
|
+
process.stderr.write(str);
|
|
26175
|
+
return;
|
|
26176
|
+
}
|
|
26177
|
+
process.stderr.write(
|
|
26178
|
+
formatErrorEnvelope(`${badChoice.flag}: unknown value "${badChoice.value}"`, {
|
|
26179
|
+
code: ERROR_CODES.ERR_BAD_ENUM,
|
|
26180
|
+
offending_flag: badChoice.flag,
|
|
26181
|
+
expected: badChoice.expected
|
|
26182
|
+
}) + "\n"
|
|
26183
|
+
);
|
|
26184
|
+
unknownFlagJsonHandled = true;
|
|
26185
|
+
return;
|
|
26186
|
+
}
|
|
25985
26187
|
const match = /unknown option '([^']+)'/.exec(plain);
|
|
25986
26188
|
if (match) {
|
|
25987
26189
|
const flag = match[1];
|
|
@@ -26568,7 +26770,7 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
|
|
|
26568
26770
|
}
|
|
26569
26771
|
});
|
|
26570
26772
|
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)").
|
|
26773
|
+
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
26774
|
try {
|
|
26573
26775
|
const repo = o.repo ?? await currentRepoFullName();
|
|
26574
26776
|
const date = o.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -26751,7 +26953,7 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
|
|
|
26751
26953
|
}
|
|
26752
26954
|
});
|
|
26753
26955
|
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").
|
|
26956
|
+
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
26957
|
const cfg = await loadConfig();
|
|
26756
26958
|
let target;
|
|
26757
26959
|
try {
|
|
@@ -26766,7 +26968,7 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
|
|
|
26766
26968
|
const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
|
|
26767
26969
|
console.log(JSON.stringify(payload));
|
|
26768
26970
|
});
|
|
26769
|
-
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").
|
|
26971
|
+
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
26972
|
const cfg = await loadConfig();
|
|
26771
26973
|
let target;
|
|
26772
26974
|
try {
|
|
@@ -26880,7 +27082,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
26880
27082
|
console.log(JSON.stringify(report, null, 2));
|
|
26881
27083
|
if (!report.rcand.canApply) process.exitCode = 1;
|
|
26882
27084
|
});
|
|
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").
|
|
27085
|
+
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
27086
|
const cfg = await loadConfig();
|
|
26885
27087
|
let target;
|
|
26886
27088
|
try {
|
|
@@ -27077,7 +27279,7 @@ function resolveCreateType(raw, command, labels) {
|
|
|
27077
27279
|
}
|
|
27078
27280
|
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
27079
27281
|
withExamples(mutating(
|
|
27080
|
-
issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").
|
|
27282
|
+
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
27283
|
// --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
|
|
27082
27284
|
// ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
|
|
27083
27285
|
// (`--parent`) and title-source are validated by the action on a real run.
|
|
@@ -27294,7 +27496,7 @@ ${list}`);
|
|
|
27294
27496
|
}
|
|
27295
27497
|
console.log(JSON.stringify({ number: parsed.number, repo, item: result.item.text, checked, changed: true }));
|
|
27296
27498
|
});
|
|
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 -").
|
|
27499
|
+
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
27500
|
let body;
|
|
27299
27501
|
let priority;
|
|
27300
27502
|
let title;
|
|
@@ -27338,7 +27540,7 @@ async function resolvePluginSha() {
|
|
|
27338
27540
|
return void 0;
|
|
27339
27541
|
}
|
|
27340
27542
|
}
|
|
27341
|
-
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").
|
|
27543
|
+
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
27544
|
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
27343
27545
|
const sourceRepo = await resolveRepo(void 0);
|
|
27344
27546
|
const pluginSha = await resolvePluginSha();
|
|
@@ -27940,17 +28142,33 @@ async function resolveRcandPlanTargets() {
|
|
|
27940
28142
|
}
|
|
27941
28143
|
}
|
|
27942
28144
|
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")
|
|
28145
|
+
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");
|
|
28146
|
+
const RELEASE_ONLY_FLAGS = [
|
|
28147
|
+
{ flags: "--announce-summary-file <path>", description: "agent-curated summary lines for the Hub Slack announcement (#883)" },
|
|
28148
|
+
{ 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)" },
|
|
28149
|
+
{ 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)" }
|
|
28150
|
+
];
|
|
28151
|
+
for (const f of RELEASE_ONLY_FLAGS) {
|
|
28152
|
+
if (commandName === "release") {
|
|
28153
|
+
trainCmd.option(f.flags, f.description);
|
|
28154
|
+
} else {
|
|
28155
|
+
trainCmd.addOption(new Option(f.flags, f.description).hideHelp());
|
|
28156
|
+
}
|
|
28157
|
+
}
|
|
28158
|
+
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
28159
|
try {
|
|
27945
28160
|
await requireFreshTrainCli(commandName);
|
|
27946
28161
|
} catch (e) {
|
|
27947
28162
|
return fail(`${commandName}: ${e.message}`);
|
|
27948
28163
|
}
|
|
27949
28164
|
if (o.ack && commandName !== "release") {
|
|
27950
|
-
return fail(
|
|
28165
|
+
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
28166
|
}
|
|
27952
28167
|
if (o.dev && commandName !== "release") {
|
|
27953
|
-
return fail(
|
|
28168
|
+
return fail(`${commandName}: --dev applies only to release \u2014 it ships development -> main skipping rc, which rcand cannot do. Run: mmi-cli release --dev`);
|
|
28169
|
+
}
|
|
28170
|
+
if (o.announceSummaryFile && commandName !== "release") {
|
|
28171
|
+
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
28172
|
}
|
|
27955
28173
|
if (o.apply && o.repo) {
|
|
27956
28174
|
const rerun = `mmi-cli ${commandName} --apply${o.watch ? " --watch" : ""}${o.dev ? " --dev" : ""}${o.json ? " --json" : ""}`;
|
|
@@ -28039,21 +28257,24 @@ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset co
|
|
|
28039
28257
|
else console.log(renderCiAuditText(report));
|
|
28040
28258
|
if (!report.ok) process.exitCode = 1;
|
|
28041
28259
|
});
|
|
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) {
|
|
28260
|
+
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) => {
|
|
28261
|
+
if (o.apply && o.parkRuleset) {
|
|
28262
|
+
return fail("ci reconcile: --apply and --park-ruleset ask for opposite things; pass one");
|
|
28263
|
+
}
|
|
28264
|
+
if (o.apply || o.parkRuleset) {
|
|
28044
28265
|
const verdict = await fetchTrainAuthority(HUB_REPO2, registryClientDeps(await loadConfig()));
|
|
28045
28266
|
if (!verdict.ok || verdict.authority.role !== "master") {
|
|
28046
|
-
return fail(
|
|
28267
|
+
return fail(`ci reconcile ${o.apply ? "--apply" : "--park-ruleset"}: master-admin required`);
|
|
28047
28268
|
}
|
|
28048
28269
|
}
|
|
28049
28270
|
const deps = ciAuditDeps();
|
|
28050
28271
|
const audit = await auditOrgCi(deps, o.repo);
|
|
28051
|
-
const applyResults = o.apply ? await Promise.all(audit.repos.map((r) => applyCiReconcileRepo(r.repo, deps))) : [];
|
|
28272
|
+
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
28273
|
const payload = { audit, apply: applyResults };
|
|
28053
28274
|
if (o.json) console.log(JSON.stringify(payload, null, 2));
|
|
28054
28275
|
else {
|
|
28055
28276
|
console.log(renderCiAuditText(audit));
|
|
28056
|
-
if (
|
|
28277
|
+
if (applyResults.length) {
|
|
28057
28278
|
for (const r of applyResults) {
|
|
28058
28279
|
console.log(`
|
|
28059
28280
|
${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${r.errors.length ? ` errors=[${r.errors.join("; ")}]` : ""}`);
|
|
@@ -28304,6 +28525,7 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
|
|
|
28304
28525
|
envHealLockPath,
|
|
28305
28526
|
gcPlan,
|
|
28306
28527
|
isOrgRegisteredRepo,
|
|
28528
|
+
parseInvalidChoiceError,
|
|
28307
28529
|
positionalTargetForm,
|
|
28308
28530
|
registryClientDeps,
|
|
28309
28531
|
repoSlug,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
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
|
|
3
|
+
"version": "3.73.0",
|
|
4
|
+
"description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the MMI plugin's skills and gates drive on Claude, Codex, and Kimi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"author": {
|