@laststance/claude-plugin-dashboard 0.1.1 → 0.2.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.
Files changed (35) hide show
  1. package/README.md +70 -32
  2. package/dist/app.d.ts +29 -0
  3. package/dist/app.js +496 -69
  4. package/dist/components/AddMarketplaceDialog.d.ts +20 -0
  5. package/dist/components/AddMarketplaceDialog.js +18 -0
  6. package/dist/components/ComponentBadges.d.ts +32 -0
  7. package/dist/components/ComponentBadges.js +82 -0
  8. package/dist/components/HelpOverlay.d.ts +15 -0
  9. package/dist/components/HelpOverlay.js +51 -0
  10. package/dist/components/KeyHints.d.ts +6 -3
  11. package/dist/components/KeyHints.js +39 -10
  12. package/dist/components/MarketplaceList.d.ts +4 -2
  13. package/dist/components/MarketplaceList.js +7 -3
  14. package/dist/components/PluginDetail.js +2 -1
  15. package/dist/components/PluginList.d.ts +29 -2
  16. package/dist/components/PluginList.js +26 -5
  17. package/dist/components/SearchInput.js +1 -1
  18. package/dist/components/TabBar.d.ts +5 -3
  19. package/dist/components/TabBar.js +20 -8
  20. package/dist/services/componentService.d.ts +35 -0
  21. package/dist/services/componentService.js +178 -0
  22. package/dist/services/marketplaceActionsService.d.ts +44 -0
  23. package/dist/services/marketplaceActionsService.js +92 -0
  24. package/dist/services/pluginService.d.ts +10 -0
  25. package/dist/services/pluginService.js +22 -0
  26. package/dist/tabs/DiscoverTab.d.ts +5 -3
  27. package/dist/tabs/DiscoverTab.js +3 -2
  28. package/dist/tabs/EnabledTab.d.ts +24 -0
  29. package/dist/tabs/EnabledTab.js +26 -0
  30. package/dist/tabs/InstalledTab.d.ts +10 -3
  31. package/dist/tabs/InstalledTab.js +14 -10
  32. package/dist/tabs/MarketplacesTab.d.ts +10 -3
  33. package/dist/tabs/MarketplacesTab.js +12 -3
  34. package/dist/types/index.d.ts +71 -1
  35. package/package.json +11 -3
