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
package/package.json
CHANGED
package/src/data/cli-tools.js
CHANGED
|
@@ -11,15 +11,15 @@ export const cliTools = [
|
|
|
11
11
|
packageName: "claudeup",
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
name: "
|
|
15
|
-
displayName: "
|
|
16
|
-
description: "
|
|
17
|
-
installCommand: "npm install -g
|
|
18
|
-
checkCommand: "
|
|
19
|
-
website: "https://github.com/MadAppGang/
|
|
14
|
+
name: "mnemex",
|
|
15
|
+
displayName: "Mnemex",
|
|
16
|
+
description: "AST-aware code search with PageRank, callers/callees, and semantic embeddings",
|
|
17
|
+
installCommand: "npm install -g mnemex",
|
|
18
|
+
checkCommand: "mnemex --version",
|
|
19
|
+
website: "https://github.com/MadAppGang/mnemex",
|
|
20
20
|
category: "ai-coding",
|
|
21
21
|
packageManager: "npm",
|
|
22
|
-
packageName: "
|
|
22
|
+
packageName: "mnemex",
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
name: "claudish",
|
package/src/data/cli-tools.ts
CHANGED
|
@@ -24,16 +24,16 @@ export const cliTools: CliTool[] = [
|
|
|
24
24
|
packageName: "claudeup",
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
|
-
name: "
|
|
28
|
-
displayName: "
|
|
27
|
+
name: "mnemex",
|
|
28
|
+
displayName: "Mnemex",
|
|
29
29
|
description:
|
|
30
|
-
"
|
|
31
|
-
installCommand: "npm install -g
|
|
32
|
-
checkCommand: "
|
|
33
|
-
website: "https://github.com/MadAppGang/
|
|
30
|
+
"AST-aware code search with PageRank, callers/callees, and semantic embeddings",
|
|
31
|
+
installCommand: "npm install -g mnemex",
|
|
32
|
+
checkCommand: "mnemex --version",
|
|
33
|
+
website: "https://github.com/MadAppGang/mnemex",
|
|
34
34
|
category: "ai-coding",
|
|
35
35
|
packageManager: "npm",
|
|
36
|
-
packageName: "
|
|
36
|
+
packageName: "mnemex",
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
name: "claudish",
|
|
@@ -162,6 +162,15 @@ export const SETTINGS_CATALOG = [
|
|
|
162
162
|
storage: { type: "env", key: "CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS" },
|
|
163
163
|
defaultValue: "false",
|
|
164
164
|
},
|
|
165
|
+
{
|
|
166
|
+
id: "attribution",
|
|
167
|
+
name: "AI Attribution in Commits & PRs",
|
|
168
|
+
description: "Add 'Co-Authored-By: Claude' and '🤖 Generated with Claude Code' to git commits and PR descriptions",
|
|
169
|
+
category: "workflow",
|
|
170
|
+
type: "boolean",
|
|
171
|
+
storage: { type: "attribution" },
|
|
172
|
+
defaultValue: "true",
|
|
173
|
+
},
|
|
165
174
|
{
|
|
166
175
|
id: "output-style",
|
|
167
176
|
name: "Output Style",
|
|
@@ -9,7 +9,8 @@ export type SettingCategory =
|
|
|
9
9
|
export type SettingType = "boolean" | "string" | "select";
|
|
10
10
|
export type SettingStorage =
|
|
11
11
|
| { type: "env"; key: string }
|
|
12
|
-
| { type: "setting"; key: string }
|
|
12
|
+
| { type: "setting"; key: string }
|
|
13
|
+
| { type: "attribution" };
|
|
13
14
|
|
|
14
15
|
export interface SettingDefinition {
|
|
15
16
|
id: string;
|
|
@@ -204,6 +205,16 @@ export const SETTINGS_CATALOG: SettingDefinition[] = [
|
|
|
204
205
|
storage: { type: "env", key: "CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS" },
|
|
205
206
|
defaultValue: "false",
|
|
206
207
|
},
|
|
208
|
+
{
|
|
209
|
+
id: "attribution",
|
|
210
|
+
name: "AI Attribution in Commits & PRs",
|
|
211
|
+
description:
|
|
212
|
+
"Add 'Co-Authored-By: Claude' and '🤖 Generated with Claude Code' to git commits and PR descriptions",
|
|
213
|
+
category: "workflow",
|
|
214
|
+
type: "boolean",
|
|
215
|
+
storage: { type: "attribution" },
|
|
216
|
+
defaultValue: "true",
|
|
217
|
+
},
|
|
207
218
|
{
|
|
208
219
|
id: "output-style",
|
|
209
220
|
name: "Output Style",
|
|
@@ -418,6 +418,8 @@ export async function getMarketplaceAutoUpdate(marketplaceName) {
|
|
|
418
418
|
// =============================================================================
|
|
419
419
|
const OLD_MARKETPLACE_NAMES = ["mag-claude-plugins", "MadAppGang-claude-code"];
|
|
420
420
|
const NEW_MARKETPLACE_NAME = "magus";
|
|
421
|
+
const NEW_MARKETPLACE_REPO = "MadAppGang/magus";
|
|
422
|
+
const OLD_MARKETPLACE_REPOS = ["MadAppGang/claude-code"];
|
|
421
423
|
/**
|
|
422
424
|
* Rename plugin keys in a Record from any old marketplace name to new.
|
|
423
425
|
* e.g., "frontend@mag-claude-plugins" → "frontend@magus"
|
|
@@ -468,11 +470,24 @@ function migrateSettingsObject(settings) {
|
|
|
468
470
|
const entry = settings.extraKnownMarketplaces[oldName];
|
|
469
471
|
delete settings.extraKnownMarketplaces[oldName];
|
|
470
472
|
if (!settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME]) {
|
|
471
|
-
settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME] =
|
|
473
|
+
settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME] = {
|
|
474
|
+
...entry,
|
|
475
|
+
source: {
|
|
476
|
+
...entry.source,
|
|
477
|
+
repo: NEW_MARKETPLACE_REPO,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
472
480
|
}
|
|
473
481
|
total++;
|
|
474
482
|
}
|
|
475
483
|
}
|
|
484
|
+
// Fix stale repo URL on existing magus entry (e.g. key is "magus" but repo is still "MadAppGang/claude-code")
|
|
485
|
+
const magusEntry = settings.extraKnownMarketplaces?.[NEW_MARKETPLACE_NAME];
|
|
486
|
+
if (magusEntry?.source?.repo &&
|
|
487
|
+
OLD_MARKETPLACE_REPOS.includes(magusEntry.source.repo)) {
|
|
488
|
+
magusEntry.source.repo = NEW_MARKETPLACE_REPO;
|
|
489
|
+
total++;
|
|
490
|
+
}
|
|
476
491
|
return total;
|
|
477
492
|
}
|
|
478
493
|
/**
|
|
@@ -481,6 +496,35 @@ function migrateSettingsObject(settings) {
|
|
|
481
496
|
* Runs across project settings, global settings, local settings,
|
|
482
497
|
* known_marketplaces.json, and installed_plugins.json.
|
|
483
498
|
*/
|
|
499
|
+
/**
|
|
500
|
+
* Decode a ~/.claude/projects/ directory name back to a filesystem path.
|
|
501
|
+
* Claude Code encodes paths by replacing "/" with "-", which is lossy when
|
|
502
|
+
* directory names themselves contain dashes (e.g., "circl-infra" → "circl/infra").
|
|
503
|
+
* Strategy: split on "-", then greedily recombine segments by checking which
|
|
504
|
+
* combinations actually exist on disk.
|
|
505
|
+
* Returns null if no valid path can be resolved.
|
|
506
|
+
*/
|
|
507
|
+
async function decodeProjectDirName(encoded) {
|
|
508
|
+
// Split into segments: "-Users-jack-dev-circl-infra" → ["Users","jack","dev","circl","infra"]
|
|
509
|
+
const segments = encoded.replace(/^-/, "").split("-");
|
|
510
|
+
if (segments.length === 0)
|
|
511
|
+
return null;
|
|
512
|
+
// Build path greedily: at each step, try joining the next segment with a dash first
|
|
513
|
+
// (preserving directory names like "circl-infra"), fall back to slash (new path segment)
|
|
514
|
+
let current = "/" + segments[0];
|
|
515
|
+
for (let i = 1; i < segments.length; i++) {
|
|
516
|
+
const withDash = current + "-" + segments[i];
|
|
517
|
+
const withSlash = current + "/" + segments[i];
|
|
518
|
+
// Prefer dash (keeps compound names intact) if that path prefix exists
|
|
519
|
+
if (await fs.pathExists(withDash)) {
|
|
520
|
+
current = withDash;
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
current = withSlash;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return (await fs.pathExists(current)) ? current : null;
|
|
527
|
+
}
|
|
484
528
|
export async function migrateMarketplaceRename(projectPath) {
|
|
485
529
|
const result = {
|
|
486
530
|
projectMigrated: 0,
|
|
@@ -639,6 +683,40 @@ export async function migrateMarketplaceRename(projectPath) {
|
|
|
639
683
|
catch {
|
|
640
684
|
/* skip if unreadable */
|
|
641
685
|
}
|
|
686
|
+
// 6. Scan all known project settings (derived from ~/.claude/projects/ directory names)
|
|
687
|
+
try {
|
|
688
|
+
const projectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
689
|
+
if (await fs.pathExists(projectsDir)) {
|
|
690
|
+
const entries = await fs.readdir(projectsDir);
|
|
691
|
+
const seenPaths = new Set();
|
|
692
|
+
// Current project (from step 1) already handled — skip it
|
|
693
|
+
const currentProject = projectPath || process.cwd();
|
|
694
|
+
seenPaths.add(currentProject);
|
|
695
|
+
for (const entry of entries) {
|
|
696
|
+
const decoded = await decodeProjectDirName(entry);
|
|
697
|
+
if (!decoded || seenPaths.has(decoded))
|
|
698
|
+
continue;
|
|
699
|
+
seenPaths.add(decoded);
|
|
700
|
+
const settingsFile = path.join(decoded, ".claude", "settings.json");
|
|
701
|
+
try {
|
|
702
|
+
if (await fs.pathExists(settingsFile)) {
|
|
703
|
+
const raw = await fs.readJson(settingsFile);
|
|
704
|
+
const count = migrateSettingsObject(raw);
|
|
705
|
+
if (count > 0) {
|
|
706
|
+
await fs.writeJson(settingsFile, raw, { spaces: 2 });
|
|
707
|
+
result.projectMigrated += count;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
/* skip individual projects that fail */
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
/* non-fatal: cross-project scan is best-effort */
|
|
719
|
+
}
|
|
642
720
|
return result;
|
|
643
721
|
}
|
|
644
722
|
/**
|
|
@@ -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
|
|
|
@@ -713,6 +731,36 @@ export interface MigrationResult {
|
|
|
713
731
|
* Runs across project settings, global settings, local settings,
|
|
714
732
|
* known_marketplaces.json, and installed_plugins.json.
|
|
715
733
|
*/
|
|
734
|
+
/**
|
|
735
|
+
* Decode a ~/.claude/projects/ directory name back to a filesystem path.
|
|
736
|
+
* Claude Code encodes paths by replacing "/" with "-", which is lossy when
|
|
737
|
+
* directory names themselves contain dashes (e.g., "circl-infra" → "circl/infra").
|
|
738
|
+
* Strategy: split on "-", then greedily recombine segments by checking which
|
|
739
|
+
* combinations actually exist on disk.
|
|
740
|
+
* Returns null if no valid path can be resolved.
|
|
741
|
+
*/
|
|
742
|
+
async function decodeProjectDirName(encoded: string): Promise<string | null> {
|
|
743
|
+
// Split into segments: "-Users-jack-dev-circl-infra" → ["Users","jack","dev","circl","infra"]
|
|
744
|
+
const segments = encoded.replace(/^-/, "").split("-");
|
|
745
|
+
if (segments.length === 0) return null;
|
|
746
|
+
|
|
747
|
+
// Build path greedily: at each step, try joining the next segment with a dash first
|
|
748
|
+
// (preserving directory names like "circl-infra"), fall back to slash (new path segment)
|
|
749
|
+
let current = "/" + segments[0];
|
|
750
|
+
for (let i = 1; i < segments.length; i++) {
|
|
751
|
+
const withDash = current + "-" + segments[i];
|
|
752
|
+
const withSlash = current + "/" + segments[i];
|
|
753
|
+
// Prefer dash (keeps compound names intact) if that path prefix exists
|
|
754
|
+
if (await fs.pathExists(withDash)) {
|
|
755
|
+
current = withDash;
|
|
756
|
+
} else {
|
|
757
|
+
current = withSlash;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return (await fs.pathExists(current)) ? current : null;
|
|
762
|
+
}
|
|
763
|
+
|
|
716
764
|
export async function migrateMarketplaceRename(
|
|
717
765
|
projectPath?: string,
|
|
718
766
|
): Promise<MigrationResult> {
|
|
@@ -885,6 +933,40 @@ export async function migrateMarketplaceRename(
|
|
|
885
933
|
/* skip if unreadable */
|
|
886
934
|
}
|
|
887
935
|
|
|
936
|
+
// 6. Scan all known project settings (derived from ~/.claude/projects/ directory names)
|
|
937
|
+
try {
|
|
938
|
+
const projectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
939
|
+
if (await fs.pathExists(projectsDir)) {
|
|
940
|
+
const entries = await fs.readdir(projectsDir);
|
|
941
|
+
const seenPaths = new Set<string>();
|
|
942
|
+
// Current project (from step 1) already handled — skip it
|
|
943
|
+
const currentProject = projectPath || process.cwd();
|
|
944
|
+
seenPaths.add(currentProject);
|
|
945
|
+
|
|
946
|
+
for (const entry of entries) {
|
|
947
|
+
const decoded = await decodeProjectDirName(entry);
|
|
948
|
+
if (!decoded || seenPaths.has(decoded)) continue;
|
|
949
|
+
seenPaths.add(decoded);
|
|
950
|
+
|
|
951
|
+
const settingsFile = path.join(decoded, ".claude", "settings.json");
|
|
952
|
+
try {
|
|
953
|
+
if (await fs.pathExists(settingsFile)) {
|
|
954
|
+
const raw = await fs.readJson(settingsFile);
|
|
955
|
+
const count = migrateSettingsObject(raw);
|
|
956
|
+
if (count > 0) {
|
|
957
|
+
await fs.writeJson(settingsFile, raw, { spaces: 2 });
|
|
958
|
+
result.projectMigrated += count;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
} catch {
|
|
962
|
+
/* skip individual projects that fail */
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
} catch {
|
|
967
|
+
/* non-fatal: cross-project scan is best-effort */
|
|
968
|
+
}
|
|
969
|
+
|
|
888
970
|
return result;
|
|
889
971
|
}
|
|
890
972
|
|
|
@@ -4,7 +4,15 @@ export async function readSettingValue(setting, scope, projectPath) {
|
|
|
4
4
|
const settings = scope === "user"
|
|
5
5
|
? await readGlobalSettings()
|
|
6
6
|
: await readSettings(projectPath);
|
|
7
|
-
if (setting.storage.type === "
|
|
7
|
+
if (setting.storage.type === "attribution") {
|
|
8
|
+
// Attribution is an object: { commit: "", pr: "" } means disabled
|
|
9
|
+
const attr = settings.attribution;
|
|
10
|
+
if (attr && attr.commit === "" && attr.pr === "") {
|
|
11
|
+
return "false";
|
|
12
|
+
}
|
|
13
|
+
return undefined; // default (enabled)
|
|
14
|
+
}
|
|
15
|
+
else if (setting.storage.type === "env") {
|
|
8
16
|
const env = settings.env;
|
|
9
17
|
return env?.[setting.storage.key];
|
|
10
18
|
}
|
|
@@ -23,7 +31,16 @@ export async function writeSettingValue(setting, value, scope, projectPath) {
|
|
|
23
31
|
const settings = scope === "user"
|
|
24
32
|
? await readGlobalSettings()
|
|
25
33
|
: await readSettings(projectPath);
|
|
26
|
-
if (setting.storage.type === "
|
|
34
|
+
if (setting.storage.type === "attribution") {
|
|
35
|
+
// Boolean toggle: "false" -> write { commit: "", pr: "" }, "true"/undefined -> delete key
|
|
36
|
+
if (value === "false") {
|
|
37
|
+
settings.attribution = { commit: "", pr: "" };
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
delete settings.attribution;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (setting.storage.type === "env") {
|
|
27
44
|
// Write to the "env" block in settings.json
|
|
28
45
|
settings.env = settings.env || {};
|
|
29
46
|
if (value === undefined || value === "" || value === setting.defaultValue) {
|
|
@@ -19,7 +19,14 @@ export async function readSettingValue(
|
|
|
19
19
|
? await readGlobalSettings()
|
|
20
20
|
: await readSettings(projectPath);
|
|
21
21
|
|
|
22
|
-
if (setting.storage.type === "
|
|
22
|
+
if (setting.storage.type === "attribution") {
|
|
23
|
+
// Attribution is an object: { commit: "", pr: "" } means disabled
|
|
24
|
+
const attr = (settings as any).attribution;
|
|
25
|
+
if (attr && attr.commit === "" && attr.pr === "") {
|
|
26
|
+
return "false";
|
|
27
|
+
}
|
|
28
|
+
return undefined; // default (enabled)
|
|
29
|
+
} else if (setting.storage.type === "env") {
|
|
23
30
|
const env = (settings as any).env as Record<string, string> | undefined;
|
|
24
31
|
return env?.[setting.storage.key];
|
|
25
32
|
} else {
|
|
@@ -45,7 +52,14 @@ export async function writeSettingValue(
|
|
|
45
52
|
? await readGlobalSettings()
|
|
46
53
|
: await readSettings(projectPath);
|
|
47
54
|
|
|
48
|
-
if (setting.storage.type === "
|
|
55
|
+
if (setting.storage.type === "attribution") {
|
|
56
|
+
// Boolean toggle: "false" -> write { commit: "", pr: "" }, "true"/undefined -> delete key
|
|
57
|
+
if (value === "false") {
|
|
58
|
+
(settings as any).attribution = { commit: "", pr: "" };
|
|
59
|
+
} else {
|
|
60
|
+
delete (settings as any).attribution;
|
|
61
|
+
}
|
|
62
|
+
} else if (setting.storage.type === "env") {
|
|
49
63
|
// Write to the "env" block in settings.json
|
|
50
64
|
(settings as any).env = (settings as any).env || {};
|
|
51
65
|
if (value === undefined || value === "" || value === setting.defaultValue) {
|
|
@@ -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
|
}
|