@mutmutco/cli 2.5.0 → 2.6.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/index.cjs +261 -25
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3309,6 +3309,9 @@ function buildHealth(i) {
|
|
|
3309
3309
|
if (!i.sagaApiUrl) problems.push("sagaApiUrl not configured in .mmi/config.json");
|
|
3310
3310
|
if (!i.identity) problems.push("no GitHub identity (gh auth token / GH_TOKEN)");
|
|
3311
3311
|
if (!i.reachable) problems.push("saga backend unreachable");
|
|
3312
|
+
if (i.reachable && i.livenessStatus === 403 && i.livenessMessage === "Forbidden") {
|
|
3313
|
+
problems.push("saga API route-level 403 from GitHubAuthorizer cache/policy");
|
|
3314
|
+
}
|
|
3312
3315
|
if (i.reachable && i.authorized === false) problems.push("saga backend rejected authenticated state access");
|
|
3313
3316
|
if (!i.key.sessionId || i.key.sessionId === "-") problems.push("unsafe session id");
|
|
3314
3317
|
const safeToWrite = problems.length === 0;
|
|
@@ -3317,6 +3320,8 @@ function buildHealth(i) {
|
|
|
3317
3320
|
safeToWrite,
|
|
3318
3321
|
identity: i.identity,
|
|
3319
3322
|
reachable: i.reachable,
|
|
3323
|
+
livenessStatus: i.livenessStatus,
|
|
3324
|
+
livenessMessage: i.livenessMessage,
|
|
3320
3325
|
authorized: i.authorized,
|
|
3321
3326
|
sagaApiUrl: i.sagaApiUrl,
|
|
3322
3327
|
key: i.key,
|
|
@@ -3598,6 +3603,36 @@ query($owner: String!, $number: Int!, $after: String) {
|
|
|
3598
3603
|
}
|
|
3599
3604
|
}
|
|
3600
3605
|
}`;
|
|
3606
|
+
var ISSUE_PROJECT_ITEM_QUERY = `
|
|
3607
|
+
query($repoOwner: String!, $repoName: String!, $number: Int!) {
|
|
3608
|
+
repository(owner: $repoOwner, name: $repoName) {
|
|
3609
|
+
issue(number: $number) {
|
|
3610
|
+
id
|
|
3611
|
+
number
|
|
3612
|
+
title
|
|
3613
|
+
url
|
|
3614
|
+
state
|
|
3615
|
+
repository { nameWithOwner }
|
|
3616
|
+
labels(first: 10) { nodes { name } }
|
|
3617
|
+
assignees(first: 10) { nodes { login } }
|
|
3618
|
+
projectItems(first: 20) {
|
|
3619
|
+
nodes {
|
|
3620
|
+
id
|
|
3621
|
+
project { id title }
|
|
3622
|
+
fieldValues(first: 8) {
|
|
3623
|
+
nodes {
|
|
3624
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
3625
|
+
name
|
|
3626
|
+
optionId
|
|
3627
|
+
field { ... on ProjectV2SingleSelectField { name } }
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
}`;
|
|
3601
3636
|
function resolveBoardConfig(cfg) {
|
|
3602
3637
|
const missing = [];
|
|
3603
3638
|
if (!cfg.projectOwner) missing.push("projectOwner");
|
|
@@ -3871,6 +3906,14 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
3871
3906
|
const cfg = resolveBoardConfig(options.config);
|
|
3872
3907
|
const gh = deps.gh ?? defaultGh;
|
|
3873
3908
|
const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial }, deps);
|
|
3909
|
+
const selector = parseIssueSelector(options.selector, collected.repo);
|
|
3910
|
+
try {
|
|
3911
|
+
findBoardItem(collected.items, selector);
|
|
3912
|
+
} catch (e) {
|
|
3913
|
+
const fallback = await fetchIssueProjectItem(gh, cfg, selector);
|
|
3914
|
+
if (!fallback) throw e;
|
|
3915
|
+
collected.items.push(fallback);
|
|
3916
|
+
}
|
|
3874
3917
|
const writable = await resolveWritableReposForClaimables(collected.items, gh, options.allowPartial ?? false);
|
|
3875
3918
|
collected.warnings.push(...writable.warnings);
|
|
3876
3919
|
collected.partial = collected.partial || writable.partial;
|
|
@@ -3882,7 +3925,6 @@ async function claimBoardIssue(options, deps = {}) {
|
|
|
3882
3925
|
warnings: collected.warnings,
|
|
3883
3926
|
partial: collected.partial
|
|
3884
3927
|
};
|
|
3885
|
-
const selector = parseIssueSelector(options.selector, collected.repo);
|
|
3886
3928
|
const flatItem = findBoardItem(collected.items, selector);
|
|
3887
3929
|
if (flatItem.status === "Todo" && flatItem.assignees.length === 0 && !writable.repos.has(flatItem.repository.toLowerCase())) {
|
|
3888
3930
|
throw new Error(`${flatItem.ref} is not claimable: viewer does not have write access to ${flatItem.repository}`);
|
|
@@ -4039,6 +4081,42 @@ async function fetchProjectPage(gh, cfg, after) {
|
|
|
4039
4081
|
if (!parsed.data) throw new Error("gh GraphQL response did not include data");
|
|
4040
4082
|
return parsed.data;
|
|
4041
4083
|
}
|
|
4084
|
+
async function fetchIssueProjectItem(gh, cfg, selector) {
|
|
4085
|
+
const [repoOwner, repoName] = selector.repo.split("/");
|
|
4086
|
+
if (!repoOwner || !repoName) return void 0;
|
|
4087
|
+
const { stdout } = await gh([
|
|
4088
|
+
"api",
|
|
4089
|
+
"graphql",
|
|
4090
|
+
"-f",
|
|
4091
|
+
`query=${ISSUE_PROJECT_ITEM_QUERY}`,
|
|
4092
|
+
"-f",
|
|
4093
|
+
`repoOwner=${repoOwner}`,
|
|
4094
|
+
"-f",
|
|
4095
|
+
`repoName=${repoName}`,
|
|
4096
|
+
"-F",
|
|
4097
|
+
`number=${selector.number}`
|
|
4098
|
+
]);
|
|
4099
|
+
const parsed = JSON.parse(stdout);
|
|
4100
|
+
const issue2 = parsed.data?.repository?.issue;
|
|
4101
|
+
if (!issue2) return void 0;
|
|
4102
|
+
const projectItem = (issue2.projectItems?.nodes ?? []).find((item) => item.project?.id === cfg.projectId);
|
|
4103
|
+
if (!projectItem) return void 0;
|
|
4104
|
+
return nodeToItem({
|
|
4105
|
+
id: projectItem.id,
|
|
4106
|
+
fieldValues: projectItem.fieldValues,
|
|
4107
|
+
content: {
|
|
4108
|
+
__typename: "Issue",
|
|
4109
|
+
id: issue2.id,
|
|
4110
|
+
number: issue2.number,
|
|
4111
|
+
title: issue2.title,
|
|
4112
|
+
url: issue2.url,
|
|
4113
|
+
state: issue2.state,
|
|
4114
|
+
repository: issue2.repository,
|
|
4115
|
+
labels: issue2.labels,
|
|
4116
|
+
assignees: issue2.assignees
|
|
4117
|
+
}
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4042
4120
|
function nodesToItems(nodes, warnings) {
|
|
4043
4121
|
const items = [];
|
|
4044
4122
|
for (const node of nodes) {
|
|
@@ -4255,6 +4333,26 @@ function parseWorktreePorcelain(stdout) {
|
|
|
4255
4333
|
}
|
|
4256
4334
|
return out;
|
|
4257
4335
|
}
|
|
4336
|
+
function samePath(a, b) {
|
|
4337
|
+
return a.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase() === b.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
4338
|
+
}
|
|
4339
|
+
function selectPrMergeCleanupWorktree(branch, before, after, startingPath) {
|
|
4340
|
+
if (!branch) return void 0;
|
|
4341
|
+
const current = after.find((w) => w.branch === branch)?.path;
|
|
4342
|
+
if (current) return current;
|
|
4343
|
+
const previous = before.find((w) => w.branch === branch)?.path;
|
|
4344
|
+
if (previous) return previous;
|
|
4345
|
+
if (startingPath && before.some((w) => w.branch === branch && samePath(w.path, startingPath))) return startingPath;
|
|
4346
|
+
return void 0;
|
|
4347
|
+
}
|
|
4348
|
+
function selectSafeWorktreeCwd(worktrees, targetPath) {
|
|
4349
|
+
if (!targetPath) return void 0;
|
|
4350
|
+
return worktrees.find((w) => !samePath(w.path, targetPath))?.path;
|
|
4351
|
+
}
|
|
4352
|
+
function branchMissingFromList(branch, stdout) {
|
|
4353
|
+
const names = stdout.split(/\r?\n/).map((line) => line.replace(/^\*\s*/, "").trim()).filter(Boolean);
|
|
4354
|
+
return !names.includes(branch);
|
|
4355
|
+
}
|
|
4258
4356
|
function formatGcPlan(plan2, apply) {
|
|
4259
4357
|
const lines = [`mmi-cli gc: ${apply ? "apply" : "dry-run"}`];
|
|
4260
4358
|
if (!plan2.branches.length && !plan2.trackingRefs.length) lines.push("nothing to clean");
|
|
@@ -4485,8 +4583,13 @@ var PLUGIN_DRIFT_LABEL = "plugin config drift (mmi@mmi duplicate rows / stale gi
|
|
|
4485
4583
|
function recordFreshness(r) {
|
|
4486
4584
|
return r.lastUpdated ?? r.installedAt ?? "";
|
|
4487
4585
|
}
|
|
4488
|
-
function
|
|
4489
|
-
return records.reduce((best, r) =>
|
|
4586
|
+
function bestRecord(records) {
|
|
4587
|
+
return records.reduce((best, r) => {
|
|
4588
|
+
const byVersion = compareVersions(r.version ?? "0", best.version ?? "0");
|
|
4589
|
+
if (byVersion > 0) return r;
|
|
4590
|
+
if (byVersion < 0) return best;
|
|
4591
|
+
return recordFreshness(r) > recordFreshness(best) ? r : best;
|
|
4592
|
+
});
|
|
4490
4593
|
}
|
|
4491
4594
|
function pluginConfigDriftFix(pluginId) {
|
|
4492
4595
|
return `\`${pluginId}\` registered as N duplicate project rows / a stale gitCommitSha in ~/.claude/plugins/installed_plugins.json \u2014 run \`mmi-cli doctor\` to collapse them to one \`scope: user\` entry at the highest version (a .bak backup is written first), or in \`/plugin\` uninstall the extra rows and reinstall once at user scope`;
|
|
@@ -4503,19 +4606,21 @@ function buildPluginConfigDriftCheck(input) {
|
|
|
4503
4606
|
const records = input.installed?.plugins?.[pluginId];
|
|
4504
4607
|
if (!Array.isArray(records) || records.length === 0) return base;
|
|
4505
4608
|
const projectRows = records.filter((r) => r.scope === "project");
|
|
4506
|
-
const
|
|
4507
|
-
const
|
|
4508
|
-
const staleShaRows =
|
|
4609
|
+
const best = bestRecord(records);
|
|
4610
|
+
const bestSha = best.gitCommitSha;
|
|
4611
|
+
const staleShaRows = bestSha ? records.filter((r) => r.gitCommitSha !== void 0 && r.gitCommitSha !== bestSha).length : 0;
|
|
4612
|
+
const duplicateRows = records.length > 1 ? records.length : 0;
|
|
4509
4613
|
const duplicateProjectRows = projectRows.length > 1 ? projectRows.length : 0;
|
|
4510
|
-
if (
|
|
4511
|
-
return { ...base, duplicateProjectRows: 0, staleShaRows: 0 };
|
|
4614
|
+
if (duplicateRows === 0 && staleShaRows === 0) {
|
|
4615
|
+
return { ...base, duplicateRows: 0, duplicateProjectRows: 0, staleShaRows: 0 };
|
|
4512
4616
|
}
|
|
4513
|
-
const { projectPath: _drop, ...rest } =
|
|
4617
|
+
const { projectPath: _drop, ...rest } = best;
|
|
4514
4618
|
const collapsed = { ...rest, scope: "user" };
|
|
4515
4619
|
return {
|
|
4516
4620
|
...base,
|
|
4517
4621
|
ok: false,
|
|
4518
4622
|
recordsToWrite: [collapsed],
|
|
4623
|
+
duplicateRows,
|
|
4519
4624
|
duplicateProjectRows,
|
|
4520
4625
|
staleShaRows
|
|
4521
4626
|
};
|
|
@@ -4529,6 +4634,58 @@ function buildGitignoreManagedBlockCheck(input) {
|
|
|
4529
4634
|
if (!changed) return base;
|
|
4530
4635
|
return { ...base, ok: false, contentToWrite: content };
|
|
4531
4636
|
}
|
|
4637
|
+
function detectSurface(env) {
|
|
4638
|
+
const has = (k) => Boolean(env[k]?.trim());
|
|
4639
|
+
if (env.MMI_AGENT_SURFACE === "codex" || has("CODEX_HOME") || (env.CLAUDE_PLUGIN_ROOT ?? "").includes(".codex")) {
|
|
4640
|
+
return "codex";
|
|
4641
|
+
}
|
|
4642
|
+
const isClaude = has("CLAUDECODE") || has("CLAUDE_CODE_ENTRYPOINT") || has("CLAUDE_PLUGIN_ROOT") || env.MMI_AGENT_SURFACE === "claude";
|
|
4643
|
+
const isVscode = env.TERM_PROGRAM === "vscode" || has("VSCODE_PID") || has("VSCODE_GIT_ASKPASS_NODE");
|
|
4644
|
+
if (isClaude && isVscode) return "claude-vscode";
|
|
4645
|
+
if (isClaude) return "claude-cli";
|
|
4646
|
+
return "shell";
|
|
4647
|
+
}
|
|
4648
|
+
function pluginRecoveryFix(surface) {
|
|
4649
|
+
const claude = "claude plugin marketplace update mmi && claude plugin update mmi@mmi && claude plugin enable mmi@mmi";
|
|
4650
|
+
switch (surface) {
|
|
4651
|
+
case "claude-vscode":
|
|
4652
|
+
return `${claude} # then reopen the VS Code workspace to reload MMI commands`;
|
|
4653
|
+
case "claude-cli":
|
|
4654
|
+
return `${claude} # then restart Claude Code, or run /reload-plugins`;
|
|
4655
|
+
case "codex":
|
|
4656
|
+
return "codex plugin marketplace upgrade mmi && codex plugin add mmi@mmi # then restart Codex";
|
|
4657
|
+
case "shell":
|
|
4658
|
+
default:
|
|
4659
|
+
return "npm install -g @mutmutco/cli@latest";
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
var INSTALLED_PLUGIN_VERSION_LABEL = "installed MMI plugin version (vs latest release)";
|
|
4663
|
+
function isSemverVersion(v) {
|
|
4664
|
+
return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
|
|
4665
|
+
}
|
|
4666
|
+
function buildInstalledPluginVersionCheck(input) {
|
|
4667
|
+
const pluginId = input.pluginId ?? MMI_PLUGIN_ID;
|
|
4668
|
+
const base = {
|
|
4669
|
+
ok: true,
|
|
4670
|
+
label: INSTALLED_PLUGIN_VERSION_LABEL,
|
|
4671
|
+
fix: pluginRecoveryFix(input.surface),
|
|
4672
|
+
pluginId
|
|
4673
|
+
};
|
|
4674
|
+
if (!input.isOrgRepo) return base;
|
|
4675
|
+
const records = input.installed?.plugins?.[pluginId];
|
|
4676
|
+
if (!Array.isArray(records) || records.length === 0) return base;
|
|
4677
|
+
const installedVersion = bestRecord(records).version;
|
|
4678
|
+
if (!isSemverVersion(installedVersion) || !isSemverVersion(input.releasedVersion)) return base;
|
|
4679
|
+
if (compareVersions(installedVersion, input.releasedVersion) >= 0) {
|
|
4680
|
+
return { ...base, installedVersion, releasedVersion: input.releasedVersion };
|
|
4681
|
+
}
|
|
4682
|
+
return {
|
|
4683
|
+
...base,
|
|
4684
|
+
ok: false,
|
|
4685
|
+
installedVersion,
|
|
4686
|
+
releasedVersion: input.releasedVersion
|
|
4687
|
+
};
|
|
4688
|
+
}
|
|
4532
4689
|
|
|
4533
4690
|
// src/stage-runner.ts
|
|
4534
4691
|
var import_node_child_process3 = require("node:child_process");
|
|
@@ -6312,24 +6469,31 @@ async function applyGcPlan(plan2, remote) {
|
|
|
6312
6469
|
await execFileP3("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS });
|
|
6313
6470
|
}
|
|
6314
6471
|
}
|
|
6315
|
-
async function cleanupLocalBranch(branch) {
|
|
6472
|
+
async function cleanupLocalBranch(branch, before = {}) {
|
|
6316
6473
|
const result = { branchDeleted: false };
|
|
6317
6474
|
if (!branch) return result;
|
|
6318
6475
|
const { stdout } = await execFileP3("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
|
|
6319
|
-
const
|
|
6320
|
-
|
|
6321
|
-
|
|
6476
|
+
const afterWorktrees = parseWorktreePorcelain(stdout);
|
|
6477
|
+
const wtPath = selectPrMergeCleanupWorktree(branch, before.worktrees ?? [], afterWorktrees, before.startingPath);
|
|
6478
|
+
const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...before.worktrees ?? []], wtPath);
|
|
6479
|
+
const git = (args) => safeCwd ? execFileP3("git", ["-C", safeCwd, ...args], { timeout: GIT_TIMEOUT_MS }) : execFileP3("git", args, { timeout: GIT_TIMEOUT_MS });
|
|
6480
|
+
if (wtPath) {
|
|
6481
|
+
await git(["worktree", "remove", "--force", wtPath]).catch(() => {
|
|
6322
6482
|
});
|
|
6323
|
-
result.worktreeRemoved =
|
|
6483
|
+
result.worktreeRemoved = wtPath;
|
|
6324
6484
|
}
|
|
6325
|
-
const current = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]) || "";
|
|
6485
|
+
const current = safeCwd ? ((await git(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => ({ stdout: "" }))).stdout || "").trim() : await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]) || "";
|
|
6326
6486
|
if (branch !== current) {
|
|
6327
|
-
await
|
|
6487
|
+
await git(["branch", "-D", branch]).then(() => {
|
|
6328
6488
|
result.branchDeleted = true;
|
|
6329
6489
|
}).catch(() => {
|
|
6330
6490
|
});
|
|
6491
|
+
if (!result.branchDeleted) {
|
|
6492
|
+
const remaining = await git(["branch", "--list", branch]).catch(() => ({ stdout: "" }));
|
|
6493
|
+
result.branchDeleted = branchMissingFromList(branch, remaining.stdout || "");
|
|
6494
|
+
}
|
|
6331
6495
|
}
|
|
6332
|
-
if (
|
|
6496
|
+
if (wtPath) await git(["worktree", "prune"]).catch(() => {
|
|
6333
6497
|
});
|
|
6334
6498
|
return result;
|
|
6335
6499
|
}
|
|
@@ -6386,6 +6550,33 @@ async function applyVersionAutoUpdate(report, log) {
|
|
|
6386
6550
|
return report;
|
|
6387
6551
|
}
|
|
6388
6552
|
}
|
|
6553
|
+
var CLAUDE_PLUGIN_TIMEOUT_MS = 6e4;
|
|
6554
|
+
async function runClaudePlugin(args) {
|
|
6555
|
+
const candidates = isWin ? ["claude.cmd", "claude.exe", "claude"] : ["claude"];
|
|
6556
|
+
for (const bin of candidates) {
|
|
6557
|
+
try {
|
|
6558
|
+
await execFileP3(bin, args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
6559
|
+
return true;
|
|
6560
|
+
} catch (err) {
|
|
6561
|
+
if (err.code === "ENOENT") continue;
|
|
6562
|
+
return false;
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
return false;
|
|
6566
|
+
}
|
|
6567
|
+
async function applyClaudePluginHeal(surface, log) {
|
|
6568
|
+
if (surface !== "claude-cli" && surface !== "claude-vscode") return false;
|
|
6569
|
+
log(" \u21BB updating the MMI plugin via `claude plugin` (marketplace \u2192 update \u2192 enable)\u2026");
|
|
6570
|
+
const steps = [
|
|
6571
|
+
["plugin", "marketplace", "update", "mmi"],
|
|
6572
|
+
["plugin", "update", "mmi@mmi"],
|
|
6573
|
+
["plugin", "enable", "mmi@mmi"]
|
|
6574
|
+
];
|
|
6575
|
+
for (const step of steps) {
|
|
6576
|
+
if (!await runClaudePlugin(step)) return false;
|
|
6577
|
+
}
|
|
6578
|
+
return true;
|
|
6579
|
+
}
|
|
6389
6580
|
var program2 = new Command();
|
|
6390
6581
|
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveVersion());
|
|
6391
6582
|
var rules = program2.command("rules").description("org rules delivery");
|
|
@@ -6527,9 +6718,15 @@ saga.command("key").option("--json", "machine-readable output").description("pri
|
|
|
6527
6718
|
async function probeBackend(url) {
|
|
6528
6719
|
try {
|
|
6529
6720
|
const res = await fetch(`${url}/saga/head`, { headers: await sagaHeaders(), signal: AbortSignal.timeout(8e3) });
|
|
6530
|
-
|
|
6721
|
+
let message = "";
|
|
6722
|
+
try {
|
|
6723
|
+
const body = await res.clone().json();
|
|
6724
|
+
message = typeof body.message === "string" ? body.message : "";
|
|
6725
|
+
} catch {
|
|
6726
|
+
}
|
|
6727
|
+
return { reachable: res.ok || res.status === 403, status: res.status, message };
|
|
6531
6728
|
} catch {
|
|
6532
|
-
return false;
|
|
6729
|
+
return { reachable: false };
|
|
6533
6730
|
}
|
|
6534
6731
|
}
|
|
6535
6732
|
async function probeSagaAccess(url, key) {
|
|
@@ -6547,9 +6744,18 @@ saga.command("health").option("--json", "machine-readable output").option("--ban
|
|
|
6547
6744
|
const key = await sagaKey(cfg, session);
|
|
6548
6745
|
const source = session.source;
|
|
6549
6746
|
const identity = await githubLogin();
|
|
6550
|
-
const
|
|
6551
|
-
const authorized = cfg.sagaApiUrl && reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
|
|
6552
|
-
const report = buildHealth({
|
|
6747
|
+
const liveness = cfg.sagaApiUrl ? await probeBackend(cfg.sagaApiUrl) : { reachable: false };
|
|
6748
|
+
const authorized = cfg.sagaApiUrl && liveness.reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
|
|
6749
|
+
const report = buildHealth({
|
|
6750
|
+
key,
|
|
6751
|
+
source,
|
|
6752
|
+
identity,
|
|
6753
|
+
reachable: liveness.reachable,
|
|
6754
|
+
livenessStatus: liveness.status,
|
|
6755
|
+
livenessMessage: liveness.message,
|
|
6756
|
+
authorized,
|
|
6757
|
+
sagaApiUrl: cfg.sagaApiUrl
|
|
6758
|
+
});
|
|
6553
6759
|
if (o.json) return console.log(JSON.stringify(report));
|
|
6554
6760
|
if (o.banner) {
|
|
6555
6761
|
const banner = healthBanner(report);
|
|
@@ -7015,10 +7221,14 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
7015
7221
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
7016
7222
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
7017
7223
|
const headRef = (await execFileP3("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
|
|
7224
|
+
const startingPath = (await execFileP3("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
7225
|
+
const beforeWorktrees = parseWorktreePorcelain(
|
|
7226
|
+
(await execFileP3("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
7227
|
+
);
|
|
7018
7228
|
await execFileP3("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout: GC_GH_TIMEOUT_MS }).catch((e) => {
|
|
7019
7229
|
if (!/used by worktree|cannot delete branch|already been merged/i.test(String(e.message || ""))) throw e;
|
|
7020
7230
|
});
|
|
7021
|
-
const cleaned = repoArgs.length ? { branchDeleted: false } : await cleanupLocalBranch(headRef);
|
|
7231
|
+
const cleaned = repoArgs.length ? { branchDeleted: false } : await cleanupLocalBranch(headRef, { worktrees: beforeWorktrees, startingPath });
|
|
7022
7232
|
console.log(JSON.stringify({ merged: number, branch: headRef, method: method.slice(2), ...cleaned }));
|
|
7023
7233
|
});
|
|
7024
7234
|
async function runBoardRead(o) {
|
|
@@ -7458,7 +7668,7 @@ function writeGitignore(content) {
|
|
|
7458
7668
|
return false;
|
|
7459
7669
|
}
|
|
7460
7670
|
}
|
|
7461
|
-
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, repo config, plugin git clone, plugin install record, .gitignore managed block, plugin config drift) and print fixes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output").action(async (opts) => {
|
|
7671
|
+
program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, repo config, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version) and print fixes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output").action(async (opts) => {
|
|
7462
7672
|
if (opts.guide) {
|
|
7463
7673
|
if (opts.json) console.log(JSON.stringify({ resources: [MMI_AGENTIC_ONBOARDING_GUIDE] }, null, 2));
|
|
7464
7674
|
else console.log(MMI_AGENTIC_ONBOARDING_GUIDE.url);
|
|
@@ -7486,12 +7696,15 @@ program2.command("doctor").description("check onboarding gates (GitHub auth, mmi
|
|
|
7486
7696
|
if (root && (0, import_node_fs4.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
|
|
7487
7697
|
}
|
|
7488
7698
|
checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
|
|
7699
|
+
const surface = detectSurface(process.env);
|
|
7700
|
+
const releasedVersion = await fetchReleasedVersion();
|
|
7489
7701
|
let versionReport = buildVersionLagReport({
|
|
7490
7702
|
currentVersion: resolveVersion(),
|
|
7491
7703
|
repoVersion: readRepoVersion(),
|
|
7492
|
-
releasedVersion
|
|
7704
|
+
releasedVersion
|
|
7493
7705
|
});
|
|
7494
7706
|
if (!opts.json) versionReport = await applyVersionAutoUpdate(versionReport, (m) => console.error(m));
|
|
7707
|
+
if (!versionReport.ok) versionReport = { ...versionReport, fix: pluginRecoveryFix(surface) };
|
|
7495
7708
|
checks.push(versionReport);
|
|
7496
7709
|
const cfg = await loadConfig();
|
|
7497
7710
|
checks.push({ ok: Boolean(cfg.sagaApiUrl), label: "repo config (.mmi/config.json)", fix: "ask a master-admin to run /bootstrap on this repo" });
|
|
@@ -7543,7 +7756,30 @@ program2.command("doctor").description("check onboarding gates (GitHub auth, mmi
|
|
|
7543
7756
|
if (!opts.banner) console.error(" \u21BB repaired: collapsed mmi@mmi to one user-scope entry (backup at installed_plugins.json.bak) \u2014 run /reload-plugins");
|
|
7544
7757
|
}
|
|
7545
7758
|
}
|
|
7759
|
+
if (!driftCheck.ok) driftCheck = { ...driftCheck, fix: pluginRecoveryFix(surface) };
|
|
7546
7760
|
checks.push(driftCheck);
|
|
7761
|
+
let installedVersionCheck = buildInstalledPluginVersionCheck({
|
|
7762
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
7763
|
+
installed,
|
|
7764
|
+
releasedVersion,
|
|
7765
|
+
surface
|
|
7766
|
+
});
|
|
7767
|
+
if (!installedVersionCheck.ok && !opts.json) {
|
|
7768
|
+
if (await applyClaudePluginHeal(surface, (m) => console.error(m))) {
|
|
7769
|
+
const healed = buildInstalledPluginVersionCheck({
|
|
7770
|
+
isOrgRepo: Boolean(cfg.sagaApiUrl),
|
|
7771
|
+
installed: readInstalledPlugins(),
|
|
7772
|
+
releasedVersion,
|
|
7773
|
+
surface
|
|
7774
|
+
});
|
|
7775
|
+
installedVersionCheck = healed;
|
|
7776
|
+
if (healed.ok && !opts.banner) {
|
|
7777
|
+
const reload = surface === "claude-vscode" ? "reopen the VS Code workspace" : "restart Claude Code (or run /reload-plugins)";
|
|
7778
|
+
console.error(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via claude plugin \u2014 ${reload} to load the new commands`);
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
7781
|
+
}
|
|
7782
|
+
checks.push(installedVersionCheck);
|
|
7547
7783
|
const gaps = checks.filter((c) => !c.ok);
|
|
7548
7784
|
const resources = doctorResourcesForGaps(gaps);
|
|
7549
7785
|
if (opts.json) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|