amicus 1.6.1 → 1.7.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "1.6.1",
3
+ "version": "1.7.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": { "name": "Christian Wagner" },
6
6
  "homepage": "https://bourbondog.github.io/amicus/",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,45 @@ All notable changes to Amicus are documented here. Format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.7.1] - 2026-06-30
9
+
10
+ ### Fixed
11
+ - **The Electron GUI now shows the current rail-yard brand mark.** The window/taskbar icon, the setup
12
+ wizard's header and footer, and the session toolbar were still rendering the pre-redesign squiggle
13
+ mark; they now use the shipped clay→gold rail-yard mark (matching the site favicon). The inline
14
+ glyphs stay token-bound (clay tracks / gold mainline) so they follow the design system.
15
+
16
+ ## [1.7.0] - 2026-06-30
17
+
18
+ Electron self-heal, a real `amicus doctor`, and the GUI on the design system — plus MCP/diagnostics correctness.
19
+
20
+ ### Added
21
+ - **Electron self-heal.** Amicus now detects a broken or quarantined Electron install (a half-extracted
22
+ or AV-removed binary) and repairs it from the local download cache — **fully offline**. New
23
+ `amicus doctor --fix` heals in place, the GUI lazily provisions itself on first use, and an opt-in
24
+ `AMICUS_PREFETCH_ELECTRON=1` aggressively prewarms it. Install-time provisioning is cache-only (no
25
+ network during `npm install`) and never fails the install.
26
+ - **`amicus doctor` is now a recovery hub.** Checks carry copy-paste remediation hints, report
27
+ OpenRouter credit/free-tier status, and warn when the resolved project root looks like an app/install
28
+ directory rather than your repo.
29
+ - **Running version in MCP responses.** `amicus_status` / `amicus_guide` now report the running amicus
30
+ version and warn when the on-disk package is newer (restart your MCP client to load it).
31
+ - **The GUI is on the design system.** The embedded OpenCode session UI is themed to the clay/gold
32
+ tokens, the load-failsafe error page and window backgrounds are token-driven, and a drift guard keeps
33
+ new hardcoded colors/fonts out of `electron/`.
34
+
35
+ ### Fixed
36
+ - **Electron no longer reads "installed" when the binary is missing.** Runtime checks (including
37
+ `amicus doctor` and the GUI launch path) now stat the actual executable instead of trusting
38
+ `path.txt`, so a quarantined/half-extracted Electron is correctly detected — the root cause of the
39
+ silently-broken setup wizard.
40
+ - **`amicus_fanout` forwards Cowork session pinning** (`--cowork-process` / parent session) to its
41
+ spawned legs, so context-inheriting fan-outs pin the right parent.
42
+ - **`amicus_status` annotation corrected** — it is no longer declared read-only/idempotent, since its
43
+ wave branch updates metadata during crash detection.
44
+ - **Wave counts account for crashed / idle-timeout legs** (documented remainder rule), so consumers
45
+ summing the named buckets no longer mismatch the total.
46
+
8
47
  ## [1.6.1] - 2026-06-30
9
48
 
10
49
  Project-directory and session-addressing correctness — agents, sessions, and the interactive GUI now agree on which project they're in.
Binary file
@@ -1,5 +1,12 @@
1
- <svg width="1024" height="1024" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <rect width="16" height="16" rx="3" fill="#2D2B2A"/>
3
- <path d="M3 2v12" stroke="#D97757" stroke-width="2" stroke-linecap="round"/>
4
- <path d="M10 2v5c0 2-3 3-7 5" stroke="#D97757" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/>
1
+ <svg width="1024" height="1024" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="32" height="32" rx="7" fill="#2D2B2A"/>
3
+ <g stroke="#D97757" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="miter">
4
+ <path d="M4 8H19"/>
5
+ <path d="M4 11H14L19 8"/>
6
+ <path d="M4 14H13L19 8"/>
7
+ <path d="M4 17H12L19 8"/>
8
+ <path d="M4 20H11L19 8"/>
9
+ <path d="M4 23H10L19 8"/>
10
+ </g>
11
+ <path d="M19 8H28" stroke="#E8B24A" stroke-width="1.2" stroke-linecap="round"/>
5
12
  </svg>
@@ -14,6 +14,8 @@
14
14
 
15
15
  'use strict';
16
16
 
