claudeup 6.5.1 → 6.6.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.
Files changed (78) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/active-closure.test.ts +184 -0
  3. package/src/__tests__/blackbox-active-closure.test.ts +119 -0
  4. package/src/__tests__/blackbox-convergence.test.ts +334 -0
  5. package/src/__tests__/blackbox-parity.test.ts +684 -0
  6. package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
  7. package/src/__tests__/blackbox-tool-state.test.ts +669 -0
  8. package/src/__tests__/cli-apply-seams.test.ts +2 -0
  9. package/src/__tests__/cli-tool-commands.test.ts +1 -1
  10. package/src/__tests__/cli-tools-adapter.test.ts +294 -0
  11. package/src/__tests__/cli-update-view.test.ts +142 -2
  12. package/src/__tests__/content-drift.test.ts +60 -0
  13. package/src/__tests__/enabled-not-installed.test.ts +1 -2
  14. package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
  15. package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
  16. package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
  17. package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
  18. package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
  19. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
  20. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
  21. package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
  22. package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
  23. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
  24. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
  25. package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
  26. package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
  27. package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
  28. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
  29. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
  30. package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
  31. package/src/__tests__/{gitignore-prerun.test.ts → gitignore-check.test.ts} +8 -6
  32. package/src/__tests__/plugin-cli-argv.test.ts +1 -1
  33. package/src/__tests__/plugin-row-text.test.ts +336 -0
  34. package/src/__tests__/scope-action.test.ts +69 -19
  35. package/src/__tests__/shell-script-callers.test.ts +8 -8
  36. package/src/__tests__/tool-probe.test.ts +1400 -0
  37. package/src/__tests__/update-apply.test.ts +283 -1
  38. package/src/__tests__/update-plan.test.ts +773 -31
  39. package/src/__tests__/version-unknown-state.test.ts +388 -0
  40. package/src/cli/router.ts +0 -5
  41. package/src/cli/update-view.ts +92 -12
  42. package/src/cli/update.ts +114 -38
  43. package/src/services/active-closure.ts +117 -0
  44. package/src/services/catalog-cache-store.ts +6 -2
  45. package/src/services/claude-settings.ts +1 -173
  46. package/src/services/{gitignore-prerun.ts → gitignore-check.ts} +14 -10
  47. package/src/services/gitignore-fixer.ts +1 -1
  48. package/src/services/marketplace-refresh.ts +30 -0
  49. package/src/services/plugin-manager.ts +200 -283
  50. package/src/services/resolver.ts +7 -2
  51. package/src/services/tool-probe.ts +1637 -0
  52. package/src/services/toolchain.ts +97 -0
  53. package/src/services/update-engine.ts +220 -32
  54. package/src/services/update-plan.ts +640 -160
  55. package/src/services/version-snapshot.ts +1 -1
  56. package/src/types/bun.d.ts +6 -0
  57. package/src/ui/App.tsx +2 -27
  58. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  59. package/src/ui/adapters/pluginsAdapter.ts +57 -0
  60. package/src/ui/components/modals/ModalContainer.tsx +0 -26
  61. package/src/ui/hooks/index.ts +0 -1
  62. package/src/ui/hooks/useGitignoreModal.ts +2 -3
  63. package/src/ui/renderers/cliToolRenderers.tsx +108 -67
  64. package/src/ui/renderers/pluginRenderers.tsx +157 -98
  65. package/src/ui/renderers/pluginRowText.ts +70 -0
  66. package/src/ui/renderers/pluginStatusText.ts +56 -0
  67. package/src/ui/screens/CliToolsScreen.tsx +154 -280
  68. package/src/ui/screens/PluginsScreen.tsx +156 -140
  69. package/src/ui/state/types.ts +0 -7
  70. package/src/__tests__/dual-write-prevention.test.ts +0 -380
  71. package/src/__tests__/gap-fill-versions.test.ts +0 -389
  72. package/src/__tests__/plugin-version-check.test.ts +0 -762
  73. package/src/cli/claude.ts +0 -21
  74. package/src/prerunner/index.ts +0 -500
  75. package/src/services/plugin-version-check.ts +0 -360
  76. package/src/services/update-cache.ts +0 -78
  77. package/src/ui/components/modals/VersionMismatchModal.tsx +0 -138
  78. 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
- }