@@ -0,0 +1,20 @@
1
+ /**
2
+ * AddMarketplaceDialog component
3
+ * Dialog for adding a new marketplace source
4
+ */
5
+ export interface AddMarketplaceDialogProps {
6
+ /** Current input value */
7
+ value: string;
8
+ /** Error message to display (if any) */
9
+ error?: string;
10
+ }
11
+ /**
12
+ * Dialog for adding a new marketplace
13
+ * Displays input field with format hints
14
+ * @param value - Current input value (controlled by parent)
15
+ * @param error - Error message to display
16
+ * @returns Dialog component
17
+ * @example
18
+ * <AddMarketplaceDialog value={inputValue} />
19
+ */
20
+ export default function AddMarketplaceDialog({ value, error, }: AddMarketplaceDialogProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * AddMarketplaceDialog component
4
+ * Dialog for adding a new marketplace source
5
+ */
6
+ import { Box, Text } from 'ink';
7
+ /**
8
+ * Dialog for adding a new marketplace
9
+ * Displays input field with format hints
10
+ * @param value - Current input value (controlled by parent)
11
+ * @param error - Error message to display
12
+ * @returns Dialog component
13
+ * @example
14
+ * <AddMarketplaceDialog value={inputValue} />
15
+ */
16
+ export default function AddMarketplaceDialog({ value, error, }) {
17
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, marginTop: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Add Marketplace" }) }), _jsx(Text, { children: "Enter marketplace source:" }), _jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1, children: [value ? _jsx(Text, { children: value }) : _jsx(Text, { dimColor: true, children: "owner/repo" }), _jsx(Text, { color: "cyan", children: "\u258C" })] }), error && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", children: error }) })), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Supported formats:" }), _jsx(Text, { dimColor: true, children: " \u2022 owner/repo (GitHub)" }), _jsx(Text, { dimColor: true, children: " \u2022 https://github.com/org/repo" }), _jsx(Text, { dimColor: true, children: " \u2022 ./local-path" })] }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "[" }), _jsx(Text, { color: "red", children: "ESC" }), _jsx(Text, { dimColor: true, children: "] Cancel" })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "[" }), _jsx(Text, { color: "green", children: "Enter" }), _jsx(Text, { dimColor: true, children: "] Add" })] })] })] }));
18
+ }
@@ -0,0 +1,32 @@
1
+ /**
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)
5
+ */
6
+ import type { PluginComponents } from '../types/index.js';
7
+ /**
8
+ * Props for ComponentBadges
9
+ */
10
+ export interface ComponentBadgesProps {
11
+ /** Component counts/flags from plugin */
12
+ components: PluginComponents | undefined;
13
+ }
14
+ /**
15
+ * Displays component type badges for a plugin
16
+ * Only shows badges for components that exist
17
+ * @param components - PluginComponents object with counts/flags
18
+ * @returns Badges component or null if no components
19
+ * @example
20
+ * <ComponentBadges components={{ skills: 5, commands: 2 }} />
21
+ * // Renders: [S:5] [/:2]
22
+ */
23
+ export default function ComponentBadges({ components, }: ComponentBadgesProps): React.ReactNode;
24
+ /**
25
+ * Get a human-readable description of component types
26
+ * @param components - PluginComponents object
27
+ * @returns Formatted string describing components
28
+ * @example
29
+ * getComponentsDescription({ skills: 3, mcpServers: 1 })
30
+ * // => "3 skills, 1 MCP server"
31
+ */
32
+ export declare function getComponentsDescription(components: PluginComponents | undefined): string;
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ComponentBadges component
4
+ * Displays plugin component type badges with icons and counts
5
+ * Icons: Skills(S), Commands(/), Agents(@), Hooks(H), MCP(M), LSP(L)
6
+ */
7
+ import { Box, Text } from 'ink';
8
+ /**
9
+ * Badge configurations with intuitive abbreviations and colors
10
+ */
11
+ const BADGE_CONFIGS = [
12
+ { label: 'S', color: 'magenta', key: 'skills' },
13
+ { label: '/', color: 'cyan', key: 'commands' },
14
+ { label: '@', color: 'blue', key: 'agents' },
15
+ { label: 'H', color: 'yellow', key: 'hooks', isBoolean: true },
16
+ { label: 'M', color: 'green', key: 'mcpServers' },
17
+ { label: 'L', color: 'blueBright', key: 'lspServers' },
18
+ ];
19
+ /**
20
+ * Displays component type badges for a plugin
21
+ * Only shows badges for components that exist
22
+ * @param components - PluginComponents object with counts/flags
23
+ * @returns Badges component or null if no components
24
+ * @example
25
+ * <ComponentBadges components={{ skills: 5, commands: 2 }} />
26
+ * // Renders: [S:5] [/:2]
27
+ */
28
+ export default function ComponentBadges({ components, }) {
29
+ if (!components) {
30
+ return null;
31
+ }
32
+ const badges = BADGE_CONFIGS.filter((config) => {
33
+ const value = components[config.key];
34
+ if (config.isBoolean) {
35
+ return value === true;
36
+ }
37
+ return typeof value === 'number' && value > 0;
38
+ });
39
+ if (badges.length === 0) {
40
+ return null;
41
+ }
42
+ return (_jsx(Box, { gap: 1, flexWrap: "wrap", children: badges.map((config) => (_jsx(Badge, { label: config.label, count: config.isBoolean ? undefined : components[config.key], color: config.color }, config.key))) }));
43
+ }
44
+ /**
45
+ * Single badge component
46
+ */
47
+ function Badge({ label, count, color, }) {
48
+ return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "[" }), _jsx(Text, { color: color, bold: true, children: label }), count !== undefined && _jsxs(Text, { dimColor: true, children: [":", count] }), _jsx(Text, { dimColor: true, children: "]" })] }));
49
+ }
50
+ /**
51
+ * Get a human-readable description of component types
52
+ * @param components - PluginComponents object
53
+ * @returns Formatted string describing components
54
+ * @example
55
+ * getComponentsDescription({ skills: 3, mcpServers: 1 })
56
+ * // => "3 skills, 1 MCP server"
57
+ */
58
+ export function getComponentsDescription(components) {
59
+ if (!components) {
60
+ return '';
61
+ }
62
+ const parts = [];
63
+ if (components.skills) {
64
+ parts.push(`${components.skills} skill${components.skills > 1 ? 's' : ''}`);
65
+ }
66
+ if (components.commands) {
67
+ parts.push(`${components.commands} command${components.commands > 1 ? 's' : ''}`);
68
+ }
69
+ if (components.agents) {
70
+ parts.push(`${components.agents} agent${components.agents > 1 ? 's' : ''}`);
71
+ }
72
+ if (components.hooks) {
73
+ parts.push('hooks');
74
+ }
75
+ if (components.mcpServers) {
76
+ parts.push(`${components.mcpServers} MCP server${components.mcpServers > 1 ? 's' : ''}`);
77
+ }
78
+ if (components.lspServers) {
79
+ parts.push(`${components.lspServers} LSP server${components.lspServers > 1 ? 's' : ''}`);
80
+ }
81
+ return parts.join(', ');
82
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * HelpOverlay component
3
+ * Displays a full-screen overlay with all available keyboard shortcuts
4
+ */
5
+ interface HelpOverlayProps {
6
+ /** Whether the overlay is visible */
7
+ isVisible: boolean;
8
+ }
9
+ /**
10
+ * Full-screen help overlay showing all keyboard shortcuts
11
+ * @example
12
+ * <HelpOverlay isVisible={showHelp} />
13
+ */
14
+ export default function HelpOverlay({ isVisible }: HelpOverlayProps): import("react/jsx-runtime").JSX.Element | null;
15
+ export {};
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * HelpOverlay component
4
+ * Displays a full-screen overlay with all available keyboard shortcuts
5
+ */
6
+ import { Box, Text } from 'ink';
7
+ const helpSections = [
8
+ {
9
+ title: 'Navigation',
10
+ items: [
11
+ { key: '←/→, Tab', description: 'Switch tabs' },
12
+ { key: '↑/↓, ^P/^N', description: 'Navigate list' },
13
+ { key: '^F/^B', description: 'Switch tabs (Emacs)' },
14
+ ],
15
+ },
16
+ {
17
+ title: 'Actions',
18
+ items: [
19
+ { key: 'i, Enter', description: 'Install / Toggle plugin' },
20
+ { key: 'u', description: 'Uninstall plugin' },
21
+ { key: 'Space', description: 'Toggle enable/disable' },
22
+ { key: 's/S', description: 'Sort options / order' },
23
+ ],
24
+ },
25
+ {
26
+ title: 'Search',
27
+ items: [
28
+ { key: '/', description: 'Enter search mode' },
29
+ { key: 'Esc, ↓', description: 'Exit search mode' },
30
+ ],
31
+ },
32
+ {
33
+ title: 'General',
34
+ items: [
35
+ { key: 'q, ^C', description: 'Quit' },
36
+ { key: 'h', description: 'Toggle this help' },
37
+ ],
38
+ },
39
+ ];
40
+ /**
41
+ * Full-screen help overlay showing all keyboard shortcuts
42
+ * @example
43
+ * <HelpOverlay isVisible={showHelp} />
44
+ */
45
+ export default function HelpOverlay({ isVisible }) {
46
+ if (!isVisible) {
47
+ return null;
48
+ }
49
+ const keyWidth = 14;
50
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Help \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }) }), helpSections.map((section) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: section.title }), section.items.map((item, itemIndex) => (_jsxs(Box, { children: [_jsx(Box, { width: keyWidth, children: _jsx(Text, { color: "green", children: item.key.padEnd(keyWidth - 2) }) }), _jsx(Text, { dimColor: true, children: item.description })] }, itemIndex)))] }, section.title))), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press h or Esc to close" }) })] }));
51
+ }
@@ -2,18 +2,21 @@
2
2
  * KeyHints component
