amicus 1.0.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +477 -0
  4. package/bin/amicus.js +382 -0
  5. package/electron/assets/icon.png +0 -0
  6. package/electron/assets/icon.svg +5 -0
  7. package/electron/fold.js +163 -0
  8. package/electron/ipc-setup.js +176 -0
  9. package/electron/load-failsafe.js +85 -0
  10. package/electron/main.js +468 -0
  11. package/electron/preload-setup.js +38 -0
  12. package/electron/preload.js +33 -0
  13. package/electron/setup-ui-alias-script.js +218 -0
  14. package/electron/setup-ui-aliases.js +85 -0
  15. package/electron/setup-ui-keys-script.js +115 -0
  16. package/electron/setup-ui-keys.js +97 -0
  17. package/electron/setup-ui-model.js +138 -0
  18. package/electron/setup-ui-styles.js +327 -0
  19. package/electron/setup-ui.js +465 -0
  20. package/electron/summary.js +118 -0
  21. package/electron/toolbar.js +229 -0
  22. package/electron/window-position.js +35 -0
  23. package/package.json +98 -0
  24. package/scripts/postinstall.js +193 -0
  25. package/scripts/setup-hooks.js +42 -0
  26. package/skill/SKILL.md +976 -0
  27. package/skills/second-opinion/COUNCIL-DESIGN.md +227 -0
  28. package/skills/second-opinion/MODEL-NOTES.md +104 -0
  29. package/skills/second-opinion/SKILL.md +389 -0
  30. package/src/cli-handlers.js +188 -0
  31. package/src/cli.js +400 -0
  32. package/src/conflict.js +144 -0
  33. package/src/context-compression.js +102 -0
  34. package/src/context.js +199 -0
  35. package/src/drift.js +144 -0
  36. package/src/environment.js +157 -0
  37. package/src/headless.js +742 -0
  38. package/src/index.js +106 -0
  39. package/src/jsonl-parser.js +180 -0
  40. package/src/mcp-server.js +625 -0
  41. package/src/mcp-tools.js +407 -0
  42. package/src/opencode-client.js +615 -0
  43. package/src/prompt-builder.js +355 -0
  44. package/src/prompts/cowork-agent-prompt.js +118 -0
  45. package/src/session-manager.js +414 -0
  46. package/src/session.js +180 -0
  47. package/src/sidecar/context-builder.js +297 -0
  48. package/src/sidecar/continue.js +212 -0
  49. package/src/sidecar/crash-handler.js +56 -0
  50. package/src/sidecar/fanout-leg.js +107 -0
  51. package/src/sidecar/fanout-output.js +46 -0
  52. package/src/sidecar/fanout.js +236 -0
  53. package/src/sidecar/interactive.js +217 -0
  54. package/src/sidecar/models.js +135 -0
  55. package/src/sidecar/progress.js +218 -0
  56. package/src/sidecar/read.js +183 -0
  57. package/src/sidecar/resume.js +221 -0
  58. package/src/sidecar/session-utils.js +288 -0
  59. package/src/sidecar/setup-window.js +79 -0
  60. package/src/sidecar/setup.js +280 -0
  61. package/src/sidecar/start.js +251 -0
  62. package/src/utils/agent-mapping.js +138 -0
  63. package/src/utils/alias-audit.js +98 -0
  64. package/src/utils/alias-resolver.js +77 -0
  65. package/src/utils/api-key-store.js +259 -0
  66. package/src/utils/api-key-validation.js +97 -0
  67. package/src/utils/auth-json.js +109 -0
  68. package/src/utils/config.js +291 -0
  69. package/src/utils/curated-models.js +82 -0
  70. package/src/utils/env-compat.js +38 -0
  71. package/src/utils/env-loader.js +54 -0
  72. package/src/utils/idle-watchdog.js +225 -0
  73. package/src/utils/input-validators.js +127 -0
  74. package/src/utils/lifecycle.js +43 -0
  75. package/src/utils/logger.js +84 -0
  76. package/src/utils/mcp-discovery.js +194 -0
  77. package/src/utils/mcp-validators.js +78 -0
  78. package/src/utils/model-catalog.js +103 -0
  79. package/src/utils/model-fetcher.js +179 -0
  80. package/src/utils/model-validator.js +207 -0
  81. package/src/utils/path-setup.js +41 -0
  82. package/src/utils/port-pid.js +39 -0
  83. package/src/utils/prompt-source.js +53 -0
  84. package/src/utils/result-schema.js +261 -0
  85. package/src/utils/server-setup.js +93 -0
  86. package/src/utils/session-abort.js +53 -0
  87. package/src/utils/session-lock.js +95 -0
  88. package/src/utils/shared-server.js +216 -0
  89. package/src/utils/start-helpers.js +76 -0
  90. package/src/utils/thinking-validators.js +92 -0
  91. package/src/utils/update-notifier-loader.js +18 -0
  92. package/src/utils/updater.js +157 -0
  93. package/src/utils/validators.js +300 -0
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module shared-server
5
+ * Manages a single shared OpenCode server for MCP sessions.
6
+ */
7
+
8
+ const { IdleWatchdog } = require('./idle-watchdog');
9
+ const { getCompatEnv } = require('./env-compat');
10
+
11
+ const MAX_RESTARTS = 3;
12
+ const RESTART_WINDOW = 5 * 60 * 1000;
13
+ const RESTART_BACKOFF = 2000;
14
+
15
+ /**
16
+ * SharedServerManager - manages a single shared OpenCode server for MCP sessions.
17
+ *
18
+ * Tracks active sessions, handles lazy server startup, deduplicates concurrent
19
+ * start requests, and supervises the server with automatic restart on crash.
20
+ */
21
+ class SharedServerManager {
22
+ /**
23
+ * @param {object} [options]
24
+ * @param {object} [options.logger] - Logger with .info(), .warn(), .error() methods
25
+ */
26
+ constructor(options = {}) {
27
+ this.logger = options.logger || console;
28
+ this.maxSessions = Number(process.env.SIDECAR_MAX_SESSIONS) || 20;
29
+ this.enabled = getCompatEnv('SHARED_SERVER') !== '0';
30
+
31
+ /** @type {object|null} Active server handle */
32
+ this.server = null;
33
+
34
+ /** @type {object|null} Active OpenCode client */
35
+ this.client = null;
36
+
37
+ /** @type {Map<string, IdleWatchdog>} Per-session watchdogs */
38
+ this._sessionWatchdogs = new Map();
39
+
40
+ /** @type {IdleWatchdog|null} Server-level idle watchdog (fires when no sessions remain) */
41
+ this._serverWatchdog = null;
42
+
43
+ /** @type {Promise|null} In-flight server start promise (for deduplication) */
44
+ this._starting = null;
45
+
46
+ /** @type {number[]} Timestamps of recent restart attempts */
47
+ this._restartTimestamps = [];
48
+ }
49
+
50
+ /**
51
+ * Number of active sessions.
52
+ * @returns {number}
53
+ */
54
+ get sessionCount() {
55
+ return this._sessionWatchdogs.size;
56
+ }
57
+
58
+ /**
59
+ * Ensure the shared server is running. Lazy-starts on first call.
60
+ * Deduplicates concurrent calls so only one start occurs.
61
+ *
62
+ * @param {object} [mcpConfig] - MCP configuration to pass to startOpenCodeServer
63
+ * @returns {Promise<{server: object, client: object}>}
64
+ */
65
+ async ensureServer(mcpConfig) {
66
+ if (this.server && this.client) {
67
+ return { server: this.server, client: this.client };
68
+ }
69
+ if (this._starting) {
70
+ return this._starting;
71
+ }
72
+ this._starting = this._doStartServer(mcpConfig).then(({ server, client }) => {
73
+ this.server = server;
74
+ this.client = client;
75
+ this._starting = null;
76
+ this._serverWatchdog = new IdleWatchdog({
77
+ mode: 'server',
78
+ onTimeout: () => {
79
+ this.logger.info?.('Shared server idle (no sessions) - shutting down');
80
+ this.shutdown();
81
+ },
82
+ }).start();
83
+ return { server, client };
84
+ }).catch((err) => {
85
+ this._starting = null;
86
+ throw err;
87
+ });
88
+ return this._starting;
89
+ }
90
+
91
+ /**
92
+ * Start the OpenCode server. Overrideable for testing.
93
+ *
94
+ * @param {object} [mcpConfig]
95
+ * @returns {Promise<{server: object, client: object}>}
96
+ */
97
+ async _doStartServer(mcpConfig) {
98
+ const { startOpenCodeServer } = require('../sidecar/session-utils');
99
+ return startOpenCodeServer(mcpConfig);
100
+ }
101
+
102
+ /**
103
+ * Register a new session. Cancels the server idle watchdog while sessions are active.
104
+ *
105
+ * @param {string} sessionId - Unique session identifier
106
+ * @param {Function} [onEvict] - Called when session is evicted due to idle timeout
107
+ * @throws {Error} If max session capacity is reached
108
+ */
109
+ addSession(sessionId, onEvict) {
110
+ if (this._sessionWatchdogs.size >= this.maxSessions) {
111
+ throw new Error(`Max sessions reached (${this.maxSessions}). Cannot add session ${sessionId}.`);
112
+ }
113
+ const watchdog = new IdleWatchdog({
114
+ mode: 'headless',
115
+ onTimeout: () => {
116
+ this.logger.info?.('Session idle timeout', { sessionId });
117
+ if (onEvict) { onEvict(sessionId); }
118
+ this.removeSession(sessionId);
119
+ },
120
+ }).start();
121
+ this._sessionWatchdogs.set(sessionId, watchdog);
122
+ if (this._serverWatchdog) {
123
+ this._serverWatchdog.cancel();
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Deregister a session. Starts the server idle watchdog when no sessions remain.
129
+ *
130
+ * @param {string} sessionId
131
+ */
132
+ removeSession(sessionId) {
133
+ const watchdog = this._sessionWatchdogs.get(sessionId);
134
+ if (watchdog) {
135
+ watchdog.cancel();
136
+ this._sessionWatchdogs.delete(sessionId);
137
+ }
138
+ if (this._sessionWatchdogs.size === 0 && this._serverWatchdog) {
139
+ this._serverWatchdog.start();
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Get the idle watchdog for a session.
145
+ *
146
+ * @param {string} sessionId
147
+ * @returns {IdleWatchdog|undefined}
148
+ */
149
+ getSessionWatchdog(sessionId) {
150
+ return this._sessionWatchdogs.get(sessionId);
151
+ }
152
+
153
+ /**
154
+ * Shut down the manager: cancel all watchdogs, close the server.
155
+ */
156
+ shutdown() {
157
+ for (const [, wd] of this._sessionWatchdogs) {
158
+ wd.cancel();
159
+ }
160
+ this._sessionWatchdogs.clear();
161
+ if (this._serverWatchdog) {
162
+ this._serverWatchdog.cancel();
163
+ this._serverWatchdog = null;
164
+ }
165
+ if (this.server) {
166
+ this.server.close();
167
+ this.server = null;
168
+ this.client = null;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Handle a server crash: log, notify sessions, then schedule restart.
174
+ *
175
+ * @param {number} exitCode - Process exit code from the crashed server
176
+ */
177
+ _onServerCrash(exitCode) {
178
+ this.logger.error?.('Shared server crashed', { exitCode });
179
+ for (const [id] of this._sessionWatchdogs) {
180
+ this.logger.warn?.('Session interrupted by server crash', { sessionId: id });
181
+ }
182
+ this.server = null;
183
+ this.client = null;
184
+ setTimeout(() => this._handleRestart(), RESTART_BACKOFF);
185
+ }
186
+
187
+ /**
188
+ * Attempt to restart the server, subject to rate limiting.
189
+ *
190
+ * @returns {Promise<boolean>} true if restart succeeded, false if rate-limited or failed
191
+ */
192
+ async _handleRestart() {
193
+ const now = Date.now();
194
+ this._restartTimestamps = this._restartTimestamps.filter(
195
+ (ts) => now - ts < RESTART_WINDOW
196
+ );
197
+ if (this._restartTimestamps.length >= MAX_RESTARTS) {
198
+ this.logger.error?.('Max restarts exceeded, not restarting shared server', {
199
+ restarts: this._restartTimestamps.length,
200
+ windowMs: RESTART_WINDOW,
201
+ });
202
+ return false;
203
+ }
204
+ this._restartTimestamps.push(now);
205
+ try {
206
+ await this.ensureServer();
207
+ this.logger.info?.('Shared server restarted successfully');
208
+ return true;
209
+ } catch (err) {
210
+ this.logger.error?.('Failed to restart shared server', { error: err.message });
211
+ return false;
212
+ }
213
+ }
214
+ }
215
+
216
+ module.exports = { SharedServerManager };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Start Command Helpers
3
+ *
4
+ * Model resolution and validation helpers extracted from bin/amicus.js
5
+ * to keep the CLI entry point under the 300-line limit.
6
+ */
7
+
8
+ /**
9
+ * Resolve model from args: resolve alias or config default.
10
+ * Returns { model, alias } or calls process.exit(1) on error.
11
+ * @param {object} args - Parsed CLI arguments
12
+ * @returns {{ model: string, alias: string|undefined }}
13
+ */
14
+ function resolveModelFromArgs(args) {
15
+ const { resolveModel, loadConfig } = require('./config');
16
+ const rawAlias = args.model;
17
+ let model;
18
+ try {
19
+ model = resolveModel(args.model);
20
+ } catch (err) {
21
+ console.error(err.message);
22
+ process.exit(1);
23
+ }
24
+
25
+ // Determine the alias used (explicit or config default)
26
+ let alias = rawAlias;
27
+ if (alias === undefined) {
28
+ const cfg = loadConfig();
29
+ if (cfg && cfg.default && !cfg.default.includes('/')) {
30
+ alias = cfg.default;
31
+ }
32
+ }
33
+ return { model, alias };
34
+ }
35
+
36
+ /**
37
+ * Validate models before launch (F3 #18: default-on).
38
+ * --no-validate-model opts out; the old opt-in --validate-model is a no-op kept for back-compat.
39
+ * Returns the (possibly corrected) model string.
40
+ * @param {object} args - Parsed CLI arguments
41
+ * @param {string|undefined} alias - The alias used for resolution
42
+ * @returns {Promise<string>} Validated model string
43
+ */
44
+ async function validateFallbackModel(args, alias) {
45
+ // F3 #18: validation is default-on. --no-validate-model opts out; the old
46
+ // opt-in --validate-model is now a no-op kept for back-compat.
47
+ if (args['no-validate-model']) { return args.model; }
48
+
49
+ const headless = args['no-ui'] || !process.stdin.isTTY;
50
+ const { detectFallback } = require('./config');
51
+
52
+ // Direct-API fallback path: keep the provider-API existence check.
53
+ if (alias && detectFallback(alias, args.model)) {
54
+ const { validateDirectModel } = require('./model-validator');
55
+ try {
56
+ return await validateDirectModel(args.model, alias, { headless });
57
+ } catch (err) {
58
+ console.error(err.message);
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ // OpenRouter (and any) resolved model: validate against the live catalog.
64
+ const { validateAgainstCatalog } = require('./model-validator');
65
+ try {
66
+ return await validateAgainstCatalog(args.model, alias);
67
+ } catch (err) {
68
+ console.error(err.message);
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ module.exports = {
74
+ resolveModelFromArgs,
75
+ validateFallbackModel,
76
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Thinking Level Validators
3
+ *
4
+ * Model-specific thinking level validation.
5
+ * Extracted from validators.js to keep modules under 300 lines.
6
+ */
7
+
8
+ /**
9
+ * Model-specific thinking level support (static fallback)
10
+ * Maps model patterns to their supported thinking levels.
11
+ *
12
+ * NOTE: For dynamic, up-to-date capabilities, use model-capabilities.js
13
+ * which fetches from OpenRouter API and caches the results.
14
+ * This static map is used as a fast fallback for CLI validation.
15
+ */
16
+ const MODEL_THINKING_SUPPORT = {
17
+ // OpenAI GPT-5.x does NOT support 'minimal'
18
+ 'gpt-5': ['none', 'low', 'medium', 'high', 'xhigh'],
19
+ // o3/o3-mini supports all levels
20
+ 'o3': ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'],
21
+ // Gemini supports all levels
22
+ 'gemini': ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'],
23
+ // Default: all levels supported
24
+ 'default': ['none', 'minimal', 'low', 'medium', 'high', 'xhigh']
25
+ };
26
+
27
+ /**
28
+ * Get supported thinking levels for a model (synchronous, static fallback)
29
+ *
30
+ * For dynamic lookup from OpenRouter API cache, use:
31
+ * const { getSupportedThinkingLevels } = require('./model-capabilities');
32
+ *
33
+ * @param {string} model - Model identifier
34
+ * @returns {string[]} Array of supported thinking levels
35
+ */
36
+ function getSupportedThinkingLevels(model) {
37
+ if (!model) {return MODEL_THINKING_SUPPORT.default;}
38
+
39
+ const modelLower = model.toLowerCase();
40
+
41
+ // Check each known model pattern
42
+ for (const [pattern, levels] of Object.entries(MODEL_THINKING_SUPPORT)) {
43
+ if (pattern !== 'default' && modelLower.includes(pattern)) {
44
+ return levels;
45
+ }
46
+ }
47
+
48
+ return MODEL_THINKING_SUPPORT.default;
49
+ }
50
+
51
+ /**
52
+ * Validate thinking level for a specific model (synchronous)
53
+ *
54
+ * For async validation with dynamic API cache, use:
55
+ * const { validateThinkingForModel } = require('./model-capabilities');
56
+ *
57
+ * @param {string} thinking - Thinking level ('minimal', 'low', 'medium', 'high', 'xhigh', 'none')
58
+ * @param {string} model - Model identifier
59
+ * @returns {{valid: boolean, error?: string, warning?: string, adjustedLevel?: string}}
60
+ */
61
+ function validateThinkingLevel(thinking, model) {
62
+ if (!thinking) {
63
+ return { valid: true };
64
+ }
65
+
66
+ const allLevels = MODEL_THINKING_SUPPORT.default;
67
+ if (!allLevels.includes(thinking)) {
68
+ return {
69
+ valid: false,
70
+ error: `Error: --thinking must be one of: ${allLevels.join(', ')}`
71
+ };
72
+ }
73
+
74
+ const supportedLevels = getSupportedThinkingLevels(model);
75
+ if (!supportedLevels.includes(thinking)) {
76
+ // Map to nearest supported level
77
+ const fallback = thinking === 'minimal' ? 'low' : 'medium';
78
+ return {
79
+ valid: true,
80
+ warning: `Warning: Model '${model}' does not support thinking level '${thinking}'. Using '${fallback}' instead.`,
81
+ adjustedLevel: fallback
82
+ };
83
+ }
84
+
85
+ return { valid: true };
86
+ }
87
+
88
+ module.exports = {
89
+ MODEL_THINKING_SUPPORT,
90
+ getSupportedThinkingLevels,
91
+ validateThinkingLevel
92
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * update-notifier Loader
3
+ *
4
+ * update-notifier v6+ is ESM-only: require() throws ERR_REQUIRE_ESM, so the
5
+ * package must be loaded with a dynamic import(). The import lives in this
6
+ * CJS seam so unit tests can mock it — jest's CJS test VM cannot execute a
7
+ * native import() without --experimental-vm-modules.
8
+ */
9
+
10
+ /**
11
+ * Dynamically import the update-notifier module.
12
+ * @returns {Promise<object>} Module namespace; the constructor is `.default`
13
+ */
14
+ function loadUpdateNotifier() {
15
+ return import('update-notifier');
16
+ }
17
+
18
+ module.exports = { loadUpdateNotifier };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Updater Module
3
+ *
4
+ * Self-update functionality for amicus.
5
+ * Checks for new versions via update-notifier and performs
6
+ * updates via npm install -g.
7
+ *
8
+ * Supports AMICUS_MOCK_UPDATE (or legacy SIDECAR_MOCK_UPDATE) env var for testing:
9
+ * "available" — getUpdateInfo returns fake update
10
+ * "updating" — getUpdateInfo returns fake update
11
+ * "success" — performUpdate resolves immediately
12
+ * "error" — performUpdate returns failure
13
+ */
14
+
15
+ const { spawn } = require('child_process');
16
+ const path = require('path');
17
+ const { logger } = require('./logger');
18
+ const { getCompatEnv } = require('./env-compat');
19
+ const { loadUpdateNotifier } = require('./update-notifier-loader');
20
+
21
+ const pkg = require(path.join(__dirname, '..', '..', 'package.json'));
22
+
23
+ const MOCK_MODES = ['available', 'updating', 'success', 'error'];
24
+ const FAKE_LATEST = '99.0.0';
25
+
26
+ /** @type {import('update-notifier').UpdateNotifier|null} */
27
+ let notifier = null;
28
+
29
+ /**
30
+ * Check if mock mode is active
31
+ * @returns {string|null} The mock mode or null
32
+ */
33
+ function getMockMode() {
34
+ const mode = getCompatEnv('MOCK_UPDATE');
35
+ if (mode && MOCK_MODES.includes(mode)) {
36
+ return mode;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Initialize the update checker with package info.
43
+ * Async because update-notifier is ESM-only and must be loaded with a
44
+ * dynamic import(). In mock mode, skips the real update-notifier call.
45
+ * @returns {Promise<void>}
46
+ */
47
+ async function initUpdateCheck() {
48
+ const mock = getMockMode();
49
+ if (mock) {
50
+ logger.debug('Update check skipped (mock mode)', { mock });
51
+ return;
52
+ }
53
+
54
+ try {
55
+ const mod = await loadUpdateNotifier();
56
+ const updateNotifier = mod.default || mod;
57
+ notifier = updateNotifier({
58
+ pkg: { name: pkg.name, version: pkg.version }
59
+ });
60
+ } catch (err) {
61
+ logger.warn('Failed to initialize update checker', { error: err.message });
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Get update information from the cached check.
67
+ * @returns {{ current: string, latest: string, hasUpdate: boolean }|null}
68
+ */
69
+ function getUpdateInfo() {
70
+ const mock = getMockMode();
71
+ if (mock) {
72
+ return {
73
+ current: pkg.version,
74
+ latest: FAKE_LATEST,
75
+ hasUpdate: true
76
+ };
77
+ }
78
+
79
+ if (!notifier || !notifier.update) {
80
+ return null;
81
+ }
82
+
83
+ return {
84
+ current: notifier.update.current,
85
+ latest: notifier.update.latest,
86
+ hasUpdate: true
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Display update notification via update-notifier's built-in notify.
92
+ * Mentions `amicus update` as the upgrade command.
93
+ */
94
+ function notifyUpdate() {
95
+ if (!notifier) {
96
+ return;
97
+ }
98
+
99
+ notifier.notify({
100
+ message: 'Update available: {currentVersion} -> {latestVersion}\n' +
101
+ 'Run `amicus update` to upgrade'
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Perform the actual update by spawning npm install -g.
107
+ * @returns {Promise<{ success: boolean, newVersion?: string, error?: string }>}
108
+ */
109
+ function performUpdate() {
110
+ const mock = getMockMode();
111
+
112
+ if (mock === 'success') {
113
+ return Promise.resolve({ success: true, newVersion: FAKE_LATEST });
114
+ }
115
+
116
+ if (mock === 'error') {
117
+ return Promise.resolve({
118
+ success: false,
119
+ error: 'Mock update error for testing'
120
+ });
121
+ }
122
+
123
+ return new Promise((resolve) => {
124
+ let stderr = '';
125
+
126
+ const proc = spawn('npm', ['install', '-g', 'amicus@latest'], {
127
+ stdio: ['ignore', 'pipe', 'pipe'],
128
+ shell: true
129
+ });
130
+
131
+ proc.stderr.on('data', (data) => {
132
+ stderr += data.toString();
133
+ });
134
+
135
+ proc.on('close', (code) => {
136
+ if (code === 0) {
137
+ resolve({ success: true });
138
+ } else {
139
+ resolve({
140
+ success: false,
141
+ error: stderr.trim() || `npm exited with code ${code}`
142
+ });
143
+ }
144
+ });
145
+
146
+ proc.on('error', (err) => {
147
+ resolve({ success: false, error: err.message });
148
+ });
149
+ });
150
+ }
151
+
152
+ module.exports = {
153
+ initUpdateCheck,
154
+ getUpdateInfo,
155
+ notifyUpdate,
156
+ performUpdate
157
+ };