claudeup 4.10.1 → 4.11.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.
@@ -38,11 +38,15 @@ import {
38
38
  checkMissingDeps,
39
39
  installPluginDeps,
40
40
  } from "../../services/plugin-setup.js";
41
+ import { checkSinglePluginMismatch } from "../../services/plugin-version-check.js";
41
42
  import {
42
43
  buildPluginBrowserItems,
43
44
  type PluginBrowserItem,
44
45
  } from "../adapters/pluginsAdapter.js";
45
- import { renderPluginRow, renderPluginDetail } from "../renderers/pluginRenderers.js";
46
+ import {
47
+ renderPluginRow,
48
+ renderPluginDetail,
49
+ } from "../renderers/pluginRenderers.js";
46
50
 
47
51
  export function PluginsScreen() {
48
52
  const { state, dispatch } = useApp();
@@ -180,15 +184,23 @@ export function PluginsScreen() {
180
184
  }
181
185
 
182
186
  if (event.name === "up" || event.name === "k") {
183
- if (isSearchActive) dispatch({ type: "SET_SEARCHING", isSearching: false });
184
- dispatch({ type: "PLUGINS_SELECT", index: Math.max(0, pluginsState.selectedIndex - 1) });
187
+ if (isSearchActive)
188
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
189
+ dispatch({
190
+ type: "PLUGINS_SELECT",
191
+ index: Math.max(0, pluginsState.selectedIndex - 1),
192
+ });
185
193
  return;
186
194
  }
187
195
  if (event.name === "down" || event.name === "j") {
188
- if (isSearchActive) dispatch({ type: "SET_SEARCHING", isSearching: false });
196
+ if (isSearchActive)
197
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
189
198
  dispatch({
190
199
  type: "PLUGINS_SELECT",
191
- index: Math.min(selectableItems.length - 1, pluginsState.selectedIndex + 1),
200
+ index: Math.min(
201
+ selectableItems.length - 1,
202
+ pluginsState.selectedIndex + 1,
203
+ ),
192
204
  });
193
205
  return;
194
206
  }
@@ -211,13 +223,21 @@ export function PluginsScreen() {
211
223
  ) {
212
224
  const item = selectableItems[pluginsState.selectedIndex];
213
225
  if (item?.kind === "category") {
214
- dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: item.marketplace.name });
226
+ dispatch({
227
+ type: "PLUGINS_TOGGLE_MARKETPLACE",
228
+ name: item.marketplace.name,
229
+ });
215
230
  }
216
231
  return;
217
232
  }
218
233
 
