@mutmutco/cli 3.37.0 → 3.39.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 +416 -63
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -32,6 +32,7 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
DEFAULT_PRIORITY: () => DEFAULT_PRIORITY,
|
|
34
34
|
awsCallerArn: () => awsCallerArn,
|
|
35
|
+
classifyParseError: () => classifyParseError,
|
|
35
36
|
gcPlan: () => gcPlan,
|
|
36
37
|
isOrgRegisteredRepo: () => isOrgRegisteredRepo,
|
|
37
38
|
registryClientDeps: () => registryClientDeps,
|
|
@@ -4763,6 +4764,13 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
|
|
|
4763
4764
|
return { claimable, blocked, warnings };
|
|
4764
4765
|
}
|
|
4765
4766
|
|
|
4767
|
+
// src/registry-degrade.ts
|
|
4768
|
+
function registryDegradeError(error) {
|
|
4769
|
+
return new Error(
|
|
4770
|
+
`Hub registry read failed (${error}) \u2014 board coords could not be discovered. This is NOT necessarily the registry: a local DNS/network fault reaches this path as a timeout too. Check in order \u2014 (1) resolve the Hub API host (fails in seconds and rules out the local-network class), (2) if it resolves, retry: a Lambda cold start clears shortly, (3) if it still fails, check auth (a rejected credential is not a timeout).`
|
|
4771
|
+
);
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4766
4774
|
// ../infra/registry-endpoints.mjs
|
|
4767
4775
|
var PROJECTS_LIST_PATH = "/projects/list";
|
|
4768
4776
|
var ORG_CONFIG_PATH = "/org/config";
|
|
@@ -6557,20 +6565,56 @@ async function fetchSecretsDrift(deps, repo) {
|
|
|
6557
6565
|
if (!data || data.mode !== "drift") return null;
|
|
6558
6566
|
return data.drift;
|
|
6559
6567
|
}
|
|
6560
|
-
|
|
6561
|
-
|
|
6568
|
+
function formatOrgCatalog(entries) {
|
|
6569
|
+
const keys = Object.keys(entries).sort();
|
|
6570
|
+
if (!keys.length) return "org-infra catalog: no declarations";
|
|
6571
|
+
const width = Math.max(...keys.map((k) => k.length));
|
|
6572
|
+
const lines = keys.map((k) => {
|
|
6573
|
+
const e = entries[k];
|
|
6574
|
+
const stages = e.stages?.length ? e.stages.join(",") : "shared";
|
|
6575
|
+
return ` ${k.padEnd(width)} owner: ${e.owner} stages: ${stages} consumers: ${(e.consumers ?? []).join(",")} \u2014 ${e.purpose}`;
|
|
6576
|
+
});
|
|
6577
|
+
return [
|
|
6578
|
+
`org-infra secret catalog \u2014 ${keys.length} declaration(s) (_org/<provider>/<KEY>; declarations only, values never live here)`,
|
|
6579
|
+
...lines
|
|
6580
|
+
].join("\n");
|
|
6581
|
+
}
|
|
6582
|
+
function formatOrgCatalogOutcome(out) {
|
|
6583
|
+
const verb = out.mode === "replace" ? "replaced" : "merged";
|
|
6584
|
+
return `org-infra catalog ${verb}: ${out.added ?? 0} added, ${out.updated ?? 0} updated, ${out.unchanged ?? 0} unchanged, ${out.removed ?? 0} removed (${out.entries ?? "?"} entries total)`;
|
|
6585
|
+
}
|
|
6586
|
+
async function secretsOrgCatalogShow(deps, opts) {
|
|
6587
|
+
let res;
|
|
6562
6588
|
try {
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6589
|
+
res = await deps.fetch(`${deps.apiUrl}/catalog/org-infra`, {
|
|
6590
|
+
method: "GET",
|
|
6591
|
+
headers: await deps.headers(),
|
|
6592
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
6593
|
+
});
|
|
6594
|
+
} catch (e) {
|
|
6595
|
+
deps.err(`secrets org-catalog: ${e.message}`);
|
|
6596
|
+
return false;
|
|
6597
|
+
}
|
|
6598
|
+
if (!res.ok) {
|
|
6599
|
+
deps.err(await upgradeMessage(res) ?? `secrets org-catalog failed: HTTP ${res.status}${await readErr(res)}`);
|
|
6566
6600
|
return false;
|
|
6567
6601
|
}
|
|
6602
|
+
const cat = await res.json();
|
|
6603
|
+
const entries = cat.entries ?? {};
|
|
6604
|
+
if (opts.json) {
|
|
6605
|
+
deps.log(JSON.stringify({ entries }, null, 2));
|
|
6606
|
+
return true;
|
|
6607
|
+
}
|
|
6608
|
+
deps.log(formatOrgCatalog(entries));
|
|
6609
|
+
return true;
|
|
6610
|
+
}
|
|
6611
|
+
async function postOrgCatalog(deps, payload) {
|
|
6568
6612
|
let res;
|
|
6569
6613
|
try {
|
|
6570
6614
|
res = await deps.fetch(`${deps.apiUrl}/catalog/org-infra`, {
|
|
6571
6615
|
method: "POST",
|
|
6572
6616
|
headers: await deps.headers({ "content-type": "application/json" }),
|
|
6573
|
-
body: JSON.stringify(
|
|
6617
|
+
body: JSON.stringify(payload),
|
|
6574
6618
|
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
6575
6619
|
});
|
|
6576
6620
|
} catch (e) {
|
|
@@ -6581,10 +6625,30 @@ async function secretsOrgCatalogSet(deps, fileBody) {
|
|
|
6581
6625
|
deps.err(await upgradeMessage(res) ?? `secrets org-catalog failed: HTTP ${res.status}${await readErr(res)}`);
|
|
6582
6626
|
return false;
|
|
6583
6627
|
}
|
|
6584
|
-
|
|
6585
|
-
deps.log(`org-infra catalog updated: ${out.entries ?? "?"} entries`);
|
|
6628
|
+
deps.log(formatOrgCatalogOutcome(await res.json()));
|
|
6586
6629
|
return true;
|
|
6587
6630
|
}
|
|
6631
|
+
async function secretsOrgCatalogSet(deps, fileBody, opts = {}) {
|
|
6632
|
+
let parsed;
|
|
6633
|
+
try {
|
|
6634
|
+
parsed = JSON.parse(fileBody);
|
|
6635
|
+
} catch {
|
|
6636
|
+
deps.err('secrets org-catalog: file is not valid JSON ({ entries: { "<provider>/<KEY>": {...} } })');
|
|
6637
|
+
return false;
|
|
6638
|
+
}
|
|
6639
|
+
if (parsed && typeof parsed === "object" && ("mode" in parsed || "remove" in parsed)) {
|
|
6640
|
+
deps.err('secrets org-catalog: the file carries { entries } only \u2014 use --replace / --remove flags, not "mode"/"remove" file fields');
|
|
6641
|
+
return false;
|
|
6642
|
+
}
|
|
6643
|
+
return postOrgCatalog(deps, {
|
|
6644
|
+
entries: parsed.entries,
|
|
6645
|
+
...opts.replace ? { mode: "replace" } : {},
|
|
6646
|
+
...opts.remove?.length ? { remove: opts.remove } : {}
|
|
6647
|
+
});
|
|
6648
|
+
}
|
|
6649
|
+
async function secretsOrgCatalogRemove(deps, remove2) {
|
|
6650
|
+
return postOrgCatalog(deps, { entries: {}, remove: remove2 });
|
|
6651
|
+
}
|
|
6588
6652
|
var DEFAULT_RUNTIME_SECRET_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
|
|
6589
6653
|
function stringList(v) {
|
|
6590
6654
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && isValidSecretKey(x)) : [];
|
|
@@ -6715,10 +6779,11 @@ async function putSecret(deps, key, value, opts) {
|
|
|
6715
6779
|
return false;
|
|
6716
6780
|
}
|
|
6717
6781
|
const repo = await targetRepo(deps, opts);
|
|
6782
|
+
const slug = opts.slug?.toLowerCase();
|
|
6718
6783
|
const res = await deps.fetch(`${deps.apiUrl}/secrets/set`, {
|
|
6719
6784
|
method: "POST",
|
|
6720
6785
|
headers: await deps.headers({ "content-type": "application/json" }),
|
|
6721
|
-
body: JSON.stringify({ repo, key, value }),
|
|
6786
|
+
body: JSON.stringify({ repo, key, value, ...slug ? { slug } : {} }),
|
|
6722
6787
|
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
6723
6788
|
});
|
|
6724
6789
|
if (!res.ok) {
|
|
@@ -6732,7 +6797,7 @@ async function putSecret(deps, key, value, opts) {
|
|
|
6732
6797
|
const provider = providerForSecretKey(key);
|
|
6733
6798
|
if (provider) {
|
|
6734
6799
|
const result = await verifySecretValue(deps, key, value, opts);
|
|
6735
|
-
const tier = classifyTier(await vaultSlug(deps, opts), key);
|
|
6800
|
+
const tier = classifyTier(slug ?? await vaultSlug(deps, opts), key);
|
|
6736
6801
|
if (result.ok) {
|
|
6737
6802
|
deps.log(`set ${key} (${tier} tier); ${result.message}`);
|
|
6738
6803
|
return true;
|
|
@@ -6745,7 +6810,7 @@ async function putSecret(deps, key, value, opts) {
|
|
|
6745
6810
|
deps.err(`set ${key} (${tier} tier), but ${result.message}`);
|
|
6746
6811
|
return false;
|
|
6747
6812
|
}
|
|
6748
|
-
deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier)`);
|
|
6813
|
+
deps.log(`set ${key} (${classifyTier(slug ?? await vaultSlug(deps, opts), key)} tier)`);
|
|
6749
6814
|
return true;
|
|
6750
6815
|
}
|
|
6751
6816
|
async function secretsSet(deps, key, opts) {
|
|
@@ -7330,7 +7395,7 @@ async function loadConfigForBoardSelector(selector, repoOption) {
|
|
|
7330
7395
|
if (!floor.sagaApiUrl) return stripMutableBoardConfig(floor);
|
|
7331
7396
|
const slug = targetRepo2 ? slugOf(targetRepo2) : await repoSlug();
|
|
7332
7397
|
const read = await fetchProjectBySlugChecked(slug, registryClientDeps(floor));
|
|
7333
|
-
if (!read.ok) throw
|
|
7398
|
+
if (!read.ok) throw registryDegradeError(read.error);
|
|
7334
7399
|
return read.project ? boardConfigFromProject(read.project, floor) : stripMutableBoardConfig(floor);
|
|
7335
7400
|
}
|
|
7336
7401
|
|
|
@@ -8878,6 +8943,7 @@ function commandLadderHint() {
|
|
|
8878
8943
|
lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
|
|
8879
8944
|
}
|
|
8880
8945
|
lines.push("[mmi] Schedules doctrine (#3228): scheduled jobs + LLM crons go through the Hub registry \u2014 eight-field header \u2192 PR \u2192 `mmi-cli org schedules register`; LLM crons ONLY via the harbour (llm: yes \u21D2 executor cursor-agent). Never hand-build an unregistered cron. Opt out per repo: `mmi-cli org schedules mode self-managed`.");
|
|
8946
|
+
lines.push('[mmi] Registering \u2260 armed (#3281): `org schedules register` writes SCHEDULE# rows; the fleet-clock reconciler arms them within ~15 min. Report "registered successfully \u2014 arms on the next fleet-clock tick", never "armed"/"live". The success JSON carries an `arming` block. Before that window an absent clock means "not reconciled yet", NOT "will never fire" \u2014 verify with `mmi-cli org schedules` after it.');
|
|
8881
8947
|
return lines.join("\n");
|
|
8882
8948
|
}
|
|
8883
8949
|
|
|
@@ -8903,6 +8969,40 @@ function resolveMergeCiPolicy(input) {
|
|
|
8903
8969
|
}
|
|
8904
8970
|
return { policy: "wait-for-checks", reason: ciWorkflows.join(", ") };
|
|
8905
8971
|
}
|
|
8972
|
+
function describePrMergeEnqueuedReason(checks) {
|
|
8973
|
+
switch (checks) {
|
|
8974
|
+
case "pending":
|
|
8975
|
+
return {
|
|
8976
|
+
reason: "checks-pending",
|
|
8977
|
+
message: "checks are still pending; auto-merge will land once they settle, so retry later is appropriate"
|
|
8978
|
+
};
|
|
8979
|
+
case "failure":
|
|
8980
|
+
return {
|
|
8981
|
+
reason: "checks-failing",
|
|
8982
|
+
message: "GitHub reports a red check; auto-merge will not land until the required checks pass, so retrying now is pointless"
|
|
8983
|
+
};
|
|
8984
|
+
case "failing":
|
|
8985
|
+
return {
|
|
8986
|
+
reason: "checks-failing",
|
|
8987
|
+
message: "GitHub reports a red check while others are still running; wait for checks to settle before retrying"
|
|
8988
|
+
};
|
|
8989
|
+
case "no-checks-reported":
|
|
8990
|
+
return {
|
|
8991
|
+
reason: "no-checks-reported",
|
|
8992
|
+
message: "GitHub reports no checks; auto-merge remains queued until the base-branch policy permits the merge"
|
|
8993
|
+
};
|
|
8994
|
+
case "success":
|
|
8995
|
+
return {
|
|
8996
|
+
reason: "checks-passing",
|
|
8997
|
+
message: "reported checks are passing, but the base-branch policy or merge queue has not completed the merge yet"
|
|
8998
|
+
};
|
|
8999
|
+
default:
|
|
9000
|
+
return {
|
|
9001
|
+
reason: "checks-unavailable",
|
|
9002
|
+
message: "the current check verdict could not be read; auto-merge remains queued, so inspect checks before retrying"
|
|
9003
|
+
};
|
|
9004
|
+
}
|
|
9005
|
+
}
|
|
8906
9006
|
function parseGhPrChecksOutput(stdout) {
|
|
8907
9007
|
const text = stdout.trim();
|
|
8908
9008
|
if (!text) return "no-checks-reported";
|
|
@@ -11236,6 +11336,17 @@ function buildIssueArgs({ type, title, body, priority, repo, labels }) {
|
|
|
11236
11336
|
for (const label of labels ?? []) args.push("--label", label);
|
|
11237
11337
|
return args;
|
|
11238
11338
|
}
|
|
11339
|
+
async function ensureLabelsExist(labels, repo, deps = {}) {
|
|
11340
|
+
const run = deps.run ?? execFileP2;
|
|
11341
|
+
for (const label of new Set(labels)) {
|
|
11342
|
+
const args = ["label", "create", label, "--color", "ededed"];
|
|
11343
|
+
if (repo) args.push("--repo", repo);
|
|
11344
|
+
try {
|
|
11345
|
+
await run("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
11346
|
+
} catch {
|
|
11347
|
+
}
|
|
11348
|
+
}
|
|
11349
|
+
}
|
|
11239
11350
|
async function bodyArgsViaFile(args, deps = {}) {
|
|
11240
11351
|
const i = args.indexOf("--body");
|
|
11241
11352
|
if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
|
|
@@ -11306,14 +11417,27 @@ function buildPrArgs({ title, body, base, head, repo, draft }) {
|
|
|
11306
11417
|
if (draft) args.push("--draft");
|
|
11307
11418
|
return args;
|
|
11308
11419
|
}
|
|
11420
|
+
function parseExistingPr(stderr) {
|
|
11421
|
+
if (!/already exists/i.test(stderr)) return void 0;
|
|
11422
|
+
const match = /https:\/\/\S*\/pull\/(\d+)/.exec(stderr);
|
|
11423
|
+
if (!match) return void 0;
|
|
11424
|
+
return { number: Number(match[1]), url: match[0] };
|
|
11425
|
+
}
|
|
11309
11426
|
async function ghCreate(args) {
|
|
11310
11427
|
const swapped = await bodyArgsViaFile(args);
|
|
11311
11428
|
try {
|
|
11312
11429
|
const { stdout } = await execFileP2("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
11313
11430
|
return parseCreatedUrl(stdout);
|
|
11314
11431
|
} catch (e) {
|
|
11315
|
-
await swapped.cleanup();
|
|
11316
11432
|
const err = e;
|
|
11433
|
+
const existing = parseExistingPr(`${err.stderr ?? ""}
|
|
11434
|
+
${err.message ?? ""}`);
|
|
11435
|
+
if (existing) {
|
|
11436
|
+
await swapped.cleanup();
|
|
11437
|
+
console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
|
|
11438
|
+
return { ...existing, existing: true };
|
|
11439
|
+
}
|
|
11440
|
+
await swapped.cleanup();
|
|
11317
11441
|
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
11318
11442
|
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
11319
11443
|
} finally {
|
|
@@ -11961,6 +12085,10 @@ function parseNpmVersion(stdout) {
|
|
|
11961
12085
|
function staleTrainCliMessage(report, commandName) {
|
|
11962
12086
|
return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`npm install -g @mutmutco/cli@latest\` to update to ${report.releasedVersion} (a standalone CLI shadows a plugin-provided mmi-cli on PATH and needs no Claude restart), then rerun ${commandName} so it uses the current train path. This gate runs before any train mutation, so this invocation changed nothing; any partial train state (local merge, pushed tag) is from an earlier run and the rerun resumes it safely`;
|
|
11963
12087
|
}
|
|
12088
|
+
function versionAutoUpdateAction(report, releasedSource) {
|
|
12089
|
+
if (report.ok || report.staleAgainst !== "released") return "none";
|
|
12090
|
+
return releasedSource === "gh" ? "npm-unreachable" : "npm";
|
|
12091
|
+
}
|
|
11964
12092
|
|
|
11965
12093
|
// src/plugin-cache-prune.ts
|
|
11966
12094
|
var PLUGIN_CACHE_KEEP = 2;
|
|
@@ -16401,14 +16529,31 @@ function registerSecretsCommands(program3) {
|
|
|
16401
16529
|
});
|
|
16402
16530
|
if (!report.ok) process.exitCode = 1;
|
|
16403
16531
|
});
|
|
16404
|
-
secrets.command("org-catalog").description("
|
|
16532
|
+
secrets.command("org-catalog").description("the _org/<provider>/* catalog declarations (#2244) \u2014 --show reads (any org member); writes are master-only and MERGE by default (#3297)").option("--show", "print the stored catalog (declarations only \u2014 names, purpose, owner; never values)").option("--json", "--show output as the exact { entries } JSON shape --file accepts (lossless read-edit-write round-trip)").option("--file <path>", 'JSON file: { "entries": { "<provider>/<KEY>": {key,purpose,group,owner,provider,stages[],consumers[]} } } \u2014 merged into the stored catalog').option("--replace", "DESTRUCTIVE: replace the whole catalog with --file exactly (requires --yes)").option("--remove <provider/KEY>", "delete one declaration from the stored catalog (repeatable)", collectMap, []).option("--yes", "confirm the destructive --replace").action((o) => withSecrets(async (d) => {
|
|
16533
|
+
if (o.show) {
|
|
16534
|
+
if (o.file || o.replace || o.remove?.length) return fail("secrets org-catalog: --show does not combine with --file/--replace/--remove");
|
|
16535
|
+
if (!await secretsOrgCatalogShow(d, { json: o.json })) process.exitCode = 1;
|
|
16536
|
+
return;
|
|
16537
|
+
}
|
|
16538
|
+
if (o.replace) {
|
|
16539
|
+
if (!o.file) return fail("secrets org-catalog: --replace needs --file (the complete catalog to write)");
|
|
16540
|
+
if (!o.yes) {
|
|
16541
|
+
return fail("secrets org-catalog --replace is DESTRUCTIVE \u2014 it deletes every declaration absent from the file (the #3297 data-loss form). Re-run with --yes to confirm; prefer the default merge, or --remove <provider/KEY> to delete one declaration.");
|
|
16542
|
+
}
|
|
16543
|
+
}
|
|
16544
|
+
if (!o.file && !o.remove?.length) {
|
|
16545
|
+
return fail("secrets org-catalog: nothing to do \u2014 pass --show, --file <path> (merged by default), or --remove <provider/KEY>");
|
|
16546
|
+
}
|
|
16405
16547
|
let body;
|
|
16406
|
-
|
|
16407
|
-
|
|
16408
|
-
|
|
16409
|
-
|
|
16548
|
+
if (o.file) {
|
|
16549
|
+
try {
|
|
16550
|
+
body = (0, import_node_fs18.readFileSync)((0, import_node_path16.resolve)(o.file), "utf8");
|
|
16551
|
+
} catch (e) {
|
|
16552
|
+
return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
|
|
16553
|
+
}
|
|
16410
16554
|
}
|
|
16411
|
-
|
|
16555
|
+
const ok = body !== void 0 ? await secretsOrgCatalogSet(d, body, { replace: o.replace, remove: o.remove }) : await secretsOrgCatalogRemove(d, o.remove ?? []);
|
|
16556
|
+
if (!ok) process.exitCode = 1;
|
|
16412
16557
|
}));
|
|
16413
16558
|
secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--skip-compose-guard", "skip the #2813 fileless-compose \u2194 DEPLOY#.noEnvFile promotion guard").option("--json", "machine-readable output").action(async (o) => {
|
|
16414
16559
|
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
@@ -16481,7 +16626,7 @@ function registerSecretsCommands(program3) {
|
|
|
16481
16626
|
const ok = await secretsSet(d, key, o);
|
|
16482
16627
|
if (!ok) process.exitCode = 1;
|
|
16483
16628
|
});
|
|
16484
|
-
secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
|
|
16629
|
+
secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix. --slug _org writes an org-infra <provider>/<KEY> coordinate (#3298) \u2014 master or an exact grant, declare-first enforced server-side").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for the value write \u2014 master or an exact grant (#3298)").action(setHandler);
|
|
16485
16630
|
secrets.command("copy").description("copy provider keys between stages in your own project vault (audit-logged; encryption keys blocked)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
|
|
16486
16631
|
const stages = ["dev", "rc", "main"];
|
|
16487
16632
|
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
@@ -16627,7 +16772,7 @@ async function mintInstallationToken(deps) {
|
|
|
16627
16772
|
}
|
|
16628
16773
|
return { token: body.token, expiresAt: body.expires_at };
|
|
16629
16774
|
}
|
|
16630
|
-
async function activateAppActor(
|
|
16775
|
+
async function activateAppActor(commandPath3, env, mint) {
|
|
16631
16776
|
const requested = (env[APP_ACTOR_ENV] ?? "").trim();
|
|
16632
16777
|
if (!requested) return "personal";
|
|
16633
16778
|
if (requested !== "app") {
|
|
@@ -16635,8 +16780,8 @@ async function activateAppActor(commandPath2, env, mint) {
|
|
|
16635
16780
|
}
|
|
16636
16781
|
delete env.GH_TOKEN;
|
|
16637
16782
|
delete env.GITHUB_TOKEN;
|
|
16638
|
-
if (!APP_ELIGIBLE_COMMANDS.has(
|
|
16639
|
-
throw new AppActorError(`app actor: '${
|
|
16783
|
+
if (!APP_ELIGIBLE_COMMANDS.has(commandPath3)) {
|
|
16784
|
+
throw new AppActorError(`app actor: '${commandPath3}' is personal-only \u2014 App identity would break viewer/@me/authorship semantics; unset ${APP_ACTOR_ENV} for this command`);
|
|
16640
16785
|
}
|
|
16641
16786
|
const minted = await mint();
|
|
16642
16787
|
env.GH_TOKEN = minted.token;
|
|
@@ -16960,9 +17105,17 @@ function awsScheduleEntry(payload) {
|
|
|
16960
17105
|
if (typeof Name !== "string" || typeof ScheduleExpression !== "string") return null;
|
|
16961
17106
|
if (State !== "ENABLED" || isJervResource(Name)) return null;
|
|
16962
17107
|
let target = "";
|
|
17108
|
+
let scheduleId;
|
|
16963
17109
|
if (Target && typeof Target === "object") {
|
|
16964
|
-
const { Arn } = Target;
|
|
17110
|
+
const { Arn, Input } = Target;
|
|
16965
17111
|
if (typeof Arn === "string") target = Arn.split(":").pop() ?? "";
|
|
17112
|
+
if (typeof Input === "string") {
|
|
17113
|
+
try {
|
|
17114
|
+
const parsed = JSON.parse(Input);
|
|
17115
|
+
if (typeof parsed?.scheduleId === "string" && parsed.scheduleId) scheduleId = parsed.scheduleId;
|
|
17116
|
+
} catch {
|
|
17117
|
+
}
|
|
17118
|
+
}
|
|
16966
17119
|
}
|
|
16967
17120
|
return {
|
|
16968
17121
|
name: Name,
|
|
@@ -16970,7 +17123,8 @@ function awsScheduleEntry(payload) {
|
|
|
16970
17123
|
executor: target ? `aws-scheduler \u2192 ${target}` : "aws-scheduler",
|
|
16971
17124
|
llm: readLlmDeclaration(typeof Description === "string" ? Description : void 0),
|
|
16972
17125
|
resolved: "live",
|
|
16973
|
-
source: `aws scheduler schedule ${Name} (eu-central-1)
|
|
17126
|
+
source: `aws scheduler schedule ${Name} (eu-central-1)`,
|
|
17127
|
+
...scheduleId ? { scheduleId } : {}
|
|
16974
17128
|
};
|
|
16975
17129
|
}
|
|
16976
17130
|
function awsScheduleRefs(payload) {
|
|
@@ -17138,7 +17292,28 @@ function unlauncheredLlmDrifts(entries) {
|
|
|
17138
17292
|
}
|
|
17139
17293
|
return drifts.sort((a, b) => a.name.localeCompare(b.name));
|
|
17140
17294
|
}
|
|
17141
|
-
|
|
17295
|
+
var HARBOUR_ARM_GRACE_MS = 30 * 60 * 1e3;
|
|
17296
|
+
function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
|
|
17297
|
+
if (!opts.schedulerRead) return [];
|
|
17298
|
+
const now = opts.now ?? Date.now();
|
|
17299
|
+
const armed = new Set(liveEntries.map((e) => e.scheduleId).filter((id) => Boolean(id)));
|
|
17300
|
+
const drifts = [];
|
|
17301
|
+
for (const row of registry2) {
|
|
17302
|
+
if (row.executor === "github-actions") continue;
|
|
17303
|
+
if (armed.has(row.id)) continue;
|
|
17304
|
+
const stamped = row.updatedAt ? Date.parse(row.updatedAt) : NaN;
|
|
17305
|
+
if (Number.isFinite(stamped) && now - stamped < HARBOUR_ARM_GRACE_MS) continue;
|
|
17306
|
+
drifts.push({
|
|
17307
|
+
class: "registered-but-unarmed",
|
|
17308
|
+
name: row.id,
|
|
17309
|
+
executor: row.executor,
|
|
17310
|
+
detail: "registered as a harbour lane but no aws-scheduler clock dispatches it \u2014 it will never fire",
|
|
17311
|
+
remedy: "check the fleet-clock reconciler tick; re-run `mmi-cli org schedules register` for the repo and verify with `mmi-cli org schedules`"
|
|
17312
|
+
});
|
|
17313
|
+
}
|
|
17314
|
+
return drifts.sort((a, b) => a.name.localeCompare(b.name));
|
|
17315
|
+
}
|
|
17316
|
+
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}) {
|
|
17142
17317
|
if (registry2 === null) {
|
|
17143
17318
|
return {
|
|
17144
17319
|
reconciliation: [],
|
|
@@ -17147,7 +17322,13 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
|
|
|
17147
17322
|
};
|
|
17148
17323
|
}
|
|
17149
17324
|
const live = githubEntries2.filter((e) => e.executor === "github-actions");
|
|
17150
|
-
const drifts =
|
|
17325
|
+
const drifts = [
|
|
17326
|
+
...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames),
|
|
17327
|
+
...registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
|
|
17328
|
+
schedulerRead: Boolean(harbour.schedulerRead),
|
|
17329
|
+
now: harbour.now
|
|
17330
|
+
})
|
|
17331
|
+
];
|
|
17151
17332
|
return { reconciliation: drifts, driftLines: drifts.map(renderDrift), incomplete: [] };
|
|
17152
17333
|
}
|
|
17153
17334
|
|
|
@@ -17260,6 +17441,7 @@ async function awsEntries() {
|
|
|
17260
17441
|
} catch (e) {
|
|
17261
17442
|
incomplete.push(`aws: events list-rules failed \u2014 ${e.message}`);
|
|
17262
17443
|
}
|
|
17444
|
+
let schedulerRead = false;
|
|
17263
17445
|
try {
|
|
17264
17446
|
const refs = awsScheduleRefs(await awsJson(["scheduler", "list-schedules"]));
|
|
17265
17447
|
for (const ref of refs) {
|
|
@@ -17267,11 +17449,12 @@ async function awsEntries() {
|
|
|
17267
17449
|
const entry = awsScheduleEntry(await awsJson(args));
|
|
17268
17450
|
if (entry) entries.push(entry);
|
|
17269
17451
|
}
|
|
17452
|
+
schedulerRead = true;
|
|
17270
17453
|
} catch (e) {
|
|
17271
17454
|
incomplete.push(`aws: scheduler listing failed \u2014 ${e.message}`);
|
|
17272
17455
|
}
|
|
17273
17456
|
const reconciliation = unlauncheredLlmDrifts(entries);
|
|
17274
|
-
return { entries, incomplete, drift: reconciliation.map(renderDrift), reconciliation };
|
|
17457
|
+
return { entries, incomplete, drift: reconciliation.map(renderDrift), reconciliation, schedulerRead };
|
|
17275
17458
|
}
|
|
17276
17459
|
async function defaultRegistryRead() {
|
|
17277
17460
|
return fetchSchedulesList(registryClientDeps(await loadConfig()));
|
|
@@ -17283,7 +17466,11 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
|
|
|
17283
17466
|
readRegistry().catch(() => null),
|
|
17284
17467
|
readProjects().catch(() => null)
|
|
17285
17468
|
]);
|
|
17286
|
-
const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2, new Set(gh.workflowNames)
|
|
17469
|
+
const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2, new Set(gh.workflowNames), {
|
|
17470
|
+
// #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
|
|
17471
|
+
awsEntries: aws.entries,
|
|
17472
|
+
schedulerRead: Boolean(aws.schedulerRead)
|
|
17473
|
+
});
|
|
17287
17474
|
const selfManaged = /* @__PURE__ */ new Set();
|
|
17288
17475
|
for (const proj of projects ?? []) {
|
|
17289
17476
|
if (proj?.schedulesMode !== "self-managed") continue;
|
|
@@ -17526,17 +17713,33 @@ async function runSchedulesLift(opts, deps = {}) {
|
|
|
17526
17713
|
}
|
|
17527
17714
|
return { repo, records, payload, response };
|
|
17528
17715
|
}
|
|
17716
|
+
var FLEET_CLOCK_RECONCILE_WINDOW = "~15 minutes";
|
|
17717
|
+
var ARMING_IS_EVENTUAL_NOTE = ` registered \u2014 not yet armed. The fleet-clock reconciler arms SCHEDULE# rows on its next tick (${FLEET_CLOCK_RECONCILE_WINDOW}).
|
|
17718
|
+
Verify with \`mmi-cli org schedules\` after that window; an empty result before it means "not reconciled yet", not "will never fire".`;
|
|
17719
|
+
var ARMING_REPORT = {
|
|
17720
|
+
armed: false,
|
|
17721
|
+
state: "pending-reconcile",
|
|
17722
|
+
withinMinutes: 15,
|
|
17723
|
+
verifyWith: "mmi-cli org schedules",
|
|
17724
|
+
report: "registered successfully \u2014 arms on the next fleet-clock tick, not yet",
|
|
17725
|
+
note: `SCHEDULE# rows are written now; the fleet-clock reconciler arms them within ${FLEET_CLOCK_RECONCILE_WINDOW}. Before that window an absent clock means "not reconciled yet", NOT "will never fire" \u2014 do not report this as armed, live, or failed.`
|
|
17726
|
+
};
|
|
17727
|
+
function withArmingReport(body) {
|
|
17728
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return body;
|
|
17729
|
+
return { ...body, arming: ARMING_REPORT };
|
|
17730
|
+
}
|
|
17529
17731
|
function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
17530
17732
|
const schedules = program3.commands.find((c) => c.name() === "schedules");
|
|
17531
17733
|
if (!schedules) throw new Error("schedules lift: registerSchedulesCommands must run first \u2014 the lift attaches to the `schedules` command");
|
|
17532
|
-
schedules.command("register").alias("lift").description(
|
|
17734
|
+
schedules.command("register").alias("lift").description(`register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from \`lift\`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187). Registering does NOT arm the clock: the fleet-clock reconciler arms the rows within ${FLEET_CLOCK_RECONCILE_WINDOW}, and the success JSON carries an \`arming\` block saying so (#3281)`).option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to lift (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write").action(async (o) => {
|
|
17533
17735
|
try {
|
|
17534
17736
|
const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
|
|
17535
17737
|
if (o.dryRun) {
|
|
17536
17738
|
console.log(JSON.stringify(result.payload, null, 2));
|
|
17537
17739
|
return;
|
|
17538
17740
|
}
|
|
17539
|
-
console.log(JSON.stringify(result.response?.body));
|
|
17741
|
+
console.log(JSON.stringify(withArmingReport(result.response?.body)));
|
|
17742
|
+
console.error(ARMING_IS_EVENTUAL_NOTE);
|
|
17540
17743
|
} catch (e) {
|
|
17541
17744
|
const err = e;
|
|
17542
17745
|
if (err instanceof RegistryUnreachableError) {
|
|
@@ -19271,9 +19474,6 @@ function registerStageCommands(program3) {
|
|
|
19271
19474
|
|
|
19272
19475
|
// src/config-load.ts
|
|
19273
19476
|
var discoveredConfig = null;
|
|
19274
|
-
function registryDegradeError(error) {
|
|
19275
|
-
return new Error(`Hub registry read failed (${error}) \u2014 board coords could not be discovered; likely transient (cold start, network, or auth blip) \u2014 retry shortly`);
|
|
19276
|
-
}
|
|
19277
19477
|
async function loadConfigOrDiscover() {
|
|
19278
19478
|
if (discoveredConfig) return discoveredConfig;
|
|
19279
19479
|
const floor = await loadConfig();
|
|
@@ -20721,6 +20921,7 @@ function validateBatchSpecs(specs) {
|
|
|
20721
20921
|
return { ok: errors.length === 0, errors, validated };
|
|
20722
20922
|
}
|
|
20723
20923
|
async function createIssuesBatch(specs, options, deps = {}) {
|
|
20924
|
+
const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
|
|
20724
20925
|
const client = deps.client ?? defaultGitHubClient();
|
|
20725
20926
|
const validation = validateBatchSpecs(specs);
|
|
20726
20927
|
if (!validation.ok) {
|
|
@@ -20733,6 +20934,14 @@ ${lines}`);
|
|
|
20733
20934
|
throw new Error("could not resolve repo \u2014 pass --repo owner/repo or set repo per row");
|
|
20734
20935
|
}
|
|
20735
20936
|
const rowRepo = (spec) => spec.repo ?? defaultRepo;
|
|
20937
|
+
const labelsByRepo = /* @__PURE__ */ new Map();
|
|
20938
|
+
for (const { spec } of validation.validated) {
|
|
20939
|
+
if (!spec.labels?.length) continue;
|
|
20940
|
+
const bucket = labelsByRepo.get(rowRepo(spec)) ?? /* @__PURE__ */ new Set();
|
|
20941
|
+
for (const label of spec.labels) bucket.add(label);
|
|
20942
|
+
labelsByRepo.set(rowRepo(spec), bucket);
|
|
20943
|
+
}
|
|
20944
|
+
for (const [repo, labels] of labelsByRepo) await ensureLabels([...labels], repo);
|
|
20736
20945
|
const created = [];
|
|
20737
20946
|
const failures = [];
|
|
20738
20947
|
for (const entry of validation.validated) {
|
|
@@ -21140,6 +21349,15 @@ async function fetchNpmReleasedVersion() {
|
|
|
21140
21349
|
return void 0;
|
|
21141
21350
|
}
|
|
21142
21351
|
}
|
|
21352
|
+
var NPM_INSTALL_TIMEOUT_MS = 12e4;
|
|
21353
|
+
async function npmSelfUpdateCli() {
|
|
21354
|
+
try {
|
|
21355
|
+
await runHostBin("npm", ["install", "-g", "@mutmutco/cli@latest"], { timeout: NPM_INSTALL_TIMEOUT_MS });
|
|
21356
|
+
return { ok: true, detail: "npm install -g @mutmutco/cli@latest exited 0" };
|
|
21357
|
+
} catch (e) {
|
|
21358
|
+
return { ok: false, detail: e.message.trim().slice(0, 200).replace(/\s+/g, " ") };
|
|
21359
|
+
}
|
|
21360
|
+
}
|
|
21143
21361
|
function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
|
|
21144
21362
|
const homeDir = surface === "codex" ? ".codex" : ".claude";
|
|
21145
21363
|
const installed = readInstalledPlugins(surface);
|
|
@@ -21198,6 +21416,17 @@ async function applyPluginHeal(surface, log, opts) {
|
|
|
21198
21416
|
}
|
|
21199
21417
|
return true;
|
|
21200
21418
|
}
|
|
21419
|
+
async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
|
|
21420
|
+
if (surfaceToken(surface) !== "claude") {
|
|
21421
|
+
return { ok: false, detail: `not a Claude surface (${surface}) \u2014 no Hub-shipped plugin to reinstall` };
|
|
21422
|
+
}
|
|
21423
|
+
const steps = [];
|
|
21424
|
+
const ok = await applyPluginHeal(surface, (msg) => steps.push(msg.trim()));
|
|
21425
|
+
return {
|
|
21426
|
+
ok,
|
|
21427
|
+
detail: ok ? "marketplace remove \u2192 add \u2192 install succeeded" : `\`claude plugin\` reinstall failed or was skipped${steps.length ? ` (${steps[steps.length - 1]})` : ""}`
|
|
21428
|
+
};
|
|
21429
|
+
}
|
|
21201
21430
|
async function runGuard(readOrigin) {
|
|
21202
21431
|
try {
|
|
21203
21432
|
const surface = detectSurface(process.env);
|
|
@@ -21733,9 +21962,9 @@ function formatExplainLoop(playbook) {
|
|
|
21733
21962
|
}
|
|
21734
21963
|
return lines.join("\n");
|
|
21735
21964
|
}
|
|
21736
|
-
function findCommandInManifest(manifest,
|
|
21965
|
+
function findCommandInManifest(manifest, commandPath3) {
|
|
21737
21966
|
const visit = (command) => {
|
|
21738
|
-
if (command.path ===
|
|
21967
|
+
if (command.path === commandPath3) return command;
|
|
21739
21968
|
for (const child2 of command.subcommands) {
|
|
21740
21969
|
const found = visit(child2);
|
|
21741
21970
|
if (found) return found;
|
|
@@ -21760,10 +21989,10 @@ function registerExplainCommand(program3) {
|
|
|
21760
21989
|
console.log(formatManifestHuman(manifest));
|
|
21761
21990
|
return;
|
|
21762
21991
|
}
|
|
21763
|
-
const
|
|
21764
|
-
const command = findCommandInManifest(manifest,
|
|
21992
|
+
const commandPath3 = commandArgs.join(" ");
|
|
21993
|
+
const command = findCommandInManifest(manifest, commandPath3);
|
|
21765
21994
|
if (!command) {
|
|
21766
|
-
fail(`explain: unknown command "${
|
|
21995
|
+
fail(`explain: unknown command "${commandPath3}"`, { code: ERROR_CODES.ERR_NOT_FOUND });
|
|
21767
21996
|
return;
|
|
21768
21997
|
}
|
|
21769
21998
|
console.log(command.subcommands.length ? formatExplainGroup(command, manifest.name) : formatExplainCommand(command, manifest.name));
|
|
@@ -22392,6 +22621,11 @@ function checkRepoWorktrees(probe) {
|
|
|
22392
22621
|
]
|
|
22393
22622
|
};
|
|
22394
22623
|
}
|
|
22624
|
+
function pluginHealTrigger(probe) {
|
|
22625
|
+
if (probe.guardState === "no-install" || probe.guardState === "unresolved") return "unresolved";
|
|
22626
|
+
const behind = Boolean(probe.installed && probe.released) && compareVersions(probe.installed, probe.released) < 0;
|
|
22627
|
+
return behind ? "behind" : null;
|
|
22628
|
+
}
|
|
22395
22629
|
function checkClaudePlugin(probe) {
|
|
22396
22630
|
const { installed, released, guardState } = probe;
|
|
22397
22631
|
const evidence = [
|
|
@@ -22399,20 +22633,22 @@ function checkClaudePlugin(probe) {
|
|
|
22399
22633
|
`released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
|
|
22400
22634
|
`resolvable: ${guardState}`
|
|
22401
22635
|
];
|
|
22402
|
-
|
|
22636
|
+
const trigger = pluginHealTrigger(probe);
|
|
22637
|
+
if (trigger === "unresolved") {
|
|
22403
22638
|
return {
|
|
22404
22639
|
ok: false,
|
|
22405
22640
|
label: guardState === "no-install" ? "Claude plugin \u2014 not installed" : "Claude plugin \u2014 unresolved (marketplace/cache missing)",
|
|
22406
|
-
fix: "run `mmi-cli plugin heal` to reinstall the MMI marketplace + plugin, then restart Claude",
|
|
22641
|
+
fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall the MMI marketplace + plugin, then restart Claude",
|
|
22407
22642
|
verbose: evidence
|
|
22408
22643
|
};
|
|
22409
22644
|
}
|
|
22410
|
-
|
|
22411
|
-
if (behind) {
|
|
22645
|
+
if (trigger === "behind") {
|
|
22412
22646
|
return {
|
|
22413
22647
|
ok: false,
|
|
22414
22648
|
label: `Claude plugin ${installed} \u2192 ${released}`,
|
|
22415
|
-
|
|
22649
|
+
// Name the CLI verb that actually fixes it, like every other red row — `/plugin` is the manual
|
|
22650
|
+
// fallback, not the first resort (#3282).
|
|
22651
|
+
fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall it, then restart Claude",
|
|
22416
22652
|
verbose: evidence
|
|
22417
22653
|
};
|
|
22418
22654
|
}
|
|
@@ -22564,10 +22800,53 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
22564
22800
|
checks.push({ ok: false, label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
22565
22801
|
}
|
|
22566
22802
|
}
|
|
22567
|
-
const
|
|
22568
|
-
|
|
22569
|
-
if (
|
|
22570
|
-
|
|
22803
|
+
const pluginProbe = { installed, released, guardState: deps.pluginGuardState(isOrgRepo) };
|
|
22804
|
+
const healTrigger = pluginHealTrigger(pluginProbe);
|
|
22805
|
+
if (apply && deps.healPlugin && healTrigger) {
|
|
22806
|
+
const heal = await deps.healPlugin();
|
|
22807
|
+
const label = healTrigger === "behind" ? `Claude plugin ${installed} \u2192 ${released}` : "Claude plugin \u2014 reinstalled";
|
|
22808
|
+
const healEvidence = [
|
|
22809
|
+
`installed: ${installed ?? "(none)"}`,
|
|
22810
|
+
`released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
|
|
22811
|
+
`resolvable: ${pluginProbe.guardState}`,
|
|
22812
|
+
`heal: ${heal.detail}`
|
|
22813
|
+
];
|
|
22814
|
+
checks.push(heal.ok ? {
|
|
22815
|
+
ok: true,
|
|
22816
|
+
label,
|
|
22817
|
+
detail: "reinstalled via the MMI marketplace (remove \u2192 add \u2192 install)",
|
|
22818
|
+
verbose: healEvidence
|
|
22819
|
+
} : {
|
|
22820
|
+
ok: false,
|
|
22821
|
+
label,
|
|
22822
|
+
fix: `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then restart Claude`,
|
|
22823
|
+
verbose: healEvidence
|
|
22824
|
+
});
|
|
22825
|
+
restartPending = true;
|
|
22826
|
+
} else {
|
|
22827
|
+
const plugin = checkClaudePlugin(pluginProbe);
|
|
22828
|
+
checks.push(plugin);
|
|
22829
|
+
if (!plugin.ok) restartPending = true;
|
|
22830
|
+
}
|
|
22831
|
+
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
22832
|
+
const cliReport = buildVersionLagReport(cliInput);
|
|
22833
|
+
if (apply && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
|
|
22834
|
+
const heal = await deps.updateCli();
|
|
22835
|
+
const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
|
|
22836
|
+
checks.push(heal.ok ? {
|
|
22837
|
+
ok: true,
|
|
22838
|
+
label: `mmi-cli ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
|
|
22839
|
+
detail: "self-updated via npm install -g; the next mmi-cli invocation runs the new version",
|
|
22840
|
+
verbose: healEvidence
|
|
22841
|
+
} : {
|
|
22842
|
+
ok: false,
|
|
22843
|
+
label: `mmi-cli ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
|
|
22844
|
+
fix: `self-update failed (${heal.detail}) \u2014 run \`npm install -g @mutmutco/cli@latest\``,
|
|
22845
|
+
verbose: healEvidence
|
|
22846
|
+
});
|
|
22847
|
+
} else {
|
|
22848
|
+
checks.push(checkCliVersion(cliInput));
|
|
22849
|
+
}
|
|
22571
22850
|
checks.push(checkPluginCache(deps.pluginCache()));
|
|
22572
22851
|
if (!opts.fast && !opts.banner && !opts.preflight) {
|
|
22573
22852
|
const probe = await deps.schedulesNotebook().catch((e) => ({
|
|
@@ -22773,6 +23052,11 @@ function mmiDoctorDeps() {
|
|
|
22773
23052
|
installedPluginVersion: installedClaudePluginVersion,
|
|
22774
23053
|
pluginGuardState: claudePluginGuardState,
|
|
22775
23054
|
releasedVersion: fetchNpmReleasedVersion,
|
|
23055
|
+
// #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
|
|
23056
|
+
updateCli: npmSelfUpdateCli,
|
|
23057
|
+
// #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
|
|
23058
|
+
// `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
|
|
23059
|
+
healPlugin: () => healClaudePluginForDoctor(),
|
|
22776
23060
|
currentCliVersion: resolveClientVersion,
|
|
22777
23061
|
readGitignore,
|
|
22778
23062
|
writeGitignore,
|
|
@@ -22873,8 +23157,57 @@ function argvWantsJson2() {
|
|
|
22873
23157
|
return process.argv.some((a) => a === "--json" || a.startsWith("--json="));
|
|
22874
23158
|
}
|
|
22875
23159
|
var unknownFlagJsonHandled = false;
|
|
23160
|
+
var PARSE_HINT_SENTINEL = "@@mmi-parse-hint@@";
|
|
23161
|
+
var DISCOVERY_HINT = "run `mmi-cli commands` for the agentic-coding map, `mmi-cli explain <command>` for detail, or `mmi-cli commands --json --primary` for machine discovery";
|
|
23162
|
+
var STALE_HINT = `(${DISCOVERY_HINT}). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)`;
|
|
23163
|
+
var lastParseErrorKind = "other";
|
|
23164
|
+
function classifyParseError(plain) {
|
|
23165
|
+
if (/unknown command/i.test(plain)) return "unknown-command";
|
|
23166
|
+
if (/unknown option|missing required argument|too many arguments|argument missing/i.test(plain)) {
|
|
23167
|
+
return "bad-arguments";
|
|
23168
|
+
}
|
|
23169
|
+
return "other";
|
|
23170
|
+
}
|
|
23171
|
+
function resolveCommandFromArgv(root, argv) {
|
|
23172
|
+
let current = root;
|
|
23173
|
+
for (const token of argv) {
|
|
23174
|
+
if (token.startsWith("-")) break;
|
|
23175
|
+
const next = current.commands.find(
|
|
23176
|
+
(c) => c.name() === token || c.aliases().includes(token)
|
|
23177
|
+
);
|
|
23178
|
+
if (!next) break;
|
|
23179
|
+
current = next;
|
|
23180
|
+
}
|
|
23181
|
+
return current === root ? void 0 : current;
|
|
23182
|
+
}
|
|
23183
|
+
function commandPath2(cmd) {
|
|
23184
|
+
const parts = [];
|
|
23185
|
+
let node = cmd;
|
|
23186
|
+
while (node && node.parent) {
|
|
23187
|
+
parts.unshift(node.name());
|
|
23188
|
+
node = node.parent;
|
|
23189
|
+
}
|
|
23190
|
+
return parts.join(" ");
|
|
23191
|
+
}
|
|
23192
|
+
function resolveParseHint() {
|
|
23193
|
+
if (lastParseErrorKind !== "bad-arguments") return STALE_HINT;
|
|
23194
|
+
const cmd = resolveCommandFromArgv(program2, process.argv.slice(2));
|
|
23195
|
+
if (!cmd) return `(${DISCOVERY_HINT})`;
|
|
23196
|
+
const path2 = commandPath2(cmd);
|
|
23197
|
+
return [
|
|
23198
|
+
`Usage: mmi-cli ${path2} ${cmd.usage()}`,
|
|
23199
|
+
"This command exists and your mmi-cli is not the problem \u2014 the ARGUMENTS did not parse.",
|
|
23200
|
+
`Run \`mmi-cli ${path2} --help\` for its signature, \`mmi-cli explain ${path2}\` for detail, or \`mmi-cli commands\` for the full map.`
|
|
23201
|
+
].join("\n");
|
|
23202
|
+
}
|
|
22876
23203
|
function envelopeAwareWriteErr(str) {
|
|
22877
23204
|
const plain = str.replace(/\[[0-9;]*m/g, "");
|
|
23205
|
+
if (plain.includes(PARSE_HINT_SENTINEL)) {
|
|
23206
|
+
if (unknownFlagJsonHandled && argvWantsJson2()) return;
|
|
23207
|
+
process.stderr.write(str.replace(PARSE_HINT_SENTINEL, resolveParseHint()));
|
|
23208
|
+
return;
|
|
23209
|
+
}
|
|
23210
|
+
lastParseErrorKind = classifyParseError(plain);
|
|
22878
23211
|
const match = /unknown option '([^']+)'/.exec(plain);
|
|
22879
23212
|
if (match) {
|
|
22880
23213
|
const flag = match[1];
|
|
@@ -22899,7 +23232,7 @@ function envelopeAwareWriteErr(str) {
|
|
|
22899
23232
|
process.stderr.write(str);
|
|
22900
23233
|
}
|
|
22901
23234
|
var program2 = new Command();
|
|
22902
|
-
program2.name("mmi-cli").description("MMI Future Hub CLI \u2014 the org control plane for agentic coding.").version(resolveClientVersion()).configureOutput({ writeErr: envelopeAwareWriteErr }).showHelpAfterError(
|
|
23235
|
+
program2.name("mmi-cli").description("MMI Future Hub CLI \u2014 the org control plane for agentic coding.").version(resolveClientVersion()).configureOutput({ writeErr: envelopeAwareWriteErr }).showHelpAfterError(PARSE_HINT_SENTINEL);
|
|
22903
23236
|
function appActorDeps() {
|
|
22904
23237
|
return {
|
|
22905
23238
|
fetchSecret: async (key) => fetchSecretValue(makeSecretsDeps(await loadConfig()), key, { repo: APP_VAULT_REPO }),
|
|
@@ -23858,6 +24191,7 @@ withExamples(mutating(
|
|
|
23858
24191
|
let title;
|
|
23859
24192
|
let issueType;
|
|
23860
24193
|
let extraLabels = [];
|
|
24194
|
+
let targetRepo2;
|
|
23861
24195
|
try {
|
|
23862
24196
|
issueType = resolveCreateType(o.type, "issue create");
|
|
23863
24197
|
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
|
|
@@ -23865,33 +24199,30 @@ withExamples(mutating(
|
|
|
23865
24199
|
if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
|
|
23866
24200
|
priority = resolveCreatePriority(o.priority, "issue create");
|
|
23867
24201
|
extraLabels = [...o.label ?? []];
|
|
24202
|
+
targetRepo2 = await resolveRepo(o.repo);
|
|
24203
|
+
if (!targetRepo2) {
|
|
24204
|
+
return fail("issue create: could not resolve the target repo \u2014 run inside a git checkout or pass --repo <owner/repo>");
|
|
24205
|
+
}
|
|
23868
24206
|
args = buildIssueArgs({
|
|
23869
24207
|
type: issueType,
|
|
23870
24208
|
title,
|
|
23871
24209
|
body,
|
|
23872
24210
|
priority,
|
|
23873
|
-
repo:
|
|
24211
|
+
repo: targetRepo2,
|
|
23874
24212
|
labels: extraLabels.length ? extraLabels : void 0
|
|
23875
24213
|
});
|
|
23876
24214
|
if (o.parent !== void 0) parseIssueRef(o.parent);
|
|
23877
24215
|
} catch (e) {
|
|
23878
24216
|
return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
|
|
23879
24217
|
}
|
|
23880
|
-
|
|
23881
|
-
const la = ["label", "create", label, "--color", "ededed"];
|
|
23882
|
-
if (o.repo) la.push("--repo", o.repo);
|
|
23883
|
-
try {
|
|
23884
|
-
await execFileP2("gh", la, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
23885
|
-
} catch {
|
|
23886
|
-
}
|
|
23887
|
-
}
|
|
24218
|
+
await ensureLabelsExist(extraLabels, targetRepo2);
|
|
23888
24219
|
const created = await ghCreate(args);
|
|
23889
|
-
const { projectItemId, onBoard } = await attachToProject(created.number,
|
|
24220
|
+
const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo2, priority);
|
|
23890
24221
|
let parent;
|
|
23891
24222
|
let parentLinkError;
|
|
23892
24223
|
if (o.parent !== void 0) {
|
|
23893
24224
|
try {
|
|
23894
|
-
parent = await linkSubIssue(ghRunner2, o.parent, created.url,
|
|
24225
|
+
parent = await linkSubIssue(ghRunner2, o.parent, created.url, targetRepo2);
|
|
23895
24226
|
} catch (e) {
|
|
23896
24227
|
const err = e;
|
|
23897
24228
|
parentLinkError = (err.stderr || err.message || String(e)).trim();
|
|
@@ -24369,7 +24700,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
24369
24700
|
}
|
|
24370
24701
|
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
|
24371
24702
|
});
|
|
24372
|
-
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)").action(async (number, o) => {
|
|
24703
|
+
pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)").action(async (number, o) => {
|
|
24373
24704
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
24374
24705
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
24375
24706
|
const [headRef, baseRef, headRefOid] = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName,headRefOid", "--jq", '.headRefName + " " + .baseRefName + " " + (.headRefOid // "")'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim().split(/\s+/);
|
|
@@ -24380,6 +24711,25 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
24380
24711
|
(await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
24381
24712
|
);
|
|
24382
24713
|
const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo);
|
|
24714
|
+
if (o.wait) {
|
|
24715
|
+
const repo = await requireRepo(o.repo);
|
|
24716
|
+
const baseBranch = await fetchRestPrSnapshot(number, repo).then((s) => s.baseRef).catch(() => "development");
|
|
24717
|
+
const wait = await waitForPrChecks({
|
|
24718
|
+
resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
|
|
24719
|
+
pollChecks: () => pollRestPrChecks(number, repo),
|
|
24720
|
+
pollMergeable: () => pollRestPrMergeable(number, repo),
|
|
24721
|
+
pollRateLimit: () => fetchRestCorePool(),
|
|
24722
|
+
baseBranch,
|
|
24723
|
+
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
24724
|
+
log: (message) => console.warn(message),
|
|
24725
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
24726
|
+
});
|
|
24727
|
+
if (wait.status !== "success" && wait.status !== "skipped") {
|
|
24728
|
+
console.warn(`pr merge: --wait stopped before merge \u2014 ${wait.status}${wait.reason ? `: ${wait.reason}` : ""}${wait.detail ? ` (${wait.detail})` : ""}`);
|
|
24729
|
+
process.exitCode = wait.status === "timeout" || wait.status === "rate-limited" ? PR_CHECKS_TIMEOUT_EXIT_CODE : 1;
|
|
24730
|
+
return;
|
|
24731
|
+
}
|
|
24732
|
+
}
|
|
24383
24733
|
if (ciPolicy.policy === "no-ci") {
|
|
24384
24734
|
const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
|
|
24385
24735
|
if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
|
|
@@ -24430,9 +24780,11 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
24430
24780
|
if (o.auto || upgradedToAuto) {
|
|
24431
24781
|
const state = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
24432
24782
|
if (state !== "MERGED") {
|
|
24433
|
-
|
|
24783
|
+
const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
|
|
24784
|
+
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
|
|
24785
|
+
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
|
|
24434
24786
|
if (upgradedToAuto && !o.auto) {
|
|
24435
|
-
console.warn(`pr merge:
|
|
24787
|
+
console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
|
|
24436
24788
|
process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
|
|
24437
24789
|
}
|
|
24438
24790
|
return;
|
|
@@ -24974,6 +25326,7 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
|
|
|
24974
25326
|
0 && (module.exports = {
|
|
24975
25327
|
DEFAULT_PRIORITY,
|
|
24976
25328
|
awsCallerArn,
|
|
25329
|
+
classifyParseError,
|
|
24977
25330
|
gcPlan,
|
|
24978
25331
|
isOrgRegisteredRepo,
|
|
24979
25332
|
registryClientDeps,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.39.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",
|