claudeup 6.5.1 → 6.7.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 +4 -4
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cli-apply-seams.test.ts +2 -0
- package/src/__tests__/cli-tool-commands.test.ts +1 -1
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +142 -2
- package/src/__tests__/content-drift.test.ts +60 -0
- package/src/__tests__/conventions-manager.test.ts +45 -45
- package/src/__tests__/enabled-not-installed.test.ts +2 -3
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/{gitignore-prerun.test.ts → gitignore-check.test.ts} +8 -6
- package/src/__tests__/models-presets-marketplace.test.ts +141 -0
- package/src/__tests__/plugin-cli-argv.test.ts +1 -1
- package/src/__tests__/plugin-manager-fallback.test.ts +3 -5
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/resolver.test.ts +3 -3
- package/src/__tests__/scope-action.test.ts +69 -19
- package/src/__tests__/shell-script-callers.test.ts +8 -8
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/update-apply.test.ts +283 -1
- package/src/__tests__/update-plan.test.ts +773 -31
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/cli/router.ts +0 -5
- package/src/cli/update-view.ts +92 -12
- package/src/cli/update.ts +114 -38
- package/src/data/models-presets.ts +10 -11
- package/src/data/predefined-profiles.ts +7 -7
- package/src/services/active-closure.ts +117 -0
- package/src/services/catalog-cache-store.ts +6 -2
- package/src/services/claude-settings.ts +2 -174
- package/src/services/community-styles.ts +1 -1
- package/src/services/{gitignore-prerun.ts → gitignore-check.ts} +14 -10
- package/src/services/gitignore-fixer.ts +1 -1
- package/src/services/marketplace-refresh.ts +30 -0
- package/src/services/plugin-manager.ts +200 -283
- package/src/services/resolver.ts +7 -2
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/update-engine.ts +220 -32
- package/src/services/update-plan.ts +640 -160
- package/src/services/version-snapshot.ts +1 -1
- package/src/types/bun.d.ts +6 -0
- package/src/ui/App.tsx +2 -27
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/pluginsAdapter.ts +57 -0
- package/src/ui/components/modals/ModalContainer.tsx +0 -26
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +2 -3
- package/src/ui/renderers/cliToolRenderers.tsx +108 -67
- package/src/ui/renderers/pluginRenderers.tsx +157 -98
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/screens/CliToolsScreen.tsx +154 -280
- package/src/ui/screens/PluginsScreen.tsx +156 -140
- package/src/ui/state/types.ts +0 -7
- package/src/__tests__/dual-write-prevention.test.ts +0 -380
- package/src/__tests__/gap-fill-versions.test.ts +0 -389
- package/src/__tests__/plugin-version-check.test.ts +0 -762
- package/src/cli/claude.ts +0 -21
- package/src/prerunner/index.ts +0 -500
- package/src/services/plugin-version-check.ts +0 -360
- package/src/services/update-cache.ts +0 -78
- package/src/ui/components/modals/VersionMismatchModal.tsx +0 -138
- package/src/ui/hooks/useMismatchModal.ts +0 -89
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plugin version mismatch detection and fix.
|
|
3
|
-
*
|
|
4
|
-
* HISTORICAL (pre-2.1.209): Claude Code's plugin loader took the FIRST entry
|
|
5
|
-
* from installed_plugins.json for each plugin ID:
|
|
6
|
-
*
|
|
7
|
-
* const installEntry = installedPluginsData.plugins[pluginId]?.[0]
|
|
8
|
-
*
|
|
9
|
-
* so the first project to install a plugin determined the version loaded for
|
|
10
|
-
* ALL projects (bug #45997).
|
|
11
|
-
*
|
|
12
|
-
* FIXED in Claude Code 2.1.209: the loader is now project-aware — it filters
|
|
13
|
-
* entries to the current project/scope (same git root or user/managed scope)
|
|
14
|
-
* BEFORE selecting, so cross-project [0] contention no longer applies. This
|
|
15
|
-
* detection therefore mostly warns on older Claude Code; it is retained for
|
|
16
|
-
* those versions but should not be treated as a current-version defect.
|
|
17
|
-
*
|
|
18
|
-
* Bug report: https://github.com/anthropics/claude-code/issues/45997
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import type { InstalledPluginEntry } from "../types/index.js";
|
|
22
|
-
import {
|
|
23
|
-
getEnabledPlugins,
|
|
24
|
-
getGlobalEnabledPlugins,
|
|
25
|
-
getLocalEnabledPlugins,
|
|
26
|
-
readInstalledPluginsRegistry,
|
|
27
|
-
writeInstalledPluginsRegistry,
|
|
28
|
-
} from "./claude-settings.js";
|
|
29
|
-
|
|
30
|
-
const BUG_REPORT_URL = "https://github.com/anthropics/claude-code/issues/45997";
|
|
31
|
-
|
|
32
|
-
// ── Types ────────────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
export interface VersionMismatchInfo {
|
|
35
|
-
/** Plugin ID, e.g. "terminal@magus" */
|
|
36
|
-
pluginId: string;
|
|
37
|
-
/** Version at entries[0] — the one Claude Code actually loads */
|
|
38
|
-
firstEntryVersion: string;
|
|
39
|
-
/** Project path of the entries[0] entry */
|
|
40
|
-
firstEntryProject: string | undefined;
|
|
41
|
-
/** Version this project has installed */
|
|
42
|
-
currentProjectVersion: string;
|
|
43
|
-
/** All entries for this plugin */
|
|
44
|
-
allEntries: InstalledPluginEntry[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface FixResult {
|
|
48
|
-
/** Number of entries updated */
|
|
49
|
-
updated: number;
|
|
50
|
-
/** Project paths that were updated */
|
|
51
|
-
projects: string[];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── Detection ────────────────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Check for plugin version mismatches caused by the [0] index bug.
|
|
58
|
-
*
|
|
59
|
-
* For each enabled plugin in the current project:
|
|
60
|
-
* 1. Reads the plugin's entry array from installed_plugins.json
|
|
61
|
-
* 2. Finds the entry matching the current project
|
|
62
|
-
* 3. Checks if entries[0] has a DIFFERENT version than the current project's entry
|
|
63
|
-
* 4. If yes: this project will load the wrong version
|
|
64
|
-
*
|
|
65
|
-
* @param projectPath - The current project path to check
|
|
66
|
-
* @returns Array of mismatch info for each affected plugin
|
|
67
|
-
*/
|
|
68
|
-
export async function checkPluginVersionMismatches(
|
|
69
|
-
projectPath: string,
|
|
70
|
-
): Promise<VersionMismatchInfo[]> {
|
|
71
|
-
const registry = await readInstalledPluginsRegistry();
|
|
72
|
-
const enabledPluginIds = await collectEnabledPluginIds(projectPath);
|
|
73
|
-
const mismatches: VersionMismatchInfo[] = [];
|
|
74
|
-
|
|
75
|
-
for (const pluginId of enabledPluginIds) {
|
|
76
|
-
const entries = registry.plugins[pluginId];
|
|
77
|
-
if (!entries || entries.length < 2) continue;
|
|
78
|
-
|
|
79
|
-
// Find the entry for the current project
|
|
80
|
-
const currentEntry = entries.find(
|
|
81
|
-
(e) =>
|
|
82
|
-
e.scope === "project" &&
|
|
83
|
-
normalizePath(e.projectPath) === normalizePath(projectPath),
|
|
84
|
-
);
|
|
85
|
-
if (!currentEntry) continue;
|
|
86
|
-
|
|
87
|
-
const firstEntry = entries[0];
|
|
88
|
-
|
|
89
|
-
// Compare the version at [0] with the current project's version
|
|
90
|
-
if (firstEntry.version !== currentEntry.version) {
|
|
91
|
-
mismatches.push({
|
|
92
|
-
pluginId,
|
|
93
|
-
firstEntryVersion: firstEntry.version,
|
|
94
|
-
firstEntryProject: firstEntry.projectPath,
|
|
95
|
-
currentProjectVersion: currentEntry.version,
|
|
96
|
-
allEntries: entries,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return mismatches;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Check a single plugin for version mismatch (used after plugin update).
|
|
106
|
-
*
|
|
107
|
-
* @param pluginId - The plugin ID to check
|
|
108
|
-
* @param projectPath - The current project path
|
|
109
|
-
* @returns Mismatch info or null if no mismatch
|
|
110
|
-
*/
|
|
111
|
-
export async function checkSinglePluginMismatch(
|
|
112
|
-
pluginId: string,
|
|
113
|
-
projectPath: string,
|
|
114
|
-
): Promise<VersionMismatchInfo | null> {
|
|
115
|
-
const registry = await readInstalledPluginsRegistry();
|
|
116
|
-
const entries = registry.plugins[pluginId];
|
|
117
|
-
if (!entries || entries.length < 2) return null;
|
|
118
|
-
|
|
119
|
-
const currentEntry = entries.find(
|
|
120
|
-
(e) =>
|
|
121
|
-
e.scope === "project" &&
|
|
122
|
-
normalizePath(e.projectPath) === normalizePath(projectPath),
|
|
123
|
-
);
|
|
124
|
-
if (!currentEntry) return null;
|
|
125
|
-
|
|
126
|
-
const firstEntry = entries[0];
|
|
127
|
-
if (firstEntry.version === currentEntry.version) return null;
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
pluginId,
|
|
131
|
-
firstEntryVersion: firstEntry.version,
|
|
132
|
-
firstEntryProject: firstEntry.projectPath,
|
|
133
|
-
currentProjectVersion: currentEntry.version,
|
|
134
|
-
allEntries: entries,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── Fix ──────────────────────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Fix a version mismatch by updating ALL entries for a plugin to use
|
|
142
|
-
* the target version. This is a JSON-only operation that rewrites
|
|
143
|
-
* installPath and version in each entry.
|
|
144
|
-
*
|
|
145
|
-
* IMPORTANT: This modifies installed_plugins.json which is Claude Code-owned.
|
|
146
|
-
* Only call after explicit user consent.
|
|
147
|
-
*
|
|
148
|
-
* @param pluginId - The plugin ID to fix
|
|
149
|
-
* @param targetVersion - The version to normalize all entries to
|
|
150
|
-
* @returns Number of updated entries and which projects were affected
|
|
151
|
-
*/
|
|
152
|
-
export async function fixPluginVersionMismatch(
|
|
153
|
-
pluginId: string,
|
|
154
|
-
targetVersion: string,
|
|
155
|
-
): Promise<FixResult> {
|
|
156
|
-
const registry = await readInstalledPluginsRegistry();
|
|
157
|
-
const entries = registry.plugins[pluginId];
|
|
158
|
-
if (!entries || entries.length === 0) {
|
|
159
|
-
return { updated: 0, projects: [] };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Find an existing entry with the target version to get the correct installPath
|
|
163
|
-
const templateEntry = entries.find((e) => e.version === targetVersion);
|
|
164
|
-
if (!templateEntry) {
|
|
165
|
-
return { updated: 0, projects: [] };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const targetInstallPath = templateEntry.installPath;
|
|
169
|
-
const updated: string[] = [];
|
|
170
|
-
|
|
171
|
-
for (const entry of entries) {
|
|
172
|
-
if (entry.version !== targetVersion) {
|
|
173
|
-
entry.version = targetVersion;
|
|
174
|
-
entry.installPath = targetInstallPath;
|
|
175
|
-
entry.lastUpdated = new Date().toISOString();
|
|
176
|
-
updated.push(entry.projectPath || "(global)");
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (updated.length > 0) {
|
|
181
|
-
await writeInstalledPluginsRegistry(registry);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return { updated: updated.length, projects: updated };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Fix all version mismatches by normalizing each plugin to its latest version.
|
|
189
|
-
*
|
|
190
|
-
* @param mismatches - Array of mismatches from checkPluginVersionMismatches
|
|
191
|
-
* @returns Map of pluginId to FixResult
|
|
192
|
-
*/
|
|
193
|
-
export async function fixAllPluginVersionMismatches(
|
|
194
|
-
mismatches: VersionMismatchInfo[],
|
|
195
|
-
): Promise<Map<string, FixResult>> {
|
|
196
|
-
const results = new Map<string, FixResult>();
|
|
197
|
-
|
|
198
|
-
// Read registry once, apply all fixes, write once
|
|
199
|
-
const registry = await readInstalledPluginsRegistry();
|
|
200
|
-
|
|
201
|
-
for (const mismatch of mismatches) {
|
|
202
|
-
const entries = registry.plugins[mismatch.pluginId];
|
|
203
|
-
if (!entries || entries.length === 0) {
|
|
204
|
-
results.set(mismatch.pluginId, { updated: 0, projects: [] });
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Use the current project's version as the target (the version the user expects)
|
|
209
|
-
const targetVersion = mismatch.currentProjectVersion;
|
|
210
|
-
const templateEntry = entries.find((e) => e.version === targetVersion);
|
|
211
|
-
if (!templateEntry) {
|
|
212
|
-
results.set(mismatch.pluginId, { updated: 0, projects: [] });
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const targetInstallPath = templateEntry.installPath;
|
|
217
|
-
const updated: string[] = [];
|
|
218
|
-
|
|
219
|
-
for (const entry of entries) {
|
|
220
|
-
if (entry.version !== targetVersion) {
|
|
221
|
-
entry.version = targetVersion;
|
|
222
|
-
entry.installPath = targetInstallPath;
|
|
223
|
-
entry.lastUpdated = new Date().toISOString();
|
|
224
|
-
updated.push(entry.projectPath || "(global)");
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
results.set(mismatch.pluginId, {
|
|
229
|
-
updated: updated.length,
|
|
230
|
-
projects: updated,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
await writeInstalledPluginsRegistry(registry);
|
|
235
|
-
return results;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ── Formatting ───────────────────────────────────────────────────────────────
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Format mismatch info for console output (prerunner).
|
|
242
|
-
*/
|
|
243
|
-
export function formatMismatchWarning(
|
|
244
|
-
mismatches: VersionMismatchInfo[],
|
|
245
|
-
): string {
|
|
246
|
-
const lines: string[] = [];
|
|
247
|
-
lines.push(
|
|
248
|
-
"WARNING: Plugin version mismatch detected (Claude Code bug #45997)",
|
|
249
|
-
);
|
|
250
|
-
lines.push(
|
|
251
|
-
"Claude Code loads the first entry from installed_plugins.json for ALL projects.",
|
|
252
|
-
);
|
|
253
|
-
lines.push(
|
|
254
|
-
"The following plugins will load a different version than what this project has installed:\n",
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
for (const m of mismatches) {
|
|
258
|
-
const firstProject = m.firstEntryProject
|
|
259
|
-
? shortenPath(m.firstEntryProject)
|
|
260
|
-
: "(global)";
|
|
261
|
-
lines.push(` ${m.pluginId}:`);
|
|
262
|
-
lines.push(` Loading v${m.firstEntryVersion} (from ${firstProject})`);
|
|
263
|
-
lines.push(` Expected v${m.currentProjectVersion} (this project)`);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
lines.push("");
|
|
267
|
-
lines.push(`Bug report: ${BUG_REPORT_URL}`);
|
|
268
|
-
lines.push(
|
|
269
|
-
"Run claudeup to fix — it can align all projects to the same version.",
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
return lines.join("\n");
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Format mismatch info for TUI modal display.
|
|
277
|
-
* Returns a multi-line string suitable for a modal message.
|
|
278
|
-
*/
|
|
279
|
-
export function formatMismatchModal(mismatches: VersionMismatchInfo[]): string {
|
|
280
|
-
const lines: string[] = [];
|
|
281
|
-
|
|
282
|
-
lines.push("Your plugins will not work correctly in this project.");
|
|
283
|
-
lines.push("");
|
|
284
|
-
lines.push("Due to a Claude Code bug, the plugin loader ignores");
|
|
285
|
-
lines.push("per-project versions and loads an older version instead.");
|
|
286
|
-
lines.push("Commands, MCP tools, and skills will be outdated.");
|
|
287
|
-
lines.push("");
|
|
288
|
-
|
|
289
|
-
for (const m of mismatches) {
|
|
290
|
-
const name = m.pluginId.split("@")[0];
|
|
291
|
-
lines.push(
|
|
292
|
-
` ${name}: v${m.firstEntryVersion} loaded, expected v${m.currentProjectVersion}`,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
lines.push("");
|
|
297
|
-
lines.push("Fix: align all projects to the same version.");
|
|
298
|
-
lines.push("Details: github.com/anthropics/claude-code/issues/45997");
|
|
299
|
-
|
|
300
|
-
return lines.join("\n");
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Collect all enabled plugin IDs across all scopes for a project.
|
|
307
|
-
*/
|
|
308
|
-
async function collectEnabledPluginIds(
|
|
309
|
-
projectPath: string,
|
|
310
|
-
): Promise<Set<string>> {
|
|
311
|
-
const pluginIds = new Set<string>();
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
const global = await getGlobalEnabledPlugins();
|
|
315
|
-
for (const [id, enabled] of Object.entries(global)) {
|
|
316
|
-
if (enabled) pluginIds.add(id);
|
|
317
|
-
}
|
|
318
|
-
} catch {
|
|
319
|
-
/* skip unreadable */
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
const project = await getEnabledPlugins(projectPath);
|
|
324
|
-
for (const [id, enabled] of Object.entries(project)) {
|
|
325
|
-
if (enabled) pluginIds.add(id);
|
|
326
|
-
}
|
|
327
|
-
} catch {
|
|
328
|
-
/* skip unreadable */
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
const local = await getLocalEnabledPlugins(projectPath);
|
|
333
|
-
for (const [id, enabled] of Object.entries(local)) {
|
|
334
|
-
if (enabled) pluginIds.add(id);
|
|
335
|
-
}
|
|
336
|
-
} catch {
|
|
337
|
-
/* skip unreadable */
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return pluginIds;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Normalize a path for comparison (resolve and trailing-slash normalize).
|
|
345
|
-
*/
|
|
346
|
-
function normalizePath(p: string | undefined): string {
|
|
347
|
-
if (!p) return "";
|
|
348
|
-
return p.replace(/\/+$/, "");
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Shorten a path for display by replacing homedir with ~.
|
|
353
|
-
*/
|
|
354
|
-
function shortenPath(p: string): string {
|
|
355
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
356
|
-
if (home && p.startsWith(home)) {
|
|
357
|
-
return `~${p.slice(home.length)}`;
|
|
358
|
-
}
|
|
359
|
-
return p;
|
|
360
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
6
|
-
|
|
7
|
-
export interface UpdateCacheData {
|
|
8
|
-
lastUpdateCheck: string; // ISO timestamp
|
|
9
|
-
lastUpdateResult: {
|
|
10
|
-
updated: string[]; // ["frontend@magus v3.13.0", ...]
|
|
11
|
-
failed: string[]; // Marketplaces that failed
|
|
12
|
-
autoUpdated?: Array<{
|
|
13
|
-
pluginId: string;
|
|
14
|
-
oldVersion: string;
|
|
15
|
-
newVersion: string;
|
|
16
|
-
}>; // Plugins that were auto-updated
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class UpdateCache {
|
|
21
|
-
private cachePath: string;
|
|
22
|
-
|
|
23
|
-
constructor() {
|
|
24
|
-
// Cache at ~/.claude/claudeup-cache.json
|
|
25
|
-
this.cachePath = path.join(os.homedir(), ".claude", "claudeup-cache.json");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async shouldCheckForUpdates(): Promise<boolean> {
|
|
29
|
-
const data = await this.getLastCheck();
|
|
30
|
-
if (!data) return true; // No cache
|
|
31
|
-
|
|
32
|
-
const lastCheck = new Date(data.lastUpdateCheck);
|
|
33
|
-
const now = new Date();
|
|
34
|
-
const elapsed = now.getTime() - lastCheck.getTime();
|
|
35
|
-
|
|
36
|
-
return elapsed >= CACHE_TTL_MS;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async getLastCheck(): Promise<UpdateCacheData | null> {
|
|
40
|
-
try {
|
|
41
|
-
const content = await fs.readFile(this.cachePath, "utf-8");
|
|
42
|
-
const data = JSON.parse(content) as UpdateCacheData;
|
|
43
|
-
return data;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
// Cache doesn't exist or is corrupted
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async saveCheck(result: UpdateCacheData): Promise<void> {
|
|
51
|
-
try {
|
|
52
|
-
// Ensure .claude directory exists
|
|
53
|
-
const cacheDir = path.dirname(this.cachePath);
|
|
54
|
-
await fs.mkdir(cacheDir, { recursive: true });
|
|
55
|
-
|
|
56
|
-
// Write cache file
|
|
57
|
-
await fs.writeFile(
|
|
58
|
-
this.cachePath,
|
|
59
|
-
JSON.stringify(result, null, 2),
|
|
60
|
-
"utf-8",
|
|
61
|
-
);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
// Non-fatal - just log and continue
|
|
64
|
-
console.warn(
|
|
65
|
-
"Warning: Failed to save update cache:",
|
|
66
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async clear(): Promise<void> {
|
|
72
|
-
try {
|
|
73
|
-
await fs.unlink(this.cachePath);
|
|
74
|
-
} catch (error) {
|
|
75
|
-
// Ignore errors (file might not exist)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import type { VersionMismatchInfo } from "../../../services/plugin-version-check.js";
|
|
3
|
-
import { theme } from "../../theme.js";
|
|
4
|
-
|
|
5
|
-
interface VersionMismatchModalProps {
|
|
6
|
-
/** List of version mismatches detected */
|
|
7
|
-
mismatches: VersionMismatchInfo[];
|
|
8
|
-
/** Currently selected option index (0 = Fix, 1 = Dismiss) */
|
|
9
|
-
defaultIndex?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const COLOR_BG = theme.surface.bg;
|
|
13
|
-
const COLOR_BORDER = theme.colors.dim;
|
|
14
|
-
const COLOR_TITLE = theme.colors.text;
|
|
15
|
-
const COLOR_MUTED = theme.colors.muted;
|
|
16
|
-
const COLOR_DIM = theme.colors.muted;
|
|
17
|
-
const COLOR_WARN = theme.colors.warning;
|
|
18
|
-
const COLOR_BAD = theme.colors.danger;
|
|
19
|
-
const COLOR_GOOD = theme.colors.success;
|
|
20
|
-
const COLOR_ACCENT = theme.colors.text;
|
|
21
|
-
|
|
22
|
-
/** Truncate a plugin name to fit the column width */
|
|
23
|
-
function shortName(pluginId: string, max: number): string {
|
|
24
|
-
const name = pluginId.split("@")[0];
|
|
25
|
-
if (name.length <= max) return name;
|
|
26
|
-
return `${name.slice(0, max - 1)}…`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Truncate a version string to fit the column width */
|
|
30
|
-
function shortVersion(v: string, max: number): string {
|
|
31
|
-
if (v.length <= max) return v;
|
|
32
|
-
return `${v.slice(0, max - 1)}…`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function VersionMismatchModal({
|
|
36
|
-
mismatches,
|
|
37
|
-
defaultIndex,
|
|
38
|
-
}: VersionMismatchModalProps) {
|
|
39
|
-
const selectedIndex = defaultIndex ?? 0;
|
|
40
|
-
|
|
41
|
-
// Column widths chosen to fit a 64-wide modal with padding
|
|
42
|
-
const NAME_W = 22;
|
|
43
|
-
const VER_W = 12;
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<box
|
|
47
|
-
flexDirection="column"
|
|
48
|
-
border
|
|
49
|
-
borderStyle="rounded"
|
|
50
|
-
borderColor={COLOR_BORDER}
|
|
51
|
-
backgroundColor={COLOR_BG}
|
|
52
|
-
paddingLeft={3}
|
|
53
|
-
paddingRight={3}
|
|
54
|
-
paddingTop={1}
|
|
55
|
-
paddingBottom={1}
|
|
56
|
-
width={64}
|
|
57
|
-
>
|
|
58
|
-
{/* Title */}
|
|
59
|
-
<box marginBottom={1}>
|
|
60
|
-
<text fg={COLOR_TITLE}>
|
|
61
|
-
<span fg={COLOR_WARN}>⚠ </span>
|
|
62
|
-
<strong>Plugin Version Mismatch</strong>
|
|
63
|
-
</text>
|
|
64
|
-
</box>
|
|
65
|
-
|
|
66
|
-
{/* Description */}
|
|
67
|
-
<box flexDirection="column" marginBottom={1}>
|
|
68
|
-
<text fg={COLOR_MUTED}>
|
|
69
|
-
Plugins in this project will load <strong>wrong versions</strong>
|
|
70
|
-
</text>
|
|
71
|
-
<text fg={COLOR_MUTED}>due to a Claude Code bug (#45997).</text>
|
|
72
|
-
</box>
|
|
73
|
-
|
|
74
|
-
{/* Table header */}
|
|
75
|
-
<box flexDirection="column" marginBottom={1}>
|
|
76
|
-
<text fg={COLOR_DIM}>
|
|
77
|
-
<strong>
|
|
78
|
-
{"Plugin".padEnd(NAME_W)}
|
|
79
|
-
{"Loaded".padEnd(VER_W)}
|
|
80
|
-
{"Expected".padEnd(VER_W)}
|
|
81
|
-
</strong>
|
|
82
|
-
</text>
|
|
83
|
-
<text fg={COLOR_BORDER}>
|
|
84
|
-
{"─".repeat(NAME_W - 1)} {"─".repeat(VER_W - 1)}{" "}
|
|
85
|
-
{"─".repeat(VER_W - 1)}
|
|
86
|
-
</text>
|
|
87
|
-
|
|
88
|
-
{/* Table rows */}
|
|
89
|
-
{mismatches.map((m) => {
|
|
90
|
-
const name = shortName(m.pluginId, NAME_W - 1);
|
|
91
|
-
const loaded = `v${shortVersion(m.firstEntryVersion, VER_W - 2)}`;
|
|
92
|
-
const expected = `v${shortVersion(m.currentProjectVersion, VER_W - 2)}`;
|
|
93
|
-
return (
|
|
94
|
-
<text key={m.pluginId} fg={COLOR_MUTED}>
|
|
95
|
-
<span fg={COLOR_ACCENT}>{name.padEnd(NAME_W)}</span>
|
|
96
|
-
<span fg={COLOR_BAD}>{loaded.padEnd(VER_W)}</span>
|
|
97
|
-
<span fg={COLOR_GOOD}>{expected.padEnd(VER_W)}</span>
|
|
98
|
-
</text>
|
|
99
|
-
);
|
|
100
|
-
})}
|
|
101
|
-
</box>
|
|
102
|
-
|
|
103
|
-
{/* Footer note */}
|
|
104
|
-
<box marginBottom={1}>
|
|
105
|
-
<text fg={COLOR_DIM}>
|
|
106
|
-
Details: github.com/anthropics/claude-code/issues/45997
|
|
107
|
-
</text>
|
|
108
|
-
</box>
|
|
109
|
-
|
|
110
|
-
{/* Options */}
|
|
111
|
-
<box flexDirection="column" paddingLeft={1}>
|
|
112
|
-
<text fg={selectedIndex === 0 ? COLOR_ACCENT : COLOR_MUTED}>
|
|
113
|
-
<span fg={selectedIndex === 0 ? COLOR_ACCENT : COLOR_DIM}>
|
|
114
|
-
{selectedIndex === 0 ? "❯ " : " "}
|
|
115
|
-
</span>
|
|
116
|
-
{selectedIndex === 0 ? (
|
|
117
|
-
<strong>Fix all projects</strong>
|
|
118
|
-
) : (
|
|
119
|
-
"Fix all projects"
|
|
120
|
-
)}
|
|
121
|
-
</text>
|
|
122
|
-
<text fg={selectedIndex === 1 ? COLOR_ACCENT : COLOR_MUTED}>
|
|
123
|
-
<span fg={selectedIndex === 1 ? COLOR_ACCENT : COLOR_DIM}>
|
|
124
|
-
{selectedIndex === 1 ? "❯ " : " "}
|
|
125
|
-
</span>
|
|
126
|
-
{selectedIndex === 1 ? <strong>Dismiss</strong> : "Dismiss"}
|
|
127
|
-
</text>
|
|
128
|
-
</box>
|
|
129
|
-
|
|
130
|
-
{/* Keybind hints */}
|
|
131
|
-
<box marginTop={1}>
|
|
132
|
-
<text fg={COLOR_DIM}>↑↓ Select • ↵ Confirm • Esc Cancel</text>
|
|
133
|
-
</box>
|
|
134
|
-
</box>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export default VersionMismatchModal;
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import {
|
|
3
|
-
type VersionMismatchInfo,
|
|
4
|
-
fixAllPluginVersionMismatches,
|
|
5
|
-
} from "../../services/plugin-version-check.js";
|
|
6
|
-
import { useApp } from "../state/AppContext.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Shared hook for showing the plugin version mismatch modal.
|
|
10
|
-
*
|
|
11
|
-
* Used in two places:
|
|
12
|
-
* 1. App.tsx — startup check shows mismatches found across all enabled plugins
|
|
13
|
-
* 2. PluginsScreen.tsx — post-install/update check shows a single plugin mismatch
|
|
14
|
-
*
|
|
15
|
-
* Both flows now share the same interactive modal with a "Fix all projects"
|
|
16
|
-
* button, so users never have to restart claudeup to access the fix.
|
|
17
|
-
*
|
|
18
|
-
* The fix uses fixAllPluginVersionMismatches() which rewrites
|
|
19
|
-
* installed_plugins.json in-place to align all entries to the current
|
|
20
|
-
* project's expected versions. After success, shows a hint to restart
|
|
21
|
-
* Claude Code so the new versions actually load (the running session
|
|
22
|
-
* has the old plugin code in memory).
|
|
23
|
-
*/
|
|
24
|
-
export function useMismatchModal() {
|
|
25
|
-
const { dispatch } = useApp();
|
|
26
|
-
|
|
27
|
-
const show = useCallback(
|
|
28
|
-
(mismatches: VersionMismatchInfo[]) => {
|
|
29
|
-
if (mismatches.length === 0) return;
|
|
30
|
-
|
|
31
|
-
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
32
|
-
|
|
33
|
-
const fix = async () => {
|
|
34
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
35
|
-
dispatch({
|
|
36
|
-
type: "SHOW_PROGRESS",
|
|
37
|
-
state: { message: "Aligning plugin versions across projects..." },
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const results = await fixAllPluginVersionMismatches(mismatches);
|
|
42
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
43
|
-
|
|
44
|
-
// Count how many projects were updated across all plugins
|
|
45
|
-
const totalUpdated = Array.from(results.values()).reduce(
|
|
46
|
-
(sum, r) => sum + r.updated,
|
|
47
|
-
0,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
dispatch({
|
|
51
|
-
type: "SHOW_MODAL",
|
|
52
|
-
modal: {
|
|
53
|
-
type: "message",
|
|
54
|
-
title: "Plugin Versions Aligned",
|
|
55
|
-
message: `Updated ${totalUpdated} project entr${totalUpdated === 1 ? "y" : "ies"} in installed_plugins.json.\n\nRestart Claude Code for the new versions to load. The running session still has the old plugin code in memory.`,
|
|
56
|
-
variant: "success",
|
|
57
|
-
onDismiss: dismiss,
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
} catch (error) {
|
|
61
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
62
|
-
dispatch({
|
|
63
|
-
type: "SHOW_MODAL",
|
|
64
|
-
modal: {
|
|
65
|
-
type: "message",
|
|
66
|
-
title: "Fix Failed",
|
|
67
|
-
message: `Could not align plugin versions: ${error instanceof Error ? error.message : String(error)}\n\nTry running claudeup again, or manually update the plugin in each project.`,
|
|
68
|
-
variant: "error",
|
|
69
|
-
onDismiss: dismiss,
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
dispatch({
|
|
76
|
-
type: "SHOW_MODAL",
|
|
77
|
-
modal: {
|
|
78
|
-
type: "version-mismatch",
|
|
79
|
-
mismatches,
|
|
80
|
-
onFix: fix,
|
|
81
|
-
onDismiss: dismiss,
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
},
|
|
85
|
-
[dispatch],
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
return { show };
|
|
89
|
-
}
|