@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.
- package/README.md +70 -32
- package/dist/app.d.ts +29 -0
- package/dist/app.js +496 -69
- package/dist/components/AddMarketplaceDialog.d.ts +20 -0
- package/dist/components/AddMarketplaceDialog.js +18 -0
- package/dist/components/ComponentBadges.d.ts +32 -0
- package/dist/components/ComponentBadges.js +82 -0
- package/dist/components/HelpOverlay.d.ts +15 -0
- package/dist/components/HelpOverlay.js +51 -0
- package/dist/components/KeyHints.d.ts +6 -3
- package/dist/components/KeyHints.js +39 -10
- package/dist/components/MarketplaceList.d.ts +4 -2
- package/dist/components/MarketplaceList.js +7 -3
- package/dist/components/PluginDetail.js +2 -1
- package/dist/components/PluginList.d.ts +29 -2
- package/dist/components/PluginList.js +26 -5
- package/dist/components/SearchInput.js +1 -1
- package/dist/components/TabBar.d.ts +5 -3
- package/dist/components/TabBar.js +20 -8
- package/dist/services/componentService.d.ts +35 -0
- package/dist/services/componentService.js +178 -0
- package/dist/services/marketplaceActionsService.d.ts +44 -0
- package/dist/services/marketplaceActionsService.js +92 -0
- package/dist/services/pluginService.d.ts +10 -0
- package/dist/services/pluginService.js +22 -0
- package/dist/tabs/DiscoverTab.d.ts +5 -3
- package/dist/tabs/DiscoverTab.js +3 -2
- package/dist/tabs/EnabledTab.d.ts +24 -0
- package/dist/tabs/EnabledTab.js +26 -0
- package/dist/tabs/InstalledTab.d.ts +10 -3
- package/dist/tabs/InstalledTab.js +14 -10
- package/dist/tabs/MarketplacesTab.d.ts +10 -3
- package/dist/tabs/MarketplacesTab.js +12 -3
- package/dist/types/index.d.ts +71 -1
- 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
|
-
|
|
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,
|
|
27
|
+
export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder, focusZone, }: DiscoverTabProps): import("react/jsx-runtime").JSX.Element;
|
|
26
28
|
export {};
|
package/dist/tabs/DiscoverTab.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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
|
-
//
|
|
16
|
-
const
|
|
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 =
|
|
20
|
-
const disabledCount =
|
|
21
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Installed plugins (",
|
|
22
|
-
|
|
23
|
-
|
|
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"] })] }),
|
|
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
|
}
|