claudeup 4.4.0 → 4.5.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
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version tracking tests
|
|
3
|
+
*
|
|
4
|
+
* The Claude CLI's `plugin install` command does NOT update `installedPluginVersions`
|
|
5
|
+
* in settings.json — it only manages `enabledPlugins`. Claudeup must save the version
|
|
6
|
+
* itself after a successful CLI install/update.
|
|
7
|
+
*
|
|
8
|
+
* PluginsScreen: no saveVersionAfterInstall helper (removed in v4.5.0), but each
|
|
9
|
+
* action handler calls saveVersionForScope after CLI success.
|
|
10
|
+
* Prerunner: calls saveGlobalInstalledPluginVersion after successful updatePlugin,
|
|
11
|
+
* but NOT when CLI is unavailable or update fails (no phantom state).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import * as url from "node:url";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// PART 1: PluginsScreen source-level policy tests
|
|
21
|
+
//
|
|
22
|
+
// The dual-write calls in PluginsScreen are inside React event handlers.
|
|
23
|
+
// Rendering the full TUI component in a unit test would require the opentui
|
|
24
|
+
// terminal runtime. Instead we assert the source does NOT contain the
|
|
25
|
+
// forbidden call pattern — this is a code-policy test that:
|
|
26
|
+
// - FAILS before the fix (6 saveVersionAfterInstall calls follow CLI calls)
|
|
27
|
+
// - PASSES after the fix (those calls are removed)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
31
|
+
const PLUGINS_SCREEN_PATH = path.resolve(
|
|
32
|
+
__dirname,
|
|
33
|
+
"../ui/screens/PluginsScreen.tsx",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
describe("PluginsScreen — no dual-write after CLI operations", () => {
|
|
37
|
+
let source: string;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
source = fs.readFileSync(PLUGINS_SCREEN_PATH, "utf8");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not call saveVersionAfterInstall immediately after cliUpdatePlugin", () => {
|
|
44
|
+
// Find every occurrence of cliUpdatePlugin in the source. Each one should
|
|
45
|
+
// NOT be immediately followed (within 3 lines) by saveVersionAfterInstall.
|
|
46
|
+
const lines = source.split("\n");
|
|
47
|
+
const violations: number[] = [];
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < lines.length; i++) {
|
|
50
|
+
if (lines[i].includes("cliUpdatePlugin(")) {
|
|
51
|
+
// Check the next 3 lines for a dual-write call
|
|
52
|
+
for (let j = i + 1; j <= Math.min(i + 3, lines.length - 1); j++) {
|
|
53
|
+
if (lines[j].includes("saveVersionAfterInstall(")) {
|
|
54
|
+
violations.push(i + 1); // 1-indexed line number
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
expect(violations).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("does not call saveVersionAfterInstall immediately after cliInstallPlugin", () => {
|
|
65
|
+
const lines = source.split("\n");
|
|
66
|
+
const violations: number[] = [];
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
if (lines[i].includes("cliInstallPlugin(")) {
|
|
70
|
+
for (let j = i + 1; j <= Math.min(i + 3, lines.length - 1); j++) {
|
|
71
|
+
if (lines[j].includes("saveVersionAfterInstall(")) {
|
|
72
|
+
violations.push(i + 1);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
expect(violations).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// PART 2: Prerunner — saveGlobalInstalledPluginVersion guarded by CLI success
|
|
85
|
+
//
|
|
86
|
+
// We mock the prerunner's module dependencies so we can call prerunClaude()
|
|
87
|
+
// directly and inspect call counts.
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
// Mutable state shared between mock factories and test assertions.
|
|
91
|
+
let mockSaveGlobal: ReturnType<typeof mock>;
|
|
92
|
+
let mockUpdatePlugin: ReturnType<typeof mock>;
|
|
93
|
+
let mockIsClaudeAvailable: ReturnType<typeof mock>;
|
|
94
|
+
let mockGetAvailablePlugins: ReturnType<typeof mock>;
|
|
95
|
+
|
|
96
|
+
// We must register mock.module before importing the module under test.
|
|
97
|
+
// Bun hoists mock.module() calls, so the mocks are active when the
|
|
98
|
+
// module is first loaded.
|
|
99
|
+
|
|
100
|
+
mock.module("../services/claude-settings.js", () => ({
|
|
101
|
+
recoverMarketplaceSettings: mock(() =>
|
|
102
|
+
Promise.resolve({ enabledAutoUpdate: [], removed: [] }),
|
|
103
|
+
),
|
|
104
|
+
migrateMarketplaceRename: mock(() =>
|
|
105
|
+
Promise.resolve({
|
|
106
|
+
projectMigrated: 0,
|
|
107
|
+
globalMigrated: 0,
|
|
108
|
+
localMigrated: 0,
|
|
109
|
+
registryMigrated: 0,
|
|
110
|
+
knownMarketplacesMigrated: false,
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
113
|
+
getGlobalEnabledPlugins: mock(() => Promise.resolve({})),
|
|
114
|
+
getEnabledPlugins: mock(() => Promise.resolve({})),
|
|
115
|
+
getLocalEnabledPlugins: mock(() => Promise.resolve({})),
|
|
116
|
+
saveGlobalInstalledPluginVersion: (...args: unknown[]) =>
|
|
117
|
+
mockSaveGlobal(...args),
|
|
118
|
+
readGlobalSettings: mock(() => Promise.resolve({ hooks: {} })),
|
|
119
|
+
writeGlobalSettings: mock(() => Promise.resolve()),
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
mock.module("../services/claude-cli.js", () => ({
|
|
123
|
+
updatePlugin: (...args: unknown[]) => mockUpdatePlugin(...args),
|
|
124
|
+
isClaudeAvailable: (...args: unknown[]) => mockIsClaudeAvailable(...args),
|
|
125
|
+
addMarketplace: mock(() => Promise.resolve()),
|
|
126
|
+
updateMarketplace: mock(() => Promise.resolve()),
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
mock.module("../services/plugin-manager.js", () => ({
|
|
130
|
+
getAvailablePlugins: (...args: unknown[]) =>
|
|
131
|
+
mockGetAvailablePlugins(...args),
|
|
132
|
+
clearMarketplaceCache: mock(() => undefined),
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
mock.module("../services/update-cache.js", () => ({
|
|
136
|
+
UpdateCache: class {
|
|
137
|
+
shouldCheckForUpdates = mock(() => Promise.resolve(true));
|
|
138
|
+
saveCheck = mock(() => Promise.resolve());
|
|
139
|
+
},
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
mock.module("../services/claude-runner.js", () => ({
|
|
143
|
+
runClaude: mock(() => Promise.resolve(0)),
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
mock.module("../data/marketplaces.js", () => ({
|
|
147
|
+
defaultMarketplaces: [],
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
mock.module("../utils/string-utils.js", () => ({
|
|
151
|
+
parsePluginId: mock((_id: string) => null),
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
mock.module("fs-extra", () => ({
|
|
155
|
+
default: {
|
|
156
|
+
pathExists: mock(() => Promise.resolve(false)),
|
|
157
|
+
readJson: mock(() => Promise.resolve({})),
|
|
158
|
+
writeJson: mock(() => Promise.resolve()),
|
|
159
|
+
ensureDir: mock(() => Promise.resolve()),
|
|
160
|
+
},
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
// Now import the module under test (after mock.module registrations).
|
|
164
|
+
const { prerunClaude } = await import("../prerunner/index.js");
|
|
165
|
+
|
|
166
|
+
// Helper: build a minimal PluginInfo-like object
|
|
167
|
+
function makePlugin(overrides: Partial<{
|
|
168
|
+
id: string;
|
|
169
|
+
enabled: boolean;
|
|
170
|
+
hasUpdate: boolean;
|
|
171
|
+
installedVersion: string;
|
|
172
|
+
version: string;
|
|
173
|
+
}> = {}): {
|
|
174
|
+
id: string;
|
|
175
|
+
enabled: boolean;
|
|
176
|
+
hasUpdate: boolean;
|
|
177
|
+
installedVersion: string;
|
|
178
|
+
version: string;
|
|
179
|
+
} {
|
|
180
|
+
return {
|
|
181
|
+
id: "test-plugin@magus",
|
|
182
|
+
enabled: true,
|
|
183
|
+
hasUpdate: true,
|
|
184
|
+
installedVersion: "1.0.0",
|
|
185
|
+
version: "1.1.0",
|
|
186
|
+
...overrides,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
describe("prerunner — saveGlobalInstalledPluginVersion call count", () => {
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
mockSaveGlobal = mock(() => Promise.resolve());
|
|
193
|
+
mockUpdatePlugin = mock(() => Promise.resolve());
|
|
194
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
195
|
+
mockGetAvailablePlugins = mock(() =>
|
|
196
|
+
Promise.resolve([makePlugin()]),
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("calls saveGlobalInstalledPluginVersion once after successful CLI update", async () => {
|
|
201
|
+
// CLI is available, updatePlugin succeeds.
|
|
202
|
+
// The CLI does NOT update installedPluginVersions, so claudeup must save
|
|
203
|
+
// the version after a successful update.
|
|
204
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
205
|
+
mockUpdatePlugin = mock(() => Promise.resolve());
|
|
206
|
+
|
|
207
|
+
await prerunClaude(["--help"], { force: true });
|
|
208
|
+
|
|
209
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("does NOT call saveGlobalInstalledPluginVersion when CLI is unavailable", async () => {
|
|
213
|
+
// CLI unavailable — we must NOT write phantom state.
|
|
214
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(false));
|
|
215
|
+
|
|
216
|
+
await prerunClaude(["--help"], { force: true });
|
|
217
|
+
|
|
218
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("does NOT call saveGlobalInstalledPluginVersion when CLI updatePlugin throws", async () => {
|
|
222
|
+
// CLI available but updatePlugin fails — must NOT write phantom state.
|
|
223
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
224
|
+
mockUpdatePlugin = mock(() =>
|
|
225
|
+
Promise.reject(new Error("CLI update failed")),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await prerunClaude(["--help"], { force: true });
|
|
229
|
+
|
|
230
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
231
|
+
});
|
|
232
|
+
});
|
package/src/prerunner/index.js
CHANGED
|
@@ -4,10 +4,10 @@ import os from "node:os";
|
|
|
4
4
|
import { UpdateCache } from "../services/update-cache.js";
|
|
5
5
|
import { getAvailablePlugins, clearMarketplaceCache, } from "../services/plugin-manager.js";
|
|
6
6
|
import { runClaude } from "../services/claude-runner.js";
|
|
7
|
-
import { recoverMarketplaceSettings, migrateMarketplaceRename, getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins,
|
|
7
|
+
import { recoverMarketplaceSettings, migrateMarketplaceRename, getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins, readGlobalSettings, writeGlobalSettings, saveGlobalInstalledPluginVersion, } from "../services/claude-settings.js";
|
|
8
8
|
import { parsePluginId } from "../utils/string-utils.js";
|
|
9
9
|
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
10
|
-
import { updatePlugin, addMarketplace, isClaudeAvailable, } from "../services/claude-cli.js";
|
|
10
|
+
import { updatePlugin, addMarketplace, updateMarketplace, isClaudeAvailable, } from "../services/claude-cli.js";
|
|
11
11
|
const MARKETPLACES_DIR = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
|
|
12
12
|
/**
|
|
13
13
|
* Collect all unique marketplace names from enabled plugins across all settings scopes.
|
|
@@ -55,6 +55,11 @@ async function getReferencedMarketplaces(projectPath) {
|
|
|
55
55
|
/**
|
|
56
56
|
* Check which referenced marketplaces are missing locally and auto-add them.
|
|
57
57
|
* Only adds marketplaces with known repos (from defaultMarketplaces).
|
|
58
|
+
*
|
|
59
|
+
* Uses `marketplace update` (not `marketplace add`) for recovery because
|
|
60
|
+
* `add` short-circuits when the marketplace is already declared in settings
|
|
61
|
+
* — even if the directory was deleted. `update` detects the missing dir and
|
|
62
|
+
* re-clones automatically.
|
|
58
63
|
*/
|
|
59
64
|
async function autoAddMissingMarketplaces(projectPath) {
|
|
60
65
|
const referenced = await getReferencedMarketplaces(projectPath);
|
|
@@ -69,12 +74,19 @@ async function autoAddMissingMarketplaces(projectPath) {
|
|
|
69
74
|
if (!defaultMp?.source.repo)
|
|
70
75
|
continue;
|
|
71
76
|
try {
|
|
72
|
-
|
|
77
|
+
// Try `marketplace update` first — it re-clones missing directories
|
|
78
|
+
await updateMarketplace(mpName);
|
|
73
79
|
added.push(mpName);
|
|
74
80
|
}
|
|
75
|
-
catch
|
|
76
|
-
//
|
|
77
|
-
|
|
81
|
+
catch {
|
|
82
|
+
// If update fails (marketplace not in settings yet), try add
|
|
83
|
+
try {
|
|
84
|
+
await addMarketplace(defaultMp.source.repo);
|
|
85
|
+
added.push(mpName);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.warn(`⚠ Failed to auto-add marketplace ${mpName}:`, error instanceof Error ? error.message : "Unknown error");
|
|
89
|
+
}
|
|
78
90
|
}
|
|
79
91
|
}
|
|
80
92
|
return added;
|
|
@@ -194,9 +206,12 @@ export async function prerunClaude(claudeArgs, options = {}) {
|
|
|
194
206
|
plugin.installedVersion &&
|
|
195
207
|
plugin.version) {
|
|
196
208
|
try {
|
|
197
|
-
if (cliAvailable) {
|
|
198
|
-
|
|
209
|
+
if (!cliAvailable) {
|
|
210
|
+
// CLI unavailable — skip update entirely, don't write phantom state
|
|
211
|
+
continue;
|
|
199
212
|
}
|
|
213
|
+
await updatePlugin(plugin.id, "user");
|
|
214
|
+
// CLI does NOT update installedPluginVersions — save it ourselves
|
|
200
215
|
await saveGlobalInstalledPluginVersion(plugin.id, plugin.version);
|
|
201
216
|
autoUpdatedPlugins.push({
|
|
202
217
|
pluginId: plugin.id,
|
package/src/prerunner/index.ts
CHANGED
|
@@ -13,15 +13,16 @@ import {
|
|
|
13
13
|
getGlobalEnabledPlugins,
|
|
14
14
|
getEnabledPlugins,
|
|
15
15
|
getLocalEnabledPlugins,
|
|
16
|
-
saveGlobalInstalledPluginVersion,
|
|
17
16
|
readGlobalSettings,
|
|
18
17
|
writeGlobalSettings,
|
|
18
|
+
saveGlobalInstalledPluginVersion,
|
|
19
19
|
} from "../services/claude-settings.js";
|
|
20
20
|
import { parsePluginId } from "../utils/string-utils.js";
|
|
21
21
|
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
22
22
|
import {
|
|
23
23
|
updatePlugin,
|
|
24
24
|
addMarketplace,
|
|
25
|
+
updateMarketplace,
|
|
25
26
|
isClaudeAvailable,
|
|
26
27
|
} from "../services/claude-cli.js";
|
|
27
28
|
|
|
@@ -85,6 +86,11 @@ async function getReferencedMarketplaces(
|
|
|
85
86
|
/**
|
|
86
87
|
* Check which referenced marketplaces are missing locally and auto-add them.
|
|
87
88
|
* Only adds marketplaces with known repos (from defaultMarketplaces).
|
|
89
|
+
*
|
|
90
|
+
* Uses `marketplace update` (not `marketplace add`) for recovery because
|
|
91
|
+
* `add` short-circuits when the marketplace is already declared in settings
|
|
92
|
+
* — even if the directory was deleted. `update` detects the missing dir and
|
|
93
|
+
* re-clones automatically.
|
|
88
94
|
*/
|
|
89
95
|
async function autoAddMissingMarketplaces(
|
|
90
96
|
projectPath?: string,
|
|
@@ -102,14 +108,20 @@ async function autoAddMissingMarketplaces(
|
|
|
102
108
|
if (!defaultMp?.source.repo) continue;
|
|
103
109
|
|
|
104
110
|
try {
|
|
105
|
-
|
|
111
|
+
// Try `marketplace update` first — it re-clones missing directories
|
|
112
|
+
await updateMarketplace(mpName);
|
|
106
113
|
added.push(mpName);
|
|
107
|
-
} catch
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)
|
|
114
|
+
} catch {
|
|
115
|
+
// If update fails (marketplace not in settings yet), try add
|
|
116
|
+
try {
|
|
117
|
+
await addMarketplace(defaultMp.source.repo);
|
|
118
|
+
added.push(mpName);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(
|
|
121
|
+
`⚠ Failed to auto-add marketplace ${mpName}:`,
|
|
122
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
113
125
|
}
|
|
114
126
|
}
|
|
115
127
|
|
|
@@ -278,9 +290,12 @@ export async function prerunClaude(
|
|
|
278
290
|
plugin.version
|
|
279
291
|
) {
|
|
280
292
|
try {
|
|
281
|
-
if (cliAvailable) {
|
|
282
|
-
|
|
293
|
+
if (!cliAvailable) {
|
|
294
|
+
// CLI unavailable — skip update entirely, don't write phantom state
|
|
295
|
+
continue;
|
|
283
296
|
}
|
|
297
|
+
await updatePlugin(plugin.id, "user");
|
|
298
|
+
// CLI does NOT update installedPluginVersions — save it ourselves
|
|
284
299
|
await saveGlobalInstalledPluginVersion(plugin.id, plugin.version);
|
|
285
300
|
|
|
286
301
|
autoUpdatedPlugins.push({
|
|
@@ -8,10 +8,9 @@ import { ScrollableList } from "../components/ScrollableList.js";
|
|
|
8
8
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
9
9
|
import { fuzzyFilter } from "../../utils/fuzzy-search.js";
|
|
10
10
|
import { getAllMarketplaces } from "../../data/marketplaces.js";
|
|
11
|
-
import { getAvailablePlugins, refreshAllMarketplaces, clearMarketplaceCache, getLocalMarketplacesInfo, } from "../../services/plugin-manager.js";
|
|
11
|
+
import { getAvailablePlugins, refreshAllMarketplaces, clearMarketplaceCache, getLocalMarketplacesInfo, saveInstalledPluginVersion, } from "../../services/plugin-manager.js";
|
|
12
12
|
import { setMcpEnvVar, getMcpEnvVars, readSettings, saveGlobalInstalledPluginVersion, saveLocalInstalledPluginVersion, } from "../../services/claude-settings.js";
|
|
13
13
|
import { saveProfile } from "../../services/profiles.js";
|
|
14
|
-
import { saveInstalledPluginVersion } from "../../services/plugin-manager.js";
|
|
15
14
|
import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
|
|
16
15
|
import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
|
|
17
16
|
import { getPluginSetupFromSource, checkMissingDeps, installPluginDeps, } from "../../services/plugin-setup.js";
|
|
@@ -207,6 +206,23 @@ export function PluginsScreen() {
|
|
|
207
206
|
else if (event.name === "s")
|
|
208
207
|
handleSaveAsProfile();
|
|
209
208
|
});
|
|
209
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
210
|
+
/**
|
|
211
|
+
* Save the installed plugin version to the correct settings file for the scope.
|
|
212
|
+
* The Claude CLI's `plugin install` does NOT update installedPluginVersions,
|
|
213
|
+
* so claudeup must do it after a successful CLI install/update.
|
|
214
|
+
*/
|
|
215
|
+
const saveVersionForScope = async (pluginId, version, scope) => {
|
|
216
|
+
if (scope === "user") {
|
|
217
|
+
await saveGlobalInstalledPluginVersion(pluginId, version);
|
|
218
|
+
}
|
|
219
|
+
else if (scope === "local") {
|
|
220
|
+
await saveLocalInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
await saveInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
210
226
|
// ── Action handlers ────────────────────────────────────────────────────────
|
|
211
227
|
const handleRefresh = async () => {
|
|
212
228
|
progress.show("Refreshing cache...");
|
|
@@ -355,25 +371,6 @@ export function PluginsScreen() {
|
|
|
355
371
|
console.error("Error installing plugin deps:", error);
|
|
356
372
|
}
|
|
357
373
|
};
|
|
358
|
-
/**
|
|
359
|
-
* Save installed plugin version to settings after CLI install/update.
|
|
360
|
-
*/
|
|
361
|
-
const saveVersionAfterInstall = async (pluginId, version, scope) => {
|
|
362
|
-
try {
|
|
363
|
-
if (scope === "user") {
|
|
364
|
-
await saveGlobalInstalledPluginVersion(pluginId, version);
|
|
365
|
-
}
|
|
366
|
-
else if (scope === "local") {
|
|
367
|
-
await saveLocalInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
await saveInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
// Non-fatal: version display may be stale but plugin still works
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
374
|
const handleSelect = async () => {
|
|
378
375
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
379
376
|
if (!item)
|
|
@@ -459,11 +456,11 @@ export function PluginsScreen() {
|
|
|
459
456
|
}
|
|
460
457
|
else if (action === "update") {
|
|
461
458
|
await cliUpdatePlugin(plugin.id, scope);
|
|
462
|
-
await
|
|
459
|
+
await saveVersionForScope(plugin.id, latestVersion, scope);
|
|
463
460
|
}
|
|
464
461
|
else {
|
|
465
462
|
await cliInstallPlugin(plugin.id, scope);
|
|
466
|
-
await
|
|
463
|
+
await saveVersionForScope(plugin.id, latestVersion, scope);
|
|
467
464
|
modal.hideModal();
|
|
468
465
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
469
466
|
await installPluginSystemDeps(plugin.name, plugin.marketplace);
|
|
@@ -488,7 +485,9 @@ export function PluginsScreen() {
|
|
|
488
485
|
modal.loading(`Updating ${plugin.name}...`);
|
|
489
486
|
try {
|
|
490
487
|
await cliUpdatePlugin(plugin.id, scope);
|
|
491
|
-
|
|
488
|
+
if (plugin.version) {
|
|
489
|
+
await saveVersionForScope(plugin.id, plugin.version, scope);
|
|
490
|
+
}
|
|
492
491
|
modal.hideModal();
|
|
493
492
|
fetchData();
|
|
494
493
|
}
|
|
@@ -508,7 +507,9 @@ export function PluginsScreen() {
|
|
|
508
507
|
try {
|
|
509
508
|
for (const plugin of updatable) {
|
|
510
509
|
await cliUpdatePlugin(plugin.id, scope);
|
|
511
|
-
|
|
510
|
+
if (plugin.version) {
|
|
511
|
+
await saveVersionForScope(plugin.id, plugin.version, scope);
|
|
512
|
+
}
|
|
512
513
|
}
|
|
513
514
|
modal.hideModal();
|
|
514
515
|
fetchData();
|
|
@@ -558,11 +559,11 @@ export function PluginsScreen() {
|
|
|
558
559
|
}
|
|
559
560
|
else if (action === "update") {
|
|
560
561
|
await cliUpdatePlugin(plugin.id, scope);
|
|
561
|
-
await
|
|
562
|
+
await saveVersionForScope(plugin.id, latestVersion, scope);
|
|
562
563
|
}
|
|
563
564
|
else {
|
|
564
565
|
await cliInstallPlugin(plugin.id, scope);
|
|
565
|
-
await
|
|
566
|
+
await saveVersionForScope(plugin.id, latestVersion, scope);
|
|
566
567
|
modal.hideModal();
|
|
567
568
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
568
569
|
await installPluginSystemDeps(plugin.name, plugin.marketplace);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
refreshAllMarketplaces,
|
|
13
13
|
clearMarketplaceCache,
|
|
14
14
|
getLocalMarketplacesInfo,
|
|
15
|
+
saveInstalledPluginVersion,
|
|
15
16
|
type PluginInfo,
|
|
16
17
|
} from "../../services/plugin-manager.js";
|
|
17
18
|
import {
|
|
@@ -22,7 +23,6 @@ import {
|
|
|
22
23
|
saveLocalInstalledPluginVersion,
|
|
23
24
|
} from "../../services/claude-settings.js";
|
|
24
25
|
import { saveProfile } from "../../services/profiles.js";
|
|
25
|
-
import { saveInstalledPluginVersion } from "../../services/plugin-manager.js";
|
|
26
26
|
import {
|
|
27
27
|
installPlugin as cliInstallPlugin,
|
|
28
28
|
uninstallPlugin as cliUninstallPlugin,
|
|
@@ -244,6 +244,27 @@ export function PluginsScreen() {
|
|
|
244
244
|
else if (event.name === "s") handleSaveAsProfile();
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Save the installed plugin version to the correct settings file for the scope.
|
|
251
|
+
* The Claude CLI's `plugin install` does NOT update installedPluginVersions,
|
|
252
|
+
* so claudeup must do it after a successful CLI install/update.
|
|
253
|
+
*/
|
|
254
|
+
const saveVersionForScope = async (
|
|
255
|
+
pluginId: string,
|
|
256
|
+
version: string,
|
|
257
|
+
scope: PluginScope,
|
|
258
|
+
): Promise<void> => {
|
|
259
|
+
if (scope === "user") {
|
|
260
|
+
await saveGlobalInstalledPluginVersion(pluginId, version);
|
|
261
|
+
} else if (scope === "local") {
|
|
262
|
+
await saveLocalInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
263
|
+
} else {
|
|
264
|
+
await saveInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
247
268
|
// ── Action handlers ────────────────────────────────────────────────────────
|
|
248
269
|
|
|
249
270
|
const handleRefresh = async () => {
|
|
@@ -452,27 +473,6 @@ export function PluginsScreen() {
|
|
|
452
473
|
}
|
|
453
474
|
};
|
|
454
475
|
|
|
455
|
-
/**
|
|
456
|
-
* Save installed plugin version to settings after CLI install/update.
|
|
457
|
-
*/
|
|
458
|
-
const saveVersionAfterInstall = async (
|
|
459
|
-
pluginId: string,
|
|
460
|
-
version: string,
|
|
461
|
-
scope: PluginScope,
|
|
462
|
-
): Promise<void> => {
|
|
463
|
-
try {
|
|
464
|
-
if (scope === "user") {
|
|
465
|
-
await saveGlobalInstalledPluginVersion(pluginId, version);
|
|
466
|
-
} else if (scope === "local") {
|
|
467
|
-
await saveLocalInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
468
|
-
} else {
|
|
469
|
-
await saveInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
470
|
-
}
|
|
471
|
-
} catch {
|
|
472
|
-
// Non-fatal: version display may be stale but plugin still works
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
|
|
476
476
|
const handleSelect = async () => {
|
|
477
477
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
478
478
|
if (!item) return;
|
|
@@ -573,10 +573,10 @@ export function PluginsScreen() {
|
|
|
573
573
|
await cliUninstallPlugin(plugin.id, scope, state.projectPath);
|
|
574
574
|
} else if (action === "update") {
|
|
575
575
|
await cliUpdatePlugin(plugin.id, scope);
|
|
576
|
-
await
|
|
576
|
+
await saveVersionForScope(plugin.id, latestVersion, scope);
|
|
577
577
|
} else {
|
|
578
578
|
await cliInstallPlugin(plugin.id, scope);
|
|
579
|
-
await
|
|
579
|
+
await saveVersionForScope(plugin.id, latestVersion, scope);
|
|
580
580
|
modal.hideModal();
|
|
581
581
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
582
582
|
await installPluginSystemDeps(plugin.name, plugin.marketplace);
|
|
@@ -602,7 +602,9 @@ export function PluginsScreen() {
|
|
|
602
602
|
modal.loading(`Updating ${plugin.name}...`);
|
|
603
603
|
try {
|
|
604
604
|
await cliUpdatePlugin(plugin.id, scope);
|
|
605
|
-
|
|
605
|
+
if (plugin.version) {
|
|
606
|
+
await saveVersionForScope(plugin.id, plugin.version, scope);
|
|
607
|
+
}
|
|
606
608
|
modal.hideModal();
|
|
607
609
|
fetchData();
|
|
608
610
|
} catch (error) {
|
|
@@ -623,7 +625,9 @@ export function PluginsScreen() {
|
|
|
623
625
|
try {
|
|
624
626
|
for (const plugin of updatable) {
|
|
625
627
|
await cliUpdatePlugin(plugin.id, scope);
|
|
626
|
-
|
|
628
|
+
if (plugin.version) {
|
|
629
|
+
await saveVersionForScope(plugin.id, plugin.version, scope);
|
|
630
|
+
}
|
|
627
631
|
}
|
|
628
632
|
modal.hideModal();
|
|
629
633
|
fetchData();
|
|
@@ -679,10 +683,10 @@ export function PluginsScreen() {
|
|
|
679
683
|
await cliUninstallPlugin(plugin.id, scope, state.projectPath);
|
|
680
684
|
} else if (action === "update") {
|
|
681
685
|
await cliUpdatePlugin(plugin.id, scope);
|
|
682
|
-
await
|
|
686
|
+
await saveVersionForScope(plugin.id, latestVersion, scope);
|
|
683
687
|
} else {
|
|
684
688
|
await cliInstallPlugin(plugin.id, scope);
|
|
685
|
-
await
|
|
689
|
+
await saveVersionForScope(plugin.id, latestVersion, scope);
|
|
686
690
|
modal.hideModal();
|
|
687
691
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
688
692
|
await installPluginSystemDeps(plugin.name, plugin.marketplace);
|