@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,178 @@
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 * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import { readJsonFile, directoryExists, fileExists } from './fileService.js';
9
+ /**
10
+ * Detect all component types for a plugin at the given install path
11
+ * @param installPath - Absolute path to the installed plugin directory
12
+ * @returns PluginComponents object with detected component counts
13
+ * - Returns undefined values for components that are not present
14
+ * - Returns counts > 0 for components that exist
15
+ * @example
16
+ * const components = detectPluginComponents('/path/to/plugin')
17
+ * // => { skills: 5, commands: 2, mcpServers: 1 }
18
+ */
19
+ export function detectPluginComponents(installPath) {
20
+ if (!directoryExists(installPath)) {
21
+ return undefined;
22
+ }
23
+ const components = {};
24
+ // Detect skills (count directories in skills/ folder)
25
+ const skillsCount = countSkills(installPath);
26
+ if (skillsCount > 0) {
27
+ components.skills = skillsCount;
28
+ }
29
+ // Detect commands (count .md files in commands/ folder)
30
+ const commandsCount = countMarkdownFiles(installPath, 'commands');
31
+ if (commandsCount > 0) {
32
+ components.commands = commandsCount;
33
+ }
34
+ // Detect agents (count .md files in agents/ folder)
35
+ const agentsCount = countMarkdownFiles(installPath, 'agents');
36
+ if (agentsCount > 0) {
37
+ components.agents = agentsCount;
38
+ }
39
+ // Detect hooks
40
+ const hasHooks = detectHooks(installPath);
41
+ if (hasHooks) {
42
+ components.hooks = true;
43
+ }
44
+ // Detect MCP servers from plugin.json
45
+ const mcpCount = countMcpServers(installPath);
46
+ if (mcpCount > 0) {
47
+ components.mcpServers = mcpCount;
48
+ }
49
+ // Detect LSP servers from .lsp.json
50
+ const lspCount = countLspServers(installPath);
51
+ if (lspCount > 0) {
52
+ components.lspServers = lspCount;
53
+ }
54
+ // Return undefined if no components detected
55
+ if (Object.keys(components).length === 0) {
56
+ return undefined;
57
+ }
58
+ return components;
59
+ }
60
+ /**
61
+ * Count skill directories in the skills/ folder
62
+ * Skills are stored as subdirectories with SKILL.md files
63
+ * @param installPath - Plugin install path
64
+ * @returns Number of skill directories
65
+ */
66
+ function countSkills(installPath) {
67
+ const skillsPath = path.join(installPath, 'skills');
68
+ if (!directoryExists(skillsPath)) {
69
+ return 0;
70
+ }
71
+ try {
72
+ const entries = fs.readdirSync(skillsPath, { withFileTypes: true });
73
+ return entries.filter((entry) => entry.isDirectory()).length;
74
+ }
75
+ catch {
76
+ return 0;
77
+ }
78
+ }
79
+ /**
80
+ * Count .md files in a specific directory
81
+ * @param installPath - Plugin install path
82
+ * @param subdir - Subdirectory name ('commands' or 'agents')
83
+ * @returns Number of .md files
84
+ */
85
+ function countMarkdownFiles(installPath, subdir) {
86
+ const dirPath = path.join(installPath, subdir);
87
+ if (!directoryExists(dirPath)) {
88
+ return 0;
89
+ }
90
+ try {
91
+ const files = fs.readdirSync(dirPath);
92
+ return files.filter((file) => file.endsWith('.md')).length;
93
+ }
94
+ catch {
95
+ return 0;
96
+ }
97
+ }
98
+ /**
99
+ * Detect if plugin has hooks configured
100
+ * Checks for hooks/ directory or hooks.json file
101
+ * @param installPath - Plugin install path
102
+ * @returns true if hooks are configured
103
+ */
104
+ function detectHooks(installPath) {
105
+ const hooksDir = path.join(installPath, 'hooks');
106
+ const hooksJson = path.join(installPath, 'hooks.json');
107
+ return directoryExists(hooksDir) || fileExists(hooksJson);
108
+ }
109
+ /**
110
+ * Count MCP servers defined in plugin.json
111
+ * @param installPath - Plugin install path
112
+ * @returns Number of MCP server configurations
113
+ */
114
+ function countMcpServers(installPath) {
115
+ // Check both .claude-plugin/plugin.json and plugin.json at root
116
+ const pluginJsonPaths = [
117
+ path.join(installPath, '.claude-plugin', 'plugin.json'),
118
+ path.join(installPath, 'plugin.json'),
119
+ ];
120
+ for (const pluginJsonPath of pluginJsonPaths) {
121
+ const pluginJson = readJsonFile(pluginJsonPath);
122
+ if (pluginJson?.mcpServers) {
123
+ return Object.keys(pluginJson.mcpServers).length;
124
+ }
125
+ }
126
+ return 0;
127
+ }
128
+ /**
129
+ * Count LSP servers defined in .lsp.json
130
+ * @param installPath - Plugin install path
131
+ * @returns Number of LSP server configurations
132
+ */
133
+ function countLspServers(installPath) {
134
+ const lspJsonPath = path.join(installPath, '.lsp.json');
135
+ const lspConfig = readJsonFile(lspJsonPath);
136
+ if (!lspConfig) {
137
+ return 0;
138
+ }
139
+ return Object.keys(lspConfig).length;
140
+ }
141
+ /**
142
+ * Check if a plugin has any components
143
+ * @param components - PluginComponents object
144
+ * @returns true if at least one component type is present
145
+ * @example
146
+ * hasAnyComponents({ skills: 2 }) // => true
147
+ * hasAnyComponents({}) // => false
148
+ * hasAnyComponents(undefined) // => false
149
+ */
150
+ export function hasAnyComponents(components) {
151
+ if (!components) {
152
+ return false;
153
+ }
154
+ return ((components.skills ?? 0) > 0 ||
155
+ (components.commands ?? 0) > 0 ||
156
+ (components.agents ?? 0) > 0 ||
157
+ components.hooks === true ||
158
+ (components.mcpServers ?? 0) > 0 ||
159
+ (components.lspServers ?? 0) > 0);
160
+ }
161
+ /**
162
+ * Get total component count for a plugin
163
+ * @param components - PluginComponents object
164
+ * @returns Total number of components (hooks count as 1)
165
+ * @example
166
+ * getTotalComponentCount({ skills: 3, commands: 2, hooks: true }) // => 6
167
+ */
168
+ export function getTotalComponentCount(components) {
169
+ if (!components) {
170
+ return 0;
171
+ }
172
+ return ((components.skills ?? 0) +
173
+ (components.commands ?? 0) +
174
+ (components.agents ?? 0) +
175
+ (components.hooks ? 1 : 0) +
176
+ (components.mcpServers ?? 0) +
177
+ (components.lspServers ?? 0));
178
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Marketplace actions service for add/remove/update operations
3
+ * Executes `claude plugin marketplace <action>` as subprocess
4
+ */
5
+ /**
6
+ * Result of a marketplace CLI action
7
+ */
8
+ export interface MarketplaceActionResult {
9
+ success: boolean;
10
+ message: string;
11
+ error?: string;
12
+ }
13
+ /**
14
+ * Add a new marketplace via Claude CLI
15
+ * @param source - Marketplace source (e.g., "owner/repo", "https://...", "./local-path")
16
+ * @returns Promise resolving to action result
17
+ * @example
18
+ * // GitHub shorthand
19
+ * addMarketplace('anthropics/claude-plugins')
20
+ * // Git URL
21
+ * addMarketplace('https://github.com/org/plugins.git')
22
+ * // Local path
23
+ * addMarketplace('./my-marketplace')
24
+ */
25
+ export declare function addMarketplace(source: string): Promise<MarketplaceActionResult>;
26
+ /**
27
+ * Remove an existing marketplace via Claude CLI
28
+ * @param name - Marketplace name/identifier to remove
29
+ * @returns Promise resolving to action result
30
+ * @example
31
+ * removeMarketplace('my-marketplace')
32
+ */
33
+ export declare function removeMarketplace(name: string): Promise<MarketplaceActionResult>;
34
+ /**
35
+ * Update marketplace catalog(s) via Claude CLI
36
+ * @param name - Optional marketplace name. If omitted, updates all marketplaces.
37
+ * @returns Promise resolving to action result
38
+ * @example
39
+ * // Update specific marketplace
40
+ * updateMarketplace('claude-plugins-official')
41
+ * // Update all marketplaces
42
+ * updateMarketplace()
43
+ */
44
+ export declare function updateMarketplace(name?: string): Promise<MarketplaceActionResult>;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Marketplace actions service for add/remove/update operations
3
+ * Executes `claude plugin marketplace <action>` as subprocess
4
+ */
5
+ import { spawn } from 'node:child_process';
6
+ /**
7
+ * Add a new marketplace via Claude CLI
8
+ * @param source - Marketplace source (e.g., "owner/repo", "https://...", "./local-path")
9
+ * @returns Promise resolving to action result
10
+ * @example
11
+ * // GitHub shorthand
12
+ * addMarketplace('anthropics/claude-plugins')
13
+ * // Git URL
14
+ * addMarketplace('https://github.com/org/plugins.git')
15
+ * // Local path
16
+ * addMarketplace('./my-marketplace')
17
+ */
18
+ export function addMarketplace(source) {
19
+ return executeMarketplaceCommand(['plugin', 'marketplace', 'add', source], `Added marketplace: ${source}`, `Failed to add marketplace: ${source}`);
20
+ }
21
+ /**
22
+ * Remove an existing marketplace via Claude CLI
23
+ * @param name - Marketplace name/identifier to remove
24
+ * @returns Promise resolving to action result
25
+ * @example
26
+ * removeMarketplace('my-marketplace')
27
+ */
28
+ export function removeMarketplace(name) {
29
+ return executeMarketplaceCommand(['plugin', 'marketplace', 'remove', name], `Removed marketplace: ${name}`, `Failed to remove marketplace: ${name}`);
30
+ }
31
+ /**
32
+ * Update marketplace catalog(s) via Claude CLI
33
+ * @param name - Optional marketplace name. If omitted, updates all marketplaces.
34
+ * @returns Promise resolving to action result
35
+ * @example
36
+ * // Update specific marketplace
37
+ * updateMarketplace('claude-plugins-official')
38
+ * // Update all marketplaces
39
+ * updateMarketplace()
40
+ */
41
+ export function updateMarketplace(name) {
42
+ const args = ['plugin', 'marketplace', 'update'];
43
+ if (name) {
44
+ args.push(name);
45
+ }
46
+ return executeMarketplaceCommand(args, name ? `Updated ${name}` : 'Updated all marketplaces', `Failed to update ${name || 'marketplaces'}`);
47
+ }
48
+ /**
49
+ * Execute a marketplace CLI command with generic args and messages
50
+ * @param args - CLI arguments to pass to claude command
51
+ * @param successMessage - Message to return on success
52
+ * @param failureMessage - Message to return on failure
53
+ * @returns Promise resolving to action result
54
+ */
55
+ function executeMarketplaceCommand(args, successMessage, failureMessage) {
56
+ return new Promise((resolve) => {
57
+ const child = spawn('claude', args, {
58
+ stdio: ['ignore', 'pipe', 'pipe'],
59
+ shell: false,
60
+ });
61
+ let stdout = '';
62
+ let stderr = '';
63
+ child.stdout?.on('data', (data) => {
64
+ stdout += data.toString();
65
+ });
66
+ child.stderr?.on('data', (data) => {
67
+ stderr += data.toString();
68
+ });
69
+ child.on('close', (code) => {
70
+ if (code === 0) {
71
+ resolve({
72
+ success: true,
73
+ message: successMessage,
74
+ });
75
+ }
76
+ else {
77
+ resolve({
78
+ success: false,
79
+ message: failureMessage,
80
+ error: stderr || stdout || `Exit code: ${code}`,
81
+ });
82
+ }
83
+ });
84
+ child.on('error', (err) => {
85
+ resolve({
86
+ success: false,
87
+ message: 'Failed to execute claude command',
88
+ error: err.message,
89
+ });
90
+ });
91
+ });
92
+ }
@@ -54,6 +54,16 @@ export declare function searchPlugins(query: string, plugins?: Plugin[]): Plugin
54
54
  * @returns Sorted plugins array
55
55
  */
56
56
  export declare function sortPlugins(plugins: Plugin[], sortBy: 'installs' | 'name' | 'date', order: 'asc' | 'desc'): Plugin[];
57
+ /**
58
+ * Search marketplaces by query
59
+ * Filters marketplaces by name, id, and source URL/repo
60
+ * @param query - Search query
61
+ * @param marketplaces - Marketplaces to search
62
+ * @returns Filtered marketplaces matching the query
63
+ * @example
64
+ * searchMarketplaces('official', marketplaces) // => marketplaces with 'official' in name/id
65
+ */
66
+ export declare function searchMarketplaces(query: string, marketplaces: Marketplace[]): Marketplace[];
57
67
  /**
58
68
  * Get plugin statistics
59
69
  * @returns Object with various plugin counts
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { readJsonFile, directoryExists, listDirectories, } from './fileService.js';
6
6
  import { getEnabledPlugins } from './settingsService.js';
7
+ import { detectPluginComponents } from './componentService.js';
7
8
  import { PATHS, getMarketplaceJsonPath } from '../utils/paths.js';
8
9
  /**
9
10
  * Load all plugins from all marketplaces
@@ -44,6 +45,10 @@ export function loadAllPlugins() {
44
45
  for (const plugin of manifest.plugins) {
45
46
  const pluginId = `${plugin.name}@${marketplace}`;
46
47
  const installedEntry = installedMap.get(pluginId);
48
+ // Detect components for installed plugins
49
+ const components = installedEntry
50
+ ? detectPluginComponents(installedEntry.installPath)
51
+ : undefined;
47
52
  plugins.push({
48
53
  id: pluginId,
49
54
  name: plugin.name,
@@ -61,6 +66,7 @@ export function loadAllPlugins() {
61
66
  tags: plugin.tags || plugin.keywords,
62
67
  isLocal: installedEntry?.isLocal,
63
68
  gitCommitSha: installedEntry?.gitCommitSha,
69
+ components,
64
70
  });
65
71
  }
66
72
  }
@@ -172,6 +178,22 @@ export function sortPlugins(plugins, sortBy, order) {
172
178
  });
173
179
  return sorted;
174
180
  }
181
+ /**
182
+ * Search marketplaces by query
183
+ * Filters marketplaces by name, id, and source URL/repo
184
+ * @param query - Search query
185
+ * @param marketplaces - Marketplaces to search
186
+ * @returns Filtered marketplaces matching the query
187
+ * @example
188
+ * searchMarketplaces('official', marketplaces) // => marketplaces with 'official' in name/id
189
+ */
190
+ export function searchMarketplaces(query, marketplaces) {
191
+ const lowerQuery = query.toLowerCase();
192
+ return marketplaces.filter((m) => m.name.toLowerCase().includes(lowerQuery) ||
193
+ m.id.toLowerCase().includes(lowerQuery) ||
194
+ m.source.url?.toLowerCase().includes(lowerQuery) ||
195
+ m.source.repo?.toLowerCase().includes(lowerQuery));
196
+ }
175
197
  /**
176
198
  * Get plugin statistics
177
199
  * @returns Object with various plugin counts
@@ -2,14 +2,15 @@
2
2
  * DiscoverTab component
3
3
  * Browse all available plugins from all marketplaces
4
4
  */
5
- import type { Plugin, AppState } from '../types/index.js';
5
+ import type { Plugin, AppState, FocusZone } from '../types/index.js';
6
6
  interface DiscoverTabProps {
7
7
  plugins: Plugin[];
8
8
  selectedIndex: number;
9
9
  searchQuery: string;
10
10
  sortBy: AppState['sortBy'];
11
11
  sortOrder: AppState['sortOrder'];
12
- isSearchMode?: boolean;
12
+ /** Current focus zone for keyboard navigation */
13
+ focusZone?: FocusZone;
13
14
  }
14
15
  /**
15
16
  * Discover tab - browse all plugins
@@ -20,7 +21,8 @@ interface DiscoverTabProps {
20
21
  * searchQuery={state.searchQuery}
21
22
  * sortBy={state.sortBy}
22
23
  * sortOrder={state.sortOrder}
24
+ * focusZone="list"
23
25
  * />
24
26
  */
25
- export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder, isSearchMode, }: DiscoverTabProps): import("react/jsx-runtime").JSX.Element;
27
+ export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder, focusZone, }: DiscoverTabProps): import("react/jsx-runtime").JSX.Element;
26
28
  export {};
