claudeup 4.23.0 → 4.24.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.23.0",
3
+ "version": "4.24.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.23.0",
68
- "claudeup-darwin-x64": "4.23.0",
69
- "claudeup-linux-x64": "4.23.0"
67
+ "claudeup-darwin-arm64": "4.24.0",
68
+ "claudeup-darwin-x64": "4.24.0",
69
+ "claudeup-linux-x64": "4.24.0"
70
70
  }
71
71
  }
@@ -0,0 +1,87 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { isEnabledButNotInstalled } from "../services/plugin-manager.js";
3
+ import type { PluginInfo } from "../services/plugin-manager.js";
4
+
5
+ /**
6
+ * "Enabled but not installed" is a broken state that previously rendered
7
+ * identically to a healthy install: a green scope square and no version.
8
+ *
9
+ * It is also invisible to every update path, because the check is
10
+ * `installedVersion && compare(latest, installedVersion) > 0` — with no
11
+ * installed version that is false, so neither the update list nor the
12
+ * prerunner's auto-update will ever touch it.
13
+ */
14
+
15
+ const plugin = (over: Partial<PluginInfo>): PluginInfo =>
16
+ ({
17
+ id: "x@magus",
18
+ name: "x",
19
+ version: "1.0.0",
20
+ description: "",
21
+ marketplace: "magus",
22
+ marketplaceDisplay: "Magus",
23
+ enabled: true,
24
+ ...over,
25
+ }) as PluginInfo;
26
+
27
+ describe("isEnabledButNotInstalled", () => {
28
+ test("enabled in project scope with no installed version is flagged", () => {
29
+ // The real case: terminal/code-analysis are in this project's enabledPlugins
30
+ // but have no registry entry for it and none at user scope.
31
+ expect(
32
+ isEnabledButNotInstalled(
33
+ plugin({ projectScope: { enabled: true }, installedVersion: undefined }),
34
+ ),
35
+ ).toBe(true);
36
+ });
37
+
38
+ test("a healthy install is not flagged", () => {
39
+ expect(
40
+ isEnabledButNotInstalled(
41
+ plugin({ projectScope: { enabled: true }, installedVersion: "1.0.0" }),
42
+ ),
43
+ ).toBe(false);
44
+ });
45
+
46
+ test('installedVersion "0.0.0" means installed-with-unknown-version, not missing', () => {
47
+ // Anthropic's official plugins record exactly this. Treating it as missing
48
+ // would mislabel every one of them as broken.
49
+ expect(
50
+ isEnabledButNotInstalled(
51
+ plugin({ userScope: { enabled: true }, installedVersion: "0.0.0" }),
52
+ ),
53
+ ).toBe(false);
54
+ });
55
+
56
+ test("not enabled anywhere is not flagged — it is simply available", () => {
57
+ expect(isEnabledButNotInstalled(plugin({ installedVersion: undefined }))).toBe(false);
58
+ });
59
+
60
+ test("orphaned plugins are excluded — they have their own 'deprecated' state", () => {
61
+ expect(
62
+ isEnabledButNotInstalled(
63
+ plugin({
64
+ isOrphaned: true,
65
+ projectScope: { enabled: true },
66
+ installedVersion: undefined,
67
+ }),
68
+ ),
69
+ ).toBe(false);
70
+ });
71
+
72
+ test("any enabled scope counts, not just project", () => {
73
+ for (const scope of ["userScope", "projectScope", "localScope"] as const) {
74
+ expect(
75
+ isEnabledButNotInstalled(plugin({ [scope]: { enabled: true } })),
76
+ ).toBe(true);
77
+ }
78
+ });
79
+
80
+ test("a scope present but disabled does not count as enabled", () => {
81
+ expect(
82
+ isEnabledButNotInstalled(
83
+ plugin({ projectScope: { enabled: false }, installedVersion: undefined }),
84
+ ),
85
+ ).toBe(false);
86
+ });
87
+ });
@@ -74,6 +74,30 @@ export interface PluginInfo {
74
74
  isOrphaned?: boolean;
75
75
  }
76
76
 
77
+ /**
78
+ * True when a plugin is enabled in some scope but no installed version resolves
79
+ * anywhere — it is listed in `enabledPlugins` but was never actually installed.
80
+ *
81
+ * This is a broken state, not a healthy one: Claude Code will not load the
82
+ * plugin, and because the update check is `installedVersion && compare(...)`,
83
+ * it is also invisible to both the update list and the prerunner's auto-update.
84
+ * It previously rendered identically to a working install.
85
+ *
86
+ * `isOrphaned` is a different failure — installed, but no longer offered by any
87
+ * marketplace — and is rendered as "deprecated" already.
88
+ *
89
+ * "0.0.0" means installed-with-unknown-version (Anthropic's official plugins
90
+ * record exactly that), so it must NOT count as missing.
91
+ */
92
+ export function isEnabledButNotInstalled(plugin: PluginInfo): boolean {
93
+ if (plugin.isOrphaned) return false;
94
+ const enabledSomewhere =
95
+ !!plugin.userScope?.enabled ||
96
+ !!plugin.projectScope?.enabled ||
97
+ !!plugin.localScope?.enabled;
98
+ return enabledSomewhere && !plugin.installedVersion;
99
+ }
100
+
77
101
 
78
102
  export async function getAvailablePlugins(
79
103
  projectPath?: string,
@@ -15,6 +15,7 @@ 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 { isEnabledButNotInstalled } from "../../services/plugin-manager.js";
18
19
 
19
20
  // ─── Category renderers ───────────────────────────────────────────────────────
20
21
 
@@ -113,10 +114,16 @@ function pluginRow(item: PluginPluginItem, isSelected: boolean): React.ReactNode
113
114
  const hasLocal = !!plugin.localScope?.enabled;
114
115
  const hasAnyScope = hasUser || hasProject || hasLocal;
115
116
 
117
+ // Enabled somewhere but nothing actually installed — a broken state that used
118
+ // to render exactly like a healthy install.
119
+ const notInstalled = isEnabledButNotInstalled(plugin);
120
+
116
121
  // Build version string — only show if plugin is installed in at least one scope
117
122
  let versionStr = "";
118
123
  if (plugin.isOrphaned) {
119
124
  versionStr = " deprecated";
125
+ } else if (notInstalled) {
126
+ versionStr = " not installed";
120
127
  } else if (hasAnyScope && plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
121
128
  versionStr = ` v${plugin.installedVersion}`;
122
129
  if (plugin.hasUpdate && plugin.version) {
@@ -158,8 +165,13 @@ function pluginRow(item: PluginPluginItem, isSelected: boolean): React.ReactNode
158
165
  <span> </span>
159
166
  <ScopeSquares user={hasUser} project={hasProject} local={hasLocal} />
160
167
  <span> </span>
161
- <span fg={hasAnyScope ? theme.colors.text : theme.colors.muted}>{displayName}</span>
162
- <MetaText text={versionStr} tone={plugin.hasUpdate ? "warning" : "muted"} />
168
+ <span fg={hasAnyScope && !notInstalled ? theme.colors.text : theme.colors.muted}>
169
+ {displayName}
170
+ </span>
171
+ <MetaText
172
+ text={versionStr}
173
+ tone={notInstalled || plugin.hasUpdate ? "warning" : "muted"}
174
+ />
163
175
  </text>
164
176
  );
165
177
  }