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.
@@ -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] = entry;
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 { /* skip if unreadable */ }
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 { /* skip if unreadable */ }
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) { local.enabledPlugins = ep; localCount += epCount; }
774
+ if (epCount > 0) {
775
+ local.enabledPlugins = ep;
776
+ localCount += epCount;
777
+ }
753
778
  const [iv, ivCount] = migratePluginKeys(local.installedPluginVersions);
754
- if (ivCount > 0) { local.installedPluginVersions = iv; localCount += ivCount; }
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 { /* skip if unreadable */ }
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(os.homedir(), ".claude", "plugins", "marketplaces");
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
- known[NEW_MARKETPLACE_NAME].installLocation.replace(
790
- oldName,
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 { /* skip if unreadable */ }
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 { /* non-fatal: directory cleanup is best-effort */ }
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, encoding: "utf-8", timeout: 5000,
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, encoding: "utf-8", timeout: 5000,
867
+ cwd: newDir,
868
+ encoding: "utf-8",
869
+ timeout: 5000,
829
870
  });
830
871
  }
831
872
  }
832
- } catch { /* non-fatal: git remote update is best-effort */ }
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) => pluginId.endsWith(`@${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 { /* skip if unreadable */ }
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
  }
@@ -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, " ", versionText, checking ? "..." : "", " "] }));
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: [_jsx("span", { fg: iconColor, children: icon }), _jsxs("span", { fg: theme.colors.text, children: [" ", tool.displayName] }), versionText ? _jsxs("span", { fg: theme.colors.success, children: [" ", versionText] }) : null, checking ? _jsx("span", { fg: theme.colors.muted, children: "..." }) : null] }));
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
- 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] }), _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: "Installed " }), _jsxs("text", { fg: theme.colors.success, children: ["v", installedVersion] })] })) : null, latestVersion ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Latest " }), _jsxs("text", { fg: theme.colors.text, children: ["v", latestVersion] })] })) : null, _jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Website " }), _jsx("text", { fg: theme.colors.link, children: tool.website })] })] }), _jsx("box", { marginTop: 2, children: !installed ? (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.success, fg: "black", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Install" })] })) : 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" })] })) })] }));
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
- {icon} {tool.displayName} {versionText}
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}> {versionText}</span> : null}
56
- {checking ? <span fg={theme.colors.muted}>{"..."}</span> : null}
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 "}</text>
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}>{"Installed "}</text>
109
- <text fg={theme.colors.success}>v{installedVersion}</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
- {latestVersion ? (
152
+ {installed && updateCommand ? (
113
153
  <box>
114
- <text fg={theme.colors.muted}>{"Latest "}</text>
115
- <text fg={theme.colors.text}>v{latestVersion}</text>
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 "}</text>
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
- <box marginTop={2}>
125
- {!installed ? (
169
+ {/* Conflict warning */}
170
+ {hasConflict ? (
171
+ <box marginTop={1} flexDirection="column">
126
172
  <box>
127
- <text bg={theme.colors.success} fg="black">
128
- {" "}
129
- Enter{" "}
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." }) }), isInstalled ? (_jsx(ActionHints, { hints: [{ key: "d", label: "Uninstall (recommended)", tone: "danger" }] })) : null] }));
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
- {isInstalled ? (
195
- <ActionHints
196
- hints={[{ key: "d", label: "Uninstall (recommended)", tone: "danger" }]}
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 label = truncate(`${profile.name} — ${pluginCount} plugins · ${skillCount} skill${skillCount !== 1 ? "s" : ""}`, 45);
90
+ const countStr = `${pluginCount}p ${skillCount}s`;
91
91
  if (isSelected) {
92
- return (_jsxs("text", { bg: "blue", fg: "white", children: [" ", label, " "] }));
92
+ return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", profile.name, " ", countStr, " "] }));
93
93
  }
94
- return (_jsxs("text", { children: [_jsx("span", { fg: theme.colors.muted, children: "- " }), _jsx("span", { fg: theme.colors.text, children: label })] }));
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", { fg: theme.colors.info, children: _jsx("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: [
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 label = truncate(
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="blue" fg="white">
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={theme.colors.muted}>{"- "}</span>
132
- <span fg={theme.colors.text}>{label}</span>
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
- <text fg={theme.colors.info}>
150
- <strong>{profile.name}</strong>
151
- </text>
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>