@@ -17,9 +17,10 @@ import SortDropdown from '../components/SortDropdown.js';
17
17
  * searchQuery={state.searchQuery}
18
18
  * sortBy={state.sortBy}
19
19
  * sortOrder={state.sortOrder}
20
+ * focusZone="list"
20
21
  * />
21
22
  */
22
- export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder, isSearchMode = false, }) {
23
+ export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder, focusZone = 'list', }) {
23
24
  const selectedPlugin = plugins[selectedIndex] ?? null;
24
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Discover plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsx(SortDropdown, { sortBy: sortBy, sortOrder: sortOrder })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: isSearchMode, placeholder: "Type to search..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12 }) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
25
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Discover plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsx(SortDropdown, { sortBy: sortBy, sortOrder: sortOrder })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' }) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
25
26
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * EnabledTab component
3
+ * View and manage enabled plugins (installed + enabled)
4
+ */
5
+ import type { Plugin, FocusZone } from '../types/index.js';
6
+ interface EnabledTabProps {
7
+ plugins: Plugin[];
8
+ selectedIndex: number;
9
+ searchQuery?: string;
10
+ /** Current focus zone for keyboard navigation */
11
+ focusZone?: FocusZone;
12
+ }
13
+ /**
14
+ * Enabled tab - view currently active plugins
15
+ * @param plugins - Filtered enabled plugins (search already applied by parent)
16
+ * @param selectedIndex - Currently selected item index
17
+ * @param searchQuery - Current search query string
18
+ * @param focusZone - Current focus zone for keyboard navigation
19
+ * @returns Enabled tab component
20
+ * @example
21
+ * <EnabledTab plugins={enabledPlugins} selectedIndex={0} searchQuery="" focusZone="list" />
22
+ */
23
+ export default function EnabledTab({ plugins, selectedIndex, searchQuery, focusZone, }: EnabledTabProps): import("react/jsx-runtime").JSX.Element;
24
+ export {};
@@ -0,0 +1,26 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * EnabledTab component
4
+ * View and manage enabled plugins (installed + enabled)
5
+ */
6
+ import { Box, Text } from 'ink';
7
+ import PluginList from '../components/PluginList.js';
8
+ import PluginDetail from '../components/PluginDetail.js';
9
+ import SearchInput from '../components/SearchInput.js';
10
+ /**
11
+ * Enabled tab - view currently active plugins
12
+ * @param plugins - Filtered enabled plugins (search already applied by parent)
13
+ * @param selectedIndex - Currently selected item index
14
+ * @param searchQuery - Current search query string
15
+ * @param focusZone - Current focus zone for keyboard navigation
16
+ * @returns Enabled tab component
17
+ * @example
18
+ * <EnabledTab plugins={enabledPlugins} selectedIndex={0} searchQuery="" focusZone="list" />
19
+ */
20
+ export default function EnabledTab({ plugins, selectedIndex, searchQuery = '', focusZone = 'list', }) {
21
+ // Plugins are already filtered by parent, use directly
22
+ const selectedPlugin = plugins[selectedIndex] ?? null;
23
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Enabled plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, children: "Currently active in Claude Code" })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search enabled plugins..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: plugins.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery ? 'No matching plugins' : 'No enabled plugins' }), _jsx(Text, { dimColor: true, children: searchQuery
24
+ ? 'Try a different search term'
25
+ : 'Enable plugins in the Installed tab or use /plugin enable' })] })) : (_jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
26
+ }
@@ -2,15 +2,22 @@
2
2
  * InstalledTab component