3
3
  * Displays keyboard shortcuts footer at the bottom of the dashboard
4
4
  */
5
+ import type { FocusZone } from '../types/index.js';
5
6
  interface KeyHintsProps {
6
7
  /** Additional context-specific hints */
7
8
  extraHints?: Array<{
8
9
  key: string;
9
10
  action: string;
10
11
  }>;
12
+ /** Current focus zone for context-aware hints */
13
+ focusZone?: FocusZone;
11
14
  }
12
15
  /**
13
16
  * Displays keyboard shortcut hints in the footer
14
17
  * @example
15
- * <KeyHints />
16
- * <KeyHints extraHints={[{ key: 'i', action: 'install' }]} />
18
+ * <KeyHints focusZone="list" />
19
+ * <KeyHints focusZone="search" extraHints={[{ key: 'i', action: 'install' }]} />
17
20
  */
18
- export default function KeyHints({ extraHints }: KeyHintsProps): import("react/jsx-runtime").JSX.Element;
21
+ export default function KeyHints({ extraHints, focusZone, }: KeyHintsProps): import("react/jsx-runtime").JSX.Element;
19
22
  export {};
@@ -4,20 +4,49 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * Displays keyboard shortcuts footer at the bottom of the dashboard
5
5
  */
6
6
  import { Box, Text } from 'ink';
