claudeup 4.11.0 → 4.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "4.11.0",
3
+ "version": "4.11.1",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -115,7 +115,7 @@ const {
115
115
  fixPluginVersionMismatch,
116
116
  fixAllPluginVersionMismatches,
117
117
  formatMismatchWarning,
118
- formatMismatchBanner,
118
+ formatMismatchModal,
119
119
  } = await import("../services/plugin-version-check.js");
120
120
 
121
121
  // ── Test suite ───────────────────────────────────────────────────────────────
@@ -739,8 +739,8 @@ describe("formatMismatchWarning", () => {
739
739
  });
740
740
  });
741
741
 
742
- describe("formatMismatchBanner", () => {
743
- it("formats concise banner text", () => {
742
+ describe("formatMismatchModal", () => {
743
+ it("formats modal text with plugin details and bug link", () => {
744
744
  const mismatches = [
745
745
  {
746
746
  pluginId: "terminal@magus",
@@ -751,10 +751,12 @@ describe("formatMismatchBanner", () => {
751
751
  },
752
752
  ];
753
753
 
754
- const banner = formatMismatchBanner(mismatches);
754
+ const modal = formatMismatchModal(mismatches);
755
755
 
756
- expect(banner).toContain("terminal@magus");
757
- expect(banner).toContain("v2.0.0");
758
- expect(banner).toContain("v4.0.2");
756
+ expect(modal).toContain("terminal");
757
+ expect(modal).toContain("v2.0.0");
758
+ expect(modal).toContain("v4.0.2");
759
+ expect(modal).toContain("45997");
760
+ expect(modal).toContain("wrong plugin version");
759
761
  });
760
762
  });
@@ -184,11 +184,20 @@ export function formatMismatchWarning(mismatches) {
184
184
  return lines.join("\n");
185
185
  }
186
186
  /**
187
- * Format mismatch info for TUI display.
187
+ * Format mismatch info for TUI modal display.
188
+ * Returns a multi-line string suitable for a modal message.
188
189
  */
189
- export function formatMismatchBanner(mismatches) {
190
- const parts = mismatches.map((m) => `${m.pluginId}: loading v${m.firstEntryVersion} instead of v${m.currentProjectVersion}`);
191
- return `Version mismatch: ${parts.join(", ")}`;
190
+ export function formatMismatchModal(mismatches) {
191
+ const lines = [];
192
+ lines.push("Claude Code has a bug where it loads the wrong plugin version.");
193
+ lines.push("It always uses the first entry in the registry, ignoring which");
194
+ lines.push("project you're in. These plugins are affected:\n");
195
+ for (const m of mismatches) {
196
+ const name = m.pluginId.split("@")[0];
197
+ lines.push(` ${name}: will load v${m.firstEntryVersion} instead of v${m.currentProjectVersion}`);
198
+ }
199
+ lines.push(`\nBug: github.com/anthropics/claude-code/issues/45997`);
200
+ return lines.join("\n");
192
201
  }
193
202
  // ── Helpers ──────────────────────────────────────────────────────────────────
194
203
  /**
@@ -268,16 +268,31 @@ export function formatMismatchWarning(
268
268
  }
269
269
 
270
270
  /**
271
- * Format mismatch info for TUI display.
271
+ * Format mismatch info for TUI modal display.
272
+ * Returns a multi-line string suitable for a modal message.
272
273
  */
273
- export function formatMismatchBanner(
274
+ export function formatMismatchModal(
274
275
  mismatches: VersionMismatchInfo[],
275
276
  ): string {
276
- const parts = mismatches.map(
277
- (m) =>
278
- `${m.pluginId}: loading v${m.firstEntryVersion} instead of v${m.currentProjectVersion}`,
277
+ const lines: string[] = [];
278
+ lines.push(
279
+ "Claude Code has a bug where it loads the wrong plugin version.",
280
+ );
281
+ lines.push(
282
+ "It always uses the first entry in the registry, ignoring which",
279
283
  );
280
- return `Version mismatch: ${parts.join(", ")}`;
284
+ lines.push("project you're in. These plugins are affected:\n");
285
+
286
+ for (const m of mismatches) {
287
+ const name = m.pluginId.split("@")[0];
288
+ lines.push(
289
+ ` ${name}: will load v${m.firstEntryVersion} instead of v${m.currentProjectVersion}`,
290
+ );
291
+ }
292
+
293
+ lines.push(`\nBug: github.com/anthropics/claude-code/issues/45997`);
294
+
295
+ return lines.join("\n");
281
296
  }
282
297
 
283
298
  // ── Helpers ──────────────────────────────────────────────────────────────────
package/src/ui/App.js CHANGED
@@ -8,7 +8,7 @@ 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, formatMismatchBanner, } from "../services/plugin-version-check.js";
11
+ import { checkPluginVersionMismatches, fixAllPluginVersionMismatches, formatMismatchModal, } from "../services/plugin-version-check.js";
12
12
  import { checkForUpdates, getCurrentVersion, } from "../services/version-check.js";
13
13
  import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
14
14
  import { ProgressBar } from "./components/layout/ProgressBar.js";
@@ -176,11 +176,11 @@ function UpdateBanner({ result }) {
176
176
  function ProgressIndicator({ message, current, total, }) {
177
177
  return (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsx(ProgressBar, { message: message, current: current, total: total }) }));
178
178
  }
179
- function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, recoveryReport, mismatchWarning, }) {
179
+ function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, recoveryReport, }) {
180
180
  const { state } = useApp();
181
181
  const { progress } = state;
182
182
  const dimensions = useDimensions();
183
- return (_jsxs("box", { flexDirection: "column", height: dimensions.terminalHeight, children: [updateInfo?.updateAvailable && _jsx(UpdateBanner, { result: updateInfo }), recoveryReport && (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsxs("text", { fg: "green", children: ["\u2713 Fixed: ", recoveryReport] }) })), mismatchWarning && (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsxs("text", { fg: "yellow", children: ["\u26A0 ", mismatchWarning, " (bug #45997)"] }) })), showDebug && (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsxs("text", { fg: "#888888", children: ["DEBUG: ", dimensions.terminalWidth, "x", dimensions.terminalHeight, " | content=", dimensions.contentHeight, " | screen=", state.currentRoute.screen] }) })), progress && _jsx(ProgressIndicator, { ...progress }), _jsx("box", { flexDirection: "column", height: dimensions.contentHeight, paddingLeft: 1, paddingRight: 1, children: _jsx(Router, {}) }), _jsx(GlobalKeyHandler, { onDebugToggle: onDebugToggle, onExit: onExit }), _jsx(ModalContainer, {})] }));
183
+ return (_jsxs("box", { flexDirection: "column", height: dimensions.terminalHeight, children: [updateInfo?.updateAvailable && _jsx(UpdateBanner, { result: updateInfo }), recoveryReport && (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsxs("text", { fg: "green", children: ["\u2713 Fixed: ", recoveryReport] }) })), showDebug && (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsxs("text", { fg: "#888888", children: ["DEBUG: ", dimensions.terminalWidth, "x", dimensions.terminalHeight, " | content=", dimensions.contentHeight, " | screen=", state.currentRoute.screen] }) })), progress && _jsx(ProgressIndicator, { ...progress }), _jsx("box", { flexDirection: "column", height: dimensions.contentHeight, paddingLeft: 1, paddingRight: 1, children: _jsx(Router, {}) }), _jsx(GlobalKeyHandler, { onDebugToggle: onDebugToggle, onExit: onExit }), _jsx(ModalContainer, {})] }));
184
184
  }
