claudeup 4.1.0 → 4.3.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 +7 -7
- package/src/data/settings-catalog.js +9 -0
- package/src/data/settings-catalog.ts +12 -1
- package/src/services/claude-settings.js +79 -1
- package/src/services/claude-settings.ts +83 -1
- package/src/services/settings-manager.js +19 -2
- package/src/services/settings-manager.ts +16 -2
- 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/settingsRenderers.js +5 -3
- package/src/ui/renderers/settingsRenderers.tsx +5 -3
- 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
|
@@ -4,6 +4,8 @@ import { theme } from "../theme.js";
|
|
|
4
4
|
|
|
5
5
|
// ─── Status type ───────────────────────────────────────────────────────────────
|
|
6
6
|
|
|
7
|
+
export type InstallMethod = "npm" | "bun" | "pnpm" | "yarn" | "brew" | "pip" | "unknown";
|
|
8
|
+
|
|
7
9
|
export interface CliToolStatus {
|
|
8
10
|
tool: CliTool;
|
|
9
11
|
installed: boolean;
|
|
@@ -11,6 +13,24 @@ export interface CliToolStatus {
|
|
|
11
13
|
latestVersion?: string;
|
|
12
14
|
hasUpdate?: boolean;
|
|
13
15
|
checking: boolean;
|
|
16
|
+
installMethod?: InstallMethod;
|
|
17
|
+
allMethods?: InstallMethod[];
|
|
18
|
+
updateCommand?: string;
|
|
19
|
+
brewFormula?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function getUninstallHint(tool: CliTool, method: InstallMethod, brewFormula?: string): string {
|
|
25
|
+
switch (method) {
|
|
26
|
+
case "bun": return `bun remove -g ${tool.packageName}`;
|
|
27
|
+
case "npm": return `npm uninstall -g ${tool.packageName}`;
|
|
28
|
+
case "pnpm": return `pnpm remove -g ${tool.packageName}`;
|
|
29
|
+
case "yarn": return `yarn global remove ${tool.packageName}`;
|
|
30
|
+
case "brew": return `brew uninstall ${brewFormula || tool.name}`;
|
|
31
|
+
case "pip": return `pip uninstall ${tool.packageName}`;
|
|
32
|
+
default: return "";
|
|
33
|
+
}
|
|
14
34
|
}
|
|
15
35
|
|
|
16
36
|
// ─── Row renderer ──────────────────────────────────────────────────────────────
|
|
@@ -20,7 +40,8 @@ export function renderCliToolRow(
|
|
|
20
40
|
_index: number,
|
|
21
41
|
isSelected: boolean,
|
|
22
42
|
): React.ReactNode {
|
|
23
|
-
const { tool, installed, installedVersion, hasUpdate, checking } = status;
|
|
43
|
+
const { tool, installed, installedVersion, hasUpdate, checking, allMethods } = status;
|
|
44
|
+
const hasConflict = allMethods && allMethods.length > 1;
|
|
24
45
|
|
|
25
46
|
let icon: string;
|
|
26
47
|
let iconColor: string;
|
|
@@ -28,32 +49,38 @@ export function renderCliToolRow(
|
|
|
28
49
|
if (!installed) {
|
|
29
50
|
icon = "○";
|
|
30
51
|
iconColor = theme.colors.muted;
|
|
52
|
+
} else if (hasConflict) {
|
|
53
|
+
icon = "!";
|
|
54
|
+
iconColor = theme.colors.danger;
|
|
31
55
|
} else if (hasUpdate) {
|
|
32
|
-
icon = "
|
|
56
|
+
icon = "*";
|
|
33
57
|
iconColor = theme.colors.warning;
|
|
34
58
|
} else {
|
|
35
59
|
icon = "●";
|
|
36
60
|
iconColor = theme.colors.success;
|
|
37
61
|
}
|
|
38
62
|
|
|
39
|
-
const versionText = installedVersion ? `v${installedVersion}` : "";
|
|
63
|
+
const versionText = installedVersion ? ` v${installedVersion}` : "";
|
|
64
|
+
const methodTag = installed && allMethods?.length
|
|
65
|
+
? ` ${allMethods.join("+")}`
|
|
66
|
+
: "";
|
|
40
67
|
|
|
41
68
|
if (isSelected) {
|
|
42
69
|
return (
|
|
43
70
|
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
44
|
-
{" "}
|
|
45
|
-
{
|
|
46
|
-
{checking ? "..." : ""}{" "}
|
|
71
|
+
{" "}{icon} {tool.displayName}{versionText}{methodTag}
|
|
72
|
+
{checking ? " ..." : ""}{" "}
|
|
47
73
|
</text>
|
|
48
74
|
);
|
|
49
75
|
}
|
|
50
76
|
|
|
51
77
|
return (
|
|
52
78
|
<text>
|
|
53
|
-
<span fg={iconColor}>{icon}</span>
|
|
79
|
+
<span fg={iconColor}> {icon}</span>
|
|
54
80
|
<span fg={theme.colors.text}> {tool.displayName}</span>
|
|
55
|
-
{versionText ? <span fg={theme.colors.success}>
|
|
56
|
-
{
|
|
81
|
+
{versionText ? <span fg={theme.colors.success}>{versionText}</span> : null}
|
|
82
|
+
{methodTag ? <span fg={hasConflict ? theme.colors.danger : theme.colors.dim}>{methodTag}</span> : null}
|
|
83
|
+
{checking ? <span fg={theme.colors.muted}>{" ..."}</span> : null}
|
|
57
84
|
</text>
|
|
58
85
|
);
|
|
59
86
|
}
|
|
@@ -76,9 +103,11 @@ export function renderCliToolDetail(
|
|
|
76
103
|
);
|
|
77
104
|
}
|
|
78
105
|
|
|
79
|
-
const { tool, installed, installedVersion, latestVersion, hasUpdate, checking } =
|
|
106
|
+
const { tool, installed, installedVersion, latestVersion, hasUpdate, checking, installMethod, allMethods, updateCommand, brewFormula } =
|
|
80
107
|
status;
|
|
81
108
|
|
|
109
|
+
const hasConflict = allMethods && allMethods.length > 1;
|
|
110
|
+
|
|
82
111
|
return (
|
|
83
112
|
<box flexDirection="column">
|
|
84
113
|
<box marginBottom={1}>
|
|
@@ -86,13 +115,14 @@ export function renderCliToolDetail(
|
|
|
86
115
|
<strong>{"⚙ "}{tool.displayName}</strong>
|
|
87
116
|
</text>
|
|
88
117
|
{hasUpdate ? <text fg={theme.colors.warning}> ⬆</text> : null}
|
|
118
|
+
{hasConflict ? <text fg={theme.colors.danger}> !</text> : null}
|
|
89
119
|
</box>
|
|
90
120
|
|
|
91
121
|
<text fg={theme.colors.muted}>{tool.description}</text>
|
|
92
122
|
|
|
93
123
|
<box marginTop={1} flexDirection="column">
|
|
94
124
|
<box>
|
|
95
|
-
<text fg={theme.colors.muted}>{"Status
|
|
125
|
+
<text fg={theme.colors.muted}>{"Status "}</text>
|
|
96
126
|
{!installed ? (
|
|
97
127
|
<text fg={theme.colors.muted}>{"○ Not installed"}</text>
|
|
98
128
|
) : checking ? (
|
|
@@ -105,45 +135,89 @@ export function renderCliToolDetail(
|
|
|
105
135
|
</box>
|
|
106
136
|
{installedVersion ? (
|
|
107
137
|
<box>
|
|
108
|
-
<text fg={theme.colors.muted}>{"
|
|
109
|
-
<text
|
|
138
|
+
<text fg={theme.colors.muted}>{"Version "}</text>
|
|
139
|
+
<text>
|
|
140
|
+
<span fg={theme.colors.success}>v{installedVersion}</span>
|
|
141
|
+
{latestVersion && hasUpdate ? (
|
|
142
|
+
<span fg={theme.colors.warning}> → v{latestVersion}</span>
|
|
143
|
+
) : null}
|
|
144
|
+
</text>
|
|
145
|
+
</box>
|
|
146
|
+
) : latestVersion ? (
|
|
147
|
+
<box>
|
|
148
|
+
<text fg={theme.colors.muted}>{"Latest "}</text>
|
|
149
|
+
<text fg={theme.colors.text}>v{latestVersion}</text>
|
|
110
150
|
</box>
|
|
111
151
|
) : null}
|
|
112
|
-
{
|
|
152
|
+
{installed && updateCommand ? (
|
|
113
153
|
<box>
|
|
114
|
-
<text fg={theme.colors.muted}>{"
|
|
115
|
-
<text fg={theme.colors.
|
|
154
|
+
<text fg={theme.colors.muted}>{"Update "}</text>
|
|
155
|
+
<text fg={theme.colors.accent}>{updateCommand}</text>
|
|
156
|
+
</box>
|
|
157
|
+
) : !installed ? (
|
|
158
|
+
<box>
|
|
159
|
+
<text fg={theme.colors.muted}>{"Install "}</text>
|
|
160
|
+
<text fg={theme.colors.accent}>{tool.installCommand}</text>
|
|
116
161
|
</box>
|
|
117
162
|
) : null}
|
|
118
163
|
<box>
|
|
119
|
-
<text fg={theme.colors.muted}>{"Website
|
|
164
|
+
<text fg={theme.colors.muted}>{"Website "}</text>
|
|
120
165
|
<text fg={theme.colors.link}>{tool.website}</text>
|
|
121
166
|
</box>
|
|
122
167
|
</box>
|
|
123
168
|
|
|
124
|
-
|
|
125
|
-
|
|
169
|
+
{/* Conflict warning */}
|
|
170
|
+
{hasConflict ? (
|
|
171
|
+
<box marginTop={1} flexDirection="column">
|
|
126
172
|
<box>
|
|
127
|
-
<text bg={theme.colors.
|
|
128
|
-
{" "}
|
|
129
|
-
|
|
173
|
+
<text bg={theme.colors.danger} fg="white">
|
|
174
|
+
<strong>{" "}Conflict: installed via {allMethods.join(" + ")}{" "}</strong>
|
|
175
|
+
</text>
|
|
176
|
+
</box>
|
|
177
|
+
<box marginTop={1}>
|
|
178
|
+
<text fg={theme.colors.muted}>
|
|
179
|
+
Multiple installs can cause version mismatches.
|
|
180
|
+
{"\n"}Keep one, remove the rest:
|
|
130
181
|
</text>
|
|
182
|
+
</box>
|
|
183
|
+
{allMethods.map((method, i) => (
|
|
184
|
+
<box key={method}>
|
|
185
|
+
<text>
|
|
186
|
+
<span fg={i === 0 ? theme.colors.success : theme.colors.danger}>
|
|
187
|
+
{i === 0 ? " ● keep " : " ○ remove "}
|
|
188
|
+
</span>
|
|
189
|
+
<span fg={theme.colors.warning}>{method}</span>
|
|
190
|
+
{i > 0 ? (
|
|
191
|
+
<span fg={theme.colors.dim}>{` ${getUninstallHint(tool, method, brewFormula)}`}</span>
|
|
192
|
+
) : (
|
|
193
|
+
<span fg={theme.colors.dim}> (active in PATH)</span>
|
|
194
|
+
)}
|
|
195
|
+
</text>
|
|
196
|
+
</box>
|
|
197
|
+
))}
|
|
198
|
+
<box marginTop={1}>
|
|
199
|
+
<text bg={theme.colors.danger} fg="white">{" "}c{" "}</text>
|
|
200
|
+
<text fg={theme.colors.muted}> Resolve — pick which to keep</text>
|
|
201
|
+
</box>
|
|
202
|
+
</box>
|
|
203
|
+
) : null}
|
|
204
|
+
|
|
205
|
+
{/* Actions */}
|
|
206
|
+
<box marginTop={2} flexDirection="column">
|
|
207
|
+
{!installed ? (
|
|
208
|
+
<box>
|
|
209
|
+
<text bg={theme.colors.success} fg="black">{" "}Enter{" "}</text>
|
|
131
210
|
<text fg={theme.colors.muted}> Install</text>
|
|
211
|
+
<text fg={theme.colors.dim}> {tool.installCommand}</text>
|
|
132
212
|
</box>
|
|
133
213
|
) : hasUpdate ? (
|
|
134
214
|
<box>
|
|
135
|
-
<text bg={theme.colors.warning} fg="black">
|
|
136
|
-
{" "}
|
|
137
|
-
Enter{" "}
|
|
138
|
-
</text>
|
|
215
|
+
<text bg={theme.colors.warning} fg="black">{" "}Enter{" "}</text>
|
|
139
216
|
<text fg={theme.colors.muted}> Update to v{latestVersion}</text>
|
|
140
217
|
</box>
|
|
141
218
|
) : (
|
|
142
219
|
<box>
|
|
143
|
-
<text bg={theme.colors.muted} fg="white">
|
|
144
|
-
{" "}
|
|
145
|
-
Enter{" "}
|
|
146
|
-
</text>
|
|
220
|
+
<text bg={theme.colors.muted} fg="white">{" "}Enter{" "}</text>
|
|
147
221
|
<text fg={theme.colors.muted}> Reinstall</text>
|
|
148
222
|
</box>
|
|
149
223
|
)}
|
|
@@ -82,7 +82,7 @@ function pluginDetail(item) {
|
|
|
82
82
|
plugin.localScope?.enabled;
|
|
83
83
|
// Orphaned/deprecated plugin
|
|
84
84
|
if (plugin.isOrphaned) {
|
|
85
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: theme.colors.warning, fg: "black", children: _jsxs("strong", { children: [" ", plugin.name, " \u2014 DEPRECATED "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.warning, children: "This plugin is no longer in the marketplace." }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.muted, children: "It was removed from the marketplace but still referenced in your settings. Press d to uninstall and clean up." }) }),
|
|
85
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: theme.colors.warning, fg: "black", children: _jsxs("strong", { children: [" ", plugin.name, " \u2014 DEPRECATED "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.warning, children: "This plugin is no longer in the marketplace." }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.muted, children: "It was removed from the marketplace but still referenced in your settings. Press d to uninstall and clean up." }) }), _jsx(ActionHints, { hints: [{ key: "d", label: isInstalled ? "Remove from all scopes" : "Clean up stale reference", tone: "danger" }] })] }));
|
|
86
86
|
}
|
|
87
87
|
// Build component counts
|
|
88
88
|
const components = [];
|
|
@@ -191,11 +191,9 @@ function pluginDetail(item: PluginPluginItem): React.ReactNode {
|
|
|
191
191
|
uninstall and clean up.
|
|
192
192
|
</text>
|
|
193
193
|
</box>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
/>
|
|
198
|
-
) : null}
|
|
194
|
+
<ActionHints
|
|
195
|
+
hints={[{ key: "d", label: isInstalled ? "Remove from all scopes" : "Clean up stale reference", tone: "danger" }]}
|
|
196
|
+
/>
|
|
199
197
|
</box>
|
|
200
198
|
);
|
|
201
199
|
}
|
|
@@ -31,9 +31,11 @@ const settingRenderer = {
|
|
|
31
31
|
renderDetail: ({ item }) => {
|
|
32
32
|
const { setting } = item;
|
|
33
33
|
const scoped = item.scopedValues;
|
|
34
|
-
const storageDesc = setting.storage.type === "
|
|
35
|
-
?
|
|
36
|
-
:
|
|
34
|
+
const storageDesc = setting.storage.type === "attribution"
|
|
35
|
+
? "settings.json: attribution"
|
|
36
|
+
: setting.storage.type === "env"
|
|
37
|
+
? `env: ${setting.storage.key}`
|
|
38
|
+
: `settings.json: ${setting.storage.key}`;
|
|
37
39
|
const userValue = formatValue(setting, scoped.user);
|
|
38
40
|
const projectValue = formatValue(setting, scoped.project);
|
|
39
41
|
const userIsSet = scoped.user !== undefined && scoped.user !== "";
|
|
@@ -79,9 +79,11 @@ const settingRenderer: ItemRenderer<SettingsSettingItem> = {
|
|
|
79
79
|
const { setting } = item;
|
|
80
80
|
const scoped = item.scopedValues;
|
|
81
81
|
const storageDesc =
|
|
82
|
-
setting.storage.type === "
|
|
83
|
-
?
|
|
84
|
-
:
|
|
82
|
+
setting.storage.type === "attribution"
|
|
83
|
+
? "settings.json: attribution"
|
|
84
|
+
: setting.storage.type === "env"
|
|
85
|
+
? `env: ${setting.storage.key}`
|
|
86
|
+
: `settings.json: ${setting.storage.key}`;
|
|
85
87
|
|
|
86
88
|
const userValue = formatValue(setting, scoped.user);
|
|
87
89
|
const projectValue = formatValue(setting, scoped.project);
|
|
@@ -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;
|