7
+ /**
8
+ * Get base hints based on current focus zone
9
+ * @param focusZone - Current focus zone
10
+ * @returns Array of hint objects
11
+ */
12
+ function getBaseHints(focusZone) {
13
+ switch (focusZone) {
14
+ case 'tabbar':
15
+ return [
16
+ { key: '←/→', action: 'switch tabs' },
17
+ { key: '↓', action: 'search/list' },
18
+ { key: 'Tab', action: 'next tab' },
19
+ { key: 'h', action: 'help' },
20
+ { key: 'q or ^C', action: 'quit' },
21
+ ];
22
+ case 'search':
23
+ return [
24
+ { key: '↑', action: 'tabs' },
25
+ { key: '↓/Enter', action: 'list' },
26
+ { key: 'ESC', action: 'clear/exit' },
27
+ { key: 'h', action: 'help' },
28
+ { key: 'q or ^C', action: 'quit' },
29
+ ];
30
+ case 'list':
31
+ default:
32
+ return [
33
+ { key: '↑/↓', action: 'navigate' },
34
+ { key: '↑(top)', action: 'search' },
35
+ { key: 'Space', action: 'toggle' },
36
+ { key: 'Tab', action: 'next tab' },
37
+ { key: 'h', action: 'help' },
38
+ { key: 'q or ^C', action: 'quit' },
39
+ ];
40
+ }
41
+ }
7
42
  /**
8
43
  * Displays keyboard shortcut hints in the footer
9
44
  * @example
10
- * <KeyHints />
11
- * <KeyHints extraHints={[{ key: 'i', action: 'install' }]} />
45
+ * <KeyHints focusZone="list" />
46
+ * <KeyHints focusZone="search" extraHints={[{ key: 'i', action: 'install' }]} />
12
47
  */
13
- export default function KeyHints({ extraHints }) {
14
- const baseHints = [
15
- { key: '←/→', action: 'tabs' },
16
- { key: '↑/↓', action: 'navigate' },
17
- { key: 'Space', action: 'toggle' },
18
- { key: '/', action: 'search' },
19
- { key: 'q', action: 'quit' },
20
- ];
48
+ export default function KeyHints({ extraHints, focusZone = 'list', }) {
49
+ const baseHints = getBaseHints(focusZone);
21
50
  const allHints = extraHints ? [...baseHints, ...extraHints] : baseHints;
22
51
  return (_jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: _jsx(Box, { gap: 2, flexWrap: "wrap", children: allHints.map((hint, index) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: "white", children: hint.key }), _jsx(Text, { dimColor: true, children: hint.action })] }, index))) }) }));
23
52
  }
@@ -6,11 +6,13 @@ import type { Marketplace } from '../types/index.js';
6
6
  interface MarketplaceListProps {
7
7
  marketplaces: Marketplace[];
8
8
  selectedIndex: number;
9
+ /** Whether the list has keyboard focus */
10
+ isFocused?: boolean;
9
11
  }
10
12
  /**
11
13
  * Displays a list of marketplaces
12
14
  * @example
13
- * <MarketplaceList marketplaces={marketplaces} selectedIndex={0} />
15
+ * <MarketplaceList marketplaces={marketplaces} selectedIndex={0} isFocused={true} />
14
16
  */
15
- export default function MarketplaceList({ marketplaces, selectedIndex, }: MarketplaceListProps): import("react/jsx-runtime").JSX.Element;
17
+ export default function MarketplaceList({ marketplaces, selectedIndex, isFocused, }: MarketplaceListProps): import("react/jsx-runtime").JSX.Element;
16
18
  export {};