185
185
  function AppContent({ onExit }) {
186
186
  const { state, dispatch } = useApp();
@@ -188,7 +188,7 @@ function AppContent({ onExit }) {
188
188
  const [showDebug, setShowDebug] = useState(false);
189
189
  const [updateInfo, setUpdateInfo] = useState(null);
190
190
  const [recoveryReport, setRecoveryReport] = useState(null);
191
- const [mismatchWarning, setMismatchWarning] = useState(null);
191
+ const [mismatchData, setMismatchData] = useState(null);
192
192
  // Check for updates on startup (non-blocking)
193
193
  useEffect(() => {
194
194
  checkForUpdates()
@@ -202,13 +202,72 @@ function AppContent({ onExit }) {
202
202
  const timer = setTimeout(() => setRecoveryReport(null), 5000);
203
203
  return () => clearTimeout(timer);
204
204
  }, [recoveryReport]);
205
- // Auto-dismiss mismatch warning after 8 seconds (longer — more important)
205
+ // Show mismatch modal when data arrives
206
206
  useEffect(() => {
207
- if (!mismatchWarning)
207
+ if (!mismatchData || mismatchData.length === 0)
208
208
  return;
209
- const timer = setTimeout(() => setMismatchWarning(null), 8000);
210
- return () => clearTimeout(timer);
211
- }, [mismatchWarning]);
209
+ dispatch({
210
+ type: "SHOW_MODAL",
211
+ modal: {
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]);
212
271
  // Auto-refresh marketplaces on startup
213
272
  useEffect(() => {
214
273
  const noRefresh = process.argv.includes("--no-refresh");
@@ -252,7 +311,7 @@ function AppContent({ onExit }) {
252
311
  checkPluginVersionMismatches(process.cwd())
253
312
  .then((mismatches) => {
254
313
  if (mismatches.length > 0) {
255
- setMismatchWarning(formatMismatchBanner(mismatches));
314
+ setMismatchData(mismatches);
256
315
  }
257
316
  })
258
317
  .catch(() => { }); // non-fatal
@@ -266,8 +325,8 @@ function AppContent({ onExit }) {
266
325
  });
267
326
  }, [dispatch]);
268
327
  // Count transient banners for dimension calculation
269
- const transientBannerCount = (recoveryReport ? 1 : 0) + (mismatchWarning ? 1 : 0);
270
- return (_jsx(DimensionsProvider, { showProgress: !!progress, showDebug: showDebug, showUpdateBanner: !!updateInfo?.updateAvailable, transientBannerCount: transientBannerCount, children: _jsx(AppContentInner, { showDebug: showDebug, onDebugToggle: () => setShowDebug((s) => !s), updateInfo: updateInfo, onExit: onExit, recoveryReport: recoveryReport, mismatchWarning: mismatchWarning }) }));
328
+ const transientBannerCount = recoveryReport ? 1 : 0;
329
+ return (_jsx(DimensionsProvider, { showProgress: !!progress, showDebug: showDebug, showUpdateBanner: !!updateInfo?.updateAvailable, transientBannerCount: transientBannerCount, children: _jsx(AppContentInner, { showDebug: showDebug, onDebugToggle: () => setShowDebug((s) => !s), updateInfo: updateInfo, onExit: onExit, recoveryReport: recoveryReport }) }));
271
330
  }
272
331
  export function App({ onExit }) {
273
332
  return (_jsx(AppProvider, { children: _jsx(AppContent, { onExit: onExit }) }));
package/src/ui/App.tsx CHANGED
@@ -30,7 +30,9 @@ import {
30
30
  } from "../services/claude-settings.js";
31
31
  import {
32
32
  checkPluginVersionMismatches,
33
- formatMismatchBanner,
33
+ fixAllPluginVersionMismatches,
34
+ formatMismatchModal,
35
+ type VersionMismatchInfo,
34
36
  } from "../services/plugin-version-check.js";
35
37
  import {
36
38
  checkForUpdates,
@@ -255,7 +257,6 @@ interface AppContentInnerProps {
255
257
  updateInfo: VersionCheckResult | null;
256
258
  onExit: () => void;
257
259
  recoveryReport: string | null;
258
- mismatchWarning: string | null;
259
260
  }
260
261
 
261
262
  function AppContentInner({
@@ -264,7 +265,6 @@ function AppContentInner({
264
265
  updateInfo,
265
266
  onExit,
266
267
  recoveryReport,
267
- mismatchWarning,
268
268
  }: AppContentInnerProps) {
269
269
  const { state } = useApp();
270
270
  const { progress } = state;
@@ -278,11 +278,6 @@ function AppContentInner({
278
278
  <text fg="green">✓ Fixed: {recoveryReport}</text>
279
279
  </box>
280
280
  )}
281
- {mismatchWarning && (
282
- <box paddingLeft={1} paddingRight={1}>
283
- <text fg="yellow">⚠ {mismatchWarning} (bug #45997)</text>
284
- </box>
285
- )}
286
281
  {showDebug && (
287
282
  <box paddingLeft={1} paddingRight={1}>
288
283
  <text fg="#888888">
@@ -321,7 +316,7 @@ function AppContent({ onExit }: AppContentProps) {
321
316
  const [showDebug, setShowDebug] = useState(false);
322
317
  const [updateInfo, setUpdateInfo] = useState<VersionCheckResult | null>(null);
323
318
  const [recoveryReport, setRecoveryReport] = useState<string | null>(null);
324
- const [mismatchWarning, setMismatchWarning] = useState<string | null>(null);
319
+ const [mismatchData, setMismatchData] = useState<VersionMismatchInfo[] | null>(null);
325
320
 
326
321
  // Check for updates on startup (non-blocking)
327
322
  useEffect(() => {
@@ -337,12 +332,71 @@ function AppContent({ onExit }: AppContentProps) {
337
332
  return () => clearTimeout(timer);
338
333
  }, [recoveryReport]);
339
334
 
340
- // Auto-dismiss mismatch warning after 8 seconds (longer — more important)
335
+ // Show mismatch modal when data arrives
341
336
  useEffect(() => {
342
- if (!mismatchWarning) return;
343
- const timer = setTimeout(() => setMismatchWarning(null), 8000);
344
- return () => clearTimeout(timer);
345
- }, [mismatchWarning]);
337
+ if (!mismatchData || mismatchData.length === 0) return;
338
+ dispatch({
339
+ type: "SHOW_MODAL",
340
+ modal: {
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]);
346
400
 
347
401
  // Auto-refresh marketplaces on startup
348
402
  useEffect(() => {
@@ -391,7 +445,7 @@ function AppContent({ onExit }: AppContentProps) {
391
445
  checkPluginVersionMismatches(process.cwd())
392
446
  .then((mismatches) => {
393
447
  if (mismatches.length > 0) {
394
- setMismatchWarning(formatMismatchBanner(mismatches));
448
+ setMismatchData(mismatches);
395
449
  }
396
450
  })
397
451
  .catch(() => {}); // non-fatal
@@ -408,7 +462,7 @@ function AppContent({ onExit }: AppContentProps) {
408
462
 
409
463
  // Count transient banners for dimension calculation
410
464
  const transientBannerCount =
411
- (recoveryReport ? 1 : 0) + (mismatchWarning ? 1 : 0);
465
+ recoveryReport ? 1 : 0;
412
466
 
413
467
  return (
414
468
  <DimensionsProvider
@@ -423,8 +477,7 @@ function AppContent({ onExit }: AppContentProps) {
423
477
  updateInfo={updateInfo}
424
478
  onExit={onExit}
425
479
  recoveryReport={recoveryReport}
426
- mismatchWarning={mismatchWarning}
427
- />
480
+ />
428
481
  </DimensionsProvider>
429
482
  );
430
483
  }