@laststance/claude-plugin-dashboard 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/app.d.ts +7 -1
- package/dist/app.js +229 -82
- package/dist/cli.js +58 -67
- package/dist/components/ComponentList.d.ts +53 -0
- package/dist/components/ComponentList.js +193 -0
- package/dist/components/KeyHints.js +25 -28
- package/dist/components/MarketplaceActionMenu.d.ts +41 -0
- package/dist/components/MarketplaceActionMenu.js +68 -0
- package/dist/components/MarketplaceDetail.d.ts +10 -3
- package/dist/components/MarketplaceDetail.js +10 -4
- package/dist/components/PluginDetail.d.ts +3 -0
- package/dist/components/PluginDetail.js +28 -4
- package/dist/components/PluginList.js +19 -7
- package/dist/services/componentService.d.ts +12 -1
- package/dist/services/componentService.js +238 -0
- package/dist/services/marketplaceActionsService.d.ts +17 -0
- package/dist/services/marketplaceActionsService.js +18 -0
- package/dist/services/pluginService.js +78 -2
- package/dist/tabs/DiscoverTab.js +1 -1
- package/dist/tabs/EnabledTab.js +2 -2
- package/dist/tabs/InstalledTab.js +2 -2
- package/dist/tabs/MarketplacesTab.d.ts +15 -2
- package/dist/tabs/MarketplacesTab.js +13 -4
- package/dist/types/index.d.ts +110 -1
- package/package.json +4 -2
|
@@ -57,6 +57,244 @@ export function detectPluginComponents(installPath) {
|
|
|
57
57
|
}
|
|
58
58
|
return components;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Detect detailed components for an installed plugin
|
|
62
|
+
* Reads skills/, commands/, agents/ directories and parses plugin.json
|
|
63
|
+
* @param installPath - Absolute path to installed plugin directory
|
|
64
|
+
* @returns Detailed component info with names and descriptions
|
|
65
|
+
* - Returns undefined if path doesn't exist or has no components
|
|
66
|
+
* @example
|
|
67
|
+
* detectComponentsDetailed('/path/to/plugin')
|
|
68
|
+
* // => { skills: [{ name: 'xlsx', description: '...', type: 'skill' }] }
|
|
69
|
+
*/
|
|
70
|
+
export function detectComponentsDetailed(installPath) {
|
|
71
|
+
if (!directoryExists(installPath)) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const detailed = {};
|
|
75
|
+
// Skills: Read directory names + SKILL.md frontmatter
|
|
76
|
+
const skills = getSkillDetails(installPath);
|
|
77
|
+
if (skills.length > 0) {
|
|
78
|
+
detailed.skills = skills;
|
|
79
|
+
}
|
|
80
|
+
// Commands: Read .md filenames
|
|
81
|
+
const commands = getMarkdownFileDetails(installPath, 'commands', 'command');
|
|
82
|
+
if (commands.length > 0) {
|
|
83
|
+
detailed.commands = commands;
|
|
84
|
+
}
|
|
85
|
+
// Agents: Read .md filenames
|
|
86
|
+
const agents = getMarkdownFileDetails(installPath, 'agents', 'agent');
|
|
87
|
+
if (agents.length > 0) {
|
|
88
|
+
detailed.agents = agents;
|
|
89
|
+
}
|
|
90
|
+
// Hooks: Read event names from hooks.json or hooks/ directory
|
|
91
|
+
const hooks = getHookNames(installPath);
|
|
92
|
+
if (hooks.length > 0) {
|
|
93
|
+
detailed.hooks = hooks;
|
|
94
|
+
}
|
|
95
|
+
// MCP Servers: Read plugin.json mcpServers keys
|
|
96
|
+
const mcpServers = getMcpServerNames(installPath);
|
|
97
|
+
if (mcpServers.length > 0) {
|
|
98
|
+
detailed.mcpServers = mcpServers;
|
|
99
|
+
}
|
|
100
|
+
// LSP Servers: Read .lsp.json keys
|
|
101
|
+
const lspServers = getLspServerNames(installPath);
|
|
102
|
+
if (lspServers.length > 0) {
|
|
103
|
+
detailed.lspServers = lspServers;
|
|
104
|
+
}
|
|
105
|
+
// Return undefined if no components detected
|
|
106
|
+
if (Object.keys(detailed).length === 0) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return detailed;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get skill details from skills/ directory
|
|
113
|
+
* Reads directory names and parses SKILL.md frontmatter for descriptions
|
|
114
|
+
* @param installPath - Plugin install path
|
|
115
|
+
* @returns Array of ComponentInfo for each skill
|
|
116
|
+
*/
|
|
117
|
+
function getSkillDetails(installPath) {
|
|
118
|
+
const skillsPath = path.join(installPath, 'skills');
|
|
119
|
+
if (!directoryExists(skillsPath)) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const entries = fs.readdirSync(skillsPath, { withFileTypes: true });
|
|
124
|
+
return entries
|
|
125
|
+
.filter((entry) => entry.isDirectory())
|
|
126
|
+
.map((entry) => {
|
|
127
|
+
const skillMdPath = path.join(skillsPath, entry.name, 'SKILL.md');
|
|
128
|
+
const description = parseSkillMdDescription(skillMdPath);
|
|
129
|
+
return {
|
|
130
|
+
name: entry.name,
|
|
131
|
+
description,
|
|
132
|
+
type: 'skill',
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Parse SKILL.md frontmatter for description
|
|
142
|
+
* Looks for `description:` field in YAML frontmatter
|
|
143
|
+
* @param skillMdPath - Path to SKILL.md file
|
|
144
|
+
* @returns Description string or undefined
|
|
145
|
+
*/
|
|
146
|
+
function parseSkillMdDescription(skillMdPath) {
|
|
147
|
+
if (!fileExists(skillMdPath)) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
152
|
+
// Match YAML frontmatter description field
|
|
153
|
+
const match = content.match(/^---\n[\s\S]*?description:\s*["']?(.+?)["']?\s*\n[\s\S]*?---/m);
|
|
154
|
+
return match?.[1]?.trim();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get component details from .md files in a directory
|
|
162
|
+
* Uses filename (minus extension) as component name
|
|
163
|
+
* @param installPath - Plugin install path
|
|
164
|
+
* @param subdir - Subdirectory name ('commands' or 'agents')
|
|
165
|
+
* @param type - Component type
|
|
166
|
+
* @returns Array of ComponentInfo
|
|
167
|
+
*/
|
|
168
|
+
function getMarkdownFileDetails(installPath, subdir, type) {
|
|
169
|
+
const dirPath = path.join(installPath, subdir);
|
|
170
|
+
if (!directoryExists(dirPath)) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const files = fs.readdirSync(dirPath);
|
|
175
|
+
return files
|
|
176
|
+
.filter((file) => file.endsWith('.md'))
|
|
177
|
+
.map((file) => {
|
|
178
|
+
const name = file.replace(/\.md$/, '');
|
|
179
|
+
const filePath = path.join(dirPath, file);
|
|
180
|
+
const description = parseFirstLineDescription(filePath);
|
|
181
|
+
return {
|
|
182
|
+
name,
|
|
183
|
+
description,
|
|
184
|
+
type,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Parse first non-empty line of a markdown file as description
|
|
194
|
+
* Properly skips YAML frontmatter block and strips heading markers
|
|
195
|
+
* @param filePath - Path to markdown file
|
|
196
|
+
* @returns First non-frontmatter, non-empty line or undefined
|
|
197
|
+
* @example
|
|
198
|
+
* // File: "---\nname: test\n---\n# My Title\n"
|
|
199
|
+
* parseFirstLineDescription(path) // => "My Title"
|
|
200
|
+
*/
|
|
201
|
+
function parseFirstLineDescription(filePath) {
|
|
202
|
+
try {
|
|
203
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
204
|
+
const lines = content.split('\n');
|
|
205
|
+
let inFrontmatter = false;
|
|
206
|
+
let frontmatterClosed = false;
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
const trimmed = line.trim();
|
|
209
|
+
// Detect frontmatter delimiter
|
|
210
|
+
if (trimmed === '---') {
|
|
211
|
+
if (!inFrontmatter && !frontmatterClosed) {
|
|
212
|
+
// Opening delimiter
|
|
213
|
+
inFrontmatter = true;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
else if (inFrontmatter) {
|
|
217
|
+
// Closing delimiter
|
|
218
|
+
inFrontmatter = false;
|
|
219
|
+
frontmatterClosed = true;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Skip lines inside frontmatter
|
|
224
|
+
if (inFrontmatter) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
// Skip empty lines
|
|
228
|
+
if (!trimmed) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Found first content line - remove heading markers and return
|
|
232
|
+
return trimmed.replace(/^#+\s*/, '');
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get hook event names from hooks configuration
|
|
242
|
+
* @param installPath - Plugin install path
|
|
243
|
+
* @returns Array of hook event names
|
|
244
|
+
*/
|
|
245
|
+
function getHookNames(installPath) {
|
|
246
|
+
// Try hooks.json first
|
|
247
|
+
const hooksJsonPath = path.join(installPath, 'hooks.json');
|
|
248
|
+
const hooksJson = readJsonFile(hooksJsonPath);
|
|
249
|
+
if (hooksJson) {
|
|
250
|
+
return Object.keys(hooksJson);
|
|
251
|
+
}
|
|
252
|
+
// Try hooks/ directory
|
|
253
|
+
const hooksDir = path.join(installPath, 'hooks');
|
|
254
|
+
if (directoryExists(hooksDir)) {
|
|
255
|
+
try {
|
|
256
|
+
const files = fs.readdirSync(hooksDir);
|
|
257
|
+
return files
|
|
258
|
+
.filter((f) => f.endsWith('.json') || f.endsWith('.js'))
|
|
259
|
+
.map((f) => f.replace(/\.(json|js)$/, ''));
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get MCP server names from plugin.json
|
|
269
|
+
* @param installPath - Plugin install path
|
|
270
|
+
* @returns Array of MCP server names
|
|
271
|
+
*/
|
|
272
|
+
function getMcpServerNames(installPath) {
|
|
273
|
+
const pluginJsonPaths = [
|
|
274
|
+
path.join(installPath, '.claude-plugin', 'plugin.json'),
|
|
275
|
+
path.join(installPath, 'plugin.json'),
|
|
276
|
+
];
|
|
277
|
+
for (const pluginJsonPath of pluginJsonPaths) {
|
|
278
|
+
const pluginJson = readJsonFile(pluginJsonPath);
|
|
279
|
+
if (pluginJson?.mcpServers) {
|
|
280
|
+
return Object.keys(pluginJson.mcpServers);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get LSP server language IDs from .lsp.json
|
|
287
|
+
* @param installPath - Plugin install path
|
|
288
|
+
* @returns Array of language IDs
|
|
289
|
+
*/
|
|
290
|
+
function getLspServerNames(installPath) {
|
|
291
|
+
const lspJsonPath = path.join(installPath, '.lsp.json');
|
|
292
|
+
const lspConfig = readJsonFile(lspJsonPath);
|
|
293
|
+
if (!lspConfig) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
return Object.keys(lspConfig);
|
|
297
|
+
}
|
|
60
298
|
/**
|
|
61
299
|
* Count skill directories in the skills/ folder
|
|
62
300
|
* Skills are stored as subdirectories with SKILL.md files
|
|
@@ -42,3 +42,20 @@ export declare function removeMarketplace(name: string): Promise<MarketplaceActi
|
|
|
42
42
|
* updateMarketplace()
|
|
43
43
|
*/
|
|
44
44
|
export declare function updateMarketplace(name?: string): Promise<MarketplaceActionResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Execute a marketplace CLI command with generic args and messages
|
|
47
|
+
* @param args - CLI arguments to pass to claude command
|
|
48
|
+
* @param successMessage - Message to return on success
|
|
49
|
+
* @param failureMessage - Message to return on failure
|
|
50
|
+
* @returns Promise resolving to action result
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Toggle auto-update setting for a marketplace
|
|
54
|
+
* @param marketplaceId - The marketplace identifier
|
|
55
|
+
* @param currentValue - Current auto-update state
|
|
56
|
+
* @returns Promise resolving to action result with new state
|
|
57
|
+
* @example
|
|
58
|
+
* toggleAutoUpdate('claude-plugins-official', false) // enables auto-update
|
|
59
|
+
* toggleAutoUpdate('claude-plugins-official', true) // disables auto-update
|
|
60
|
+
*/
|
|
61
|
+
export declare function toggleAutoUpdate(marketplaceId: string, currentValue: boolean): Promise<MarketplaceActionResult>;
|
|
@@ -52,6 +52,24 @@ export function updateMarketplace(name) {
|
|
|
52
52
|
* @param failureMessage - Message to return on failure
|
|
53
53
|
* @returns Promise resolving to action result
|
|
54
54
|
*/
|
|
55
|
+
/**
|
|
56
|
+
* Toggle auto-update setting for a marketplace
|
|
57
|
+
* @param marketplaceId - The marketplace identifier
|
|
58
|
+
* @param currentValue - Current auto-update state
|
|
59
|
+
* @returns Promise resolving to action result with new state
|
|
60
|
+
* @example
|
|
61
|
+
* toggleAutoUpdate('claude-plugins-official', false) // enables auto-update
|
|
62
|
+
* toggleAutoUpdate('claude-plugins-official', true) // disables auto-update
|
|
63
|
+
*/
|
|
64
|
+
export function toggleAutoUpdate(marketplaceId, currentValue) {
|
|
65
|
+
const newValue = !currentValue;
|
|
66
|
+
const args = newValue
|
|
67
|
+
? ['plugin', 'marketplace', 'auto-update', 'enable', marketplaceId]
|
|
68
|
+
: ['plugin', 'marketplace', 'auto-update', 'disable', marketplaceId];
|
|
69
|
+
return executeMarketplaceCommand(args, newValue
|
|
70
|
+
? `Enabled auto-update for ${marketplaceId}`
|
|
71
|
+
: `Disabled auto-update for ${marketplaceId}`, `Failed to toggle auto-update for ${marketplaceId}`);
|
|
72
|
+
}
|
|
55
73
|
function executeMarketplaceCommand(args, successMessage, failureMessage) {
|
|
56
74
|
return new Promise((resolve) => {
|
|
57
75
|
const child = spawn('claude', args, {
|
|
@@ -4,7 +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
|
+
import { detectPluginComponents, detectComponentsDetailed, } from './componentService.js';
|
|
8
8
|
import { PATHS, getMarketplaceJsonPath } from '../utils/paths.js';
|
|
9
9
|
/**
|
|
10
10
|
* Load all plugins from all marketplaces
|
|
@@ -45,10 +45,20 @@ export function loadAllPlugins() {
|
|
|
45
45
|
for (const plugin of manifest.plugins) {
|
|
46
46
|
const pluginId = `${plugin.name}@${marketplace}`;
|
|
47
47
|
const installedEntry = installedMap.get(pluginId);
|
|
48
|
-
// Detect components for installed plugins
|
|
48
|
+
// Detect components for installed plugins (counts)
|
|
49
49
|
const components = installedEntry
|
|
50
50
|
? detectPluginComponents(installedEntry.installPath)
|
|
51
51
|
: undefined;
|
|
52
|
+
// Detect detailed components
|
|
53
|
+
// - For installed: read from file system (names + descriptions)
|
|
54
|
+
// - For not installed: parse from marketplace JSON (names only)
|
|
55
|
+
let componentsDetailed;
|
|
56
|
+
if (installedEntry) {
|
|
57
|
+
componentsDetailed = detectComponentsDetailed(installedEntry.installPath);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
componentsDetailed = parseMarketplaceComponents(plugin);
|
|
61
|
+
}
|
|
52
62
|
plugins.push({
|
|
53
63
|
id: pluginId,
|
|
54
64
|
name: plugin.name,
|
|
@@ -67,6 +77,7 @@ export function loadAllPlugins() {
|
|
|
67
77
|
isLocal: installedEntry?.isLocal,
|
|
68
78
|
gitCommitSha: installedEntry?.gitCommitSha,
|
|
69
79
|
components,
|
|
80
|
+
componentsDetailed,
|
|
70
81
|
});
|
|
71
82
|
}
|
|
72
83
|
}
|
|
@@ -120,6 +131,7 @@ export function loadMarketplaces() {
|
|
|
120
131
|
installLocation: data.installLocation,
|
|
121
132
|
lastUpdated: data.lastUpdated,
|
|
122
133
|
pluginCount,
|
|
134
|
+
autoUpdate: data.autoUpdate ?? false,
|
|
123
135
|
});
|
|
124
136
|
}
|
|
125
137
|
// Sort by plugin count (descending)
|
|
@@ -208,3 +220,67 @@ export function getPluginStatistics() {
|
|
|
208
220
|
marketplaces: marketplaces.length,
|
|
209
221
|
};
|
|
210
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Parse component names from marketplace plugin entry
|
|
225
|
+
* Extracts component names from skills[], agents[], commands[] arrays
|
|
226
|
+
* @param plugin - Marketplace plugin entry
|
|
227
|
+
* @returns Detailed components with names only (no descriptions)
|
|
228
|
+
* @example
|
|
229
|
+
* parseMarketplaceComponents({ skills: ['./skills/xlsx', './skills/docx'] })
|
|
230
|
+
* // => { skills: [{ name: 'xlsx', type: 'skill' }, { name: 'docx', type: 'skill' }] }
|
|
231
|
+
*/
|
|
232
|
+
function parseMarketplaceComponents(plugin) {
|
|
233
|
+
const detailed = {};
|
|
234
|
+
// Parse skills array (e.g., ["./skills/xlsx", "./skills/docx"])
|
|
235
|
+
if (plugin.skills && Array.isArray(plugin.skills)) {
|
|
236
|
+
const skills = plugin.skills
|
|
237
|
+
.map((skillPath) => extractComponentName(skillPath, 'skill'))
|
|
238
|
+
.filter((item) => item !== null);
|
|
239
|
+
if (skills.length > 0) {
|
|
240
|
+
detailed.skills = skills;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Parse agents array (if available in marketplace)
|
|
244
|
+
if (plugin.agents && Array.isArray(plugin.agents)) {
|
|
245
|
+
const agents = plugin.agents
|
|
246
|
+
.map((agentPath) => extractComponentName(agentPath, 'agent'))
|
|
247
|
+
.filter((item) => item !== null);
|
|
248
|
+
if (agents.length > 0) {
|
|
249
|
+
detailed.agents = agents;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Parse commands array (if available in marketplace)
|
|
253
|
+
if (plugin.commands && Array.isArray(plugin.commands)) {
|
|
254
|
+
const commands = plugin.commands
|
|
255
|
+
.map((cmdPath) => extractComponentName(cmdPath, 'command'))
|
|
256
|
+
.filter((item) => item !== null);
|
|
257
|
+
if (commands.length > 0) {
|
|
258
|
+
detailed.commands = commands;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Return undefined if no components found
|
|
262
|
+
if (Object.keys(detailed).length === 0) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
return detailed;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Extract component name from path string
|
|
269
|
+
* @param pathStr - Path like "./skills/xlsx" or just "xlsx"
|
|
270
|
+
* @param type - Component type
|
|
271
|
+
* @returns ComponentInfo or null if invalid
|
|
272
|
+
*/
|
|
273
|
+
function extractComponentName(pathStr, type) {
|
|
274
|
+
if (typeof pathStr !== 'string' || !pathStr.trim()) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
// Extract basename from path
|
|
278
|
+
// "./skills/xlsx" -> "xlsx"
|
|
279
|
+
// "xlsx" -> "xlsx"
|
|
280
|
+
const parts = pathStr.split('/');
|
|
281
|
+
const name = parts[parts.length - 1]?.trim();
|
|
282
|
+
if (!name) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return { name, type };
|
|
286
|
+
}
|
package/dist/tabs/DiscoverTab.js
CHANGED
|
@@ -22,5 +22,5 @@ import SortDropdown from '../components/SortDropdown.js';
|
|
|
22
22
|
*/
|
|
23
23
|
export default function DiscoverTab({ plugins, selectedIndex, searchQuery, sortBy, sortOrder, focusZone = 'list', }) {
|
|
24
24
|
const selectedPlugin = plugins[selectedIndex] ?? null;
|
|
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
|
+
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, overflow: "hidden", children: [_jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: _jsx(PluginList, { plugins: plugins, selectedIndex: selectedIndex, visibleCount: 12, isFocused: focusZone === 'list' }) }), _jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: _jsx(PluginDetail, { plugin: selectedPlugin }, selectedPlugin?.id ?? 'none') })] })] }));
|
|
26
26
|
}
|
package/dist/tabs/EnabledTab.js
CHANGED
|
@@ -20,7 +20,7 @@ import SearchInput from '../components/SearchInput.js';
|
|
|
20
20
|
export default function EnabledTab({ plugins, selectedIndex, searchQuery = '', focusZone = 'list', }) {
|
|
21
21
|
// Plugins are already filtered by parent, use directly
|
|
22
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
|
|
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, overflow: "hidden", children: [_jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", 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
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 }) })] })] }));
|
|
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", overflow: "hidden", children: _jsx(PluginDetail, { plugin: selectedPlugin }, selectedPlugin?.id ?? 'none') })] })] }));
|
|
26
26
|
}
|
|
@@ -22,7 +22,7 @@ export default function InstalledTab({ plugins, selectedIndex, searchQuery = '',
|
|
|
22
22
|
// Count enabled/disabled
|
|
23
23
|
const enabledCount = plugins.filter((p) => p.isEnabled).length;
|
|
24
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
|
|
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, overflow: "hidden", children: [_jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", 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
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 }) })] })] }));
|
|
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", overflow: "hidden", children: _jsx(PluginDetail, { plugin: selectedPlugin }, selectedPlugin?.id ?? 'none') })] })] }));
|
|
28
28
|
}
|
|
@@ -9,6 +9,10 @@ interface MarketplacesTabProps {
|
|
|
9
9
|
searchQuery?: string;
|
|
10
10
|
/** Current focus zone for keyboard navigation */
|
|
11
11
|
focusZone?: FocusZone;
|
|
12
|
+
/** Whether to show the marketplace action menu */
|
|
13
|
+
showActionMenu?: boolean;
|
|
14
|
+
/** Selected index in the action menu */
|
|
15
|
+
actionMenuSelectedIndex?: number;
|
|
12
16
|
}
|
|
13
17
|
/**
|
|
14
18
|
* Marketplaces tab - manage plugin sources
|
|
@@ -16,8 +20,17 @@ interface MarketplacesTabProps {
|
|
|
16
20
|
* @param selectedIndex - Currently selected item index
|
|
17
21
|
* @param searchQuery - Current search query string
|
|
18
22
|
* @param focusZone - Current focus zone for keyboard navigation
|
|
23
|
+
* @param showActionMenu - Whether to show the action menu
|
|
24
|
+
* @param actionMenuSelectedIndex - Selected index in action menu
|
|
19
25
|
* @example
|
|
20
|
-
* <MarketplacesTab
|
|
26
|
+
* <MarketplacesTab
|
|
27
|
+
* marketplaces={marketplaces}
|
|
28
|
+
* selectedIndex={0}
|
|
29
|
+
* searchQuery=""
|
|
30
|
+
* focusZone="list"
|
|
31
|
+
* showActionMenu={false}
|
|
32
|
+
* actionMenuSelectedIndex={0}
|
|
33
|
+
* />
|
|
21
34
|
*/
|
|
22
|
-
export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery, focusZone, }: MarketplacesTabProps): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery, focusZone, showActionMenu, actionMenuSelectedIndex, }: MarketplacesTabProps): import("react/jsx-runtime").JSX.Element;
|
|
23
36
|
export {};
|
|
@@ -13,18 +13,27 @@ import SearchInput from '../components/SearchInput.js';
|
|
|
13
13
|
* @param selectedIndex - Currently selected item index
|
|
14
14
|
* @param searchQuery - Current search query string
|
|
15
15
|
* @param focusZone - Current focus zone for keyboard navigation
|
|
16
|
+
* @param showActionMenu - Whether to show the action menu
|
|
17
|
+
* @param actionMenuSelectedIndex - Selected index in action menu
|
|
16
18
|
* @example
|
|
17
|
-
* <MarketplacesTab
|
|
19
|
+
* <MarketplacesTab
|
|
20
|
+
* marketplaces={marketplaces}
|
|
21
|
+
* selectedIndex={0}
|
|
22
|
+
* searchQuery=""
|
|
23
|
+
* focusZone="list"
|
|
24
|
+
* showActionMenu={false}
|
|
25
|
+
* actionMenuSelectedIndex={0}
|
|
26
|
+
* />
|
|
18
27
|
*/
|
|
19
|
-
export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery = '', focusZone = 'list', }) {
|
|
28
|
+
export default function MarketplacesTab({ marketplaces, selectedIndex, searchQuery = '', focusZone = 'list', showActionMenu = false, actionMenuSelectedIndex = 0, }) {
|
|
20
29
|
const selectedMarketplace = marketplaces[selectedIndex] ?? null;
|
|
21
30
|
// Count total plugins across all marketplaces
|
|
22
31
|
const totalPlugins = marketplaces.reduce((sum, m) => sum + (m.pluginCount || 0), 0);
|
|
23
32
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: ["Marketplaces (", marketplaces.length > 0
|
|
24
33
|
? `${selectedIndex + 1}/${marketplaces.length}`
|
|
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
|
|
34
|
+
: '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, overflow: "hidden", children: [_jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: marketplaces.length === 0 ? (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: searchQuery
|
|
26
35
|
? 'No matching marketplaces'
|
|
27
36
|
: 'No marketplaces found' }), _jsx(Text, { dimColor: true, children: searchQuery
|
|
28
37
|
? '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 }) })] })] }));
|
|
38
|
+
: 'Add marketplaces with /plugin add-marketplace' })] })) : (_jsx(MarketplaceList, { marketplaces: marketplaces, selectedIndex: selectedIndex, isFocused: focusZone === 'list' })) }), _jsx(Box, { width: "50%", flexDirection: "column", overflow: "hidden", children: _jsx(MarketplaceDetail, { marketplace: selectedMarketplace, showActionMenu: showActionMenu, actionMenuSelectedIndex: actionMenuSelectedIndex }) })] })] }));
|
|
30
39
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -3,7 +3,92 @@
|
|
|
3
3
|
* These types aggregate data from multiple Claude Code configuration files
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Component
|
|
6
|
+
* Component type identifier
|
|
7
|
+
*/
|
|
8
|
+
export type ComponentType = 'skill' | 'command' | 'agent' | 'hook' | 'mcp' | 'lsp';
|
|
9
|
+
/**
|
|
10
|
+
* Individual component information with name and optional description
|
|
11
|
+
*
|
|
12
|
+
* Data Source Architecture:
|
|
13
|
+
* ```
|
|
14
|
+
* ┌─────────────────────────────────────────────────────────────┐
|
|
15
|
+
* │ Plugin Installed? │
|
|
16
|
+
* │ │ │
|
|
17
|
+
* │ ┌──── YES ─────┴───── NO ────┐ │
|
|
18
|
+
* │ │ │ │
|
|
19
|
+
* │ ▼ ▼ │
|
|
20
|
+
* │ ┌─────────────────────┐ ┌─────────────────────┐ │
|
|
21
|
+
* │ │ File System Scan │ │ Marketplace JSON │ │
|
|
22
|
+
* │ │ ───────────────── │ │ ───────────────── │ │
|
|
23
|
+
* │ │ skills/{name}/ │ │ skills: [ │ │
|
|
24
|
+
* │ │ commands/{name}.md │ │ "./skills/xlsx" │ │
|
|
25
|
+
* │ │ agents/{name}.md │ │ ] │ │
|
|
26
|
+
* │ │ plugin.json keys │ │ │ │
|
|
27
|
+
* │ └─────────────────────┘ └─────────────────────┘ │
|
|
28
|
+
* │ │ │ │
|
|
29
|
+
* │ ▼ ▼ │
|
|
30
|
+
* │ Names + Descriptions Names only (if available) │
|
|
31
|
+
* │ │
|
|
32
|
+
* │ └────────────────────────────┘ │
|
|
33
|
+
* │ │ │
|
|
34
|
+
* │ ▼ │
|
|
35
|
+
* │ PluginComponentsDetailed │
|
|
36
|
+
* │ ───────────────────────── │
|
|
37
|
+
* │ { │
|
|
38
|
+
* │ skills: [ │
|
|
39
|
+
* │ { name: "xlsx", description: "..." }, │
|
|
40
|
+
* │ { name: "docx" } │
|
|
41
|
+
* │ ], │
|
|
42
|
+
* │ mcpServers: ["supabase", "context7"] │
|
|
43
|
+
* │ } │
|
|
44
|
+
* └─────────────────────────────────────────────────────────────┘
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Skill with description (from SKILL.md frontmatter)
|
|
49
|
+
* { name: "xlsx", description: "Spreadsheet editing", type: "skill" }
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Command without description
|
|
53
|
+
* { name: "code-review", type: "command" }
|
|
54
|
+
*/
|
|
55
|
+
export interface ComponentInfo {
|
|
56
|
+
/** Component name (e.g., "xlsx", "code-review") */
|
|
57
|
+
name: string;
|
|
58
|
+
/** Optional description from SKILL.md frontmatter or first line of .md file */
|
|
59
|
+
description?: string;
|
|
60
|
+
/** Component type for display categorization */
|
|
61
|
+
type: ComponentType;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Detailed component information for a plugin
|
|
65
|
+
* Extends PluginComponents (counts) with actual component names and descriptions
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* {
|
|
69
|
+
* skills: [
|
|
70
|
+
* { name: "xlsx", description: "Spreadsheet editing", type: "skill" },
|
|
71
|
+
* { name: "docx", type: "skill" }
|
|
72
|
+
* ],
|
|
73
|
+
* mcpServers: ["supabase", "context7"]
|
|
74
|
+
* }
|
|
75
|
+
*/
|
|
76
|
+
export interface PluginComponentsDetailed {
|
|
77
|
+
/** Skill details (name + optional description) */
|
|
78
|
+
skills?: ComponentInfo[];
|
|
79
|
+
/** Command details */
|
|
80
|
+
commands?: ComponentInfo[];
|
|
81
|
+
/** Agent details */
|
|
82
|
+
agents?: ComponentInfo[];
|
|
83
|
+
/** Hook event names */
|
|
84
|
+
hooks?: string[];
|
|
85
|
+
/** MCP server names from plugin.json mcpServers keys */
|
|
86
|
+
mcpServers?: string[];
|
|
87
|
+
/** LSP server language IDs from .lsp.json keys */
|
|
88
|
+
lspServers?: string[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Component types provided by a plugin (counts only)
|
|
7
92
|
* Detected by scanning plugin directory structure and plugin.json
|
|
8
93
|
* @example
|
|
9
94
|
* { skills: 5, commands: 2, mcpServers: 1 } // Plugin with skills, commands, and MCP
|
|
@@ -64,6 +149,8 @@ export interface Plugin {
|
|
|
64
149
|
gitCommitSha?: string;
|
|
65
150
|
/** Component types provided by this plugin (skills, commands, MCP, etc.) */
|
|
66
151
|
components?: PluginComponents;
|
|
152
|
+
/** Detailed component info with names and descriptions */
|
|
153
|
+
componentsDetailed?: PluginComponentsDetailed;
|
|
67
154
|
}
|
|
68
155
|
/**
|
|
69
156
|
* Raw installed plugin data from installed_plugins.json
|
|
@@ -98,6 +185,7 @@ export interface Marketplace {
|
|
|
98
185
|
installLocation: string;
|
|
99
186
|
lastUpdated: string;
|
|
100
187
|
pluginCount?: number;
|
|
188
|
+
autoUpdate?: boolean;
|
|
101
189
|
}
|
|
102
190
|
/**
|
|
103
191
|
* Structure of known_marketplaces.json file
|
|
@@ -111,6 +199,7 @@ export interface KnownMarketplacesFile {
|
|
|
111
199
|
};
|
|
112
200
|
installLocation: string;
|
|
113
201
|
lastUpdated: string;
|
|
202
|
+
autoUpdate?: boolean;
|
|
114
203
|
};
|
|
115
204
|
}
|
|
116
205
|
/**
|
|
@@ -128,6 +217,12 @@ export interface MarketplacePluginEntry {
|
|
|
128
217
|
homepage?: string;
|
|
129
218
|
tags?: string[];
|
|
130
219
|
keywords?: string[];
|
|
220
|
+
/** Skill paths from marketplace.json (e.g., ["./skills/xlsx"]) */
|
|
221
|
+
skills?: string[];
|
|
222
|
+
/** Agent paths from marketplace.json */
|
|
223
|
+
agents?: string[];
|
|
224
|
+
/** Command paths from marketplace.json */
|
|
225
|
+
commands?: string[];
|
|
131
226
|
}
|
|
132
227
|
/**
|
|
133
228
|
* Structure of marketplace.json file
|
|
@@ -229,6 +324,10 @@ export interface AppState {
|
|
|
229
324
|
showAddMarketplaceDialog: boolean;
|
|
230
325
|
/** Error message for add marketplace dialog */
|
|
231
326
|
addMarketplaceError: string | null;
|
|
327
|
+
/** Whether marketplace action menu is showing */
|
|
328
|
+
showMarketplaceActionMenu: boolean;
|
|
329
|
+
/** Selected index in marketplace action menu */
|
|
330
|
+
actionMenuSelectedIndex: number;
|
|
232
331
|
}
|
|
233
332
|
/**
|
|
234
333
|
* Action types for useReducer
|
|
@@ -317,4 +416,14 @@ export type Action = {
|
|
|
317
416
|
} | {
|
|
318
417
|
type: 'SET_ADD_MARKETPLACE_ERROR';
|
|
319
418
|
payload: string | null;
|
|
419
|
+
} | {
|
|
420
|
+
type: 'SHOW_MARKETPLACE_ACTION_MENU';
|
|
421
|
+
} | {
|
|
422
|
+
type: 'HIDE_MARKETPLACE_ACTION_MENU';
|
|
423
|
+
} | {
|
|
424
|
+
type: 'SET_ACTION_MENU_INDEX';
|
|
425
|
+
payload: number;
|
|
426
|
+
} | {
|
|
427
|
+
type: 'MOVE_ACTION_MENU_SELECTION';
|
|
428
|
+
payload: 'up' | 'down';
|
|
320
429
|
};
|