@@ -7,15 +7,19 @@ import { Box, Text } from 'ink';
7
7
  /**
8
8
  * Displays a list of marketplaces
9
9
  * @example
10
- * <MarketplaceList marketplaces={marketplaces} selectedIndex={0} />
10
+ * <MarketplaceList marketplaces={marketplaces} selectedIndex={0} isFocused={true} />
11
11
  */
12
- export default function MarketplaceList({ marketplaces, selectedIndex, }) {
12
+ export default function MarketplaceList({ marketplaces, selectedIndex, isFocused = true, }) {
13
13
  if (marketplaces.length === 0) {
14
14
  return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "No marketplaces found" }) }));
15
15
  }
16
16
  return (_jsx(Box, { flexDirection: "column", children: marketplaces.map((marketplace, index) => {
17
17
  const isSelected = index === selectedIndex;
18
- return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected ? _jsx(Text, { color: "cyan", children: '>' }) : _jsx(Text, { children: " " }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected ? 'cyan' : 'white', children: marketplace.name || marketplace.id }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [marketplace.pluginCount || 0, " plugins"] })] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: getSourceDisplay(marketplace) })] })] }, marketplace.id));
18
+ return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected && isFocused ? (_jsx(Text, { color: "cyan", children: '>' })) : isSelected ? (_jsx(Text, { color: "gray", children: '›' })) : (_jsx(Text, { children: " " })) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected && isFocused
19
+ ? 'cyan'
20
+ : isSelected
21
+ ? 'gray'
22
+ : 'white', children: marketplace.name || marketplace.id }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [marketplace.pluginCount || 0, " plugins"] })] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: getSourceDisplay(marketplace) })] })] }, marketplace.id));
19
23
  }) }));
20
24
  }
21
25
  /**
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  */
6
6
  import { Box, Text } from 'ink';
7
7
  import StatusIcon from './StatusIcon.js';
8
+ import ComponentBadges from './ComponentBadges.js';
8
9
  /**
9
10
  * Displays detailed information about a selected plugin
10
11
  * @example
@@ -14,7 +15,7 @@ export default function PluginDetail({ plugin }) {
14
15
  if (!plugin) {
15
16
  return (_jsx(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: "Select a plugin to view details" }) }));
16
17
  }
17
- return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }), _jsx(Text, { bold: true, color: "cyan", children: plugin.name })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "wrap", children: plugin.description || 'No description available' }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(DetailRow, { label: "Marketplace", value: plugin.marketplace }), _jsx(DetailRow, { label: "Version", value: plugin.version }), _jsx(DetailRow, { label: "Installs", value: formatCount(plugin.installCount) }), plugin.category && (_jsx(DetailRow, { label: "Category", value: plugin.category })), plugin.author && (_jsx(DetailRow, { label: "Author", value: plugin.author.name })), plugin.homepage && (_jsx(DetailRow, { label: "Homepage", value: plugin.homepage }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Status:" }), plugin.isInstalled ? (plugin.isEnabled ? (_jsx(Text, { color: "green", bold: true, children: "Installed & Enabled" })) : (_jsx(Text, { color: "yellow", bold: true, children: "Installed & Disabled" }))) : (_jsx(Text, { color: "gray", children: "Not Installed" }))] }), plugin.installedAt && (_jsxs(Text, { dimColor: true, children: ["Installed: ", formatDate(plugin.installedAt)] }))] }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "column", children: plugin.isInstalled ? (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "Space" }), ' ', plugin.isEnabled ? 'disable' : 'enable', " |", ' ', _jsx(Text, { bold: true, color: "white", children: "u" }), ' ', "uninstall"] }) })) : (_jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "i" }), ' ', "install"] })) })] }));
18
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }), _jsx(Text, { bold: true, color: "cyan", children: plugin.name })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "wrap", children: plugin.description || 'No description available' }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(DetailRow, { label: "Marketplace", value: plugin.marketplace }), _jsx(DetailRow, { label: "Version", value: plugin.version }), _jsx(DetailRow, { label: "Installs", value: formatCount(plugin.installCount) }), plugin.category && (_jsx(DetailRow, { label: "Category", value: plugin.category })), plugin.author && (_jsx(DetailRow, { label: "Author", value: plugin.author.name })), plugin.homepage && (_jsx(DetailRow, { label: "Homepage", value: plugin.homepage })), plugin.components && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "gray", children: "Components:" }), _jsx(ComponentBadges, { components: plugin.components })] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Status:" }), plugin.isInstalled ? (plugin.isEnabled ? (_jsx(Text, { color: "green", bold: true, children: "Installed & Enabled" })) : (_jsx(Text, { color: "yellow", bold: true, children: "Installed & Disabled" }))) : (_jsx(Text, { color: "gray", children: "Not Installed" }))] }), plugin.installedAt && (_jsxs(Text, { dimColor: true, children: ["Installed: ", formatDate(plugin.installedAt)] }))] }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "column", children: plugin.isInstalled ? (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "Space" }), ' ', plugin.isEnabled ? 'disable' : 'enable', " |", ' ', _jsx(Text, { bold: true, color: "white", children: "u" }), ' ', "uninstall"] }) })) : (_jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "i" }), ' ', "install"] })) })] }));
18
19
  }
19
20
  /**
20
21
  * Single detail row with label and value
@@ -9,11 +9,38 @@ interface PluginListProps {
9
9
  selectedIndex: number;
10
10
  /** Maximum visible items (for virtual scrolling) */
