@laststance/claude-plugin-dashboard 0.3.0 → 0.3.2

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 (47) hide show
  1. package/README.md +1 -0
  2. package/dist/app.js +346 -211
  3. package/dist/cli.js +3 -1
  4. package/dist/components/ComponentBadges.d.ts +0 -9
  5. package/dist/components/ComponentBadges.js +0 -33
  6. package/dist/components/ComponentDetail.d.ts +32 -0
  7. package/dist/components/ComponentDetail.js +106 -0
  8. package/dist/components/ComponentList.d.ts +36 -2
  9. package/dist/components/ComponentList.js +105 -11
  10. package/dist/components/HelpOverlay.js +1 -0
  11. package/dist/components/KeyHints.d.ts +1 -0
  12. package/dist/components/KeyHints.js +8 -1
  13. package/dist/components/PluginDetail.d.ts +16 -3
  14. package/dist/components/PluginDetail.js +29 -3
  15. package/dist/services/componentService.d.ts +10 -42
  16. package/dist/services/componentService.js +19 -412
  17. package/dist/services/components/hookService.d.ts +17 -0
  18. package/dist/services/components/hookService.js +45 -0
  19. package/dist/services/components/index.d.ts +41 -0
  20. package/dist/services/components/index.js +126 -0
  21. package/dist/services/components/markdownService.d.ts +39 -0
  22. package/dist/services/components/markdownService.js +147 -0
  23. package/dist/services/components/serverService.d.ts +28 -0
  24. package/dist/services/components/serverService.js +69 -0
  25. package/dist/services/components/skillService.d.ts +48 -0
  26. package/dist/services/components/skillService.js +164 -0
  27. package/dist/services/components/utils.d.ts +23 -0
  28. package/dist/services/components/utils.js +42 -0
  29. package/dist/services/pluginActionsService.d.ts +31 -2
  30. package/dist/services/pluginActionsService.js +65 -6
  31. package/dist/store/index.d.ts +46 -0
  32. package/dist/store/index.js +47 -0
  33. package/dist/store/slices/marketplaceSlice.d.ts +344 -0
  34. package/dist/store/slices/marketplaceSlice.js +152 -0
  35. package/dist/store/slices/pluginSlice.d.ts +1544 -0
  36. package/dist/store/slices/pluginSlice.js +191 -0
  37. package/dist/store/slices/uiSlice.d.ts +147 -0
  38. package/dist/store/slices/uiSlice.js +126 -0
  39. package/dist/tabs/DiscoverTab.d.ts +8 -2
  40. package/dist/tabs/DiscoverTab.js +2 -2
  41. package/dist/tabs/EnabledTab.d.ts +8 -2
  42. package/dist/tabs/EnabledTab.js +2 -2
  43. package/dist/tabs/ErrorsTab.js +1 -1
  44. package/dist/tabs/InstalledTab.d.ts +8 -2
  45. package/dist/tabs/InstalledTab.js +2 -2
  46. package/dist/types/index.d.ts +47 -4
  47. package/package.json +7 -2
@@ -2,415 +2,22 @@
2
2
  * Component service for detecting plugin component types
3
3
  * Parses plugin.json and scans plugin directory structure to identify
