@mutmutco/cli 3.38.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 +353 -51
- 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";
|
|
@@ -11317,14 +11417,27 @@ function buildPrArgs({ title, body, base, head, repo, draft }) {
|
|
|
11317
11417
|
if (draft) args.push("--draft");
|
|
11318
11418
|
return args;
|
|
11319
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
|
+
}
|
|
11320
11426
|
async function ghCreate(args) {
|
|
11321
11427
|
const swapped = await bodyArgsViaFile(args);
|
|
11322
11428
|
try {
|
|
11323
11429
|
const { stdout } = await execFileP2("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
|
|
11324
11430
|
return parseCreatedUrl(stdout);
|
|
11325
11431
|
} catch (e) {
|
|
11326
|
-
await swapped.cleanup();
|
|
11327
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();
|
|
11328
11441
|
const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
|
|
11329
11442
|
fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
|
|
11330
11443
|
} finally {
|
|
@@ -16416,14 +16529,31 @@ function registerSecretsCommands(program3) {
|
|
|
16416
16529
|
});
|
|
16417
16530
|
if (!report.ok) process.exitCode = 1;
|
|
16418
16531
|
});
|
|
16419
|
-
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
|
+
}
|
|
16420
16547
|
let body;
|
|
16421
|
-
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
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
|
+
}
|
|
16425
16554
|
}
|
|
16426
|
-
|
|
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;
|
|
16427
16557
|
}));
|
|
16428
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) => {
|
|
16429
16559
|
if (!["dev", "rc", "main"].includes(o.stage)) {
|
|
@@ -16496,7 +16626,7 @@ function registerSecretsCommands(program3) {
|
|
|
16496
16626
|
const ok = await secretsSet(d, key, o);
|
|
16497
16627
|
if (!ok) process.exitCode = 1;
|
|
16498
16628
|
});
|
|
16499
|
-
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);
|
|
16500
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) => {
|
|
16501
16631
|
const stages = ["dev", "rc", "main"];
|
|
16502
16632
|
if (!stages.includes(o.from) || !stages.includes(o.to)) {
|
|
@@ -16642,7 +16772,7 @@ async function mintInstallationToken(deps) {
|
|
|
16642
16772
|
}
|
|
16643
16773
|
return { token: body.token, expiresAt: body.expires_at };
|
|
16644
16774
|
}
|
|
16645
|
-
async function activateAppActor(
|
|
16775
|
+
async function activateAppActor(commandPath3, env, mint) {
|
|
16646
16776
|
const requested = (env[APP_ACTOR_ENV] ?? "").trim();
|
|
16647
16777
|
if (!requested) return "personal";
|
|
16648
16778
|
if (requested !== "app") {
|
|
@@ -16650,8 +16780,8 @@ async function activateAppActor(commandPath2, env, mint) {
|
|
|
16650
16780
|
}
|
|
16651
16781
|
delete env.GH_TOKEN;
|
|
16652
16782
|
delete env.GITHUB_TOKEN;
|
|
16653
|
-
if (!APP_ELIGIBLE_COMMANDS.has(
|
|
16654
|
-
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`);
|
|
16655
16785
|
}
|
|
16656
16786
|
const minted = await mint();
|
|
16657
16787
|
env.GH_TOKEN = minted.token;
|
|
@@ -16975,9 +17105,17 @@ function awsScheduleEntry(payload) {
|
|
|
16975
17105
|
if (typeof Name !== "string" || typeof ScheduleExpression !== "string") return null;
|
|
16976
17106
|
if (State !== "ENABLED" || isJervResource(Name)) return null;
|
|
16977
17107
|
let target = "";
|
|
17108
|
+
let scheduleId;
|
|
16978
17109
|
if (Target && typeof Target === "object") {
|
|
16979
|
-
const { Arn } = Target;
|
|
17110
|
+
const { Arn, Input } = Target;
|
|
16980
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
|
+
}
|
|
16981
17119
|
}
|
|
16982
17120
|
return {
|
|
16983
17121
|
name: Name,
|
|
@@ -16985,7 +17123,8 @@ function awsScheduleEntry(payload) {
|
|
|
16985
17123
|
executor: target ? `aws-scheduler \u2192 ${target}` : "aws-scheduler",
|
|
16986
17124
|
llm: readLlmDeclaration(typeof Description === "string" ? Description : void 0),
|
|
16987
17125
|
resolved: "live",
|
|
16988
|
-
source: `aws scheduler schedule ${Name} (eu-central-1)
|
|
17126
|
+
source: `aws scheduler schedule ${Name} (eu-central-1)`,
|
|
17127
|
+
...scheduleId ? { scheduleId } : {}
|
|
16989
17128
|
};
|
|
16990
17129
|
}
|
|
16991
17130
|
function awsScheduleRefs(payload) {
|
|
@@ -17153,7 +17292,28 @@ function unlauncheredLlmDrifts(entries) {
|
|
|
17153
17292
|
}
|
|
17154
17293
|
return drifts.sort((a, b) => a.name.localeCompare(b.name));
|
|
17155
17294
|
}
|
|
17156
|
-
|
|
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 = {}) {
|
|
17157
17317
|
if (registry2 === null) {
|
|
17158
17318
|
return {
|
|
17159
17319
|
reconciliation: [],
|
|
@@ -17162,7 +17322,13 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
|
|
|
17162
17322
|
};
|
|
17163
17323
|
}
|
|
17164
17324
|
const live = githubEntries2.filter((e) => e.executor === "github-actions");
|
|
17165
|
-
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
|
+
];
|
|
17166
17332
|
return { reconciliation: drifts, driftLines: drifts.map(renderDrift), incomplete: [] };
|
|
17167
17333
|
}
|
|
17168
17334
|
|
|
@@ -17275,6 +17441,7 @@ async function awsEntries() {
|
|
|
17275
17441
|
} catch (e) {
|
|
17276
17442
|
incomplete.push(`aws: events list-rules failed \u2014 ${e.message}`);
|
|
17277
17443
|
}
|
|
17444
|
+
let schedulerRead = false;
|
|
17278
17445
|
try {
|
|
17279
17446
|
const refs = awsScheduleRefs(await awsJson(["scheduler", "list-schedules"]));
|
|
17280
17447
|
for (const ref of refs) {
|
|
@@ -17282,11 +17449,12 @@ async function awsEntries() {
|
|
|
17282
17449
|
const entry = awsScheduleEntry(await awsJson(args));
|
|
17283
17450
|
if (entry) entries.push(entry);
|
|
17284
17451
|
}
|
|
17452
|
+
schedulerRead = true;
|
|
17285
17453
|
} catch (e) {
|
|
17286
17454
|
incomplete.push(`aws: scheduler listing failed \u2014 ${e.message}`);
|
|
17287
17455
|
}
|
|
17288
17456
|
const reconciliation = unlauncheredLlmDrifts(entries);
|
|
17289
|
-
return { entries, incomplete, drift: reconciliation.map(renderDrift), reconciliation };
|
|
17457
|
+
return { entries, incomplete, drift: reconciliation.map(renderDrift), reconciliation, schedulerRead };
|
|
17290
17458
|
}
|
|
17291
17459
|
async function defaultRegistryRead() {
|
|
17292
17460
|
return fetchSchedulesList(registryClientDeps(await loadConfig()));
|
|
@@ -17298,7 +17466,11 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
|
|
|
17298
17466
|
readRegistry().catch(() => null),
|
|
17299
17467
|
readProjects().catch(() => null)
|
|
17300
17468
|
]);
|
|
17301
|
-
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
|
+
});
|
|
17302
17474
|
const selfManaged = /* @__PURE__ */ new Set();
|
|
17303
17475
|
for (const proj of projects ?? []) {
|
|
17304
17476
|
if (proj?.schedulesMode !== "self-managed") continue;
|
|
@@ -17541,17 +17713,33 @@ async function runSchedulesLift(opts, deps = {}) {
|
|
|
17541
17713
|
}
|
|
17542
17714
|
return { repo, records, payload, response };
|
|
17543
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
|
+
}
|
|
17544
17731
|
function registerSchedulesLiftCommand(program3, deps = {}) {
|
|
17545
17732
|
const schedules = program3.commands.find((c) => c.name() === "schedules");
|
|
17546
17733
|
if (!schedules) throw new Error("schedules lift: registerSchedulesCommands must run first \u2014 the lift attaches to the `schedules` command");
|
|
17547
|
-
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) => {
|
|
17548
17735
|
try {
|
|
17549
17736
|
const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
|
|
17550
17737
|
if (o.dryRun) {
|
|
17551
17738
|
console.log(JSON.stringify(result.payload, null, 2));
|
|
17552
17739
|
return;
|
|
17553
17740
|
}
|
|
17554
|
-
console.log(JSON.stringify(result.response?.body));
|
|
17741
|
+
console.log(JSON.stringify(withArmingReport(result.response?.body)));
|
|
17742
|
+
console.error(ARMING_IS_EVENTUAL_NOTE);
|
|
17555
17743
|
} catch (e) {
|
|
17556
17744
|
const err = e;
|
|
17557
17745
|
if (err instanceof RegistryUnreachableError) {
|
|
@@ -19286,9 +19474,6 @@ function registerStageCommands(program3) {
|
|
|
19286
19474
|
|
|
19287
19475
|
// src/config-load.ts
|
|
19288
19476
|
var discoveredConfig = null;
|
|
19289
|
-
function registryDegradeError(error) {
|
|
19290
|
-
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`);
|
|
19291
|
-
}
|
|
19292
19477
|
async function loadConfigOrDiscover() {
|
|
19293
19478
|
if (discoveredConfig) return discoveredConfig;
|
|
19294
19479
|
const floor = await loadConfig();
|
|
@@ -21231,6 +21416,17 @@ async function applyPluginHeal(surface, log, opts) {
|
|
|
21231
21416
|
}
|
|
21232
21417
|
return true;
|
|
21233
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
|
+
}
|
|
21234
21430
|
async function runGuard(readOrigin) {
|
|
21235
21431
|
try {
|
|
21236
21432
|
const surface = detectSurface(process.env);
|
|
@@ -21766,9 +21962,9 @@ function formatExplainLoop(playbook) {
|
|
|
21766
21962
|
}
|
|
21767
21963
|
return lines.join("\n");
|
|
21768
21964
|
}
|
|
21769
|
-
function findCommandInManifest(manifest,
|
|
21965
|
+
function findCommandInManifest(manifest, commandPath3) {
|
|
21770
21966
|
const visit = (command) => {
|
|
21771
|
-
if (command.path ===
|
|
21967
|
+
if (command.path === commandPath3) return command;
|
|
21772
21968
|
for (const child2 of command.subcommands) {
|
|
21773
21969
|
const found = visit(child2);
|
|
21774
21970
|
if (found) return found;
|
|
@@ -21793,10 +21989,10 @@ function registerExplainCommand(program3) {
|
|
|
21793
21989
|
console.log(formatManifestHuman(manifest));
|
|
21794
21990
|
return;
|
|
21795
21991
|
}
|
|
21796
|
-
const
|
|
21797
|
-
const command = findCommandInManifest(manifest,
|
|
21992
|
+
const commandPath3 = commandArgs.join(" ");
|
|
21993
|
+
const command = findCommandInManifest(manifest, commandPath3);
|
|
21798
21994
|
if (!command) {
|
|
21799
|
-
fail(`explain: unknown command "${
|
|
21995
|
+
fail(`explain: unknown command "${commandPath3}"`, { code: ERROR_CODES.ERR_NOT_FOUND });
|
|
21800
21996
|
return;
|
|
21801
21997
|
}
|
|
21802
21998
|
console.log(command.subcommands.length ? formatExplainGroup(command, manifest.name) : formatExplainCommand(command, manifest.name));
|
|
@@ -22425,6 +22621,11 @@ function checkRepoWorktrees(probe) {
|
|
|
22425
22621
|
]
|
|
22426
22622
|
};
|
|
22427
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
|
+
}
|
|
22428
22629
|
function checkClaudePlugin(probe) {
|
|
22429
22630
|
const { installed, released, guardState } = probe;
|
|
22430
22631
|
const evidence = [
|
|
@@ -22432,20 +22633,22 @@ function checkClaudePlugin(probe) {
|
|
|
22432
22633
|
`released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
|
|
22433
22634
|
`resolvable: ${guardState}`
|
|
22434
22635
|
];
|
|
22435
|
-
|
|
22636
|
+
const trigger = pluginHealTrigger(probe);
|
|
22637
|
+
if (trigger === "unresolved") {
|
|
22436
22638
|
return {
|
|
22437
22639
|
ok: false,
|
|
22438
22640
|
label: guardState === "no-install" ? "Claude plugin \u2014 not installed" : "Claude plugin \u2014 unresolved (marketplace/cache missing)",
|
|
22439
|
-
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",
|
|
22440
22642
|
verbose: evidence
|
|
22441
22643
|
};
|
|
22442
22644
|
}
|
|
22443
|
-
|
|
22444
|
-
if (behind) {
|
|
22645
|
+
if (trigger === "behind") {
|
|
22445
22646
|
return {
|
|
22446
22647
|
ok: false,
|
|
22447
22648
|
label: `Claude plugin ${installed} \u2192 ${released}`,
|
|
22448
|
-
|
|
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",
|
|
22449
22652
|
verbose: evidence
|
|
22450
22653
|
};
|
|
22451
22654
|
}
|
|
@@ -22597,9 +22800,34 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
22597
22800
|
checks.push({ ok: false, label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
22598
22801
|
}
|
|
22599
22802
|
}
|
|
22600
|
-
const
|
|
22601
|
-
|
|
22602
|
-
if (
|
|
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
|
+
}
|
|
22603
22831
|
const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
|
|
22604
22832
|
const cliReport = buildVersionLagReport(cliInput);
|
|
22605
22833
|
if (apply && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
|
|
@@ -22826,6 +23054,9 @@ function mmiDoctorDeps() {
|
|
|
22826
23054
|
releasedVersion: fetchNpmReleasedVersion,
|
|
22827
23055
|
// #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
|
|
22828
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(),
|
|
22829
23060
|
currentCliVersion: resolveClientVersion,
|
|
22830
23061
|
readGitignore,
|
|
22831
23062
|
writeGitignore,
|
|
@@ -22926,8 +23157,57 @@ function argvWantsJson2() {
|
|
|
22926
23157
|
return process.argv.some((a) => a === "--json" || a.startsWith("--json="));
|
|
22927
23158
|
}
|
|
22928
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
|
+
}
|
|
22929
23203
|
function envelopeAwareWriteErr(str) {
|
|
22930
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);
|
|
22931
23211
|
const match = /unknown option '([^']+)'/.exec(plain);
|
|
22932
23212
|
if (match) {
|
|
22933
23213
|
const flag = match[1];
|
|
@@ -22952,7 +23232,7 @@ function envelopeAwareWriteErr(str) {
|
|
|
22952
23232
|
process.stderr.write(str);
|
|
22953
23233
|
}
|
|
22954
23234
|
var program2 = new Command();
|
|
22955
|
-
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);
|
|
22956
23236
|
function appActorDeps() {
|
|
22957
23237
|
return {
|
|
22958
23238
|
fetchSecret: async (key) => fetchSecretValue(makeSecretsDeps(await loadConfig()), key, { repo: APP_VAULT_REPO }),
|
|
@@ -24420,7 +24700,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
24420
24700
|
}
|
|
24421
24701
|
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
|
24422
24702
|
});
|
|
24423
|
-
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) => {
|
|
24424
24704
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
24425
24705
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
24426
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+/);
|
|
@@ -24431,6 +24711,25 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
24431
24711
|
(await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
24432
24712
|
);
|
|
24433
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
|
+
}
|
|
24434
24733
|
if (ciPolicy.policy === "no-ci") {
|
|
24435
24734
|
const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
|
|
24436
24735
|
if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
|
|
@@ -24481,9 +24780,11 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
24481
24780
|
if (o.auto || upgradedToAuto) {
|
|
24482
24781
|
const state = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
24483
24782
|
if (state !== "MERGED") {
|
|
24484
|
-
|
|
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}.`);
|
|
24485
24786
|
if (upgradedToAuto && !o.auto) {
|
|
24486
|
-
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.`);
|
|
24487
24788
|
process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
|
|
24488
24789
|
}
|
|
24489
24790
|
return;
|
|
@@ -25025,6 +25326,7 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
|
|
|
25025
25326
|
0 && (module.exports = {
|
|
25026
25327
|
DEFAULT_PRIORITY,
|
|
25027
25328
|
awsCallerArn,
|
|
25329
|
+
classifyParseError,
|
|
25028
25330
|
gcPlan,
|
|
25029
25331
|
isOrgRegisteredRepo,
|
|
25030
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",
|