11
11
  visibleCount?: number;
12
+ /** Whether the list has keyboard focus */
13
+ isFocused?: boolean;
12
14
  }
13
15
  /**
14
16
  * Scrollable plugin list with selection
15
17
  * @example
16
- * <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} />
18
+ * <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} isFocused={true} />
17
19
  */
18
- export default function PluginList({ plugins, selectedIndex, visibleCount, }: PluginListProps): import("react/jsx-runtime").JSX.Element;
20
+ export default function PluginList({ plugins, selectedIndex, visibleCount, isFocused, }: PluginListProps): import("react/jsx-runtime").JSX.Element;
21
+ /**
22
+ * Truncate text to max length with ellipsis
23
+ * @param text - The text to truncate
24
+ * @param maxLength - Maximum length including ellipsis
25
+ * @returns
26
+ * - Original text if within maxLength
27
+ * - Truncated text with "..." suffix if exceeds maxLength
28
+ * @example
29
+ * truncate('Hello', 10) // => 'Hello'
30
+ * truncate('Hello World', 8) // => 'Hello...'
31
+ */
32
+ export declare function truncate(text: string, maxLength: number): string;
33
+ /**
34
+ * Format large numbers with K/M suffix
35
+ * @param count - The number to format
36
+ * @returns
37
+ * - "X.XM" for millions (>= 1,000,000)
38
+ * - "X.XK" for thousands (>= 1,000)
39
+ * - String representation for smaller numbers
40
+ * @example
41
+ * formatCount(1500) // => '1.5K'
42
+ * formatCount(1200000) // => '1.2M'
43
+ * formatCount(500) // => '500'
44
+ */
45
+ export declare function formatCount(count: number): string;
19
46
  export {};
@@ -9,9 +9,9 @@ import StatusIcon from './StatusIcon.js';
9
9
  /**
10
10
  * Scrollable plugin list with selection
11
11
  * @example
12
- * <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} />
12
+ * <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} isFocused={true} />
13
13
  */
14
- export default function PluginList({ plugins, selectedIndex, visibleCount = 15, }) {
14
+ export default function PluginList({ plugins, selectedIndex, visibleCount = 15, isFocused = true, }) {
15
15
  if (plugins.length === 0) {
16
16
  return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "No plugins found" }) }));
17
17
  }
@@ -29,21 +29,42 @@ export default function PluginList({ plugins, selectedIndex, visibleCount = 15,
29
29
  return (_jsxs(Box, { flexDirection: "column", children: [hasPrevious && _jsxs(Text, { dimColor: true, children: ["\u2191 ", startIndex, " more above"] }), visiblePlugins.map((plugin, index) => {
30
30
  const actualIndex = startIndex + index;
31
31
  const isSelected = actualIndex === selectedIndex;
32
- return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected ? _jsx(Text, { color: "cyan", children: '>' }) : _jsx(Text, { children: " " }) }), _jsx(Box, { width: 2, children: _jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected ? 'cyan' : 'white', children: plugin.name }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { color: "gray", children: truncate(plugin.marketplace, 20) }), plugin.installCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [formatCount(plugin.installCount), " installs"] })] }))] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: truncate(plugin.description, 60) })] })] }, plugin.id));
32
+ return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected && isFocused ? (_jsx(Text, { color: "cyan", children: '>' })) : isSelected ? (_jsx(Text, { color: "gray", children: '›' })) : (_jsx(Text, { children: " " })) }), _jsx(Box, { width: 2, children: _jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected && isFocused
33
+ ? 'cyan'
34
+ : isSelected
35
+ ? 'gray'
36
+ : 'white', children: plugin.name }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { color: "gray", children: truncate(plugin.marketplace, 20) }), plugin.installCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [formatCount(plugin.installCount), " installs"] })] }))] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: truncate(plugin.description, 60) })] })] }, plugin.id));
33
37
  }), hasMore && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", plugins.length - endIndex, " more below"] }))] }));
