claudeup 4.11.2 → 4.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ui/App.js +6 -63
- package/src/ui/App.tsx +5 -64
- package/src/ui/components/modals/ModalContainer.js +26 -0
- package/src/ui/components/modals/ModalContainer.tsx +26 -0
- package/src/ui/components/modals/VersionMismatchModal.js +36 -0
- package/src/ui/components/modals/VersionMismatchModal.tsx +140 -0
- package/src/ui/hooks/index.js +1 -0
- package/src/ui/hooks/index.ts +1 -0
- package/src/ui/hooks/useMismatchModal.js +77 -0
- package/src/ui/hooks/useMismatchModal.ts +95 -0
- package/src/ui/screens/PluginsScreen.js +45 -13
- package/src/ui/screens/PluginsScreen.tsx +54 -17
- package/src/ui/state/types.ts +7 -0
package/package.json
CHANGED
package/src/ui/App.js
CHANGED
|
@@ -8,7 +8,8 @@ import { ModalContainer } from "./components/modals/index.js";
|
|
|
8
8
|
import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, } from "./screens/index.js";
|
|
9
9
|
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
10
10
|
import { migrateMarketplaceRename, recoverMarketplaceSettings, } from "../services/claude-settings.js";
|
|
11
|
-
import { checkPluginVersionMismatches,
|
|
11
|
+
import { checkPluginVersionMismatches, } from "../services/plugin-version-check.js";
|
|
12
|
+
import { useMismatchModal } from "./hooks/useMismatchModal.js";
|
|
12
13
|
import { checkForUpdates, getCurrentVersion, } from "../services/version-check.js";
|
|
13
14
|
import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
|
|
14
15
|
import { ProgressBar } from "./components/layout/ProgressBar.js";
|
|
@@ -189,6 +190,7 @@ function AppContent({ onExit }) {
|
|
|
189
190
|
const [updateInfo, setUpdateInfo] = useState(null);
|
|
190
191
|
const [recoveryReport, setRecoveryReport] = useState(null);
|
|
191
192
|
const [mismatchData, setMismatchData] = useState(null);
|
|
193
|
+
const mismatchModal = useMismatchModal();
|
|
192
194
|
// Check for updates on startup (non-blocking)
|
|
193
195
|
useEffect(() => {
|
|
194
196
|
checkForUpdates()
|
|
@@ -206,68 +208,9 @@ function AppContent({ onExit }) {
|
|
|
206
208
|
useEffect(() => {
|
|
207
209
|
if (!mismatchData || mismatchData.length === 0)
|
|
208
210
|
return;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
type: "select",
|
|
213
|
-
title: "⚠ Plugin Version Mismatch",
|
|
214
|
-
message: formatMismatchModal(mismatchData),
|
|
215
|
-
options: [
|
|
216
|
-
{
|
|
217
|
-
label: "Fix all projects",
|
|
218
|
-
value: "fix",
|
|
219
|
-
description: "Align all projects to this project's versions",
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
label: "Dismiss",
|
|
223
|
-
value: "dismiss",
|
|
224
|
-
description: "Ignore for now",
|
|
225
|
-
},
|
|
226
|
-
],
|
|
227
|
-
onSelect: async (value) => {
|
|
228
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
229
|
-
if (value === "fix") {
|
|
230
|
-
dispatch({
|
|
231
|
-
type: "SHOW_PROGRESS",
|
|
232
|
-
state: { message: "Fixing plugin versions..." },
|
|
233
|
-
});
|
|
234
|
-
try {
|
|
235
|
-
await fixAllPluginVersionMismatches(mismatchData);
|
|
236
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
237
|
-
dispatch({
|
|
238
|
-
type: "SHOW_MODAL",
|
|
239
|
-
modal: {
|
|
240
|
-
type: "message",
|
|
241
|
-
title: "Fixed",
|
|
242
|
-
message: "All plugin versions aligned. Restart Claude Code for changes to take effect.",
|
|
243
|
-
variant: "success",
|
|
244
|
-
onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
250
|
-
dispatch({
|
|
251
|
-
type: "SHOW_MODAL",
|
|
252
|
-
modal: {
|
|
253
|
-
type: "message",
|
|
254
|
-
title: "Error",
|
|
255
|
-
message: "Failed to fix plugin versions. Try running claudeup again.",
|
|
256
|
-
variant: "error",
|
|
257
|
-
onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
setMismatchData(null);
|
|
263
|
-
},
|
|
264
|
-
onCancel: () => {
|
|
265
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
266
|
-
setMismatchData(null);
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
}, [mismatchData, dispatch]);
|
|
211
|
+
mismatchModal.show(mismatchData);
|
|
212
|
+
setMismatchData(null);
|
|
213
|
+
}, [mismatchData, mismatchModal]);
|
|
271
214
|
// Auto-refresh marketplaces on startup
|
|
272
215
|
useEffect(() => {
|
|
273
216
|
const noRefresh = process.argv.includes("--no-refresh");
|
package/src/ui/App.tsx
CHANGED
|
@@ -30,10 +30,9 @@ import {
|
|
|
30
30
|
} from "../services/claude-settings.js";
|
|
31
31
|
import {
|
|
32
32
|
checkPluginVersionMismatches,
|
|
33
|
-
fixAllPluginVersionMismatches,
|
|
34
|
-
formatMismatchModal,
|
|
35
33
|
type VersionMismatchInfo,
|
|
36
34
|
} from "../services/plugin-version-check.js";
|
|
35
|
+
import { useMismatchModal } from "./hooks/useMismatchModal.js";
|
|
37
36
|
import {
|
|
38
37
|
checkForUpdates,
|
|
39
38
|
getCurrentVersion,
|
|
@@ -317,6 +316,7 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
317
316
|
const [updateInfo, setUpdateInfo] = useState<VersionCheckResult | null>(null);
|
|
318
317
|
const [recoveryReport, setRecoveryReport] = useState<string | null>(null);
|
|
319
318
|
const [mismatchData, setMismatchData] = useState<VersionMismatchInfo[] | null>(null);
|
|
319
|
+
const mismatchModal = useMismatchModal();
|
|
320
320
|
|
|
321
321
|
// Check for updates on startup (non-blocking)
|
|
322
322
|
useEffect(() => {
|
|
@@ -335,68 +335,9 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
335
335
|
// Show mismatch modal when data arrives
|
|
336
336
|
useEffect(() => {
|
|
337
337
|
if (!mismatchData || mismatchData.length === 0) return;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
type: "select",
|
|
342
|
-
title: "⚠ Plugin Version Mismatch",
|
|
343
|
-
message: formatMismatchModal(mismatchData),
|
|
344
|
-
options: [
|
|
345
|
-
{
|
|
346
|
-
label: "Fix all projects",
|
|
347
|
-
value: "fix",
|
|
348
|
-
description: "Align all projects to this project's versions",
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
label: "Dismiss",
|
|
352
|
-
value: "dismiss",
|
|
353
|
-
description: "Ignore for now",
|
|
354
|
-
},
|
|
355
|
-
],
|
|
356
|
-
onSelect: async (value: string) => {
|
|
357
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
358
|
-
if (value === "fix") {
|
|
359
|
-
dispatch({
|
|
360
|
-
type: "SHOW_PROGRESS",
|
|
361
|
-
state: { message: "Fixing plugin versions..." },
|
|
362
|
-
});
|
|
363
|
-
try {
|
|
364
|
-
await fixAllPluginVersionMismatches(mismatchData);
|
|
365
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
366
|
-
dispatch({
|
|
367
|
-
type: "SHOW_MODAL",
|
|
368
|
-
modal: {
|
|
369
|
-
type: "message",
|
|
370
|
-
title: "Fixed",
|
|
371
|
-
message:
|
|
372
|
-
"All plugin versions aligned. Restart Claude Code for changes to take effect.",
|
|
373
|
-
variant: "success",
|
|
374
|
-
onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
|
|
375
|
-
},
|
|
376
|
-
});
|
|
377
|
-
} catch {
|
|
378
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
379
|
-
dispatch({
|
|
380
|
-
type: "SHOW_MODAL",
|
|
381
|
-
modal: {
|
|
382
|
-
type: "message",
|
|
383
|
-
title: "Error",
|
|
384
|
-
message: "Failed to fix plugin versions. Try running claudeup again.",
|
|
385
|
-
variant: "error",
|
|
386
|
-
onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
|
|
387
|
-
},
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
setMismatchData(null);
|
|
392
|
-
},
|
|
393
|
-
onCancel: () => {
|
|
394
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
395
|
-
setMismatchData(null);
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
}, [mismatchData, dispatch]);
|
|
338
|
+
mismatchModal.show(mismatchData);
|
|
339
|
+
setMismatchData(null);
|
|
340
|
+
}, [mismatchData, mismatchModal]);
|
|
400
341
|
|
|
401
342
|
// Auto-refresh marketplaces on startup
|
|
402
343
|
useEffect(() => {
|
|
@@ -7,6 +7,7 @@ import { InputModal } from "./InputModal.js";
|
|
|
7
7
|
import { SelectModal } from "./SelectModal.js";
|
|
8
8
|
import { MessageModal } from "./MessageModal.js";
|
|
9
9
|
import { LoadingModal } from "./LoadingModal.js";
|
|
10
|
+
import { VersionMismatchModal } from "./VersionMismatchModal.js";
|
|
10
11
|
/**
|
|
11
12
|
* Container that renders the active modal as an overlay
|
|
12
13
|
* Handles ALL keyboard events when a modal is open to avoid
|
|
@@ -27,6 +28,9 @@ export function ModalContainer() {
|
|
|
27
28
|
if (modal?.type === "select") {
|
|
28
29
|
setSelectIndex(modal.defaultIndex ?? 0);
|
|
29
30
|
}
|
|
31
|
+
else if (modal?.type === "version-mismatch") {
|
|
32
|
+
setSelectIndex(0);
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
// Handle keyboard events for modals
|
|
32
36
|
useKeyboard((key) => {
|
|
@@ -44,6 +48,8 @@ export function ModalContainer() {
|
|
|
44
48
|
modal.onCancel();
|
|
45
49
|
else if (modal.type === "message")
|
|
46
50
|
modal.onDismiss();
|
|
51
|
+
else if (modal.type === "version-mismatch")
|
|
52
|
+
modal.onDismiss();
|
|
47
53
|
return;
|
|
48
54
|
}
|
|
49
55
|
// 'q' to close — but NOT for input modals (need to type 'q')
|
|
@@ -54,6 +60,8 @@ export function ModalContainer() {
|
|
|
54
60
|
modal.onCancel();
|
|
55
61
|
else if (modal.type === "message")
|
|
56
62
|
modal.onDismiss();
|
|
63
|
+
else if (modal.type === "version-mismatch")
|
|
64
|
+
modal.onDismiss();
|
|
57
65
|
return;
|
|
58
66
|
}
|
|
59
67
|
// Input modal — let OpenTUI <input> handle Enter via onSubmit
|
|
@@ -73,6 +81,22 @@ export function ModalContainer() {
|
|
|
73
81
|
}
|
|
74
82
|
return;
|
|
75
83
|
}
|
|
84
|
+
// Version-mismatch modal — handle navigation and selection (2 options)
|
|
85
|
+
if (modal.type === "version-mismatch") {
|
|
86
|
+
if (key.name === "return" || key.name === "enter") {
|
|
87
|
+
if (selectIndex === 0)
|
|
88
|
+
modal.onFix();
|
|
89
|
+
else
|
|
90
|
+
modal.onDismiss();
|
|
91
|
+
}
|
|
92
|
+
else if (key.name === "up" || key.name === "k") {
|
|
93
|
+
setSelectIndex((prev) => Math.max(0, prev - 1));
|
|
94
|
+
}
|
|
95
|
+
else if (key.name === "down" || key.name === "j") {
|
|
96
|
+
setSelectIndex((prev) => Math.min(1, prev + 1));
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
76
100
|
// Message modal — Enter to dismiss
|
|
77
101
|
if (modal.type === "message") {
|
|
78
102
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -101,6 +125,8 @@ export function ModalContainer() {
|
|
|
101
125
|
return (_jsx(MessageModal, { title: modal.title, message: modal.message, variant: modal.variant, onDismiss: modal.onDismiss }));
|
|
102
126
|
case "loading":
|
|
103
127
|
return _jsx(LoadingModal, { message: modal.message });
|
|
128
|
+
case "version-mismatch":
|
|
129
|
+
return (_jsx(VersionMismatchModal, { mismatches: modal.mismatches, defaultIndex: selectIndex }));
|
|
104
130
|
default:
|
|
105
131
|
return null;
|
|
106
132
|
}
|
|
@@ -6,6 +6,7 @@ import { InputModal } from "./InputModal.js";
|
|
|
6
6
|
import { SelectModal } from "./SelectModal.js";
|
|
7
7
|
import { MessageModal } from "./MessageModal.js";
|
|
8
8
|
import { LoadingModal } from "./LoadingModal.js";
|
|
9
|
+
import { VersionMismatchModal } from "./VersionMismatchModal.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Container that renders the active modal as an overlay
|
|
@@ -28,6 +29,8 @@ export function ModalContainer() {
|
|
|
28
29
|
modalRef.current = modal;
|
|
29
30
|
if (modal?.type === "select") {
|
|
30
31
|
setSelectIndex(modal.defaultIndex ?? 0);
|
|
32
|
+
} else if (modal?.type === "version-mismatch") {
|
|
33
|
+
setSelectIndex(0);
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -42,6 +45,7 @@ export function ModalContainer() {
|
|
|
42
45
|
else if (modal.type === "input") modal.onCancel();
|
|
43
46
|
else if (modal.type === "select") modal.onCancel();
|
|
44
47
|
else if (modal.type === "message") modal.onDismiss();
|
|
48
|
+
else if (modal.type === "version-mismatch") modal.onDismiss();
|
|
45
49
|
return;
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -50,6 +54,7 @@ export function ModalContainer() {
|
|
|
50
54
|
if (modal.type === "confirm") modal.onCancel();
|
|
51
55
|
else if (modal.type === "select") modal.onCancel();
|
|
52
56
|
else if (modal.type === "message") modal.onDismiss();
|
|
57
|
+
else if (modal.type === "version-mismatch") modal.onDismiss();
|
|
53
58
|
return;
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -70,6 +75,19 @@ export function ModalContainer() {
|
|
|
70
75
|
return;
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
// Version-mismatch modal — handle navigation and selection (2 options)
|
|
79
|
+
if (modal.type === "version-mismatch") {
|
|
80
|
+
if (key.name === "return" || key.name === "enter") {
|
|
81
|
+
if (selectIndex === 0) modal.onFix();
|
|
82
|
+
else modal.onDismiss();
|
|
83
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
84
|
+
setSelectIndex((prev) => Math.max(0, prev - 1));
|
|
85
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
86
|
+
setSelectIndex((prev) => Math.min(1, prev + 1));
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
// Message modal — Enter to dismiss
|
|
74
92
|
if (modal.type === "message") {
|
|
75
93
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -137,6 +155,14 @@ export function ModalContainer() {
|
|
|
137
155
|
case "loading":
|
|
138
156
|
return <LoadingModal message={modal.message} />;
|
|
139
157
|
|
|
158
|
+
case "version-mismatch":
|
|
159
|
+
return (
|
|
160
|
+
<VersionMismatchModal
|
|
161
|
+
mismatches={modal.mismatches}
|
|
162
|
+
defaultIndex={selectIndex}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
|
|
140
166
|
default:
|
|
141
167
|
return null;
|
|
142
168
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
const COLOR_BG = "#1C1C1E";
|
|
3
|
+
const COLOR_BORDER = "#525252";
|
|
4
|
+
const COLOR_TITLE = "#EDEDED";
|
|
5
|
+
const COLOR_MUTED = "#A1A1AA";
|
|
6
|
+
const COLOR_DIM = "#71717A";
|
|
7
|
+
const COLOR_WARN = "#FBBF24";
|
|
8
|
+
const COLOR_BAD = "#F87171";
|
|
9
|
+
const COLOR_GOOD = "#4ADE80";
|
|
10
|
+
const COLOR_ACCENT = "#F4F4F5";
|
|
11
|
+
/** Truncate a plugin name to fit the column width */
|
|
12
|
+
function shortName(pluginId, max) {
|
|
13
|
+
const name = pluginId.split("@")[0];
|
|
14
|
+
if (name.length <= max)
|
|
15
|
+
return name;
|
|
16
|
+
return `${name.slice(0, max - 1)}…`;
|
|
17
|
+
}
|
|
18
|
+
/** Truncate a version string to fit the column width */
|
|
19
|
+
function shortVersion(v, max) {
|
|
20
|
+
if (v.length <= max)
|
|
21
|
+
return v;
|
|
22
|
+
return `${v.slice(0, max - 1)}…`;
|
|
23
|
+
}
|
|
24
|
+
export function VersionMismatchModal({ mismatches, defaultIndex, }) {
|
|
25
|
+
const selectedIndex = defaultIndex ?? 0;
|
|
26
|
+
// Column widths chosen to fit a 64-wide modal with padding
|
|
27
|
+
const NAME_W = 22;
|
|
28
|
+
const VER_W = 12;
|
|
29
|
+
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: COLOR_BORDER, backgroundColor: COLOR_BG, paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 64, children: [_jsx("box", { marginBottom: 1, children: _jsxs("text", { fg: COLOR_TITLE, children: [_jsx("span", { fg: COLOR_WARN, children: "\u26A0 " }), _jsx("strong", { children: "Plugin Version Mismatch" })] }) }), _jsxs("box", { flexDirection: "column", marginBottom: 1, children: [_jsxs("text", { fg: COLOR_MUTED, children: ["Plugins in this project will load ", _jsx("strong", { children: "wrong versions" })] }), _jsx("text", { fg: COLOR_MUTED, children: "due to a Claude Code bug (#45997)." })] }), _jsxs("box", { flexDirection: "column", marginBottom: 1, children: [_jsx("text", { fg: COLOR_DIM, children: _jsxs("strong", { children: ["Plugin".padEnd(NAME_W), "Loaded".padEnd(VER_W), "Expected".padEnd(VER_W)] }) }), _jsxs("text", { fg: COLOR_BORDER, children: ["─".repeat(NAME_W - 1), " ", "─".repeat(VER_W - 1), " ", "─".repeat(VER_W - 1)] }), mismatches.map((m) => {
|
|
30
|
+
const name = shortName(m.pluginId, NAME_W - 1);
|
|
31
|
+
const loaded = `v${shortVersion(m.firstEntryVersion, VER_W - 2)}`;
|
|
32
|
+
const expected = `v${shortVersion(m.currentProjectVersion, VER_W - 2)}`;
|
|
33
|
+
return (_jsxs("text", { fg: COLOR_MUTED, children: [_jsx("span", { fg: COLOR_ACCENT, children: name.padEnd(NAME_W) }), _jsx("span", { fg: COLOR_BAD, children: loaded.padEnd(VER_W) }), _jsx("span", { fg: COLOR_GOOD, children: expected.padEnd(VER_W) })] }, m.pluginId));
|
|
34
|
+
})] }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: COLOR_DIM, children: "Details: github.com/anthropics/claude-code/issues/45997" }) }), _jsxs("box", { flexDirection: "column", paddingLeft: 1, children: [_jsxs("text", { fg: selectedIndex === 0 ? COLOR_ACCENT : COLOR_MUTED, children: [_jsx("span", { fg: selectedIndex === 0 ? COLOR_ACCENT : COLOR_DIM, children: selectedIndex === 0 ? "❯ " : " " }), selectedIndex === 0 ? (_jsx("strong", { children: "Fix all projects" })) : ("Fix all projects")] }), _jsxs("text", { fg: selectedIndex === 1 ? COLOR_ACCENT : COLOR_MUTED, children: [_jsx("span", { fg: selectedIndex === 1 ? COLOR_ACCENT : COLOR_DIM, children: selectedIndex === 1 ? "❯ " : " " }), selectedIndex === 1 ? _jsx("strong", { children: "Dismiss" }) : "Dismiss"] })] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: COLOR_DIM, children: "\u2191\u2193 Select \u2022 \u21B5 Confirm \u2022 Esc Cancel" }) })] }));
|
|
35
|
+
}
|
|
36
|
+
export default VersionMismatchModal;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { VersionMismatchInfo } from "../../../services/plugin-version-check.js";
|
|
3
|
+
|
|
4
|
+
interface VersionMismatchModalProps {
|
|
5
|
+
/** List of version mismatches detected */
|
|
6
|
+
mismatches: VersionMismatchInfo[];
|
|
7
|
+
/** Currently selected option index (0 = Fix, 1 = Dismiss) */
|
|
8
|
+
defaultIndex?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const COLOR_BG = "#1C1C1E";
|
|
12
|
+
const COLOR_BORDER = "#525252";
|
|
13
|
+
const COLOR_TITLE = "#EDEDED";
|
|
14
|
+
const COLOR_MUTED = "#A1A1AA";
|
|
15
|
+
const COLOR_DIM = "#71717A";
|
|
16
|
+
const COLOR_WARN = "#FBBF24";
|
|
17
|
+
const COLOR_BAD = "#F87171";
|
|
18
|
+
const COLOR_GOOD = "#4ADE80";
|
|
19
|
+
const COLOR_ACCENT = "#F4F4F5";
|
|
20
|
+
|
|
21
|
+
/** Truncate a plugin name to fit the column width */
|
|
22
|
+
function shortName(pluginId: string, max: number): string {
|
|
23
|
+
const name = pluginId.split("@")[0];
|
|
24
|
+
if (name.length <= max) return name;
|
|
25
|
+
return `${name.slice(0, max - 1)}…`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Truncate a version string to fit the column width */
|
|
29
|
+
function shortVersion(v: string, max: number): string {
|
|
30
|
+
if (v.length <= max) return v;
|
|
31
|
+
return `${v.slice(0, max - 1)}…`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function VersionMismatchModal({
|
|
35
|
+
mismatches,
|
|
36
|
+
defaultIndex,
|
|
37
|
+
}: VersionMismatchModalProps) {
|
|
38
|
+
const selectedIndex = defaultIndex ?? 0;
|
|
39
|
+
|
|
40
|
+
// Column widths chosen to fit a 64-wide modal with padding
|
|
41
|
+
const NAME_W = 22;
|
|
42
|
+
const VER_W = 12;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<box
|
|
46
|
+
flexDirection="column"
|
|
47
|
+
border
|
|
48
|
+
borderStyle="rounded"
|
|
49
|
+
borderColor={COLOR_BORDER}
|
|
50
|
+
backgroundColor={COLOR_BG}
|
|
51
|
+
paddingLeft={3}
|
|
52
|
+
paddingRight={3}
|
|
53
|
+
paddingTop={1}
|
|
54
|
+
paddingBottom={1}
|
|
55
|
+
width={64}
|
|
56
|
+
>
|
|
57
|
+
{/* Title */}
|
|
58
|
+
<box marginBottom={1}>
|
|
59
|
+
<text fg={COLOR_TITLE}>
|
|
60
|
+
<span fg={COLOR_WARN}>⚠ </span>
|
|
61
|
+
<strong>Plugin Version Mismatch</strong>
|
|
62
|
+
</text>
|
|
63
|
+
</box>
|
|
64
|
+
|
|
65
|
+
{/* Description */}
|
|
66
|
+
<box flexDirection="column" marginBottom={1}>
|
|
67
|
+
<text fg={COLOR_MUTED}>
|
|
68
|
+
Plugins in this project will load <strong>wrong versions</strong>
|
|
69
|
+
</text>
|
|
70
|
+
<text fg={COLOR_MUTED}>due to a Claude Code bug (#45997).</text>
|
|
71
|
+
</box>
|
|
72
|
+
|
|
73
|
+
{/* Table header */}
|
|
74
|
+
<box flexDirection="column" marginBottom={1}>
|
|
75
|
+
<text fg={COLOR_DIM}>
|
|
76
|
+
<strong>
|
|
77
|
+
{"Plugin".padEnd(NAME_W)}
|
|
78
|
+
{"Loaded".padEnd(VER_W)}
|
|
79
|
+
{"Expected".padEnd(VER_W)}
|
|
80
|
+
</strong>
|
|
81
|
+
</text>
|
|
82
|
+
<text fg={COLOR_BORDER}>
|
|
83
|
+
{"─".repeat(NAME_W - 1)} {"─".repeat(VER_W - 1)} {"─".repeat(VER_W - 1)}
|
|
84
|
+
</text>
|
|
85
|
+
|
|
86
|
+
{/* Table rows */}
|
|
87
|
+
{mismatches.map((m) => {
|
|
88
|
+
const name = shortName(m.pluginId, NAME_W - 1);
|
|
89
|
+
const loaded = `v${shortVersion(m.firstEntryVersion, VER_W - 2)}`;
|
|
90
|
+
const expected = `v${shortVersion(m.currentProjectVersion, VER_W - 2)}`;
|
|
91
|
+
return (
|
|
92
|
+
<text key={m.pluginId} fg={COLOR_MUTED}>
|
|
93
|
+
<span fg={COLOR_ACCENT}>{name.padEnd(NAME_W)}</span>
|
|
94
|
+
<span fg={COLOR_BAD}>{loaded.padEnd(VER_W)}</span>
|
|
95
|
+
<span fg={COLOR_GOOD}>{expected.padEnd(VER_W)}</span>
|
|
96
|
+
</text>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
99
|
+
</box>
|
|
100
|
+
|
|
101
|
+
{/* Footer note */}
|
|
102
|
+
<box marginBottom={1}>
|
|
103
|
+
<text fg={COLOR_DIM}>
|
|
104
|
+
Details: github.com/anthropics/claude-code/issues/45997
|
|
105
|
+
</text>
|
|
106
|
+
</box>
|
|
107
|
+
|
|
108
|
+
{/* Options */}
|
|
109
|
+
<box flexDirection="column" paddingLeft={1}>
|
|
110
|
+
<text
|
|
111
|
+
fg={selectedIndex === 0 ? COLOR_ACCENT : COLOR_MUTED}
|
|
112
|
+
>
|
|
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
|
|
123
|
+
fg={selectedIndex === 1 ? COLOR_ACCENT : COLOR_MUTED}
|
|
124
|
+
>
|
|
125
|
+
<span fg={selectedIndex === 1 ? COLOR_ACCENT : COLOR_DIM}>
|
|
126
|
+
{selectedIndex === 1 ? "❯ " : " "}
|
|
127
|
+
</span>
|
|
128
|
+
{selectedIndex === 1 ? <strong>Dismiss</strong> : "Dismiss"}
|
|
129
|
+
</text>
|
|
130
|
+
</box>
|
|
131
|
+
|
|
132
|
+
{/* Keybind hints */}
|
|
133
|
+
<box marginTop={1}>
|
|
134
|
+
<text fg={COLOR_DIM}>↑↓ Select • ↵ Confirm • Esc Cancel</text>
|
|
135
|
+
</box>
|
|
136
|
+
</box>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default VersionMismatchModal;
|
package/src/ui/hooks/index.js
CHANGED
package/src/ui/hooks/index.ts
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useApp } from "../state/AppContext.js";
|
|
3
|
+
import { fixAllPluginVersionMismatches, } from "../../services/plugin-version-check.js";
|
|
4
|
+
/**
|
|
5
|
+
* Shared hook for showing the plugin version mismatch modal.
|
|
6
|
+
*
|
|
7
|
+
* Used in two places:
|
|
8
|
+
* 1. App.tsx — startup check shows mismatches found across all enabled plugins
|
|
9
|
+
* 2. PluginsScreen.tsx — post-install/update check shows a single plugin mismatch
|
|
10
|
+
*
|
|
11
|
+
* Both flows now share the same interactive modal with a "Fix all projects"
|
|
12
|
+
* button, so users never have to restart claudeup to access the fix.
|
|
13
|
+
*
|
|
14
|
+
* The fix uses fixAllPluginVersionMismatches() which rewrites
|
|
15
|
+
* installed_plugins.json in-place to align all entries to the current
|
|
16
|
+
* project's expected versions. After success, shows a hint to restart
|
|
17
|
+
* Claude Code so the new versions actually load (the running session
|
|
18
|
+
* has the old plugin code in memory).
|
|
19
|
+
*/
|
|
20
|
+
export function useMismatchModal() {
|
|
21
|
+
const { dispatch } = useApp();
|
|
22
|
+
const show = useCallback((mismatches) => {
|
|
23
|
+
if (mismatches.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
26
|
+
const fix = async () => {
|
|
27
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
28
|
+
dispatch({
|
|
29
|
+
type: "SHOW_PROGRESS",
|
|
30
|
+
state: { message: "Aligning plugin versions across projects..." },
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
const results = await fixAllPluginVersionMismatches(mismatches);
|
|
34
|
+
dispatch({ type: "HIDE_PROGRESS" });
|
|
35
|
+
// Count how many projects were updated across all plugins
|
|
36
|
+
const totalUpdated = Array.from(results.values()).reduce((sum, r) => sum + r.updated, 0);
|
|
37
|
+
dispatch({
|
|
38
|
+
type: "SHOW_MODAL",
|
|
39
|
+
modal: {
|
|
40
|
+
type: "message",
|
|
41
|
+
title: "Plugin Versions Aligned",
|
|
42
|
+
message: `Updated ${totalUpdated} project entr${totalUpdated === 1 ? "y" : "ies"} in ` +
|
|
43
|
+
`installed_plugins.json.\n\n` +
|
|
44
|
+
`Restart Claude Code for the new versions to load. ` +
|
|
45
|
+
`The running session still has the old plugin code in memory.`,
|
|
46
|
+
variant: "success",
|
|
47
|
+
onDismiss: dismiss,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
dispatch({ type: "HIDE_PROGRESS" });
|
|
53
|
+
dispatch({
|
|
54
|
+
type: "SHOW_MODAL",
|
|
55
|
+
modal: {
|
|
56
|
+
type: "message",
|
|
57
|
+
title: "Fix Failed",
|
|
58
|
+
message: `Could not align plugin versions: ${error instanceof Error ? error.message : String(error)}\n\n` +
|
|
59
|
+
`Try running claudeup again, or manually update the plugin in each project.`,
|
|
60
|
+
variant: "error",
|
|
61
|
+
onDismiss: dismiss,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
dispatch({
|
|
67
|
+
type: "SHOW_MODAL",
|
|
68
|
+
modal: {
|
|
69
|
+
type: "version-mismatch",
|
|
70
|
+
mismatches,
|
|
71
|
+
onFix: fix,
|
|
72
|
+
onDismiss: dismiss,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}, [dispatch]);
|
|
76
|
+
return { show };
|
|
77
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useApp } from "../state/AppContext.js";
|
|
3
|
+
import {
|
|
4
|
+
fixAllPluginVersionMismatches,
|
|
5
|
+
type VersionMismatchInfo,
|
|
6
|
+
} from "../../services/plugin-version-check.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:
|
|
56
|
+
`Updated ${totalUpdated} project entr${totalUpdated === 1 ? "y" : "ies"} in ` +
|
|
57
|
+
`installed_plugins.json.\n\n` +
|
|
58
|
+
`Restart Claude Code for the new versions to load. ` +
|
|
59
|
+
`The running session still has the old plugin code in memory.`,
|
|
60
|
+
variant: "success",
|
|
61
|
+
onDismiss: dismiss,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
dispatch({ type: "HIDE_PROGRESS" });
|
|
66
|
+
dispatch({
|
|
67
|
+
type: "SHOW_MODAL",
|
|
68
|
+
modal: {
|
|
69
|
+
type: "message",
|
|
70
|
+
title: "Fix Failed",
|
|
71
|
+
message:
|
|
72
|
+
`Could not align plugin versions: ${error instanceof Error ? error.message : String(error)}\n\n` +
|
|
73
|
+
`Try running claudeup again, or manually update the plugin in each project.`,
|
|
74
|
+
variant: "error",
|
|
75
|
+
onDismiss: dismiss,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
dispatch({
|
|
82
|
+
type: "SHOW_MODAL",
|
|
83
|
+
modal: {
|
|
84
|
+
type: "version-mismatch",
|
|
85
|
+
mismatches,
|
|
86
|
+
onFix: fix,
|
|
87
|
+
onDismiss: dismiss,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
[dispatch],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return { show };
|
|
95
|
+
}
|
|
@@ -14,7 +14,8 @@ import { saveProfile } from "../../services/profiles.js";
|
|
|
14
14
|
import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
|
|
15
15
|
import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
|
|
16
16
|
import { getPluginSetupFromSource, checkMissingDeps, installPluginDeps, } from "../../services/plugin-setup.js";
|
|
17
|
-
import { checkSinglePluginMismatch } from "../../services/plugin-version-check.js";
|
|
17
|
+
import { checkPluginVersionMismatches, checkSinglePluginMismatch, } from "../../services/plugin-version-check.js";
|
|
18
|
+
import { useMismatchModal } from "../hooks/useMismatchModal.js";
|
|
18
19
|
import { buildPluginBrowserItems, } from "../adapters/pluginsAdapter.js";
|
|
19
20
|
import { renderPluginRow, renderPluginDetail, } from "../renderers/pluginRenderers.js";
|
|
20
21
|
export function PluginsScreen() {
|
|
@@ -23,6 +24,7 @@ export function PluginsScreen() {
|
|
|
23
24
|
const modal = useModal();
|
|
24
25
|
const progress = useProgress();
|
|
25
26
|
const dimensions = useDimensions();
|
|
27
|
+
const mismatchModal = useMismatchModal();
|
|
26
28
|
const isSearchActive = state.isSearching &&
|
|
27
29
|
state.currentRoute.screen === "plugins" &&
|
|
28
30
|
!state.modal;
|
|
@@ -213,6 +215,8 @@ export function PluginsScreen() {
|
|
|
213
215
|
handleUpdate();
|
|
214
216
|
else if (event.name === "a")
|
|
215
217
|
handleUpdateAll();
|
|
218
|
+
else if (event.name === "m")
|
|
219
|
+
handleCheckMismatches();
|
|
216
220
|
else if (event.name === "s")
|
|
217
221
|
handleSaveAsProfile();
|
|
218
222
|
});
|
|
@@ -234,22 +238,19 @@ export function PluginsScreen() {
|
|
|
234
238
|
}
|
|
235
239
|
};
|
|
236
240
|
/**
|
|
237
|
-
* Check for version mismatch after a plugin update and
|
|
241
|
+
* Check for version mismatch after a plugin update and show the
|
|
242
|
+
* interactive mismatch modal with a "Fix all projects" button.
|
|
243
|
+
*
|
|
238
244
|
* The [0] index bug in Claude Code means the update may not actually
|
|
239
|
-
* take effect if another project's entry is at index 0.
|
|
245
|
+
* take effect if another project's entry is at index 0. The shared
|
|
246
|
+
* useMismatchModal hook provides the same fix flow available at startup.
|
|
240
247
|
*/
|
|
241
248
|
const warnIfVersionMismatch = async (pluginId) => {
|
|
242
249
|
try {
|
|
243
250
|
const projectPath = state.projectPath || process.cwd();
|
|
244
251
|
const mismatch = await checkSinglePluginMismatch(pluginId, projectPath);
|
|
245
252
|
if (mismatch) {
|
|
246
|
-
|
|
247
|
-
`but Claude Code will load v${mismatch.firstEntryVersion} due to a known bug.\n\n` +
|
|
248
|
-
`The plugin loader always uses the first entry in installed_plugins.json, ` +
|
|
249
|
-
`which belongs to another project.\n\n` +
|
|
250
|
-
`Bug: https://github.com/anthropics/claude-code/issues/45997\n\n` +
|
|
251
|
-
`To fix: open claudeup and use the version alignment tool, ` +
|
|
252
|
-
`or update the plugin in all projects to the same version.`, "error");
|
|
253
|
+
mismatchModal.show([mismatch]);
|
|
253
254
|
}
|
|
254
255
|
}
|
|
255
256
|
catch {
|
|
@@ -567,9 +568,21 @@ export function PluginsScreen() {
|
|
|
567
568
|
updatedPluginIds.push(plugin.id);
|
|
568
569
|
}
|
|
569
570
|
modal.hideModal();
|
|
570
|
-
//
|
|
571
|
+
// Batch mismatch checks into a single modal
|
|
572
|
+
const projectPath = state.projectPath || process.cwd();
|
|
573
|
+
const allMismatches = [];
|
|
571
574
|
for (const pluginId of updatedPluginIds) {
|
|
572
|
-
|
|
575
|
+
try {
|
|
576
|
+
const m = await checkSinglePluginMismatch(pluginId, projectPath);
|
|
577
|
+
if (m)
|
|
578
|
+
allMismatches.push(m);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// best-effort
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (allMismatches.length > 0) {
|
|
585
|
+
mismatchModal.show(allMismatches);
|
|
573
586
|
}
|
|
574
587
|
fetchData();
|
|
575
588
|
}
|
|
@@ -578,6 +591,25 @@ export function PluginsScreen() {
|
|
|
578
591
|
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
579
592
|
}
|
|
580
593
|
};
|
|
594
|
+
/**
|
|
595
|
+
* Manual on-demand check for plugin version mismatches across projects.
|
|
596
|
+
* Bound to the `m` keybind. Shows a success message if everything is
|
|
597
|
+
* aligned, or the interactive fix modal if any mismatches are found.
|
|
598
|
+
*/
|
|
599
|
+
const handleCheckMismatches = async () => {
|
|
600
|
+
try {
|
|
601
|
+
const projectPath = state.projectPath || process.cwd();
|
|
602
|
+
const mismatches = await checkPluginVersionMismatches(projectPath);
|
|
603
|
+
if (mismatches.length === 0) {
|
|
604
|
+
await modal.message("No Mismatches", "All enabled plugins in this project will load their expected versions. No action needed.", "success");
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
mismatchModal.show(mismatches);
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
await modal.message("Error", `Failed to check mismatches: ${error}`, "error");
|
|
611
|
+
}
|
|
612
|
+
};
|
|
581
613
|
const handleScopeToggle = async (scope) => {
|
|
582
614
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
583
615
|
if (!item || item.kind !== "plugin")
|
|
@@ -704,7 +736,7 @@ export function PluginsScreen() {
|
|
|
704
736
|
const selectedItem = selectableItems[pluginsState.selectedIndex];
|
|
705
737
|
const footerHints = isSearchActive
|
|
706
738
|
? "type to filter │ Enter:done │ Esc:clear"
|
|
707
|
-
: "u/p/l:toggle │ U:update │ a:all │ s:profile │ /:search";
|
|
739
|
+
: "u/p/l:toggle │ U:update │ a:all │ m:mismatches │ s:profile │ /:search";
|
|
708
740
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
709
741
|
const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
|
|
710
742
|
const installedCount = plugins.filter((p) => p.enabled).length;
|
|
@@ -38,7 +38,12 @@ import {
|
|
|
38
38
|
checkMissingDeps,
|
|
39
39
|
installPluginDeps,
|
|
40
40
|
} from "../../services/plugin-setup.js";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
checkPluginVersionMismatches,
|
|
43
|
+
checkSinglePluginMismatch,
|
|
44
|
+
type VersionMismatchInfo,
|
|
45
|
+
} from "../../services/plugin-version-check.js";
|
|
46
|
+
import { useMismatchModal } from "../hooks/useMismatchModal.js";
|
|
42
47
|
import {
|
|
43
48
|
buildPluginBrowserItems,
|
|
44
49
|
type PluginBrowserItem,
|
|
@@ -54,6 +59,7 @@ export function PluginsScreen() {
|
|
|
54
59
|
const modal = useModal();
|
|
55
60
|
const progress = useProgress();
|
|
56
61
|
const dimensions = useDimensions();
|
|
62
|
+
const mismatchModal = useMismatchModal();
|
|
57
63
|
|
|
58
64
|
const isSearchActive =
|
|
59
65
|
state.isSearching &&
|
|
@@ -261,6 +267,7 @@ export function PluginsScreen() {
|
|
|
261
267
|
else if (event.name === "d") handleRemoveDeprecated();
|
|
262
268
|
else if (event.name === "U") handleUpdate();
|
|
263
269
|
else if (event.name === "a") handleUpdateAll();
|
|
270
|
+
else if (event.name === "m") handleCheckMismatches();
|
|
264
271
|
else if (event.name === "s") handleSaveAsProfile();
|
|
265
272
|
});
|
|
266
273
|
|
|
@@ -290,26 +297,19 @@ export function PluginsScreen() {
|
|
|
290
297
|
};
|
|
291
298
|
|
|
292
299
|
/**
|
|
293
|
-
* Check for version mismatch after a plugin update and
|
|
300
|
+
* Check for version mismatch after a plugin update and show the
|
|
301
|
+
* interactive mismatch modal with a "Fix all projects" button.
|
|
302
|
+
*
|
|
294
303
|
* The [0] index bug in Claude Code means the update may not actually
|
|
295
|
-
* take effect if another project's entry is at index 0.
|
|
304
|
+
* take effect if another project's entry is at index 0. The shared
|
|
305
|
+
* useMismatchModal hook provides the same fix flow available at startup.
|
|
296
306
|
*/
|
|
297
307
|
const warnIfVersionMismatch = async (pluginId: string): Promise<void> => {
|
|
298
308
|
try {
|
|
299
309
|
const projectPath = state.projectPath || process.cwd();
|
|
300
310
|
const mismatch = await checkSinglePluginMismatch(pluginId, projectPath);
|
|
301
311
|
if (mismatch) {
|
|
302
|
-
|
|
303
|
-
"Version Mismatch Warning",
|
|
304
|
-
`${pluginId} was updated to v${mismatch.currentProjectVersion} for this project, ` +
|
|
305
|
-
`but Claude Code will load v${mismatch.firstEntryVersion} due to a known bug.\n\n` +
|
|
306
|
-
`The plugin loader always uses the first entry in installed_plugins.json, ` +
|
|
307
|
-
`which belongs to another project.\n\n` +
|
|
308
|
-
`Bug: https://github.com/anthropics/claude-code/issues/45997\n\n` +
|
|
309
|
-
`To fix: open claudeup and use the version alignment tool, ` +
|
|
310
|
-
`or update the plugin in all projects to the same version.`,
|
|
311
|
-
"error",
|
|
312
|
-
);
|
|
312
|
+
mismatchModal.show([mismatch]);
|
|
313
313
|
}
|
|
314
314
|
} catch {
|
|
315
315
|
// Non-fatal: mismatch check is best-effort
|
|
@@ -722,9 +722,19 @@ export function PluginsScreen() {
|
|
|
722
722
|
updatedPluginIds.push(plugin.id);
|
|
723
723
|
}
|
|
724
724
|
modal.hideModal();
|
|
725
|
-
//
|
|
725
|
+
// Batch mismatch checks into a single modal
|
|
726
|
+
const projectPath = state.projectPath || process.cwd();
|
|
727
|
+
const allMismatches: VersionMismatchInfo[] = [];
|
|
726
728
|
for (const pluginId of updatedPluginIds) {
|
|
727
|
-
|
|
729
|
+
try {
|
|
730
|
+
const m = await checkSinglePluginMismatch(pluginId, projectPath);
|
|
731
|
+
if (m) allMismatches.push(m);
|
|
732
|
+
} catch {
|
|
733
|
+
// best-effort
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (allMismatches.length > 0) {
|
|
737
|
+
mismatchModal.show(allMismatches);
|
|
728
738
|
}
|
|
729
739
|
fetchData();
|
|
730
740
|
} catch (error) {
|
|
@@ -733,6 +743,33 @@ export function PluginsScreen() {
|
|
|
733
743
|
}
|
|
734
744
|
};
|
|
735
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Manual on-demand check for plugin version mismatches across projects.
|
|
748
|
+
* Bound to the `m` keybind. Shows a success message if everything is
|
|
749
|
+
* aligned, or the interactive fix modal if any mismatches are found.
|
|
750
|
+
*/
|
|
751
|
+
const handleCheckMismatches = async () => {
|
|
752
|
+
try {
|
|
753
|
+
const projectPath = state.projectPath || process.cwd();
|
|
754
|
+
const mismatches = await checkPluginVersionMismatches(projectPath);
|
|
755
|
+
if (mismatches.length === 0) {
|
|
756
|
+
await modal.message(
|
|
757
|
+
"No Mismatches",
|
|
758
|
+
"All enabled plugins in this project will load their expected versions. No action needed.",
|
|
759
|
+
"success",
|
|
760
|
+
);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
mismatchModal.show(mismatches);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
await modal.message(
|
|
766
|
+
"Error",
|
|
767
|
+
`Failed to check mismatches: ${error}`,
|
|
768
|
+
"error",
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
|
|
736
773
|
const handleScopeToggle = async (scope: "user" | "project" | "local") => {
|
|
737
774
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
738
775
|
if (!item || item.kind !== "plugin") return;
|
|
@@ -901,7 +938,7 @@ export function PluginsScreen() {
|
|
|
901
938
|
|
|
902
939
|
const footerHints = isSearchActive
|
|
903
940
|
? "type to filter │ Enter:done │ Esc:clear"
|
|
904
|
-
: "u/p/l:toggle │ U:update │ a:all │ s:profile │ /:search";
|
|
941
|
+
: "u/p/l:toggle │ U:update │ a:all │ m:mismatches │ s:profile │ /:search";
|
|
905
942
|
|
|
906
943
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
907
944
|
const plugins: PluginInfo[] =
|
package/src/ui/state/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
SkillInfo,
|
|
7
7
|
} from "../../types/index.js";
|
|
8
8
|
import type { PluginInfo } from "../../services/plugin-manager.js";
|
|
9
|
+
import type { VersionMismatchInfo } from "../../services/plugin-version-check.js";
|
|
9
10
|
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// Route Types
|
|
@@ -86,6 +87,12 @@ export type ModalState =
|
|
|
86
87
|
| {
|
|
87
88
|
type: "loading";
|
|
88
89
|
message: string;
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
type: "version-mismatch";
|
|
93
|
+
mismatches: VersionMismatchInfo[];
|
|
94
|
+
onFix: () => void;
|
|
95
|
+
onDismiss: () => void;
|
|
89
96
|
};
|
|
90
97
|
|
|
91
98
|
// ============================================================================
|