@laststance/claude-plugin-dashboard 0.2.2 → 0.3.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/README.md CHANGED
@@ -32,7 +32,12 @@ Built with [Ink](https://github.com/vadimdemedes/ink) (React for CLI).
32
32
  ## Installation
33
33
 
34
34
  ```bash
35
- npm install -g @laststance/claude-plugin-dashboard
35
+ npx @laststance/claude-plugin-dashboard@latest
36
+
37
+ ```
38
+
39
+ ```bash
40
+ npm install -g @laststance/claude-plugin-dashboard@latest
36
41
  ```
37
42
 
38
43
  ## Usage
@@ -244,6 +249,17 @@ MIT © [Laststance.io](https://github.com/laststance)
244
249
 
245
250
  ## Changelog
246
251
 
252
+ ### v0.2.2
253
+
254
+ - Claude Code v2.1.3 compatibility (unified Skills/Commands model)
255
+
256
+ ### v0.2.1
257
+
258
+ - **Marketplace Management**: Add/remove/refresh marketplaces directly from dashboard
259
+ - **Plugin Component Types**: Display skills, agents, hooks, MCP servers in detail view
260
+ - **Bug Fix**: Search filter now works correctly in Enabled and Installed tabs
261
+ - **CI/CD**: GitHub Actions workflow, CI and Codecov badges
262
+
247
263
  ### v0.2.0
248
264
 
249
265
  - **Enabled tab**: New default view showing active plugins (installed AND enabled)
package/dist/app.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Main App component for Claude Code Plugin Dashboard
3
3
  * Interactive TUI to browse and manage Claude Code plugins
4
4
  */
5
- import type { AppState, Action, Plugin, FocusZone } from './types/index.js';
5
+ import type { AppState, Action, Plugin, Marketplace, FocusZone } from './types/index.js';
6
6
  /**
7
7
  * Initial application state
8
8
  */
@@ -31,6 +31,12 @@ export declare function getItemsForTab(state: AppState): unknown[];
31
31
  * Get filtered and sorted plugins for discover tab
32
32
  */
33
33
  export declare function getFilteredPlugins(state: AppState): Plugin[];
34
+ /**
35
+ * Get filtered marketplaces based on search query
36
+ * @param state - Current app state
37
+ * @returns Filtered array of marketplaces
38
+ */
39
+ export declare function getFilteredMarketplaces(state: AppState): Marketplace[];
34
40
  /**
35
41
  * Main App component
36
42
  */
package/dist/app.js CHANGED
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  */
6
6
  import { useEffect, useReducer } from 'react';
7
7
  import { Box, Text, useInput, useApp } from 'ink';
8
+ import { match, P } from 'ts-pattern';
8
9
  import TabBar, { getNextTab } from './components/TabBar.js';
9
10
  import KeyHints from './components/KeyHints.js';
10
11
  import DiscoverTab from './tabs/DiscoverTab.js';
@@ -15,7 +16,7 @@ import ErrorsTab from './tabs/ErrorsTab.js';
15
16
  import { loadAllPlugins, loadMarketplaces, searchPlugins, searchMarketplaces, sortPlugins, } from './services/pluginService.js';
16
17
  import { togglePlugin } from './services/settingsService.js';
17
18
  import { installPlugin, uninstallPlugin, } from './services/pluginActionsService.js';
18
- import { addMarketplace, removeMarketplace, updateMarketplace, } from './services/marketplaceActionsService.js';
19
+ import { addMarketplace, removeMarketplace, updateMarketplace, toggleAutoUpdate, } from './services/marketplaceActionsService.js';
19
20
  import AddMarketplaceDialog from './components/AddMarketplaceDialog.js';
20
21
  import ConfirmDialog from './components/ConfirmDialog.js';
21
22
  import HelpOverlay from './components/HelpOverlay.js';
@@ -45,6 +46,8 @@ export const initialState = {
45
46
  confirmRemoveMarketplace: false,
46
47
  showAddMarketplaceDialog: false,
47
48
  addMarketplaceError: null,
49
+ showMarketplaceActionMenu: false,
50
+ actionMenuSelectedIndex: 0,
48
51
  };
49
52
  /**
50
53
  * Get available focus zones for the current tab
@@ -65,16 +68,12 @@ export function getAvailableZones(activeTab) {
65
68
  * @returns Display message for the operation
66
69
  */
67
70
  function getMarketplaceOperationMessage(operation, marketplaceId) {
68
- switch (operation) {
69
- case 'adding':
70
- return 'Adding marketplace...';
71
- case 'removing':
72
- return `Removing ${marketplaceId}...`;
73
- case 'updating':
74
- return `Updating ${marketplaceId || 'marketplaces'}...`;
75
- default:
76
- return '';
77
- }
71
+ return match(operation)
72
+ .with('adding', () => 'Adding marketplace...')
73
+ .with('removing', () => `Removing ${marketplaceId}...`)
74
+ .with('updating', () => `Updating ${marketplaceId || 'marketplaces'}...`)
75
+ .with('idle', () => '')
76
+ .exhaustive();
78
77
  }
79
78
  /**
80
79
  * State reducer for application state management
@@ -89,6 +88,8 @@ export function appReducer(state, action) {
89
88
  selectedIndex: 0,
90
89
  searchQuery: '',
91
90
  message: null,
91
+ showMarketplaceActionMenu: false,
92
+ actionMenuSelectedIndex: 0,
92
93
  };
93
94
  case 'NEXT_TAB':
94
95
  return {
@@ -98,6 +99,8 @@ export function appReducer(state, action) {
98
99
  selectedIndex: 0,
99
100
  searchQuery: '',
100
101
  message: null,
102
+ showMarketplaceActionMenu: false,
103
+ actionMenuSelectedIndex: 0,
101
104
  };
102
105
  case 'PREV_TAB':
103
106
  return {
@@ -107,6 +110,8 @@ export function appReducer(state, action) {
107
110
  selectedIndex: 0,
108
111
  searchQuery: '',
109
112
  message: null,
113
+ showMarketplaceActionMenu: false,
114
+ actionMenuSelectedIndex: 0,
110
115
  };
111
116
  case 'SET_FOCUS_ZONE':
112
117
  return {
@@ -279,6 +284,33 @@ export function appReducer(state, action) {
279
284
  marketplaceOperation: 'idle',
280
285
  operationMarketplaceId: null,
281
286
  };
287
+ case 'SHOW_MARKETPLACE_ACTION_MENU':
288
+ return {
289
+ ...state,
290
+ showMarketplaceActionMenu: true,
291
+ actionMenuSelectedIndex: 0,
292
+ };
293
+ case 'HIDE_MARKETPLACE_ACTION_MENU':
294
+ return {
295
+ ...state,
296
+ showMarketplaceActionMenu: false,
297
+ actionMenuSelectedIndex: 0,
298
+ };
299
+ case 'SET_ACTION_MENU_INDEX':
300
+ return {
301
+ ...state,
302
+ actionMenuSelectedIndex: action.payload,
303
+ };
304
+ case 'MOVE_ACTION_MENU_SELECTION': {
305
+ const maxIndex = 3; // 4 actions: browse, update, autoUpdate, remove
306
+ const newIndex = action.payload === 'up'
307
+ ? Math.max(0, state.actionMenuSelectedIndex - 1)
308
+ : Math.min(maxIndex, state.actionMenuSelectedIndex + 1);
309
+ return {
310
+ ...state,
311
+ actionMenuSelectedIndex: newIndex,
312
+ };
313
+ }
282
314
  default:
283
315
  return state;
284
316
  }
@@ -292,28 +324,23 @@ export function appReducer(state, action) {
292
324
  * // => Only installed plugins matching 'su'
293
325
  */
294
326
  export function getItemsForTab(state) {
295
- switch (state.activeTab) {
296
- case 'enabled': {
297
- const enabledPlugins = state.plugins.filter((p) => p.isInstalled && p.isEnabled);
298
- return state.searchQuery
299
- ? searchPlugins(state.searchQuery, enabledPlugins)
300
- : enabledPlugins;
301
- }
302
- case 'installed': {
303
- const installedPlugins = state.plugins.filter((p) => p.isInstalled);
304
- return state.searchQuery
305
- ? searchPlugins(state.searchQuery, installedPlugins)
306
- : installedPlugins;
307
- }
308
- case 'discover':
309
- return getFilteredPlugins(state);
310
- case 'marketplaces':
311
- return state.marketplaces;
312
- case 'errors':
313
- return state.errors;
314
- default:
315
- return [];
316
- }
327
+ return match(state.activeTab)
328
+ .with('enabled', () => {
329
+ const enabledPlugins = state.plugins.filter((p) => p.isInstalled && p.isEnabled);
330
+ return state.searchQuery
331
+ ? searchPlugins(state.searchQuery, enabledPlugins)
332
+ : enabledPlugins;
333
+ })
334
+ .with('installed', () => {
335
+ const installedPlugins = state.plugins.filter((p) => p.isInstalled);
336
+ return state.searchQuery
337
+ ? searchPlugins(state.searchQuery, installedPlugins)
338
+ : installedPlugins;
339
+ })
340
+ .with('discover', () => getFilteredPlugins(state))
341
+ .with('marketplaces', () => state.marketplaces)
342
+ .with('errors', () => state.errors)
343
+ .otherwise(() => []);
317
344
  }
318
345
  /**
319
346
  * Get filtered and sorted plugins for discover tab
@@ -328,6 +355,19 @@ export function getFilteredPlugins(state) {
328
355
  plugins = sortPlugins(plugins, state.sortBy, state.sortOrder);
329
356
  return plugins;
330
357
  }
358
+ /**
359
+ * Get filtered marketplaces based on search query
360
+ * @param state - Current app state
361
+ * @returns Filtered array of marketplaces
362
+ */
363
+ export function getFilteredMarketplaces(state) {
364
+ let marketplaces = state.marketplaces;
365
+ // Apply search filter
366
+ if (state.searchQuery) {
367
+ marketplaces = searchMarketplaces(state.searchQuery, marketplaces);
368
+ }
369
+ return marketplaces;
370
+ }
331
371
  /**
332
372
  * Main App component
333
373
  */
@@ -488,6 +528,46 @@ export default function App() {
488
528
  });
489
529
  }