3
3
  * View and manage installed plugins
4
4
  */
5
- import type { Plugin } from '../types/index.js';
5
+ import type { Plugin, FocusZone } from '../types/index.js';
6
6
  interface InstalledTabProps {
7
7
  plugins: Plugin[];
8
8
  selectedIndex: number;
9
+ searchQuery?: string;
10
+ /** Current focus zone for keyboard navigation */
11
+ focusZone?: FocusZone;
9
12
  }
10
13
  /**
11
14
  * Installed tab - manage installed plugins
15
+ * @param plugins - Filtered installed plugins (search already applied by parent)
16
+ * @param selectedIndex - Currently selected item index
17
+ * @param searchQuery - Current search query string
18
+ * @param focusZone - Current focus zone for keyboard navigation
12
19
  * @example
13
- * <InstalledTab plugins={installedPlugins} selectedIndex={0} />
20
+ * <InstalledTab plugins={installedPlugins} selectedIndex={0} searchQuery="" focusZone="list" />
14
21
  */
15
- export default function InstalledTab({ plugins, selectedIndex, }: InstalledTabProps): import("react/jsx-runtime").JSX.Element;
22
+ export default function InstalledTab({ plugins, selectedIndex, searchQuery, focusZone, }: InstalledTabProps): import("react/jsx-runtime").JSX.Element;
16
23
  export {};
