@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.
- package/README.md +1 -0
- package/dist/app.js +346 -211
- package/dist/cli.js +3 -1
- 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 +36 -2
- package/dist/components/ComponentList.js +105 -11
- package/dist/components/HelpOverlay.js +1 -0
- package/dist/components/KeyHints.d.ts +1 -0
- package/dist/components/KeyHints.js +8 -1
- package/dist/components/PluginDetail.d.ts +16 -3
- package/dist/components/PluginDetail.js +29 -3
- package/dist/services/componentService.d.ts +10 -42
- package/dist/services/componentService.js +19 -412
- 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/pluginActionsService.d.ts +31 -2
- package/dist/services/pluginActionsService.js +65 -6
- 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 +2 -2
- package/dist/tabs/ErrorsTab.js +1 -1
- package/dist/tabs/InstalledTab.d.ts +8 -2
- package/dist/tabs/InstalledTab.js +2 -2
- package/dist/types/index.d.ts +47 -4
- package/package.json +7 -2
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
16
16
|
*/
|
|
17
17
|
import { withFullScreen } from 'fullscreen-ink';
|
|
18
18
|
import { match, P } from 'ts-pattern';
|
|
19
|
+
import { Provider } from 'react-redux';
|
|
19
20
|
import App from './app.js';
|
|
21
|
+
import { store } from './store/index.js';
|
|
20
22
|
import { loadAllPlugins, loadInstalledPlugins, loadMarketplaces, getPluginStatistics, getPluginById, } from './services/pluginService.js';
|
|
21
23
|
import { enablePlugin, disablePlugin, togglePlugin, } from './services/settingsService.js';
|
|
22
24
|
import { fileExists } from './services/fileService.js';
|
|
@@ -307,7 +309,7 @@ else {
|
|
|
307
309
|
}
|
|
308
310
|
// Use fullscreen-ink for alternate screen buffer management
|
|
309
311
|
// This prevents rendering artifacts when switching tabs
|
|
310
|
-
const ink = withFullScreen(_jsx(App, {}));
|
|
312
|
+
const ink = withFullScreen(_jsx(Provider, { store: store, children: _jsx(App, {}) }));
|
|
311
313
|
ink.start();
|
|
312
314
|
ink.waitUntilExit();
|
|
313
315
|
}
|
|
@@ -21,12 +21,3 @@ export interface ComponentBadgesProps {
|
|
|
21
21
|
* // Renders: Skills:5 Slash:2
|
|
22
22
|
*/
|
|
23
23
|
export default function ComponentBadges({ components, }: ComponentBadgesProps): React.ReactNode;
|
|
24
|
-
/**
|
|
25
|
-
* Get a human-readable description of component types
|
|
26
|
-
* @param components - PluginComponents object
|
|
27
|
-
* @returns Formatted string describing components
|
|
28
|
-
* @example
|
|
29
|
-
* getComponentsDescription({ skills: 3, mcpServers: 1 })
|
|
30
|
-
* // => "3 skills, 1 MCP server"
|
|
31
|
-
*/
|
|
32
|
-
export declare function getComponentsDescription(components: PluginComponents | undefined): string;
|
|
@@ -48,36 +48,3 @@ export default function ComponentBadges({ components, }) {
|
|
|
48
48
|
function Badge({ label, count, color, }) {
|
|
49
49
|
return (_jsxs(Text, { children: [_jsx(Text, { color: color, bold: true, children: label }), count !== undefined && _jsxs(Text, { dimColor: true, children: [":", count] })] }));
|
|
50
50
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Get a human-readable description of component types
|
|
53
|
-
* @param components - PluginComponents object
|
|
54
|
-
* @returns Formatted string describing components
|
|
55
|
-
* @example
|
|
56
|
-
* getComponentsDescription({ skills: 3, mcpServers: 1 })
|
|
57
|
-
* // => "3 skills, 1 MCP server"
|
|
58
|
-
*/
|
|
59
|
-
export function getComponentsDescription(components) {
|
|
60
|
-
if (!components) {
|
|
61
|
-
return '';
|
|
62
|
-
}
|
|
63
|
-
const parts = [];
|
|
64
|
-
if (components.skills) {
|
|
65
|
-
parts.push(`${components.skills} skill${components.skills > 1 ? 's' : ''}`);
|
|
66
|
-
}
|
|
67
|
-
if (components.commands) {
|
|
68
|
-
parts.push(`${components.commands} command${components.commands > 1 ? 's' : ''}`);
|
|
69
|
-
}
|
|
70
|
-
if (components.agents) {
|
|
71
|
-
parts.push(`${components.agents} agent${components.agents > 1 ? 's' : ''}`);
|
|
72
|
-
}
|
|
73
|
-
if (components.hooks) {
|
|
74
|
-
parts.push('hooks');
|
|
75
|
-
}
|
|
76
|
-
if (components.mcpServers) {
|
|
77
|
-
parts.push(`${components.mcpServers} MCP server${components.mcpServers > 1 ? 's' : ''}`);
|
|
78
|
-
}
|
|
79
|
-
if (components.lspServers) {
|
|
80
|
-
parts.push(`${components.lspServers} LSP server${components.lspServers > 1 ? 's' : ''}`);
|
|
81
|
-
}
|
|
82
|
-
return parts.join(', ');
|
|
83
|
-
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComponentDetail component
|
|
3
|
+
* Displays detailed information about a selected component
|
|
4
|
+
* Shows name, type, description, allowed tools, and full content
|
|
5
|
+
*/
|
|
6
|
+
import type { ComponentDetailedInfo } from '../types/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Props for ComponentDetail
|
|
9
|
+
*/
|
|
10
|
+
export interface ComponentDetailProps {
|
|
11
|
+
/** Detailed component info to display */
|
|
12
|
+
component: ComponentDetailedInfo | null;
|
|
13
|
+
/** Maximum height for the detail panel */
|
|
14
|
+
maxHeight?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Displays detailed component information in a panel
|
|
18
|
+
* Used when user selects a component from the ComponentList
|
|
19
|
+
* Supports compact mode when maxHeight <= 4 (shows only name, type, description)
|
|
20
|
+
* @param props - ComponentDetailProps
|
|
21
|
+
* @returns React node
|
|
22
|
+
* @example
|
|
23
|
+
* <ComponentDetail
|
|
24
|
+
* component={{
|
|
25
|
+
* name: "sentry-code-review",
|
|
26
|
+
* type: "skill",
|
|
27
|
+
* description: "Analyze Sentry comments",
|
|
28
|
+
* allowedTools: ["Read", "Edit"]
|
|
29
|
+
* }}
|
|
30
|
+
* />
|
|
31
|
+
*/
|
|
32
|
+
export default function ComponentDetail({ component, maxHeight, }: ComponentDetailProps): React.ReactNode;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ComponentDetail component
|
|
4
|
+
* Displays detailed information about a selected component
|
|
5
|
+
* Shows name, type, description, allowed tools, and full content
|
|
6
|
+
*/
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { Box, Text } from 'ink';
|
|
9
|
+
/**
|
|
10
|
+
* Type badge colors for visual distinction
|
|
11
|
+
*/
|
|
12
|
+
const TYPE_COLORS = {
|
|
13
|
+
skill: 'magenta',
|
|
14
|
+
command: 'cyan',
|
|
15
|
+
agent: 'blue',
|
|
16
|
+
hook: 'yellow',
|
|
17
|
+
mcp: 'green',
|
|
18
|
+
lsp: 'blueBright',
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Type labels for display
|
|
22
|
+
*/
|
|
23
|
+
const TYPE_LABELS = {
|
|
24
|
+
skill: 'Skill',
|
|
25
|
+
command: 'Slash Command',
|
|
26
|
+
agent: 'Agent',
|
|
27
|
+
hook: 'Hook',
|
|
28
|
+
mcp: 'MCP Server',
|
|
29
|
+
lsp: 'LSP Server',
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Compact mode threshold - show minimal info when height is small
|
|
33
|
+
*/
|
|
34
|
+
const COMPACT_MODE_THRESHOLD = 4;
|
|
35
|
+
/**
|
|
36
|
+
* Displays detailed component information in a panel
|
|
37
|
+
* Used when user selects a component from the ComponentList
|
|
38
|
+
* Supports compact mode when maxHeight <= 4 (shows only name, type, description)
|
|
39
|
+
* @param props - ComponentDetailProps
|
|
40
|
+
* @returns React node
|
|
41
|
+
* @example
|
|
42
|
+
* <ComponentDetail
|
|
43
|
+
* component={{
|
|
44
|
+
* name: "sentry-code-review",
|
|
45
|
+
* type: "skill",
|
|
46
|
+
* description: "Analyze Sentry comments",
|
|
47
|
+
* allowedTools: ["Read", "Edit"]
|
|
48
|
+
* }}
|
|
49
|
+
* />
|
|
50
|
+
*/
|
|
51
|
+
export default function ComponentDetail({ component, maxHeight = 10, }) {
|
|
52
|
+
// Safe content height calculation (at least 1 line)
|
|
53
|
+
const contentHeight = Math.max(1, maxHeight - 6);
|
|
54
|
+
const isCompact = maxHeight <= COMPACT_MODE_THRESHOLD;
|
|
55
|
+
if (!component) {
|
|
56
|
+
return (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: maxHeight, children: _jsx(Text, { dimColor: true, children: "Select a component to view details" }) }));
|
|
57
|
+
}
|
|
58
|
+
const typeColor = TYPE_COLORS[component.type] || 'white';
|
|
59
|
+
const typeLabel = TYPE_LABELS[component.type] || component.type;
|
|
60
|
+
// Compact mode: minimal info only (name, type, description)
|
|
61
|
+
if (isCompact) {
|
|
62
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, height: maxHeight, overflow: "hidden", children: [_jsxs(Box, { height: 1, children: [_jsxs(Text, { bold: true, color: "white", children: ["\uD83D\uDCE6 ", truncateString(component.name, 20)] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: typeColor, bold: true, children: ["[", typeLabel, "]"] })] }), _jsx(Box, { height: 1, children: _jsx(Text, { wrap: "truncate", dimColor: true, children: component.description || 'No description' }) })] }));
|
|
63
|
+
}
|
|
64
|
+
// Full mode: show all details
|
|
65
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, height: maxHeight, overflow: "hidden", children: [_jsxs(Box, { height: 1, children: [_jsxs(Text, { bold: true, color: "white", children: ["\uD83D\uDCE6 ", component.name] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: typeColor, bold: true, children: ["[", typeLabel, "]"] })] }), component.description && (_jsx(Box, { height: 1, children: _jsx(Text, { wrap: "truncate", children: component.description }) })), component.allowedTools && component.allowedTools.length > 0 && (_jsxs(Box, { height: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Tools:", ' '] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: component.allowedTools.join(', ') })] })), component.filePath && (_jsx(Box, { height: 1, children: _jsxs(Text, { color: "gray", children: ["\uD83D\uDCC4 ", shortenPath(component.filePath)] }) })), component.fullDescription && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u2500\u2500 Content \u2500\u2500" }), _jsx(Box, { height: contentHeight, overflow: "hidden", children: _jsx(Text, { dimColor: true, wrap: "truncate", children: truncateContent(component.fullDescription, contentHeight) }) })] }))] }));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Truncate string to max length with ellipsis
|
|
69
|
+
* @param str - String to truncate
|
|
70
|
+
* @param maxLength - Maximum length
|
|
71
|
+
* @returns Truncated string
|
|
72
|
+
*/
|
|
73
|
+
function truncateString(str, maxLength) {
|
|
74
|
+
if (str.length <= maxLength) {
|
|
75
|
+
return str;
|
|
76
|
+
}
|
|
77
|
+
return str.slice(0, maxLength - 1) + '…';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Shorten file path for display (cross-platform)
|
|
81
|
+
* Shows only the last few path components
|
|
82
|
+
* @param filePath - Full file path
|
|
83
|
+
* @returns Shortened path with forward slashes for consistent display
|
|
84
|
+
*/
|
|
85
|
+
function shortenPath(filePath) {
|
|
86
|
+
const normalized = path.normalize(filePath);
|
|
87
|
+
const parts = normalized.split(path.sep);
|
|
88
|
+
// Show last 4 components: .../skills/component-name/SKILL.md
|
|
89
|
+
if (parts.length > 4) {
|
|
90
|
+
return '.../' + parts.slice(-4).join('/');
|
|
91
|
+
}
|
|
92
|
+
return parts.join('/');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Truncate multi-line content to fit within height
|
|
96
|
+
* @param content - Full content string
|
|
97
|
+
* @param maxLines - Maximum number of lines
|
|
98
|
+
* @returns Truncated content
|
|
99
|
+
*/
|
|
100
|
+
function truncateContent(content, maxLines) {
|
|
101
|
+
const lines = content.split('\n');
|
|
102
|
+
if (lines.length <= maxLines) {
|
|
103
|
+
return content;
|
|
104
|
+
}
|
|
105
|
+
return lines.slice(0, maxLines).join('\n') + '\n...';
|
|
106
|
+
}
|
|
@@ -8,7 +8,19 @@
|
|
|
8
8
|
* - Not installed: Names only from marketplace JSON (if available)
|
|
9
9
|
* - Fallback: Count-only display from PluginComponents
|
|
10
10
|
*/
|
|
11
|
-
import type { PluginComponents, PluginComponentsDetailed } from '../types/index.js';
|
|
11
|
+
import type { ComponentInfo, PluginComponents, PluginComponentsDetailed } from '../types/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* Flattened component item for selection tracking
|
|
14
|
+
* Combines type and info for easy navigation
|
|
15
|
+
*/
|
|
16
|
+
export interface FlatComponentItem {
|
|
17
|
+
/** Component info */
|
|
18
|
+
info: ComponentInfo;
|
|
19
|
+
/** Category label */
|
|
20
|
+
category: string;
|
|
21
|
+
/** Category color */
|
|
22
|
+
color: string;
|
|
23
|
+
}
|
|
12
24
|
/**
|
|
13
25
|
* Props for ComponentList
|
|
14
26
|
*/
|
|
@@ -19,19 +31,41 @@ export interface ComponentListProps {
|
|
|
19
31
|
componentsDetailed?: PluginComponentsDetailed;
|
|
20
32
|
/** Maximum visible items per category (default: 3) */
|
|
21
33
|
maxItems?: number;
|
|
34
|
+
/** Whether this list is focused for selection */
|
|
35
|
+
isFocused?: boolean;
|
|
36
|
+
/** Currently selected index in flattened list */
|
|
37
|
+
selectedIndex?: number;
|
|
38
|
+
/** Callback when selection changes */
|
|
39
|
+
onSelect?: (item: FlatComponentItem, index: number) => void;
|
|
40
|
+
/** Number of visible items in virtual scroll viewport (default: 5) */
|
|
41
|
+
visibleCount?: number;
|
|
22
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Flatten all components from detailed info into a single array for selection
|
|
45
|
+
* @param componentsDetailed - Detailed component info
|
|
46
|
+
* @returns Array of FlatComponentItem for navigation
|
|
47
|
+
* @example
|
|
48
|
+
* flattenComponents({ skills: [{ name: 'xlsx', type: 'skill' }] })
|
|
49
|
+
* // => [{ info: { name: 'xlsx', type: 'skill' }, category: 'Skills', color: 'magenta' }]
|
|
50
|
+
*/
|
|
51
|
+
export declare function flattenComponents(componentsDetailed?: PluginComponentsDetailed): FlatComponentItem[];
|
|
23
52
|
/**
|
|
24
53
|
* Displays component details in a collapsible list
|
|
25
54
|
* Shows names when available, falls back to counts
|
|
55
|
+
* Supports selection mode when isFocused is true
|
|
56
|
+
* Uses virtual scrolling when focused to prevent layout overflow
|
|
26
57
|
* @param props - ComponentListProps
|
|
27
58
|
* @returns React node or null if no components
|
|
28
59
|
* @example
|
|
29
60
|
* <ComponentList
|
|
30
61
|
* componentsDetailed={{ skills: [{ name: 'xlsx', type: 'skill' }] }}
|
|
31
62
|
* maxItems={3}
|
|
63
|
+
* isFocused={true}
|
|
64
|
+
* selectedIndex={0}
|
|
65
|
+
* visibleCount={5}
|
|
32
66
|
* />
|
|
33
67
|
*/
|
|
34
|
-
export default function ComponentList({ components, componentsDetailed, maxItems, }: ComponentListProps): React.ReactNode;
|
|
68
|
+
export default function ComponentList({ components, componentsDetailed, maxItems, isFocused, selectedIndex, visibleCount, }: ComponentListProps): React.ReactNode;
|
|
35
69
|
/**
|
|
36
70
|
* Check if PluginComponentsDetailed has any data
|
|
37
71
|
* @param detailed - Detailed components object
|
|
@@ -61,18 +61,62 @@ const CATEGORY_CONFIGS = [
|
|
|
61
61
|
* Default maximum visible items per category
|
|
62
62
|
*/
|
|
63
63
|
const DEFAULT_MAX_ITEMS = 3;
|
|
64
|
+
/**
|
|
65
|
+
* Flatten all components from detailed info into a single array for selection
|
|
66
|
+
* @param componentsDetailed - Detailed component info
|
|
67
|
+
* @returns Array of FlatComponentItem for navigation
|
|
68
|
+
* @example
|
|
69
|
+
* flattenComponents({ skills: [{ name: 'xlsx', type: 'skill' }] })
|
|
70
|
+
* // => [{ info: { name: 'xlsx', type: 'skill' }, category: 'Skills', color: 'magenta' }]
|
|
71
|
+
*/
|
|
72
|
+
export function flattenComponents(componentsDetailed) {
|
|
73
|
+
if (!componentsDetailed)
|
|
74
|
+
return [];
|
|
75
|
+
const items = [];
|
|
76
|
+
// Process each category in order
|
|
77
|
+
for (const config of CATEGORY_CONFIGS) {
|
|
78
|
+
const detailedItems = componentsDetailed[config.detailedKey];
|
|
79
|
+
if (!detailedItems?.length)
|
|
80
|
+
continue;
|
|
81
|
+
if (isComponentInfoArray(detailedItems)) {
|
|
82
|
+
for (const info of detailedItems) {
|
|
83
|
+
items.push({ info, category: config.label, color: config.color });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// String array (mcpServers, lspServers, hooks) - convert to ComponentInfo
|
|
88
|
+
for (const name of detailedItems) {
|
|
89
|
+
items.push({
|
|
90
|
+
info: { name, type: config.type },
|
|
91
|
+
category: config.label,
|
|
92
|
+
color: config.color,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return items;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Default visible count for virtual scroll
|
|
101
|
+
*/
|
|
102
|
+
const DEFAULT_VISIBLE_COUNT = 5;
|
|
64
103
|
/**
|
|
65
104
|
* Displays component details in a collapsible list
|
|
66
105
|
* Shows names when available, falls back to counts
|
|
106
|
+
* Supports selection mode when isFocused is true
|
|
107
|
+
* Uses virtual scrolling when focused to prevent layout overflow
|
|
67
108
|
* @param props - ComponentListProps
|
|
68
109
|
* @returns React node or null if no components
|
|
69
110
|
* @example
|
|
70
111
|
* <ComponentList
|
|
71
112
|
* componentsDetailed={{ skills: [{ name: 'xlsx', type: 'skill' }] }}
|
|
72
113
|
* maxItems={3}
|
|
114
|
+
* isFocused={true}
|
|
115
|
+
* selectedIndex={0}
|
|
116
|
+
* visibleCount={5}
|
|
73
117
|
* />
|
|
74
118
|
*/
|
|
75
|
-
export default function ComponentList({ components, componentsDetailed, maxItems = DEFAULT_MAX_ITEMS, }) {
|
|
119
|
+
export default function ComponentList({ components, componentsDetailed, maxItems = DEFAULT_MAX_ITEMS, isFocused = false, selectedIndex = 0, visibleCount = DEFAULT_VISIBLE_COUNT, }) {
|
|
76
120
|
// No data at all
|
|
77
121
|
if (!components && !componentsDetailed) {
|
|
78
122
|
return null;
|
|
@@ -83,6 +127,14 @@ export default function ComponentList({ components, componentsDetailed, maxItems
|
|
|
83
127
|
if (!hasDetailedData && !hasCountData) {
|
|
84
128
|
return null;
|
|
85
129
|
}
|
|
130
|
+
// When focused, use flat virtual scroll approach
|
|
131
|
+
if (isFocused && hasDetailedData) {
|
|
132
|
+
const flatItems = flattenComponents(componentsDetailed);
|
|
133
|
+
return (_jsx(VirtualScrollList, { items: flatItems, selectedIndex: selectedIndex, visibleCount: visibleCount }));
|
|
134
|
+
}
|
|
135
|
+
// Normal mode: show collapsed categories
|
|
136
|
+
// Track current index across all categories
|
|
137
|
+
let currentFlatIndex = 0;
|
|
86
138
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 0, children: _jsx(Text, { color: "cyan", bold: true, children: "\u2500\u2500 Components \u2500\u2500" }) }), CATEGORY_CONFIGS.map((config) => {
|
|
87
139
|
const detailedItems = componentsDetailed?.[config.detailedKey];
|
|
88
140
|
const count = components?.[config.countKey];
|
|
@@ -94,11 +146,15 @@ export default function ComponentList({ components, componentsDetailed, maxItems
|
|
|
94
146
|
if (detailedItems && detailedItems.length > 0) {
|
|
95
147
|
// Type guard: detailedItems could be ComponentInfo[] or string[]
|
|
96
148
|
if (isComponentInfoArray(detailedItems)) {
|
|
97
|
-
|
|
149
|
+
const startIndex = currentFlatIndex;
|
|
150
|
+
currentFlatIndex += detailedItems.length;
|
|
151
|
+
return (_jsx(CategorySection, { label: config.label, color: config.color, items: detailedItems, maxItems: maxItems, isFocused: false, selectedIndex: selectedIndex, startIndex: startIndex }, config.detailedKey));
|
|
98
152
|
}
|
|
99
153
|
else {
|
|
100
154
|
// String array (mcpServers, lspServers, hooks)
|
|
101
|
-
|
|
155
|
+
const startIndex = currentFlatIndex;
|
|
156
|
+
currentFlatIndex += detailedItems.length;
|
|
157
|
+
return (_jsx(CategorySectionSimple, { label: config.label, color: config.color, items: detailedItems, maxItems: maxItems, isFocused: false, selectedIndex: selectedIndex, startIndex: startIndex }, config.detailedKey));
|
|
102
158
|
}
|
|
103
159
|
}
|
|
104
160
|
// Count-only fallback (number) or boolean hooks fallback
|
|
@@ -112,24 +168,62 @@ export default function ComponentList({ components, componentsDetailed, maxItems
|
|
|
112
168
|
return null;
|
|
113
169
|
})] }));
|
|
114
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Virtual scroll list for focused mode
|
|
173
|
+
* Shows a fixed viewport with scroll indicators
|
|
174
|
+
* @param props - VirtualScrollListProps
|
|
175
|
+
* @returns React node
|
|
176
|
+
*/
|
|
177
|
+
function VirtualScrollList({ items, selectedIndex, visibleCount, }) {
|
|
178
|
+
const totalItems = items.length;
|
|
179
|
+
// Calculate scroll window centered on selection
|
|
180
|
+
const halfVisible = Math.floor(visibleCount / 2);
|
|
181
|
+
let startIndex = Math.max(0, selectedIndex - halfVisible);
|
|
182
|
+
const endIndex = Math.min(totalItems, startIndex + visibleCount);
|
|
183
|
+
// Adjust start if we hit the end
|
|
184
|
+
startIndex = Math.max(0, endIndex - visibleCount);
|
|
185
|
+
const visibleItems = items.slice(startIndex, endIndex);
|
|
186
|
+
const itemsAbove = startIndex;
|
|
187
|
+
const itemsBelow = totalItems - endIndex;
|
|
188
|
+
// Height: 1 header + visibleCount items + 1 indicator (always show space for indicator)
|
|
189
|
+
const listHeight = 1 + visibleCount + 1;
|
|
190
|
+
return (_jsxs(Box, { flexDirection: "column", height: listHeight, children: [_jsxs(Box, { height: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u2500\u2500 Components \u2500\u2500" }), _jsx(Text, { dimColor: true, children: " (\u2191\u2193 select, \u2190 back)" })] }), visibleItems.map((item, idx) => {
|
|
191
|
+
const actualIndex = startIndex + idx;
|
|
192
|
+
const isSelected = actualIndex === selectedIndex;
|
|
193
|
+
return (_jsx(Box, { height: 1, children: _jsxs(Text, { inverse: isSelected, children: [isSelected ? ' ▶ ' : ' ', _jsxs(Text, { color: item.color, children: ["[", item.category.charAt(0), "]"] }), ' ', item.info.name] }) }, `${item.category}-${item.info.name}`));
|
|
194
|
+
}), _jsx(Box, { height: 1, children: itemsAbove > 0 && itemsBelow > 0 ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2191", itemsAbove, " more \u00B7 \u2193", itemsBelow, " more"] })) : itemsAbove > 0 ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2191", itemsAbove, " more"] })) : itemsBelow > 0 ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2193", itemsBelow, " more"] })) : (_jsx(Text, { children: " " })) })] }));
|
|
195
|
+
}
|
|
115
196
|
/**
|
|
116
197
|
* Single category section with collapsible ComponentInfo items
|
|
117
198
|
* Shows first N items, then "+M more..." for overflow
|
|
199
|
+
* Supports selection highlighting when focused
|
|
118
200
|
* @returns React node for the category section
|
|
119
201
|
*/
|
|
120
|
-
function CategorySection({ label, color, items, maxItems, }) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
202
|
+
function CategorySection({ label, color, items, maxItems, isFocused = false, selectedIndex = 0, startIndex = 0, }) {
|
|
203
|
+
// When focused, show all items to allow selection
|
|
204
|
+
// When not focused, limit to maxItems
|
|
205
|
+
const visibleItems = isFocused ? items : items.slice(0, maxItems);
|
|
206
|
+
const remainingCount = isFocused ? 0 : items.length - maxItems;
|
|
207
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: color, bold: true, children: label }), _jsxs(Text, { dimColor: true, children: [" (", items.length, ")"] })] }), visibleItems.map((item, index) => {
|
|
208
|
+
const flatIndex = startIndex + index;
|
|
209
|
+
const isSelected = isFocused && flatIndex === selectedIndex;
|
|
210
|
+
return (_jsx(Box, { height: 1, children: _jsxs(Text, { inverse: isSelected, children: [isSelected ? ' ▶ ' : ' ', item.name, item.description && !isSelected && (_jsxs(Text, { dimColor: true, children: [" - ", truncate(item.description, 25)] }))] }) }, item.name));
|
|
211
|
+
}), remainingCount > 0 && (_jsxs(Text, { dimColor: true, children: [' ', "\u2514\u2500 +", remainingCount, " more..."] }))] }));
|
|
124
212
|
}
|
|
125
213
|
/**
|
|
126
214
|
* Category section for simple string arrays (mcpServers, lspServers, hooks)
|
|
215
|
+
* Supports selection highlighting when focused
|
|
127
216
|
* @returns React node for the category section
|
|
128
217
|
*/
|
|
129
|
-
function CategorySectionSimple({ label, color, items, maxItems, }) {
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
218
|
+
function CategorySectionSimple({ label, color, items, maxItems, isFocused = false, selectedIndex = 0, startIndex = 0, }) {
|
|
219
|
+
// When focused, show all items to allow selection
|
|
220
|
+
const visibleItems = isFocused ? items : items.slice(0, maxItems);
|
|
221
|
+
const remainingCount = isFocused ? 0 : items.length - maxItems;
|
|
222
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: color, bold: true, children: label }), _jsxs(Text, { dimColor: true, children: [" (", items.length, ")"] })] }), visibleItems.map((item, index) => {
|
|
223
|
+
const flatIndex = startIndex + index;
|
|
224
|
+
const isSelected = isFocused && flatIndex === selectedIndex;
|
|
225
|
+
return (_jsx(Box, { height: 1, children: _jsxs(Text, { inverse: isSelected, children: [isSelected ? ' ▶ ' : ' ', item] }) }, item));
|
|
226
|
+
}), remainingCount > 0 && (_jsxs(Text, { dimColor: true, children: [' ', "\u2514\u2500 +", remainingCount, " more..."] }))] }));
|
|
133
227
|
}
|
|
134
228
|
/**
|
|
135
229
|
* Category section showing only count (fallback when no detailed info)
|
|
@@ -18,6 +18,7 @@ const helpSections = [
|
|
|
18
18
|
items: [
|
|
19
19
|
{ key: 'i, Enter', description: 'Install / Toggle plugin' },
|
|
20
20
|
{ key: 'u', description: 'Uninstall plugin' },
|
|
21
|
+
{ key: 'U', description: 'Update all installed plugins' },
|
|
21
22
|
{ key: 'Space', description: 'Toggle enable/disable' },
|
|
22
23
|
{ key: 's/S', description: 'Sort options / order' },
|
|
23
24
|
],
|
|
@@ -17,6 +17,7 @@ interface KeyHintsProps {
|
|
|
17
17
|
* @example
|
|
18
18
|
* <KeyHints focusZone="list" />
|
|
19
19
|
* <KeyHints focusZone="search" extraHints={[{ key: 'i', action: 'install' }]} />
|
|
20
|
+
* <KeyHints focusZone="components" />
|
|
20
21
|
*/
|
|
21
22
|
export default function KeyHints({ extraHints, focusZone, }: KeyHintsProps): import("react/jsx-runtime").JSX.Element;
|
|
22
23
|
export {};
|
|
@@ -33,6 +33,12 @@ function getBaseHints(focusZone) {
|
|
|
33
33
|
{ key: 'Tab', action: 'next tab' },
|
|
34
34
|
{ key: 'h', action: 'help' },
|
|
35
35
|
{ key: 'q or ^C', action: 'quit' },
|
|
36
|
+
])
|
|
37
|
+
.with('components', () => [
|
|
38
|
+
{ key: '↑/↓', action: 'select component' },
|
|
39
|
+
{ key: '←', action: 'back to plugin' },
|
|
40
|
+
{ key: 'h', action: 'help' },
|
|
41
|
+
{ key: 'q or ^C', action: 'quit' },
|
|
36
42
|
])
|
|
37
43
|
.exhaustive();
|
|
38
44
|
}
|
|
@@ -41,9 +47,10 @@ function getBaseHints(focusZone) {
|
|
|
41
47
|
* @example
|
|
42
48
|
* <KeyHints focusZone="list" />
|
|
43
49
|
* <KeyHints focusZone="search" extraHints={[{ key: 'i', action: 'install' }]} />
|
|
50
|
+
* <KeyHints focusZone="components" />
|
|
44
51
|
*/
|
|
45
52
|
export default function KeyHints({ extraHints, focusZone = 'list', }) {
|
|
46
53
|
const baseHints = getBaseHints(focusZone);
|
|
47
54
|
const allHints = extraHints ? [...baseHints, ...extraHints] : baseHints;
|
|
48
|
-
return (_jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: _jsx(Box, { gap: 2, flexWrap: "wrap", children: allHints.map((hint
|
|
55
|
+
return (_jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: _jsx(Box, { gap: 2, flexWrap: "wrap", children: allHints.map((hint) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: "white", children: hint.key }), _jsx(Text, { dimColor: true, children: hint.action })] }, `${hint.key}-${hint.action}`))) }) }));
|
|
49
56
|
}
|
|
@@ -4,15 +4,28 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Uses fixed height to prevent layout jumping when switching between plugins
|
|
6
6
|
* with different amounts of content (e.g., varying component counts).
|
|
7
|
+
*
|
|
8
|
+
* Supports component focus mode for navigating and viewing component details
|
|
7
9
|
*/
|
|
8
|
-
import type { Plugin } from '../types/index.js';
|
|
10
|
+
import type { ComponentDetailedInfo, Plugin } from '../types/index.js';
|
|
9
11
|
interface PluginDetailProps {
|
|
10
12
|
plugin: Plugin | null;
|
|
13
|
+
/** Whether component focus mode is active */
|
|
14
|
+
componentFocusMode?: boolean;
|
|
15
|
+
/** Currently selected component index */
|
|
16
|
+
selectedComponentIndex?: number;
|
|
17
|
+
/** Selected component's detailed info (fetched from service) */
|
|
18
|
+
selectedComponentDetail?: ComponentDetailedInfo | null;
|
|
11
19
|
}
|
|
12
20
|
/**
|
|
13
21
|
* Displays detailed information about a selected plugin
|
|
22
|
+
* Supports component focus mode for drilling into component details
|
|
14
23
|
* @example
|
|
15
|
-
* <PluginDetail
|
|
24
|
+
* <PluginDetail
|
|
25
|
+
* plugin={selectedPlugin}
|
|
26
|
+
* componentFocusMode={true}
|
|
27
|
+
* selectedComponentIndex={0}
|
|
28
|
+
* />
|
|
16
29
|
*/
|
|
17
|
-
export default function PluginDetail({ plugin }: PluginDetailProps): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export default function PluginDetail({ plugin, componentFocusMode, selectedComponentIndex, selectedComponentDetail, }: PluginDetailProps): import("react/jsx-runtime").JSX.Element;
|
|
18
31
|
export {};
|
|
@@ -5,29 +5,55 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
*
|
|
6
6
|
* Uses fixed height to prevent layout jumping when switching between plugins
|
|
7
7
|
* with different amounts of content (e.g., varying component counts).
|
|
8
|
+
*
|
|
9
|
+
* Supports component focus mode for navigating and viewing component details
|
|
8
10
|
*/
|
|
9
11
|
import { Box, Text } from 'ink';
|
|
10
12
|
import StatusIcon from './StatusIcon.js';
|
|
11
13
|
import ComponentBadges from './ComponentBadges.js';
|
|
12
14
|
import ComponentList, { hasAnyCountComponents, hasAnyDetailedComponents, } from './ComponentList.js';
|
|
15
|
+
import ComponentDetail from './ComponentDetail.js';
|
|
13
16
|
/**
|
|
14
17
|
* Fixed height for PluginDetail panel (in terminal lines)
|
|
15
18
|
* Matches PluginList's visibleCount * 2 + 2 = 26 lines
|
|
16
19
|
* This ensures both panels have consistent height regardless of content
|
|
17
20
|
*/
|
|
18
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;
|
|
19
38
|
/**
|
|
20
39
|
* Displays detailed information about a selected plugin
|
|
40
|
+
* Supports component focus mode for drilling into component details
|
|
21
41
|
* @example
|
|
22
|
-
* <PluginDetail
|
|
42
|
+
* <PluginDetail
|
|
43
|
+
* plugin={selectedPlugin}
|
|
44
|
+
* componentFocusMode={true}
|
|
45
|
+
* selectedComponentIndex={0}
|
|
46
|
+
* />
|
|
23
47
|
*/
|
|
24
|
-
export default function PluginDetail({ plugin }) {
|
|
48
|
+
export default function PluginDetail({ plugin, componentFocusMode = false, selectedComponentIndex = 0, selectedComponentDetail = null, }) {
|
|
25
49
|
if (!plugin) {
|
|
26
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" }) }));
|
|
27
51
|
}
|
|
28
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)) ||
|
|
29
53
|
(plugin.componentsDetailed &&
|
|
30
|
-
hasAnyDetailedComponents(plugin.componentsDetailed))) && (_jsxs(_Fragment, { children: [_jsx(Box, { marginY: 1,
|
|
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"] })) }))] }));
|
|
31
57
|
}
|
|
32
58
|
/**
|
|
33
59
|
* Single detail row with label and value
|
|
@@ -2,45 +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
|
-
* Detect detailed components for an installed plugin
|
|
20
|
-
* Reads skills/, commands/, agents/ directories and parses plugin.json
|
|
21
|
-
* @param installPath - Absolute path to installed plugin directory
|
|
22
|
-
* @returns Detailed component info with names and descriptions
|
|
23
|
-
* - Returns undefined if path doesn't exist or has no components
|
|
24
|
-
* @example
|
|
25
|
-
* detectComponentsDetailed('/path/to/plugin')
|
|
26
|
-
* // => { skills: [{ name: 'xlsx', description: '...', type: 'skill' }] }
|
|
27
|
-
*/
|
|
28
|
-
export declare function detectComponentsDetailed(installPath: string): PluginComponentsDetailed | undefined;
|
|
29
|
-
/**
|
|
30
|
-
* Check if a plugin has any components
|
|
31
|
-
* @param components - PluginComponents object
|
|
32
|
-
* @returns true if at least one component type is present
|
|
33
|
-
* @example
|
|
34
|
-
* hasAnyComponents({ skills: 2 }) // => true
|
|
35
|
-
* hasAnyComponents({}) // => false
|
|
36
|
-
* hasAnyComponents(undefined) // => false
|
|
37
|
-
*/
|
|
38
|
-
export declare function hasAnyComponents(components: PluginComponents | undefined): boolean;
|
|
39
|
-
/**
|
|
40
|
-
* Get total component count for a plugin
|
|
41
|
-
* @param components - PluginComponents object
|
|
42
|
-
* @returns Total number of components (hooks count as 1)
|
|
43
|
-
* @example
|
|
44
|
-
* getTotalComponentCount({ skills: 3, commands: 2, hooks: true }) // => 6
|
|
45
|
-
*/
|
|
46
|
-
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';
|