@laststance/claude-plugin-dashboard 0.2.3 → 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 (58) hide show
  1. package/README.md +7 -1
  2. package/dist/app.d.ts +7 -1
  3. package/dist/app.js +544 -262
  4. package/dist/cli.js +60 -67
  5. package/dist/components/ComponentBadges.d.ts +0 -9
  6. package/dist/components/ComponentBadges.js +0 -33
  7. package/dist/components/ComponentDetail.d.ts +32 -0
  8. package/dist/components/ComponentDetail.js +106 -0
  9. package/dist/components/ComponentList.d.ts +87 -0
  10. package/dist/components/ComponentList.js +287 -0
  11. package/dist/components/HelpOverlay.js +1 -0
  12. package/dist/components/KeyHints.d.ts +1 -0
  13. package/dist/components/KeyHints.js +33 -29
  14. package/dist/components/MarketplaceActionMenu.d.ts +41 -0
  15. package/dist/components/MarketplaceActionMenu.js +68 -0
  16. package/dist/components/MarketplaceDetail.d.ts +10 -3
  17. package/dist/components/MarketplaceDetail.js +10 -4
  18. package/dist/components/PluginDetail.d.ts +19 -3
  19. package/dist/components/PluginDetail.js +56 -6
  20. package/dist/components/PluginList.js +19 -7
  21. package/dist/services/componentService.d.ts +10 -31
  22. package/dist/services/componentService.js +19 -174
  23. package/dist/services/components/hookService.d.ts +17 -0
  24. package/dist/services/components/hookService.js +45 -0
  25. package/dist/services/components/index.d.ts +41 -0
  26. package/dist/services/components/index.js +126 -0
  27. package/dist/services/components/markdownService.d.ts +39 -0
  28. package/dist/services/components/markdownService.js +147 -0
  29. package/dist/services/components/serverService.d.ts +28 -0
  30. package/dist/services/components/serverService.js +69 -0
  31. package/dist/services/components/skillService.d.ts +48 -0
  32. package/dist/services/components/skillService.js +164 -0
  33. package/dist/services/components/utils.d.ts +23 -0
  34. package/dist/services/components/utils.js +42 -0
  35. package/dist/services/marketplaceActionsService.d.ts +17 -0
  36. package/dist/services/marketplaceActionsService.js +18 -0
  37. package/dist/services/pluginActionsService.d.ts +31 -2
  38. package/dist/services/pluginActionsService.js +65 -6
  39. package/dist/services/pluginService.js +78 -2
  40. package/dist/store/index.d.ts +46 -0
  41. package/dist/store/index.js +47 -0
  42. package/dist/store/slices/marketplaceSlice.d.ts +344 -0
  43. package/dist/store/slices/marketplaceSlice.js +152 -0
  44. package/dist/store/slices/pluginSlice.d.ts +1544 -0
  45. package/dist/store/slices/pluginSlice.js +191 -0
  46. package/dist/store/slices/uiSlice.d.ts +147 -0
  47. package/dist/store/slices/uiSlice.js +126 -0
  48. package/dist/tabs/DiscoverTab.d.ts +8 -2
  49. package/dist/tabs/DiscoverTab.js +2 -2
  50. package/dist/tabs/EnabledTab.d.ts +8 -2
  51. package/dist/tabs/EnabledTab.js +3 -3
  52. package/dist/tabs/ErrorsTab.js +1 -1
  53. package/dist/tabs/InstalledTab.d.ts +8 -2
  54. package/dist/tabs/InstalledTab.js +3 -3
  55. package/dist/tabs/MarketplacesTab.d.ts +15 -2
  56. package/dist/tabs/MarketplacesTab.js +13 -4
  57. package/dist/types/index.d.ts +157 -5
  58. package/package.json +10 -3
@@ -1,27 +1,77 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * PluginDetail component
4
4
  * Right panel showing detailed plugin information
5
+ *
6
+ * Uses fixed height to prevent layout jumping when switching between plugins
7
+ * with different amounts of content (e.g., varying component counts).
8
+ *
9
+ * Supports component focus mode for navigating and viewing component details
5
10
  */