@@ -6,19 +6,23 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
6
6
  import { Box, Text } from 'ink';
7
7
  import PluginList from '../components/PluginList.js';
8
8
  import PluginDetail from '../components/PluginDetail.js';
9
+ import SearchInput from '../components/SearchInput.js';
9
10
  /**
10
11
  * Installed tab - manage installed plugins
12
+ * @param plugins - Filtered installed plugins (search already applied by parent)
13
+ * @param selectedIndex - Currently selected item index
14
+ * @param searchQuery - Current search query string
15
+ * @param focusZone - Current focus zone for keyboard navigation
11
16
  * @example
12
- * <InstalledTab plugins={installedPlugins} selectedIndex={0} />
17
+ * <InstalledTab plugins={installedPlugins} selectedIndex={0} searchQuery="" focusZone="list" />
13
18
  */
14
- export default function InstalledTab({ plugins, selectedIndex, }) {
15
- // Filter to installed plugins only
16
- const installedPlugins = plugins.filter((p) => p.isInstalled);
17
- const selectedPlugin = installedPlugins[selectedIndex] ?? null;
19
+ export default function InstalledTab({ plugins, selectedIndex, searchQuery = '', focusZone = 'list', }) {
20
+ // Plugins are already filtered by parent, use directly
21
+ const selectedPlugin = plugins[selectedIndex] ?? null;
18
22
  // Count enabled/disabled
19
- const enabledCount = installedPlugins.filter((p) => p.isEnabled).length;
20
- const disabledCount = installedPlugins.length - enabledCount;
21
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Installed plugins (", installedPlugins.length > 0
22
- ? `${selectedIndex + 1}/${installedPlugins.length}`
23
- : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "green", children: ["\u25CF ", enabledCount, " enabled"] }), _jsxs(Text, { color: "yellow", children: ["\u25D0 ", disabledCount, " disabled"] })] })] }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: installedPlugins.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "No plugins installed" }), _jsxs(Text, { dimColor: true, children: ["Use the Discover tab or", ' ', _jsx(Text, { color: "white", children: "/plugin install" }), " in Claude Code"] })] })) : (_jsx(PluginList, { plugins: installedPlugins, selectedIndex: selectedIndex, visibleCount: 12 })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
23
+ const enabledCount = plugins.filter((p) => p.isEnabled).length;
24
+ const disabledCount = plugins.length - enabledCount;
25
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Installed plugins (", plugins.length > 0 ? `${selectedIndex + 1}/${plugins.length}` : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "green", children: ["\u25CF ", enabledCount, " enabled"] }), _jsxs(Text, { color: "yellow", children: ["\u25D0 ", disabledCount, " disabled"] })] })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search installed plugins..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: plugins.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery ? 'No matching plugins' : 'No plugins installed' }), _jsx(Text, { dimColor: true, children: searchQuery
26
+ ? 'Try a different search term'
27
+ : 'Use the Discover tab or /plugin install in Claude Code' })] })) : (_jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(PluginDetail, { plugin: selectedPlugin }) })] })] }));
24
28
  }
