claudeup 4.25.0 → 4.26.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": "4.25.0",
3
+ "version": "4.26.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -64,8 +64,8 @@
64
64
  "typescript": "^5.6.3"
65
65
  },
66
66
  "optionalDependencies": {
67
- "claudeup-darwin-arm64": "4.25.0",
68
- "claudeup-darwin-x64": "4.25.0",
69
- "claudeup-linux-x64": "4.25.0"
67
+ "claudeup-darwin-arm64": "4.26.0",
68
+ "claudeup-darwin-x64": "4.26.0",
69
+ "claudeup-linux-x64": "4.26.0"
70
70
  }
71
71
  }
@@ -0,0 +1,81 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import {
3
+ clearMarketplaceCache,
4
+ fetchMarketplacePlugins,
5
+ getMarketplaceVersion,
6
+ } from "../services/marketplace-fetcher";
7
+
8
+ /**
9
+ * Coverage for capturing a marketplace's `metadata.version` during a fetch.
10
+ *
11
+ * The plugins TUI header renders "Name vX.Y.Z (count)" when a marketplace
12
+ * declares a version, so `fetchMarketplacePlugins` must stash the version and
13
+ * expose it via `getMarketplaceVersion`. Marketplaces without a version must
14
+ * yield `undefined` rather than crashing or leaking a stray "vundefined".
15
+ */
16
+
17
+ const realFetch = globalThis.fetch;
18
+
19
+ function mockFetchOnce(body: unknown): void {
20
+ globalThis.fetch = (async () =>
21
+ new Response(JSON.stringify(body), {
22
+ status: 200,
23
+ headers: { "content-type": "application/json" },
24
+ })) as typeof fetch;
25
+ }
26
+
27
+ describe("fetchMarketplacePlugins — metadata.version capture", () => {
28
+ beforeEach(() => {
29
+ clearMarketplaceCache();
30
+ });
31
+
32
+ afterEach(() => {
33
+ globalThis.fetch = realFetch;
34
+ clearMarketplaceCache();
35
+ });
36
+
37
+ it("captures metadata.version so getMarketplaceVersion returns it", async () => {
38
+ mockFetchOnce({
39
+ metadata: { version: "8.0.0" },
40
+ plugins: [{ name: "dev", description: "Dev workflow" }],
41
+ });
42
+
43
+ await fetchMarketplacePlugins("magus", "MadAppGang/magus");
44
+
45
+ expect(getMarketplaceVersion("magus")).toBe("8.0.0");
46
+ });
47
+
48
+ it("yields undefined when marketplace.json has no metadata.version", async () => {
49
+ mockFetchOnce({
50
+ plugins: [{ name: "core", description: "Anthropic core plugin" }],
51
+ });
52
+
53
+ await fetchMarketplacePlugins("official", "anthropics/official");
54
+
55
+ expect(getMarketplaceVersion("official")).toBeUndefined();
56
+ });
57
+
58
+ it("yields undefined for a marketplace with metadata but no version field", async () => {
59
+ mockFetchOnce({
60
+ metadata: { description: "no version here" },
61
+ plugins: [{ name: "core", description: "" }],
62
+ });
63
+
64
+ await fetchMarketplacePlugins("no-version", "owner/no-version");
65
+
66
+ expect(getMarketplaceVersion("no-version")).toBeUndefined();
67
+ });
68
+
69
+ it("clears captured versions on clearMarketplaceCache", async () => {
70
+ mockFetchOnce({
71
+ metadata: { version: "1.0.0" },
72
+ plugins: [],
73
+ });
74
+
75
+ await fetchMarketplacePlugins("magus-marketing", "MadAppGang/magus-marketing");
76
+ expect(getMarketplaceVersion("magus-marketing")).toBe("1.0.0");
77
+
78
+ clearMarketplaceCache();
79
+ expect(getMarketplaceVersion("magus-marketing")).toBeUndefined();
80
+ });
81
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Change B: our own marketplaces (magus, magus-marketing) are always listed in
3
+ * the plugins TUI — even when unregistered — and sorted to the top.
4
+ *
5
+ * `magus-marketing` is deliberately NOT `featured`, so before this change it was
6
+ * invisible until manually registered. The `owned` flag both (a) qualifies it
7
+ * for the plugin-manager "always include" gate and (b) sorts it first.
8
+ */
9
+ import { describe, expect, it } from "bun:test";
10
+ import {
11
+ defaultMarketplaces,
12
+ getAllMarketplaces,
13
+ } from "../data/marketplaces.js";
14
+ import type { LocalMarketplace } from "../services/local-marketplace.js";
15
+ import type { Marketplace } from "../types/index.js";
16
+ import { buildPluginBrowserItems } from "../ui/adapters/pluginsAdapter.js";
17
+
18
+ describe("owned marketplaces", () => {
19
+ it("marks magus and magus-marketing as owned, and leaves others untouched", () => {
20
+ const magus = defaultMarketplaces.find((m) => m.name === "magus");
21
+ const marketing = defaultMarketplaces.find(
22
+ (m) => m.name === "magus-marketing",
23
+ );
24
+ expect(magus?.owned).toBe(true);
25
+ expect(marketing?.owned).toBe(true);
26
+ expect(
27
+ defaultMarketplaces.find((m) => m.name === "claude-plugins-official")
28
+ ?.owned,
29
+ ).toBeUndefined();
30
+ });
31
+
32
+ it("qualifies magus-marketing for the always-show gate without being featured", () => {
33
+ const marketing = defaultMarketplaces.find(
34
+ (m) => m.name === "magus-marketing",
35
+ );
36
+ expect(marketing).toBeDefined();
37
+ // The plugin-manager gate: configured || official || featured || owned.
38
+ const alwaysShown = !!(
39
+ marketing?.official ||
40
+ marketing?.featured ||
41
+ marketing?.owned
42
+ );
43
+ expect(alwaysShown).toBe(true);
44
+ expect(marketing?.featured).toBeUndefined(); // and NOT via featured
45
+ });
46
+
47
+ it("sorts owned first and deprecated last, regardless of input order", () => {
48
+ const mk = (name: string, extra: Partial<Marketplace>): Marketplace => ({
49
+ name,
50
+ displayName: name,
51
+ source: { source: "github", repo: `x/${name}` },
52
+ description: "",
53
+ ...extra,
54
+ });
55
+ // Deliberately "wrong" order: official, deprecated, owned.
56
+ const marketplaces: Marketplace[] = [
57
+ mk("official-one", { official: true }),
58
+ mk("claude-code-plugins", { deprecated: true }),
59
+ mk("mine", { owned: true }),
60
+ ];
61
+
62
+ const items = buildPluginBrowserItems({
63
+ marketplaces,
64
+ plugins: [],
65
+ collapsedMarketplaces: new Set(),
66
+ });
67
+
68
+ const order: string[] = [];
69
+ for (const i of items)
70
+ if (i.kind === "category") order.push(i.marketplace.name);
71
+
72
+ expect(order[0]).toBe("mine"); // owned first
73
+ expect(order[order.length - 1]).toBe("claude-code-plugins"); // deprecated last
74
+ });
75
+
76
+ it("preserves owned when getAllMarketplaces rebuilds a locally-cached marketplace", () => {
77
+ // The local-cache path reconstructs the Marketplace object; owned must
78
+ // survive so already-cloned magus still sorts first / always shows.
79
+ const local = new Map<string, LocalMarketplace>([
80
+ [
81
+ "magus",
82
+ {
83
+ name: "Magus",
84
+ description: "",
85
+ gitRepo: "MadAppGang/magus",
86
+ plugins: [],
87
+ },
88
+ ],
89
+ ]);
90
+ const magus = getAllMarketplaces(local).find((m) => m.name === "magus");
91
+ expect(magus?.owned).toBe(true);
92
+ });
93
+ });
@@ -1,5 +1,5 @@
1
- import type { Marketplace } from "../types/index.js";
2
1
  import type { LocalMarketplace } from "../services/local-marketplace.js";
2
+ import type { Marketplace } from "../types/index.js";
3
3
  import { formatMarketplaceName } from "../utils/string-utils.js";
4
4
 
5
5
  /**
@@ -38,6 +38,7 @@ export const defaultMarketplaces: Marketplace[] = [
38
38
  "Professional plugins for frontend, backend, and code analysis",
39
39
  official: false,
40
40
  featured: true, // Always show plugins (expanded by default)
41
+ owned: true, // Ours: always listed, sorted to the top
41
42
  },
42
43
  {
43
44
  name: "magus-marketing",
@@ -49,6 +50,7 @@ export const defaultMarketplaces: Marketplace[] = [
49
50
  description:
50
51
  "SEO, cold email outreach, AI image generation, and video editing plugins",
51
52
  official: false,
53
+ owned: true, // Ours: always listed even when uninstalled, sorted to the top
52
54
  // Not featured: a niche channel, so keep it collapsed by default.
53
55
  },
54
56
  {
@@ -138,6 +140,7 @@ export function getAllMarketplaces(
138
140
  defaultMp?.official ?? repo.toLowerCase().includes("anthropics/"),
139
141
  featured: defaultMp?.featured,
140
142
  deprecated: defaultMp?.deprecated,
143
+ owned: defaultMp?.owned,
141
144
  });
142
145
  }
143
146
  }
@@ -236,7 +236,6 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
236
236
  "statusline",
237
237
  "multimodel",
238
238
  "gtd",
239
- "agentdev",
240
239
  ],
241
240
  anthropicPlugins: [
242
241
  "code-review",
@@ -23,8 +23,26 @@ export interface MarketplacePlugin {
23
23
  // Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
24
24
  const marketplaceCache = new Map<string, MarketplacePlugin[]>();
25
25
 
26
+ // Session-level cache for each marketplace's declared version (from
27
+ // marketplace.json's top-level `metadata.version`). Mirrors `marketplaceCache`
28
+ // so the TUI can surface a marketplace's version without re-fetching. Only
29
+ // populated for marketplaces that actually declare a version.
30
+ const marketplaceVersionCache = new Map<string, string>();
31
+
26
32
  export function clearMarketplaceCache(): void {
27
33
  marketplaceCache.clear();
34
+ marketplaceVersionCache.clear();
35
+ }
36
+
37
+ /**
38
+ * Returns the version a marketplace declared in its `metadata.version`, if one
39
+ * was seen during a successful fetch this session. Returns `undefined` for
40
+ * marketplaces that declare no version (or haven't been fetched yet).
41
+ */
42
+ export function getMarketplaceVersion(
43
+ marketplaceName: string,
44
+ ): string | undefined {
45
+ return marketplaceVersionCache.get(marketplaceName);
28
46
  }
29
47
 
30
48
  export async function fetchMarketplacePlugins(
@@ -75,9 +93,19 @@ export async function fetchMarketplacePlugins(
75
93
  homepage?: string;
76
94
  tags?: string[];
77
95
  }
78
- const data = (await response.json()) as { plugins?: RawPlugin[] };
96
+ const data = (await response.json()) as {
97
+ plugins?: RawPlugin[];
98
+ metadata?: { version?: string };
99
+ };
79
100
  const plugins: MarketplacePlugin[] = [];
80
101
 
102
+ // Capture the marketplace's declared version (if any) so the TUI can
103
+ // display it. Absent/blank versions are simply not cached.
104
+ const version = data.metadata?.version;
105
+ if (typeof version === "string" && version.length > 0) {
106
+ marketplaceVersionCache.set(marketplaceName, version);
107
+ }
108
+
81
109
  if (data.plugins && Array.isArray(data.plugins)) {
82
110
  for (const plugin of data.plugins) {
83
111
  plugins.push({
@@ -154,7 +154,12 @@ export async function getAvailablePlugins(
154
154
  // Always include official and featured marketplaces so users can browse them
155
155
  const marketplaceNames = new Set<string>();
156
156
  for (const mp of defaultMarketplaces) {
157
- if (configuredMarketplaces[mp.name] || mp.official || mp.featured) {
157
+ if (
158
+ configuredMarketplaces[mp.name] ||
159
+ mp.official ||
160
+ mp.featured ||
161
+ mp.owned
162
+ ) {
158
163
  marketplaceNames.add(mp.name);
159
164
  }
160
165
  }
@@ -339,7 +344,12 @@ export async function getGlobalAvailablePlugins(): Promise<PluginInfo[]> {
339
344
  // Always include official and featured marketplaces so users can browse them
340
345
  const marketplaceNames = new Set<string>();
341
346
  for (const mp of defaultMarketplaces) {
342
- if (configuredMarketplaces[mp.name] || mp.official || mp.featured) {
347
+ if (
348
+ configuredMarketplaces[mp.name] ||
349
+ mp.official ||
350
+ mp.featured ||
351
+ mp.owned
352
+ ) {
343
353
  marketplaceNames.add(mp.name);
344
354
  }
345
355
  }
@@ -42,6 +42,7 @@ export interface Marketplace {
42
42
  official?: boolean;
43
43
  featured?: boolean; // Featured marketplaces have plugins fetched by default (like official)
44
44
  deprecated?: boolean; // Deprecated marketplaces are sorted to the bottom of the list
45
+ owned?: boolean; // Our own marketplaces (magus*): always listed even when uninstalled, and sorted to the top
45
46
  }
46
47
 
47
48
  export interface DiscoveredMarketplace {
@@ -97,12 +97,10 @@ export function buildPluginBrowserItems({
97
97
  pluginsByMarketplace.set(plugin.marketplace, existing);
98
98
  }
99
99
 
100
- // Sort marketplaces: deprecated ones go to the bottom
101
- const sortedMarketplaces = [...marketplaces].sort((a, b) => {
102
- const aDeprecated = a.name === "claude-code-plugins" ? 1 : 0;
103
- const bDeprecated = b.name === "claude-code-plugins" ? 1 : 0;
104
- return aDeprecated - bDeprecated;
105
- });
100
+ // Sort marketplaces: our own (magus*) first, deprecated last, others between.
101
+ const rank = (m: Marketplace) =>
102
+ m.owned ? 0 : m.deprecated || m.name === "claude-code-plugins" ? 2 : 1;
103
+ const sortedMarketplaces = [...marketplaces].sort((a, b) => rank(a) - rank(b));
106
104
 
107
105
  const items: PluginBrowserItem[] = [];
108
106
 
@@ -3,6 +3,8 @@ import React from "react";
3
3
  interface CategoryHeaderProps {
4
4
  /** Category title */
5
5
  title: string;
6
+ /** Optional version to render after the title (e.g., "8.0.0" → " v8.0.0") */
7
+ version?: string;
6
8
  /** Status badge (e.g., "✓ Configured", "3 plugins") */
7
9
  status?: string;
8
10
  /** Status badge color */
@@ -15,12 +17,14 @@ interface CategoryHeaderProps {
15
17
 
16
18
  export function CategoryHeader({
17
19
  title,
20
+ version,
18
21
  status,
19
22
  statusColor = "green",
20
23
  expanded = true,
21
24
  count,
22
25
  }: CategoryHeaderProps) {
23
26
  const expandIcon = expanded ? "▼" : "▶";
27
+ const versionBadge = version ? ` v${version}` : "";
24
28
  const countBadge = count !== undefined ? ` (${count})` : "";
25
29
  const statusText = status ? ` ${status}` : "";
26
30
 
@@ -31,6 +35,7 @@ export function CategoryHeader({
31
35
  <span fg="white">
32
36
  <strong> {title}</strong>
33
37
  </span>
38
+ <span fg="#666666">{versionBadge}</span>
34
39
  <span fg="#666666">{countBadge}</span>
35
40
  <span fg="#666666"> ────</span>
36
41
  <span fg={statusColor}>{statusText}</span>
@@ -15,21 +15,27 @@ import {
15
15
  } from "../components/primitives/index.js";
16
16
  import { theme } from "../theme.js";
17
17
  import { highlightMatches } from "../../utils/fuzzy-search.js";
18
+ import { getMarketplaceVersion } from "../../services/marketplace-fetcher.js";
18
19
  import { isEnabledButNotInstalled } from "../../services/plugin-manager.js";
19
20
 
20
21
  // ─── Category renderers ───────────────────────────────────────────────────────
21
22
 
22
23
  function categoryRow(item: PluginCategoryItem, isSelected: boolean): React.ReactNode {
23
24
  const mp = item.marketplace;
25
+ // Marketplaces that declare a `metadata.version` render "Name v8.0.0 (15)";
26
+ // those without keep the plain "Name (15)" form.
27
+ const version = getMarketplaceVersion(mp.name);
24
28
 
25
29
  if (isSelected) {
26
30
  const arrow = item.isExpanded ? "▼" : "▶";
31
+ const versionLabel = version ? ` v${version}` : "";
27
32
  const count = item.pluginCount > 0 ? ` (${item.pluginCount})` : "";
28
33
  return (
29
34
  <SelectableRow selected={true}>
30
35
  <strong>
31
36
  {" "}
32
37
  {arrow} {mp.displayName}
38
+ {versionLabel}
33
39
  {count}{" "}
34
40
  </strong>
35
41
  </SelectableRow>
@@ -49,6 +55,7 @@ function categoryRow(item: PluginCategoryItem, isSelected: boolean): React.React
49
55
  return (
50
56
  <CategoryHeader
51
57
  title={mp.displayName}
58
+ version={version}
52
59
  expanded={item.isExpanded}
53
60
  count={item.pluginCount}
54
61
  status={item.badge}