34
38
  }
35
39
  /**
36
40
  * Truncate text to max length with ellipsis
41
+ * @param text - The text to truncate
42
+ * @param maxLength - Maximum length including ellipsis
43
+ * @returns
44
+ * - Original text if within maxLength
45
+ * - Truncated text with "..." suffix if exceeds maxLength
46
+ * @example
47
+ * truncate('Hello', 10) // => 'Hello'
48
+ * truncate('Hello World', 8) // => 'Hello...'
37
49
  */
38
- function truncate(text, maxLength) {
50
+ export function truncate(text, maxLength) {
39
51
  if (text.length <= maxLength)
40
52
  return text;
41
53
  return text.slice(0, maxLength - 3) + '...';
42
54
  }
43
55
  /**
44
56
  * Format large numbers with K/M suffix
57
+ * @param count - The number to format
58
+ * @returns
59
+ * - "X.XM" for millions (>= 1,000,000)
60
+ * - "X.XK" for thousands (>= 1,000)
61
+ * - String representation for smaller numbers
62
+ * @example
63
+ * formatCount(1500) // => '1.5K'
64
+ * formatCount(1200000) // => '1.2M'
65
+ * formatCount(500) // => '500'
45
66
  */
46
- function formatCount(count) {
67
+ export function formatCount(count) {
47
68
  if (count >= 1000000) {
48
69
  return `${(count / 1000000).toFixed(1)}M`;
49
70
  }
@@ -10,5 +10,5 @@ import { Box, Text } from 'ink';
10
10
  * <SearchInput query={searchQuery} isActive={isSearchMode} />
11
11
  */
12
12
  export default function SearchInput({ query, isActive = false, placeholder = 'Type to search...', }) {
13
- return (_jsxs(Box, { borderStyle: isActive ? 'round' : 'single', borderColor: isActive ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(Text, { color: isActive ? 'cyan' : 'gray', children: "Q " }), query ? _jsx(Text, { children: query }) : _jsx(Text, { dimColor: true, children: placeholder }), isActive && _jsx(Text, { color: "cyan", children: "\u258C" })] }));
13
+ return (_jsxs(Box, { borderStyle: isActive ? 'round' : 'single', borderColor: isActive ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(Text, { color: isActive ? 'cyan' : 'gray', children: "\uD83D\uDD0D " }), query ? _jsx(Text, { children: query }) : _jsx(Text, { dimColor: true, children: placeholder }), isActive && _jsx(Text, { color: "cyan", children: "\u258C" })] }));
14
14
  }
@@ -3,17 +3,19 @@
3
3
  * Horizontal tab navigation for the dashboard
4
4
  * Supports ← → arrow key navigation
5
5
  */
6
- type Tab = 'discover' | 'installed' | 'marketplaces' | 'errors';
6
+ type Tab = 'enabled' | 'installed' | 'discover' | 'marketplaces' | 'errors';
7
7
  interface TabBarProps {
8
8
  activeTab: Tab;
9
9
  onTabChange?: (tab: Tab) => void;
10
+ /** Whether the tab bar has keyboard focus */
11
+ isFocused?: boolean;
10
12
  }
11
13
  /**
12
14
  * Horizontal tab bar component
13
15
  * @example
14
- * <TabBar activeTab="discover" onTabChange={setActiveTab} />
16
+ * <TabBar activeTab="discover" isFocused={true} />
15
17
  */
16
- export default function TabBar({ activeTab }: TabBarProps): import("react/jsx-runtime").JSX.Element;
18
+ export default function TabBar({ activeTab, isFocused }: TabBarProps): import("react/jsx-runtime").JSX.Element;
17
19
  /**
18
20
  * Get the next tab in the cycle
19
21
  * @param currentTab - Current active tab
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * TabBar component
4
4
  * Horizontal tab navigation for the dashboard
@@ -6,21 +6,33 @@ import { jsx as _jsx } from "react/jsx-runtime";
6
6
  */
7
7
  import { Box, Text } from 'ink';
8
8
  const TABS = [
9
- { id: 'discover', label: 'Discover' },
9
+ { id: 'enabled', label: 'Enabled' },
10
10
  { id: 'installed', label: 'Installed' },
11
+ { id: 'discover', label: 'Discover' },
11
12
  { id: 'marketplaces', label: 'Marketplaces' },
12
13
  { id: 'errors', label: 'Errors' },
13
14
  ];
15
+ /** Color constants for consistent theming */
16
+ const COLORS = {
17
+ /** Background color when tab bar is focused */
18
+ FOCUS_BG: '#1a3a4a',
19
+ /** Background color for active tab (not focused) */
20
+ ACTIVE_BG: '#333333',
21
+ /** Foreground color for active/focused elements */
22
+ ACTIVE_FG: 'cyan',
23
+ /** Foreground color for inactive elements */
24
+ INACTIVE_FG: 'gray',
25
+ };
14
26
  /**
15
27
  * Horizontal tab bar component
16
28
  * @example
17
- * <TabBar activeTab="discover" onTabChange={setActiveTab} />
29
+ * <TabBar activeTab="discover" isFocused={true} />
18
30
  */
19
- export default function TabBar({ activeTab }) {
20
- return (_jsx(Box, { gap: 2, marginBottom: 1, children: TABS.map((tab) => {
21
- const isActive = tab.id === activeTab;
22
- return (_jsx(Box, { children: isActive ? (_jsx(Text, { bold: true, color: "cyan", backgroundColor: "#333333", children: ` ${tab.label} ` })) : (_jsx(Text, { color: "gray", children: ` ${tab.label} ` })) }, tab.id));
23
- }) }));
31
+ export default function TabBar({ activeTab, isFocused = false }) {
32
+ return (_jsxs(Box, { gap: 2, marginBottom: 1, children: [isFocused && _jsx(Text, { color: COLORS.ACTIVE_FG, children: "\u25B6" }), TABS.map((tab) => {
33
+ const isActive = tab.id === activeTab;
34
+ return (_jsx(Box, { children: isActive ? (_jsx(Text, { bold: true, color: COLORS.ACTIVE_FG, backgroundColor: isFocused ? COLORS.FOCUS_BG : COLORS.ACTIVE_BG, children: isFocused ? `[${tab.label}]` : ` ${tab.label} ` })) : (_jsx(Text, { color: COLORS.INACTIVE_FG, children: ` ${tab.label} ` })) }, tab.id));
35
+ })] }));
24
36
  }
25
37
  /**
26
38
  * Get the next tab in the cycle
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Component service for detecting plugin component types
3
+ * Parses plugin.json and scans plugin directory structure to identify
4
+ * skills, commands, agents, hooks, MCP servers, and LSP servers
5
+ */
6
+ import type { PluginComponents } from '../types/index.js';
7
+ /**
8
+ * Detect all component types for a plugin at the given install path
9
+ * @param installPath - Absolute path to the installed plugin directory
10
+ * @returns PluginComponents object with detected component counts
11
+ * - Returns undefined values for components that are not present
12
+ * - Returns counts > 0 for components that exist
13
+ * @example
14
+ * const components = detectPluginComponents('/path/to/plugin')
15
+ * // => { skills: 5, commands: 2, mcpServers: 1 }
16
+ */
17
+ export declare function detectPluginComponents(installPath: string): PluginComponents | undefined;
18
+ /**
19
+ * Check if a plugin has any components
20
+ * @param components - PluginComponents object
21
+ * @returns true if at least one component type is present
22
+ * @example
23
+ * hasAnyComponents({ skills: 2 }) // => true
24
+ * hasAnyComponents({}) // => false
25
+ * hasAnyComponents(undefined) // => false
26
+ */
27
+ export declare function hasAnyComponents(components: PluginComponents | undefined): boolean;
28
+ /**
29
+ * Get total component count for a plugin
30
+ * @param components - PluginComponents object
31
+ * @returns Total number of components (hooks count as 1)
32
+ * @example
33
+ * getTotalComponentCount({ skills: 3, commands: 2, hooks: true }) // => 6
34
+ */
35
+ export declare function getTotalComponentCount(components: PluginComponents | undefined): number;