@@ -2,15 +2,22 @@
2
2
  * MarketplacesTab component
3
3
  * View and manage marketplace sources
4
4
  */
5
- import type { Marketplace } from '../types/index.js';
5
+ import type { Marketplace, FocusZone } from '../types/index.js';
6
6
  interface MarketplacesTabProps {
7
7
  marketplaces: Marketplace[];
8
8
  selectedIndex: number;
9
+ searchQuery?: string;
10
+ /** Current focus zone for keyboard navigation */
11
+ focusZone?: FocusZone;
9
12
  }
10
13
  /**
11
14
  * Marketplaces tab - manage plugin sources
15
+ * @param marketplaces - Filtered marketplaces (search already applied by parent)
16
+ * @param selectedIndex - Currently selected item index
17
+ * @param searchQuery - Current search query string
18
+ * @param focusZone - Current focus zone for keyboard navigation
12
19
  * @example
13
- * <MarketplacesTab marketplaces={marketplaces} selectedIndex={0} />
20
+ * <MarketplacesTab marketplaces={marketplaces} selectedIndex={0} searchQuery="" focusZone="list" />
14
21
  */
15
- export default function MarketplacesTab({ marketplaces, selectedIndex, }: MarketplacesTabProps): import("react/jsx-runtime").JSX.Element;
22
+ export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery, focusZone, }: MarketplacesTabProps): import("react/jsx-runtime").JSX.Element;
16
23
  export {};