17
+ const { tokenCss } = require('../src/design/tokens');
18
+
17
19
  const DEFAULT_TIMEOUT_MS = 15000;
18
20
  const ERR_ABORTED = -3; // benign: fires on in-page redirects/navigation replacement
19
21
 
@@ -62,14 +64,19 @@ function escapeHTML(value) {
62
64
  * @returns {string} full HTML document
63
65
  */
64
66
  function buildLoadErrorHTML({ url, errorCode, errorDescription }) {
67
+ // This page renders after the OpenCode UI definitively failed to load, in a
68
+ // data: URL context. Inline the design tokens with absolute font URLs so the
69
+ // bundled webfonts resolve and var(--x) references work with no external CSS.
65
70
  return `<!DOCTYPE html>
66
71
  <html><head><style>
67
- body { background: #2D2B2A; color: #D4D0CC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
72
+ ${tokenCss({ absoluteFontUrls: true })}
73
+ body { background: var(--bg); color: var(--text-2);
74
+ font-family: var(--font-sans);
68
75
  display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
69
76
  .box { max-width: 440px; padding: 24px; }
70
- h1 { color: #D97757; font-size: 16px; margin: 0 0 12px; }
71
- p { font-size: 13px; line-height: 1.5; color: #A09B96; margin: 0 0 10px; }
72
- code { font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: 11px; color: #D4D0CC;
77
+ h1 { color: var(--accent); font-size: 16px; margin: 0 0 12px; }
78
+ p { font-size: 13px; line-height: 1.5; color: var(--text-2); margin: 0 0 10px; }
79
+ code { font-family: var(--font-mono); font-size: 11px; color: var(--text-1);
73
80
  word-break: break-all; }
74
81
  </style></head><body>
75
82
  <div class="box">
package/electron/main.js CHANGED
@@ -22,6 +22,8 @@ const { registerSetupHandlers } = require('./ipc-setup');
22
22
  const { computeWindowPosition } = require('./window-position');
23
23
  const { attachLoadFailsafe, buildLoadErrorHTML } = require('./load-failsafe');
24
24
  const { buildSessionRoute } = require('./session-route');
25
+ const { buildOpencodeThemeCSS } = require('./opencode-theme');
26
+ const { TOKENS } = require('../src/design/tokens');
25
27
 
26
28
  const ICON_PATH = path.join(__dirname, 'assets', 'icon.png');
27
29
 
@@ -98,7 +100,7 @@ function createAmicusWindow() {
98
100
  width: WIN_W, height: WIN_H, minWidth: 550, minHeight: 600,
99
101
  x: winX, y: winY,
100
102
  show: false,
101
- frame: true, backgroundColor: '#2D2B2A',
103
+ frame: true, backgroundColor: TOKENS.bg,
102
104
  title: 'Amicus',
103
105
  icon: ICON_PATH,
104
106
  webPreferences: {
@@ -148,13 +150,14 @@ function createAmicusWindow() {
148
150
  url: OPENCODE_URL, sessionId: OPENCODE_SESSION_ID, taskId: TASK_ID
149
151
  });
150
152
 
151
- // Use Electron's insertCSS API on dom-ready to hide OpenCode branding.
152
- // This is more reliable than preload DOM injection in BrowserView.
153
+ // Use Electron's insertCSS API on dom-ready to hide OpenCode branding AND
154
+ // theme the embedded chat surface to the clay/gold tokens (#49). The theme
155
+ // is token-driven — it inlines tokenCss() and remaps OpenCode's own :root
156
+ // custom properties — so it tracks the toolbar without brittle class
157
+ // selectors. insertCSS is more reliable than preload DOM injection in a
158
+ // BrowserView. NOTE: the live visual match is a user-side CDP/manual check.
153
159
  contentView.webContents.on('dom-ready', () => {
154
- contentView.webContents.insertCSS(`
155
- #root > div > header { display: none !important; }
156
- svg[viewBox="0 0 234 42"] { visibility: hidden !important; }
157
- `).catch(() => {});
160
+ contentView.webContents.insertCSS(buildOpencodeThemeCSS()).catch(() => {});
158
161
  });
159
162
 
160
163
  // Navigate directly to the session URL to bypass the project selection screen.
@@ -290,7 +293,7 @@ async function createSetupWindow() {
290
293
 
291
294
  mainWindow = new BrowserWindow({
292
295
  width: 560, height: 680, minWidth: 480, minHeight: 580,
293
- frame: true, backgroundColor: '#2D2B2A',
296
+ frame: true, backgroundColor: TOKENS.bg,
294
297
  title: `${getBrandName(CLIENT)} Setup`,
295
298
  icon: ICON_PATH,
296
299
  resizable: false,
@@ -443,7 +446,7 @@ function createSettingsChildWindow() {
443
446
  const settingsWin = new BrowserWindow({
444
447
  width: 560, height: 680,
445
448
  parent: mainWindow, modal: false,
446
- frame: true, backgroundColor: '#2D2B2A',
449
+ frame: true, backgroundColor: TOKENS.bg,
447
450
  title: `${getBrandName(CLIENT)} Settings`,
448
451
  icon: ICON_PATH,
449
452
  resizable: false,
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Issue #49 — token-driven theme for the embedded OpenCode web UI.
5
+ *
6
+ * main.js injects this via `contentView.webContents.insertCSS(...)` on
7
+ * `dom-ready`. Before #49 that hook only HID OpenCode's header/wordmark; this
8
+ * module extends it so the embedded chat surface inherits the clay/gold tokens
9
+ * and matches the token-driven Amicus toolbar (toolbar.js).
10
+ *
11
+ * Robustness note (the fragile bit): OpenCode (1.17.11 at time of writing) is a
12
+ * SolidStart app whose generated class names can change between releases. So we
13
+ * PREFER overriding OpenCode's OWN :root CSS custom properties — a much more
14
+ * stable surface than `.css-abc123` class selectors. We:
15
+ * 1. inline the canonical token CSS (tokenCss) so OUR vars + @font-face are
16
+ * available inside the BrowserView (absolute font URLs, same as toolbar.js
17
+ * / setup-ui-styles.js / load-failsafe.js do),
18
+ * 2. remap a generous superset of OpenCode's plausible theme custom-property
19
+ * names to our tokens (var(--...)), covering the prefixes OpenCode has
20
+ * shipped (--sst-*, --theme-*, --ock-*, --color-*), and
21
+ * 3. add a few low-specificity element fallbacks (body/font) for the case
22
+ * where a release renames its root vars entirely.
23
+ *
24
+ * Keep this side-effect-free and Electron-free so it stays unit-testable
25
+ * without booting Electron (main.js boots Electron on require).
26
+ *
27
+ * NOT verifiable headless: the live visual match against a running OpenCode
28
+ * session is a USER-SIDE CDP/manual check.
29
+ */
30
+
31
+ const { tokenCss } = require('../src/design/tokens');
32
+
33
+ /**
34
+ * The original hide-only rules (header + wordmark) preserved from the pre-#49
35
+ * dom-ready hook. rebrandUI() swaps the wordmark; these keep OpenCode's own
36
+ * chrome out of view until/while that runs.
37
+ */
38
+ const HIDE_CHROME = `
39
+ #root > div > header { display: none !important; }
40
+ svg[viewBox="0 0 234 42"] { visibility: hidden !important; }
41
+ `;
42
+
43
+ /**
44
+ * Map OpenCode's own theme custom properties onto our clay/gold tokens. We list
45
+ * the property names under every prefix OpenCode has used so a rename in one
46
+ * family still leaves the others driving the surface. Unknown vars are simply
47
+ * inert — setting an unused custom property has no effect — so over-listing is
48
+ * safe and is the robust play here.
49
+ */
50
+ const OPENCODE_ROOT_OVERRIDES = `
51
+ :root {
52
+ /* --- backgrounds / surfaces --- */
53
+ --sst-color-background: var(--bg);
54
+ --sst-color-background-panel: var(--surface-1);
55
+ --sst-color-background-element: var(--surface-2);
56
+ --theme-background: var(--bg);
57
+ --theme-background-panel: var(--surface-1);
58
+ --theme-background-element: var(--surface-2);
59
+ --ock-background: var(--bg);
60
+ --ock-background-panel: var(--surface-1);
61
+ --color-background: var(--bg);
62
+ --color-surface: var(--surface-1);
63
+ --color-surface-raised: var(--surface-2);
64
+
65
+ /* --- borders --- */
66
+ --sst-color-border: var(--border);
67
+ --theme-border: var(--border);
68
+ --ock-border: var(--border);
69
+ --color-border: var(--border);
70
+ --color-border-strong: var(--border-strong);
71
+
72
+ /* --- text --- */
73
+ --sst-color-text: var(--text-1);
74
+ --sst-color-text-secondary: var(--text-2);
75
+ --sst-color-text-muted: var(--text-3);
76
+ --theme-text: var(--text-1);
77
+ --theme-text-muted: var(--text-2);
78
+ --ock-text: var(--text-1);
79
+ --ock-text-muted: var(--text-2);
80
+ --color-text: var(--text-1);
81
+ --color-text-muted: var(--text-2);
82
+
83
+ /* --- brand accent (clay) --- */
84
+ --sst-color-primary: var(--accent-500);
85
+ --sst-color-accent: var(--accent-500);
86
+ --theme-primary: var(--accent-500);
87
+ --theme-accent: var(--accent-500);
88
+ --ock-primary: var(--accent-500);
89
+ --ock-accent: var(--accent-500);
90
+ --color-primary: var(--accent-500);
91
+ --color-accent: var(--accent-500);
92
+ --color-link: var(--accent-400);
93
+
94
+ /* --- counter-accent (gold) --- */
95
+ --sst-color-secondary: var(--gold-400);
96
+ --theme-secondary: var(--gold-400);
97
+ --ock-secondary: var(--gold-400);
98
+ --color-secondary: var(--gold-400);
99
+ }
100
+ `;
101
+
102
+ /**
103
+ * Low-specificity element fallbacks. These only bite if OpenCode renamed its
104
+ * root vars entirely (so the overrides above no-op); they intentionally avoid
105
+ * generated class names. Kept gentle so they don't fight OpenCode's layout.
106
+ */
107
+ const ELEMENT_FALLBACKS = `
108
+ html, body {
109
+ background-color: var(--bg);
110
+ color: var(--text-1);
111
+ font-family: var(--font-sans);
112
+ }
113
+ a { color: var(--accent-500); }
114
+ ::selection { background: var(--accent-soft); }
115
+ `;
116
+
117
+ /**
118
+ * Build the full theme CSS string injected into the OpenCode BrowserView.
119
+ * @returns {string} hide-chrome + inlined tokens + :root overrides + fallbacks.
120
+ */
121
+ function buildOpencodeThemeCSS() {
122
+ return [
123
+ HIDE_CHROME,
124
+ tokenCss({ absoluteFontUrls: true }),
125
+ OPENCODE_ROOT_OVERRIDES,
126
+ ELEMENT_FALLBACKS,
127
+ ].join('\n');
128
+ }
129
+
130
+ module.exports = { buildOpencodeThemeCSS };
@@ -34,7 +34,7 @@ function injectBrandingCss() {
34
34
  try {
35
35
  const style = document.createElement('style');
36
36
  style.textContent = [
37
- 'html, body { background-color: #2D2B2A !important; }',
37
+ 'html, body { background-color: var(--bg, #0a0a0a) !important; }',
38
38
  '#root > div > header { display: none !important; }',
39
39
  'svg[viewBox="0 0 234 42"] { display: none !important; }',
40
40
  ].join('\n');
@@ -18,7 +18,8 @@ function __rawWizardCSS() {
18
18
  padding: 10px 20px; border-bottom: 1px solid var(--border);
19
19
  }
20
20
  .header svg { flex-shrink: 0; }
21
- .header svg path { stroke: var(--accent); }
21
+ .header svg path { stroke: var(--accent); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: miter; }
22
+ .header svg path.brand-main { stroke: var(--gold-400); }
22
23
  .header-title {
23
24
  color: var(--accent); font-size: 12px; font-weight: 600;
24
25
  letter-spacing: 0.8px; text-transform: uppercase;
@@ -319,7 +320,8 @@ function __rawWizardCSS() {
319
320
  letter-spacing: 0.8px; text-transform: uppercase;
320
321
  display: flex; align-items: center; gap: 6px;
321
322
  }
322
- .footer-brand svg path { stroke: var(--accent); }
323
+ .footer-brand svg path { stroke: var(--accent); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: miter; }
324
+ .footer-brand svg path.brand-main { stroke: var(--gold-400); }
323
325
  .footer-nav { display: flex; gap: 6px; }
324
326
  .nav-btn {
325
327
  padding: 6px 16px; border: 1px solid var(--border);
@@ -33,7 +33,7 @@ function buildSetupHTML(options = {}) {
33
33
  return `<!DOCTYPE html>
34
34
  <html><head><meta charset="utf-8"><title>Amicus Setup</title>
35
35
  <style>${css}</style></head><body>
36
- <div class="header"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 2v12" stroke-width="2" stroke-linecap="round"/><path d="M10 2v5c0 2-3 3-7 5" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/></svg><span class="header-title">${brandName} Setup</span></div>
36
+ <div class="header"><svg width="18" height="18" viewBox="0 0 32 32" fill="none"><path d="M4 8H19"/><path d="M4 11H14L19 8"/><path d="M4 14H13L19 8"/><path d="M4 17H12L19 8"/><path d="M4 20H11L19 8"/><path d="M4 23H10L19 8"/><path class="brand-main" d="M19 8H28"/></svg><span class="header-title">${brandName} Setup</span></div>
37
37
  <div class="progress-bar"><div class="progress-step active" id="step-1"><span class="progress-dot">1</span><span>API Keys</span></div><div class="progress-connector"></div><div class="progress-step" id="step-2"><span class="progress-dot">2</span><span>Models</span></div><div class="progress-connector"></div><div class="progress-step" id="step-3"><span class="progress-dot">3</span><span>Routing</span></div><div class="progress-connector"></div><div class="progress-step" id="step-4"><span class="progress-dot">4</span><span>Review</span></div></div>
38
38
  <div class="content">
39
39
  <div class="wizard-step visible" id="wizard-step-1"><div id="import-notice"></div>${keysHtml}</div>
@@ -50,7 +50,7 @@ function buildSetupHTML(options = {}) {
50
50
  </div>
51
51
  </div>
52
52
  </div>
53
- <div class="footer"><div class="footer-brand"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M3 2v12" stroke-width="2" stroke-linecap="round"/><path d="M10 2v5c0 2-3 3-7 5" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/></svg> ${brandName}</div><div class="footer-nav"><button class="nav-btn" id="back-btn" style="display:none">Back</button><button class="nav-btn primary" id="next-btn" disabled>Next</button><button class="nav-btn primary" id="finish-btn" style="display:none">Finish</button></div></div>
53
+ <div class="footer"><div class="footer-brand"><svg width="15" height="15" viewBox="0 0 32 32" fill="none"><path d="M4 8H19"/><path d="M4 11H14L19 8"/><path d="M4 14H13L19 8"/><path d="M4 17H12L19 8"/><path d="M4 20H11L19 8"/><path d="M4 23H10L19 8"/><path class="brand-main" d="M19 8H28"/></svg> ${brandName}</div><div class="footer-nav"><button class="nav-btn" id="back-btn" style="display:none">Back</button><button class="nav-btn primary" id="next-btn" disabled>Next</button><button class="nav-btn primary" id="finish-btn" style="display:none">Finish</button></div></div>
54
54
  ${buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson)}
55
55
  </body></html>`;
56
56
  }
@@ -70,7 +70,8 @@ function buildToolbarHTML(options = {}) {
70
70
  letter-spacing: 0.8px;
71
71
  text-transform: uppercase;
72
72
  }
73
- .logo path { stroke: var(--accent); }
73
+ .logo path { stroke: var(--accent); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: miter; }
74
+ .logo path.brand-main { stroke: var(--gold-400); }
74
75
  .sep { color: var(--border-strong); font-size: 14px; }
75
76
  .detail, .timer {
76
77
  color: var(--text-3);
@@ -136,9 +137,10 @@ function buildToolbarHTML(options = {}) {
136
137
  }
137
138
  .update-banner .dismiss-btn:hover { color: var(--text-1); }`;
138
139
 
139
- const logoSvg = `<svg class="logo" width="16" height="16" viewBox="0 0 16 16" fill="none">
140
- <path d="M3 2v12" stroke-width="2" stroke-linecap="round"/>
141
- <path d="M10 2v5c0 2-3 3-7 5" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/>
140
+ const logoSvg = `<svg class="logo" width="16" height="16" viewBox="0 0 32 32" fill="none">
141
+ <path d="M4 8H19"/><path d="M4 11H14L19 8"/><path d="M4 14H13L19 8"/>
142
+ <path d="M4 17H12L19 8"/><path d="M4 20H11L19 8"/><path d="M4 23H10L19 8"/>
143
+ <path class="brand-main" d="M19 8H28"/>
142
144
  </svg>`;
143
145
 
144
146
  if (mode === 'setup') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "1.6.1",
3
+ "version": "1.7.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
  "keywords": [
6
6
  "claude",
@@ -13,8 +13,14 @@ const path = require('path');
13
13
  const os = require('os');
14
14
  const { execFileSync } = require('child_process');
15
15
 
16
+ const { repairElectron } = require('../src/sidecar/electron-install');
17
+ const HINTS = require('../src/utils/remediation-hints');
18
+
16
19
  const SETUP_HOOKS_SCRIPT = path.join(__dirname, 'setup-hooks.js');
17
20
 
21
+ /** Short cache-only provision budget: a slow disk must never hang the install. */
22
+ const PROVISION_TIMEOUT_MS = 15000;
23
+
18
24
  const SKILL_SOURCE = path.join(__dirname, '..', 'skills', 'sidecar', 'SKILL.md');
19
25
  const COUNCIL_SOURCE_DIR = path.join(__dirname, '..', 'skills', 'second-opinion');
20
26
 
@@ -175,40 +181,56 @@ function registerClaudeDesktop() {
175
181
  }
176
182
 
177
183
  /**
178
- * Resolve the Electron binary path the same way src/sidecar/interactive.js
179
- * getElectronPath() does: require('electron') returns the absolute path to the
180
- * binary (or throws if the optionalDependency never installed/extracted).
181
- * @returns {string|null} Path to the Electron binary, or null if unresolvable.
182
- */
183
- function resolveElectron() {
184
- try {
185
- return require('electron');
186
- } catch {
187
- return null;
188
- }
189
- }
190
-
191
- /**
192
- * NON-FATAL verification that the OPTIONAL electron binary actually extracted.
193
- * Electron is an optionalDependency: npm exits 0 even if its download/extract
194
- * fails (or AV quarantines electron.exe), so without this the user only finds
195
- * out the GUI is broken much later. Warn clearly that headless runs + the
196
- * council still work, and point at `amicus doctor` / reinstall to get the GUI.
184
+ * NON-FATAL, CACHE-ONLY provisioning of the OPTIONAL electron binary (#57,
185
+ * supersedes #30's warn-only verifyElectron). Electron is an optionalDependency:
186
+ * npm exits 0 even if its download/extract failed (or AV quarantined
187
+ * electron.exe), so without this the user only finds out the GUI is broken much
188
+ * later.
189
+ *
190
+ * We heal from the LOCAL cache via repairElectron({cacheOnly:true}) — NO network
191
+ * during install. When a cached zip extracts cleanly the GUI just works. When
192
+ * there is no cache (or the repair defers/contends), we emit a clear notice that
193
+ * the GUI provisions on first use and that headless runs + the council already
194
+ * work, then point at `amicus doctor --fix` (#56) — NOT a reinstall, which can
195
+ * loop. A short timeout keeps a slow disk from ever hanging the install.
196
+ *
197
+ * This MUST never throw out of postinstall — the whole body (sync setup, the
198
+ * awaited repair, and a synchronous-throw resolver) is guarded so nothing here
199
+ * can turn into a non-zero exit (#29 always-exit-0 guard preserved).
197
200
  *
198
- * This MUST never throw out of postinstall — the whole body is guarded so a
199
- * resolver failure or a missing fs can never turn into a non-zero exit.
201
+ * OPT-IN PREWARM (#60): when AMICUS_PREFETCH_ELECTRON=1 the user has asked to
202
+ * aggressively prewarm the GUI at install time. We additionally drive a full
203
+ * (possibly-networked) fetch via repairElectron({force:true}). This is the ONLY
204
+ * path that may hit the network during install; it stays opt-in and non-fatal
205
+ * (a throw here is swallowed and exit 0 is preserved). The DEFAULT remains
206
+ * cache-only (#57).
200
207
  *
201
- * @param {object} deps - { resolveElectron } override for testing.
208
+ * @param {object} deps - { repairElectron } override for testing.
209
+ * @returns {Promise<void>}
202
210
  */
203
- function verifyElectron(deps = {}) {
211
+ async function provisionElectron(deps = {}) {
204
212
  try {
205
- const _resolve = deps.resolveElectron || resolveElectron;
206
- const binPath = _resolve();
207
- if (binPath && fs.existsSync(binPath)) { return; }
208
- console.warn('[amicus] Warning: the Electron binary did not install — the interactive GUI / setup-wizard is unavailable.');
209
- console.warn('[amicus] Headless runs and the council still work. Run `amicus doctor` to check, or `npm install -g amicus` to reinstall and add the GUI.');
213
+ const _repair = deps.repairElectron || repairElectron;
214
+
215
+ // Opt-in aggressive prewarm (#60): full fetch if needed. Non-fatal.
216
+ if (process.env.AMICUS_PREFETCH_ELECTRON === '1') {
217
+ console.log('[amicus] AMICUS_PREFETCH_ELECTRON=1 prewarming the Electron GUI binary (may download)...');
218
+ const forced = await _repair({ force: true });
219
+ if (forced && (forced.repaired || forced.usable)) {
220
+ console.log('[amicus] Electron GUI binary prewarmed.');
221
+ return;
222
+ }
223
+ console.warn('[amicus] Note: Electron prewarm did not complete now — the GUI provisions on first use.');
224
+ return;
225
+ }
226
+
227
+ const result = await _repair({ cacheOnly: true, timeoutMs: PROVISION_TIMEOUT_MS });
228
+ if (result && (result.repaired || result.usable)) { return; }
229
+ // No cache hit (deferred), contended, or otherwise not provisioned now.
230
+ console.warn('[amicus] Note: the Electron GUI binary is not provisioned yet — it will download on first use of the interactive GUI / setup-wizard.');
231
+ console.warn(`[amicus] Headless runs and the council already work. To provision the GUI now: ${HINTS.doctorFix}`);
210
232
  } catch {
211
- // Never let the electron check throw out of postinstall.
233
+ // Never let provisioning throw out of postinstall.
212
234
  }
213
235
  }
214
236
 
@@ -247,7 +269,7 @@ function setupHooks(deps = {}) {
247
269
  }
248
270
  }
249
271
 
250
- function main(deps = {}) {
272
+ async function main(deps = {}) {
251
273
  if (process.env.AMICUS_SKIP_POSTINSTALL === '1') {
252
274
  console.log('[amicus] AMICUS_SKIP_POSTINSTALL set — skipping global setup (plugin channel handles registration).');
253
275
  return;
@@ -257,6 +279,7 @@ function main(deps = {}) {
257
279
  const _registerClaudeCode = deps.registerClaudeCode || registerClaudeCode;
258
280
  const _registerClaudeDesktop = deps.registerClaudeDesktop || registerClaudeDesktop;
259
281
  const _setupHooks = deps.setupHooks || setupHooks;
282
+ const _provisionElectron = deps.provisionElectron || provisionElectron;
260
283
 
261
284
  console.log('[amicus] Installing...');
262
285
  // Dev-only: configure git hooks (no-op for consumers). Folded in from the
@@ -267,8 +290,9 @@ function main(deps = {}) {
267
290
  _registerClaudeCode();
268
291
  _registerClaudeDesktop();
269
292
 
270
- // Non-fatal: warn (only) if the optional Electron binary failed to extract.
271
- verifyElectron(deps);
293
+ // Non-fatal, cache-only: heal the optional Electron binary from local cache
294
+ // or emit a deferred notice (GUI provisions on first use). Never throws.
295
+ await _provisionElectron(deps);
272
296
 
273
297
  console.log('');
274
298
  console.log('[amicus] Setup:');
@@ -282,9 +306,9 @@ function main(deps = {}) {
282
306
  * package, but skill-copy + MCP registration are optional — amicus itself still
283
307
  * works without them. Warn clearly and exit 0 (mirrors scripts/setup-hooks.js).
284
308
  */
285
- function runCli(deps = {}) {
309
+ async function runCli(deps = {}) {
286
310
  try {
287
- main(deps);
311
+ await main(deps);
288
312
  } catch (err) {
289
313
  console.warn(`[amicus] Warning: optional post-install setup failed: ${err && err.message}`);
290
314
  console.warn('[amicus] Skill install + MCP registration are optional — amicus itself still works.');
@@ -299,4 +323,4 @@ if (require.main === module) {
299
323
  runCli();
300
324
  }
301
325
 
302
- module.exports = { main, runCli, addMcpToConfigFile, installSkill, installCouncilSkill, setupHooks, verifyElectron, resolveElectron, COUNCIL_FILES };
326
+ module.exports = { main, runCli, addMcpToConfigFile, installSkill, installCouncilSkill, setupHooks, provisionElectron, COUNCIL_FILES };