6
11
  import { Box, Text } from 'ink';
7
12
  import StatusIcon from './StatusIcon.js';
8
13
  import ComponentBadges from './ComponentBadges.js';
14
+ import ComponentList, { hasAnyCountComponents, hasAnyDetailedComponents, } from './ComponentList.js';
15
+ import ComponentDetail from './ComponentDetail.js';
16
+ /**
17
+ * Fixed height for PluginDetail panel (in terminal lines)
18
+ * Matches PluginList's visibleCount * 2 + 2 = 26 lines
19
+ * This ensures both panels have consistent height regardless of content
20
+ */
21
+ const DETAIL_PANEL_HEIGHT = 26;
22
+ /**
23
+ * Layout height constants for consistent rendering
24
+ * Total budget: 26 lines (DETAIL_PANEL_HEIGHT)
25
+ *
26
+ * Normal mode:
27
+ * Border/Padding: 4 | Header: 2 | Description: 2 | Metadata: 7
28
+ * Separator: 1 | ComponentList: 4 | Status: 3 | Actions: 3
29
+ *
30
+ * Focused mode (hide Status/Actions to maximize component browsing):
31
+ * Border/Padding: 4 | Header: 2 | Description: 2 | Metadata: 7
32
+ * Separator: 1 | ComponentList: 7 | ComponentDetail: 3
33
+ */
34
+ const COMPONENT_LIST_HEIGHT_NORMAL = 4;
35
+ const COMPONENT_LIST_HEIGHT_FOCUSED = 7;
36
+ const COMPONENT_DETAIL_HEIGHT = 3;
37
+ const COMPONENT_LIST_VISIBLE_COUNT = 5;
9
38
  /**
10
39
  * Displays detailed information about a selected plugin
40
+ * Supports component focus mode for drilling into component details
11
41
  * @example
12
- * <PluginDetail plugin={selectedPlugin} />
42
+ * <PluginDetail
43
+ * plugin={selectedPlugin}
44
+ * componentFocusMode={true}
45
+ * selectedComponentIndex={0}
46
+ * />
13
47
  */
14
- export default function PluginDetail({ plugin }) {
48
+ export default function PluginDetail({ plugin, componentFocusMode = false, selectedComponentIndex = 0, selectedComponentDetail = null, }) {
15
49
  if (!plugin) {
16
- return (_jsx(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: "Select a plugin to view details" }) }));
50
+ return (_jsx(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "gray", height: DETAIL_PANEL_HEIGHT, children: _jsx(Text, { dimColor: true, children: "Select a plugin to view details" }) }));
17
51
  }
