@mutmutco/cli 3.56.0 → 3.58.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 +513 -70
- package/package.json +2 -2
package/dist/main.cjs
CHANGED
|
@@ -7331,9 +7331,116 @@ function defaultSpawn(command, args, env) {
|
|
|
7331
7331
|
if (r.error) throw r.error;
|
|
7332
7332
|
return r.status ?? 1;
|
|
7333
7333
|
}
|
|
7334
|
-
|
|
7334
|
+
function classifyUseToken(token) {
|
|
7335
|
+
if (!isValidSecretKey(token)) return "command";
|
|
7336
|
+
const leaf = token.slice(token.lastIndexOf("/") + 1);
|
|
7337
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(leaf)) return "key";
|
|
7338
|
+
return /^[A-Z]/.test(leaf) ? "ambiguous" : "command";
|
|
7339
|
+
}
|
|
7340
|
+
var USE_OWN_FLAGS = /* @__PURE__ */ new Set(["--repo", "--slug", "--name", "--key"]);
|
|
7341
|
+
function secretsUseTail(argv) {
|
|
7342
|
+
for (let i = 0; i < argv.length - 1; i += 1) {
|
|
7343
|
+
if (argv[i] === "secrets" && argv[i + 1] === "use") return argv.slice(i + 2);
|
|
7344
|
+
}
|
|
7345
|
+
return [];
|
|
7346
|
+
}
|
|
7347
|
+
function excessBareTokens(head) {
|
|
7348
|
+
let excess = 0;
|
|
7349
|
+
let positionalTaken = false;
|
|
7350
|
+
for (let i = 0; i < head.length; ) {
|
|
7351
|
+
const tok = head[i];
|
|
7352
|
+
const eq = tok.indexOf("=");
|
|
7353
|
+
const flag = eq === -1 ? tok : tok.slice(0, eq);
|
|
7354
|
+
if (USE_OWN_FLAGS.has(flag)) {
|
|
7355
|
+
if (flag === "--key") positionalTaken = true;
|
|
7356
|
+
i += eq === -1 ? 2 : 1;
|
|
7357
|
+
continue;
|
|
7358
|
+
}
|
|
7359
|
+
if (tok.startsWith("-")) {
|
|
7360
|
+
i += 1;
|
|
7361
|
+
continue;
|
|
7362
|
+
}
|
|
7363
|
+
if (positionalTaken) excess += 1;
|
|
7364
|
+
else positionalTaken = true;
|
|
7365
|
+
i += 1;
|
|
7366
|
+
}
|
|
7367
|
+
return excess;
|
|
7368
|
+
}
|
|
7369
|
+
function separatorIsOurs(head) {
|
|
7370
|
+
return excessBareTokens(head) === 0;
|
|
7371
|
+
}
|
|
7372
|
+
function parseSecretsUseArgv(tail) {
|
|
7373
|
+
const flags = {};
|
|
7374
|
+
const keys = [];
|
|
7375
|
+
const firstSep = tail.indexOf("--");
|
|
7376
|
+
const sep2 = firstSep !== -1 && separatorIsOurs(tail.slice(0, firstSep)) ? firstSep : -1;
|
|
7377
|
+
const head = sep2 === -1 ? tail : tail.slice(0, sep2);
|
|
7378
|
+
let command = sep2 === -1 ? [] : tail.slice(sep2 + 1).slice();
|
|
7379
|
+
for (let i = 0; i < head.length; ) {
|
|
7380
|
+
const tok = head[i];
|
|
7381
|
+
const eq = tok.indexOf("=");
|
|
7382
|
+
const flag = eq === -1 ? tok : tok.slice(0, eq);
|
|
7383
|
+
if (USE_OWN_FLAGS.has(flag)) {
|
|
7384
|
+
const value = eq === -1 ? head[i + 1] : tok.slice(eq + 1);
|
|
7385
|
+
if (value === void 0) return { kind: "error", error: `secrets use: ${flag} needs a value` };
|
|
7386
|
+
if (flag === "--repo") flags.repo = value;
|
|
7387
|
+
else if (flag === "--slug") flags.slug = value;
|
|
7388
|
+
else if (flag === "--name") flags.name = value;
|
|
7389
|
+
else keys.push(value);
|
|
7390
|
+
i += eq === -1 ? 2 : 1;
|
|
7391
|
+
continue;
|
|
7392
|
+
}
|
|
7393
|
+
if (tok === "--help" || tok === "-h") return { kind: "help" };
|
|
7394
|
+
if (tok.startsWith("-")) {
|
|
7395
|
+
return {
|
|
7396
|
+
kind: "error",
|
|
7397
|
+
error: `secrets use: unknown option ${tok}. \`use\` owns only --key, --repo, --slug and --name; the wrapped command's own flags go after \`--\`, following the command itself.`
|
|
7398
|
+
};
|
|
7399
|
+
}
|
|
7400
|
+
if (keys.length === 0) {
|
|
7401
|
+
keys.push(tok);
|
|
7402
|
+
i += 1;
|
|
7403
|
+
continue;
|
|
7404
|
+
}
|
|
7405
|
+
if (classifyUseToken(tok) !== "command") {
|
|
7406
|
+
const fix = [...keys, tok].map((k) => `--key ${k}`).join(" ");
|
|
7407
|
+
return {
|
|
7408
|
+
kind: "error",
|
|
7409
|
+
error: `secrets use: '${tok}' looks like a second secret key, but \`use\` takes one positional key, so it would be RUN as the command. Name each key explicitly: mmi-cli secrets use ${fix} -- <command> (if '${tok}' really is the command, put it after \`--\`)`
|
|
7410
|
+
};
|
|
7411
|
+
}
|
|
7412
|
+
command = head.slice(i).slice();
|
|
7413
|
+
break;
|
|
7414
|
+
}
|
|
7415
|
+
if (keys.length === 0) return { kind: "error", error: "secrets use: missing <key> (or --key <key>)" };
|
|
7416
|
+
return { kind: "parsed", value: { keys, ...flags, command } };
|
|
7417
|
+
}
|
|
7418
|
+
function resolveUseEnvNames(keys, name) {
|
|
7419
|
+
if (name && keys.length > 1) {
|
|
7420
|
+
return { ok: false, error: `secrets use: --name renames ONE env var, but ${keys.length} keys were given \u2014 drop --name, or run one key per call.` };
|
|
7421
|
+
}
|
|
7422
|
+
const entries = keys.map((key) => ({
|
|
7423
|
+
key,
|
|
7424
|
+
envName: (name && keys.length === 1 ? name : secretLeafName(key)).toUpperCase().replace(/[^A-Z0-9_]/g, "_")
|
|
7425
|
+
}));
|
|
7426
|
+
const claimedBy = /* @__PURE__ */ new Map();
|
|
7427
|
+
for (const e of entries) {
|
|
7428
|
+
const prior = claimedBy.get(e.envName);
|
|
7429
|
+
if (prior) {
|
|
7430
|
+
return { ok: false, error: `secrets use: ${prior} and ${e.key} both inject as ${e.envName} \u2014 one would silently overwrite the other. Run one key per call.` };
|
|
7431
|
+
}
|
|
7432
|
+
claimedBy.set(e.envName, e.key);
|
|
7433
|
+
}
|
|
7434
|
+
return { ok: true, entries };
|
|
7435
|
+
}
|
|
7436
|
+
async function secretsUse(deps, keys, opts) {
|
|
7335
7437
|
const command = opts.command ?? [];
|
|
7438
|
+
const key = keys[0];
|
|
7336
7439
|
if (command.length === 0) {
|
|
7440
|
+
if (keys.length > 1) {
|
|
7441
|
+
deps.err(`secrets use: ${keys.length} keys given but no command \u2014 multiple keys only make sense with \`-- <command>\`. For guidance on one key: mmi-cli secrets use ${key}`);
|
|
7442
|
+
return false;
|
|
7443
|
+
}
|
|
7337
7444
|
const slug2 = await vaultSlug(deps, opts);
|
|
7338
7445
|
const tier = classifyTier(slug2, key);
|
|
7339
7446
|
const path2 = secretParamName(slug2, key);
|
|
@@ -7356,12 +7463,31 @@ async function secretsUse(deps, key, opts) {
|
|
|
7356
7463
|
);
|
|
7357
7464
|
return;
|
|
7358
7465
|
}
|
|
7359
|
-
|
|
7360
|
-
|
|
7466
|
+
for (const k of keys) {
|
|
7467
|
+
if (!isValidSecretKey(k)) {
|
|
7468
|
+
deps.err(`invalid secret key ${JSON.stringify(k)}`);
|
|
7469
|
+
return false;
|
|
7470
|
+
}
|
|
7471
|
+
}
|
|
7472
|
+
const named = resolveUseEnvNames(keys, opts.name);
|
|
7473
|
+
if (!named.ok) {
|
|
7474
|
+
deps.err(named.error);
|
|
7361
7475
|
return false;
|
|
7362
7476
|
}
|
|
7363
7477
|
const repo = await targetRepo(deps, opts);
|
|
7364
7478
|
const slug = opts.slug?.toLowerCase();
|
|
7479
|
+
const env = { ...process.env };
|
|
7480
|
+
for (const { key: k, envName } of named.entries) {
|
|
7481
|
+
const value = await fetchSecretForUse(deps, { repo, key: k, slug });
|
|
7482
|
+
if (value == null) return false;
|
|
7483
|
+
env[envName] = value;
|
|
7484
|
+
}
|
|
7485
|
+
const [cmd, ...args] = command;
|
|
7486
|
+
const run = deps.spawn ?? defaultSpawn;
|
|
7487
|
+
const code = run(cmd, args, env);
|
|
7488
|
+
return code === 0;
|
|
7489
|
+
}
|
|
7490
|
+
async function fetchSecretForUse(deps, { repo, key, slug }) {
|
|
7365
7491
|
const res = await deps.fetch(`${deps.apiUrl}/secrets/get`, {
|
|
7366
7492
|
method: "POST",
|
|
7367
7493
|
headers: await deps.headers({ "content-type": "application/json" }),
|
|
@@ -7373,23 +7499,19 @@ async function secretsUse(deps, key, opts) {
|
|
|
7373
7499
|
if (res.status === 404 && body.code === "secret_not_found") {
|
|
7374
7500
|
const guidance = resolveNotFoundGuidance({ key, repo, slug, report: await probeCapabilities(deps, repo) });
|
|
7375
7501
|
for (const line of guidance.lines) deps.err(line);
|
|
7376
|
-
return
|
|
7502
|
+
return null;
|
|
7377
7503
|
}
|
|
7378
7504
|
deps.err(
|
|
7379
7505
|
await upgradeMessage(res, body) ?? (res.status === 403 ? `secrets use: not authorized for ${key} (HTTP 403) \u2014 ask the master for a \`secrets grant\`${errorDetail(body)}` : `secrets use failed: HTTP ${res.status}${errorDetail(body)}`)
|
|
7380
7506
|
);
|
|
7381
|
-
return
|
|
7507
|
+
return null;
|
|
7382
7508
|
}
|
|
7383
7509
|
const { value } = await res.json();
|
|
7384
7510
|
if (!value) {
|
|
7385
7511
|
deps.err(`secrets use: no value returned for ${key}`);
|
|
7386
|
-
return
|
|
7512
|
+
return null;
|
|
7387
7513
|
}
|
|
7388
|
-
|
|
7389
|
-
const [cmd, ...args] = command;
|
|
7390
|
-
const run = deps.spawn ?? defaultSpawn;
|
|
7391
|
-
const code = run(cmd, args, { ...process.env, [envName]: value });
|
|
7392
|
-
return code === 0;
|
|
7514
|
+
return value;
|
|
7393
7515
|
}
|
|
7394
7516
|
|
|
7395
7517
|
// src/report.ts
|
|
@@ -9251,9 +9373,10 @@ function checkMarketplaceAutoUpdate(probe) {
|
|
|
9251
9373
|
if (!probe) return null;
|
|
9252
9374
|
const evidence = [`${probe.name}: auto-update ${probe.effective ? "on" : "off"} \u2014 ${probe.reason}`];
|
|
9253
9375
|
if (probe.effective) {
|
|
9254
|
-
return { ok: true, label: `marketplace auto-update (${probe.name})`, detail: "on", verbose: evidence };
|
|
9376
|
+
return { id: "marketplace-autoupdate", ok: true, label: `marketplace auto-update (${probe.name})`, detail: "on", verbose: evidence };
|
|
9255
9377
|
}
|
|
9256
9378
|
return {
|
|
9379
|
+
id: "marketplace-autoupdate",
|
|
9257
9380
|
ok: true,
|
|
9258
9381
|
warn: true,
|
|
9259
9382
|
label: `marketplace auto-update (${probe.name})`,
|
|
@@ -9266,18 +9389,24 @@ function checkMarketplaceCatalogRef(probe) {
|
|
|
9266
9389
|
const label = `marketplace catalog ref (${probe.name})`;
|
|
9267
9390
|
const evidence = [`${probe.name}: ${probe.reason}`, `auto-update: ${probe.autoUpdate ? "on" : "off"}`];
|
|
9268
9391
|
if (probe.agrees) {
|
|
9269
|
-
return { ok: true, label, detail: `${probe.ref}`, verbose: evidence };
|
|
9392
|
+
return { id: "marketplace-catalog-ref", ok: true, label, detail: `${probe.ref}`, verbose: evidence };
|
|
9270
9393
|
}
|
|
9271
9394
|
const drift = probe.unpinned ? "unpinned" : `pinned to ${probe.ref}, not ${CATALOG_CONTENT_REF}`;
|
|
9272
9395
|
if (probe.autoUpdate) {
|
|
9273
9396
|
return {
|
|
9397
|
+
id: "marketplace-catalog-ref",
|
|
9274
9398
|
ok: false,
|
|
9275
9399
|
label,
|
|
9276
|
-
|
|
9400
|
+
// #3574: `drift` moves to `detail`, matching the warn branch below, which always had it there. The
|
|
9401
|
+
// ✗ branch carried it in `fix` only because `renderCheckLine` discarded `detail` on failure — one
|
|
9402
|
+
// row, two conventions, decided by which field would actually print.
|
|
9403
|
+
detail: `${drift} while auto-update is ON`,
|
|
9404
|
+
fix: `this machine installs plugin releases advertised by a branch nobody released; ${CATALOG_REF_PIN_STEPS}`,
|
|
9277
9405
|
verbose: evidence
|
|
9278
9406
|
};
|
|
9279
9407
|
}
|
|
9280
9408
|
return {
|
|
9409
|
+
id: "marketplace-catalog-ref",
|
|
9281
9410
|
ok: true,
|
|
9282
9411
|
warn: true,
|
|
9283
9412
|
label,
|
|
@@ -9605,6 +9734,23 @@ function describePrMergeEnqueuedReason(checks) {
|
|
|
9605
9734
|
};
|
|
9606
9735
|
}
|
|
9607
9736
|
}
|
|
9737
|
+
function decidePrMergeCleanupGate(input) {
|
|
9738
|
+
const state = (input.state ?? "").trim().toUpperCase();
|
|
9739
|
+
if (state === "MERGED") return { action: "cleanup" };
|
|
9740
|
+
if (!state) {
|
|
9741
|
+
return {
|
|
9742
|
+
action: "refuse",
|
|
9743
|
+
reason: "state-unreadable",
|
|
9744
|
+
message: `could not read the PR's state after the merge attempt${input.stateError ? `: ${input.stateError}` : ""} \u2014 cleanup skipped because the merge is unproven`
|
|
9745
|
+
};
|
|
9746
|
+
}
|
|
9747
|
+
if (input.autoRequested) return { action: "enqueued" };
|
|
9748
|
+
return {
|
|
9749
|
+
action: "refuse",
|
|
9750
|
+
reason: "not-merged",
|
|
9751
|
+
message: `the PR is ${state}, not MERGED \u2014 the merge did not happen, so the branch and worktree were left untouched`
|
|
9752
|
+
};
|
|
9753
|
+
}
|
|
9608
9754
|
function parseGhPrChecksOutput(stdout) {
|
|
9609
9755
|
const text = stdout.trim();
|
|
9610
9756
|
if (!text) return "no-checks-reported";
|
|
@@ -17716,10 +17862,30 @@ function registerSecretsCommands(program3) {
|
|
|
17716
17862
|
if (!ok) process.exitCode = 1;
|
|
17717
17863
|
}));
|
|
17718
17864
|
secrets.command("rm <key>").description("remove a secret from your own full project vault; org-infra requires master or an exact grant. --slug _org removes an org-infra <provider>/<KEY> value (#3315)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for the value removal \u2014 master or an exact grant (#3315)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
|
|
17719
|
-
secrets.command("use <key> [command...]").description("consume
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17865
|
+
secrets.command("use").usage("<key> | --key <key> [--key <key>\u2026] [-- <command>] [--repo <owner/repo>] [--slug <slug>] [--name <ENVVAR>]").argument("[args...]", "one secret key (or use --key), then `--` and the command to run").description("consume secrets KEYLESS: own-project full tree or exactly granted org-infra keys; injects each into the command env and never prints one. ONE positional key, or several via repeated `--key` (#3561). The wrapped command keeps its OWN flags (`-- node -e \u2026`, `-- curl -H \u2026`) with or without the `--`, because npm's PowerShell shim swallows `--` before the CLI sees it (#3436)").allowUnknownOption().helpOption(false).addHelpText("after", [
|
|
17866
|
+
"",
|
|
17867
|
+
"Options:",
|
|
17868
|
+
" --key <key> a secret key; repeat for several. The only form no shell can misread \u2014",
|
|
17869
|
+
" `npm` and `slack/token` are BOTH valid key names and plausible commands.",
|
|
17870
|
+
" --repo <owner/repo> target repo (defaults to the current repo)",
|
|
17871
|
+
" --slug <slug> org-infra namespace (e.g. _org) for a granted org secret",
|
|
17872
|
+
" --name <ENVVAR> env var to inject under, single key only (default: the key leaf, UPPER_SNAKE)",
|
|
17873
|
+
"",
|
|
17874
|
+
"These belong BEFORE the `--`. Everything after it is the wrapped command, untouched."
|
|
17875
|
+
].join("\n")).action(function() {
|
|
17876
|
+
const parsed = parseSecretsUseArgv(secretsUseTail(process.argv));
|
|
17877
|
+
if (parsed.kind === "help") return this.outputHelp();
|
|
17878
|
+
return withSecrets(async (d) => {
|
|
17879
|
+
if (parsed.kind === "error") {
|
|
17880
|
+
d.err(parsed.error);
|
|
17881
|
+
process.exitCode = 1;
|
|
17882
|
+
return;
|
|
17883
|
+
}
|
|
17884
|
+
const { keys, command, ...o } = parsed.value;
|
|
17885
|
+
const ok = await secretsUse(d, keys, { ...o, command });
|
|
17886
|
+
if (ok === false) process.exitCode = 1;
|
|
17887
|
+
});
|
|
17888
|
+
});
|
|
17723
17889
|
secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to one specific org-infra secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
|
|
17724
17890
|
secrets.command("revoke <repo> <login> <key>").description("MASTER-ONLY: withdraw a previously granted org-infra secret access").action((repo, login, key) => withSecrets((d) => secretsRevoke(d, repo, login, key, {})));
|
|
17725
17891
|
}
|
|
@@ -17864,11 +18030,11 @@ function renderPoolDetail(probe) {
|
|
|
17864
18030
|
function checkGithubPools(probe) {
|
|
17865
18031
|
if (!probe) return [];
|
|
17866
18032
|
const lines = [];
|
|
17867
|
-
lines.push(probe.personal ? { ok: true, label: "github pools (personal)", detail: renderPoolDetail(probe.personal) } : { ok: false, label: "github pools (personal)",
|
|
18033
|
+
lines.push(probe.personal ? { id: "github-pools-personal", ok: true, label: "github pools (personal)", detail: renderPoolDetail(probe.personal) } : { id: "github-pools-personal", ok: false, label: "github pools (personal)", detail: "rate_limit unreadable", fix: "check `gh auth status`" });
|
|
17868
18034
|
if (typeof probe.app === "string") {
|
|
17869
|
-
lines.push({ ok: false, label: "github pools (app actor)", fix: probe.app });
|
|
18035
|
+
lines.push({ id: "github-pools-app", ok: false, label: "github pools (app actor)", fix: probe.app });
|
|
17870
18036
|
} else if (probe.app) {
|
|
17871
|
-
lines.push({ ok: true, label: "github pools (app actor)", detail: renderPoolDetail(probe.app) });
|
|
18037
|
+
lines.push({ id: "github-pools-app", ok: true, label: "github pools (app actor)", detail: renderPoolDetail(probe.app) });
|
|
17872
18038
|
}
|
|
17873
18039
|
return lines;
|
|
17874
18040
|
}
|
|
@@ -18096,6 +18262,92 @@ var import_node_child_process10 = require("node:child_process");
|
|
|
18096
18262
|
var import_node_util7 = require("node:util");
|
|
18097
18263
|
|
|
18098
18264
|
// src/schedules.ts
|
|
18265
|
+
function cronFieldValues(field, min, max) {
|
|
18266
|
+
const out = /* @__PURE__ */ new Set();
|
|
18267
|
+
for (const part of field.split(",")) {
|
|
18268
|
+
const [rangePart, stepPart] = part.split("/");
|
|
18269
|
+
const step = stepPart === void 0 ? 1 : Number(stepPart);
|
|
18270
|
+
if (!Number.isInteger(step) || step < 1) return null;
|
|
18271
|
+
let lo;
|
|
18272
|
+
let hi;
|
|
18273
|
+
if (rangePart === "*" || rangePart === "?") {
|
|
18274
|
+
lo = min;
|
|
18275
|
+
hi = max;
|
|
18276
|
+
} else if (rangePart.includes("-")) {
|
|
18277
|
+
const [a, b] = rangePart.split("-");
|
|
18278
|
+
lo = Number(a);
|
|
18279
|
+
hi = Number(b);
|
|
18280
|
+
} else {
|
|
18281
|
+
lo = Number(rangePart);
|
|
18282
|
+
hi = Number(rangePart);
|
|
18283
|
+
}
|
|
18284
|
+
if (!Number.isInteger(lo) || !Number.isInteger(hi) || lo < min || hi > max || lo > hi) return null;
|
|
18285
|
+
for (let v = lo; v <= hi; v += step) out.add(v);
|
|
18286
|
+
}
|
|
18287
|
+
return out.size ? out : null;
|
|
18288
|
+
}
|
|
18289
|
+
function parseCronSpec(raw) {
|
|
18290
|
+
const inner = raw.trim().replace(/^cron\(/i, "").replace(/\)$/, "").trim();
|
|
18291
|
+
const f = inner.split(/\s+/);
|
|
18292
|
+
if (f.length !== 5 && f.length !== 6) return null;
|
|
18293
|
+
const aws = f.length === 6;
|
|
18294
|
+
const [minute, hour, dom, month, dowRaw] = f;
|
|
18295
|
+
const minutes = cronFieldValues(minute, 0, 59);
|
|
18296
|
+
const hours = cronFieldValues(hour, 0, 23);
|
|
18297
|
+
const doms = cronFieldValues(dom, 1, 31);
|
|
18298
|
+
const months = cronFieldValues(month, 1, 12);
|
|
18299
|
+
const dows = cronFieldValues(dowRaw, aws ? 1 : 0, aws ? 7 : 7);
|
|
18300
|
+
if (!minutes || !hours || !doms || !months || !dows) return null;
|
|
18301
|
+
const normalizedDow = new Set([...dows].map((d) => aws ? d - 1 : d % 7));
|
|
18302
|
+
return {
|
|
18303
|
+
minute: minutes,
|
|
18304
|
+
hour: hours,
|
|
18305
|
+
dom: doms,
|
|
18306
|
+
month: months,
|
|
18307
|
+
dow: normalizedDow,
|
|
18308
|
+
domAny: dom === "*" || dom === "?",
|
|
18309
|
+
dowAny: dowRaw === "*" || dowRaw === "?"
|
|
18310
|
+
};
|
|
18311
|
+
}
|
|
18312
|
+
function cronMatches(spec, d) {
|
|
18313
|
+
if (!spec.minute.has(d.getUTCMinutes())) return false;
|
|
18314
|
+
if (!spec.hour.has(d.getUTCHours())) return false;
|
|
18315
|
+
if (!spec.month.has(d.getUTCMonth() + 1)) return false;
|
|
18316
|
+
const domHit = spec.dom.has(d.getUTCDate());
|
|
18317
|
+
const dowHit = spec.dow.has(d.getUTCDay());
|
|
18318
|
+
if (spec.domAny && spec.dowAny) return true;
|
|
18319
|
+
if (spec.domAny) return dowHit;
|
|
18320
|
+
if (spec.dowAny) return domHit;
|
|
18321
|
+
return domHit || dowHit;
|
|
18322
|
+
}
|
|
18323
|
+
var FIRST_TICK_SCAN_DAYS = 400;
|
|
18324
|
+
function scheduleTickState(cadence, armedAt, now) {
|
|
18325
|
+
if (!cadence || !armedAt) return {};
|
|
18326
|
+
const armed = /* @__PURE__ */ new Date(`${armedAt}T00:00:00Z`);
|
|
18327
|
+
if (Number.isNaN(armed.getTime())) return {};
|
|
18328
|
+
const specs = splitCadence(cadence).flatMap((c) => [...c.match(/cron\([^)]*\)/gi) ?? [], ...extractCronTokens(c)]).map(parseCronSpec).filter((s) => s !== null);
|
|
18329
|
+
if (!specs.length) return {};
|
|
18330
|
+
const cursor = new Date(armed.getTime());
|
|
18331
|
+
cursor.setUTCSeconds(0, 0);
|
|
18332
|
+
const limit = armed.getTime() + FIRST_TICK_SCAN_DAYS * 864e5;
|
|
18333
|
+
while (cursor.getTime() <= limit) {
|
|
18334
|
+
if (specs.some((s) => cronMatches(s, cursor))) {
|
|
18335
|
+
return { firstTickDue: cursor.toISOString(), ticked: cursor.getTime() <= now.getTime() };
|
|
18336
|
+
}
|
|
18337
|
+
cursor.setTime(cursor.getTime() + 6e4);
|
|
18338
|
+
}
|
|
18339
|
+
return {};
|
|
18340
|
+
}
|
|
18341
|
+
var WEEKDAY = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
18342
|
+
function resolvedLabel(entry, now) {
|
|
18343
|
+
if (entry.resolved !== "live") return entry.resolved;
|
|
18344
|
+
const { firstTickDue, ticked } = scheduleTickState(entry.cadence, entry.armedAt, now);
|
|
18345
|
+
if (!firstTickDue) return entry.resolved;
|
|
18346
|
+
const due = new Date(firstTickDue);
|
|
18347
|
+
const armedDay = WEEKDAY[(/* @__PURE__ */ new Date(`${entry.armedAt}T00:00:00Z`)).getUTCDay()];
|
|
18348
|
+
const dueStamp = `${firstTickDue.slice(0, 16).replace("T", " ")}Z`;
|
|
18349
|
+
return ticked ? `live \u2014 first tick was due ${dueStamp}` : `live \u2014 awaiting first tick ${dueStamp} (armed ${armedDay}, fires ${WEEKDAY[due.getUTCDay()]})`;
|
|
18350
|
+
}
|
|
18099
18351
|
var ORG = "mutmutco";
|
|
18100
18352
|
var DOC_START_MARKER = "<!-- schedules:inventory:start -->";
|
|
18101
18353
|
var DOC_END_MARKER = "<!-- schedules:inventory:end -->";
|
|
@@ -18253,19 +18505,23 @@ function sortEntries(entries) {
|
|
|
18253
18505
|
(a, b) => a.executor.localeCompare(b.executor) || a.name.localeCompare(b.name)
|
|
18254
18506
|
);
|
|
18255
18507
|
}
|
|
18256
|
-
function formatSchedulesTable(entries) {
|
|
18508
|
+
function formatSchedulesTable(entries, now = /* @__PURE__ */ new Date()) {
|
|
18257
18509
|
if (!entries.length) return "no armed org schedules found";
|
|
18510
|
+
const resolvedOf = new Map(entries.map((e) => [e, resolvedLabel(e, now)]));
|
|
18258
18511
|
const w = (pick, head) => Math.max(head.length, ...entries.map((e) => pick(e).length));
|
|
18259
18512
|
const nameW = w((e) => e.name, "JOB");
|
|
18260
18513
|
const cadW = w((e) => e.cadence, "CADENCE");
|
|
18261
18514
|
const exeW = w((e) => e.executor, "EXECUTOR");
|
|
18262
18515
|
const llmW = w((e) => e.llm, "LLM");
|
|
18263
|
-
const resW = w((e) => e.resolved, "RESOLVED");
|
|
18516
|
+
const resW = w((e) => resolvedOf.get(e) ?? e.resolved, "RESOLVED");
|
|
18264
18517
|
const row = (n, c, x, l, r, s) => `${n.padEnd(nameW)} ${c.padEnd(cadW)} ${x.padEnd(exeW)} ${l.padEnd(llmW)} ${r.padEnd(resW)} ${s}`;
|
|
18265
18518
|
const lines = [row("JOB", "CADENCE", "EXECUTOR", "LLM", "RESOLVED", "SOURCE")];
|
|
18266
|
-
for (const e of entries) lines.push(row(e.name, e.cadence, e.executor, e.llm, e.resolved, e.source));
|
|
18519
|
+
for (const e of entries) lines.push(row(e.name, e.cadence, e.executor, e.llm, resolvedOf.get(e) ?? e.resolved, e.source));
|
|
18267
18520
|
return lines.join("\n");
|
|
18268
18521
|
}
|
|
18522
|
+
function withTickState(entry, now = /* @__PURE__ */ new Date()) {
|
|
18523
|
+
return { ...entry, ...scheduleTickState(entry.cadence, entry.armedAt, now) };
|
|
18524
|
+
}
|
|
18269
18525
|
function renderDocSection(entries, generatedAtIso) {
|
|
18270
18526
|
const lines = [
|
|
18271
18527
|
"| Job | Cadence | Executor | LLM | Resolved | Source |",
|
|
@@ -18625,7 +18881,8 @@ function registerSchedulesCommands(program3) {
|
|
|
18625
18881
|
return;
|
|
18626
18882
|
}
|
|
18627
18883
|
if (o.json) {
|
|
18628
|
-
|
|
18884
|
+
const now = /* @__PURE__ */ new Date();
|
|
18885
|
+
console.log(JSON.stringify({ org: entries.map((e) => withTickState(e, now)), incomplete, drift, reconciliation }, null, 2));
|
|
18629
18886
|
} else {
|
|
18630
18887
|
console.log(formatSchedulesTable(entries));
|
|
18631
18888
|
reportDrift(drift);
|
|
@@ -21808,6 +22065,97 @@ function formatStaleLeaks(leaks) {
|
|
|
21808
22065
|
async function repoRootOf() {
|
|
21809
22066
|
return (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
21810
22067
|
}
|
|
22068
|
+
function classifyLandBranchMergeState(prs) {
|
|
22069
|
+
if (!prs) return { state: "unknown", numbers: [] };
|
|
22070
|
+
if (!prs.length) return { state: "no-pr", numbers: [] };
|
|
22071
|
+
const pick = (s) => prs.filter((pr2) => pr2.state.toUpperCase() === s).map((pr2) => pr2.number);
|
|
22072
|
+
const open2 = pick("OPEN");
|
|
22073
|
+
if (open2.length) return { state: "open", numbers: open2 };
|
|
22074
|
+
const known = /* @__PURE__ */ new Set(["OPEN", "CLOSED", "MERGED"]);
|
|
22075
|
+
const unrecognised = prs.filter((pr2) => !known.has(pr2.state.toUpperCase()));
|
|
22076
|
+
if (unrecognised.length) return { state: "unknown", numbers: prs.map((pr2) => pr2.number) };
|
|
22077
|
+
const merged = pick("MERGED");
|
|
22078
|
+
if (!merged.length) return { state: "closed-unmerged", numbers: prs.map((pr2) => pr2.number) };
|
|
22079
|
+
if (merged.length < prs.length) return { state: "mixed", numbers: prs.map((pr2) => pr2.number) };
|
|
22080
|
+
return { state: "merged", numbers: merged };
|
|
22081
|
+
}
|
|
22082
|
+
function describeLandMergeState(v) {
|
|
22083
|
+
const refs = v.numbers.map((n) => `#${n}`).join(", ");
|
|
22084
|
+
switch (v.state) {
|
|
22085
|
+
case "merged":
|
|
22086
|
+
return `PR ${refs} merged`;
|
|
22087
|
+
case "closed-unmerged":
|
|
22088
|
+
return `PR ${refs} CLOSED \u2014 NOT merged`;
|
|
22089
|
+
case "mixed":
|
|
22090
|
+
return `PR ${refs} \u2014 some merged, some CLOSED unmerged; which one owns the branch head is unproven`;
|
|
22091
|
+
case "open":
|
|
22092
|
+
return `PR ${refs} still OPEN \u2014 NOT merged`;
|
|
22093
|
+
case "no-pr":
|
|
22094
|
+
return "no pull request for this branch";
|
|
22095
|
+
case "unknown":
|
|
22096
|
+
return refs ? `PR ${refs} reported a pull-request state this CLI does not recognise \u2014 merge NOT confirmed` : "pull-request state could not be read \u2014 merge NOT confirmed";
|
|
22097
|
+
}
|
|
22098
|
+
}
|
|
22099
|
+
function decideWorktreeLandRefCleanup(input) {
|
|
22100
|
+
if (input.keepRemote) return { action: "proceed" };
|
|
22101
|
+
const verdict = classifyLandBranchMergeState(input.prs);
|
|
22102
|
+
const refs = verdict.numbers.map((n) => `#${n}`).join(", ");
|
|
22103
|
+
if (verdict.state === "unknown") {
|
|
22104
|
+
return verdict.numbers.length ? {
|
|
22105
|
+
action: "keep-remote",
|
|
22106
|
+
reason: "pr-state-unrecognised",
|
|
22107
|
+
message: `pull request ${refs} for '${input.branch}' reported a state this CLI does not recognise \u2014 keeping the remote branch rather than deleting a ref whose merge is unproven`
|
|
22108
|
+
} : {
|
|
22109
|
+
action: "keep-remote",
|
|
22110
|
+
reason: "pr-state-unreadable",
|
|
22111
|
+
message: `could not read the pull-request state for '${input.branch}' \u2014 keeping the remote branch so an unmerged head ref cannot be deleted on a guess`
|
|
22112
|
+
};
|
|
22113
|
+
}
|
|
22114
|
+
if (verdict.state === "open") {
|
|
22115
|
+
return {
|
|
22116
|
+
action: "refuse",
|
|
22117
|
+
reason: "open-pr",
|
|
22118
|
+
message: `'${input.branch}' still has an OPEN pull request (${refs}). Deleting its head ref would auto-close the PR and leave the work on no branch. Merge it first, or re-run with --keep-remote for a local-only clean`
|
|
22119
|
+
};
|
|
22120
|
+
}
|
|
22121
|
+
if (verdict.state === "mixed") {
|
|
22122
|
+
return {
|
|
22123
|
+
action: "keep-remote",
|
|
22124
|
+
reason: "mixed-pr-state",
|
|
22125
|
+
message: `'${input.branch}' has both a merged and a closed-unmerged pull request (${refs}) and nothing here proves which owns the current head \u2014 keeping the remote branch rather than deleting a ref that may hold unmerged work`
|
|
22126
|
+
};
|
|
22127
|
+
}
|
|
22128
|
+
if (verdict.state === "closed-unmerged") {
|
|
22129
|
+
return {
|
|
22130
|
+
action: "refuse",
|
|
22131
|
+
reason: "unmerged-pr",
|
|
22132
|
+
message: `'${input.branch}' has a pull request that never merged (${refs}). The remote branch is the only thing still holding that work. Re-run with --keep-remote for a local-only clean`
|
|
22133
|
+
};
|
|
22134
|
+
}
|
|
22135
|
+
return { action: "proceed" };
|
|
22136
|
+
}
|
|
22137
|
+
async function fetchLandBranchPrs(branch, repo) {
|
|
22138
|
+
try {
|
|
22139
|
+
const { stdout } = await execFileP2("gh", [
|
|
22140
|
+
"pr",
|
|
22141
|
+
"list",
|
|
22142
|
+
"--head",
|
|
22143
|
+
branch,
|
|
22144
|
+
"--state",
|
|
22145
|
+
"all",
|
|
22146
|
+
"--limit",
|
|
22147
|
+
"20",
|
|
22148
|
+
...repo ? ["--repo", repo] : [],
|
|
22149
|
+
"--json",
|
|
22150
|
+
"number,state"
|
|
22151
|
+
], { timeout: GH_TIMEOUT_MS });
|
|
22152
|
+
const parsed = JSON.parse(stdout.trim() || "null");
|
|
22153
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
22154
|
+
return parsed.filter((pr2) => Boolean(pr2) && typeof pr2 === "object" && typeof pr2.number === "number" && typeof pr2.state === "string").map((pr2) => ({ number: pr2.number, state: pr2.state }));
|
|
22155
|
+
} catch {
|
|
22156
|
+
return void 0;
|
|
22157
|
+
}
|
|
22158
|
+
}
|
|
21811
22159
|
async function fetchIssueTitle(repo, number) {
|
|
21812
22160
|
try {
|
|
21813
22161
|
const { stdout } = await execFileP2("gh", ["issue", "view", String(number), "--repo", repo, "--json", "title", "--jq", ".title"], { timeout: GH_TIMEOUT_MS });
|
|
@@ -21869,6 +22217,18 @@ function registerWorktreeCommands(program3) {
|
|
|
21869
22217
|
if (landDirty) {
|
|
21870
22218
|
return fail(`worktree land: refusing to land '${branch}' \u2014 uncommitted changes in ${wtPath}; commit, stash, or remove the worktree manually`, { code: ERROR_CODES.ERR_BAD_ENUM });
|
|
21871
22219
|
}
|
|
22220
|
+
const landPrs = await fetchLandBranchPrs(branch);
|
|
22221
|
+
const mergeVerdict = classifyLandBranchMergeState(landPrs);
|
|
22222
|
+
const landRefs = decideWorktreeLandRefCleanup({
|
|
22223
|
+
branch,
|
|
22224
|
+
prs: landPrs,
|
|
22225
|
+
keepRemote: Boolean(o.keepRemote)
|
|
22226
|
+
});
|
|
22227
|
+
if (landRefs.action === "refuse") {
|
|
22228
|
+
return fail(`worktree land: ${landRefs.message}`, { code: ERROR_CODES.ERR_BAD_ENUM });
|
|
22229
|
+
}
|
|
22230
|
+
const keepRemoteBranch = Boolean(o.keepRemote) || landRefs.action === "keep-remote";
|
|
22231
|
+
if (landRefs.action === "keep-remote") console.warn(`worktree land: ${landRefs.message}.`);
|
|
21872
22232
|
const report = [];
|
|
21873
22233
|
if (hasStage) {
|
|
21874
22234
|
try {
|
|
@@ -21888,14 +22248,28 @@ function registerWorktreeCommands(program3) {
|
|
|
21888
22248
|
status: removeOutcome.status === "removed" ? removeOutcome.recovery ? `done (${removeOutcome.recovery})` : "done" : `failed: ${removeOutcome.error ?? "lock held"} \u2014 run: git -C "${primaryCheckout}" worktree remove --force "${wtPath}"`
|
|
21889
22249
|
});
|
|
21890
22250
|
report.push(await bestEffortGit(["branch", "-D", branch], primaryCheckout, `delete local branch ${branch}`));
|
|
21891
|
-
if (!
|
|
22251
|
+
if (!keepRemoteBranch) {
|
|
21892
22252
|
report.push(await bestEffortGit(["push", o.remote, "--delete", branch], void 0, `delete remote branch ${branch}`, GH_MUTATION_TIMEOUT_MS));
|
|
22253
|
+
} else if (!o.keepRemote) {
|
|
22254
|
+
report.push({ step: `delete remote branch ${branch}`, status: `skipped: ${landRefs.action === "keep-remote" ? landRefs.reason : "kept"}` });
|
|
21893
22255
|
}
|
|
21894
22256
|
report.push(await bestEffortGit(["worktree", "prune"], primaryCheckout, "prune worktree metadata"));
|
|
21895
|
-
const result = {
|
|
22257
|
+
const result = {
|
|
22258
|
+
dryRun: false,
|
|
22259
|
+
...plan,
|
|
22260
|
+
...o.keepRemote ? { keepRemote: true } : {},
|
|
22261
|
+
// #3566: the merge outcome, machine-readable, so an orchestrator never has to infer it from the
|
|
22262
|
+
// absence of an error. `merged` is the ONLY value that means the work reached the base branch.
|
|
22263
|
+
mergeState: mergeVerdict.state,
|
|
22264
|
+
prNumbers: mergeVerdict.numbers,
|
|
22265
|
+
report
|
|
22266
|
+
};
|
|
21896
22267
|
if (o.json) return console.log(JSON.stringify(result, null, 2));
|
|
21897
|
-
console.log(`worktree land: cleaned up branch ${branch}`);
|
|
22268
|
+
console.log(`worktree land: cleaned up branch ${branch} (${describeLandMergeState(mergeVerdict)})`);
|
|
21898
22269
|
for (const r of report) console.log(` ${r.step}: ${r.status}`);
|
|
22270
|
+
if (mergeVerdict.state !== "merged") {
|
|
22271
|
+
console.warn(`worktree land: '${branch}' was cleaned up but NOT merged \u2014 ${describeLandMergeState(mergeVerdict)}. Do not report this work as shipped.`);
|
|
22272
|
+
}
|
|
21899
22273
|
if (report.some((r) => r.status.startsWith("failed"))) process.exitCode = 1;
|
|
21900
22274
|
} catch (e) {
|
|
21901
22275
|
return failGraceful(`worktree land: ${e.message}`);
|
|
@@ -22642,8 +23016,11 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
22642
23016
|
updateRecipe: [CLAUDE_RECOVERY]
|
|
22643
23017
|
}
|
|
22644
23018
|
};
|
|
22645
|
-
function nonClaudeSurfaceHealMessage() {
|
|
22646
|
-
|
|
23019
|
+
function nonClaudeSurfaceHealMessage(surface) {
|
|
23020
|
+
if (surface === "codex") {
|
|
23021
|
+
return "Codex ships an MMI plugin (#3563). Install or repair it with:\n codex plugin marketplace add mutmutco/MMI-Hub && codex plugin add mmi@mutmutco\n Then run /hooks and TRUST the MMI hooks \u2014 bundled hooks are skipped until reviewed.\n Update the CLI too: npm i -g @mutmutco/cli";
|
|
23022
|
+
}
|
|
23023
|
+
return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude and Codex only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
|
|
22647
23024
|
}
|
|
22648
23025
|
function healStepAborts(step, ok) {
|
|
22649
23026
|
return !ok && step.gated;
|
|
@@ -22840,7 +23217,7 @@ async function runGuard(readOrigin) {
|
|
|
22840
23217
|
}
|
|
22841
23218
|
async function runPluginHeal(surface = detectSurface(process.env)) {
|
|
22842
23219
|
if (surfaceToken(surface) !== "claude") {
|
|
22843
|
-
console.log(nonClaudeSurfaceHealMessage());
|
|
23220
|
+
console.log(nonClaudeSurfaceHealMessage(surfaceToken(surface) ?? void 0));
|
|
22844
23221
|
return;
|
|
22845
23222
|
}
|
|
22846
23223
|
const descriptor = PLUGIN_SURFACE_HEAL.claude;
|
|
@@ -23982,7 +24359,9 @@ function renderCheckLine(check) {
|
|
|
23982
24359
|
if (check.ok) {
|
|
23983
24360
|
return check.detail ? `\u2713 ${check.label} \u2014 ${check.detail}` : `\u2713 ${check.label}`;
|
|
23984
24361
|
}
|
|
23985
|
-
|
|
24362
|
+
const head = check.detail ? `\u2717 ${check.label} \u2014 ${check.detail}` : `\u2717 ${check.label}`;
|
|
24363
|
+
return check.fix ? `${head}
|
|
24364
|
+
${VERBOSE_INDENT}\u2192 ${check.fix}` : head;
|
|
23986
24365
|
}
|
|
23987
24366
|
function renderReport(checks, opts) {
|
|
23988
24367
|
const lines = checks.map(renderCheckLine);
|
|
@@ -23996,7 +24375,8 @@ function renderTally(checks) {
|
|
|
23996
24375
|
}
|
|
23997
24376
|
function renderDoctorText(checks, opts) {
|
|
23998
24377
|
const lines = [];
|
|
23999
|
-
|
|
24378
|
+
const shown = opts.verbose ? checks : checks.filter((c) => !c.ok || c.warn);
|
|
24379
|
+
for (const check of shown) {
|
|
24000
24380
|
lines.push(renderCheckLine(check));
|
|
24001
24381
|
if (opts.verbose) {
|
|
24002
24382
|
for (const evidence of check.verbose ?? []) lines.push(`${VERBOSE_INDENT}${evidence}`);
|
|
@@ -24019,6 +24399,7 @@ function checkGithubAuth(probe) {
|
|
|
24019
24399
|
const rescuers = reach.otherAccountsThatCanSee ?? [];
|
|
24020
24400
|
return {
|
|
24021
24401
|
ok: false,
|
|
24402
|
+
id: "github-auth",
|
|
24022
24403
|
label: "github auth",
|
|
24023
24404
|
fix: rescuers.length ? `the active gh account (${login}) cannot see ${reach.repo}; another account is logged in \u2014 try: gh auth switch --user ${rescuers[0]}` : `the active gh account (${login}) cannot see ${reach.repo} \u2014 check org membership, or: gh auth login`,
|
|
24024
24405
|
verbose: [
|
|
@@ -24031,6 +24412,7 @@ function checkGithubAuth(probe) {
|
|
|
24031
24412
|
}
|
|
24032
24413
|
return {
|
|
24033
24414
|
ok: authed,
|
|
24415
|
+
id: "github-auth",
|
|
24034
24416
|
label: "github auth",
|
|
24035
24417
|
fix: probe.ghInstalled ? 'run: gh auth login --hostname github.com --git-protocol https --web --scopes "project"' : "install GitHub CLI (https://cli.github.com), then: gh auth login",
|
|
24036
24418
|
verbose: [
|
|
@@ -24046,6 +24428,7 @@ function checkAwsIdentity(probe) {
|
|
|
24046
24428
|
const arn = probe.callerArn?.trim();
|
|
24047
24429
|
return {
|
|
24048
24430
|
ok: !arn || !arn.endsWith(":root"),
|
|
24431
|
+
id: "aws-identity",
|
|
24049
24432
|
label: "aws identity",
|
|
24050
24433
|
fix: "use a non-root IAM user/session profile (set AWS_PROFILE or run: aws sso login), then verify `aws sts get-caller-identity` is not :root",
|
|
24051
24434
|
// Name the caller. A green "aws identity" says only "not root" — it does not say WHO, and "no AWS
|
|
@@ -24060,6 +24443,7 @@ function checkAwsIdentity(probe) {
|
|
|
24060
24443
|
function checkRepoWorktrees(probe) {
|
|
24061
24444
|
return {
|
|
24062
24445
|
ok: !(probe.isOrgRepo && probe.hasRepoLocalWorktrees),
|
|
24446
|
+
id: "repo-worktrees",
|
|
24063
24447
|
label: "repo worktrees",
|
|
24064
24448
|
fix: "repo-local `.worktrees/` is not the canonical path \u2014 use `mmi-cli worktree create <branch>` (sibling `../worktrees/`), then remove the in-repo `.worktrees/`",
|
|
24065
24449
|
verbose: [
|
|
@@ -24078,11 +24462,12 @@ function checkWorktreeRoots(probe) {
|
|
|
24078
24462
|
`scanned by worktree gc/list: ${probe.authoritative}`,
|
|
24079
24463
|
...probe.stray.length ? probe.stray.map((s) => `unreachable by any gc: ${s.path} \u2014 ${s.dirs} dir(s), ${formatBytes(s.bytes, s.partial)}`) : ["no other *worktrees* directory beside this repo"]
|
|
24080
24464
|
];
|
|
24081
|
-
if (!probe.stray.length) return { ok: true, label: "worktree roots", verbose: evidence };
|
|
24465
|
+
if (!probe.stray.length) return { ok: true, id: "worktree-roots", label: "worktree roots", verbose: evidence };
|
|
24082
24466
|
const named = probe.stray.map((s) => `${s.path} (${s.dirs} dir(s), ${formatBytes(s.bytes, s.partial)})`).join("; ");
|
|
24083
24467
|
const cutShort = probe.stray.some((s) => s.partial) ? " (a \u2265 figure is a floor \u2014 the size walk hit its time budget, so the real total is larger)" : "";
|
|
24084
24468
|
return {
|
|
24085
24469
|
ok: false,
|
|
24470
|
+
id: "worktree-roots",
|
|
24086
24471
|
label: "worktree roots",
|
|
24087
24472
|
// #3485: report-only in EVERY mode means report-only in the exit code too. `worktree gc --root` does
|
|
24088
24473
|
// exist and is the eventual fix, but this row is deliberately not a gate: the remedy is a human
|
|
@@ -24111,16 +24496,20 @@ function checkClaudePlugin(probe) {
|
|
|
24111
24496
|
const trigger = pluginHealTrigger(probe);
|
|
24112
24497
|
if (trigger === "unresolved") {
|
|
24113
24498
|
return {
|
|
24499
|
+
id: "claude-plugin",
|
|
24114
24500
|
ok: false,
|
|
24115
|
-
label:
|
|
24501
|
+
label: "Claude plugin",
|
|
24502
|
+
detail: guardState === "no-install" ? "not installed" : "unresolved (marketplace/cache missing)",
|
|
24116
24503
|
fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall the MMI marketplace + plugin, then restart Claude",
|
|
24117
24504
|
verbose: evidence
|
|
24118
24505
|
};
|
|
24119
24506
|
}
|
|
24120
24507
|
if (trigger === "behind") {
|
|
24121
24508
|
return {
|
|
24509
|
+
id: "claude-plugin",
|
|
24122
24510
|
ok: false,
|
|
24123
|
-
label:
|
|
24511
|
+
label: "Claude plugin",
|
|
24512
|
+
detail: `${installed} \u2192 ${released}`,
|
|
24124
24513
|
// Name the CLI verb that actually fixes it, like every other red row — `/plugin` is the manual
|
|
24125
24514
|
// fallback, not the first resort (#3282).
|
|
24126
24515
|
fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall it, then restart Claude",
|
|
@@ -24128,7 +24517,7 @@ function checkClaudePlugin(probe) {
|
|
|
24128
24517
|
};
|
|
24129
24518
|
}
|
|
24130
24519
|
if (!released) return null;
|
|
24131
|
-
return { ok: true, label: installed ?
|
|
24520
|
+
return { id: "claude-plugin", ok: true, label: "Claude plugin", ...installed ? { detail: installed } : {}, verbose: evidence };
|
|
24132
24521
|
}
|
|
24133
24522
|
function checkCliVersion(input, releasedNote) {
|
|
24134
24523
|
const report = buildVersionLagReport(input);
|
|
@@ -24137,10 +24526,12 @@ function checkCliVersion(input, releasedNote) {
|
|
|
24137
24526
|
`published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}${report.releasedVersion && releasedNote ? ` ${releasedNote}` : ""}`
|
|
24138
24527
|
];
|
|
24139
24528
|
if (!report.releasedVersion) return null;
|
|
24140
|
-
if (report.ok) return { ok: true, label:
|
|
24529
|
+
if (report.ok) return { id: "cli-version", ok: true, label: "mmi-cli", detail: report.currentVersion, verbose: evidence };
|
|
24141
24530
|
return {
|
|
24531
|
+
id: "cli-version",
|
|
24142
24532
|
ok: false,
|
|
24143
|
-
label:
|
|
24533
|
+
label: "mmi-cli",
|
|
24534
|
+
detail: `${report.currentVersion} \u2192 ${report.releasedVersion}`,
|
|
24144
24535
|
fix: report.fix,
|
|
24145
24536
|
verbose: evidence
|
|
24146
24537
|
};
|
|
@@ -24154,6 +24545,7 @@ function checkPluginCache(input) {
|
|
|
24154
24545
|
if (input.stale.length === 0 && input.staging.length === 0) {
|
|
24155
24546
|
return {
|
|
24156
24547
|
ok: true,
|
|
24548
|
+
id: "plugin-cache",
|
|
24157
24549
|
label: "plugin cache",
|
|
24158
24550
|
detail: input.cached ? `${input.cached} version(s), nothing stale` : "no cache",
|
|
24159
24551
|
verbose: evidence
|
|
@@ -24170,6 +24562,7 @@ function checkPluginCache(input) {
|
|
|
24170
24562
|
}
|
|
24171
24563
|
return {
|
|
24172
24564
|
ok: false,
|
|
24565
|
+
id: "plugin-cache",
|
|
24173
24566
|
label: "plugin cache",
|
|
24174
24567
|
// #3485: deliberately NOT `reportOnly`, and not in the closed set docs/doctor-contract.md enumerates.
|
|
24175
24568
|
// A checker argued it qualifies because no `doctor --apply` clears it; I tagged it, then reverted.
|
|
@@ -24188,11 +24581,19 @@ function checkTrainSync(result) {
|
|
|
24188
24581
|
const evidence = result.branches.length ? result.branches.map((b) => `${b.branch}: ${b.status}${b.reason ? ` (${b.reason})` : ""}`) : ["no local train branches to sync"];
|
|
24189
24582
|
const problems = result.branches.filter((b) => b.status === "skipped" && (b.reason === "diverged" || b.reason === "ff-failed"));
|
|
24190
24583
|
if (problems.length) {
|
|
24191
|
-
return {
|
|
24584
|
+
return {
|
|
24585
|
+
ok: false,
|
|
24586
|
+
id: "train-branches",
|
|
24587
|
+
label: "train branches",
|
|
24588
|
+
detail: problems.map((b) => `${b.branch} ${b.reason}`).join(", "),
|
|
24589
|
+
fix: problems.map((b) => b.note).join("; "),
|
|
24590
|
+
verbose: evidence
|
|
24591
|
+
};
|
|
24192
24592
|
}
|
|
24193
24593
|
const moved = result.branches.filter((b) => b.status === "synced");
|
|
24194
24594
|
return {
|
|
24195
24595
|
ok: true,
|
|
24596
|
+
id: "train-branches",
|
|
24196
24597
|
label: "train branches",
|
|
24197
24598
|
detail: moved.length ? `fast-forwarded ${moved.map((b) => b.branch).join(", ")}` : void 0,
|
|
24198
24599
|
verbose: evidence
|
|
@@ -24209,6 +24610,7 @@ function checkSchedules(probe) {
|
|
|
24209
24610
|
if (probe.incomplete.length) {
|
|
24210
24611
|
return {
|
|
24211
24612
|
ok: false,
|
|
24613
|
+
id: "schedules",
|
|
24212
24614
|
label: "schedules",
|
|
24213
24615
|
// #3485: ruled report-only — an unreadable source is a read failure somewhere in the org, and
|
|
24214
24616
|
// nothing in this checkout clears it.
|
|
@@ -24221,6 +24623,7 @@ function checkSchedules(probe) {
|
|
|
24221
24623
|
if (probe.drift.length) {
|
|
24222
24624
|
return {
|
|
24223
24625
|
ok: false,
|
|
24626
|
+
id: "schedules",
|
|
24224
24627
|
label: "schedules",
|
|
24225
24628
|
// #3485 ruled this row report-only because the drift classes are owned by other repos. #3492 makes
|
|
24226
24629
|
// that conditional rather than blanket: when a finding IS clearable from this checkout — a
|
|
@@ -24233,16 +24636,17 @@ function checkSchedules(probe) {
|
|
|
24233
24636
|
verbose: evidence
|
|
24234
24637
|
};
|
|
24235
24638
|
}
|
|
24236
|
-
return { ok: true, label: "schedules", detail: `${probe.armed} armed`, verbose: evidence };
|
|
24639
|
+
return { ok: true, id: "schedules", label: "schedules", detail: `${probe.armed} armed`, verbose: evidence };
|
|
24237
24640
|
}
|
|
24238
24641
|
function checkDocsAudit(probe) {
|
|
24239
24642
|
if (!probe) return null;
|
|
24240
24643
|
if (!probe.armed) {
|
|
24241
|
-
return { ok: true, label: "docs-audit", detail: "janitor not armed", verbose: [probe.detail] };
|
|
24644
|
+
return { ok: true, id: "docs-audit", label: "docs-audit", detail: "janitor not armed", verbose: [probe.detail] };
|
|
24242
24645
|
}
|
|
24243
24646
|
if (!probe.ok) {
|
|
24244
24647
|
return {
|
|
24245
24648
|
ok: false,
|
|
24649
|
+
id: "docs-audit",
|
|
24246
24650
|
label: "docs-audit",
|
|
24247
24651
|
// #3485: ruled report-only — the remedy is re-running or re-arming the janitor in the repo that
|
|
24248
24652
|
// owns it. `mmi-cli docs audit record` can write a verdict from here, but hand-writing one to clear
|
|
@@ -24252,7 +24656,7 @@ function checkDocsAudit(probe) {
|
|
|
24252
24656
|
verbose: [probe.detail]
|
|
24253
24657
|
};
|
|
24254
24658
|
}
|
|
24255
|
-
return { ok: true, label: "docs-audit", detail: probe.detail, verbose: [probe.detail] };
|
|
24659
|
+
return { ok: true, id: "docs-audit", label: "docs-audit", detail: probe.detail, verbose: [probe.detail] };
|
|
24256
24660
|
}
|
|
24257
24661
|
function checkSessionPayload(probe) {
|
|
24258
24662
|
if (!probe) return null;
|
|
@@ -24269,8 +24673,13 @@ function checkSessionPayload(probe) {
|
|
|
24269
24673
|
if (status === "red") {
|
|
24270
24674
|
return {
|
|
24271
24675
|
ok: false,
|
|
24676
|
+
id: "sessionstart-payload",
|
|
24272
24677
|
label: "SessionStart payload",
|
|
24273
|
-
|
|
24678
|
+
// #3574: the count belongs in `detail`, like the amber row below already had it. The red row put
|
|
24679
|
+
// it in `fix` because the ✗ branch used to discard `detail`, which also left the head rendering as
|
|
24680
|
+
// a bare `✗ SessionStart payload` with the one number that matters buried in the advice.
|
|
24681
|
+
detail: `${chars} characters, over the ${SESSION_PAYLOAD_CAP_CHARS}-character cap`,
|
|
24682
|
+
fix: `Claude Code truncates it to a ~2,000-character preview with no error, so agents are starting on partial doctrine; cut a block from the session-start banner`,
|
|
24274
24683
|
verbose: evidence
|
|
24275
24684
|
};
|
|
24276
24685
|
}
|
|
@@ -24281,6 +24690,7 @@ function checkSessionPayload(probe) {
|
|
|
24281
24690
|
// amber tier dead code on the one lane that runs every session — drift past the budget was
|
|
24282
24691
|
// invisible until it became a truncation. Still ✓, still never moves the exit code.
|
|
24283
24692
|
warn: true,
|
|
24693
|
+
id: "sessionstart-payload",
|
|
24284
24694
|
label: "SessionStart payload",
|
|
24285
24695
|
detail: `amber \u2014 ${chars} characters, over the ${SESSION_PAYLOAD_BUDGET_CHARS}-character budget and ${SESSION_PAYLOAD_CAP_CHARS - chars} from the hard cap`,
|
|
24286
24696
|
verbose: evidence
|
|
@@ -24288,6 +24698,7 @@ function checkSessionPayload(probe) {
|
|
|
24288
24698
|
}
|
|
24289
24699
|
return {
|
|
24290
24700
|
ok: true,
|
|
24701
|
+
id: "sessionstart-payload",
|
|
24291
24702
|
label: "SessionStart payload",
|
|
24292
24703
|
detail: `${chars} of ${SESSION_PAYLOAD_BUDGET_CHARS} budgeted characters`,
|
|
24293
24704
|
verbose: evidence
|
|
@@ -24335,12 +24746,12 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24335
24746
|
`managed block: ${gi.ok ? "present and current" : "missing or out of date"}`
|
|
24336
24747
|
];
|
|
24337
24748
|
if (gi.ok) {
|
|
24338
|
-
checks.push({ ok: true, label: "gitignore block", verbose: giEvidence });
|
|
24749
|
+
checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
|
|
24339
24750
|
} else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
|
|
24340
|
-
checks.push({ ok: true, label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
|
|
24751
|
+
checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
|
|
24341
24752
|
restartPending = true;
|
|
24342
24753
|
} else {
|
|
24343
|
-
checks.push({ ok: false, label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
24754
|
+
checks.push({ ok: false, id: "gitignore-block", label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
|
|
24344
24755
|
}
|
|
24345
24756
|
}
|
|
24346
24757
|
const releasedNote = deps.releasedVersionNote?.();
|
|
@@ -24348,7 +24759,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24348
24759
|
const healTrigger = pluginHealTrigger(pluginProbe);
|
|
24349
24760
|
if (applyEnv && deps.healPlugin && healTrigger) {
|
|
24350
24761
|
const heal = await deps.healPlugin();
|
|
24351
|
-
const
|
|
24762
|
+
const measured = healTrigger === "behind" ? `${installed} \u2192 ${released}` : "unresolved install";
|
|
24352
24763
|
const healEvidence = [
|
|
24353
24764
|
`installed: ${installed ?? "(none)"}`,
|
|
24354
24765
|
`released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
|
|
@@ -24356,13 +24767,16 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24356
24767
|
`heal: ${heal.detail}`
|
|
24357
24768
|
];
|
|
24358
24769
|
checks.push(heal.ok ? {
|
|
24770
|
+
id: "claude-plugin",
|
|
24359
24771
|
ok: true,
|
|
24360
|
-
label,
|
|
24361
|
-
detail:
|
|
24772
|
+
label: "Claude plugin",
|
|
24773
|
+
detail: `${measured} \u2014 reinstalled via the MMI marketplace (remove \u2192 add \u2192 install)`,
|
|
24362
24774
|
verbose: healEvidence
|
|
24363
24775
|
} : {
|
|
24776
|
+
id: "claude-plugin",
|
|
24364
24777
|
ok: false,
|
|
24365
|
-
label,
|
|
24778
|
+
label: "Claude plugin",
|
|
24779
|
+
detail: measured,
|
|
24366
24780
|
// #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
|
|
24367
24781
|
// not fix what it found — but it is not this invocation's to clear. The other process is doing it;
|
|
24368
24782
|
// waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
|
|
@@ -24385,13 +24799,16 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24385
24799
|
const heal = await deps.updateCli(cliReport.releasedVersion);
|
|
24386
24800
|
const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
|
|
24387
24801
|
checks.push(heal.ok ? {
|
|
24802
|
+
id: "cli-version",
|
|
24388
24803
|
ok: true,
|
|
24389
|
-
label:
|
|
24390
|
-
detail:
|
|
24804
|
+
label: "mmi-cli",
|
|
24805
|
+
detail: `${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion} \u2014 self-updated via npm install -g; the next mmi-cli invocation runs the new version`,
|
|
24391
24806
|
verbose: healEvidence
|
|
24392
24807
|
} : {
|
|
24808
|
+
id: "cli-version",
|
|
24393
24809
|
ok: false,
|
|
24394
|
-
label:
|
|
24810
|
+
label: "mmi-cli",
|
|
24811
|
+
detail: `${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
|
|
24395
24812
|
// #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
|
|
24396
24813
|
...heal.skipped ? { reportOnly: true } : {},
|
|
24397
24814
|
fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(cliReport.releasedVersion)}\``,
|
|
@@ -24439,6 +24856,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24439
24856
|
const message = e instanceof Error ? e.message : String(e);
|
|
24440
24857
|
checks.push({
|
|
24441
24858
|
ok: false,
|
|
24859
|
+
id: "train-branches",
|
|
24442
24860
|
label: "train branches",
|
|
24443
24861
|
fix: `sync failed \u2014 ${message}`,
|
|
24444
24862
|
verbose: [`sync threw: ${message}`, "no train branch was fast-forwarded this run"]
|
|
@@ -24455,6 +24873,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24455
24873
|
if (r.removedWorktreeDirs.length) detailParts.push(`${r.removedWorktreeDirs.length} dead worktrees`);
|
|
24456
24874
|
if (r.removedTrackingRefs.length) detailParts.push(`${r.removedTrackingRefs.length} stale refs`);
|
|
24457
24875
|
checks.push({
|
|
24876
|
+
id: "branches-worktrees",
|
|
24458
24877
|
ok: true,
|
|
24459
24878
|
label: "branches / worktrees",
|
|
24460
24879
|
detail: reaped ? `reaped ${detailParts.join(", ")}` : "nothing stale",
|
|
@@ -24476,10 +24895,14 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24476
24895
|
...plan.trackingRefs.map((r) => `stale tracking ref: ${r.ref}`),
|
|
24477
24896
|
...plan.worktreeDirs.map((w) => `dead worktree: ${w.path} (${w.reason})`)
|
|
24478
24897
|
];
|
|
24479
|
-
checks.push(n === 0 ? { ok: true, label: "branches / worktrees", verbose: ["nothing reapable"] } : {
|
|
24898
|
+
checks.push(n === 0 ? { id: "branches-worktrees", ok: true, label: "branches / worktrees", verbose: ["nothing reapable"] } : {
|
|
24899
|
+
id: "branches-worktrees",
|
|
24480
24900
|
ok: false,
|
|
24481
24901
|
label: "branches / worktrees",
|
|
24482
|
-
|
|
24902
|
+
// #3574: `${n} stale` was prepended to `fix` because the ✗ branch would not render `detail` —
|
|
24903
|
+
// the same fact this row already puts in `detail` on its ✓ path four lines up. One convention now.
|
|
24904
|
+
detail: `${n} stale`,
|
|
24905
|
+
fix: "run `mmi-cli doctor --apply` to reap merged branches, stale refs, and dead worktrees",
|
|
24483
24906
|
verbose: gcEvidence
|
|
24484
24907
|
});
|
|
24485
24908
|
}
|
|
@@ -24490,10 +24913,10 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24490
24913
|
if (applyRepo && scratch.plan.safeAuto.length > 0) {
|
|
24491
24914
|
const applied = deps.executeScratchGc(repoRoot2, { apply: true });
|
|
24492
24915
|
const pruned = applied.applied?.pruned.length ?? 0;
|
|
24493
|
-
checks.push({ ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
|
|
24916
|
+
checks.push({ id: "scratch", ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
|
|
24494
24917
|
if (pruned) restartPending = true;
|
|
24495
24918
|
} else {
|
|
24496
|
-
checks.push(scratch.plan.safeAuto.length === 0 ? { ok: true, label: "scratch", verbose: scratchEvidence } : { ok: false, label: "scratch",
|
|
24919
|
+
checks.push(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor --apply`", verbose: scratchEvidence });
|
|
24497
24920
|
}
|
|
24498
24921
|
}
|
|
24499
24922
|
const exitCode = doctorReportExitCode(checks);
|
|
@@ -26514,6 +26937,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
26514
26937
|
jsonParity(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) => {
|
|
26515
26938
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
26516
26939
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
26940
|
+
const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
|
|
26517
26941
|
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+/);
|
|
26518
26942
|
const headIsProtected = isProtectedBranch(headRef);
|
|
26519
26943
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
@@ -26589,18 +27013,37 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
26589
27013
|
}
|
|
26590
27014
|
if (!ghPrMergeLocalBranchDeleteWarning(message)) throw e;
|
|
26591
27015
|
});
|
|
26592
|
-
|
|
26593
|
-
|
|
26594
|
-
|
|
26595
|
-
|
|
26596
|
-
|
|
26597
|
-
|
|
26598
|
-
|
|
26599
|
-
|
|
26600
|
-
|
|
26601
|
-
|
|
26602
|
-
|
|
27016
|
+
const stateRead = await readGhPrStateWithRetry(
|
|
27017
|
+
() => execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => r.stdout)
|
|
27018
|
+
);
|
|
27019
|
+
const gate = decidePrMergeCleanupGate({
|
|
27020
|
+
state: stateRead.ok ? stateRead.state : void 0,
|
|
27021
|
+
stateError: stateRead.ok ? void 0 : stateRead.error,
|
|
27022
|
+
autoRequested: Boolean(o.auto || upgradedToAuto)
|
|
27023
|
+
});
|
|
27024
|
+
if (gate.action === "enqueued") {
|
|
27025
|
+
const state = stateRead.ok ? stateRead.state : "";
|
|
27026
|
+
const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
|
|
27027
|
+
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
|
|
27028
|
+
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
|
|
27029
|
+
if (upgradedToAuto && !o.auto) {
|
|
27030
|
+
console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
|
|
27031
|
+
process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
|
|
26603
27032
|
}
|
|
27033
|
+
return;
|
|
27034
|
+
}
|
|
27035
|
+
if (gate.action === "refuse") {
|
|
27036
|
+
console.log(JSON.stringify({
|
|
27037
|
+
mergeStatus: "not-merged",
|
|
27038
|
+
reason: gate.reason,
|
|
27039
|
+
pr: number,
|
|
27040
|
+
branch: headRef,
|
|
27041
|
+
state: stateRead.ok ? stateRead.state : "unknown",
|
|
27042
|
+
cleanupStatus: "skipped"
|
|
27043
|
+
}));
|
|
27044
|
+
console.error(`pr merge: ${gate.message}. Nothing was deleted \u2014 re-check the PR and retry.`);
|
|
27045
|
+
process.exitCode = 1;
|
|
27046
|
+
return;
|
|
26604
27047
|
}
|
|
26605
27048
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
26606
27049
|
const remoteBranch = await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
@@ -26641,7 +27084,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
26641
27084
|
}
|
|
26642
27085
|
};
|
|
26643
27086
|
}
|
|
26644
|
-
const boardAdvance = await advanceClosedIssuesToDone2(number,
|
|
27087
|
+
const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
|
|
26645
27088
|
console.log(JSON.stringify({
|
|
26646
27089
|
...buildPrMergeResultPayload({
|
|
26647
27090
|
number,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.58.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",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "node build.mjs",
|
|
34
|
-
"test": "vitest run
|
|
34
|
+
"test": "vitest run",
|
|
35
35
|
"typecheck": "tsc --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|