4
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 (legacy location, now unified with skills in Claude Code v2.1.3+)
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
- * 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
- }
298
- /**
299
- * Count skill directories in the skills/ folder
300
- * Skills are stored as subdirectories with SKILL.md files
301
- * @param installPath - Plugin install path
302
- * @returns Number of skill directories
303
- */
304
- function countSkills(installPath) {
305
- const skillsPath = path.join(installPath, 'skills');
306
- if (!directoryExists(skillsPath)) {
307
- return 0;
308
- }
309
- try {
310
- const entries = fs.readdirSync(skillsPath, { withFileTypes: true });
311
- return entries.filter((entry) => entry.isDirectory()).length;
312
- }
313
- catch {
314
- return 0;
315
- }
316
- }
317
- /**
318
- * Count .md files in a specific directory
319
- * @param installPath - Plugin install path
320
- * @param subdir - Subdirectory name ('commands' or 'agents')
321
- * @returns Number of .md files
322
- */
323
- function countMarkdownFiles(installPath, subdir) {
324
- const dirPath = path.join(installPath, subdir);
325
- if (!directoryExists(dirPath)) {
326
- return 0;
327
- }
328
- try {
329
- const files = fs.readdirSync(dirPath);
330
- return files.filter((file) => file.endsWith('.md')).length;
331
- }
332
- catch {
333
- return 0;
334
- }
335
- }
336
- /**
337
- * Detect if plugin has hooks configured
338
- * Checks for hooks/ directory or hooks.json file
339
- * @param installPath - Plugin install path
340
- * @returns true if hooks are configured
341
- */
342
- function detectHooks(installPath) {
343
- const hooksDir = path.join(installPath, 'hooks');
344
- const hooksJson = path.join(installPath, 'hooks.json');
345
- return directoryExists(hooksDir) || fileExists(hooksJson);
346
- }
347
- /**
348
- * Count MCP servers defined in plugin.json
349
- * @param installPath - Plugin install path
350
- * @returns Number of MCP server configurations
351
- */
352
- function countMcpServers(installPath) {
353
- // Check both .claude-plugin/plugin.json and plugin.json at root
354
- const pluginJsonPaths = [
355
- path.join(installPath, '.claude-plugin', 'plugin.json'),
356
- path.join(installPath, 'plugin.json'),
357
- ];
358
- for (const pluginJsonPath of pluginJsonPaths) {
359
- const pluginJson = readJsonFile(pluginJsonPath);
360
- if (pluginJson?.mcpServers) {
361
- return Object.keys(pluginJson.mcpServers).length;
362
- }
363
- }
364
- return 0;
365
- }
366
- /**
367
- * Count LSP servers defined in .lsp.json
368
- * @param installPath - Plugin install path
369
- * @returns Number of LSP server configurations
370
- */
371
- function countLspServers(installPath) {
372
- const lspJsonPath = path.join(installPath, '.lsp.json');
373
- const lspConfig = readJsonFile(lspJsonPath);
374
- if (!lspConfig) {
375
- return 0;
376
- }
377
- return Object.keys(lspConfig).length;
378
- }
379
- /**
380
- * Check if a plugin has any components
381
- * @param components - PluginComponents object
382
- * @returns true if at least one component type is present
383
- * @example
384
- * hasAnyComponents({ skills: 2 }) // => true
385
- * hasAnyComponents({}) // => false
386
- * hasAnyComponents(undefined) // => false
387
- */
388
- export function hasAnyComponents(components) {
389
- if (!components) {
390
- return false;
391
- }
392
- return ((components.skills ?? 0) > 0 ||
393
- (components.commands ?? 0) > 0 ||
394
- (components.agents ?? 0) > 0 ||
395
- components.hooks === true ||
396
- (components.mcpServers ?? 0) > 0 ||
397
- (components.lspServers ?? 0) > 0);
398
- }
399
- /**
400
- * Get total component count for a plugin
401
- * @param components - PluginComponents object
402
- * @returns Total number of components (hooks count as 1)
403
- * @example
404
- * getTotalComponentCount({ skills: 3, commands: 2, hooks: true }) // => 6
405
- */
406
- export function getTotalComponentCount(components) {
407
- if (!components) {
408
- return 0;
409
- }
410
- return ((components.skills ?? 0) +
411
- (components.commands ?? 0) +
412
- (components.agents ?? 0) +
413
- (components.hooks ? 1 : 0) +
414
- (components.mcpServers ?? 0) +
415
- (components.lspServers ?? 0));
416
- }
5
+ *
6
+ * This is a facade module that re-exports from the components/ submodules.
7
+ * For implementation details, see:
8
+ * - components/skillService.ts - Skill detection and SKILL.md parsing
9
+ * - components/markdownService.ts - Command/Agent markdown parsing
10
+ * - components/hookService.ts - Hook detection
11
+ * - components/serverService.ts - MCP/LSP server detection
12
+ * - components/utils.ts - Utility functions
13
+ */
14
+ // Re-export everything from the components module for backward compatibility
15
+ export {
16
+ // Main detection functions
17
+ detectPluginComponents, detectComponentsDetailed,
18
+ // Skill functions
19
+ parseSkillMdFull, getSkillDetailedInfo,
20
+ // Markdown functions
21
+ getMarkdownComponentDetailedInfo,
22
+ // Utility functions
23
+ hasAnyComponents, getTotalComponentCount, } from './components/index.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Hook component detection service
3
+ * Handles hooks/ directory and hooks.json scanning
4
+ */
5
+ /**
6
+ * Detect if plugin has hooks configured
7
+ * Checks for hooks/ directory or hooks.json file
8
+ * @param installPath - Plugin install path
9
+ * @returns true if hooks are configured
10
+ */
11
+ export declare function detectHooks(installPath: string): boolean;
12
+ /**
13
+ * Get hook event names from hooks configuration
14
+ * @param installPath - Plugin install path
15
+ * @returns Array of hook event names
16
+ */
17
+ export declare function getHookNames(installPath: string): string[];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Hook component detection service
3
+ * Handles hooks/ directory and hooks.json scanning
4
+ */
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import { readJsonFile, directoryExists, fileExists } from '../fileService.js';
8
+ /**
9
+ * Detect if plugin has hooks configured
10
+ * Checks for hooks/ directory or hooks.json file
11
+ * @param installPath - Plugin install path
12
+ * @returns true if hooks are configured
13
+ */
14
+ export function detectHooks(installPath) {
15
+ const hooksDir = path.join(installPath, 'hooks');
16
+ const hooksJson = path.join(installPath, 'hooks.json');
17
+ return directoryExists(hooksDir) || fileExists(hooksJson);
18
+ }
19
+ /**
20
+ * Get hook event names from hooks configuration
21
+ * @param installPath - Plugin install path
22
+ * @returns Array of hook event names
23
+ */
24
+ export function getHookNames(installPath) {
25
+ // Try hooks.json first
26
+ const hooksJsonPath = path.join(installPath, 'hooks.json');
27
+ const hooksJson = readJsonFile(hooksJsonPath);
28
+ if (hooksJson) {
29
+ return Object.keys(hooksJson);
30
+ }
31
+ // Try hooks/ directory
32
+ const hooksDir = path.join(installPath, 'hooks');
33
+ if (directoryExists(hooksDir)) {
34
+ try {
35
+ const files = fs.readdirSync(hooksDir);
36
+ return files
37
+ .filter((f) => f.endsWith('.json') || f.endsWith('.js'))
38
+ .map((f) => f.replace(/\.(json|js)$/, ''));
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
44
+ return [];
45
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Component service module index
3
+ * Re-exports all component detection and parsing functions
4
+ *
5
+ * This module provides a facade for detecting plugin components:
6
+ * - Skills (skills/ directory with SKILL.md files)
7
+ * - Commands (commands/ directory with .md files)
8
+ * - Agents (agents/ directory with .md files)
9
+ * - Hooks (hooks/ directory or hooks.json)
10
+ * - MCP Servers (plugin.json mcpServers)
11
+ * - LSP Servers (.lsp.json)
12
+ */
13
+ export { hasAnyComponents, getTotalComponentCount } from './utils.js';
14
+ export { countSkills, getSkillDetails, parseSkillMdFull, getSkillDetailedInfo, } from './skillService.js';
15
+ export { countMarkdownFiles, getMarkdownFileDetails, getMarkdownComponentDetailedInfo, parseFirstLineDescriptionFromContent, } from './markdownService.js';
16
+ export { detectHooks, getHookNames } from './hookService.js';
17
+ export { countMcpServers, getMcpServerNames, countLspServers, getLspServerNames, } from './serverService.js';
18
+ export type { ComponentInfo, ComponentDetailedInfo } from '../../types/index.js';
19
+ import type { PluginComponents, PluginComponentsDetailed } from '../../types/index.js';
20
+ /**
21
+ * Detect all component types for a plugin at the given install path
22
+ * @param installPath - Absolute path to the installed plugin directory
23
+ * @returns PluginComponents object with detected component counts
24
+ * - Returns undefined values for components that are not present
25
+ * - Returns counts > 0 for components that exist
26
+ * @example
27
+ * const components = detectPluginComponents('/path/to/plugin')
28
+ * // => { skills: 5, commands: 2, mcpServers: 1 }
29
+ */
30
+ export declare function detectPluginComponents(installPath: string): PluginComponents | undefined;
31
+ /**
32
+ * Detect detailed components for an installed plugin
33
+ * Reads skills/, commands/, agents/ directories and parses plugin.json
34
+ * @param installPath - Absolute path to installed plugin directory
35
+ * @returns Detailed component info with names and descriptions
36
+ * - Returns undefined if path doesn't exist or has no components
37
+ * @example
38
+ * detectComponentsDetailed('/path/to/plugin')
39
+ * // => { skills: [{ name: 'xlsx', description: '...', type: 'skill' }] }
40
+ */
41
+ export declare function detectComponentsDetailed(installPath: string): PluginComponentsDetailed | undefined;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Component service module index
3
+ * Re-exports all component detection and parsing functions
4
+ *
5
+ * This module provides a facade for detecting plugin components:
6
+ * - Skills (skills/ directory with SKILL.md files)
7
+ * - Commands (commands/ directory with .md files)
8
+ * - Agents (agents/ directory with .md files)
9
+ * - Hooks (hooks/ directory or hooks.json)
10
+ * - MCP Servers (plugin.json mcpServers)
11
+ * - LSP Servers (.lsp.json)
12
+ */
13
+ // Re-export all from submodules for backward compatibility
14
+ export { hasAnyComponents, getTotalComponentCount } from './utils.js';
15
+ export { countSkills, getSkillDetails, parseSkillMdFull, getSkillDetailedInfo, } from './skillService.js';
16
+ export { countMarkdownFiles, getMarkdownFileDetails, getMarkdownComponentDetailedInfo, parseFirstLineDescriptionFromContent, } from './markdownService.js';
17
+ export { detectHooks, getHookNames } from './hookService.js';
18
+ export { countMcpServers, getMcpServerNames, countLspServers, getLspServerNames, } from './serverService.js';
19
+ import { directoryExists } from '../fileService.js';
20
+ // Import from submodules for orchestration functions
21
+ import { countSkills, getSkillDetails } from './skillService.js';
22
+ import { countMarkdownFiles, getMarkdownFileDetails, } from './markdownService.js';
23
+ import { detectHooks, getHookNames } from './hookService.js';
24
+ import { countMcpServers, getMcpServerNames, countLspServers, getLspServerNames, } from './serverService.js';
25
+ /**
26
+ * Detect all component types for a plugin at the given install path
27
+ * @param installPath - Absolute path to the installed plugin directory
28
+ * @returns PluginComponents object with detected component counts
29
+ * - Returns undefined values for components that are not present
30
+ * - Returns counts > 0 for components that exist
31
+ * @example
32
+ * const components = detectPluginComponents('/path/to/plugin')
33
+ * // => { skills: 5, commands: 2, mcpServers: 1 }
34
+ */
35
+ export function detectPluginComponents(installPath) {
36
+ if (!directoryExists(installPath)) {
37
+ return undefined;
38
+ }
39
+ const components = {};
40
+ // Detect skills (count directories in skills/ folder)
41
+ const skillsCount = countSkills(installPath);
42
+ if (skillsCount > 0) {
43
+ components.skills = skillsCount;
44
+ }
45
+ // Detect commands (legacy location, now unified with skills in Claude Code v2.1.3+)
46
+ const commandsCount = countMarkdownFiles(installPath, 'commands');
47
+ if (commandsCount > 0) {
48
+ components.commands = commandsCount;
49
+ }
50
+ // Detect agents (count .md files in agents/ folder)
51
+ const agentsCount = countMarkdownFiles(installPath, 'agents');
52
+ if (agentsCount > 0) {
53
+ components.agents = agentsCount;
54
+ }
55
+ // Detect hooks
56
+ const hasHooks = detectHooks(installPath);
57
+ if (hasHooks) {
58
+ components.hooks = true;
59
+ }
60
+ // Detect MCP servers from plugin.json
61
+ const mcpCount = countMcpServers(installPath);
62
+ if (mcpCount > 0) {
63
+ components.mcpServers = mcpCount;
64
+ }
65
+ // Detect LSP servers from .lsp.json
66
+ const lspCount = countLspServers(installPath);
67
+ if (lspCount > 0) {
68
+ components.lspServers = lspCount;
69
+ }
70
+ // Return undefined if no components detected
71
+ if (Object.keys(components).length === 0) {
72
+ return undefined;
73
+ }
74
+ return components;
75
+ }
76
+ /**
77
+ * Detect detailed components for an installed plugin
78
+ * Reads skills/, commands/, agents/ directories and parses plugin.json
79
+ * @param installPath - Absolute path to installed plugin directory
80
+ * @returns Detailed component info with names and descriptions
81
+ * - Returns undefined if path doesn't exist or has no components
82
+ * @example
83
+ * detectComponentsDetailed('/path/to/plugin')
84
+ * // => { skills: [{ name: 'xlsx', description: '...', type: 'skill' }] }
85
+ */
86
+ export function detectComponentsDetailed(installPath) {
87
+ if (!directoryExists(installPath)) {
88
+ return undefined;
89
+ }
90
+ const detailed = {};
91
+ // Skills: Read directory names + SKILL.md frontmatter
92
+ const skills = getSkillDetails(installPath);
93
+ if (skills.length > 0) {
94
+ detailed.skills = skills;
95
+ }
96
+ // Commands: Read .md filenames
97
+ const commands = getMarkdownFileDetails(installPath, 'commands', 'command');
98
+ if (commands.length > 0) {
99
+ detailed.commands = commands;
100
+ }
101
+ // Agents: Read .md filenames
102
+ const agents = getMarkdownFileDetails(installPath, 'agents', 'agent');
103
+ if (agents.length > 0) {
104
+ detailed.agents = agents;
105
+ }
106
+ // Hooks: Read event names from hooks.json or hooks/ directory
107
+ const hooks = getHookNames(installPath);
108
+ if (hooks.length > 0) {
109
+ detailed.hooks = hooks;
110
+ }
111
+ // MCP Servers: Read plugin.json mcpServers keys
112
+ const mcpServers = getMcpServerNames(installPath);
113
+ if (mcpServers.length > 0) {
114
+ detailed.mcpServers = mcpServers;
115
+ }
116
+ // LSP Servers: Read .lsp.json keys
117
+ const lspServers = getLspServerNames(installPath);
118
+ if (lspServers.length > 0) {
119
+ detailed.lspServers = lspServers;
120
+ }
121
+ // Return undefined if no components detected
122
+ if (Object.keys(detailed).length === 0) {
123
+ return undefined;
124
+ }
125
+ return detailed;
126
+ }