490
530
  }
531
+ /**
532
+ * Handle toggling auto-update for a marketplace
533
+ * @param marketplaceId - ID of the marketplace
534
+ * @param currentValue - Current auto-update state
535
+ */
536
+ async function handleToggleAutoUpdate(marketplaceId, currentValue) {
537
+ dispatch({
538
+ type: 'START_MARKETPLACE_OPERATION',
539
+ payload: { operation: 'updating', marketplaceId },
540
+ });
541
+ const result = await toggleAutoUpdate(marketplaceId, currentValue);
542
+ dispatch({ type: 'END_MARKETPLACE_OPERATION' });
543
+ if (result.success) {
544
+ // Reload marketplaces to get fresh state
545
+ const marketplaces = loadMarketplaces();
546
+ dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
547
+ dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
548
+ }
549
+ else {
550
+ dispatch({
551
+ type: 'SET_MESSAGE',
552
+ payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
553
+ });
554
+ }
555
+ }
556
+ /**
557
+ * Handle browsing plugins for a specific marketplace
558
+ * Switches to Discover tab with marketplace filter applied
559
+ * @param marketplaceId - ID of the marketplace to browse
560
+ */
561
+ function handleBrowseMarketplacePlugins(marketplaceId) {
562
+ // Switch to Discover tab
563
+ dispatch({ type: 'SET_TAB', payload: 'discover' });
564
+ // Set search query to filter by marketplace
565
+ dispatch({ type: 'SET_SEARCH_QUERY', payload: marketplaceId });
566
+ dispatch({
567
+ type: 'SET_MESSAGE',
568
+ payload: `Browsing plugins from ${marketplaceId}`,
569
+ });
570
+ }
491
571
  // Keyboard input handling