219
234
  if (isSearchActive) {
220
- if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
235
+ if (
236
+ event.name.length === 1 &&
237
+ !event.ctrl &&
238
+ !event.meta &&
239
+ !/[0-9]/.test(event.name)
240
+ ) {
221
241
  dispatch({
222
242
  type: "PLUGINS_SET_SEARCH",
223
243
  query: pluginsState.searchQuery + event.name,
@@ -259,12 +279,43 @@ export function PluginsScreen() {
259
279
  if (scope === "user") {
260
280
  await saveGlobalInstalledPluginVersion(pluginId, version);
261
281
  } else if (scope === "local") {
262
- await saveLocalInstalledPluginVersion(pluginId, version, state.projectPath);
282
+ await saveLocalInstalledPluginVersion(
283
+ pluginId,
284
+ version,
285
+ state.projectPath,
286
+ );
263
287
  } else {
264
288
  await saveInstalledPluginVersion(pluginId, version, state.projectPath);
265
289
  }
266
290
  };
267
291
 
292
+ /**
293
+ * Check for version mismatch after a plugin update and warn the user.
294
+ * 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.
296
+ */
297
+ const warnIfVersionMismatch = async (pluginId: string): Promise<void> => {
298
+ try {
299
+ const projectPath = state.projectPath || process.cwd();
300
+ const mismatch = await checkSinglePluginMismatch(pluginId, projectPath);
301
+ if (mismatch) {
302
+ await modal.message(
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
+ );
313
+ }
314
+ } catch {
315
+ // Non-fatal: mismatch check is best-effort
316
+ }
317
+ };
318
+
268
319
  // ── Action handlers ────────────────────────────────────────────────────────
269
320
 
270
321
  const handleRefresh = async () => {
@@ -351,7 +402,10 @@ export function PluginsScreen() {
351
402
  const pluginSource = await getPluginSourcePath(marketplace, pluginName);
352
403
  if (!pluginSource) return true;
353
404
 
354
- const requirements = await getPluginEnvRequirements(marketplace, pluginSource);
405
+ const requirements = await getPluginEnvRequirements(
406
+ marketplace,
407
+ pluginSource,
408
+ );
355
409
  if (requirements.length === 0) return true;
356
410
 
357
411
  const existingEnvVars = await getMcpEnvVars(state.projectPath);
@@ -441,7 +495,8 @@ export function PluginsScreen() {
441
495
  if (missing.pip?.length) parts.push(`pip: ${missing.pip.join(", ")}`);
442
496
  if (missing.brew?.length) parts.push(`brew: ${missing.brew.join(", ")}`);
443
497
  if (missing.npm?.length) parts.push(`npm: ${missing.npm.join(", ")}`);
444
- if (missing.cargo?.length) parts.push(`cargo: ${missing.cargo.join(", ")}`);
498
+ if (missing.cargo?.length)
499
+ parts.push(`cargo: ${missing.cargo.join(", ")}`);
445
500
 
446
501
  const wantInstall = await modal.confirm(
447
502
  "Install Dependencies?",
@@ -455,7 +510,9 @@ export function PluginsScreen() {
455
510
  modal.hideModal();
456
511
 
457
512
  if (result.failed.length > 0) {
458
- const failMsg = result.failed.map((f) => `${f.pkg}: ${f.error}`).join("\n");
513
+ const failMsg = result.failed
514
+ .map((f) => `${f.pkg}: ${f.error}`)
515
+ .join("\n");
459
516
  await modal.message(
460
517
  "Partial Install",
461
518
  `Installed: ${result.installed.length}\nFailed:\n${failMsg}`,
@@ -516,7 +573,11 @@ export function PluginsScreen() {
516
573
  ) => {
517
574
  const installed = scope?.enabled;
518
575
  const ver = scope?.version;
519
- const hasUpdate = ver && latestVersion && ver !== latestVersion && latestVersion !== "0.0.0";
576
+ const hasUpdate =
577
+ ver &&
578
+ latestVersion &&
579
+ ver !== latestVersion &&
580
+ latestVersion !== "0.0.0";
520
581
  let label = installed ? `● ${name}` : `○ ${name}`;
521
582
  label += ` (${desc})`;
522
583
  if (ver) label += ` v${ver}`;
@@ -525,12 +586,25 @@ export function PluginsScreen() {
525
586
  };
526
587
 
527
588
  const scopeOptions = [
528
- { label: buildScopeLabel("User", plugin.userScope, "global"), value: "user" },
529
- { label: buildScopeLabel("Project", plugin.projectScope, "team"), value: "project" },
530
- { label: buildScopeLabel("Local", plugin.localScope, "private"), value: "local" },
589
+ {
590
+ label: buildScopeLabel("User", plugin.userScope, "global"),
591
+ value: "user",
592
+ },
593
+ {
594
+ label: buildScopeLabel("Project", plugin.projectScope, "team"),
595
+ value: "project",
596
+ },
597
+ {
598
+ label: buildScopeLabel("Local", plugin.localScope, "private"),
599
+ value: "local",
600
+ },
531
601
  ];
532
602
 
533
- const scopeValue = await modal.select(plugin.name, `Select scope to toggle:`, scopeOptions);
603
+ const scopeValue = await modal.select(
604
+ plugin.name,
605
+ `Select scope to toggle:`,
606
+ scopeOptions,
607
+ );
534
608
  if (scopeValue === null) return;
535
609
 
536
610
  const selectedScope =
@@ -542,7 +616,11 @@ export function PluginsScreen() {
542
616
  const isInstalledInScope = selectedScope?.enabled;
543
617
  const installedVersion = selectedScope?.version;
544
618
  const scopeLabel =
545
- scopeValue === "user" ? "User" : scopeValue === "project" ? "Project" : "Local";
619
+ scopeValue === "user"
620
+ ? "User"
621
+ : scopeValue === "project"
622
+ ? "Project"
623
+ : "Local";
546
624
 
547
625
  const hasUpdateInScope =
548
626
  isInstalledInScope &&
@@ -585,6 +663,9 @@ export function PluginsScreen() {
585
663
  if (action !== "install") {
586
664
  modal.hideModal();
587
665
  }
666
+ if (action === "update") {
667
+ await warnIfVersionMismatch(plugin.id);
668
+ }
588
669
  fetchData();
589
670
  } catch (error) {
590
671
  modal.hideModal();
@@ -598,7 +679,8 @@ export function PluginsScreen() {
598
679
  if (!item || item.kind !== "plugin" || !item.plugin.hasUpdate) return;
599
680
 
600
681
  const plugin = item.plugin;
601
- const scope: PluginScope = pluginsState.scope === "global" ? "user" : "project";
682
+ const scope: PluginScope =
683
+ pluginsState.scope === "global" ? "user" : "project";
602
684
 
603
685
  modal.loading(
604
686
  `Updating ${plugin.name}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
@@ -609,6 +691,7 @@ export function PluginsScreen() {
609
691
  await saveVersionForScope(plugin.id, plugin.version, scope);
610
692
  }
611
693
  modal.hideModal();
694
+ await warnIfVersionMismatch(plugin.id);
612
695
  fetchData();
613
696
  } catch (error) {
614
697
  modal.hideModal();
@@ -622,7 +705,9 @@ export function PluginsScreen() {
622
705
  const updatable = pluginsState.plugins.data.filter((p) => p.hasUpdate);
623
706
  if (updatable.length === 0) return;
624
707
 
625
- const scope: PluginScope = pluginsState.scope === "global" ? "user" : "project";
708
+ const scope: PluginScope =
709
+ pluginsState.scope === "global" ? "user" : "project";
710
+ const updatedPluginIds: string[] = [];
626
711
 
627
712
  try {
628
713
  for (let i = 0; i < updatable.length; i++) {
@@ -634,8 +719,13 @@ export function PluginsScreen() {
634
719
  if (plugin.version) {
635
720
  await saveVersionForScope(plugin.id, plugin.version, scope);
636
721
  }
722
+ updatedPluginIds.push(plugin.id);
637
723
  }
638
724
  modal.hideModal();
725
+ // Check for mismatches on all updated plugins
726
+ for (const pluginId of updatedPluginIds) {
727
+ await warnIfVersionMismatch(pluginId);
728
+ }
639
729
  fetchData();
640
730
  } catch (error) {
641
731
  modal.hideModal();
@@ -701,6 +791,9 @@ export function PluginsScreen() {
701
791
  if (action !== "install") {
702
792
  modal.hideModal();
703
793
  }
794
+ if (action === "update") {
795
+ await warnIfVersionMismatch(plugin.id);
796
+ }
704
797
  fetchData();
705
798
  } catch (error) {
706
799
  modal.hideModal();
@@ -745,8 +838,14 @@ export function PluginsScreen() {
745
838
  "Save to scope",
746
839
  "Where should this profile be saved?",
747
840
  [
748
- { label: "User — ~/.claude/profiles.json (available everywhere)", value: "user" },
749
- { label: "Project.claude/profiles.json (shared with team via git)", value: "project" },
841
+ {
842
+ label: "User~/.claude/profiles.json (available everywhere)",
843
+ value: "user",
844
+ },
845
+ {
846
+ label: "Project — .claude/profiles.json (shared with team via git)",
847
+ value: "project",
848
+ },
750
849
  ],
751
850
  );
752
851
  if (scopeChoice === null) return;
@@ -808,7 +907,13 @@ export function PluginsScreen() {
808
907
  const plugins: PluginInfo[] =
809
908
  pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
810
909
  const installedCount = plugins.filter((p) => p.enabled).length;
811
- const updateCount = plugins.filter((p) => p.hasUpdate && (p.userScope?.enabled || p.projectScope?.enabled || p.localScope?.enabled)).length;
910
+ const updateCount = plugins.filter(
911
+ (p) =>
912
+ p.hasUpdate &&
913
+ (p.userScope?.enabled ||
914
+ p.projectScope?.enabled ||
915
+ p.localScope?.enabled),
916
+ ).length;
812
917
  const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}`;
813
918
  const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""} │ / to search`;
814
919
 
@@ -833,11 +938,17 @@ export function PluginsScreen() {
833
938
  getKey={(item) => item.id}
834
939
  />
835
940
  {pluginsState.searchQuery && selectableItems.length === 0 && (
836
- <EmptyFilterState query={pluginsState.searchQuery} entityName="plugins" />
941
+ <EmptyFilterState
942
+ query={pluginsState.searchQuery}
943
+ entityName="plugins"
944
+ />
837
945
  )}
838
946
  </box>
839
947
  }
840
- detailPanel={renderPluginDetail(selectedItem, pluginsState.collapsedMarketplaces)}
948
+ detailPanel={renderPluginDetail(
949
+ selectedItem,
950
+ pluginsState.collapsedMarketplaces,
951
+ )}
841
952
  />
842
953
  );
843
954
  }
@@ -7,7 +7,7 @@ const SCREEN_HEADER_HEIGHT = 4; // ScreenLayout: border + title row + status/sea
7
7
  const SCREEN_FOOTER_HEIGHT = 2; // ScreenLayout: border-top + footer content
8
8
  const APP_MARGINS = 1; // buffer for margins/padding
9
9
  const PROGRESS_HEIGHT = 1; // when visible
10
- function calculateDimensions(columns, rows, showProgress, showDebug, showUpdateBanner) {
10
+ function calculateDimensions(columns, rows, showProgress, showDebug, showUpdateBanner, transientBannerCount) {
11
11
  const terminalWidth = columns;
12
12
  const terminalHeight = rows;
13
13
  // Calculate total available height for ScreenLayout (includes its header, panels, footer)
@@ -18,6 +18,7 @@ function calculateDimensions(columns, rows, showProgress, showDebug, showUpdateB
18
18
  contentHeight -= 1;
19
19
  if (showUpdateBanner)
20
20
  contentHeight -= 1;
21
+ contentHeight -= transientBannerCount;
21
22
  contentHeight = Math.max(10, contentHeight); // Minimum 10 lines for full layout
22
23
  // Calculate available content width (accounting for padding)
23
24
  const contentWidth = Math.max(40, terminalWidth - 4);
@@ -33,22 +34,23 @@ function calculateDimensions(columns, rows, showProgress, showDebug, showUpdateB
33
34
  listPanelHeight,
34
35
  };
35
36
  }
36
- export function DimensionsProvider({ children, showProgress = false, showDebug = false, showUpdateBanner = false, }) {
37
+ export function DimensionsProvider({ children, showProgress = false, showDebug = false, showUpdateBanner = false, transientBannerCount = 0, }) {
37
38
  const renderer = useRenderer();
38
- const [dimensions, setDimensions] = useState(() => calculateDimensions(renderer.width, renderer.height, showProgress, showDebug, showUpdateBanner));
39
+ const [dimensions, setDimensions] = useState(() => calculateDimensions(renderer.width, renderer.height, showProgress, showDebug, showUpdateBanner, transientBannerCount));
39
40
  // Handle terminal resize
40
41
  useOnResize((width, height) => {
41
- setDimensions(calculateDimensions(width, height, showProgress, showDebug, showUpdateBanner));
42
+ setDimensions(calculateDimensions(width, height, showProgress, showDebug, showUpdateBanner, transientBannerCount));
42
43
  });
43
- // Update when showProgress/showDebug/showUpdateBanner changes
44
+ // Update when showProgress/showDebug/showUpdateBanner/transientBannerCount changes
44
45
  useEffect(() => {
45
- setDimensions(calculateDimensions(renderer.width, renderer.height, showProgress, showDebug, showUpdateBanner));
46
+ setDimensions(calculateDimensions(renderer.width, renderer.height, showProgress, showDebug, showUpdateBanner, transientBannerCount));
46
47
  }, [
47
48
  renderer.width,
48
49
  renderer.height,
49
50
  showProgress,
50
51
  showDebug,
51
52
  showUpdateBanner,
53
+ transientBannerCount,
52
54
  ]);
53
55
  return (_jsx(DimensionsContext.Provider, { value: dimensions, children: children }));
54
56
  }
@@ -30,6 +30,8 @@ interface DimensionsProviderProps {
30
30
  showDebug?: boolean;
31
31
  /** Whether update banner is visible */
32
32
  showUpdateBanner?: boolean;
33
+ /** Number of transient banners (recovery, mismatch) currently visible */
34
+ transientBannerCount?: number;
33
35
  }
34
36
 
35
37
  function calculateDimensions(
@@ -38,6 +40,7 @@ function calculateDimensions(
38
40
  showProgress: boolean,
39
41
  showDebug: boolean,
40
42
  showUpdateBanner: boolean,
43
+ transientBannerCount: number,
41
44
  ): Dimensions {
42
45
  const terminalWidth = columns;
43
46
  const terminalHeight = rows;
@@ -47,6 +50,7 @@ function calculateDimensions(
47
50
  if (showProgress) contentHeight -= PROGRESS_HEIGHT;
48
51
  if (showDebug) contentHeight -= 1;
49
52
  if (showUpdateBanner) contentHeight -= 1;
53
+ contentHeight -= transientBannerCount;
50
54
  contentHeight = Math.max(10, contentHeight); // Minimum 10 lines for full layout
51
55
 
52
56
  // Calculate available content width (accounting for padding)
@@ -74,6 +78,7 @@ export function DimensionsProvider({
74
78
  showProgress = false,
75
79
  showDebug = false,
76
80
  showUpdateBanner = false,
81
+ transientBannerCount = 0,
77
82
  }: DimensionsProviderProps) {
78
83
  const renderer = useRenderer();
79
84
 
@@ -84,6 +89,7 @@ export function DimensionsProvider({
84
89
  showProgress,
85
90
  showDebug,
86
91
  showUpdateBanner,
92
+ transientBannerCount,
87
93
  ),
88
94
  );
89
95
 
@@ -96,11 +102,12 @@ export function DimensionsProvider({
96
102
  showProgress,
97
103
  showDebug,
98
104
  showUpdateBanner,
105
+ transientBannerCount,
99
106
  ),
100
107
  );
101
108
  });
102
109
 
103
- // Update when showProgress/showDebug/showUpdateBanner changes
110
+ // Update when showProgress/showDebug/showUpdateBanner/transientBannerCount changes
104
111
  useEffect(() => {
105
112
  setDimensions(
106
113
  calculateDimensions(
@@ -109,6 +116,7 @@ export function DimensionsProvider({
109
116
  showProgress,
110
117
  showDebug,
111
118
  showUpdateBanner,
119
+ transientBannerCount,
112
120
  ),
113
121
  );
114
122
  }, [
@@ -117,6 +125,7 @@ export function DimensionsProvider({
117
125
  showProgress,
118
126
  showDebug,
119
127
  showUpdateBanner,
128
+ transientBannerCount,
120
129
  ]);
121
130
 
122
131
  return (