18
- return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }), _jsx(Text, { bold: true, color: "cyan", children: plugin.name })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "wrap", children: plugin.description || 'No description available' }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(DetailRow, { label: "Marketplace", value: plugin.marketplace }), _jsx(DetailRow, { label: "Version", value: plugin.version }), _jsx(DetailRow, { label: "Installs", value: formatCount(plugin.installCount) }), plugin.category && (_jsx(DetailRow, { label: "Category", value: plugin.category })), plugin.author && (_jsx(DetailRow, { label: "Author", value: plugin.author.name })), plugin.homepage && (_jsx(DetailRow, { label: "Homepage", value: plugin.homepage })), plugin.components && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "gray", children: "Components:" }), _jsx(ComponentBadges, { components: plugin.components })] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Status:" }), plugin.isInstalled ? (plugin.isEnabled ? (_jsx(Text, { color: "green", bold: true, children: "Installed & Enabled" })) : (_jsx(Text, { color: "yellow", bold: true, children: "Installed & Disabled" }))) : (_jsx(Text, { color: "gray", children: "Not Installed" }))] }), plugin.installedAt && (_jsxs(Text, { dimColor: true, children: ["Installed: ", formatDate(plugin.installedAt)] }))] }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "column", children: plugin.isInstalled ? (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "Space" }), ' ', plugin.isEnabled ? 'disable' : 'enable', " |", ' ', _jsx(Text, { bold: true, color: "white", children: "u" }), ' ', "uninstall"] }) })) : (_jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "i" }), ' ', "install"] })) })] }));
52
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", height: DETAIL_PANEL_HEIGHT, overflow: "hidden", children: [_jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }), _jsx(Text, { bold: true, color: "cyan", children: plugin.name })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "wrap", children: plugin.description || 'No description available' }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(DetailRow, { label: "Marketplace", value: plugin.marketplace }), _jsx(DetailRow, { label: "Version", value: plugin.version || '-' }), _jsx(DetailRow, { label: "Installs", value: formatCount(plugin.installCount) }), _jsx(DetailRow, { label: "Category", value: plugin.category || '-' }), _jsx(DetailRow, { label: "Author", value: plugin.author?.name || '-' }), _jsx(DetailRow, { label: "Homepage", value: plugin.homepage || '-' }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "gray", children: 'Components:'.padEnd(12) }), plugin.components ? (_jsx(ComponentBadges, { components: plugin.components })) : (_jsx(Text, { color: "gray", children: "-" }))] })] }), ((plugin.components && hasAnyCountComponents(plugin.components)) ||
53
+ (plugin.componentsDetailed &&
54
+ hasAnyDetailedComponents(plugin.componentsDetailed))) && (_jsxs(_Fragment, { children: [_jsx(Box, { marginY: 1, height: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(36) }) }), _jsx(Box, { height: componentFocusMode
55
+ ? COMPONENT_LIST_HEIGHT_FOCUSED
56
+ : COMPONENT_LIST_HEIGHT_NORMAL, overflow: "hidden", children: _jsx(ComponentList, { components: plugin.components, componentsDetailed: plugin.componentsDetailed, maxItems: 3, isFocused: componentFocusMode, selectedIndex: selectedComponentIndex, visibleCount: COMPONENT_LIST_VISIBLE_COUNT }) })] })), componentFocusMode && selectedComponentDetail && (_jsx(Box, { height: COMPONENT_DETAIL_HEIGHT, overflow: "hidden", children: _jsx(ComponentDetail, { component: selectedComponentDetail, maxHeight: COMPONENT_DETAIL_HEIGHT }) })), !componentFocusMode && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Status:" }), plugin.isInstalled ? (plugin.isEnabled ? (_jsx(Text, { color: "green", bold: true, wrap: "truncate", children: "Enabled" })) : (_jsx(Text, { color: "yellow", bold: true, wrap: "truncate", children: "Disabled" }))) : (_jsx(Text, { color: "gray", wrap: "truncate", children: "Not Installed" }))] }), plugin.installedAt && (_jsxs(Text, { dimColor: true, wrap: "truncate", children: ["Installed: ", formatDate(plugin.installedAt)] }))] })), !componentFocusMode && (_jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "column", children: plugin.isInstalled ? (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "Space" }), ' ', plugin.isEnabled ? 'disable' : 'enable', " |", ' ', _jsx(Text, { bold: true, color: "white", children: "u" }), ' ', "uninstall |", ' ', _jsx(Text, { bold: true, color: "white", children: "\u2192" }), ' ', "components"] }) })) : (_jsxs(Text, { dimColor: true, children: [_jsx(Text, { bold: true, color: "white", children: "i" }), ' ', "install |", ' ', _jsx(Text, { bold: true, color: "white", children: "\u2192" }), ' ', "components"] })) }))] }));
19
57
  }
20
58
  /**
21
59
  * Single detail row with label and value
60
+ * Pads both label and value to fixed widths to prevent ghost text artifacts
61
+ * when content changes between selections
62
+ * @param label - Label text (e.g., "Marketplace", "Version")
63
+ * @param value - Value to display
64
+ * @returns Detail row with fixed-width padding to overwrite previous content
22
65
  */
