@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.
- package/README.md +7 -1
- package/dist/app.d.ts +7 -1
- package/dist/app.js +544 -262
- package/dist/cli.js +60 -67
- package/dist/components/ComponentBadges.d.ts +0 -9
- package/dist/components/ComponentBadges.js +0 -33
- package/dist/components/ComponentDetail.d.ts +32 -0
- package/dist/components/ComponentDetail.js +106 -0
- package/dist/components/ComponentList.d.ts +87 -0
- package/dist/components/ComponentList.js +287 -0
- package/dist/components/HelpOverlay.js +1 -0
- package/dist/components/KeyHints.d.ts +1 -0
- package/dist/components/KeyHints.js +33 -29
- 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 +19 -3
- package/dist/components/PluginDetail.js +56 -6
- package/dist/components/PluginList.js +19 -7
- package/dist/services/componentService.d.ts +10 -31
- package/dist/services/componentService.js +19 -174
- package/dist/services/components/hookService.d.ts +17 -0
- package/dist/services/components/hookService.js +45 -0
- package/dist/services/components/index.d.ts +41 -0
- package/dist/services/components/index.js +126 -0
- package/dist/services/components/markdownService.d.ts +39 -0
- package/dist/services/components/markdownService.js +147 -0
- package/dist/services/components/serverService.d.ts +28 -0
- package/dist/services/components/serverService.js +69 -0
- package/dist/services/components/skillService.d.ts +48 -0
- package/dist/services/components/skillService.js +164 -0
- package/dist/services/components/utils.d.ts +23 -0
- package/dist/services/components/utils.js +42 -0
- package/dist/services/marketplaceActionsService.d.ts +17 -0
- package/dist/services/marketplaceActionsService.js +18 -0
- package/dist/services/pluginActionsService.d.ts +31 -2
- package/dist/services/pluginActionsService.js +65 -6
- package/dist/services/pluginService.js +78 -2
- package/dist/store/index.d.ts +46 -0
- package/dist/store/index.js +47 -0
- package/dist/store/slices/marketplaceSlice.d.ts +344 -0
- package/dist/store/slices/marketplaceSlice.js +152 -0
- package/dist/store/slices/pluginSlice.d.ts +1544 -0
- package/dist/store/slices/pluginSlice.js +191 -0
- package/dist/store/slices/uiSlice.d.ts +147 -0
- package/dist/store/slices/uiSlice.js +126 -0
- package/dist/tabs/DiscoverTab.d.ts +8 -2
- package/dist/tabs/DiscoverTab.js +2 -2
- package/dist/tabs/EnabledTab.d.ts +8 -2
- package/dist/tabs/EnabledTab.js +3 -3
- package/dist/tabs/ErrorsTab.js +1 -1
- package/dist/tabs/InstalledTab.d.ts +8 -2
- package/dist/tabs/InstalledTab.js +3 -3
- package/dist/tabs/MarketplacesTab.d.ts +15 -2
- package/dist/tabs/MarketplacesTab.js +13 -4
- package/dist/types/index.d.ts +157 -5
- 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
|
|
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) }),
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}),
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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;
|