492
572
  useInput((input, key) => {
493
573
  // Block all input during operations (plugin or marketplace)
@@ -513,7 +593,7 @@ export default function App() {
513
593
  }
514
594
  // Handle plugin uninstall confirmation dialog
515
595
  if (state.confirmUninstall && state.operationPluginId) {
516
- if (input === 'y' || input === 'Y') {
596
+ if (input === 'y' || input === 'Y' || key.return) {
517
597
  dispatch({ type: 'HIDE_CONFIRM_UNINSTALL' });
518
598
  handleUninstall(state.operationPluginId);
519
599
  return;
@@ -527,7 +607,7 @@ export default function App() {
527
607
  }
528
608
  // Handle marketplace remove confirmation dialog
529
609
  if (state.confirmRemoveMarketplace && state.operationMarketplaceId) {
530
- if (input === 'y' || input === 'Y') {
610
+ if (input === 'y' || input === 'Y' || key.return) {
531
611
  dispatch({ type: 'HIDE_CONFIRM_REMOVE_MARKETPLACE' });
532
612
  handleRemoveMarketplace(state.operationMarketplaceId);
533
613
  return;
@@ -570,6 +650,54 @@ export default function App() {
570
650
  }
571
651
  return;
572
652
  }
653
+ // Handle marketplace action menu
654
+ if (state.showMarketplaceActionMenu) {
655
+ const selectedMarketplace = getFilteredMarketplaces(state)[state.selectedIndex];
656
+ if (!selectedMarketplace) {
657
+ dispatch({ type: 'HIDE_MARKETPLACE_ACTION_MENU' });
658
+ return;
659
+ }
660
+ // Up/Down arrow navigation
661
+ if (key.upArrow || (key.ctrl && input === 'p')) {
662
+ dispatch({ type: 'MOVE_ACTION_MENU_SELECTION', payload: 'up' });
663
+ return;
664
+ }
665
+ if (key.downArrow || (key.ctrl && input === 'n')) {
666
+ dispatch({ type: 'MOVE_ACTION_MENU_SELECTION', payload: 'down' });
667
+ return;
668
+ }
669
+ // Execute action on Enter
670
+ if (key.return) {
671
+ dispatch({ type: 'HIDE_MARKETPLACE_ACTION_MENU' });
672
+ const actionIndex = state.actionMenuSelectedIndex;
673
+ if (actionIndex === 0) {
674
+ // Browse plugins
675
+ handleBrowseMarketplacePlugins(selectedMarketplace.id);
676
+ }
677
+ else if (actionIndex === 1) {
678
+ // Update marketplace
679
+ handleUpdateMarketplace(selectedMarketplace.id);
680
+ }
681
+ else if (actionIndex === 2) {
682
+ // Toggle auto-update
683
+ handleToggleAutoUpdate(selectedMarketplace.id, selectedMarketplace.autoUpdate ?? false);
684
+ }
685
+ else if (actionIndex === 3) {
686
+ // Remove marketplace (show confirmation)
687
+ dispatch({
688
+ type: 'SHOW_CONFIRM_REMOVE_MARKETPLACE',
689
+ payload: selectedMarketplace.id,
690
+ });
691
+ }
692
+ return;
693
+ }
694
+ // Close menu on Escape
695
+ if (key.escape) {
696
+ dispatch({ type: 'HIDE_MARKETPLACE_ACTION_MENU' });
697
+ return;
698
+ }
699
+ return;
700
+ }
573
701
  // Search mode input (when focusZone is 'search')
574
702
  if (state.focusZone === 'search') {
575
703
  // Up arrow: move focus to tabbar
@@ -766,6 +894,14 @@ export default function App() {
766
894
  }
767
895
  // Marketplace-specific key bindings
768
896
  if (state.activeTab === 'marketplaces' && state.focusZone === 'list') {
897
+ // Open action menu (Enter or m key)
898
+ if (key.return || input === 'm') {
899
+ const selectedMarketplace = filteredMarketplaces[state.selectedIndex];
900
+ if (selectedMarketplace) {
901
+ dispatch({ type: 'SHOW_MARKETPLACE_ACTION_MENU' });
902
+ }
903
+ return;
904
+ }
769
905
  // Add marketplace (a key)
770
906
  if (input === 'a') {
771
907
  dispatch({ type: 'SHOW_ADD_MARKETPLACE_DIALOG' });
@@ -853,56 +989,67 @@ export default function App() {
853
989
  const filteredMarketplaces = state.searchQuery
854
990
  ? searchMarketplaces(state.searchQuery, state.marketplaces)
855
991
  : state.marketplaces;
856
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, color: "magenta", children: "\u26A1 Claude Code Plugin Dashboard" }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: ["v", packageJson.version] })] }), _jsx(TabBar, { activeTab: state.activeTab, isFocused: state.focusZone === 'tabbar' }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [state.activeTab === 'enabled' && (_jsx(EnabledTab, { plugins: enabledPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'installed' && (_jsx(InstalledTab, { plugins: installedPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'discover' && (_jsx(DiscoverTab, { plugins: filteredPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, sortBy: state.sortBy, sortOrder: state.sortOrder, focusZone: state.focusZone })), state.activeTab === 'marketplaces' && (_jsx(MarketplacesTab, { marketplaces: filteredMarketplaces, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'errors' && (_jsx(ErrorsTab, { errors: state.errors, selectedIndex: state.selectedIndex }))] }), state.confirmUninstall && state.operationPluginId && (_jsx(ConfirmDialog, { message: `Uninstall ${state.operationPluginId}?` })), state.confirmRemoveMarketplace && state.operationMarketplaceId && (_jsx(ConfirmDialog, { message: `Remove marketplace ${state.operationMarketplaceId}?` })), state.showAddMarketplaceDialog && (_jsx(AddMarketplaceDialog, { value: state.searchQuery, error: state.addMarketplaceError ?? undefined })), _jsx(HelpOverlay, { isVisible: state.showHelp }), state.message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: state.message }) })), _jsx(KeyHints, { focusZone: state.focusZone, extraHints: (() => {
857
- // Search mode - no extra hints (base hints cover it)
858
- if (state.focusZone === 'search') {
859
- return undefined;
860
- }
861
- // TabBar mode - no extra hints
862
- if (state.focusZone === 'tabbar') {
863
- return undefined;
992
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, color: "magenta", children: "\u26A1 Claude Code Plugin Dashboard" }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: ["v", packageJson.version] })] }), _jsx(TabBar, { activeTab: state.activeTab, isFocused: state.focusZone === 'tabbar' }), _jsx(Box, { flexGrow: 1, flexDirection: "column", children: match(state.activeTab)
993
+ .with('enabled', () => (_jsx(EnabledTab, { plugins: enabledPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone }, "enabled")))
994
+ .with('installed', () => (_jsx(InstalledTab, { plugins: installedPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone }, "installed")))
995
+ .with('discover', () => (_jsx(DiscoverTab, { plugins: filteredPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, sortBy: state.sortBy, sortOrder: state.sortOrder, focusZone: state.focusZone }, "discover")))
996
+ .with('marketplaces', () => (_jsx(MarketplacesTab, { marketplaces: filteredMarketplaces, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone, showActionMenu: state.showMarketplaceActionMenu, actionMenuSelectedIndex: state.actionMenuSelectedIndex }, "marketplaces")))
997
+ .with('errors', () => (_jsx(ErrorsTab, { errors: state.errors, selectedIndex: state.selectedIndex }, "errors")))
998
+ .exhaustive() }), state.confirmUninstall && state.operationPluginId && (_jsx(ConfirmDialog, { message: `Uninstall ${state.operationPluginId}?` })), state.confirmRemoveMarketplace && state.operationMarketplaceId && (_jsx(ConfirmDialog, { message: `Remove marketplace ${state.operationMarketplaceId}?` })), state.showAddMarketplaceDialog && (_jsx(AddMarketplaceDialog, { value: state.searchQuery, error: state.addMarketplaceError ?? undefined })), _jsx(HelpOverlay, { isVisible: state.showHelp }), state.message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: state.message }) })), _jsx(KeyHints, { focusZone: state.focusZone, extraHints: match([state.focusZone, state.activeTab])
999
+ .with(['search', P._], () => undefined)
1000
+ .with(['tabbar', P._], () => undefined)
1001
+ .with(['list', 'enabled'], () => {
1002
+ const hints = [
1003
+ { key: '/', action: 'search' },
1004
+ { key: 'i', action: 'install' },
1005
+ { key: 'u', action: 'uninstall' },
1006
+ ];
1007
+ const selectedPlugin = enabledPlugins[state.selectedIndex];
1008
+ if (selectedPlugin) {
1009
+ hints.push({
1010
+ key: 'Enter',
1011
+ action: selectedPlugin.isInstalled ? 'toggle' : 'install',
1012
+ });
864
1013
  }
865
- // List mode - add tab-specific hints
866
- // Plugin tabs hints (enabled, installed, discover)
867
- if (state.activeTab === 'enabled' ||
868
- state.activeTab === 'installed' ||
869
- state.activeTab === 'discover') {
870
- const hints = [
871
- { key: '/', action: 'search' },
872
- { key: 'i', action: 'install' },
873
- { key: 'u', action: 'uninstall' },
874
- ];
875
- // Get selected plugin to determine Enter action
876
- const items = state.activeTab === 'enabled'
877
- ? enabledPlugins
878
- : state.activeTab === 'installed'
879
- ? installedPlugins
880
- : filteredPlugins;
881
- const selectedPlugin = items[state.selectedIndex];
882
- // Add contextual Enter hint
883
- if (selectedPlugin) {
884
- if (!selectedPlugin.isInstalled) {
885
- hints.push({ key: 'Enter', action: 'install' });
886
- }
887
- else {
888
- hints.push({ key: 'Enter', action: 'toggle' });
889
- }
890
- }
891
- // Add sort hint for discover tab
892
- if (state.activeTab === 'discover') {
893
- hints.push({ key: 's', action: 'sort' });
894
- }
895
- return hints;
1014
+ return hints;
1015
+ })
1016
+ .with(['list', 'installed'], () => {
1017
+ const hints = [
1018
+ { key: '/', action: 'search' },
1019
+ { key: 'i', action: 'install' },
1020
+ { key: 'u', action: 'uninstall' },
1021
+ ];
1022
+ const selectedPlugin = installedPlugins[state.selectedIndex];
1023
+ if (selectedPlugin) {
1024
+ hints.push({
1025
+ key: 'Enter',
1026
+ action: selectedPlugin.isInstalled ? 'toggle' : 'install',
1027
+ });
896
1028
  }
897
- // Marketplaces tab hints
898
- if (state.activeTab === 'marketplaces') {
899
- return [
900
- { key: '/', action: 'search' },
901
- { key: 'a', action: 'add' },
902
- { key: 'd', action: 'remove' },
903
- { key: 'u', action: 'update' },
904
- ];
1029
+ return hints;
1030
+ })
1031
+ .with(['list', 'discover'], () => {
1032
+ const hints = [
1033
+ { key: '/', action: 'search' },
1034
+ { key: 'i', action: 'install' },
1035
+ { key: 'u', action: 'uninstall' },
1036
+ ];
1037
+ const selectedPlugin = filteredPlugins[state.selectedIndex];
1038
+ if (selectedPlugin) {
1039
+ hints.push({
1040
+ key: 'Enter',
1041
+ action: selectedPlugin.isInstalled ? 'toggle' : 'install',
1042
+ });
905
1043
  }
906
- return undefined;
907
- })() })] }));
1044
+ hints.push({ key: 's', action: 'sort' });
1045
+ return hints;
1046
+ })
1047
+ .with(['list', 'marketplaces'], () => [
1048
+ { key: '/', action: 'search' },
1049
+ { key: 'a', action: 'add' },
1050
+ { key: 'd', action: 'remove' },
1051
+ { key: 'u', action: 'update' },
1052
+ ])
1053
+ .with(['list', 'errors'], () => undefined)
1054
+ .otherwise(() => undefined) })] }));
908
1055
  }
