claudeup 4.0.1 → 4.2.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 +1 -1
- package/src/data/cli-tools.js +7 -7
- package/src/data/cli-tools.ts +8 -9
- package/src/data/marketplaces.js +6 -2
- package/src/data/marketplaces.ts +10 -3
- package/src/prerunner/index.js +71 -7
- package/src/prerunner/index.ts +94 -6
- package/src/services/claude-settings.js +79 -12
- package/src/services/claude-settings.ts +101 -19
- package/src/types/index.ts +33 -0
- package/src/ui/renderers/cliToolRenderers.js +28 -7
- package/src/ui/renderers/cliToolRenderers.tsx +104 -30
- package/src/ui/renderers/pluginRenderers.js +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +3 -5
- package/src/ui/renderers/profileRenderers.js +4 -4
- package/src/ui/renderers/profileRenderers.tsx +10 -12
- package/src/ui/screens/CliToolsScreen.js +152 -49
- package/src/ui/screens/CliToolsScreen.tsx +176 -51
- package/src/ui/screens/PluginsScreen.js +27 -0
- package/src/ui/screens/PluginsScreen.tsx +25 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import { useEffect, useCallback, useState, useRef } from "react";
|
|
3
|
-
import { exec
|
|
3
|
+
import { exec } from "child_process";
|
|
4
4
|
import { promisify } from "util";
|
|
5
5
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
6
6
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
@@ -22,17 +22,94 @@ function parseVersion(versionOutput) {
|
|
|
22
22
|
const match = versionOutput.match(/v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
|
|
23
23
|
return match ? match[1] : undefined;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
function methodFromPath(binPath) {
|
|
26
|
+
if (binPath.includes("/.bun/"))
|
|
27
|
+
return "bun";
|
|
28
|
+
if (binPath.includes("/homebrew/") || binPath.includes("/Cellar/"))
|
|
29
|
+
return "brew";
|
|
30
|
+
if (binPath.includes("/.local/share/claude") || binPath.includes("/.local/bin/claude"))
|
|
31
|
+
return "npm";
|
|
32
|
+
if (binPath.includes("/.nvm/") || binPath.includes("/node_modules/"))
|
|
33
|
+
return "npm";
|
|
34
|
+
if (binPath.includes("/.local/share/pnpm") || binPath.includes("/pnpm/"))
|
|
35
|
+
return "pnpm";
|
|
36
|
+
if (binPath.includes("/.yarn/"))
|
|
37
|
+
return "yarn";
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Extract brew formula name from a Cellar symlink target like ../Cellar/gemini-cli/0.35.2/bin/gemini */
|
|
41
|
+
function extractBrewFormula(binPath, linkTarget) {
|
|
42
|
+
// Try symlink target first: ../Cellar/{formula}/{version}/...
|
|
43
|
+
const cellarMatch = linkTarget.match(/Cellar\/([^/]+)\//);
|
|
44
|
+
if (cellarMatch)
|
|
45
|
+
return cellarMatch[1];
|
|
46
|
+
// Try binary path: /opt/homebrew/Cellar/{formula}/...
|
|
47
|
+
const pathMatch = binPath.match(/Cellar\/([^/]+)\//);
|
|
48
|
+
if (pathMatch)
|
|
49
|
+
return pathMatch[1];
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
async function detectInstallMethods(tool) {
|
|
53
|
+
const fallback = tool.packageManager === "pip" ? "pip" : "unknown";
|
|
26
54
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
55
|
+
// which -a returns all matching binaries across PATH
|
|
56
|
+
const { stdout } = await execAsync(`which -a ${tool.name} 2>/dev/null`, {
|
|
57
|
+
timeout: 3000,
|
|
29
58
|
shell: "/bin/bash",
|
|
30
59
|
});
|
|
31
|
-
const
|
|
32
|
-
|
|
60
|
+
const paths = stdout.trim().split("\n").filter((p) => p && !p.includes("aliased"));
|
|
61
|
+
if (paths.length === 0)
|
|
62
|
+
return { primary: fallback, all: [] };
|
|
63
|
+
const methods = [];
|
|
64
|
+
let brewFormula;
|
|
65
|
+
for (const binPath of paths) {
|
|
66
|
+
let method = methodFromPath(binPath);
|
|
67
|
+
let linkTarget = "";
|
|
68
|
+
// Check symlink target
|
|
69
|
+
try {
|
|
70
|
+
const { stdout: lt } = await execAsync(`readlink "${binPath}" 2>/dev/null || true`, { timeout: 2000, shell: "/bin/bash" });
|
|
71
|
+
linkTarget = lt.trim();
|
|
72
|
+
if (!method)
|
|
73
|
+
method = methodFromPath(linkTarget);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// ignore
|
|
77
|
+
}
|
|
78
|
+
// Detect brew formula name
|
|
79
|
+
if (method === "brew" && !brewFormula) {
|
|
80
|
+
brewFormula = extractBrewFormula(binPath, linkTarget);
|
|
81
|
+
}
|
|
82
|
+
if (method && !methods.includes(method))
|
|
83
|
+
methods.push(method);
|
|
84
|
+
}
|
|
85
|
+
if (methods.length === 0)
|
|
86
|
+
return { primary: fallback, all: [] };
|
|
87
|
+
return { primary: methods[0], all: methods, brewFormula };
|
|
33
88
|
}
|
|
34
89
|
catch {
|
|
35
|
-
return
|
|
90
|
+
return { primary: fallback, all: [] };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function getUninstallCommand(tool, method, brewFormula) {
|
|
94
|
+
switch (method) {
|
|
95
|
+
case "bun": return `bun remove -g ${tool.packageName}`;
|
|
96
|
+
case "npm": return `npm uninstall -g ${tool.packageName}`;
|
|
97
|
+
case "pnpm": return `pnpm remove -g ${tool.packageName}`;
|
|
98
|
+
case "yarn": return `yarn global remove ${tool.packageName}`;
|
|
99
|
+
case "brew": return `brew uninstall ${brewFormula || tool.name}`;
|
|
100
|
+
case "pip": return `pip uninstall -y ${tool.packageName}`;
|
|
101
|
+
default: return "";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function getUpdateCommand(tool, method, brewFormula) {
|
|
105
|
+
switch (method) {
|
|
106
|
+
case "bun": return `bun install -g ${tool.packageName}`;
|
|
107
|
+
case "npm": return `npm install -g ${tool.packageName}`;
|
|
108
|
+
case "pnpm": return `pnpm install -g ${tool.packageName}`;
|
|
109
|
+
case "yarn": return `yarn global add ${tool.packageName}`;
|
|
110
|
+
case "brew": return `brew upgrade ${brewFormula || tool.name}`;
|
|
111
|
+
case "pip": return tool.installCommand;
|
|
112
|
+
default: return tool.installCommand;
|
|
36
113
|
}
|
|
37
114
|
}
|
|
38
115
|
async function getInstalledVersion(tool) {
|
|
@@ -109,20 +186,24 @@ export function CliToolsScreen() {
|
|
|
109
186
|
const fetchVersionInfo = useCallback(async () => {
|
|
110
187
|
for (let i = 0; i < cliTools.length; i++) {
|
|
111
188
|
const tool = cliTools[i];
|
|
112
|
-
|
|
189
|
+
// Run all checks in parallel, then update with all results
|
|
190
|
+
Promise.all([
|
|
191
|
+
getInstalledVersion(tool),
|
|
192
|
+
getLatestVersion(tool),
|
|
193
|
+
detectInstallMethods(tool),
|
|
194
|
+
]).then(([version, latest, info]) => {
|
|
113
195
|
updateToolStatus(i, {
|
|
114
196
|
installedVersion: version,
|
|
115
197
|
installed: version !== undefined,
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
getLatestVersion(tool).then((latest) => {
|
|
119
|
-
const current = statusesRef.current[i];
|
|
120
|
-
updateToolStatus(i, {
|
|
121
198
|
latestVersion: latest,
|
|
122
199
|
checking: false,
|
|
123
|
-
hasUpdate:
|
|
124
|
-
? compareVersions(
|
|
200
|
+
hasUpdate: version && latest
|
|
201
|
+
? compareVersions(version, latest) < 0
|
|
125
202
|
: false,
|
|
203
|
+
installMethod: version ? info.primary : undefined,
|
|
204
|
+
allMethods: version && info.all.length > 1 ? info.all : undefined,
|
|
205
|
+
updateCommand: version ? getUpdateCommand(tool, info.primary, info.brewFormula) : undefined,
|
|
206
|
+
brewFormula: info.brewFormula,
|
|
126
207
|
});
|
|
127
208
|
});
|
|
128
209
|
}
|
|
@@ -151,7 +232,10 @@ export function CliToolsScreen() {
|
|
|
151
232
|
else if (event.name === "a") {
|
|
152
233
|
handleUpdateAll();
|
|
153
234
|
}
|
|
154
|
-
else if (event.name === "
|
|
235
|
+
else if (event.name === "c") {
|
|
236
|
+
handleResolveConflict();
|
|
237
|
+
}
|
|
238
|
+
else if (event.name === "enter" || event.name === "return") {
|
|
155
239
|
handleInstall();
|
|
156
240
|
}
|
|
157
241
|
});
|
|
@@ -166,35 +250,66 @@ export function CliToolsScreen() {
|
|
|
166
250
|
cacheInitialized = false;
|
|
167
251
|
fetchVersionInfo();
|
|
168
252
|
};
|
|
253
|
+
const runCommand = async (command) => {
|
|
254
|
+
try {
|
|
255
|
+
await execAsync(command, {
|
|
256
|
+
shell: "/bin/bash",
|
|
257
|
+
timeout: 60000,
|
|
258
|
+
});
|
|
259
|
+
return { ok: true };
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
263
|
+
return { ok: false, error: msg };
|
|
264
|
+
}
|
|
265
|
+
};
|
|
169
266
|
const handleInstall = async () => {
|
|
170
267
|
const status = toolStatuses[cliToolsState.selectedIndex];
|
|
171
268
|
if (!status)
|
|
172
269
|
return;
|
|
173
|
-
const { tool, installed, hasUpdate } = status;
|
|
270
|
+
const { tool, installed, hasUpdate, updateCommand } = status;
|
|
174
271
|
const action = !installed ? "Installing" : hasUpdate ? "Updating" : "Reinstalling";
|
|
175
|
-
const
|
|
176
|
-
? await isInstalledViaHomebrew(tool.name)
|
|
177
|
-
: false;
|
|
178
|
-
const command = !installed
|
|
179
|
-
? tool.installCommand
|
|
180
|
-
: viaHomebrew
|
|
181
|
-
? `brew upgrade ${tool.name}`
|
|
182
|
-
: tool.installCommand;
|
|
272
|
+
const command = installed && updateCommand ? updateCommand : tool.installCommand;
|
|
183
273
|
modal.loading(`${action} ${tool.displayName}...`);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
stdio: "pipe",
|
|
188
|
-
shell: "/bin/bash",
|
|
189
|
-
});
|
|
190
|
-
modal.hideModal();
|
|
274
|
+
const result = await runCommand(command);
|
|
275
|
+
modal.hideModal();
|
|
276
|
+
if (result.ok) {
|
|
191
277
|
handleRefresh();
|
|
192
278
|
}
|
|
193
|
-
|
|
194
|
-
modal.hideModal();
|
|
279
|
+
else {
|
|
195
280
|
await modal.message("Error", `Failed to ${action.toLowerCase()} ${tool.displayName}.\n\nTry running manually:\n${command}`, "error");
|
|
196
281
|
}
|
|
197
282
|
};
|
|
283
|
+
const handleResolveConflict = async () => {
|
|
284
|
+
const status = toolStatuses[cliToolsState.selectedIndex];
|
|
285
|
+
if (!status || !status.allMethods || status.allMethods.length < 2)
|
|
286
|
+
return;
|
|
287
|
+
const { tool, allMethods, installMethod, brewFormula } = status;
|
|
288
|
+
// Let user pick which install to keep
|
|
289
|
+
const options = allMethods.map((method) => ({
|
|
290
|
+
label: `Keep ${method}${method === installMethod ? " (active)" : ""}, remove others`,
|
|
291
|
+
value: method,
|
|
292
|
+
}));
|
|
293
|
+
const keep = await modal.select(`Resolve ${tool.displayName} conflict`, `Installed via ${allMethods.join(" + ")}. Which to keep?`, options);
|
|
294
|
+
if (keep === null)
|
|
295
|
+
return;
|
|
296
|
+
const toRemove = allMethods.filter((m) => m !== keep);
|
|
297
|
+
modal.loading(`Removing ${toRemove.join(", ")} install(s)...`);
|
|
298
|
+
const errors = [];
|
|
299
|
+
for (const method of toRemove) {
|
|
300
|
+
const cmd = getUninstallCommand(tool, method, brewFormula);
|
|
301
|
+
if (!cmd)
|
|
302
|
+
continue;
|
|
303
|
+
const result = await runCommand(cmd);
|
|
304
|
+
if (!result.ok)
|
|
305
|
+
errors.push(`${method}: ${cmd}`);
|
|
306
|
+
}
|
|
307
|
+
modal.hideModal();
|
|
308
|
+
if (errors.length > 0) {
|
|
309
|
+
await modal.message("Partial", `Some removals failed. Try manually:\n\n${errors.join("\n")}`, "error");
|
|
310
|
+
}
|
|
311
|
+
handleRefresh();
|
|
312
|
+
};
|
|
198
313
|
const handleUpdateAll = async () => {
|
|
199
314
|
const updatable = toolStatuses.filter((s) => s.hasUpdate);
|
|
200
315
|
if (updatable.length === 0) {
|
|
@@ -203,20 +318,8 @@ export function CliToolsScreen() {
|
|
|
203
318
|
}
|
|
204
319
|
modal.loading(`Updating ${updatable.length} tool(s)...`);
|
|
205
320
|
for (const status of updatable) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
? `brew upgrade ${status.tool.name}`
|
|
209
|
-
: status.tool.installCommand;
|
|
210
|
-
try {
|
|
211
|
-
execSync(command, {
|
|
212
|
-
encoding: "utf-8",
|
|
213
|
-
stdio: "pipe",
|
|
214
|
-
shell: "/bin/bash",
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
// Continue with other updates
|
|
219
|
-
}
|
|
321
|
+
const command = status.updateCommand || status.tool.installCommand;
|
|
322
|
+
await runCommand(command);
|
|
220
323
|
}
|
|
221
324
|
modal.hideModal();
|
|
222
325
|
handleRefresh();
|
|
@@ -225,6 +328,6 @@ export function CliToolsScreen() {
|
|
|
225
328
|
const installedCount = toolStatuses.filter((s) => s.installed).length;
|
|
226
329
|
const updateCount = toolStatuses.filter((s) => s.hasUpdate).length;
|
|
227
330
|
const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Installed: " }), _jsxs("span", { fg: "cyan", children: [installedCount, "/", toolStatuses.length] }), updateCount > 0 && (_jsxs(_Fragment, { children: [_jsx("span", { fg: "gray", children: " \u2502 Updates: " }), _jsx("span", { fg: "yellow", children: updateCount })] }))] }));
|
|
228
|
-
return (_jsx(ScreenLayout, { title: "claudeup CLI Tools", currentScreen: "cli-tools", statusLine: statusContent, footerHints: "
|
|
331
|
+
return (_jsx(ScreenLayout, { title: "claudeup CLI Tools", currentScreen: "cli-tools", statusLine: statusContent, footerHints: "Enter:install \u2502 a:update all \u2502 c:fix conflict \u2502 r:refresh", listPanel: _jsx(ScrollableList, { items: toolStatuses, selectedIndex: cliToolsState.selectedIndex, renderItem: renderCliToolRow, maxHeight: dimensions.listPanelHeight }), detailPanel: renderCliToolDetail(selectedStatus) }));
|
|
229
332
|
}
|
|
230
333
|
export default CliToolsScreen;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useCallback, useState, useRef } from "react";
|
|
2
|
-
import { exec
|
|
2
|
+
import { exec } from "child_process";
|
|
3
3
|
import { promisify } from "util";
|
|
4
4
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
5
5
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
renderCliToolRow,
|
|
12
12
|
renderCliToolDetail,
|
|
13
13
|
type CliToolStatus,
|
|
14
|
+
type InstallMethod,
|
|
14
15
|
} from "../renderers/cliToolRenderers.js";
|
|
15
16
|
|
|
16
17
|
const execAsync = promisify(exec);
|
|
@@ -31,19 +32,105 @@ function parseVersion(versionOutput: string): string | undefined {
|
|
|
31
32
|
return match ? match[1] : undefined;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
function methodFromPath(binPath: string): InstallMethod | null {
|
|
36
|
+
if (binPath.includes("/.bun/")) return "bun";
|
|
37
|
+
if (binPath.includes("/homebrew/") || binPath.includes("/Cellar/")) return "brew";
|
|
38
|
+
if (binPath.includes("/.local/share/claude") || binPath.includes("/.local/bin/claude")) return "npm";
|
|
39
|
+
if (binPath.includes("/.nvm/") || binPath.includes("/node_modules/")) return "npm";
|
|
40
|
+
if (binPath.includes("/.local/share/pnpm") || binPath.includes("/pnpm/")) return "pnpm";
|
|
41
|
+
if (binPath.includes("/.yarn/")) return "yarn";
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface InstallInfo {
|
|
46
|
+
primary: InstallMethod;
|
|
47
|
+
all: InstallMethod[];
|
|
48
|
+
brewFormula?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Extract brew formula name from a Cellar symlink target like ../Cellar/gemini-cli/0.35.2/bin/gemini */
|
|
52
|
+
function extractBrewFormula(binPath: string, linkTarget: string): string | undefined {
|
|
53
|
+
// Try symlink target first: ../Cellar/{formula}/{version}/...
|
|
54
|
+
const cellarMatch = linkTarget.match(/Cellar\/([^/]+)\//);
|
|
55
|
+
if (cellarMatch) return cellarMatch[1];
|
|
56
|
+
// Try binary path: /opt/homebrew/Cellar/{formula}/...
|
|
57
|
+
const pathMatch = binPath.match(/Cellar\/([^/]+)\//);
|
|
58
|
+
if (pathMatch) return pathMatch[1];
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function detectInstallMethods(
|
|
63
|
+
tool: import("../../data/cli-tools.js").CliTool,
|
|
64
|
+
): Promise<InstallInfo> {
|
|
65
|
+
const fallback = tool.packageManager === "pip" ? "pip" : "unknown";
|
|
35
66
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
67
|
+
// which -a returns all matching binaries across PATH
|
|
68
|
+
const { stdout } = await execAsync(`which -a ${tool.name} 2>/dev/null`, {
|
|
69
|
+
timeout: 3000,
|
|
38
70
|
shell: "/bin/bash",
|
|
39
71
|
});
|
|
40
|
-
const
|
|
41
|
-
|
|
72
|
+
const paths = stdout.trim().split("\n").filter((p) => p && !p.includes("aliased"));
|
|
73
|
+
if (paths.length === 0) return { primary: fallback as InstallMethod, all: [] };
|
|
74
|
+
|
|
75
|
+
const methods: InstallMethod[] = [];
|
|
76
|
+
let brewFormula: string | undefined;
|
|
77
|
+
|
|
78
|
+
for (const binPath of paths) {
|
|
79
|
+
let method = methodFromPath(binPath);
|
|
80
|
+
let linkTarget = "";
|
|
81
|
+
|
|
82
|
+
// Check symlink target
|
|
83
|
+
try {
|
|
84
|
+
const { stdout: lt } = await execAsync(
|
|
85
|
+
`readlink "${binPath}" 2>/dev/null || true`,
|
|
86
|
+
{ timeout: 2000, shell: "/bin/bash" },
|
|
87
|
+
);
|
|
88
|
+
linkTarget = lt.trim();
|
|
89
|
+
if (!method) method = methodFromPath(linkTarget);
|
|
90
|
+
} catch {
|
|
91
|
+
// ignore
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Detect brew formula name
|
|
95
|
+
if (method === "brew" && !brewFormula) {
|
|
96
|
+
brewFormula = extractBrewFormula(binPath, linkTarget);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (method && !methods.includes(method)) methods.push(method);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (methods.length === 0) return { primary: fallback as InstallMethod, all: [] };
|
|
103
|
+
return { primary: methods[0]!, all: methods, brewFormula };
|
|
42
104
|
} catch {
|
|
43
|
-
return
|
|
105
|
+
return { primary: fallback as InstallMethod, all: [] };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getUninstallCommand(tool: import("../../data/cli-tools.js").CliTool, method: InstallMethod, brewFormula?: string): string {
|
|
110
|
+
switch (method) {
|
|
111
|
+
case "bun": return `bun remove -g ${tool.packageName}`;
|
|
112
|
+
case "npm": return `npm uninstall -g ${tool.packageName}`;
|
|
113
|
+
case "pnpm": return `pnpm remove -g ${tool.packageName}`;
|
|
114
|
+
case "yarn": return `yarn global remove ${tool.packageName}`;
|
|
115
|
+
case "brew": return `brew uninstall ${brewFormula || tool.name}`;
|
|
116
|
+
case "pip": return `pip uninstall -y ${tool.packageName}`;
|
|
117
|
+
default: return "";
|
|
44
118
|
}
|
|
45
119
|
}
|
|
46
120
|
|
|
121
|
+
function getUpdateCommand(tool: import("../../data/cli-tools.js").CliTool, method: InstallMethod, brewFormula?: string): string {
|
|
122
|
+
switch (method) {
|
|
123
|
+
case "bun": return `bun install -g ${tool.packageName}`;
|
|
124
|
+
case "npm": return `npm install -g ${tool.packageName}`;
|
|
125
|
+
case "pnpm": return `pnpm install -g ${tool.packageName}`;
|
|
126
|
+
case "yarn": return `yarn global add ${tool.packageName}`;
|
|
127
|
+
case "brew": return `brew upgrade ${brewFormula || tool.name}`;
|
|
128
|
+
case "pip": return tool.installCommand;
|
|
129
|
+
default: return tool.installCommand;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
47
134
|
async function getInstalledVersion(
|
|
48
135
|
tool: import("../../data/cli-tools.js").CliTool,
|
|
49
136
|
): Promise<string | undefined> {
|
|
@@ -138,22 +225,26 @@ export function CliToolsScreen() {
|
|
|
138
225
|
const fetchVersionInfo = useCallback(async () => {
|
|
139
226
|
for (let i = 0; i < cliTools.length; i++) {
|
|
140
227
|
const tool = cliTools[i]!;
|
|
141
|
-
|
|
228
|
+
|
|
229
|
+
// Run all checks in parallel, then update with all results
|
|
230
|
+
Promise.all([
|
|
231
|
+
getInstalledVersion(tool),
|
|
232
|
+
getLatestVersion(tool),
|
|
233
|
+
detectInstallMethods(tool),
|
|
234
|
+
]).then(([version, latest, info]) => {
|
|
142
235
|
updateToolStatus(i, {
|
|
143
236
|
installedVersion: version,
|
|
144
237
|
installed: version !== undefined,
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
getLatestVersion(tool).then((latest) => {
|
|
149
|
-
const current = statusesRef.current[i]!;
|
|
150
|
-
updateToolStatus(i, {
|
|
151
238
|
latestVersion: latest,
|
|
152
239
|
checking: false,
|
|
153
240
|
hasUpdate:
|
|
154
|
-
|
|
155
|
-
? compareVersions(
|
|
241
|
+
version && latest
|
|
242
|
+
? compareVersions(version, latest) < 0
|
|
156
243
|
: false,
|
|
244
|
+
installMethod: version ? info.primary : undefined,
|
|
245
|
+
allMethods: version && info.all.length > 1 ? info.all : undefined,
|
|
246
|
+
updateCommand: version ? getUpdateCommand(tool, info.primary, info.brewFormula) : undefined,
|
|
247
|
+
brewFormula: info.brewFormula,
|
|
157
248
|
});
|
|
158
249
|
});
|
|
159
250
|
}
|
|
@@ -183,7 +274,9 @@ export function CliToolsScreen() {
|
|
|
183
274
|
handleRefresh();
|
|
184
275
|
} else if (event.name === "a") {
|
|
185
276
|
handleUpdateAll();
|
|
186
|
-
} else if (event.name === "
|
|
277
|
+
} else if (event.name === "c") {
|
|
278
|
+
handleResolveConflict();
|
|
279
|
+
} else if (event.name === "enter" || event.name === "return") {
|
|
187
280
|
handleInstall();
|
|
188
281
|
}
|
|
189
282
|
});
|
|
@@ -202,34 +295,34 @@ export function CliToolsScreen() {
|
|
|
202
295
|
fetchVersionInfo();
|
|
203
296
|
};
|
|
204
297
|
|
|
298
|
+
const runCommand = async (command: string): Promise<{ ok: boolean; error?: string }> => {
|
|
299
|
+
try {
|
|
300
|
+
await execAsync(command, {
|
|
301
|
+
shell: "/bin/bash",
|
|
302
|
+
timeout: 60000,
|
|
303
|
+
});
|
|
304
|
+
return { ok: true };
|
|
305
|
+
} catch (err) {
|
|
306
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
307
|
+
return { ok: false, error: msg };
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
205
311
|
const handleInstall = async () => {
|
|
206
312
|
const status = toolStatuses[cliToolsState.selectedIndex];
|
|
207
313
|
if (!status) return;
|
|
208
314
|
|
|
209
|
-
const { tool, installed, hasUpdate } = status;
|
|
315
|
+
const { tool, installed, hasUpdate, updateCommand } = status;
|
|
210
316
|
const action = !installed ? "Installing" : hasUpdate ? "Updating" : "Reinstalling";
|
|
211
|
-
|
|
212
|
-
const viaHomebrew = installed
|
|
213
|
-
? await isInstalledViaHomebrew(tool.name)
|
|
214
|
-
: false;
|
|
215
|
-
|
|
216
|
-
const command = !installed
|
|
217
|
-
? tool.installCommand
|
|
218
|
-
: viaHomebrew
|
|
219
|
-
? `brew upgrade ${tool.name}`
|
|
220
|
-
: tool.installCommand;
|
|
317
|
+
const command = installed && updateCommand ? updateCommand : tool.installCommand;
|
|
221
318
|
|
|
222
319
|
modal.loading(`${action} ${tool.displayName}...`);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
shell: "/bin/bash",
|
|
228
|
-
});
|
|
229
|
-
modal.hideModal();
|
|
320
|
+
const result = await runCommand(command);
|
|
321
|
+
modal.hideModal();
|
|
322
|
+
|
|
323
|
+
if (result.ok) {
|
|
230
324
|
handleRefresh();
|
|
231
|
-
}
|
|
232
|
-
modal.hideModal();
|
|
325
|
+
} else {
|
|
233
326
|
await modal.message(
|
|
234
327
|
"Error",
|
|
235
328
|
`Failed to ${action.toLowerCase()} ${tool.displayName}.\n\nTry running manually:\n${command}`,
|
|
@@ -238,6 +331,49 @@ export function CliToolsScreen() {
|
|
|
238
331
|
}
|
|
239
332
|
};
|
|
240
333
|
|
|
334
|
+
const handleResolveConflict = async () => {
|
|
335
|
+
const status = toolStatuses[cliToolsState.selectedIndex];
|
|
336
|
+
if (!status || !status.allMethods || status.allMethods.length < 2) return;
|
|
337
|
+
|
|
338
|
+
const { tool, allMethods, installMethod, brewFormula } = status;
|
|
339
|
+
|
|
340
|
+
// Let user pick which install to keep
|
|
341
|
+
const options = allMethods.map((method) => ({
|
|
342
|
+
label: `Keep ${method}${method === installMethod ? " (active)" : ""}, remove others`,
|
|
343
|
+
value: method,
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
const keep = await modal.select(
|
|
347
|
+
`Resolve ${tool.displayName} conflict`,
|
|
348
|
+
`Installed via ${allMethods.join(" + ")}. Which to keep?`,
|
|
349
|
+
options,
|
|
350
|
+
);
|
|
351
|
+
if (keep === null) return;
|
|
352
|
+
|
|
353
|
+
const toRemove = allMethods.filter((m) => m !== keep);
|
|
354
|
+
modal.loading(`Removing ${toRemove.join(", ")} install(s)...`);
|
|
355
|
+
|
|
356
|
+
const errors: string[] = [];
|
|
357
|
+
for (const method of toRemove) {
|
|
358
|
+
const cmd = getUninstallCommand(tool, method, brewFormula);
|
|
359
|
+
if (!cmd) continue;
|
|
360
|
+
const result = await runCommand(cmd);
|
|
361
|
+
if (!result.ok) errors.push(`${method}: ${cmd}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
modal.hideModal();
|
|
365
|
+
|
|
366
|
+
if (errors.length > 0) {
|
|
367
|
+
await modal.message(
|
|
368
|
+
"Partial",
|
|
369
|
+
`Some removals failed. Try manually:\n\n${errors.join("\n")}`,
|
|
370
|
+
"error",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
handleRefresh();
|
|
375
|
+
};
|
|
376
|
+
|
|
241
377
|
const handleUpdateAll = async () => {
|
|
242
378
|
const updatable = toolStatuses.filter((s) => s.hasUpdate);
|
|
243
379
|
if (updatable.length === 0) {
|
|
@@ -248,19 +384,8 @@ export function CliToolsScreen() {
|
|
|
248
384
|
modal.loading(`Updating ${updatable.length} tool(s)...`);
|
|
249
385
|
|
|
250
386
|
for (const status of updatable) {
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
? `brew upgrade ${status.tool.name}`
|
|
254
|
-
: status.tool.installCommand;
|
|
255
|
-
try {
|
|
256
|
-
execSync(command, {
|
|
257
|
-
encoding: "utf-8",
|
|
258
|
-
stdio: "pipe",
|
|
259
|
-
shell: "/bin/bash",
|
|
260
|
-
});
|
|
261
|
-
} catch {
|
|
262
|
-
// Continue with other updates
|
|
263
|
-
}
|
|
387
|
+
const command = status.updateCommand || status.tool.installCommand;
|
|
388
|
+
await runCommand(command);
|
|
264
389
|
}
|
|
265
390
|
|
|
266
391
|
modal.hideModal();
|
|
@@ -291,7 +416,7 @@ export function CliToolsScreen() {
|
|
|
291
416
|
title="claudeup CLI Tools"
|
|
292
417
|
currentScreen="cli-tools"
|
|
293
418
|
statusLine={statusContent}
|
|
294
|
-
footerHints="
|
|
419
|
+
footerHints="Enter:install │ a:update all │ c:fix conflict │ r:refresh"
|
|
295
420
|
listPanel={
|
|
296
421
|
<ScrollableList
|
|
297
422
|
items={toolStatuses}
|
|
@@ -198,6 +198,8 @@ export function PluginsScreen() {
|
|
|
198
198
|
handleScopeToggle("project");
|
|
199
199
|
else if (event.name === "l")
|
|
200
200
|
handleScopeToggle("local");
|
|
201
|
+
else if (event.name === "d")
|
|
202
|
+
handleRemoveDeprecated();
|
|
201
203
|
else if (event.name === "U")
|
|
202
204
|
handleUpdate();
|
|
203
205
|
else if (event.name === "a")
|
|
@@ -575,6 +577,31 @@ export function PluginsScreen() {
|
|
|
575
577
|
await modal.message("Error", `Failed: ${error}`, "error");
|
|
576
578
|
}
|
|
577
579
|
};
|
|
580
|
+
const handleRemoveDeprecated = async () => {
|
|
581
|
+
const item = selectableItems[pluginsState.selectedIndex];
|
|
582
|
+
if (!item || item.kind !== "plugin" || !item.plugin.isOrphaned)
|
|
583
|
+
return;
|
|
584
|
+
const plugin = item.plugin;
|
|
585
|
+
modal.loading(`Removing ${plugin.name}...`);
|
|
586
|
+
try {
|
|
587
|
+
// Remove from all scopes — try all to clean up stale references
|
|
588
|
+
const scopes = ["user", "project", "local"];
|
|
589
|
+
for (const scope of scopes) {
|
|
590
|
+
try {
|
|
591
|
+
await cliUninstallPlugin(plugin.id, scope, state.projectPath);
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// Ignore errors for scopes where it doesn't exist
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
modal.hideModal();
|
|
598
|
+
fetchData();
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
modal.hideModal();
|
|
602
|
+
await modal.message("Error", `Failed to remove: ${error}`, "error");
|
|
603
|
+
}
|
|
604
|
+
};
|
|
578
605
|
const handleSaveAsProfile = async () => {
|
|
579
606
|
const settings = await readSettings(state.projectPath);
|
|
580
607
|
const enabledPlugins = settings.enabledPlugins ?? {};
|
|
@@ -238,6 +238,7 @@ export function PluginsScreen() {
|
|
|
238
238
|
else if (event.name === "u") handleScopeToggle("user");
|
|
239
239
|
else if (event.name === "p") handleScopeToggle("project");
|
|
240
240
|
else if (event.name === "l") handleScopeToggle("local");
|
|
241
|
+
else if (event.name === "d") handleRemoveDeprecated();
|
|
241
242
|
else if (event.name === "U") handleUpdate();
|
|
242
243
|
else if (event.name === "a") handleUpdateAll();
|
|
243
244
|
else if (event.name === "s") handleSaveAsProfile();
|
|
@@ -696,6 +697,30 @@ export function PluginsScreen() {
|
|
|
696
697
|
}
|
|
697
698
|
};
|
|
698
699
|
|
|
700
|
+
const handleRemoveDeprecated = async () => {
|
|
701
|
+
const item = selectableItems[pluginsState.selectedIndex];
|
|
702
|
+
if (!item || item.kind !== "plugin" || !item.plugin.isOrphaned) return;
|
|
703
|
+
|
|
704
|
+
const plugin = item.plugin;
|
|
705
|
+
modal.loading(`Removing ${plugin.name}...`);
|
|
706
|
+
try {
|
|
707
|
+
// Remove from all scopes — try all to clean up stale references
|
|
708
|
+
const scopes: PluginScope[] = ["user", "project", "local"];
|
|
709
|
+
for (const scope of scopes) {
|
|
710
|
+
try {
|
|
711
|
+
await cliUninstallPlugin(plugin.id, scope, state.projectPath);
|
|
712
|
+
} catch {
|
|
713
|
+
// Ignore errors for scopes where it doesn't exist
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
modal.hideModal();
|
|
717
|
+
fetchData();
|
|
718
|
+
} catch (error) {
|
|
719
|
+
modal.hideModal();
|
|
720
|
+
await modal.message("Error", `Failed to remove: ${error}`, "error");
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
699
724
|
const handleSaveAsProfile = async () => {
|
|
700
725
|
const settings = await readSettings(state.projectPath);
|
|
701
726
|
const enabledPlugins = settings.enabledPlugins ?? {};
|