@mutmutco/cli 3.57.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 +411 -65
- package/package.json +1 -1
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,
|
|
@@ -17733,10 +17862,30 @@ function registerSecretsCommands(program3) {
|
|
|
17733
17862
|
if (!ok) process.exitCode = 1;
|
|
17734
17863
|
}));
|
|
17735
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)));
|
|
17736
|
-
secrets.command("use <key> [command...]").description("consume
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
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
|
+
});
|
|
17740
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, {})));
|
|
17741
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, {})));
|
|
17742
17891
|
}
|
|
@@ -17881,11 +18030,11 @@ function renderPoolDetail(probe) {
|
|
|
17881
18030
|
function checkGithubPools(probe) {
|
|
17882
18031
|
if (!probe) return [];
|
|
17883
18032
|
const lines = [];
|
|
17884
|
-
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`" });
|
|
17885
18034
|
if (typeof probe.app === "string") {
|
|
17886
|
-
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 });
|
|
17887
18036
|
} else if (probe.app) {
|
|
17888
|
-
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) });
|
|
17889
18038
|
}
|
|
17890
18039
|
return lines;
|
|
17891
18040
|
}
|
|
@@ -18113,6 +18262,92 @@ var import_node_child_process10 = require("node:child_process");
|
|
|
18113
18262
|
var import_node_util7 = require("node:util");
|
|
18114
18263
|
|
|
18115
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
|
+
}
|
|
18116
18351
|
var ORG = "mutmutco";
|
|
18117
18352
|
var DOC_START_MARKER = "<!-- schedules:inventory:start -->";
|
|
18118
18353
|
var DOC_END_MARKER = "<!-- schedules:inventory:end -->";
|
|
@@ -18270,19 +18505,23 @@ function sortEntries(entries) {
|
|
|
18270
18505
|
(a, b) => a.executor.localeCompare(b.executor) || a.name.localeCompare(b.name)
|
|
18271
18506
|
);
|
|
18272
18507
|
}
|
|
18273
|
-
function formatSchedulesTable(entries) {
|
|
18508
|
+
function formatSchedulesTable(entries, now = /* @__PURE__ */ new Date()) {
|
|
18274
18509
|
if (!entries.length) return "no armed org schedules found";
|
|
18510
|
+
const resolvedOf = new Map(entries.map((e) => [e, resolvedLabel(e, now)]));
|
|
18275
18511
|
const w = (pick, head) => Math.max(head.length, ...entries.map((e) => pick(e).length));
|
|
18276
18512
|
const nameW = w((e) => e.name, "JOB");
|
|
18277
18513
|
const cadW = w((e) => e.cadence, "CADENCE");
|
|
18278
18514
|
const exeW = w((e) => e.executor, "EXECUTOR");
|
|
18279
18515
|
const llmW = w((e) => e.llm, "LLM");
|
|
18280
|
-
const resW = w((e) => e.resolved, "RESOLVED");
|
|
18516
|
+
const resW = w((e) => resolvedOf.get(e) ?? e.resolved, "RESOLVED");
|
|
18281
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}`;
|
|
18282
18518
|
const lines = [row("JOB", "CADENCE", "EXECUTOR", "LLM", "RESOLVED", "SOURCE")];
|
|
18283
|
-
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));
|
|
18284
18520
|
return lines.join("\n");
|
|
18285
18521
|
}
|
|
18522
|
+
function withTickState(entry, now = /* @__PURE__ */ new Date()) {
|
|
18523
|
+
return { ...entry, ...scheduleTickState(entry.cadence, entry.armedAt, now) };
|
|
18524
|
+
}
|
|
18286
18525
|
function renderDocSection(entries, generatedAtIso) {
|
|
18287
18526
|
const lines = [
|
|
18288
18527
|
"| Job | Cadence | Executor | LLM | Resolved | Source |",
|
|
@@ -18642,7 +18881,8 @@ function registerSchedulesCommands(program3) {
|
|
|
18642
18881
|
return;
|
|
18643
18882
|
}
|
|
18644
18883
|
if (o.json) {
|
|
18645
|
-
|
|
18884
|
+
const now = /* @__PURE__ */ new Date();
|
|
18885
|
+
console.log(JSON.stringify({ org: entries.map((e) => withTickState(e, now)), incomplete, drift, reconciliation }, null, 2));
|
|
18646
18886
|
} else {
|
|
18647
18887
|
console.log(formatSchedulesTable(entries));
|
|
18648
18888
|
reportDrift(drift);
|
|
@@ -21825,28 +22065,71 @@ function formatStaleLeaks(leaks) {
|
|
|
21825
22065
|
async function repoRootOf() {
|
|
21826
22066
|
return (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
21827
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
|
+
}
|
|
21828
22099
|
function decideWorktreeLandRefCleanup(input) {
|
|
21829
22100
|
if (input.keepRemote) return { action: "proceed" };
|
|
21830
|
-
|
|
21831
|
-
|
|
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
|
+
} : {
|
|
21832
22109
|
action: "keep-remote",
|
|
21833
22110
|
reason: "pr-state-unreadable",
|
|
21834
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`
|
|
21835
22112
|
};
|
|
21836
22113
|
}
|
|
21837
|
-
|
|
21838
|
-
if (open2.length) {
|
|
22114
|
+
if (verdict.state === "open") {
|
|
21839
22115
|
return {
|
|
21840
22116
|
action: "refuse",
|
|
21841
22117
|
reason: "open-pr",
|
|
21842
|
-
message: `'${input.branch}' still has an OPEN pull request (
|
|
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`
|
|
21843
22126
|
};
|
|
21844
22127
|
}
|
|
21845
|
-
if (
|
|
22128
|
+
if (verdict.state === "closed-unmerged") {
|
|
21846
22129
|
return {
|
|
21847
22130
|
action: "refuse",
|
|
21848
22131
|
reason: "unmerged-pr",
|
|
21849
|
-
message: `'${input.branch}' has a pull request that never merged (
|
|
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`
|
|
21850
22133
|
};
|
|
21851
22134
|
}
|
|
21852
22135
|
return { action: "proceed" };
|
|
@@ -21934,9 +22217,11 @@ function registerWorktreeCommands(program3) {
|
|
|
21934
22217
|
if (landDirty) {
|
|
21935
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 });
|
|
21936
22219
|
}
|
|
22220
|
+
const landPrs = await fetchLandBranchPrs(branch);
|
|
22221
|
+
const mergeVerdict = classifyLandBranchMergeState(landPrs);
|
|
21937
22222
|
const landRefs = decideWorktreeLandRefCleanup({
|
|
21938
22223
|
branch,
|
|
21939
|
-
prs:
|
|
22224
|
+
prs: landPrs,
|
|
21940
22225
|
keepRemote: Boolean(o.keepRemote)
|
|
21941
22226
|
});
|
|
21942
22227
|
if (landRefs.action === "refuse") {
|
|
@@ -21969,10 +22254,22 @@ function registerWorktreeCommands(program3) {
|
|
|
21969
22254
|
report.push({ step: `delete remote branch ${branch}`, status: `skipped: ${landRefs.action === "keep-remote" ? landRefs.reason : "kept"}` });
|
|
21970
22255
|
}
|
|
21971
22256
|
report.push(await bestEffortGit(["worktree", "prune"], primaryCheckout, "prune worktree metadata"));
|
|
21972
|
-
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
|
+
};
|
|
21973
22267
|
if (o.json) return console.log(JSON.stringify(result, null, 2));
|
|
21974
|
-
console.log(`worktree land: cleaned up branch ${branch}`);
|
|
22268
|
+
console.log(`worktree land: cleaned up branch ${branch} (${describeLandMergeState(mergeVerdict)})`);
|
|
21975
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
|
+
}
|
|
21976
22273
|
if (report.some((r) => r.status.startsWith("failed"))) process.exitCode = 1;
|
|
21977
22274
|
} catch (e) {
|
|
21978
22275
|
return failGraceful(`worktree land: ${e.message}`);
|
|
@@ -22719,8 +23016,11 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
22719
23016
|
updateRecipe: [CLAUDE_RECOVERY]
|
|
22720
23017
|
}
|
|
22721
23018
|
};
|
|
22722
|
-
function nonClaudeSurfaceHealMessage() {
|
|
22723
|
-
|
|
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)";
|
|
22724
23024
|
}
|
|
22725
23025
|
function healStepAborts(step, ok) {
|
|
22726
23026
|
return !ok && step.gated;
|
|
@@ -22917,7 +23217,7 @@ async function runGuard(readOrigin) {
|
|
|
22917
23217
|
}
|
|
22918
23218
|
async function runPluginHeal(surface = detectSurface(process.env)) {
|
|
22919
23219
|
if (surfaceToken(surface) !== "claude") {
|
|
22920
|
-
console.log(nonClaudeSurfaceHealMessage());
|
|
23220
|
+
console.log(nonClaudeSurfaceHealMessage(surfaceToken(surface) ?? void 0));
|
|
22921
23221
|
return;
|
|
22922
23222
|
}
|
|
22923
23223
|
const descriptor = PLUGIN_SURFACE_HEAL.claude;
|
|
@@ -24059,7 +24359,9 @@ function renderCheckLine(check) {
|
|
|
24059
24359
|
if (check.ok) {
|
|
24060
24360
|
return check.detail ? `\u2713 ${check.label} \u2014 ${check.detail}` : `\u2713 ${check.label}`;
|
|
24061
24361
|
}
|
|
24062
|
-
|
|
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;
|
|
24063
24365
|
}
|
|
24064
24366
|
function renderReport(checks, opts) {
|
|
24065
24367
|
const lines = checks.map(renderCheckLine);
|
|
@@ -24073,7 +24375,8 @@ function renderTally(checks) {
|
|
|
24073
24375
|
}
|
|
24074
24376
|
function renderDoctorText(checks, opts) {
|
|
24075
24377
|
const lines = [];
|
|
24076
|
-
|
|
24378
|
+
const shown = opts.verbose ? checks : checks.filter((c) => !c.ok || c.warn);
|
|
24379
|
+
for (const check of shown) {
|
|
24077
24380
|
lines.push(renderCheckLine(check));
|
|
24078
24381
|
if (opts.verbose) {
|
|
24079
24382
|
for (const evidence of check.verbose ?? []) lines.push(`${VERBOSE_INDENT}${evidence}`);
|
|
@@ -24096,6 +24399,7 @@ function checkGithubAuth(probe) {
|
|
|
24096
24399
|
const rescuers = reach.otherAccountsThatCanSee ?? [];
|
|
24097
24400
|
return {
|
|
24098
24401
|
ok: false,
|
|
24402
|
+
id: "github-auth",
|
|
24099
24403
|
label: "github auth",
|
|
24100
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`,
|
|
24101
24405
|
verbose: [
|
|
@@ -24108,6 +24412,7 @@ function checkGithubAuth(probe) {
|
|
|
24108
24412
|
}
|
|
24109
24413
|
return {
|
|
24110
24414
|
ok: authed,
|
|
24415
|
+
id: "github-auth",
|
|
24111
24416
|
label: "github auth",
|
|
24112
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",
|
|
24113
24418
|
verbose: [
|
|
@@ -24123,6 +24428,7 @@ function checkAwsIdentity(probe) {
|
|
|
24123
24428
|
const arn = probe.callerArn?.trim();
|
|
24124
24429
|
return {
|
|
24125
24430
|
ok: !arn || !arn.endsWith(":root"),
|
|
24431
|
+
id: "aws-identity",
|
|
24126
24432
|
label: "aws identity",
|
|
24127
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",
|
|
24128
24434
|
// Name the caller. A green "aws identity" says only "not root" — it does not say WHO, and "no AWS
|
|
@@ -24137,6 +24443,7 @@ function checkAwsIdentity(probe) {
|
|
|
24137
24443
|
function checkRepoWorktrees(probe) {
|
|
24138
24444
|
return {
|
|
24139
24445
|
ok: !(probe.isOrgRepo && probe.hasRepoLocalWorktrees),
|
|
24446
|
+
id: "repo-worktrees",
|
|
24140
24447
|
label: "repo worktrees",
|
|
24141
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/`",
|
|
24142
24449
|
verbose: [
|
|
@@ -24155,11 +24462,12 @@ function checkWorktreeRoots(probe) {
|
|
|
24155
24462
|
`scanned by worktree gc/list: ${probe.authoritative}`,
|
|
24156
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"]
|
|
24157
24464
|
];
|
|
24158
|
-
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 };
|
|
24159
24466
|
const named = probe.stray.map((s) => `${s.path} (${s.dirs} dir(s), ${formatBytes(s.bytes, s.partial)})`).join("; ");
|
|
24160
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)" : "";
|
|
24161
24468
|
return {
|
|
24162
24469
|
ok: false,
|
|
24470
|
+
id: "worktree-roots",
|
|
24163
24471
|
label: "worktree roots",
|
|
24164
24472
|
// #3485: report-only in EVERY mode means report-only in the exit code too. `worktree gc --root` does
|
|
24165
24473
|
// exist and is the eventual fix, but this row is deliberately not a gate: the remedy is a human
|
|
@@ -24188,16 +24496,20 @@ function checkClaudePlugin(probe) {
|
|
|
24188
24496
|
const trigger = pluginHealTrigger(probe);
|
|
24189
24497
|
if (trigger === "unresolved") {
|
|
24190
24498
|
return {
|
|
24499
|
+
id: "claude-plugin",
|
|
24191
24500
|
ok: false,
|
|
24192
|
-
label:
|
|
24501
|
+
label: "Claude plugin",
|
|
24502
|
+
detail: guardState === "no-install" ? "not installed" : "unresolved (marketplace/cache missing)",
|
|
24193
24503
|
fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall the MMI marketplace + plugin, then restart Claude",
|
|
24194
24504
|
verbose: evidence
|
|
24195
24505
|
};
|
|
24196
24506
|
}
|
|
24197
24507
|
if (trigger === "behind") {
|
|
24198
24508
|
return {
|
|
24509
|
+
id: "claude-plugin",
|
|
24199
24510
|
ok: false,
|
|
24200
|
-
label:
|
|
24511
|
+
label: "Claude plugin",
|
|
24512
|
+
detail: `${installed} \u2192 ${released}`,
|
|
24201
24513
|
// Name the CLI verb that actually fixes it, like every other red row — `/plugin` is the manual
|
|
24202
24514
|
// fallback, not the first resort (#3282).
|
|
24203
24515
|
fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall it, then restart Claude",
|
|
@@ -24205,7 +24517,7 @@ function checkClaudePlugin(probe) {
|
|
|
24205
24517
|
};
|
|
24206
24518
|
}
|
|
24207
24519
|
if (!released) return null;
|
|
24208
|
-
return { ok: true, label: installed ?
|
|
24520
|
+
return { id: "claude-plugin", ok: true, label: "Claude plugin", ...installed ? { detail: installed } : {}, verbose: evidence };
|
|
24209
24521
|
}
|
|
24210
24522
|
function checkCliVersion(input, releasedNote) {
|
|
24211
24523
|
const report = buildVersionLagReport(input);
|
|
@@ -24214,10 +24526,12 @@ function checkCliVersion(input, releasedNote) {
|
|
|
24214
24526
|
`published: ${report.releasedVersion ?? "(not checked \u2014 offline or --fast)"}${report.releasedVersion && releasedNote ? ` ${releasedNote}` : ""}`
|
|
24215
24527
|
];
|
|
24216
24528
|
if (!report.releasedVersion) return null;
|
|
24217
|
-
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 };
|
|
24218
24530
|
return {
|
|
24531
|
+
id: "cli-version",
|
|
24219
24532
|
ok: false,
|
|
24220
|
-
label:
|
|
24533
|
+
label: "mmi-cli",
|
|
24534
|
+
detail: `${report.currentVersion} \u2192 ${report.releasedVersion}`,
|
|
24221
24535
|
fix: report.fix,
|
|
24222
24536
|
verbose: evidence
|
|
24223
24537
|
};
|
|
@@ -24231,6 +24545,7 @@ function checkPluginCache(input) {
|
|
|
24231
24545
|
if (input.stale.length === 0 && input.staging.length === 0) {
|
|
24232
24546
|
return {
|
|
24233
24547
|
ok: true,
|
|
24548
|
+
id: "plugin-cache",
|
|
24234
24549
|
label: "plugin cache",
|
|
24235
24550
|
detail: input.cached ? `${input.cached} version(s), nothing stale` : "no cache",
|
|
24236
24551
|
verbose: evidence
|
|
@@ -24247,6 +24562,7 @@ function checkPluginCache(input) {
|
|
|
24247
24562
|
}
|
|
24248
24563
|
return {
|
|
24249
24564
|
ok: false,
|
|
24565
|
+
id: "plugin-cache",
|
|
24250
24566
|
label: "plugin cache",
|
|
24251
24567
|
// #3485: deliberately NOT `reportOnly`, and not in the closed set docs/doctor-contract.md enumerates.
|
|
24252
24568
|
// A checker argued it qualifies because no `doctor --apply` clears it; I tagged it, then reverted.
|
|
@@ -24265,11 +24581,19 @@ function checkTrainSync(result) {
|
|
|
24265
24581
|
const evidence = result.branches.length ? result.branches.map((b) => `${b.branch}: ${b.status}${b.reason ? ` (${b.reason})` : ""}`) : ["no local train branches to sync"];
|
|
24266
24582
|
const problems = result.branches.filter((b) => b.status === "skipped" && (b.reason === "diverged" || b.reason === "ff-failed"));
|
|
24267
24583
|
if (problems.length) {
|
|
24268
|
-
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
|
+
};
|
|
24269
24592
|
}
|
|
24270
24593
|
const moved = result.branches.filter((b) => b.status === "synced");
|
|
24271
24594
|
return {
|
|
24272
24595
|
ok: true,
|
|
24596
|
+
id: "train-branches",
|
|
24273
24597
|
label: "train branches",
|
|
24274
24598
|
detail: moved.length ? `fast-forwarded ${moved.map((b) => b.branch).join(", ")}` : void 0,
|
|
24275
24599
|
verbose: evidence
|
|
@@ -24286,6 +24610,7 @@ function checkSchedules(probe) {
|
|
|
24286
24610
|
if (probe.incomplete.length) {
|
|
24287
24611
|
return {
|
|
24288
24612
|
ok: false,
|
|
24613
|
+
id: "schedules",
|
|
24289
24614
|
label: "schedules",
|
|
24290
24615
|
// #3485: ruled report-only — an unreadable source is a read failure somewhere in the org, and
|
|
24291
24616
|
// nothing in this checkout clears it.
|
|
@@ -24298,6 +24623,7 @@ function checkSchedules(probe) {
|
|
|
24298
24623
|
if (probe.drift.length) {
|
|
24299
24624
|
return {
|
|
24300
24625
|
ok: false,
|
|
24626
|
+
id: "schedules",
|
|
24301
24627
|
label: "schedules",
|
|
24302
24628
|
// #3485 ruled this row report-only because the drift classes are owned by other repos. #3492 makes
|
|
24303
24629
|
// that conditional rather than blanket: when a finding IS clearable from this checkout — a
|
|
@@ -24310,16 +24636,17 @@ function checkSchedules(probe) {
|
|
|
24310
24636
|
verbose: evidence
|
|
24311
24637
|
};
|
|
24312
24638
|
}
|
|
24313
|
-
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 };
|
|
24314
24640
|
}
|
|
24315
24641
|
function checkDocsAudit(probe) {
|
|
24316
24642
|
if (!probe) return null;
|
|
24317
24643
|
if (!probe.armed) {
|
|
24318
|
-
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] };
|
|
24319
24645
|
}
|
|
24320
24646
|
if (!probe.ok) {
|
|
24321
24647
|
return {
|
|
24322
24648
|
ok: false,
|
|
24649
|
+
id: "docs-audit",
|
|
24323
24650
|
label: "docs-audit",
|
|
24324
24651
|
// #3485: ruled report-only — the remedy is re-running or re-arming the janitor in the repo that
|
|
24325
24652
|
// owns it. `mmi-cli docs audit record` can write a verdict from here, but hand-writing one to clear
|
|
@@ -24329,7 +24656,7 @@ function checkDocsAudit(probe) {
|
|
|
24329
24656
|
verbose: [probe.detail]
|
|
24330
24657
|
};
|
|
24331
24658
|
}
|
|
24332
|
-
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] };
|
|
24333
24660
|
}
|
|
24334
24661
|
function checkSessionPayload(probe) {
|
|
24335
24662
|
if (!probe) return null;
|
|
@@ -24346,8 +24673,13 @@ function checkSessionPayload(probe) {
|
|
|
24346
24673
|
if (status === "red") {
|
|
24347
24674
|
return {
|
|
24348
24675
|
ok: false,
|
|
24676
|
+
id: "sessionstart-payload",
|
|
24349
24677
|
label: "SessionStart payload",
|
|
24350
|
-
|
|
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`,
|
|
24351
24683
|
verbose: evidence
|
|
24352
24684
|
};
|
|
24353
24685
|
}
|
|
@@ -24358,6 +24690,7 @@ function checkSessionPayload(probe) {
|
|
|
24358
24690
|
// amber tier dead code on the one lane that runs every session — drift past the budget was
|
|
24359
24691
|
// invisible until it became a truncation. Still ✓, still never moves the exit code.
|
|
24360
24692
|
warn: true,
|
|
24693
|
+
id: "sessionstart-payload",
|
|
24361
24694
|
label: "SessionStart payload",
|
|
24362
24695
|
detail: `amber \u2014 ${chars} characters, over the ${SESSION_PAYLOAD_BUDGET_CHARS}-character budget and ${SESSION_PAYLOAD_CAP_CHARS - chars} from the hard cap`,
|
|
24363
24696
|
verbose: evidence
|
|
@@ -24365,6 +24698,7 @@ function checkSessionPayload(probe) {
|
|
|
24365
24698
|
}
|
|
24366
24699
|
return {
|
|
24367
24700
|
ok: true,
|
|
24701
|
+
id: "sessionstart-payload",
|
|
24368
24702
|
label: "SessionStart payload",
|
|
24369
24703
|
detail: `${chars} of ${SESSION_PAYLOAD_BUDGET_CHARS} budgeted characters`,
|
|
24370
24704
|
verbose: evidence
|
|
@@ -24412,12 +24746,12 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24412
24746
|
`managed block: ${gi.ok ? "present and current" : "missing or out of date"}`
|
|
24413
24747
|
];
|
|
24414
24748
|
if (gi.ok) {
|
|
24415
|
-
checks.push({ ok: true, label: "gitignore block", verbose: giEvidence });
|
|
24749
|
+
checks.push({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
|
|
24416
24750
|
} else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
|
|
24417
|
-
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 });
|
|
24418
24752
|
restartPending = true;
|
|
24419
24753
|
} else {
|
|
24420
|
-
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 });
|
|
24421
24755
|
}
|
|
24422
24756
|
}
|
|
24423
24757
|
const releasedNote = deps.releasedVersionNote?.();
|
|
@@ -24425,7 +24759,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24425
24759
|
const healTrigger = pluginHealTrigger(pluginProbe);
|
|
24426
24760
|
if (applyEnv && deps.healPlugin && healTrigger) {
|
|
24427
24761
|
const heal = await deps.healPlugin();
|
|
24428
|
-
const
|
|
24762
|
+
const measured = healTrigger === "behind" ? `${installed} \u2192 ${released}` : "unresolved install";
|
|
24429
24763
|
const healEvidence = [
|
|
24430
24764
|
`installed: ${installed ?? "(none)"}`,
|
|
24431
24765
|
`released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
|
|
@@ -24433,13 +24767,16 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24433
24767
|
`heal: ${heal.detail}`
|
|
24434
24768
|
];
|
|
24435
24769
|
checks.push(heal.ok ? {
|
|
24770
|
+
id: "claude-plugin",
|
|
24436
24771
|
ok: true,
|
|
24437
|
-
label,
|
|
24438
|
-
detail:
|
|
24772
|
+
label: "Claude plugin",
|
|
24773
|
+
detail: `${measured} \u2014 reinstalled via the MMI marketplace (remove \u2192 add \u2192 install)`,
|
|
24439
24774
|
verbose: healEvidence
|
|
24440
24775
|
} : {
|
|
24776
|
+
id: "claude-plugin",
|
|
24441
24777
|
ok: false,
|
|
24442
|
-
label,
|
|
24778
|
+
label: "Claude plugin",
|
|
24779
|
+
detail: measured,
|
|
24443
24780
|
// #3489: a heal skipped because another doctor holds the env-heal lock is a real ✗ — this run did
|
|
24444
24781
|
// not fix what it found — but it is not this invocation's to clear. The other process is doing it;
|
|
24445
24782
|
// waiting is the correct response and a re-run finds it healed. A heal that RAN and failed is a
|
|
@@ -24462,13 +24799,16 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24462
24799
|
const heal = await deps.updateCli(cliReport.releasedVersion);
|
|
24463
24800
|
const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
|
|
24464
24801
|
checks.push(heal.ok ? {
|
|
24802
|
+
id: "cli-version",
|
|
24465
24803
|
ok: true,
|
|
24466
|
-
label:
|
|
24467
|
-
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`,
|
|
24468
24806
|
verbose: healEvidence
|
|
24469
24807
|
} : {
|
|
24808
|
+
id: "cli-version",
|
|
24470
24809
|
ok: false,
|
|
24471
|
-
label:
|
|
24810
|
+
label: "mmi-cli",
|
|
24811
|
+
detail: `${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
|
|
24472
24812
|
// #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
|
|
24473
24813
|
...heal.skipped ? { reportOnly: true } : {},
|
|
24474
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)}\``,
|
|
@@ -24516,6 +24856,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24516
24856
|
const message = e instanceof Error ? e.message : String(e);
|
|
24517
24857
|
checks.push({
|
|
24518
24858
|
ok: false,
|
|
24859
|
+
id: "train-branches",
|
|
24519
24860
|
label: "train branches",
|
|
24520
24861
|
fix: `sync failed \u2014 ${message}`,
|
|
24521
24862
|
verbose: [`sync threw: ${message}`, "no train branch was fast-forwarded this run"]
|
|
@@ -24532,6 +24873,7 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24532
24873
|
if (r.removedWorktreeDirs.length) detailParts.push(`${r.removedWorktreeDirs.length} dead worktrees`);
|
|
24533
24874
|
if (r.removedTrackingRefs.length) detailParts.push(`${r.removedTrackingRefs.length} stale refs`);
|
|
24534
24875
|
checks.push({
|
|
24876
|
+
id: "branches-worktrees",
|
|
24535
24877
|
ok: true,
|
|
24536
24878
|
label: "branches / worktrees",
|
|
24537
24879
|
detail: reaped ? `reaped ${detailParts.join(", ")}` : "nothing stale",
|
|
@@ -24553,10 +24895,14 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24553
24895
|
...plan.trackingRefs.map((r) => `stale tracking ref: ${r.ref}`),
|
|
24554
24896
|
...plan.worktreeDirs.map((w) => `dead worktree: ${w.path} (${w.reason})`)
|
|
24555
24897
|
];
|
|
24556
|
-
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",
|
|
24557
24900
|
ok: false,
|
|
24558
24901
|
label: "branches / worktrees",
|
|
24559
|
-
|
|
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",
|
|
24560
24906
|
verbose: gcEvidence
|
|
24561
24907
|
});
|
|
24562
24908
|
}
|
|
@@ -24567,10 +24913,10 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
24567
24913
|
if (applyRepo && scratch.plan.safeAuto.length > 0) {
|
|
24568
24914
|
const applied = deps.executeScratchGc(repoRoot2, { apply: true });
|
|
24569
24915
|
const pruned = applied.applied?.pruned.length ?? 0;
|
|
24570
|
-
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 });
|
|
24571
24917
|
if (pruned) restartPending = true;
|
|
24572
24918
|
} else {
|
|
24573
|
-
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 });
|
|
24574
24920
|
}
|
|
24575
24921
|
}
|
|
24576
24922
|
const exitCode = doctorReportExitCode(checks);
|
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",
|