package/dist/cli.js CHANGED
@@ -14,12 +14,14 @@ import { jsx as _jsx } from "react/jsx-runtime";
14
14
  * claude-plugin-dashboard toggle <plugin-id> # Toggle plugin
15
15
  * claude-plugin-dashboard help # Show help
16
16
  */
17
- import { render } from 'ink';
17
+ import { withFullScreen } from 'fullscreen-ink';
18
+ import { match, P } from 'ts-pattern';
18
19
  import App from './app.js';
19
20
  import { loadAllPlugins, loadInstalledPlugins, loadMarketplaces, getPluginStatistics, getPluginById, } from './services/pluginService.js';
20
21
  import { enablePlugin, disablePlugin, togglePlugin, } from './services/settingsService.js';
21
22
  import { fileExists } from './services/fileService.js';
22
23
  import { PATHS } from './utils/paths.js';
24
+ import packageJson from '../package.json' with { type: 'json' };
23
25
  const args = process.argv.slice(2);
24
26
  const command = args[0];
25
27
  const subCommand = args[1];
@@ -243,66 +245,57 @@ function checkClaudeCodeInstalled() {
243
245
  if (command) {
244
246
  // Non-interactive mode
245
247
  checkClaudeCodeInstalled();
246
- switch (command) {
247
- case 'status':
248
- showStatus();
249
- break;
250
- case 'list':
251
- if (subCommand === '--installed' || args.includes('--installed')) {
252
- listPlugins({ installed: true });
253
- }
254
- else if (subCommand === '--marketplace' ||
255
- args.includes('--marketplace')) {
256
- const marketplaceIndex = args.indexOf('--marketplace');
257
- const marketplace = args[marketplaceIndex + 1];
258
- listPlugins({ marketplace });
259
- }
260
- else {
261
- listPlugins({});
262
- }
263
- break;
264
- case 'info':
265
- if (!subCommand) {
266
- console.error('Usage: claude-plugin-dashboard info <plugin-id>');
267
- process.exit(1);
268
- }
269
- showPluginInfo(subCommand);
270
- break;
271
- case 'enable':
272
- if (!subCommand) {
273
- console.error('Usage: claude-plugin-dashboard enable <plugin-id>');
274
- process.exit(1);
275
- }
276
- handleEnable(subCommand);
277
- break;
278
- case 'disable':
279
- if (!subCommand) {
280
- console.error('Usage: claude-plugin-dashboard disable <plugin-id>');
281
- process.exit(1);
282
- }
283
- handleDisable(subCommand);
284
- break;
285
- case 'toggle':
286
- if (!subCommand) {
287
- console.error('Usage: claude-plugin-dashboard toggle <plugin-id>');
288
- process.exit(1);
289
- }
290
- handleToggle(subCommand);
291
- break;
292
- case 'help':
293
- case '-h':
294
- case '--help':
295
- showHelp();
296
- break;
297
- case '-v':
298
- case '--version':
299
- console.log('claude-plugin-dashboard v0.1.0');
300
- break;
301
- default:
302
- console.error(`Unknown command: ${command}`);
303
- console.log('Run "claude-plugin-dashboard help" for usage information.');
248
+ match(command)
249
+ .with('status', () => showStatus())
250
+ .with('list', () => {
251
+ if (subCommand === '--installed' || args.includes('--installed')) {
252
+ listPlugins({ installed: true });
253
+ }
254
+ else if (subCommand === '--marketplace' ||
255
+ args.includes('--marketplace')) {
256
+ const marketplaceIndex = args.indexOf('--marketplace');
257
+ const marketplace = args[marketplaceIndex + 1];
258
+ listPlugins({ marketplace });
259
+ }
260
+ else {
261
+ listPlugins({});
262
+ }
263
+ })
264
+ .with('info', () => {
265
+ if (!subCommand) {
266
+ console.error('Usage: claude-plugin-dashboard info <plugin-id>');
304
267
  process.exit(1);
305
- }
268
+ }
269
+ showPluginInfo(subCommand);
270
+ })
271
+ .with('enable', () => {
272
+ if (!subCommand) {
273
+ console.error('Usage: claude-plugin-dashboard enable <plugin-id>');
274
+ process.exit(1);
275
+ }
276
+ handleEnable(subCommand);
277
+ })
278
+ .with('disable', () => {
279
+ if (!subCommand) {
280
+ console.error('Usage: claude-plugin-dashboard disable <plugin-id>');
281
+ process.exit(1);
282
+ }
283
+ handleDisable(subCommand);
284
+ })
285
+ .with('toggle', () => {
286
+ if (!subCommand) {
287
+ console.error('Usage: claude-plugin-dashboard toggle <plugin-id>');
288
+ process.exit(1);
289
+ }
290
+ handleToggle(subCommand);
291
+ })
292
+ .with(P.union('help', '-h', '--help'), () => showHelp())
293
+ .with(P.union('-v', '--version'), () => console.log(`claude-plugin-dashboard v${packageJson.version}`))
294
+ .otherwise((cmd) => {
295
+ console.error(`Unknown command: ${cmd}`);
296
+ console.log('Run "claude-plugin-dashboard help" for usage information.');
297
+ process.exit(1);
298
+ });
306
299
  }
307
300
  else {
308
301
  // Interactive mode
@@ -312,11 +305,9 @@ else {
312
305
  console.log('Use "claude-plugin-dashboard help" for non-interactive commands.');
313
306
  process.exit(1);
314
307
  }
315
- const instance = render(_jsx(App, {}));
316
- // Clear screen when app exits (q key or Ctrl+C)
317
- instance.waitUntilExit().then(() => {
318
- instance.clear();
319
- // Clear entire terminal screen and reset cursor to top-left
320
- process.stdout.write('\x1b[2J\x1b[H');
321
- });
308
+ // Use fullscreen-ink for alternate screen buffer management
309
+ // This prevents rendering artifacts when switching tabs
310
+ const ink = withFullScreen(_jsx(App, {}));
311
+ ink.start();
312
+ ink.waitUntilExit();
322
313
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ComponentBadges component
3
- * Displays plugin component type badges with icons and counts
4
- * Icons: Skills(S), Commands(/), Agents(@), Hooks(H), MCP(M), LSP(L)
3
+ * Displays plugin component type badges with readable text labels and counts
4
+ * Labels: Skills, Slash, Agents, Hooks, MCP, LSP
5
5
  */
6
6
  import type { PluginComponents } from '../types/index.js';
7
7
  /**
@@ -18,7 +18,7 @@ export interface ComponentBadgesProps {
18
18
  * @returns Badges component or null if no components
19
19
  * @example
20
20
  * <ComponentBadges components={{ skills: 5, commands: 2 }} />
21
- * // Renders: [S:5] [/:2]
21
+ * // Renders: Skills:5 Slash:2
22
22
  */
23
23
  export default function ComponentBadges({ components, }: ComponentBadgesProps): React.ReactNode;
24
24
  /**