claudeup 3.6.6 → 3.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.6.6",
3
+ "version": "3.7.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Plugin system dependency installer
3
+ *
4
+ * Auto-detects available package managers and installs plugin dependencies
5
+ * declared in plugin.json's "setup" section.
6
+ *
7
+ * Supported managers:
8
+ * - pip: Python packages (auto-detects uv > pip3 > pip)
9
+ * - brew: Homebrew formulae (macOS/Linux)
10
+ * - npm: Global npm packages
11
+ * - cargo: Rust crates
12
+ *
13
+ * Example plugin.json:
14
+ * {
15
+ * "setup": {
16
+ * "pip": ["browser-use", "mcp"],
17
+ * "brew": ["memextech/tap/ht-mcp"]
18
+ * }
19
+ * }
20
+ */
21
+ import { execFile } from "node:child_process";
22
+ import { promisify } from "node:util";
23
+ import fs from "fs-extra";
24
+ import path from "node:path";
25
+ import os from "node:os";
26
+ import { which } from "../utils/command-utils.js";
27
+ const execFileAsync = promisify(execFile);
28
+ /**
29
+ * Run a command and return success/failure
30
+ */
31
+ async function run(cmd, args, timeoutMs = 120000) {
32
+ try {
33
+ const { stdout, stderr } = await execFileAsync(cmd, args, {
34
+ timeout: timeoutMs,
35
+ });
36
+ return { ok: true, stdout: stdout.trim(), stderr: stderr.trim() };
37
+ }
38
+ catch (error) {
39
+ const e = error;
40
+ return {
41
+ ok: false,
42
+ stdout: e.stdout?.trim() || "",
43
+ stderr: e.stderr?.trim() || e.message || "unknown error",
44
+ };
45
+ }
46
+ }
47
+ /**
48
+ * Detect the best available Python package installer
49
+ * Prefers: uv > pip3 > pip
50
+ */
51
+ async function detectPipCommand() {
52
+ // Prefer uv (fast, modern)
53
+ if (await which("uv")) {
54
+ return {
55
+ cmd: "uv",
56
+ args: ["pip", "install", "--system", "--break-system-packages"],
57
+ };
58
+ }
59
+ // Fall back to pip3
60
+ if (await which("pip3")) {
61
+ return {
62
+ cmd: "pip3",
63
+ args: ["install", "--break-system-packages"],
64
+ };
65
+ }
66
+ // Fall back to pip
67
+ if (await which("pip")) {
68
+ return {
69
+ cmd: "pip",
70
+ args: ["install", "--break-system-packages"],
71
+ };
72
+ }
73
+ return null;
74
+ }
75
+ /**
76
+ * Check if a Python package is already installed
77
+ */
78
+ async function isPythonPackageInstalled(pkg) {
79
+ // Convert package name to importable module name (e.g., browser-use → browser_use)
80
+ const moduleName = pkg.replace(/-/g, "_");
81
+ const result = await run("python3", ["-c", `import ${moduleName}`]);
82
+ return result.ok;
83
+ }
84
+ /**
85
+ * Check if a binary is available in PATH
86
+ */
87
+ async function isBinaryAvailable(name) {
88
+ return (await which(name)) !== null;
89
+ }
90
+ /**
91
+ * Extract binary name from a brew formula or npm package
92
+ * e.g., "memextech/tap/ht-mcp" → "ht-mcp"
93
+ */
94
+ function extractBinaryName(pkg) {
95
+ return pkg.split("/").pop() || pkg;
96
+ }
97
+ /**
98
+ * Install pip packages
99
+ */
100
+ async function installPipPackages(packages, result) {
101
+ const pip = await detectPipCommand();
102
+ if (!pip) {
103
+ for (const pkg of packages) {
104
+ result.failed.push({ pkg: `pip:${pkg}`, error: "No pip/uv found" });
105
+ }
106
+ return;
107
+ }
108
+ // Filter out already installed
109
+ const toInstall = [];
110
+ for (const pkg of packages) {
111
+ if (await isPythonPackageInstalled(pkg)) {
112
+ result.skipped.push(`pip:${pkg}`);
113
+ }
114
+ else {
115
+ toInstall.push(pkg);
116
+ }
117
+ }
118
+ if (toInstall.length === 0)
119
+ return;
120
+ // Install all at once
121
+ const { ok, stderr } = await run(pip.cmd, [...pip.args, ...toInstall]);
122
+ if (ok) {
123
+ for (const pkg of toInstall) {
124
+ result.installed.push(`pip:${pkg}`);
125
+ }
126
+ }
127
+ else {
128
+ for (const pkg of toInstall) {
129
+ result.failed.push({ pkg: `pip:${pkg}`, error: stderr });
130
+ }
131
+ }
132
+ }
133
+ /**
134
+ * Install brew packages
135
+ */
136
+ async function installBrewPackages(packages, result) {
137
+ const brewPath = await which("brew");
138
+ if (!brewPath) {
139
+ for (const pkg of packages) {
140
+ result.failed.push({
141
+ pkg: `brew:${pkg}`,
142
+ error: "Homebrew not installed",
143
+ });
144
+ }
145
+ return;
146
+ }
147
+ for (const pkg of packages) {
148
+ const binaryName = extractBinaryName(pkg);
149
+ if (await isBinaryAvailable(binaryName)) {
150
+ result.skipped.push(`brew:${pkg}`);
151
+ continue;
152
+ }
153
+ const { ok, stderr } = await run(brewPath, ["install", pkg]);
154
+ if (ok) {
155
+ result.installed.push(`brew:${pkg}`);
156
+ }
157
+ else {
158
+ result.failed.push({ pkg: `brew:${pkg}`, error: stderr });
159
+ }
160
+ }
161
+ }
162
+ /**
163
+ * Install npm global packages
164
+ */
165
+ async function installNpmPackages(packages, result) {
166
+ const npmPath = await which("npm");
167
+ if (!npmPath) {
168
+ for (const pkg of packages) {
169
+ result.failed.push({ pkg: `npm:${pkg}`, error: "npm not found" });
170
+ }
171
+ return;
172
+ }
173
+ for (const pkg of packages) {
174
+ if (await isBinaryAvailable(pkg)) {
175
+ result.skipped.push(`npm:${pkg}`);
176
+ continue;
177
+ }
178
+ const { ok, stderr } = await run(npmPath, ["install", "-g", pkg]);
179
+ if (ok) {
180
+ result.installed.push(`npm:${pkg}`);
181
+ }
182
+ else {
183
+ result.failed.push({ pkg: `npm:${pkg}`, error: stderr });
184
+ }
185
+ }
186
+ }
187
+ /**
188
+ * Install cargo packages
189
+ */
190
+ async function installCargoPackages(packages, result) {
191
+ const cargoPath = await which("cargo");
192
+ if (!cargoPath) {
193
+ for (const pkg of packages) {
194
+ result.failed.push({ pkg: `cargo:${pkg}`, error: "cargo not found" });
195
+ }
196
+ return;
197
+ }
198
+ for (const pkg of packages) {
199
+ if (await isBinaryAvailable(pkg)) {
200
+ result.skipped.push(`cargo:${pkg}`);
201
+ continue;
202
+ }
203
+ const { ok, stderr } = await run(cargoPath, ["install", pkg], 300000);
204
+ if (ok) {
205
+ result.installed.push(`cargo:${pkg}`);
206
+ }
207
+ else {
208
+ result.failed.push({ pkg: `cargo:${pkg}`, error: stderr });
209
+ }
210
+ }
211
+ }
212
+ /**
213
+ * Read the setup config from a plugin's cached manifest
214
+ */
215
+ export async function getPluginSetupConfig(marketplace, pluginName) {
216
+ const cacheDir = path.join(os.homedir(), ".claude", "plugins", "cache", marketplace);
217
+ // Find the plugin directory (versioned)
218
+ if (!(await fs.pathExists(cacheDir)))
219
+ return null;
220
+ const entries = await fs.readdir(cacheDir);
221
+ // Look for the plugin name directory
222
+ const pluginDir = entries.find((e) => e === pluginName);
223
+ if (!pluginDir)
224
+ return null;
225
+ const pluginPath = path.join(cacheDir, pluginDir);
226
+ const stat = await fs.stat(pluginPath);
227
+ if (!stat.isDirectory())
228
+ return null;
229
+ // Find the version directory
230
+ const versions = await fs.readdir(pluginPath);
231
+ if (versions.length === 0)
232
+ return null;
233
+ // Use the latest version directory
234
+ const latestVersion = versions.sort().pop();
235
+ const manifestPath = path.join(pluginPath, latestVersion, "plugin.json");
236
+ if (!(await fs.pathExists(manifestPath)))
237
+ return null;
238
+ try {
239
+ const manifest = await fs.readJson(manifestPath);
240
+ return manifest.setup || null;
241
+ }
242
+ catch {
243
+ return null;
244
+ }
245
+ }
246
+ /**
247
+ * Read setup config from a local plugin directory (marketplace source)
248
+ */
249
+ export async function getPluginSetupFromSource(marketplace, pluginName) {
250
+ const marketplaceDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces", marketplace);
251
+ if (!(await fs.pathExists(marketplaceDir)))
252
+ return null;
253
+ // Try common plugin locations
254
+ for (const subdir of ["plugins", ""]) {
255
+ const pluginJson = subdir
256
+ ? path.join(marketplaceDir, subdir, pluginName, "plugin.json")
257
+ : path.join(marketplaceDir, pluginName, "plugin.json");
258
+ if (await fs.pathExists(pluginJson)) {
259
+ try {
260
+ const manifest = await fs.readJson(pluginJson);
261
+ return manifest.setup || null;
262
+ }
263
+ catch {
264
+ continue;
265
+ }
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+ /**
271
+ * Install all dependencies declared in a plugin's setup config.
272
+ * Auto-detects available package managers.
273
+ */
274
+ export async function installPluginDeps(setup) {
275
+ const result = {
276
+ installed: [],
277
+ skipped: [],
278
+ failed: [],
279
+ };
280
+ if (setup.pip?.length) {
281
+ await installPipPackages(setup.pip, result);
282
+ }
283
+ if (setup.brew?.length) {
284
+ await installBrewPackages(setup.brew, result);
285
+ }
286
+ if (setup.npm?.length) {
287
+ await installNpmPackages(setup.npm, result);
288
+ }
289
+ if (setup.cargo?.length) {
290
+ await installCargoPackages(setup.cargo, result);
291
+ }
292
+ return result;
293
+ }
294
+ /**
295
+ * Check which dependencies from a setup config are missing.
296
+ * Returns a filtered SetupConfig with only missing deps.
297
+ */
298
+ export async function checkMissingDeps(setup) {
299
+ const missing = {};
300
+ if (setup.pip?.length) {
301
+ const missingPip = [];
302
+ for (const pkg of setup.pip) {
303
+ if (!(await isPythonPackageInstalled(pkg))) {
304
+ missingPip.push(pkg);
305
+ }
306
+ }
307
+ if (missingPip.length > 0)
308
+ missing.pip = missingPip;
309
+ }
310
+ if (setup.brew?.length) {
311
+ const missingBrew = [];
312
+ for (const pkg of setup.brew) {
313
+ const bin = extractBinaryName(pkg);
314
+ if (!(await isBinaryAvailable(bin))) {
315
+ missingBrew.push(pkg);
316
+ }
317
+ }
318
+ if (missingBrew.length > 0)
319
+ missing.brew = missingBrew;
320
+ }
321
+ if (setup.npm?.length) {
322
+ const missingNpm = [];
323
+ for (const pkg of setup.npm) {
324
+ if (!(await isBinaryAvailable(pkg))) {
325
+ missingNpm.push(pkg);
326
+ }
327
+ }
328
+ if (missingNpm.length > 0)
329
+ missing.npm = missingNpm;
330
+ }
331
+ if (setup.cargo?.length) {
332
+ const missingCargo = [];
333
+ for (const pkg of setup.cargo) {
334
+ if (!(await isBinaryAvailable(pkg))) {
335
+ missingCargo.push(pkg);
336
+ }
337
+ }
338
+ if (missingCargo.length > 0)
339
+ missing.cargo = missingCargo;
340
+ }
341
+ return missing;
342
+ }
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Plugin system dependency installer
3
+ *
4
+ * Auto-detects available package managers and installs plugin dependencies
5
+ * declared in plugin.json's "setup" section.
6
+ *
7
+ * Supported managers:
8
+ * - pip: Python packages (auto-detects uv > pip3 > pip)
9
+ * - brew: Homebrew formulae (macOS/Linux)
10
+ * - npm: Global npm packages
11
+ * - cargo: Rust crates
12
+ *
13
+ * Example plugin.json:
14
+ * {
15
+ * "setup": {
16
+ * "pip": ["browser-use", "mcp"],
17
+ * "brew": ["memextech/tap/ht-mcp"]
18
+ * }
19
+ * }
20
+ */
21
+
22
+ import { execFile } from "node:child_process";
23
+ import { promisify } from "node:util";
24
+ import fs from "fs-extra";
25
+ import path from "node:path";
26
+ import os from "node:os";
27
+ import { which } from "../utils/command-utils.js";
28
+
29
+ const execFileAsync = promisify(execFile);
30
+
31
+ export interface SetupConfig {
32
+ pip?: string[];
33
+ brew?: string[];
34
+ npm?: string[];
35
+ cargo?: string[];
36
+ }
37
+
38
+ export interface SetupResult {
39
+ installed: string[];
40
+ skipped: string[];
41
+ failed: Array<{ pkg: string; error: string }>;
42
+ }
43
+
44
+ /**
45
+ * Run a command and return success/failure
46
+ */
47
+ async function run(
48
+ cmd: string,
49
+ args: string[],
50
+ timeoutMs = 120000,
51
+ ): Promise<{ ok: boolean; stdout: string; stderr: string }> {
52
+ try {
53
+ const { stdout, stderr } = await execFileAsync(cmd, args, {
54
+ timeout: timeoutMs,
55
+ });
56
+ return { ok: true, stdout: stdout.trim(), stderr: stderr.trim() };
57
+ } catch (error: unknown) {
58
+ const e = error as { stdout?: string; stderr?: string; message?: string };
59
+ return {
60
+ ok: false,
61
+ stdout: e.stdout?.trim() || "",
62
+ stderr: e.stderr?.trim() || e.message || "unknown error",
63
+ };
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Detect the best available Python package installer
69
+ * Prefers: uv > pip3 > pip
70
+ */
71
+ async function detectPipCommand(): Promise<{
72
+ cmd: string;
73
+ args: string[];
74
+ } | null> {
75
+ // Prefer uv (fast, modern)
76
+ if (await which("uv")) {
77
+ return {
78
+ cmd: "uv",
79
+ args: ["pip", "install", "--system", "--break-system-packages"],
80
+ };
81
+ }
82
+ // Fall back to pip3
83
+ if (await which("pip3")) {
84
+ return {
85
+ cmd: "pip3",
86
+ args: ["install", "--break-system-packages"],
87
+ };
88
+ }
89
+ // Fall back to pip
90
+ if (await which("pip")) {
91
+ return {
92
+ cmd: "pip",
93
+ args: ["install", "--break-system-packages"],
94
+ };
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Check if a Python package is already installed
101
+ */
102
+ async function isPythonPackageInstalled(pkg: string): Promise<boolean> {
103
+ // Convert package name to importable module name (e.g., browser-use → browser_use)
104
+ const moduleName = pkg.replace(/-/g, "_");
105
+ const result = await run("python3", ["-c", `import ${moduleName}`]);
106
+ return result.ok;
107
+ }
108
+
109
+ /**
110
+ * Check if a binary is available in PATH
111
+ */
112
+ async function isBinaryAvailable(name: string): Promise<boolean> {
113
+ return (await which(name)) !== null;
114
+ }
115
+
116
+ /**
117
+ * Extract binary name from a brew formula or npm package
118
+ * e.g., "memextech/tap/ht-mcp" → "ht-mcp"
119
+ */
120
+ function extractBinaryName(pkg: string): string {
121
+ return pkg.split("/").pop() || pkg;
122
+ }
123
+
124
+ /**
125
+ * Install pip packages
126
+ */
127
+ async function installPipPackages(
128
+ packages: string[],
129
+ result: SetupResult,
130
+ ): Promise<void> {
131
+ const pip = await detectPipCommand();
132
+ if (!pip) {
133
+ for (const pkg of packages) {
134
+ result.failed.push({ pkg: `pip:${pkg}`, error: "No pip/uv found" });
135
+ }
136
+ return;
137
+ }
138
+
139
+ // Filter out already installed
140
+ const toInstall: string[] = [];
141
+ for (const pkg of packages) {
142
+ if (await isPythonPackageInstalled(pkg)) {
143
+ result.skipped.push(`pip:${pkg}`);
144
+ } else {
145
+ toInstall.push(pkg);
146
+ }
147
+ }
148
+
149
+ if (toInstall.length === 0) return;
150
+
151
+ // Install all at once
152
+ const { ok, stderr } = await run(pip.cmd, [...pip.args, ...toInstall]);
153
+ if (ok) {
154
+ for (const pkg of toInstall) {
155
+ result.installed.push(`pip:${pkg}`);
156
+ }
157
+ } else {
158
+ for (const pkg of toInstall) {
159
+ result.failed.push({ pkg: `pip:${pkg}`, error: stderr });
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Install brew packages
166
+ */
167
+ async function installBrewPackages(
168
+ packages: string[],
169
+ result: SetupResult,
170
+ ): Promise<void> {
171
+ const brewPath = await which("brew");
172
+ if (!brewPath) {
173
+ for (const pkg of packages) {
174
+ result.failed.push({
175
+ pkg: `brew:${pkg}`,
176
+ error: "Homebrew not installed",
177
+ });
178
+ }
179
+ return;
180
+ }
181
+
182
+ for (const pkg of packages) {
183
+ const binaryName = extractBinaryName(pkg);
184
+ if (await isBinaryAvailable(binaryName)) {
185
+ result.skipped.push(`brew:${pkg}`);
186
+ continue;
187
+ }
188
+
189
+ const { ok, stderr } = await run(brewPath, ["install", pkg]);
190
+ if (ok) {
191
+ result.installed.push(`brew:${pkg}`);
192
+ } else {
193
+ result.failed.push({ pkg: `brew:${pkg}`, error: stderr });
194
+ }
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Install npm global packages
200
+ */
201
+ async function installNpmPackages(
202
+ packages: string[],
203
+ result: SetupResult,
204
+ ): Promise<void> {
205
+ const npmPath = await which("npm");
206
+ if (!npmPath) {
207
+ for (const pkg of packages) {
208
+ result.failed.push({ pkg: `npm:${pkg}`, error: "npm not found" });
209
+ }
210
+ return;
211
+ }
212
+
213
+ for (const pkg of packages) {
214
+ if (await isBinaryAvailable(pkg)) {
215
+ result.skipped.push(`npm:${pkg}`);
216
+ continue;
217
+ }
218
+
219
+ const { ok, stderr } = await run(npmPath, ["install", "-g", pkg]);
220
+ if (ok) {
221
+ result.installed.push(`npm:${pkg}`);
222
+ } else {
223
+ result.failed.push({ pkg: `npm:${pkg}`, error: stderr });
224
+ }
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Install cargo packages
230
+ */
231
+ async function installCargoPackages(
232
+ packages: string[],
233
+ result: SetupResult,
234
+ ): Promise<void> {
235
+ const cargoPath = await which("cargo");
236
+ if (!cargoPath) {
237
+ for (const pkg of packages) {
238
+ result.failed.push({ pkg: `cargo:${pkg}`, error: "cargo not found" });
239
+ }
240
+ return;
241
+ }
242
+
243
+ for (const pkg of packages) {
244
+ if (await isBinaryAvailable(pkg)) {
245
+ result.skipped.push(`cargo:${pkg}`);
246
+ continue;
247
+ }
248
+
249
+ const { ok, stderr } = await run(cargoPath, ["install", pkg], 300000);
250
+ if (ok) {
251
+ result.installed.push(`cargo:${pkg}`);
252
+ } else {
253
+ result.failed.push({ pkg: `cargo:${pkg}`, error: stderr });
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Read the setup config from a plugin's cached manifest
260
+ */
261
+ export async function getPluginSetupConfig(
262
+ marketplace: string,
263
+ pluginName: string,
264
+ ): Promise<SetupConfig | null> {
265
+ const cacheDir = path.join(
266
+ os.homedir(),
267
+ ".claude",
268
+ "plugins",
269
+ "cache",
270
+ marketplace,
271
+ );
272
+
273
+ // Find the plugin directory (versioned)
274
+ if (!(await fs.pathExists(cacheDir))) return null;
275
+
276
+ const entries = await fs.readdir(cacheDir);
277
+ // Look for the plugin name directory
278
+ const pluginDir = entries.find((e) => e === pluginName);
279
+ if (!pluginDir) return null;
280
+
281
+ const pluginPath = path.join(cacheDir, pluginDir);
282
+ const stat = await fs.stat(pluginPath);
283
+ if (!stat.isDirectory()) return null;
284
+
285
+ // Find the version directory
286
+ const versions = await fs.readdir(pluginPath);
287
+ if (versions.length === 0) return null;
288
+
289
+ // Use the latest version directory
290
+ const latestVersion = versions.sort().pop()!;
291
+ const manifestPath = path.join(pluginPath, latestVersion, "plugin.json");
292
+
293
+ if (!(await fs.pathExists(manifestPath))) return null;
294
+
295
+ try {
296
+ const manifest = await fs.readJson(manifestPath);
297
+ return manifest.setup || null;
298
+ } catch {
299
+ return null;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Read setup config from a local plugin directory (marketplace source)
305
+ */
306
+ export async function getPluginSetupFromSource(
307
+ marketplace: string,
308
+ pluginName: string,
309
+ ): Promise<SetupConfig | null> {
310
+ const marketplaceDir = path.join(
311
+ os.homedir(),
312
+ ".claude",
313
+ "plugins",
314
+ "marketplaces",
315
+ marketplace,
316
+ );
317
+
318
+ if (!(await fs.pathExists(marketplaceDir))) return null;
319
+
320
+ // Try common plugin locations
321
+ for (const subdir of ["plugins", ""]) {
322
+ const pluginJson = subdir
323
+ ? path.join(marketplaceDir, subdir, pluginName, "plugin.json")
324
+ : path.join(marketplaceDir, pluginName, "plugin.json");
325
+
326
+ if (await fs.pathExists(pluginJson)) {
327
+ try {
328
+ const manifest = await fs.readJson(pluginJson);
329
+ return manifest.setup || null;
330
+ } catch {
331
+ continue;
332
+ }
333
+ }
334
+ }
335
+
336
+ return null;
337
+ }
338
+
339
+ /**
340
+ * Install all dependencies declared in a plugin's setup config.
341
+ * Auto-detects available package managers.
342
+ */
343
+ export async function installPluginDeps(
344
+ setup: SetupConfig,
345
+ ): Promise<SetupResult> {
346
+ const result: SetupResult = {
347
+ installed: [],
348
+ skipped: [],
349
+ failed: [],
350
+ };
351
+
352
+ if (setup.pip?.length) {
353
+ await installPipPackages(setup.pip, result);
354
+ }
355
+ if (setup.brew?.length) {
356
+ await installBrewPackages(setup.brew, result);
357
+ }
358
+ if (setup.npm?.length) {
359
+ await installNpmPackages(setup.npm, result);
360
+ }
361
+ if (setup.cargo?.length) {
362
+ await installCargoPackages(setup.cargo, result);
363
+ }
364
+
365
+ return result;
366
+ }
367
+
368
+ /**
369
+ * Check which dependencies from a setup config are missing.
370
+ * Returns a filtered SetupConfig with only missing deps.
371
+ */
372
+ export async function checkMissingDeps(
373
+ setup: SetupConfig,
374
+ ): Promise<SetupConfig> {
375
+ const missing: SetupConfig = {};
376
+
377
+ if (setup.pip?.length) {
378
+ const missingPip: string[] = [];
379
+ for (const pkg of setup.pip) {
380
+ if (!(await isPythonPackageInstalled(pkg))) {
381
+ missingPip.push(pkg);
382
+ }
383
+ }
384
+ if (missingPip.length > 0) missing.pip = missingPip;
385
+ }
386
+
387
+ if (setup.brew?.length) {
388
+ const missingBrew: string[] = [];
389
+ for (const pkg of setup.brew) {
390
+ const bin = extractBinaryName(pkg);
391
+ if (!(await isBinaryAvailable(bin))) {
392
+ missingBrew.push(pkg);
393
+ }
394
+ }
395
+ if (missingBrew.length > 0) missing.brew = missingBrew;
396
+ }
397
+
398
+ if (setup.npm?.length) {
399
+ const missingNpm: string[] = [];
400
+ for (const pkg of setup.npm) {
401
+ if (!(await isBinaryAvailable(pkg))) {
402
+ missingNpm.push(pkg);
403
+ }
404
+ }
405
+ if (missingNpm.length > 0) missing.npm = missingNpm;
406
+ }
407
+
408
+ if (setup.cargo?.length) {
409
+ const missingCargo: string[] = [];
410
+ for (const pkg of setup.cargo) {
411
+ if (!(await isBinaryAvailable(pkg))) {
412
+ missingCargo.push(pkg);
413
+ }
414
+ }
415
+ if (missingCargo.length > 0) missing.cargo = missingCargo;
416
+ }
417
+
418
+ return missing;
419
+ }
@@ -12,6 +12,7 @@ import { getAvailablePlugins, refreshAllMarketplaces, clearMarketplaceCache, get
12
12
  import { setMcpEnvVar, getMcpEnvVars, } from "../../services/claude-settings.js";
13
13
  import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
14
14
  import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
15
+ import { getPluginSetupFromSource, checkMissingDeps, installPluginDeps, } from "../../services/plugin-setup.js";
15
16
  export function PluginsScreen() {
16
17
  const { state, dispatch } = useApp();
17
18
  const { plugins: pluginsState } = state;
@@ -352,6 +353,52 @@ export function PluginsScreen() {
352
353
  return true; // Don't block installation on config errors
353
354
  }
354
355
  };
356
+ /**
357
+ * Install system dependencies required by a plugin's MCP servers
358
+ * Auto-detects available package managers (uv/pip, brew, npm, cargo)
359
+ */
360
+ const installPluginSystemDeps = async (pluginName, marketplace) => {
361
+ try {
362
+ const setup = await getPluginSetupFromSource(marketplace, pluginName);
363
+ if (!setup)
364
+ return;
365
+ const missing = await checkMissingDeps(setup);
366
+ const hasMissing = (missing.pip?.length || 0) +
367
+ (missing.brew?.length || 0) +
368
+ (missing.npm?.length || 0) +
369
+ (missing.cargo?.length || 0) > 0;
370
+ if (!hasMissing)
371
+ return;
372
+ // Build description of what will be installed
373
+ const parts = [];
374
+ if (missing.pip?.length)
375
+ parts.push(`pip: ${missing.pip.join(", ")}`);
376
+ if (missing.brew?.length)
377
+ parts.push(`brew: ${missing.brew.join(", ")}`);
378
+ if (missing.npm?.length)
379
+ parts.push(`npm: ${missing.npm.join(", ")}`);
380
+ if (missing.cargo?.length)
381
+ parts.push(`cargo: ${missing.cargo.join(", ")}`);
382
+ const wantInstall = await modal.confirm("Install Dependencies?", `This plugin needs system dependencies:\n\n${parts.join("\n")}\n\nInstall now?`);
383
+ if (!wantInstall)
384
+ return;
385
+ modal.loading("Installing dependencies...");
386
+ const result = await installPluginDeps(missing);
387
+ modal.hideModal();
388
+ if (result.failed.length > 0) {
389
+ const failMsg = result.failed
390
+ .map((f) => `${f.pkg}: ${f.error}`)
391
+ .join("\n");
392
+ await modal.message("Partial Install", `Installed: ${result.installed.length}\nFailed:\n${failMsg}`, "error");
393
+ }
394
+ else if (result.installed.length > 0) {
395
+ await modal.message("Dependencies Installed", `Installed ${result.installed.length} package(s):\n${result.installed.join(", ")}`, "success");
396
+ }
397
+ }
398
+ catch (error) {
399
+ console.error("Error installing plugin deps:", error);
400
+ }
401
+ };
355
402
  const handleSelect = async () => {
356
403
  const item = selectableItems[pluginsState.selectedIndex];
357
404
  if (!item)
@@ -464,9 +511,10 @@ export function PluginsScreen() {
464
511
  }
465
512
  else {
466
513
  await cliInstallPlugin(plugin.id, scope);
467
- // On fresh install, prompt for MCP server env vars if needed
514
+ // On fresh install, configure env vars and install system deps
468
515
  modal.hideModal();
469
516
  await collectPluginEnvVars(plugin.name, plugin.marketplace);
517
+ await installPluginSystemDeps(plugin.name, plugin.marketplace);
470
518
  }
471
519
  if (action !== "install") {
472
520
  modal.hideModal();
@@ -563,9 +611,10 @@ export function PluginsScreen() {
563
611
  }
564
612
  else {
565
613
  await cliInstallPlugin(plugin.id, scope);
566
- // On fresh install, prompt for MCP server env vars if needed
614
+ // On fresh install, configure env vars and install system deps
567
615
  modal.hideModal();
568
616
  await collectPluginEnvVars(plugin.name, plugin.marketplace);
617
+ await installPluginSystemDeps(plugin.name, plugin.marketplace);
569
618
  }
570
619
  if (action !== "install") {
571
620
  modal.hideModal();
@@ -28,6 +28,11 @@ import {
28
28
  getPluginEnvRequirements,
29
29
  getPluginSourcePath,
30
30
  } from "../../services/plugin-mcp-config.js";
31
+ import {
32
+ getPluginSetupFromSource,
33
+ checkMissingDeps,
34
+ installPluginDeps,
35
+ } from "../../services/plugin-setup.js";
31
36
  import type { Marketplace } from "../../types/index.js";
32
37
 
33
38
  interface ListItem {
@@ -466,6 +471,66 @@ export function PluginsScreen() {
466
471
  }
467
472
  };
468
473
 
474
+ /**
475
+ * Install system dependencies required by a plugin's MCP servers
476
+ * Auto-detects available package managers (uv/pip, brew, npm, cargo)
477
+ */
478
+ const installPluginSystemDeps = async (
479
+ pluginName: string,
480
+ marketplace: string,
481
+ ): Promise<void> => {
482
+ try {
483
+ const setup = await getPluginSetupFromSource(marketplace, pluginName);
484
+ if (!setup) return;
485
+
486
+ const missing = await checkMissingDeps(setup);
487
+ const hasMissing =
488
+ (missing.pip?.length || 0) +
489
+ (missing.brew?.length || 0) +
490
+ (missing.npm?.length || 0) +
491
+ (missing.cargo?.length || 0) > 0;
492
+
493
+ if (!hasMissing) return;
494
+
495
+ // Build description of what will be installed
496
+ const parts: string[] = [];
497
+ if (missing.pip?.length) parts.push(`pip: ${missing.pip.join(", ")}`);
498
+ if (missing.brew?.length) parts.push(`brew: ${missing.brew.join(", ")}`);
499
+ if (missing.npm?.length) parts.push(`npm: ${missing.npm.join(", ")}`);
500
+ if (missing.cargo?.length) parts.push(`cargo: ${missing.cargo.join(", ")}`);
501
+
502
+ const wantInstall = await modal.confirm(
503
+ "Install Dependencies?",
504
+ `This plugin needs system dependencies:\n\n${parts.join("\n")}\n\nInstall now?`,
505
+ );
506
+
507
+ if (!wantInstall) return;
508
+
509
+ modal.loading("Installing dependencies...");
510
+ const result = await installPluginDeps(missing);
511
+ modal.hideModal();
512
+
513
+ if (result.failed.length > 0) {
514
+ const failMsg = result.failed
515
+ .map((f) => `${f.pkg}: ${f.error}`)
516
+ .join("\n");
517
+ await modal.message(
518
+ "Partial Install",
519
+ `Installed: ${result.installed.length}\nFailed:\n${failMsg}`,
520
+ "error",
521
+ );
522
+ } else if (result.installed.length > 0) {
523
+ await modal.message(
524
+ "Dependencies Installed",
525
+ `Installed ${result.installed.length} package(s):\n${result.installed.join(", ")}`,
526
+ "success",
527
+ );
528
+ }
529
+ } catch (error) {
530
+ console.error("Error installing plugin deps:", error);
531
+ }
532
+ };
533
+
469
534
  const handleSelect = async () => {
470
535
  const item = selectableItems[pluginsState.selectedIndex];
471
536
  if (!item) return;
@@ -601,9 +666,10 @@ export function PluginsScreen() {
601
666
  } else {
602
667
  await cliInstallPlugin(plugin.id, scope);
603
668
 
604
- // On fresh install, prompt for MCP server env vars if needed
669
+ // On fresh install, configure env vars and install system deps
605
670
  modal.hideModal();
606
671
  await collectPluginEnvVars(plugin.name, plugin.marketplace);
672
+ await installPluginSystemDeps(plugin.name, plugin.marketplace);
607
673
  }
608
674
  if (action !== "install") {
609
675
  modal.hideModal();
@@ -708,9 +774,10 @@ export function PluginsScreen() {
708
774
  } else {
709
775
  await cliInstallPlugin(plugin.id, scope);
710
776
 
711
- // On fresh install, prompt for MCP server env vars if needed
777
+ // On fresh install, configure env vars and install system deps
712
778
  modal.hideModal();
713
779
  await collectPluginEnvVars(plugin.name, plugin.marketplace);
780
+ await installPluginSystemDeps(plugin.name, plugin.marketplace);
714
781
  }
715
782
  if (action !== "install") {
716
783
  modal.hideModal();