claudeup 4.32.0 → 4.32.1
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.32.
|
|
3
|
+
"version": "4.32.1",
|
|
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.32.
|
|
68
|
-
"claudeup-darwin-x64": "4.32.
|
|
69
|
-
"claudeup-linux-x64": "4.32.
|
|
67
|
+
"claudeup-darwin-arm64": "4.32.1",
|
|
68
|
+
"claudeup-darwin-x64": "4.32.1",
|
|
69
|
+
"claudeup-linux-x64": "4.32.1"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
isInstalledInScope,
|
|
4
|
+
resolveScopeAction,
|
|
5
|
+
} from "../services/plugin-manager.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The scope keys (u / p / l) and the Enter scope picker both decide between
|
|
9
|
+
* install, update and uninstall. That decision used to read `scope.enabled` —
|
|
10
|
+
* the `enabledPlugins` flag from a settings file — and ignore `scope.version`,
|
|
11
|
+
* which is the registry-backed answer to "did Claude Code actually install
|
|
12
|
+
* this".
|
|
13
|
+
*
|
|
14
|
+
* Those two disagree in a real, common state: a plugin listed in
|
|
15
|
+
* `enabledPlugins` with no entry in `installed_plugins.json` for the current
|
|
16
|
+
* project path. The list row renders it "not installed" (isEnabledButNotInstalled),
|
|
17
|
+
* and pressing its scope key ran `claude plugin uninstall`. The user asked to
|
|
18
|
+
* install the thing the UI told them was missing, and it was removed instead.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
describe("isInstalledInScope", () => {
|
|
22
|
+
test("enabled with a registry version is installed", () => {
|
|
23
|
+
expect(isInstalledInScope({ enabled: true, version: "3.0.2" })).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("enabled with NO version is not installed — the broken state", () => {
|
|
27
|
+
// dev@magus, measured: `.claude/settings.json` had "dev@magus": true while
|
|
28
|
+
// installed_plugins.json had no entry resolving for the project path.
|
|
29
|
+
expect(isInstalledInScope({ enabled: true })).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a version with the flag off is not installed either", () => {
|
|
33
|
+
expect(isInstalledInScope({ enabled: false, version: "3.0.2" })).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("an absent scope is not installed", () => {
|
|
37
|
+
expect(isInstalledInScope(undefined)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('"0.0.0" counts as installed — Anthropic official plugins record it', () => {
|
|
41
|
+
expect(isInstalledInScope({ enabled: true, version: "0.0.0" })).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("resolveScopeAction", () => {
|
|
46
|
+
test("REGRESSION: enabled but not installed resolves to install, never uninstall", () => {
|
|
47
|
+
// This is the reported bug. Before the fix this returned "uninstall".
|
|
48
|
+
expect(resolveScopeAction({ enabled: true }, "3.0.2")).toBe("install");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a scope that was never enabled resolves to install", () => {
|
|
52
|
+
expect(resolveScopeAction(undefined, "3.0.2")).toBe("install");
|
|
53
|
+
expect(resolveScopeAction({ enabled: false }, "3.0.2")).toBe("install");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("a healthy install at the latest version resolves to uninstall (toggle off)", () => {
|
|
57
|
+
expect(resolveScopeAction({ enabled: true, version: "3.0.2" }, "3.0.2")).toBe(
|
|
58
|
+
"uninstall",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a healthy install behind the catalog resolves to update", () => {
|
|
63
|
+
expect(resolveScopeAction({ enabled: true, version: "2.9.0" }, "3.0.2")).toBe(
|
|
64
|
+
"update",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('an unknown catalog version ("0.0.0") never fabricates an update', () => {
|
|
69
|
+
// There is nothing to update *to*, so toggling must still mean uninstall.
|
|
70
|
+
expect(resolveScopeAction({ enabled: true, version: "3.0.2" }, "0.0.0")).toBe(
|
|
71
|
+
"uninstall",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("installed with unknown version resolves to update when a real version is published", () => {
|
|
76
|
+
// "0.0.0" installed against a real catalog version is a genuine upgrade
|
|
77
|
+
// path, not the broken state — it must not be treated as uninstall.
|
|
78
|
+
expect(resolveScopeAction({ enabled: true, version: "0.0.0" }, "3.0.2")).toBe(
|
|
79
|
+
"update",
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -135,6 +135,48 @@ export function isEnabledButNotInstalled(plugin: PluginInfo): boolean {
|
|
|
135
135
|
return enabledSomewhere && !plugin.installedVersion;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* True when a scope genuinely has the plugin installed.
|
|
140
|
+
*
|
|
141
|
+
* `enabled` on its own is not proof. It is the `enabledPlugins` flag out of a
|
|
142
|
+
* settings file, and only claudeup and the user maintain it; `version` comes
|
|
143
|
+
* from `installed_plugins.json` via `overlayRegistryVersions`, which is what
|
|
144
|
+
* Claude Code actually loaded. When the two disagree — flag set, no registry
|
|
145
|
+
* entry — the plugin is in the broken enabled-but-not-installed state that the
|
|
146
|
+
* list already renders as "not installed".
|
|
147
|
+
*
|
|
148
|
+
* Every action that touches the filesystem must use this, not the flag alone.
|
|
149
|
+
* Deciding on the flag is what made the scope keys uninstall a plugin the row
|
|
150
|
+
* had just labelled "not installed".
|
|
151
|
+
*
|
|
152
|
+
* A version of "0.0.0" still counts as installed — Anthropic's official plugins
|
|
153
|
+
* record exactly that — which is why this tests presence, not `isKnownVersion`.
|
|
154
|
+
*/
|
|
155
|
+
export function isInstalledInScope(scope: ScopeStatus | undefined): boolean {
|
|
156
|
+
return !!scope?.enabled && !!scope.version;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* What a scope toggle (`u` / `p` / `l`, or the Enter scope picker) should do.
|
|
161
|
+
*
|
|
162
|
+
* `install` doubles as the repair for enabled-but-not-installed. Measured
|
|
163
|
+
* against Claude Code 2.1.223 in exactly that state — `dev@magus` flagged in
|
|
164
|
+
* `.claude/settings.json` with no registry entry for the project path —
|
|
165
|
+
* `claude plugin install dev@magus --scope project` installed it normally and
|
|
166
|
+
* wrote the registry entry. It does not report "already installed", so this
|
|
167
|
+
* needs no uninstall-first dance (unlike content drift, see repairPlugin).
|
|
168
|
+
*/
|
|
169
|
+
export function resolveScopeAction(
|
|
170
|
+
scope: ScopeStatus | undefined,
|
|
171
|
+
latestVersion: string,
|
|
172
|
+
): "install" | "update" | "uninstall" {
|
|
173
|
+
if (!isInstalledInScope(scope)) return "install";
|
|
174
|
+
const installed = scope?.version;
|
|
175
|
+
const hasUpdate =
|
|
176
|
+
!!installed && latestVersion !== "0.0.0" && installed !== latestVersion;
|
|
177
|
+
return hasUpdate ? "update" : "uninstall";
|
|
178
|
+
}
|
|
179
|
+
|
|
138
180
|
|
|
139
181
|
export async function getAvailablePlugins(
|
|
140
182
|
projectPath?: string,
|
|
@@ -16,9 +16,13 @@ import {
|
|
|
16
16
|
import { theme } from "../theme.js";
|
|
17
17
|
import { highlightMatches } from "../../utils/fuzzy-search.js";
|
|
18
18
|
import { getMarketplaceVersion } from "../../services/marketplace-fetcher.js";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
isEnabledButNotInstalled,
|
|
21
|
+
isInstalledInScope,
|
|
22
|
+
} from "../../services/plugin-manager.js";
|
|
20
23
|
import { isKnownVersion } from "../../services/version-snapshot.js";
|
|
21
24
|
import type { PluginRelease } from "../../types/index.js";
|
|
25
|
+
import type { ScopeStatus } from "../../services/plugin-manager.js";
|
|
22
26
|
|
|
23
27
|
// ─── Category renderers ───────────────────────────────────────────────────────
|
|
24
28
|
|
|
@@ -223,10 +227,14 @@ function pluginRow(item: PluginPluginItem, isSelected: boolean): React.ReactNode
|
|
|
223
227
|
|
|
224
228
|
function pluginDetail(item: PluginPluginItem): React.ReactNode {
|
|
225
229
|
const { plugin } = item;
|
|
230
|
+
// "Installed" has to mean the same thing here as in the list row. Reading the
|
|
231
|
+
// `enabledPlugins` flag alone made this panel print "● Installed" for the
|
|
232
|
+
// exact plugin the row beside it was labelling "not installed".
|
|
226
233
|
const isInstalled =
|
|
227
|
-
plugin.userScope
|
|
228
|
-
plugin.projectScope
|
|
229
|
-
plugin.localScope
|
|
234
|
+
isInstalledInScope(plugin.userScope) ||
|
|
235
|
+
isInstalledInScope(plugin.projectScope) ||
|
|
236
|
+
isInstalledInScope(plugin.localScope);
|
|
237
|
+
const brokenInstall = isEnabledButNotInstalled(plugin);
|
|
230
238
|
|
|
231
239
|
// Orphaned/deprecated plugin
|
|
232
240
|
if (plugin.isOrphaned) {
|
|
@@ -284,10 +292,30 @@ function pluginDetail(item: PluginPluginItem): React.ReactNode {
|
|
|
284
292
|
|
|
285
293
|
{/* Status line */}
|
|
286
294
|
<box marginTop={1}>
|
|
287
|
-
<text
|
|
288
|
-
{
|
|
295
|
+
<text
|
|
296
|
+
fg={
|
|
297
|
+
isInstalled
|
|
298
|
+
? theme.colors.success
|
|
299
|
+
: brokenInstall
|
|
300
|
+
? theme.colors.warning
|
|
301
|
+
: theme.colors.muted
|
|
302
|
+
}
|
|
303
|
+
>
|
|
304
|
+
{isInstalled
|
|
305
|
+
? "● Installed"
|
|
306
|
+
: brokenInstall
|
|
307
|
+
? "○ Not installed — enabled in settings, no files on disk"
|
|
308
|
+
: "○ Not installed"}
|
|
289
309
|
</text>
|
|
290
310
|
</box>
|
|
311
|
+
{brokenInstall ? (
|
|
312
|
+
<box>
|
|
313
|
+
<text fg={theme.colors.muted}>
|
|
314
|
+
Claude Code cannot load it in this state. Press the scope key to
|
|
315
|
+
repair.
|
|
316
|
+
</text>
|
|
317
|
+
</box>
|
|
318
|
+
) : null}
|
|
291
319
|
|
|
292
320
|
{/* Description */}
|
|
293
321
|
<box marginTop={1} marginBottom={1}>
|
|
@@ -331,48 +359,27 @@ function pluginDetail(item: PluginPluginItem): React.ReactNode {
|
|
|
331
359
|
<strong>Scopes:</strong>
|
|
332
360
|
</text>
|
|
333
361
|
<box marginTop={1} flexDirection="column">
|
|
334
|
-
<
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
<
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
</span>
|
|
356
|
-
<span fg={theme.scopes.project}>Project</span>
|
|
357
|
-
<span> team</span>
|
|
358
|
-
{plugin.projectScope?.version ? (
|
|
359
|
-
<span fg={theme.scopes.project}> v{plugin.projectScope.version}</span>
|
|
360
|
-
) : null}
|
|
361
|
-
</text>
|
|
362
|
-
<text>
|
|
363
|
-
<span bg={theme.scopes.local} fg="black">
|
|
364
|
-
{" "}
|
|
365
|
-
l{" "}
|
|
366
|
-
</span>
|
|
367
|
-
<span fg={plugin.localScope?.enabled ? theme.scopes.local : theme.colors.muted}>
|
|
368
|
-
{plugin.localScope?.enabled ? " ● " : " ○ "}
|
|
369
|
-
</span>
|
|
370
|
-
<span fg={theme.scopes.local}>Local</span>
|
|
371
|
-
<span> private</span>
|
|
372
|
-
{plugin.localScope?.version ? (
|
|
373
|
-
<span fg={theme.scopes.local}> v{plugin.localScope.version}</span>
|
|
374
|
-
) : null}
|
|
375
|
-
</text>
|
|
362
|
+
<ScopeLine
|
|
363
|
+
keyHint="u"
|
|
364
|
+
name="User"
|
|
365
|
+
qualifier="global"
|
|
366
|
+
color={theme.scopes.user}
|
|
367
|
+
scope={plugin.userScope}
|
|
368
|
+
/>
|
|
369
|
+
<ScopeLine
|
|
370
|
+
keyHint="p"
|
|
371
|
+
name="Project"
|
|
372
|
+
qualifier="team"
|
|
373
|
+
color={theme.scopes.project}
|
|
374
|
+
scope={plugin.projectScope}
|
|
375
|
+
/>
|
|
376
|
+
<ScopeLine
|
|
377
|
+
keyHint="l"
|
|
378
|
+
name="Local"
|
|
379
|
+
qualifier="private"
|
|
380
|
+
color={theme.scopes.local}
|
|
381
|
+
scope={plugin.localScope}
|
|
382
|
+
/>
|
|
376
383
|
</box>
|
|
377
384
|
</DetailSection>
|
|
378
385
|
|
|
@@ -389,6 +396,47 @@ function pluginDetail(item: PluginPluginItem): React.ReactNode {
|
|
|
389
396
|
);
|
|
390
397
|
}
|
|
391
398
|
|
|
399
|
+
/**
|
|
400
|
+
* One line of the detail panel's scope breakdown.
|
|
401
|
+
*
|
|
402
|
+
* The filled dot means installed — registry-backed, same as everywhere else.
|
|
403
|
+
* A scope that is enabled with nothing installed is called out explicitly
|
|
404
|
+
* rather than shown as a plain empty dot, because those two states need
|
|
405
|
+
* different actions from the user and used to look identical.
|
|
406
|
+
*/
|
|
407
|
+
function ScopeLine({
|
|
408
|
+
keyHint,
|
|
409
|
+
name,
|
|
410
|
+
qualifier,
|
|
411
|
+
color,
|
|
412
|
+
scope,
|
|
413
|
+
}: {
|
|
414
|
+
keyHint: string;
|
|
415
|
+
name: string;
|
|
416
|
+
qualifier: string;
|
|
417
|
+
color: string;
|
|
418
|
+
scope?: ScopeStatus;
|
|
419
|
+
}): React.ReactNode {
|
|
420
|
+
const installed = isInstalledInScope(scope);
|
|
421
|
+
return (
|
|
422
|
+
<text>
|
|
423
|
+
<span bg={color} fg="black">
|
|
424
|
+
{" "}
|
|
425
|
+
{keyHint}{" "}
|
|
426
|
+
</span>
|
|
427
|
+
<span fg={installed ? color : theme.colors.muted}>
|
|
428
|
+
{installed ? " ● " : " ○ "}
|
|
429
|
+
</span>
|
|
430
|
+
<span fg={color}>{name}</span>
|
|
431
|
+
<span> {qualifier}</span>
|
|
432
|
+
{scope?.version ? <span fg={color}> v{scope.version}</span> : null}
|
|
433
|
+
{!installed && scope?.enabled ? (
|
|
434
|
+
<span fg={theme.colors.warning}> enabled, not installed</span>
|
|
435
|
+
) : null}
|
|
436
|
+
</text>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
392
440
|
/**
|
|
393
441
|
* Recent release history, newest first.
|
|
394
442
|
*
|
|
@@ -18,8 +18,11 @@ import {
|
|
|
18
18
|
refreshAllMarketplaces,
|
|
19
19
|
clearMarketplaceCache,
|
|
20
20
|
getLocalMarketplacesInfo,
|
|
21
|
+
isInstalledInScope,
|
|
22
|
+
resolveScopeAction,
|
|
21
23
|
saveInstalledPluginVersion,
|
|
22
24
|
type PluginInfo,
|
|
25
|
+
type ScopeStatus,
|
|
23
26
|
} from "../../services/plugin-manager.js";
|
|
24
27
|
import {
|
|
25
28
|
setMcpEnvVar,
|
|
@@ -605,10 +608,10 @@ export function PluginsScreen() {
|
|
|
605
608
|
|
|
606
609
|
const buildScopeLabel = (
|
|
607
610
|
name: string,
|
|
608
|
-
scope:
|
|
611
|
+
scope: ScopeStatus | undefined,
|
|
609
612
|
desc: string,
|
|
610
613
|
) => {
|
|
611
|
-
const installed = scope
|
|
614
|
+
const installed = isInstalledInScope(scope);
|
|
612
615
|
const ver = scope?.version;
|
|
613
616
|
const hasUpdate =
|
|
614
617
|
ver &&
|
|
@@ -619,6 +622,10 @@ export function PluginsScreen() {
|
|
|
619
622
|
label += ` (${desc})`;
|
|
620
623
|
if (ver) label += ` v${ver}`;
|
|
621
624
|
if (hasUpdate) label += ` → v${latestVersion}`;
|
|
625
|
+
// Enabled in settings with nothing installed. Without saying so the
|
|
626
|
+
// row reads exactly like a scope that was never enabled, and the
|
|
627
|
+
// user cannot tell that picking it repairs rather than adds.
|
|
628
|
+
if (!installed && scope?.enabled) label += " — not installed, repairs";
|
|
622
629
|
return label;
|
|
623
630
|
};
|
|
624
631
|
|
|
@@ -650,8 +657,6 @@ export function PluginsScreen() {
|
|
|
650
657
|
: scopeValue === "project"
|
|
651
658
|
? plugin.projectScope
|
|
652
659
|
: plugin.localScope;
|
|
653
|
-
const isInstalledInScope = selectedScope?.enabled;
|
|
654
|
-
const installedVersion = selectedScope?.version;
|
|
655
660
|
const scopeLabel =
|
|
656
661
|
scopeValue === "user"
|
|
657
662
|
? "User"
|
|
@@ -659,20 +664,9 @@ export function PluginsScreen() {
|
|
|
659
664
|
? "Project"
|
|
660
665
|
: "Local";
|
|
661
666
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
latestVersion !== "0.0.0" &&
|
|
666
|
-
installedVersion !== latestVersion;
|
|
667
|
-
|
|
668
|
-
let action: "update" | "install" | "uninstall";
|
|
669
|
-
if (isInstalledInScope && hasUpdateInScope) {
|
|
670
|
-
action = "update";
|
|
671
|
-
} else if (isInstalledInScope) {
|
|
672
|
-
action = "uninstall";
|
|
673
|
-
} else {
|
|
674
|
-
action = "install";
|
|
675
|
-
}
|
|
667
|
+
// Decided from the registry-backed version, not the `enabledPlugins`
|
|
668
|
+
// flag alone — see resolveScopeAction.
|
|
669
|
+
const action = resolveScopeAction(selectedScope, latestVersion);
|
|
676
670
|
|
|
677
671
|
try {
|
|
678
672
|
const scope = scopeValue as PluginScope;
|
|
@@ -843,23 +837,12 @@ export function PluginsScreen() {
|
|
|
843
837
|
: scope === "project"
|
|
844
838
|
? plugin.projectScope
|
|
845
839
|
: plugin.localScope;
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
latestVersion !== "0.0.0" &&
|
|
853
|
-
installedVersion !== latestVersion;
|
|
854
|
-
|
|
855
|
-
let action: "update" | "install" | "uninstall";
|
|
856
|
-
if (isInstalledInScope && hasUpdateInScope) {
|
|
857
|
-
action = "update";
|
|
858
|
-
} else if (isInstalledInScope) {
|
|
859
|
-
action = "uninstall";
|
|
860
|
-
} else {
|
|
861
|
-
action = "install";
|
|
862
|
-
}
|
|
840
|
+
|
|
841
|
+
// This used to key off `scopeData.enabled` alone, so a plugin the list
|
|
842
|
+
// had just labelled "not installed" answered its own scope key by
|
|
843
|
+
// uninstalling itself — the `enabledPlugins` flag was set, the registry
|
|
844
|
+
// entry was missing, and only the flag was consulted.
|
|
845
|
+
const action = resolveScopeAction(scopeData, latestVersion);
|
|
863
846
|
|
|
864
847
|
try {
|
|
865
848
|
if (action === "uninstall") {
|