claudeup 3.6.6 → 3.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.
package/package.json
CHANGED
|
@@ -0,0 +1,357 @@
|
|
|
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
|
+
* Detect the best available Node.js package installer for globals
|
|
164
|
+
* Prefers: bun > npm
|
|
165
|
+
*/
|
|
166
|
+
async function detectNpmCommand() {
|
|
167
|
+
const bunPath = await which("bun");
|
|
168
|
+
if (bunPath) {
|
|
169
|
+
return { cmd: bunPath, args: ["install", "-g"], label: "bun" };
|
|
170
|
+
}
|
|
171
|
+
const npmPath = await which("npm");
|
|
172
|
+
if (npmPath) {
|
|
173
|
+
return { cmd: npmPath, args: ["install", "-g"], label: "npm" };
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Install global Node.js packages (prefers bun > npm)
|
|
179
|
+
*/
|
|
180
|
+
async function installNpmPackages(packages, result) {
|
|
181
|
+
const installer = await detectNpmCommand();
|
|
182
|
+
if (!installer) {
|
|
183
|
+
for (const pkg of packages) {
|
|
184
|
+
result.failed.push({ pkg: `npm:${pkg}`, error: "No bun or npm found" });
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const pkg of packages) {
|
|
189
|
+
if (await isBinaryAvailable(pkg)) {
|
|
190
|
+
result.skipped.push(`npm:${pkg}`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const { ok, stderr } = await run(installer.cmd, [...installer.args, pkg]);
|
|
194
|
+
if (ok) {
|
|
195
|
+
result.installed.push(`${installer.label}:${pkg}`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
result.failed.push({ pkg: `${installer.label}:${pkg}`, error: stderr });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Install cargo packages
|
|
204
|
+
*/
|
|
205
|
+
async function installCargoPackages(packages, result) {
|
|
206
|
+
const cargoPath = await which("cargo");
|
|
207
|
+
if (!cargoPath) {
|
|
208
|
+
for (const pkg of packages) {
|
|
209
|
+
result.failed.push({ pkg: `cargo:${pkg}`, error: "cargo not found" });
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const pkg of packages) {
|
|
214
|
+
if (await isBinaryAvailable(pkg)) {
|
|
215
|
+
result.skipped.push(`cargo:${pkg}`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const { ok, stderr } = await run(cargoPath, ["install", pkg], 300000);
|
|
219
|
+
if (ok) {
|
|
220
|
+
result.installed.push(`cargo:${pkg}`);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
result.failed.push({ pkg: `cargo:${pkg}`, error: stderr });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Read the setup config from a plugin's cached manifest
|
|
229
|
+
*/
|
|
230
|
+
export async function getPluginSetupConfig(marketplace, pluginName) {
|
|
231
|
+
const cacheDir = path.join(os.homedir(), ".claude", "plugins", "cache", marketplace);
|
|
232
|
+
// Find the plugin directory (versioned)
|
|
233
|
+
if (!(await fs.pathExists(cacheDir)))
|
|
234
|
+
return null;
|
|
235
|
+
const entries = await fs.readdir(cacheDir);
|
|
236
|
+
// Look for the plugin name directory
|
|
237
|
+
const pluginDir = entries.find((e) => e === pluginName);
|
|
238
|
+
if (!pluginDir)
|
|
239
|
+
return null;
|
|
240
|
+
const pluginPath = path.join(cacheDir, pluginDir);
|
|
241
|
+
const stat = await fs.stat(pluginPath);
|
|
242
|
+
if (!stat.isDirectory())
|
|
243
|
+
return null;
|
|
244
|
+
// Find the version directory
|
|
245
|
+
const versions = await fs.readdir(pluginPath);
|
|
246
|
+
if (versions.length === 0)
|
|
247
|
+
return null;
|
|
248
|
+
// Use the latest version directory
|
|
249
|
+
const latestVersion = versions.sort().pop();
|
|
250
|
+
const manifestPath = path.join(pluginPath, latestVersion, "plugin.json");
|
|
251
|
+
if (!(await fs.pathExists(manifestPath)))
|
|
252
|
+
return null;
|
|
253
|
+
try {
|
|
254
|
+
const manifest = await fs.readJson(manifestPath);
|
|
255
|
+
return manifest.setup || null;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Read setup config from a local plugin directory (marketplace source)
|
|
263
|
+
*/
|
|
264
|
+
export async function getPluginSetupFromSource(marketplace, pluginName) {
|
|
265
|
+
const marketplaceDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces", marketplace);
|
|
266
|
+
if (!(await fs.pathExists(marketplaceDir)))
|
|
267
|
+
return null;
|
|
268
|
+
// Try common plugin locations
|
|
269
|
+
for (const subdir of ["plugins", ""]) {
|
|
270
|
+
const pluginJson = subdir
|
|
271
|
+
? path.join(marketplaceDir, subdir, pluginName, "plugin.json")
|
|
272
|
+
: path.join(marketplaceDir, pluginName, "plugin.json");
|
|
273
|
+
if (await fs.pathExists(pluginJson)) {
|
|
274
|
+
try {
|
|
275
|
+
const manifest = await fs.readJson(pluginJson);
|
|
276
|
+
return manifest.setup || null;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Install all dependencies declared in a plugin's setup config.
|
|
287
|
+
* Auto-detects available package managers.
|
|
288
|
+
*/
|
|
289
|
+
export async function installPluginDeps(setup) {
|
|
290
|
+
const result = {
|
|
291
|
+
installed: [],
|
|
292
|
+
skipped: [],
|
|
293
|
+
failed: [],
|
|
294
|
+
};
|
|
295
|
+
if (setup.pip?.length) {
|
|
296
|
+
await installPipPackages(setup.pip, result);
|
|
297
|
+
}
|
|
298
|
+
if (setup.brew?.length) {
|
|
299
|
+
await installBrewPackages(setup.brew, result);
|
|
300
|
+
}
|
|
301
|
+
if (setup.npm?.length) {
|
|
302
|
+
await installNpmPackages(setup.npm, result);
|
|
303
|
+
}
|
|
304
|
+
if (setup.cargo?.length) {
|
|
305
|
+
await installCargoPackages(setup.cargo, result);
|
|
306
|
+
}
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Check which dependencies from a setup config are missing.
|
|
311
|
+
* Returns a filtered SetupConfig with only missing deps.
|
|
312
|
+
*/
|
|
313
|
+
export async function checkMissingDeps(setup) {
|
|
314
|
+
const missing = {};
|
|
315
|
+
if (setup.pip?.length) {
|
|
316
|
+
const missingPip = [];
|
|
317
|
+
for (const pkg of setup.pip) {
|
|
318
|
+
if (!(await isPythonPackageInstalled(pkg))) {
|
|
319
|
+
missingPip.push(pkg);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (missingPip.length > 0)
|
|
323
|
+
missing.pip = missingPip;
|
|
324
|
+
}
|
|
325
|
+
if (setup.brew?.length) {
|
|
326
|
+
const missingBrew = [];
|
|
327
|
+
for (const pkg of setup.brew) {
|
|
328
|
+
const bin = extractBinaryName(pkg);
|
|
329
|
+
if (!(await isBinaryAvailable(bin))) {
|
|
330
|
+
missingBrew.push(pkg);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (missingBrew.length > 0)
|
|
334
|
+
missing.brew = missingBrew;
|
|
335
|
+
}
|
|
336
|
+
if (setup.npm?.length) {
|
|
337
|
+
const missingNpm = [];
|
|
338
|
+
for (const pkg of setup.npm) {
|
|
339
|
+
if (!(await isBinaryAvailable(pkg))) {
|
|
340
|
+
missingNpm.push(pkg);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (missingNpm.length > 0)
|
|
344
|
+
missing.npm = missingNpm;
|
|
345
|
+
}
|
|
346
|
+
if (setup.cargo?.length) {
|
|
347
|
+
const missingCargo = [];
|
|
348
|
+
for (const pkg of setup.cargo) {
|
|
349
|
+
if (!(await isBinaryAvailable(pkg))) {
|
|
350
|
+
missingCargo.push(pkg);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (missingCargo.length > 0)
|
|
354
|
+
missing.cargo = missingCargo;
|
|
355
|
+
}
|
|
356
|
+
return missing;
|
|
357
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
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
|
+
* Detect the best available Node.js package installer for globals
|
|
200
|
+
* Prefers: bun > npm
|
|
201
|
+
*/
|
|
202
|
+
async function detectNpmCommand(): Promise<{
|
|
203
|
+
cmd: string;
|
|
204
|
+
args: string[];
|
|
205
|
+
label: string;
|
|
206
|
+
} | null> {
|
|
207
|
+
const bunPath = await which("bun");
|
|
208
|
+
if (bunPath) {
|
|
209
|
+
return { cmd: bunPath, args: ["install", "-g"], label: "bun" };
|
|
210
|
+
}
|
|
211
|
+
const npmPath = await which("npm");
|
|
212
|
+
if (npmPath) {
|
|
213
|
+
return { cmd: npmPath, args: ["install", "-g"], label: "npm" };
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Install global Node.js packages (prefers bun > npm)
|
|
220
|
+
*/
|
|
221
|
+
async function installNpmPackages(
|
|
222
|
+
packages: string[],
|
|
223
|
+
result: SetupResult,
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
const installer = await detectNpmCommand();
|
|
226
|
+
if (!installer) {
|
|
227
|
+
for (const pkg of packages) {
|
|
228
|
+
result.failed.push({ pkg: `npm:${pkg}`, error: "No bun or npm found" });
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const pkg of packages) {
|
|
234
|
+
if (await isBinaryAvailable(pkg)) {
|
|
235
|
+
result.skipped.push(`npm:${pkg}`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const { ok, stderr } = await run(installer.cmd, [...installer.args, pkg]);
|
|
240
|
+
if (ok) {
|
|
241
|
+
result.installed.push(`${installer.label}:${pkg}`);
|
|
242
|
+
} else {
|
|
243
|
+
result.failed.push({ pkg: `${installer.label}:${pkg}`, error: stderr });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Install cargo packages
|
|
250
|
+
*/
|
|
251
|
+
async function installCargoPackages(
|
|
252
|
+
packages: string[],
|
|
253
|
+
result: SetupResult,
|
|
254
|
+
): Promise<void> {
|
|
255
|
+
const cargoPath = await which("cargo");
|
|
256
|
+
if (!cargoPath) {
|
|
257
|
+
for (const pkg of packages) {
|
|
258
|
+
result.failed.push({ pkg: `cargo:${pkg}`, error: "cargo not found" });
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const pkg of packages) {
|
|
264
|
+
if (await isBinaryAvailable(pkg)) {
|
|
265
|
+
result.skipped.push(`cargo:${pkg}`);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const { ok, stderr } = await run(cargoPath, ["install", pkg], 300000);
|
|
270
|
+
if (ok) {
|
|
271
|
+
result.installed.push(`cargo:${pkg}`);
|
|
272
|
+
} else {
|
|
273
|
+
result.failed.push({ pkg: `cargo:${pkg}`, error: stderr });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Read the setup config from a plugin's cached manifest
|
|
280
|
+
*/
|
|
281
|
+
export async function getPluginSetupConfig(
|
|
282
|
+
marketplace: string,
|
|
283
|
+
pluginName: string,
|
|
284
|
+
): Promise<SetupConfig | null> {
|
|
285
|
+
const cacheDir = path.join(
|
|
286
|
+
os.homedir(),
|
|
287
|
+
".claude",
|
|
288
|
+
"plugins",
|
|
289
|
+
"cache",
|
|
290
|
+
marketplace,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Find the plugin directory (versioned)
|
|
294
|
+
if (!(await fs.pathExists(cacheDir))) return null;
|
|
295
|
+
|
|
296
|
+
const entries = await fs.readdir(cacheDir);
|
|
297
|
+
// Look for the plugin name directory
|
|
298
|
+
const pluginDir = entries.find((e) => e === pluginName);
|
|
299
|
+
if (!pluginDir) return null;
|
|
300
|
+
|
|
301
|
+
const pluginPath = path.join(cacheDir, pluginDir);
|
|
302
|
+
const stat = await fs.stat(pluginPath);
|
|
303
|
+
if (!stat.isDirectory()) return null;
|
|
304
|
+
|
|
305
|
+
// Find the version directory
|
|
306
|
+
const versions = await fs.readdir(pluginPath);
|
|
307
|
+
if (versions.length === 0) return null;
|
|
308
|
+
|
|
309
|
+
// Use the latest version directory
|
|
310
|
+
const latestVersion = versions.sort().pop()!;
|
|
311
|
+
const manifestPath = path.join(pluginPath, latestVersion, "plugin.json");
|
|
312
|
+
|
|
313
|
+
if (!(await fs.pathExists(manifestPath))) return null;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const manifest = await fs.readJson(manifestPath);
|
|
317
|
+
return manifest.setup || null;
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Read setup config from a local plugin directory (marketplace source)
|
|
325
|
+
*/
|
|
326
|
+
export async function getPluginSetupFromSource(
|
|
327
|
+
marketplace: string,
|
|
328
|
+
pluginName: string,
|
|
329
|
+
): Promise<SetupConfig | null> {
|
|
330
|
+
const marketplaceDir = path.join(
|
|
331
|
+
os.homedir(),
|
|
332
|
+
".claude",
|
|
333
|
+
"plugins",
|
|
334
|
+
"marketplaces",
|
|
335
|
+
marketplace,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (!(await fs.pathExists(marketplaceDir))) return null;
|
|
339
|
+
|
|
340
|
+
// Try common plugin locations
|
|
341
|
+
for (const subdir of ["plugins", ""]) {
|
|
342
|
+
const pluginJson = subdir
|
|
343
|
+
? path.join(marketplaceDir, subdir, pluginName, "plugin.json")
|
|
344
|
+
: path.join(marketplaceDir, pluginName, "plugin.json");
|
|
345
|
+
|
|
346
|
+
if (await fs.pathExists(pluginJson)) {
|
|
347
|
+
try {
|
|
348
|
+
const manifest = await fs.readJson(pluginJson);
|
|
349
|
+
return manifest.setup || null;
|
|
350
|
+
} catch {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Install all dependencies declared in a plugin's setup config.
|
|
361
|
+
* Auto-detects available package managers.
|
|
362
|
+
*/
|
|
363
|
+
export async function installPluginDeps(
|
|
364
|
+
setup: SetupConfig,
|
|
365
|
+
): Promise<SetupResult> {
|
|
366
|
+
const result: SetupResult = {
|
|
367
|
+
installed: [],
|
|
368
|
+
skipped: [],
|
|
369
|
+
failed: [],
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
if (setup.pip?.length) {
|
|
373
|
+
await installPipPackages(setup.pip, result);
|
|
374
|
+
}
|
|
375
|
+
if (setup.brew?.length) {
|
|
376
|
+
await installBrewPackages(setup.brew, result);
|
|
377
|
+
}
|
|
378
|
+
if (setup.npm?.length) {
|
|
379
|
+
await installNpmPackages(setup.npm, result);
|
|
380
|
+
}
|
|
381
|
+
if (setup.cargo?.length) {
|
|
382
|
+
await installCargoPackages(setup.cargo, result);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Check which dependencies from a setup config are missing.
|
|
390
|
+
* Returns a filtered SetupConfig with only missing deps.
|
|
391
|
+
*/
|
|
392
|
+
export async function checkMissingDeps(
|
|
393
|
+
setup: SetupConfig,
|
|
394
|
+
): Promise<SetupConfig> {
|
|
395
|
+
const missing: SetupConfig = {};
|
|
396
|
+
|
|
397
|
+
if (setup.pip?.length) {
|
|
398
|
+
const missingPip: string[] = [];
|
|
399
|
+
for (const pkg of setup.pip) {
|
|
400
|
+
if (!(await isPythonPackageInstalled(pkg))) {
|
|
401
|
+
missingPip.push(pkg);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (missingPip.length > 0) missing.pip = missingPip;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (setup.brew?.length) {
|
|
408
|
+
const missingBrew: string[] = [];
|
|
409
|
+
for (const pkg of setup.brew) {
|
|
410
|
+
const bin = extractBinaryName(pkg);
|
|
411
|
+
if (!(await isBinaryAvailable(bin))) {
|
|
412
|
+
missingBrew.push(pkg);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (missingBrew.length > 0) missing.brew = missingBrew;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (setup.npm?.length) {
|
|
419
|
+
const missingNpm: string[] = [];
|
|
420
|
+
for (const pkg of setup.npm) {
|
|
421
|
+
if (!(await isBinaryAvailable(pkg))) {
|
|
422
|
+
missingNpm.push(pkg);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (missingNpm.length > 0) missing.npm = missingNpm;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (setup.cargo?.length) {
|
|
429
|
+
const missingCargo: string[] = [];
|
|
430
|
+
for (const pkg of setup.cargo) {
|
|
431
|
+
if (!(await isBinaryAvailable(pkg))) {
|
|
432
|
+
missingCargo.push(pkg);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (missingCargo.length > 0) missing.cargo = missingCargo;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return missing;
|
|
439
|
+
}
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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();
|