@@ -6,16 +6,25 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
6
6
  import { Box, Text } from 'ink';
7
7
  import MarketplaceList from '../components/MarketplaceList.js';
8
8
  import MarketplaceDetail from '../components/MarketplaceDetail.js';
9
+ import SearchInput from '../components/SearchInput.js';
9
10
  /**
10
11
  * Marketplaces tab - manage plugin sources
12
+ * @param marketplaces - Filtered marketplaces (search already applied by parent)
13
+ * @param selectedIndex - Currently selected item index
14
+ * @param searchQuery - Current search query string
15
+ * @param focusZone - Current focus zone for keyboard navigation
11
16
  * @example
12
- * <MarketplacesTab marketplaces={marketplaces} selectedIndex={0} />
17
+ * <MarketplacesTab marketplaces={marketplaces} selectedIndex={0} searchQuery="" focusZone="list" />
13
18
  */
14
- export default function MarketplacesTab({ marketplaces, selectedIndex, }) {
19
+ export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery = '', focusZone = 'list', }) {
15
20
  const selectedMarketplace = marketplaces[selectedIndex] ?? null;
16
21
  // Count total plugins across all marketplaces
17
22
  const totalPlugins = marketplaces.reduce((sum, m) => sum + (m.pluginCount || 0), 0);
18
23
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Marketplaces (", marketplaces.length > 0
19
24
  ? `${selectedIndex + 1}/${marketplaces.length}`
20
- : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { color: "gray", children: [totalPlugins, " total plugins"] })] }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: marketplaces.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "No marketplaces found" }), _jsxs(Text, { dimColor: true, children: ["Add marketplaces with", ' ', _jsx(Text, { color: "white", children: "/plugin add-marketplace" })] })] })) : (_jsx(MarketplaceList, { marketplaces: marketplaces, selectedIndex: selectedIndex })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(MarketplaceDetail, { marketplace: selectedMarketplace }) })] })] }));
25
+ : '0', ")"] }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { color: "gray", children: [totalPlugins, " total plugins"] })] }), _jsx(Box, { marginBottom: 1, children: _jsx(SearchInput, { query: searchQuery, isActive: focusZone === 'search', placeholder: "Type to search marketplaces..." }) }), _jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: marketplaces.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery
26
+ ? 'No matching marketplaces'
27
+ : 'No marketplaces found' }), _jsx(Text, { dimColor: true, children: searchQuery
28
+ ? 'Try a different search term'
29
+ : 'Add marketplaces with /plugin add-marketplace' })] })) : (_jsx(MarketplaceList, { marketplaces: marketplaces, selectedIndex: selectedIndex, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(MarketplaceDetail, { marketplace: selectedMarketplace }) })] })] }));
21
30
  }