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
|
@@ -635,6 +635,8 @@ export interface MarketplaceRecoveryResult {
|
|
|
635
635
|
|
|
636
636
|
const OLD_MARKETPLACE_NAMES = ["mag-claude-plugins", "MadAppGang-claude-code"];
|
|
637
637
|
const NEW_MARKETPLACE_NAME = "magus";
|
|
638
|
+
const NEW_MARKETPLACE_REPO = "MadAppGang/magus";
|
|
639
|
+
const OLD_MARKETPLACE_REPOS = ["MadAppGang/claude-code"];
|
|
638
640
|
|
|
639
641
|
/**
|
|
640
642
|
* Rename plugin keys in a Record from any old marketplace name to new.
|
|
@@ -690,12 +692,28 @@ function migrateSettingsObject(settings: ClaudeSettings): number {
|
|
|
690
692
|
const entry = settings.extraKnownMarketplaces[oldName];
|
|
691
693
|
delete settings.extraKnownMarketplaces[oldName];
|
|
692
694
|
if (!settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME]) {
|
|
693
|
-
settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME] =
|
|
695
|
+
settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME] = {
|
|
696
|
+
...entry,
|
|
697
|
+
source: {
|
|
698
|
+
...entry.source,
|
|
699
|
+
repo: NEW_MARKETPLACE_REPO,
|
|
700
|
+
},
|
|
701
|
+
};
|
|
694
702
|
}
|
|
695
703
|
total++;
|
|
696
704
|
}
|
|
697
705
|
}
|
|
698
706
|
|
|
707
|
+
// Fix stale repo URL on existing magus entry (e.g. key is "magus" but repo is still "MadAppGang/claude-code")
|
|
708
|
+
const magusEntry = settings.extraKnownMarketplaces?.[NEW_MARKETPLACE_NAME];
|
|
709
|
+
if (
|
|
710
|
+
magusEntry?.source?.repo &&
|
|
711
|
+
OLD_MARKETPLACE_REPOS.includes(magusEntry.source.repo)
|
|
712
|
+
) {
|
|
713
|
+
magusEntry.source.repo = NEW_MARKETPLACE_REPO;
|
|
714
|
+
total++;
|
|
715
|
+
}
|
|
716
|
+
|
|
699
717
|
return total;
|
|
700
718
|
}
|
|
701
719
|
|
|
@@ -732,7 +750,9 @@ export async function migrateMarketplaceRename(
|
|
|
732
750
|
await writeSettings(settings, projectPath);
|
|
733
751
|
result.projectMigrated = count;
|
|
734
752
|
}
|
|
735
|
-
} catch {
|
|
753
|
+
} catch {
|
|
754
|
+
/* skip if unreadable */
|
|
755
|
+
}
|
|
736
756
|
|
|
737
757
|
// 2. Global settings
|
|
738
758
|
try {
|
|
@@ -742,24 +762,39 @@ export async function migrateMarketplaceRename(
|
|
|
742
762
|
await writeGlobalSettings(settings);
|
|
743
763
|
result.globalMigrated = count;
|
|
744
764
|
}
|
|
745
|
-
} catch {
|
|
765
|
+
} catch {
|
|
766
|
+
/* skip if unreadable */
|
|
767
|
+
}
|
|
746
768
|
|
|
747
769
|
// 3. Local settings (settings.local.json)
|
|
748
770
|
try {
|
|
749
771
|
const local = await readLocalSettings(projectPath);
|
|
750
772
|
let localCount = 0;
|
|
751
773
|
const [ep, epCount] = migratePluginKeys(local.enabledPlugins);
|
|
752
|
-
if (epCount > 0) {
|
|
774
|
+
if (epCount > 0) {
|
|
775
|
+
local.enabledPlugins = ep;
|
|
776
|
+
localCount += epCount;
|
|
777
|
+
}
|
|
753
778
|
const [iv, ivCount] = migratePluginKeys(local.installedPluginVersions);
|
|
754
|
-
if (ivCount > 0) {
|
|
779
|
+
if (ivCount > 0) {
|
|
780
|
+
local.installedPluginVersions = iv;
|
|
781
|
+
localCount += ivCount;
|
|
782
|
+
}
|
|
755
783
|
if (localCount > 0) {
|
|
756
784
|
await writeLocalSettings(local, projectPath);
|
|
757
785
|
result.localMigrated = localCount;
|
|
758
786
|
}
|
|
759
|
-
} catch {
|
|
787
|
+
} catch {
|
|
788
|
+
/* skip if unreadable */
|
|
789
|
+
}
|
|
760
790
|
|
|
761
791
|
// 4. known_marketplaces.json — rename old keys + physical directory cleanup
|
|
762
|
-
const pluginsDir = path.join(
|
|
792
|
+
const pluginsDir = path.join(
|
|
793
|
+
os.homedir(),
|
|
794
|
+
".claude",
|
|
795
|
+
"plugins",
|
|
796
|
+
"marketplaces",
|
|
797
|
+
);
|
|
763
798
|
const newDir = path.join(pluginsDir, NEW_MARKETPLACE_NAME);
|
|
764
799
|
|
|
765
800
|
try {
|
|
@@ -785,11 +820,9 @@ export async function migrateMarketplaceRename(
|
|
|
785
820
|
|
|
786
821
|
// Ensure installLocation doesn't reference old directory names
|
|
787
822
|
if (known[NEW_MARKETPLACE_NAME]?.installLocation?.includes(oldName)) {
|
|
788
|
-
known[NEW_MARKETPLACE_NAME].installLocation =
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
NEW_MARKETPLACE_NAME,
|
|
792
|
-
);
|
|
823
|
+
known[NEW_MARKETPLACE_NAME].installLocation = known[
|
|
824
|
+
NEW_MARKETPLACE_NAME
|
|
825
|
+
].installLocation.replace(oldName, NEW_MARKETPLACE_NAME);
|
|
793
826
|
knownModified = true;
|
|
794
827
|
}
|
|
795
828
|
}
|
|
@@ -798,7 +831,9 @@ export async function migrateMarketplaceRename(
|
|
|
798
831
|
await writeKnownMarketplaces(known);
|
|
799
832
|
result.knownMarketplacesMigrated = true;
|
|
800
833
|
}
|
|
801
|
-
} catch {
|
|
834
|
+
} catch {
|
|
835
|
+
/* skip if unreadable */
|
|
836
|
+
}
|
|
802
837
|
|
|
803
838
|
// 4b. Rename/remove old physical directories (runs even if keys were already migrated)
|
|
804
839
|
for (const oldName of OLD_MARKETPLACE_NAMES) {
|
|
@@ -812,7 +847,9 @@ export async function migrateMarketplaceRename(
|
|
|
812
847
|
await fs.remove(oldDir);
|
|
813
848
|
}
|
|
814
849
|
}
|
|
815
|
-
} catch {
|
|
850
|
+
} catch {
|
|
851
|
+
/* non-fatal: directory cleanup is best-effort */
|
|
852
|
+
}
|
|
816
853
|
}
|
|
817
854
|
|
|
818
855
|
// 4c. Update git remote URL in the marketplace clone (old → new repo)
|
|
@@ -820,16 +857,22 @@ export async function migrateMarketplaceRename(
|
|
|
820
857
|
if (await fs.pathExists(path.join(newDir, ".git"))) {
|
|
821
858
|
const { execSync } = await import("node:child_process");
|
|
822
859
|
const remote = execSync("git remote get-url origin", {
|
|
823
|
-
cwd: newDir,
|
|
860
|
+
cwd: newDir,
|
|
861
|
+
encoding: "utf-8",
|
|
862
|
+
timeout: 5000,
|
|
824
863
|
}).trim();
|
|
825
864
|
if (remote.includes("claude-code") && remote.includes("MadAppGang")) {
|
|
826
865
|
const newRemote = remote.replace("claude-code", NEW_MARKETPLACE_NAME);
|
|
827
866
|
execSync(`git remote set-url origin "${newRemote}"`, {
|
|
828
|
-
cwd: newDir,
|
|
867
|
+
cwd: newDir,
|
|
868
|
+
encoding: "utf-8",
|
|
869
|
+
timeout: 5000,
|
|
829
870
|
});
|
|
830
871
|
}
|
|
831
872
|
}
|
|
832
|
-
} catch {
|
|
873
|
+
} catch {
|
|
874
|
+
/* non-fatal: git remote update is best-effort */
|
|
875
|
+
}
|
|
833
876
|
|
|
834
877
|
// 5. installed_plugins.json — rename plugin ID keys
|
|
835
878
|
try {
|
|
@@ -837,7 +880,9 @@ export async function migrateMarketplaceRename(
|
|
|
837
880
|
let regCount = 0;
|
|
838
881
|
const newPlugins: typeof registry.plugins = {};
|
|
839
882
|
for (const [pluginId, entries] of Object.entries(registry.plugins)) {
|
|
840
|
-
const oldName = OLD_MARKETPLACE_NAMES.find((n) =>
|
|
883
|
+
const oldName = OLD_MARKETPLACE_NAMES.find((n) =>
|
|
884
|
+
pluginId.endsWith(`@${n}`),
|
|
885
|
+
);
|
|
841
886
|
if (oldName) {
|
|
842
887
|
const pluginName = pluginId.slice(0, pluginId.lastIndexOf("@"));
|
|
843
888
|
const newKey = `${pluginName}@${NEW_MARKETPLACE_NAME}`;
|
|
@@ -854,7 +899,44 @@ export async function migrateMarketplaceRename(
|
|
|
854
899
|
await writeInstalledPluginsRegistry(registry);
|
|
855
900
|
result.registryMigrated = regCount;
|
|
856
901
|
}
|
|
857
|
-
} catch {
|
|
902
|
+
} catch {
|
|
903
|
+
/* skip if unreadable */
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// 6. Scan all known project settings (derived from ~/.claude/projects/ directory names)
|
|
907
|
+
try {
|
|
908
|
+
const projectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
909
|
+
if (await fs.pathExists(projectsDir)) {
|
|
910
|
+
const entries = await fs.readdir(projectsDir);
|
|
911
|
+
const seenPaths = new Set<string>();
|
|
912
|
+
// Current project (from step 1) already handled — skip it
|
|
913
|
+
const currentProject = projectPath || process.cwd();
|
|
914
|
+
seenPaths.add(currentProject);
|
|
915
|
+
|
|
916
|
+
for (const entry of entries) {
|
|
917
|
+
// Directory names encode paths: -Users-jack-dev-foo → /Users/jack/dev/foo
|
|
918
|
+
const decoded = entry.replace(/^-/, "/").replace(/-/g, "/");
|
|
919
|
+
if (seenPaths.has(decoded)) continue;
|
|
920
|
+
seenPaths.add(decoded);
|
|
921
|
+
|
|
922
|
+
const settingsFile = path.join(decoded, ".claude", "settings.json");
|
|
923
|
+
try {
|
|
924
|
+
if (await fs.pathExists(settingsFile)) {
|
|
925
|
+
const raw = await fs.readJson(settingsFile);
|
|
926
|
+
const count = migrateSettingsObject(raw);
|
|
927
|
+
if (count > 0) {
|
|
928
|
+
await fs.writeJson(settingsFile, raw, { spaces: 2 });
|
|
929
|
+
result.projectMigrated += count;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
} catch {
|
|
933
|
+
/* skip individual projects that fail */
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
} catch {
|
|
938
|
+
/* non-fatal: cross-project scan is best-effort */
|
|
939
|
+
}
|
|
858
940
|
|
|
859
941
|
return result;
|
|
860
942
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -243,3 +243,36 @@ export interface ProfileEntry {
|
|
|
243
243
|
updatedAt: string;
|
|
244
244
|
scope: "user" | "project";
|
|
245
245
|
}
|
|
246
|
+
|
|
247
|
+
// ─── Predefined Profile Types ──────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/** A skill reference for predefined profiles */
|
|
250
|
+
export interface PredefinedSkill {
|
|
251
|
+
name: string;
|
|
252
|
+
repo: string;
|
|
253
|
+
skillPath: string;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Settings that can be configured in a predefined profile */
|
|
257
|
+
export interface PredefinedSettings {
|
|
258
|
+
effortLevel?: "low" | "medium" | "high";
|
|
259
|
+
alwaysThinkingEnabled?: boolean;
|
|
260
|
+
model?: "claude-sonnet-4-6" | "claude-opus-4-6";
|
|
261
|
+
outputStyle?: "concise" | "explanatory" | "formal";
|
|
262
|
+
CLAUDE_CODE_ENABLE_TASKS?: boolean;
|
|
263
|
+
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS?: boolean;
|
|
264
|
+
includeGitInstructions?: boolean;
|
|
265
|
+
respectGitignore?: boolean;
|
|
266
|
+
enableAllProjectMcpServers?: boolean;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** A predefined (built-in) profile for claudeup */
|
|
270
|
+
export interface PredefinedProfile {
|
|
271
|
+
id: string;
|
|
272
|
+
name: string;
|
|
273
|
+
description: string;
|
|
274
|
+
targetAudience: string;
|
|
275
|
+
plugins: Record<string, boolean>;
|
|
276
|
+
skills: PredefinedSkill[];
|
|
277
|
+
settings: PredefinedSettings;
|
|
278
|
+
}
|
|
@@ -1,33 +1,54 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import { theme } from "../theme.js";
|
|
3
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
4
|
+
function getUninstallHint(tool, method, brewFormula) {
|
|
5
|
+
switch (method) {
|
|
6
|
+
case "bun": return `bun remove -g ${tool.packageName}`;
|
|
7
|
+
case "npm": return `npm uninstall -g ${tool.packageName}`;
|
|
8
|
+
case "pnpm": return `pnpm remove -g ${tool.packageName}`;
|
|
9
|
+
case "yarn": return `yarn global remove ${tool.packageName}`;
|
|
10
|
+
case "brew": return `brew uninstall ${brewFormula || tool.name}`;
|
|
11
|
+
case "pip": return `pip uninstall ${tool.packageName}`;
|
|
12
|
+
default: return "";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
3
15
|
// ─── Row renderer ──────────────────────────────────────────────────────────────
|
|
4
16
|
export function renderCliToolRow(status, _index, isSelected) {
|
|
5
|
-
const { tool, installed, installedVersion, hasUpdate, checking } = status;
|
|
17
|
+
const { tool, installed, installedVersion, hasUpdate, checking, allMethods } = status;
|
|
18
|
+
const hasConflict = allMethods && allMethods.length > 1;
|
|
6
19
|
let icon;
|
|
7
20
|
let iconColor;
|
|
8
21
|
if (!installed) {
|
|
9
22
|
icon = "○";
|
|
10
23
|
iconColor = theme.colors.muted;
|
|
11
24
|
}
|
|
25
|
+
else if (hasConflict) {
|
|
26
|
+
icon = "!";
|
|
27
|
+
iconColor = theme.colors.danger;
|
|
28
|
+
}
|
|
12
29
|
else if (hasUpdate) {
|
|
13
|
-
icon = "
|
|
30
|
+
icon = "*";
|
|
14
31
|
iconColor = theme.colors.warning;
|
|
15
32
|
}
|
|
16
33
|
else {
|
|
17
34
|
icon = "●";
|
|
18
35
|
iconColor = theme.colors.success;
|
|
19
36
|
}
|
|
20
|
-
const versionText = installedVersion ? `v${installedVersion}` : "";
|
|
37
|
+
const versionText = installedVersion ? ` v${installedVersion}` : "";
|
|
38
|
+
const methodTag = installed && allMethods?.length
|
|
39
|
+
? ` ${allMethods.join("+")}`
|
|
40
|
+
: "";
|
|
21
41
|
if (isSelected) {
|
|
22
|
-
return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", icon, " ", tool.displayName,
|
|
42
|
+
return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", icon, " ", tool.displayName, versionText, methodTag, checking ? " ..." : "", " "] }));
|
|
23
43
|
}
|
|
24
|
-
return (_jsxs("text", { children: [
|
|
44
|
+
return (_jsxs("text", { children: [_jsxs("span", { fg: iconColor, children: [" ", icon] }), _jsxs("span", { fg: theme.colors.text, children: [" ", tool.displayName] }), versionText ? _jsx("span", { fg: theme.colors.success, children: versionText }) : null, methodTag ? _jsx("span", { fg: hasConflict ? theme.colors.danger : theme.colors.dim, children: methodTag }) : null, checking ? _jsx("span", { fg: theme.colors.muted, children: " ..." }) : null] }));
|
|
25
45
|
}
|
|
26
46
|
// ─── Detail renderer ───────────────────────────────────────────────────────────
|
|
27
47
|
export function renderCliToolDetail(status) {
|
|
28
48
|
if (!status) {
|
|
29
49
|
return (_jsx("box", { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsx("text", { fg: theme.colors.muted, children: "Select a tool to see details" }) }));
|
|
30
50
|
}
|
|
31
|
-
const { tool, installed, installedVersion, latestVersion, hasUpdate, checking } = status;
|
|
32
|
-
|
|
51
|
+
const { tool, installed, installedVersion, latestVersion, hasUpdate, checking, installMethod, allMethods, updateCommand, brewFormula } = status;
|
|
52
|
+
const hasConflict = allMethods && allMethods.length > 1;
|
|
53
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: theme.colors.info, children: _jsxs("strong", { children: ["⚙ ", tool.displayName] }) }), hasUpdate ? _jsx("text", { fg: theme.colors.warning, children: " \u2B06" }) : null, hasConflict ? _jsx("text", { fg: theme.colors.danger, children: " !" }) : null] }), _jsx("text", { fg: theme.colors.muted, children: tool.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Status " }), !installed ? (_jsx("text", { fg: theme.colors.muted, children: "○ Not installed" })) : checking ? (_jsx("text", { fg: theme.colors.success, children: "● Checking..." })) : hasUpdate ? (_jsx("text", { fg: theme.colors.warning, children: "● Update available" })) : (_jsx("text", { fg: theme.colors.success, children: "● Up to date" }))] }), installedVersion ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Version " }), _jsxs("text", { children: [_jsxs("span", { fg: theme.colors.success, children: ["v", installedVersion] }), latestVersion && hasUpdate ? (_jsxs("span", { fg: theme.colors.warning, children: [" \u2192 v", latestVersion] })) : null] })] })) : latestVersion ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Latest " }), _jsxs("text", { fg: theme.colors.text, children: ["v", latestVersion] })] })) : null, installed && updateCommand ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Update " }), _jsx("text", { fg: theme.colors.accent, children: updateCommand })] })) : !installed ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Install " }), _jsx("text", { fg: theme.colors.accent, children: tool.installCommand })] })) : null, _jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Website " }), _jsx("text", { fg: theme.colors.link, children: tool.website })] })] }), hasConflict ? (_jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("box", { children: _jsx("text", { bg: theme.colors.danger, fg: "white", children: _jsxs("strong", { children: [" ", "Conflict: installed via ", allMethods.join(" + "), " "] }) }) }), _jsx("box", { marginTop: 1, children: _jsxs("text", { fg: theme.colors.muted, children: ["Multiple installs can cause version mismatches.", "\n", "Keep one, remove the rest:"] }) }), allMethods.map((method, i) => (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: i === 0 ? theme.colors.success : theme.colors.danger, children: i === 0 ? " ● keep " : " ○ remove " }), _jsx("span", { fg: theme.colors.warning, children: method }), i > 0 ? (_jsx("span", { fg: theme.colors.dim, children: ` ${getUninstallHint(tool, method, brewFormula)}` })) : (_jsx("span", { fg: theme.colors.dim, children: " (active in PATH)" }))] }) }, method))), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: theme.colors.danger, fg: "white", children: [" ", "c", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Resolve \u2014 pick which to keep" })] })] })) : null, _jsx("box", { marginTop: 2, flexDirection: "column", children: !installed ? (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.success, fg: "black", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Install" }), _jsxs("text", { fg: theme.colors.dim, children: [" ", tool.installCommand] })] })) : hasUpdate ? (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.warning, fg: "black", children: [" ", "Enter", " "] }), _jsxs("text", { fg: theme.colors.muted, children: [" Update to v", latestVersion] })] })) : (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.muted, fg: "white", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Reinstall" })] })) })] }));
|
|
33
54
|
}
|
|
@@ -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
|
}
|
|
@@ -87,11 +87,11 @@ const predefinedRenderer = {
|
|
|
87
87
|
const { profile } = item;
|
|
88
88
|
const pluginCount = profile.magusPlugins.length + profile.anthropicPlugins.length;
|
|
89
89
|
const skillCount = profile.skills.length;
|
|
90
|
-
const
|
|
90
|
+
const countStr = `${pluginCount}p ${skillCount}s`;
|
|
91
91
|
if (isSelected) {
|
|
92
|
-
return (_jsxs("text", { bg:
|
|
92
|
+
return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", profile.name, " ", countStr, " "] }));
|
|
93
93
|
}
|
|
94
|
-
return (_jsxs("text", { children: [
|
|
94
|
+
return (_jsxs("text", { children: [_jsxs("span", { fg: "white", children: [" ", profile.name] }), _jsxs("span", { fg: theme.colors.dim, children: [" ", countStr] })] }));
|
|
95
95
|
},
|
|
96
96
|
renderDetail: ({ item }) => {
|
|
97
97
|
const { profile } = item;
|
|
@@ -99,7 +99,7 @@ const predefinedRenderer = {
|
|
|
99
99
|
const envMap = profile.settings["env"] ?? {};
|
|
100
100
|
const tasksOn = envMap["CLAUDE_CODE_ENABLE_TASKS"] === "true";
|
|
101
101
|
const teamsOn = envMap["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] === "true";
|
|
102
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", {
|
|
102
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { children: _jsx("text", { bg: theme.colors.accent, fg: "white", children: _jsxs("strong", { children: [" ", profile.name, " "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: theme.colors.muted, children: profile.description }) }), _jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: `\nMagus (${profile.magusPlugins.length})` }) }), _jsx("box", { children: _jsx("text", { fg: "#00bfa5", children: profile.magusPlugins.map((p) => ` ■ ${p}`).join("\n") }) }), _jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: `\nAnthropic (${profile.anthropicPlugins.length})` }) }), _jsx("box", { children: _jsx("text", { fg: "#b39ddb", children: profile.anthropicPlugins.map((p) => ` ■ ${p}`).join("\n") }) }), profile.skills.length > 0 && (_jsxs(_Fragment, { children: [_jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: `\nSkills (${profile.skills.length})` }) }), _jsx("box", { children: _jsx("text", { fg: "#ffd54f", children: profile.skills.map((s) => ` ■ ${s}`).join("\n") }) })] })), _jsx("box", { children: _jsx("text", { fg: theme.colors.dim, children: `\n${DIVIDER}` }) }), _jsx("box", { children: _jsx("text", { fg: theme.colors.muted, children: [
|
|
103
103
|
...settingEntries.map(([k, v]) => ` ${humanizeKey(k).padEnd(18)}${humanizeValue(k, v)}`),
|
|
104
104
|
...(tasksOn ? [` ${"Tasks".padEnd(18)}on`] : []),
|
|
105
105
|
...(teamsOn ? [` ${"Agent Teams".padEnd(18)}on`] : []),
|
|
@@ -112,24 +112,20 @@ const predefinedRenderer: ItemRenderer<{ kind: "predefined"; profile: Predefined
|
|
|
112
112
|
const pluginCount =
|
|
113
113
|
profile.magusPlugins.length + profile.anthropicPlugins.length;
|
|
114
114
|
const skillCount = profile.skills.length;
|
|
115
|
-
const
|
|
116
|
-
`${profile.name} — ${pluginCount} plugins · ${skillCount} skill${skillCount !== 1 ? "s" : ""}`,
|
|
117
|
-
45,
|
|
118
|
-
);
|
|
115
|
+
const countStr = `${pluginCount}p ${skillCount}s`;
|
|
119
116
|
|
|
120
117
|
if (isSelected) {
|
|
121
118
|
return (
|
|
122
|
-
<text bg=
|
|
123
|
-
{" "}
|
|
124
|
-
{label}{" "}
|
|
119
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
120
|
+
{" "}{profile.name} {countStr}{" "}
|
|
125
121
|
</text>
|
|
126
122
|
);
|
|
127
123
|
}
|
|
128
124
|
|
|
129
125
|
return (
|
|
130
126
|
<text>
|
|
131
|
-
<span fg=
|
|
132
|
-
<span fg={theme.colors.
|
|
127
|
+
<span fg="white">{" "}{profile.name}</span>
|
|
128
|
+
<span fg={theme.colors.dim}> {countStr}</span>
|
|
133
129
|
</text>
|
|
134
130
|
);
|
|
135
131
|
},
|
|
@@ -146,9 +142,11 @@ const predefinedRenderer: ItemRenderer<{ kind: "predefined"; profile: Predefined
|
|
|
146
142
|
|
|
147
143
|
return (
|
|
148
144
|
<box flexDirection="column">
|
|
149
|
-
<
|
|
150
|
-
<
|
|
151
|
-
|
|
145
|
+
<box>
|
|
146
|
+
<text bg={theme.colors.accent} fg="white">
|
|
147
|
+
<strong> {profile.name} </strong>
|
|
148
|
+
</text>
|
|
149
|
+
</box>
|
|
152
150
|
<box marginTop={1}>
|
|
153
151
|
<text fg={theme.colors.muted}>{profile.description}</text>
|
|
154
152
|
</box>
|