claudeup 3.4.0 → 3.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -1,4 +1,23 @@
1
1
  import { formatMarketplaceName } from "../utils/string-utils.js";
2
+ /**
3
+ * Known repo aliases — GitHub redirects the old repo to the new one,
4
+ * so these should be treated as identical for deduplication.
5
+ */
6
+ const REPO_ALIASES = {
7
+ "madappgang/claude-code": "madappgang/magus",
8
+ };
9
+ /** Normalize a repo string through known aliases. */
10
+ function normalizeRepo(repo) {
11
+ const lower = repo.toLowerCase();
12
+ return REPO_ALIASES[lower] || lower;
13
+ }
14
+ /**
15
+ * Deprecated marketplace names that should be hidden if the canonical
16
+ * marketplace is already present. Maps deprecated name → canonical name.
17
+ */
18
+ export const deprecatedMarketplaces = {
19
+ "mag-claude-plugins": "magus",
20
+ };
2
21
  export const defaultMarketplaces = [
3
22
  {
4
23
  name: "magus",
@@ -37,23 +56,36 @@ export function getMarketplaceByName(name) {
37
56
  }
38
57
  // Get all available marketplaces from local cache + hardcoded defaults
39
58
  // Local cache is primary source of truth, defaults are fallback
40
- // Deduplicates by repo URL to avoid showing same marketplace twice
59
+ // Deduplicates by normalized repo URL to avoid showing same marketplace twice
41
60
  export function getAllMarketplaces(localMarketplaces) {
42
61
  const all = new Map();
43
62
  const seenRepos = new Set();
44
63
  // Primary source: local cache (what's actually cloned)
45
64
  if (localMarketplaces) {
46
65
  for (const [name, local] of localMarketplaces) {
47
- const repo = local.gitRepo || "";
66
+ const repo = normalizeRepo(local.gitRepo || "");
67
+ // Skip deprecated marketplaces if their canonical replacement exists
68
+ // (or will be added from defaults)
69
+ const canonical = deprecatedMarketplaces[name];
70
+ if (canonical) {
71
+ // If canonical already in the map or in defaults, skip this entry
72
+ if (all.has(canonical) || defaultMarketplaces.some((m) => m.name === canonical)) {
73
+ continue;
74
+ }
75
+ }
76
+ // Skip if another marketplace already claimed this repo URL
77
+ if (repo && seenRepos.has(repo))
78
+ continue;
48
79
  if (repo)
49
- seenRepos.add(repo.toLowerCase());
80
+ seenRepos.add(repo);
50
81
  // Check if this marketplace has defaults (for official/featured flags)
51
82
  const defaultMp = defaultMarketplaces.find((m) => m.name === name);
52
83
  all.set(name, {
53
84
  name,
54
- displayName: local.name || formatMarketplaceName(name),
55
- source: { source: "github", repo },
56
- description: local.description || "",
85
+ // Prefer default displayName over stale local clone data
86
+ displayName: defaultMp?.displayName || local.name || formatMarketplaceName(name),
87
+ source: { source: "github", repo: defaultMp?.source.repo || local.gitRepo || "" },
88
+ description: defaultMp?.description || local.description || "",
57
89
  official: defaultMp?.official ?? repo.toLowerCase().includes("anthropics/"),
58
90
  featured: defaultMp?.featured,
59
91
  });
@@ -61,7 +93,7 @@ export function getAllMarketplaces(localMarketplaces) {
61
93
  }
62
94
  // Fallback: hardcoded defaults (only if their repo isn't already represented)
63
95
  for (const mp of defaultMarketplaces) {
64
- const repo = mp.source.repo?.toLowerCase() || "";
96
+ const repo = normalizeRepo(mp.source.repo || "");
65
97
  if (!all.has(mp.name) && !seenRepos.has(repo)) {
66
98
  all.set(mp.name, mp);
67
99
  if (repo)
@@ -2,6 +2,28 @@ import type { Marketplace } from "../types/index.js";
2
2
  import type { LocalMarketplace } from "../services/local-marketplace.js";
3
3
  import { formatMarketplaceName } from "../utils/string-utils.js";
4
4
 
5
+ /**
6
+ * Known repo aliases — GitHub redirects the old repo to the new one,
7
+ * so these should be treated as identical for deduplication.
8
+ */
9
+ const REPO_ALIASES: Record<string, string> = {
10
+ "madappgang/claude-code": "madappgang/magus",
11
+ };
12
+
13
+ /** Normalize a repo string through known aliases. */
14
+ function normalizeRepo(repo: string): string {
15
+ const lower = repo.toLowerCase();
16
+ return REPO_ALIASES[lower] || lower;
17
+ }
18
+
19
+ /**
20
+ * Deprecated marketplace names that should be hidden if the canonical
21
+ * marketplace is already present. Maps deprecated name → canonical name.
22
+ */
23
+ export const deprecatedMarketplaces: Record<string, string> = {
24
+ "mag-claude-plugins": "magus",
25
+ };
26
+
5
27
  export const defaultMarketplaces: Marketplace[] = [
6
28
  {
7
29
  name: "magus",
@@ -45,7 +67,7 @@ export function getMarketplaceByName(name: string): Marketplace | undefined {
45
67
 
46
68
  // Get all available marketplaces from local cache + hardcoded defaults
47
69
  // Local cache is primary source of truth, defaults are fallback
48
- // Deduplicates by repo URL to avoid showing same marketplace twice
70
+ // Deduplicates by normalized repo URL to avoid showing same marketplace twice
49
71
  export function getAllMarketplaces(
50
72
  localMarketplaces?: Map<string, LocalMarketplace>,
51
73
  ): Marketplace[] {
@@ -55,17 +77,31 @@ export function getAllMarketplaces(
55
77
  // Primary source: local cache (what's actually cloned)
56
78
  if (localMarketplaces) {
57
79
  for (const [name, local] of localMarketplaces) {
58
- const repo = local.gitRepo || "";
59
- if (repo) seenRepos.add(repo.toLowerCase());
80
+ const repo = normalizeRepo(local.gitRepo || "");
81
+
82
+ // Skip deprecated marketplaces if their canonical replacement exists
83
+ // (or will be added from defaults)
84
+ const canonical = deprecatedMarketplaces[name];
85
+ if (canonical) {
86
+ // If canonical already in the map or in defaults, skip this entry
87
+ if (all.has(canonical) || defaultMarketplaces.some((m) => m.name === canonical)) {
88
+ continue;
89
+ }
90
+ }
91
+
92
+ // Skip if another marketplace already claimed this repo URL
93
+ if (repo && seenRepos.has(repo)) continue;
94
+ if (repo) seenRepos.add(repo);
60
95
 
61
96
  // Check if this marketplace has defaults (for official/featured flags)
62
97
  const defaultMp = defaultMarketplaces.find((m) => m.name === name);
63
98
 
64
99
  all.set(name, {
65
100
  name,
66
- displayName: local.name || formatMarketplaceName(name),
67
- source: { source: "github" as const, repo },
68
- description: local.description || "",
101
+ // Prefer default displayName over stale local clone data
102
+ displayName: defaultMp?.displayName || local.name || formatMarketplaceName(name),
103
+ source: { source: "github" as const, repo: defaultMp?.source.repo || local.gitRepo || "" },
104
+ description: defaultMp?.description || local.description || "",
69
105
  official:
70
106
  defaultMp?.official ?? repo.toLowerCase().includes("anthropics/"),
71
107
  featured: defaultMp?.featured,
@@ -75,7 +111,7 @@ export function getAllMarketplaces(
75
111
 
76
112
  // Fallback: hardcoded defaults (only if their repo isn't already represented)
77
113
  for (const mp of defaultMarketplaces) {
78
- const repo = mp.source.repo?.toLowerCase() || "";
114
+ const repo = normalizeRepo(mp.source.repo || "");
79
115
  if (!all.has(mp.name) && !seenRepos.has(repo)) {
80
116
  all.set(mp.name, mp);
81
117
  if (repo) seenRepos.add(repo);
@@ -519,23 +519,70 @@ export async function migrateMarketplaceRename(projectPath) {
519
519
  }
520
520
  }
521
521
  catch { /* skip if unreadable */ }
522
- // 4. known_marketplaces.json — rename the key
522
+ // 4. known_marketplaces.json — rename the key + physical directory cleanup
523
+ const pluginsDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
524
+ const oldDir = path.join(pluginsDir, OLD_MARKETPLACE_NAME);
525
+ const newDir = path.join(pluginsDir, NEW_MARKETPLACE_NAME);
523
526
  try {
524
527
  const known = await readKnownMarketplaces();
528
+ let knownModified = false;
525
529
  if (known[OLD_MARKETPLACE_NAME]) {
526
- known[NEW_MARKETPLACE_NAME] = {
527
- ...known[OLD_MARKETPLACE_NAME],
528
- source: {
529
- ...known[OLD_MARKETPLACE_NAME].source,
530
- repo: "MadAppGang/magus",
531
- },
532
- };
530
+ const oldEntry = known[OLD_MARKETPLACE_NAME];
531
+ // If canonical entry already exists, just delete the old one
532
+ if (!known[NEW_MARKETPLACE_NAME]) {
533
+ known[NEW_MARKETPLACE_NAME] = {
534
+ ...oldEntry,
535
+ source: {
536
+ ...oldEntry.source,
537
+ repo: "MadAppGang/magus",
538
+ },
539
+ };
540
+ }
533
541
  delete known[OLD_MARKETPLACE_NAME];
542
+ knownModified = true;
543
+ }
544
+ // Ensure installLocation points to new directory name
545
+ if (known[NEW_MARKETPLACE_NAME]?.installLocation?.includes(OLD_MARKETPLACE_NAME)) {
546
+ known[NEW_MARKETPLACE_NAME].installLocation =
547
+ known[NEW_MARKETPLACE_NAME].installLocation.replace(OLD_MARKETPLACE_NAME, NEW_MARKETPLACE_NAME);
548
+ knownModified = true;
549
+ }
550
+ if (knownModified) {
534
551
  await writeKnownMarketplaces(known);
535
552
  result.knownMarketplacesMigrated = true;
536
553
  }
537
554
  }
538
555
  catch { /* skip if unreadable */ }
556
+ // 4b. Rename/remove the old physical directory (runs even if key was already migrated)
557
+ try {
558
+ if (await fs.pathExists(oldDir)) {
559
+ if (!(await fs.pathExists(newDir))) {
560
+ await fs.rename(oldDir, newDir);
561
+ }
562
+ else {
563
+ // Both exist — remove the old one (magus dir is canonical)
564
+ await fs.remove(oldDir);
565
+ }
566
+ }
567
+ }
568
+ catch { /* non-fatal: directory cleanup is best-effort */ }
569
+ // 4c. Update git remote URL in the marketplace clone (old → new repo)
570
+ try {
571
+ const marketplaceDir = await fs.pathExists(newDir) ? newDir : oldDir;
572
+ if (await fs.pathExists(path.join(marketplaceDir, ".git"))) {
573
+ const { execSync } = await import("node:child_process");
574
+ const remote = execSync("git remote get-url origin", {
575
+ cwd: marketplaceDir, encoding: "utf-8", timeout: 5000,
576
+ }).trim();
577
+ if (remote.includes("claude-code") && remote.includes("MadAppGang")) {
578
+ const newRemote = remote.replace("claude-code", NEW_MARKETPLACE_NAME);
579
+ execSync(`git remote set-url origin "${newRemote}"`, {
580
+ cwd: marketplaceDir, encoding: "utf-8", timeout: 5000,
581
+ });
582
+ }
583
+ }
584
+ }
585
+ catch { /* non-fatal: git remote update is best-effort */ }
539
586
  // 5. installed_plugins.json — rename plugin ID keys
540
587
  try {
541
588
  const registry = await readInstalledPluginsRegistry();
@@ -748,23 +748,77 @@ export async function migrateMarketplaceRename(
748
748
  }
749
749
  } catch { /* skip if unreadable */ }
750
750
 
751
- // 4. known_marketplaces.json — rename the key
751
+ // 4. known_marketplaces.json — rename the key + physical directory cleanup
752
+ const pluginsDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
753
+ const oldDir = path.join(pluginsDir, OLD_MARKETPLACE_NAME);
754
+ const newDir = path.join(pluginsDir, NEW_MARKETPLACE_NAME);
755
+
752
756
  try {
753
757
  const known = await readKnownMarketplaces();
758
+ let knownModified = false;
759
+
754
760
  if (known[OLD_MARKETPLACE_NAME]) {
755
- known[NEW_MARKETPLACE_NAME] = {
756
- ...known[OLD_MARKETPLACE_NAME],
757
- source: {
758
- ...known[OLD_MARKETPLACE_NAME].source,
759
- repo: "MadAppGang/magus",
760
- },
761
- };
761
+ const oldEntry = known[OLD_MARKETPLACE_NAME];
762
+
763
+ // If canonical entry already exists, just delete the old one
764
+ if (!known[NEW_MARKETPLACE_NAME]) {
765
+ known[NEW_MARKETPLACE_NAME] = {
766
+ ...oldEntry,
767
+ source: {
768
+ ...oldEntry.source,
769
+ repo: "MadAppGang/magus",
770
+ },
771
+ };
772
+ }
762
773
  delete known[OLD_MARKETPLACE_NAME];
774
+ knownModified = true;
775
+ }
776
+
777
+ // Ensure installLocation points to new directory name
778
+ if (known[NEW_MARKETPLACE_NAME]?.installLocation?.includes(OLD_MARKETPLACE_NAME)) {
779
+ known[NEW_MARKETPLACE_NAME].installLocation =
780
+ known[NEW_MARKETPLACE_NAME].installLocation.replace(
781
+ OLD_MARKETPLACE_NAME,
782
+ NEW_MARKETPLACE_NAME,
783
+ );
784
+ knownModified = true;
785
+ }
786
+
787
+ if (knownModified) {
763
788
  await writeKnownMarketplaces(known);
764
789
  result.knownMarketplacesMigrated = true;
765
790
  }
766
791
  } catch { /* skip if unreadable */ }
767
792
 
793
+ // 4b. Rename/remove the old physical directory (runs even if key was already migrated)
794
+ try {
795
+ if (await fs.pathExists(oldDir)) {
796
+ if (!(await fs.pathExists(newDir))) {
797
+ await fs.rename(oldDir, newDir);
798
+ } else {
799
+ // Both exist — remove the old one (magus dir is canonical)
800
+ await fs.remove(oldDir);
801
+ }
802
+ }
803
+ } catch { /* non-fatal: directory cleanup is best-effort */ }
804
+
805
+ // 4c. Update git remote URL in the marketplace clone (old → new repo)
806
+ try {
807
+ const marketplaceDir = await fs.pathExists(newDir) ? newDir : oldDir;
808
+ if (await fs.pathExists(path.join(marketplaceDir, ".git"))) {
809
+ const { execSync } = await import("node:child_process");
810
+ const remote = execSync("git remote get-url origin", {
811
+ cwd: marketplaceDir, encoding: "utf-8", timeout: 5000,
812
+ }).trim();
813
+ if (remote.includes("claude-code") && remote.includes("MadAppGang")) {
814
+ const newRemote = remote.replace("claude-code", NEW_MARKETPLACE_NAME);
815
+ execSync(`git remote set-url origin "${newRemote}"`, {
816
+ cwd: marketplaceDir, encoding: "utf-8", timeout: 5000,
817
+ });
818
+ }
819
+ }
820
+ } catch { /* non-fatal: git remote update is best-effort */ }
821
+
768
822
  // 5. installed_plugins.json — rename plugin ID keys
769
823
  try {
770
824
  const registry = await readInstalledPluginsRegistry();