amicus 4.5.0 → 4.5.1
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/docs/ROADMAP.md +6 -8
- package/docs/council.md +5 -2
- package/docs/usage.md +1 -1
- package/package.json +1 -1
- package/src/cli-handlers-doctor.js +10 -0
- package/src/mcp-council-run.js +4 -2
- package/src/sidecar/electron-state.js +61 -0
- package/src/sidecar/workspace-auto-open.js +18 -4
- package/src/utils/doctor-electron-mcp-check.js +150 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.1",
|
|
4
4
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christian Wagner"
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
All notable changes to Amicus are documented here. Format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
|
|
5
5
|
|
|
6
|
+
## [4.5.1] - 2026-07-30
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`electron-mcp` doctor check — doctor now validates Electron in the install the MCP actually
|
|
11
|
+
runs from (#76).** `amicus doctor` used to probe Electron only in the copy doctor itself runs
|
|
12
|
+
from (usually the global install), while `npx -y amicus@latest mcp` serves councils from an
|
|
13
|
+
npx-cache copy — so doctor could print `Electron: ok` while every `ui: true` run failed with
|
|
14
|
+
`electron-absent` (the electron-flavored recurrence of the engine's green-while-broken defect).
|
|
15
|
+
The new check enumerates running/global/npx-cache installs (reusing the engine scanner) and
|
|
16
|
+
probes Electron in each through a dual-root resolver — npm nests `electron` under
|
|
17
|
+
`amicus/node_modules` in a global install but hoists it to a sibling in the npx cache (the #69
|
|
18
|
+
layout lesson, now applied to Electron). With `--fix`, binary-missing npx copies are healed in
|
|
19
|
+
place via `repairElectron` under the #56 timeout guard; never-installed copies are reported,
|
|
20
|
+
not repaired.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **`workspaceOpenReason` distinguishes a broken Electron from a missing one (#76).**
|
|
25
|
+
`electron-absent` now means the electron package was never installed; the new
|
|
26
|
+
`electron-broken: binary missing under <dir> — run `amicus doctor --fix`` covers the
|
|
27
|
+
package-present-but-binary-missing state (interrupted postinstall, AV quarantine) that the old
|
|
28
|
+
single reason conflated with it — naming the exact dir and the one-command fix.
|
|
29
|
+
|
|
6
30
|
## [4.5.0] - 2026-07-28
|
|
7
31
|
|
|
8
32
|
"Save and share your councils" — complex run configurations become one command, repeatable and
|
package/README.md
CHANGED
package/docs/ROADMAP.md
CHANGED
|
@@ -7,14 +7,12 @@ major jump, gated on funding/cofounder. The observability arc is split so the **
|
|
|
7
7
|
first (v4.3)** and the **Electron "Council Workspace" (v4.4)** rides on top of it. `--dry-run` cost
|
|
8
8
|
preview dropped to the backlog.
|
|
9
9
|
|
|
10
|
-
Amicus is at **v4.
|
|
11
|
-
|
|
10
|
+
Amicus is at **v4.5.0** (tagged 2026-07-28). Each 4.x rev below leads with the benefit, not the
|
|
11
|
+
plumbing.
|
|
12
12
|
|
|
13
|
-
**Status:** v4.0 through **v4.
|
|
14
|
-
record of what landed, not a plan. **v4.
|
|
15
|
-
|
|
16
|
-
split 2026-07-27 (design: `docs/superpowers/specs/2026-07-27-v4.5-save-and-share-design.md`). v5.0
|
|
17
|
-
remains forward-looking.
|
|
13
|
+
**Status:** v4.0 through **v4.5.0** have **shipped** — everything down to the v4.6 heading is a
|
|
14
|
+
record of what landed, not a plan. **v4.6 (composition + tagging + GUI ergonomics) is the next
|
|
15
|
+
rev.** v5.0 remains forward-looking.
|
|
18
16
|
|
|
19
17
|
> 📁 **Reading this from an npm install?** Some references below point at working documents that
|
|
20
18
|
> live in the git repository and are deliberately **not** in the published package — anything under
|
|
@@ -87,7 +85,7 @@ and a review that honestly finds nothing stops being an error.
|
|
|
87
85
|
> repo's working notes (`.superpowers/sdd/v441/backlog-and-proposal.md`, local-only) and in the
|
|
88
86
|
> repo's root `BACKLOG.md` — neither ships in the npm package; see the note at the top.
|
|
89
87
|
|
|
90
|
-
## v4.5 — "Save and share your councils" *(scope split 2026-07-27 — composition moved to v4.6)*
|
|
88
|
+
## v4.5 — "Save and share your councils" *(scope split 2026-07-27 — composition moved to v4.6)* — ✅ SHIPPED v4.5.0, 2026-07-28
|
|
91
89
|
**Benefit:** complex councils become one-command, repeatable, and shareable — and the flagship GUI
|
|
92
90
|
stops hiding. Design: `docs/superpowers/specs/2026-07-27-v4.5-save-and-share-design.md`.
|
|
93
91
|
- **★ Auto-open the Council Workspace on a council run (Christian, 2026-07-26)** — when a council is
|
package/docs/council.md
CHANGED
|
@@ -413,8 +413,11 @@ bottom, first match wins:
|
|
|
413
413
|
6. Otherwise → opens.
|
|
414
414
|
|
|
415
415
|
**Response fields.** `amicus_council_run`'s result always carries `workspaceOpened: boolean`, and,
|
|
416
|
-
**only when it did not open**, `workspaceOpenReason` — one of `param-suppressed`, `electron-absent
|
|
417
|
-
|
|
416
|
+
**only when it did not open**, `workspaceOpenReason` — one of `param-suppressed`, `electron-absent`
|
|
417
|
+
(the electron package was never installed for this copy), an `electron-broken:` detail (the package
|
|
418
|
+
is present but its binary never arrived — interrupted download or AV quarantine; the reason names
|
|
419
|
+
the electron dir and points at `amicus doctor --fix`), `no-display`, `config-disabled`,
|
|
420
|
+
`client-not-code-local`, or a `spawn-failed:`/`auto-open-failed:`
|
|
418
421
|
detail if the decision said to open but the launch itself failed. The launch is fire-and-forget: it
|
|
419
422
|
never blocks the tool's response, and a launch failure never fails the council run itself — the run
|
|
420
423
|
proceeds exactly as it would with no Workspace at all, and `amicus watch <runId> --ui` still works
|
package/docs/usage.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.1",
|
|
4
4
|
"mcpName": "io.github.BourbonDog/amicus",
|
|
5
5
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
6
6
|
"keywords": [
|
|
@@ -7,6 +7,8 @@ const mcpChecks = require('./utils/doctor-mcp-checks');
|
|
|
7
7
|
// engine-mcp check body — verifies the engine in the npx-cache copies the MCP
|
|
8
8
|
// actually launches (bug report #1). Split out to keep this file under the gate.
|
|
9
9
|
const engineCheck = require('./utils/doctor-engine-check');
|
|
10
|
+
// electron-mcp check body (#76) — same blind spot, electron-flavored.
|
|
11
|
+
const electronMcpCheck = require('./utils/doctor-electron-mcp-check');
|
|
10
12
|
// local-providers check body (v4.2 §4.7 C8) — split out to keep this file
|
|
11
13
|
// under the gate (mirrors the engineCheck/mcpChecks split above).
|
|
12
14
|
const localProvidersCheck = require('./utils/doctor-local-providers-check');
|
|
@@ -47,6 +49,8 @@ function realDeps() {
|
|
|
47
49
|
scanEngineInstalls: () => require('./utils/engine-install-scan').scanEngineInstalls(),
|
|
48
50
|
// report #2: copy-from-sibling self-heal for `doctor --fix`.
|
|
49
51
|
repairEngine: (o) => require('./utils/engine-repair').repairEngine(o),
|
|
52
|
+
// electron-mcp check (#76): same enumeration, electron probed per copy.
|
|
53
|
+
scanElectronInstalls: () => electronMcpCheck.scanElectronInstalls(),
|
|
50
54
|
getElectronPath: () => require('./sidecar/interactive-process').getElectronPath(),
|
|
51
55
|
// #56: self-heal primitive for `doctor --fix`. Pure probe (getElectronPath)
|
|
52
56
|
// stays separate; repair only runs when fix is requested.
|
|
@@ -161,6 +165,12 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
161
165
|
// install) can't hide a broken copy the MCP would spawn (bug report #1/#4).
|
|
162
166
|
checks.push(await guardAsync('engine-mcp', 'OpenCode engine (MCP launch path)', () => engineCheck.evaluateEngineMcp(d)));
|
|
163
167
|
|
|
168
|
+
// #76: same green-while-broken blind spot for Electron — probe the npx-cache
|
|
169
|
+
// copies `ui: true` actually depends on. fixTimeoutMs forwards the #56
|
|
170
|
+
// never-hang guard to the per-copy repairElectron calls.
|
|
171
|
+
checks.push(await guardAsync('electron-mcp', 'Electron (MCP launch path)',
|
|
172
|
+
() => electronMcpCheck.evaluateElectronMcp({ ...d, fixTimeoutMs: FIX_TIMEOUT_MS })));
|
|
173
|
+
|
|
164
174
|
checks.push(await guardAsync('electron', 'Electron (interactive GUI)', async () => {
|
|
165
175
|
if (d.getElectronPath()) {
|
|
166
176
|
return { id: 'electron', name: 'Electron (interactive GUI)', status: 'ok', message: 'installed', hint: null };
|
package/src/mcp-council-run.js
CHANGED
|
@@ -241,11 +241,13 @@ async function handleCouncilRunTool(input, project, helpers) {
|
|
|
241
241
|
let workspaceOpened = false;
|
|
242
242
|
let workspaceOpenReason = null;
|
|
243
243
|
try {
|
|
244
|
-
const {
|
|
244
|
+
const { probeElectronState } = require('./sidecar/electron-state');
|
|
245
245
|
const { getWorkspaceAutoOpen } = require('./utils/config');
|
|
246
|
+
const es = probeElectronState(); // 3-state (#76): splits absent vs broken
|
|
246
247
|
const decision = ao.decide({
|
|
247
248
|
client: helpers.clientName,
|
|
248
|
-
|
|
249
|
+
electronState: es.state,
|
|
250
|
+
electronDir: es.electronDir,
|
|
249
251
|
platform: process.platform,
|
|
250
252
|
env: process.env,
|
|
251
253
|
autoOpenConfig: getWorkspaceAutoOpen(),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Electron install-state probes (#76).
|
|
3
|
+
*
|
|
4
|
+
* Distinguishes the two states isElectronUsable() collapses into one boolean:
|
|
5
|
+
* 'package-missing' — the electron optionalDependency was never installed
|
|
6
|
+
* (possibly deliberate: headless-only install)
|
|
7
|
+
* 'binary-missing' — package present but dist/<exe> absent (interrupted
|
|
8
|
+
* postinstall, AV quarantine) — repairElectron territory
|
|
9
|
+
* 'ok' — the resolved exe exists on disk
|
|
10
|
+
*
|
|
11
|
+
* Also owns electronDirFor(): the per-install dual-root layout probe. npm
|
|
12
|
+
* NESTS electron under amicus/node_modules in a global install but HOISTS it
|
|
13
|
+
* to a sibling in the npx cache — the same layout asymmetry that broke the
|
|
14
|
+
* engine probe (#69). In-process require.resolve handles hoisting via walk-up,
|
|
15
|
+
* which is why the running copy probes itself fine; only CROSS-install probing
|
|
16
|
+
* (doctor) needs this explicit dual-root check.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const fsDefault = require('fs');
|
|
23
|
+
const { isElectronUsable, defaultElectronDir } = require('./electron-install');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Locate the electron package dir serving a given amicus install. Checks the
|
|
27
|
+
* nested root first (require.resolve walk-up order), then the hoisted sibling.
|
|
28
|
+
* @param {string} pkgDir amicus package root
|
|
29
|
+
* @param {{fs?:object}} [deps]
|
|
30
|
+
* @returns {string|null} electron package dir, or null when not installed
|
|
31
|
+
*/
|
|
32
|
+
function electronDirFor(pkgDir, { fs = fsDefault } = {}) {
|
|
33
|
+
const candidates = [
|
|
34
|
+
path.join(pkgDir, 'node_modules', 'electron'),
|
|
35
|
+
path.join(path.dirname(pkgDir), 'electron'),
|
|
36
|
+
];
|
|
37
|
+
for (const dir of candidates) {
|
|
38
|
+
try { if (fs.existsSync(dir)) { return dir; } } catch { /* unreadable root */ }
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Probe one electron package dir into the 3-state verdict.
|
|
45
|
+
* `electronDir: null` (electronDirFor found nothing) short-circuits to
|
|
46
|
+
* package-missing; an explicit dir without package.json reports the same.
|
|
47
|
+
* @param {{electronDir?:string|null, fs?:object, platform?:string, env?:object}} [opts]
|
|
48
|
+
* @returns {{state:'ok'|'package-missing'|'binary-missing', electronDir:string|null}}
|
|
49
|
+
*/
|
|
50
|
+
function probeElectronState({
|
|
51
|
+
electronDir = defaultElectronDir(), fs = fsDefault, platform = process.platform, env = process.env,
|
|
52
|
+
} = {}) {
|
|
53
|
+
if (!electronDir) { return { state: 'package-missing', electronDir: null }; }
|
|
54
|
+
let hasPkg = false;
|
|
55
|
+
try { hasPkg = fs.existsSync(path.join(electronDir, 'package.json')); } catch { /* unreadable */ }
|
|
56
|
+
if (!hasPkg) { return { state: 'package-missing', electronDir }; }
|
|
57
|
+
const usable = isElectronUsable({ electronDir, fs, platform, env });
|
|
58
|
+
return { state: usable ? 'ok' : 'binary-missing', electronDir };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { electronDirFor, probeElectronState };
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* Decision order (spec §6 guard 4):
|
|
8
8
|
* 1. uiParam === false → 'param-suppressed' (explicit user request beats everything, checked first)
|
|
9
9
|
* 2. Hard guards (always checked, beat even explicit true):
|
|
10
|
-
* -
|
|
10
|
+
* - electron package-missing → 'electron-absent'
|
|
11
|
+
* - electron binary-missing → 'electron-broken: …' (#76: package present but
|
|
12
|
+
* the exe never arrived — a repairable state the old boolean conflated
|
|
13
|
+
* with never-installed; the reason names the dir and the fix)
|
|
11
14
|
* - platform === 'linux' && !env.DISPLAY → 'no-display'
|
|
12
15
|
* 3. uiParam === true → 'ok' (explicit request overrides config and client gate, never hard guards)
|
|
13
16
|
* 4. autoOpenConfig === false → 'config-disabled'
|
|
@@ -16,7 +19,9 @@
|
|
|
16
19
|
*
|
|
17
20
|
* @param {object} options
|
|
18
21
|
* @param {string} options.client - The client type (e.g., 'code-local', 'cowork', 'code-web')
|
|
19
|
-
* @param {boolean} options.electronUsable -
|
|
22
|
+
* @param {boolean} [options.electronUsable] - Legacy boolean probe (used only when electronState is absent)
|
|
23
|
+
* @param {'ok'|'package-missing'|'binary-missing'} [options.electronState] - 3-state probe (#76); takes precedence
|
|
24
|
+
* @param {string|null} [options.electronDir] - Resolved electron dir, named in the electron-broken reason
|
|
20
25
|
* @param {string} options.platform - The platform (e.g., 'win32', 'darwin', 'linux')
|
|
21
26
|
* @param {object} options.env - Environment variables object
|
|
22
27
|
* @param {boolean} options.autoOpenConfig - The config.workspace.autoOpen setting
|
|
@@ -26,6 +31,8 @@
|
|
|
26
31
|
function shouldAutoOpenWorkspace({
|
|
27
32
|
client,
|
|
28
33
|
electronUsable,
|
|
34
|
+
electronState,
|
|
35
|
+
electronDir,
|
|
29
36
|
platform,
|
|
30
37
|
env,
|
|
31
38
|
autoOpenConfig,
|
|
@@ -36,10 +43,17 @@ function shouldAutoOpenWorkspace({
|
|
|
36
43
|
return { open: false, reason: 'param-suppressed' };
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
// Step 2: Hard guards (always checked, beat even explicit uiParam === true)
|
|
40
|
-
|
|
46
|
+
// Step 2: Hard guards (always checked, beat even explicit uiParam === true).
|
|
47
|
+
// electronState (3-state, #76) wins over the legacy electronUsable boolean,
|
|
48
|
+
// whose false collapses to package-missing (the old 'electron-absent').
|
|
49
|
+
const state = electronState || (electronUsable ? 'ok' : 'package-missing');
|
|
50
|
+
if (state === 'package-missing') {
|
|
41
51
|
return { open: false, reason: 'electron-absent' };
|
|
42
52
|
}
|
|
53
|
+
if (state === 'binary-missing') {
|
|
54
|
+
const where = electronDir ? ` under ${electronDir}` : '';
|
|
55
|
+
return { open: false, reason: `electron-broken: binary missing${where} — run \`amicus doctor --fix\`` };
|
|
56
|
+
}
|
|
43
57
|
|
|
44
58
|
if (platform === 'linux' && !env.DISPLAY) {
|
|
45
59
|
return { open: false, reason: 'no-display' };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module utils/doctor-electron-mcp-check
|
|
3
|
+
* The `electron-mcp` doctor check ("Electron (MCP launch path)"), split out of
|
|
4
|
+
* src/cli-handlers-doctor.js to keep that file under the 300-line gate
|
|
5
|
+
* (mirrors doctor-engine-check.js, which did the same for the engine — #76 is
|
|
6
|
+
* the electron-flavored recurrence of that check's bug report #1).
|
|
7
|
+
*
|
|
8
|
+
* The existing `electron` check verifies Electron in the RUNNING install. This
|
|
9
|
+
* one verifies the copies the MCP actually launches from — the npx-cache
|
|
10
|
+
* installs `npx -y amicus@latest mcp` resolves to — so a green doctor can no
|
|
11
|
+
* longer hide an npx copy whose `ui: true` will fail with electron-absent.
|
|
12
|
+
*
|
|
13
|
+
* Severity is WARN at worst (never error): a broken Electron only costs the
|
|
14
|
+
* GUI; headless councils still work — same tier the running-copy check uses.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const HINTS = require('./remediation-hints');
|
|
20
|
+
|
|
21
|
+
const plural = (n, one, many) => (n === 1 ? one : many);
|
|
22
|
+
|
|
23
|
+
/** One-line detail for a broken copy, distinguishing the two states (#76). */
|
|
24
|
+
const describeBroken = (i) => (i.state === 'binary-missing'
|
|
25
|
+
? `${i.pkgDir} (binary missing; electron dir: ${i.electronDir})`
|
|
26
|
+
: `${i.pkgDir} (not installed)`);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Enumerate the amicus installs that could serve the MCP and probe Electron in
|
|
30
|
+
* each via the dual-root resolver (#69 lesson: npx HOISTS electron to a
|
|
31
|
+
* sibling; a global install nests it).
|
|
32
|
+
* @param {object} [deps] engine-install-scan seams plus {readAmicusMcpConfig}
|
|
33
|
+
* @returns {{installs:Array<{kind,pkgDir,electronDir,state}>, mcpLaunch:string}}
|
|
34
|
+
*/
|
|
35
|
+
function scanElectronInstalls(deps = {}) {
|
|
36
|
+
const fs = deps.fs || require('fs');
|
|
37
|
+
const platform = deps.platform || process.platform;
|
|
38
|
+
const readAmicusMcpConfig = deps.readAmicusMcpConfig
|
|
39
|
+
|| (() => require('./mcp-discovery').readAmicusMcpConfig());
|
|
40
|
+
const { listAmicusInstalls, classifyLaunch } = require('./engine-install-scan');
|
|
41
|
+
const { electronDirFor, probeElectronState } = require('../sidecar/electron-state');
|
|
42
|
+
|
|
43
|
+
const installs = listAmicusInstalls(deps).map((i) => {
|
|
44
|
+
const probe = probeElectronState({ electronDir: electronDirFor(i.pkgDir, { fs }), fs, platform });
|
|
45
|
+
return { ...i, electronDir: probe.electronDir, state: probe.state };
|
|
46
|
+
});
|
|
47
|
+
let config = null;
|
|
48
|
+
try { config = readAmicusMcpConfig(); } catch { /* unreadable config */ }
|
|
49
|
+
return { installs, mcpLaunch: classifyLaunch(config) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {{scanElectronInstalls: () => {installs:Array, mcpLaunch:string}}} d
|
|
54
|
+
* @returns {{id,name,status,message,hint}}
|
|
55
|
+
*/
|
|
56
|
+
function evaluateElectronInstalls(d) {
|
|
57
|
+
const id = 'electron-mcp';
|
|
58
|
+
const name = 'Electron (MCP launch path)';
|
|
59
|
+
const { installs, mcpLaunch } = d.scanElectronInstalls();
|
|
60
|
+
|
|
61
|
+
if (mcpLaunch === 'none') {
|
|
62
|
+
return { id, name, status: 'ok', message: 'no amicus MCP registered — not checked', hint: null };
|
|
63
|
+
}
|
|
64
|
+
if (mcpLaunch === 'path') {
|
|
65
|
+
return {
|
|
66
|
+
id, name, status: 'ok',
|
|
67
|
+
message: 'MCP launches from a fixed path — covered by the Electron (interactive GUI) check', hint: null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 'npx' (and the 'unknown' fallback): verify the npx-cache copies, the ones
|
|
72
|
+
// whose optional-dependency electron install can silently half-complete.
|
|
73
|
+
const npxCopies = installs.filter((i) => i.kind === 'npx');
|
|
74
|
+
if (npxCopies.length === 0) {
|
|
75
|
+
return {
|
|
76
|
+
id, name, status: 'warn',
|
|
77
|
+
message: 'MCP launches via npx; no cached copy to inspect yet — run one council, then re-run doctor',
|
|
78
|
+
hint: null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const broken = npxCopies.filter((i) => i.state !== 'ok');
|
|
83
|
+
if (broken.length === 0) {
|
|
84
|
+
return {
|
|
85
|
+
id, name, status: 'ok',
|
|
86
|
+
message: `electron present in ${npxCopies.length} npx-cache ${plural(npxCopies.length, 'copy', 'copies')}`,
|
|
87
|
+
hint: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// `doctor --fix` can heal binary-missing (repairElectron); a never-installed
|
|
92
|
+
// package it cannot — keep the hint honest about which state is fixable.
|
|
93
|
+
const anyRepairable = broken.some((i) => i.state === 'binary-missing');
|
|
94
|
+
if (npxCopies.length === 1) {
|
|
95
|
+
const [only] = broken;
|
|
96
|
+
const message = only.state === 'binary-missing'
|
|
97
|
+
? `electron binary missing in the npx-cache copy the MCP launches: ${only.pkgDir} (electron dir: ${only.electronDir})`
|
|
98
|
+
: `electron not installed in the npx-cache copy the MCP launches: ${only.pkgDir} — GUI auto-open unavailable; headless still works`;
|
|
99
|
+
return { id, name, status: 'warn', message, hint: anyRepairable ? HINTS.doctorFix : null };
|
|
100
|
+
}
|
|
101
|
+
const detail = broken.map(describeBroken).join('; ');
|
|
102
|
+
return {
|
|
103
|
+
id, name, status: 'warn',
|
|
104
|
+
message: `electron unavailable in ${broken.length}/${npxCopies.length} npx-cache copies: ${detail}`,
|
|
105
|
+
hint: anyRepairable ? HINTS.doctorFix : null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fix-aware wrapper. When d.fix and the scan shows binary-missing npx copies,
|
|
111
|
+
* heal each in place via d.repairElectron({electronDir}) — the package dir is
|
|
112
|
+
* already on disk, so repairElectron can read its version and provision the
|
|
113
|
+
* exe — then re-report from a fresh scan. package-missing copies are never
|
|
114
|
+
* repaired (no package to repair into). Mirrors evaluateEngineMcp.
|
|
115
|
+
* @param {object} d doctor deps (scanElectronInstalls, fix?, repairElectron?, fixTimeoutMs?)
|
|
116
|
+
* @returns {Promise<{id,name,status,message,hint}>}
|
|
117
|
+
*/
|
|
118
|
+
async function evaluateElectronMcp(d) {
|
|
119
|
+
const verdict = evaluateElectronInstalls(d);
|
|
120
|
+
if (!d.fix || verdict.status === 'ok') { return verdict; }
|
|
121
|
+
|
|
122
|
+
const { installs } = d.scanElectronInstalls();
|
|
123
|
+
const repairable = installs.filter((i) => i.kind === 'npx' && i.state === 'binary-missing');
|
|
124
|
+
if (repairable.length === 0) { return verdict; }
|
|
125
|
+
|
|
126
|
+
const results = [];
|
|
127
|
+
for (const b of repairable) {
|
|
128
|
+
let r;
|
|
129
|
+
try {
|
|
130
|
+
r = await d.repairElectron({
|
|
131
|
+
electronDir: b.electronDir,
|
|
132
|
+
...(d.fixTimeoutMs ? { timeoutMs: d.fixTimeoutMs } : {}),
|
|
133
|
+
});
|
|
134
|
+
} catch (e) { r = { repaired: false, reason: e && e.message }; }
|
|
135
|
+
results.push({ electronDir: b.electronDir, ...r });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const after = evaluateElectronInstalls(d); // fresh scan reflects the repairs
|
|
139
|
+
if (after.status === 'ok') {
|
|
140
|
+
const n = results.length;
|
|
141
|
+
return { ...after, message: `${after.message} (self-healed ${n} npx-cache ${plural(n, 'copy', 'copies')})` };
|
|
142
|
+
}
|
|
143
|
+
const failed = results.filter((r) => !r.repaired)
|
|
144
|
+
.map((r) => `${r.electronDir}${r.reason ? ` — ${r.reason}` : ''}`).join('; ');
|
|
145
|
+
return failed
|
|
146
|
+
? { ...after, message: `${after.message}; self-heal incomplete: ${failed}` }
|
|
147
|
+
: after;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { scanElectronInstalls, evaluateElectronInstalls, evaluateElectronMcp };
|