23
66
  function DetailRow({ label, value }) {
24
- return (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: "gray", children: [label, ":"] }), _jsx(Text, { children: value })] }));
67
+ // Fixed widths to ensure consistent line length and prevent ghost text
68
+ const LABEL_WIDTH = 12;
69
+ const VALUE_WIDTH = 40;
70
+ const paddedLabel = `${label}:`.padEnd(LABEL_WIDTH);
71
+ // Truncate long values, pad short values with spaces to overwrite old content
72
+ const truncatedValue = value.length > VALUE_WIDTH ? value.slice(0, VALUE_WIDTH - 1) + '…' : value;
73
+ const paddedValue = truncatedValue.padEnd(VALUE_WIDTH);
74
+ return (_jsxs(Text, { children: [_jsx(Text, { color: "gray", children: paddedLabel }), _jsx(Text, { children: paddedValue })] }));
25
75
  }
26
76
  /**
27
77
  * Format large numbers with K/M suffix
@@ -26,15 +26,27 @@ export default function PluginList({ plugins, selectedIndex, visibleCount = 15,
26
26
  const visiblePlugins = plugins.slice(startIndex, endIndex);
27
27
  const hasMore = endIndex < plugins.length;
28
28
  const hasPrevious = startIndex > 0;
29
- return (_jsxs(Box, { flexDirection: "column", children: [hasPrevious && _jsxs(Text, { dimColor: true, children: ["\u2191 ", startIndex, " more above"] }), visiblePlugins.map((plugin, index) => {
29
+ // Calculate fixed height: each item is 2 lines + 1 line for top indicator + 1 line for bottom indicator
30
+ // Total fixed lines: visibleCount * 2 (items) + 2 (indicators)
31
+ const totalLines = visibleCount * 2 + 2;
32
+ // Lines used by items
33
+ const itemLines = visiblePlugins.length * 2;
34
+ // Lines used by indicators (always rendered, but may be empty)
35
+ const topIndicatorLine = 1;
36
+ const bottomIndicatorLine = 1;
37
+ // Calculate padding lines needed to maintain fixed height
38
+ const usedLines = itemLines + topIndicatorLine + bottomIndicatorLine;
39
+ const paddingLines = Math.max(0, totalLines - usedLines);
40
+ return (_jsxs(Box, { flexDirection: "column", height: totalLines, children: [_jsx(Text, { dimColor: true, children: hasPrevious ? `↑ ${startIndex} more above` : ' ' }), visiblePlugins.map((plugin, index) => {
30
41
  const actualIndex = startIndex + index;
31
42
  const isSelected = actualIndex === selectedIndex;
32
- return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected && isFocused ? (_jsx(Text, { color: "cyan", children: '>' })) : isSelected ? (_jsx(Text, { color: "gray", children: '›' })) : (_jsx(Text, { children: " " })) }), _jsx(Box, { width: 2, children: _jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected && isFocused
33
- ? 'cyan'
34
- : isSelected
35
- ? 'gray'
36
- : 'white', children: plugin.name }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { color: "gray", children: truncate(plugin.marketplace, 20) }), plugin.installCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [formatCount(plugin.installCount), " installs"] })] }))] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: truncate(plugin.description, 60) })] })] }, plugin.id));
37
- }), hasMore && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", plugins.length - endIndex, " more below"] }))] }));
43
+ return (_jsxs(Box, { paddingX: 1, height: 2, children: [_jsx(Box, { width: 2, children: isSelected && isFocused ? (_jsx(Text, { color: "cyan", children: '>' })) : isSelected ? (_jsx(Text, { color: "gray", children: '›' })) : (_jsx(Text, { children: " " })) }), _jsx(Box, { width: 2, children: _jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }) }), _jsxs(Box, { flexDirection: "column", overflow: "hidden", children: [_jsx(Box, { height: 1, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { bold: true, color: isSelected && isFocused
44
+ ? 'cyan'
45
+ : isSelected
46
+ ? 'gray'
47
+ : 'white', children: plugin.name }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Text, { color: "gray", children: truncate(plugin.marketplace, 20) }), plugin.installCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsxs(Text, { color: "gray", children: [formatCount(plugin.installCount), " installs"] })] }))] }) }), _jsx(Box, { height: 1, children: _jsx(Text, { dimColor: true, wrap: "truncate", children: truncate(plugin.description, 60) }) })] })] }, plugin.id));
48
+ }), paddingLines > 0 &&
49
+ Array.from({ length: paddingLines }).map((_, i) => (_jsx(Text, { children: " " }, `pad-${i}`))), _jsx(Text, { dimColor: true, children: hasMore ? `↓ ${plugins.length - endIndex} more below` : ' ' })] }));
38
50
  }
39
51
  /**
40
52
  * Truncate text to max length with ellipsis
@@ -2,34 +2,13 @@
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 type { PluginComponents } from '../types/index.js';
7
- /**
8
- * Detect all component types for a plugin at the given install path
9
- * @param installPath - Absolute path to the installed plugin directory
10
- * @returns PluginComponents object with detected component counts
11
- * - Returns undefined values for components that are not present
12
- * - Returns counts > 0 for components that exist
13
- * @example
14
- * const components = detectPluginComponents('/path/to/plugin')
15
- * // => { skills: 5, commands: 2, mcpServers: 1 }
16
- */
17
- export declare function detectPluginComponents(installPath: string): PluginComponents | undefined;
18
- /**
19
- * Check if a plugin has any components
20
- * @param components - PluginComponents object
21
- * @returns true if at least one component type is present
22
- * @example
23
- * hasAnyComponents({ skills: 2 }) // => true
24
- * hasAnyComponents({}) // => false
25
- * hasAnyComponents(undefined) // => false
26
- */
27
- export declare function hasAnyComponents(components: PluginComponents | undefined): boolean;
28
- /**
29
- * Get total component count for a plugin
30
- * @param components - PluginComponents object
31
- * @returns Total number of components (hooks count as 1)
32
- * @example
33
- * getTotalComponentCount({ skills: 3, commands: 2, hooks: true }) // => 6
34
- */
35
- export declare function getTotalComponentCount(components: PluginComponents | undefined): number;
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
+ export { detectPluginComponents, detectComponentsDetailed, parseSkillMdFull, getSkillDetailedInfo, getMarkdownComponentDetailedInfo, hasAnyComponents, getTotalComponentCount, } from './components/index.js';
@@ -2,177 +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
- * 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
- }
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
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Markdown component detection and parsing service
3
+ * Handles commands/ and agents/ directory scanning
4
+ */
5
+ import type { ComponentInfo, ComponentDetailedInfo } from '../../types/index.js';
6
+ /**
7
+ * Count .md files in a specific directory
8
+ * @param installPath - Plugin install path
9
+ * @param subdir - Subdirectory name ('commands' or 'agents')
10
+ * @returns Number of .md files
11
+ */
12
+ export declare function countMarkdownFiles(installPath: string, subdir: string): number;
13
+ /**
14
+ * Get component details from .md files in a directory
15
+ * Uses filename (minus extension) as component name
16
+ * @param installPath - Plugin install path
17
+ * @param subdir - Subdirectory name ('commands' or 'agents')
18
+ * @param type - Component type
19
+ * @returns Array of ComponentInfo
20
+ */
21
+ export declare function getMarkdownFileDetails(installPath: string, subdir: string, type: 'command' | 'agent'): ComponentInfo[];
22
+ /**
23
+ * Get detailed info for a command or agent markdown file
24
+ * @param installPath - Plugin install path
25
+ * @param componentName - Name of the component (without .md)
26
+ * @param type - Component type ('command' or 'agent')
27
+ * @returns ComponentDetailedInfo with full content
28
+ */
29
+ export declare function getMarkdownComponentDetailedInfo(installPath: string, componentName: string, type: 'command' | 'agent'): ComponentDetailedInfo | undefined;
30
+ /**
31
+ * Parse first non-empty line of content as description
32
+ * Properly skips YAML frontmatter block and strips heading markers
33
+ * @param content - Markdown content string
34
+ * @returns First non-frontmatter, non-empty line or undefined
35
+ * @example
36
+ * parseFirstLineDescriptionFromContent("---\nname: test\n---\n# My Title\n")
37
+ * // => "My Title"
38
+ */
39
+ export declare function parseFirstLineDescriptionFromContent(content: string): string | undefined;