@laststance/claude-plugin-dashboard 0.1.1 → 0.2.1
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 +70 -32
- package/dist/app.d.ts +29 -0
- package/dist/app.js +496 -69
- package/dist/components/AddMarketplaceDialog.d.ts +20 -0
- package/dist/components/AddMarketplaceDialog.js +18 -0
- package/dist/components/ComponentBadges.d.ts +32 -0
- package/dist/components/ComponentBadges.js +82 -0
- package/dist/components/HelpOverlay.d.ts +15 -0
- package/dist/components/HelpOverlay.js +51 -0
- package/dist/components/KeyHints.d.ts +6 -3
- package/dist/components/KeyHints.js +39 -10
- package/dist/components/MarketplaceList.d.ts +4 -2
- package/dist/components/MarketplaceList.js +7 -3
- package/dist/components/PluginDetail.js +2 -1
- package/dist/components/PluginList.d.ts +29 -2
- package/dist/components/PluginList.js +26 -5
- package/dist/components/SearchInput.js +1 -1
- package/dist/components/TabBar.d.ts +5 -3
- package/dist/components/TabBar.js +20 -8
- package/dist/services/componentService.d.ts +35 -0
- package/dist/services/componentService.js +178 -0
- package/dist/services/marketplaceActionsService.d.ts +44 -0
- package/dist/services/marketplaceActionsService.js +92 -0
- package/dist/services/pluginService.d.ts +10 -0
- package/dist/services/pluginService.js +22 -0
- package/dist/tabs/DiscoverTab.d.ts +5 -3
- package/dist/tabs/DiscoverTab.js +3 -2
- package/dist/tabs/EnabledTab.d.ts +24 -0
- package/dist/tabs/EnabledTab.js +26 -0
- package/dist/tabs/InstalledTab.d.ts +10 -3
- package/dist/tabs/InstalledTab.js +14 -10
- package/dist/tabs/MarketplacesTab.d.ts +10 -3
- package/dist/tabs/MarketplacesTab.js +12 -3
- package/dist/types/index.d.ts +71 -1
- package/package.json +11 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AddMarketplaceDialog component
|
|
3
|
+
* Dialog for adding a new marketplace source
|
|
4
|
+
*/
|
|
5
|
+
export interface AddMarketplaceDialogProps {
|
|
6
|
+
/** Current input value */
|
|
7
|
+
value: string;
|
|
8
|
+
/** Error message to display (if any) */
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Dialog for adding a new marketplace
|
|
13
|
+
* Displays input field with format hints
|
|
14
|
+
* @param value - Current input value (controlled by parent)
|
|
15
|
+
* @param error - Error message to display
|
|
16
|
+
* @returns Dialog component
|
|
17
|
+
* @example
|
|
18
|
+
* <AddMarketplaceDialog value={inputValue} />
|
|
19
|
+
*/
|
|
20
|
+
export default function AddMarketplaceDialog({ value, error, }: AddMarketplaceDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* AddMarketplaceDialog component
|
|
4
|
+
* Dialog for adding a new marketplace source
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
/**
|
|
8
|
+
* Dialog for adding a new marketplace
|
|
9
|
+
* Displays input field with format hints
|
|
10
|
+
* @param value - Current input value (controlled by parent)
|
|
11
|
+
* @param error - Error message to display
|
|
12
|
+
* @returns Dialog component
|
|
13
|
+
* @example
|
|
14
|
+
* <AddMarketplaceDialog value={inputValue} />
|
|
15
|
+
*/
|
|
16
|
+
export default function AddMarketplaceDialog({ value, error, }) {
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, marginTop: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Add Marketplace" }) }), _jsx(Text, { children: "Enter marketplace source:" }), _jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1, children: [value ? _jsx(Text, { children: value }) : _jsx(Text, { dimColor: true, children: "owner/repo" }), _jsx(Text, { color: "cyan", children: "\u258C" })] }), error && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", children: error }) })), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Supported formats:" }), _jsx(Text, { dimColor: true, children: " \u2022 owner/repo (GitHub)" }), _jsx(Text, { dimColor: true, children: " \u2022 https://github.com/org/repo" }), _jsx(Text, { dimColor: true, children: " \u2022 ./local-path" })] }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "[" }), _jsx(Text, { color: "red", children: "ESC" }), _jsx(Text, { dimColor: true, children: "] Cancel" })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "[" }), _jsx(Text, { color: "green", children: "Enter" }), _jsx(Text, { dimColor: true, children: "] Add" })] })] })] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComponentBadges component
|
|
3
|
+
* Displays plugin component type badges with icons and counts
|
|
4
|
+
* Icons: Skills(S), Commands(/), Agents(@), Hooks(H), MCP(M), LSP(L)
|
|
5
|
+
*/
|
|
6
|
+
import type { PluginComponents } from '../types/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Props for ComponentBadges
|
|
9
|
+
*/
|
|
10
|
+
export interface ComponentBadgesProps {
|
|
11
|
+
/** Component counts/flags from plugin */
|
|
12
|
+
components: PluginComponents | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Displays component type badges for a plugin
|
|
16
|
+
* Only shows badges for components that exist
|
|
17
|
+
* @param components - PluginComponents object with counts/flags
|
|
18
|
+
* @returns Badges component or null if no components
|
|
19
|
+
* @example
|
|
20
|
+
* <ComponentBadges components={{ skills: 5, commands: 2 }} />
|
|
21
|
+
* // Renders: [S:5] [/:2]
|
|
22
|
+
*/
|
|
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;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ComponentBadges component
|
|
4
|
+
* Displays plugin component type badges with icons and counts
|
|
5
|
+
* Icons: Skills(S), Commands(/), Agents(@), Hooks(H), MCP(M), LSP(L)
|
|
6
|
+
*/
|
|
7
|
+
import { Box, Text } from 'ink';
|
|
8
|
+
/**
|
|
9
|
+
* Badge configurations with intuitive abbreviations and colors
|
|
10
|
+
*/
|
|
11
|
+
const BADGE_CONFIGS = [
|
|
12
|
+
{ label: 'S', color: 'magenta', key: 'skills' },
|
|
13
|
+
{ label: '/', color: 'cyan', key: 'commands' },
|
|
14
|
+
{ label: '@', color: 'blue', key: 'agents' },
|
|
15
|
+
{ label: 'H', color: 'yellow', key: 'hooks', isBoolean: true },
|
|
16
|
+
{ label: 'M', color: 'green', key: 'mcpServers' },
|
|
17
|
+
{ label: 'L', color: 'blueBright', key: 'lspServers' },
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Displays component type badges for a plugin
|
|
21
|
+
* Only shows badges for components that exist
|
|
22
|
+
* @param components - PluginComponents object with counts/flags
|
|
23
|
+
* @returns Badges component or null if no components
|
|
24
|
+
* @example
|
|
25
|
+
* <ComponentBadges components={{ skills: 5, commands: 2 }} />
|
|
26
|
+
* // Renders: [S:5] [/:2]
|
|
27
|
+
*/
|
|
28
|
+
export default function ComponentBadges({ components, }) {
|
|
29
|
+
if (!components) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const badges = BADGE_CONFIGS.filter((config) => {
|
|
33
|
+
const value = components[config.key];
|
|
34
|
+
if (config.isBoolean) {
|
|
35
|
+
return value === true;
|
|
36
|
+
}
|
|
37
|
+
return typeof value === 'number' && value > 0;
|
|
38
|
+
});
|
|
39
|
+
if (badges.length === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return (_jsx(Box, { gap: 1, flexWrap: "wrap", children: badges.map((config) => (_jsx(Badge, { label: config.label, count: config.isBoolean ? undefined : components[config.key], color: config.color }, config.key))) }));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Single badge component
|
|
46
|
+
*/
|
|
47
|
+
function Badge({ label, count, color, }) {
|
|
48
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "[" }), _jsx(Text, { color: color, bold: true, children: label }), count !== undefined && _jsxs(Text, { dimColor: true, children: [":", count] }), _jsx(Text, { dimColor: true, children: "]" })] }));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get a human-readable description of component types
|
|
52
|
+
* @param components - PluginComponents object
|
|
53
|
+
* @returns Formatted string describing components
|
|
54
|
+
* @example
|
|
55
|
+
* getComponentsDescription({ skills: 3, mcpServers: 1 })
|
|
56
|
+
* // => "3 skills, 1 MCP server"
|
|
57
|
+
*/
|
|
58
|
+
export function getComponentsDescription(components) {
|
|
59
|
+
if (!components) {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
const parts = [];
|
|
63
|
+
if (components.skills) {
|
|
64
|
+
parts.push(`${components.skills} skill${components.skills > 1 ? 's' : ''}`);
|
|
65
|
+
}
|
|
66
|
+
if (components.commands) {
|
|
67
|
+
parts.push(`${components.commands} command${components.commands > 1 ? 's' : ''}`);
|
|
68
|
+
}
|
|
69
|
+
if (components.agents) {
|
|
70
|
+
parts.push(`${components.agents} agent${components.agents > 1 ? 's' : ''}`);
|
|
71
|
+
}
|
|
72
|
+
if (components.hooks) {
|
|
73
|
+
parts.push('hooks');
|
|
74
|
+
}
|
|
75
|
+
if (components.mcpServers) {
|
|
76
|
+
parts.push(`${components.mcpServers} MCP server${components.mcpServers > 1 ? 's' : ''}`);
|
|
77
|
+
}
|
|
78
|
+
if (components.lspServers) {
|
|
79
|
+
parts.push(`${components.lspServers} LSP server${components.lspServers > 1 ? 's' : ''}`);
|
|
80
|
+
}
|
|
81
|
+
return parts.join(', ');
|
|
82
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HelpOverlay component
|
|
3
|
+
* Displays a full-screen overlay with all available keyboard shortcuts
|
|
4
|
+
*/
|
|
5
|
+
interface HelpOverlayProps {
|
|
6
|
+
/** Whether the overlay is visible */
|
|
7
|
+
isVisible: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Full-screen help overlay showing all keyboard shortcuts
|
|
11
|
+
* @example
|
|
12
|
+
* <HelpOverlay isVisible={showHelp} />
|
|
13
|
+
*/
|
|
14
|
+
export default function HelpOverlay({ isVisible }: HelpOverlayProps): import("react/jsx-runtime").JSX.Element | null;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* HelpOverlay component
|
|
4
|
+
* Displays a full-screen overlay with all available keyboard shortcuts
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
const helpSections = [
|
|
8
|
+
{
|
|
9
|
+
title: 'Navigation',
|
|
10
|
+
items: [
|
|
11
|
+
{ key: '←/→, Tab', description: 'Switch tabs' },
|
|
12
|
+
{ key: '↑/↓, ^P/^N', description: 'Navigate list' },
|
|
13
|
+
{ key: '^F/^B', description: 'Switch tabs (Emacs)' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: 'Actions',
|
|
18
|
+
items: [
|
|
19
|
+
{ key: 'i, Enter', description: 'Install / Toggle plugin' },
|
|
20
|
+
{ key: 'u', description: 'Uninstall plugin' },
|
|
21
|
+
{ key: 'Space', description: 'Toggle enable/disable' },
|
|
22
|
+
{ key: 's/S', description: 'Sort options / order' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: 'Search',
|
|
27
|
+
items: [
|
|
28
|
+
{ key: '/', description: 'Enter search mode' },
|
|
29
|
+
{ key: 'Esc, ↓', description: 'Exit search mode' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: 'General',
|
|
34
|
+
items: [
|
|
35
|
+
{ key: 'q, ^C', description: 'Quit' },
|
|
36
|
+
{ key: 'h', description: 'Toggle this help' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Full-screen help overlay showing all keyboard shortcuts
|
|
42
|
+
* @example
|
|
43
|
+
* <HelpOverlay isVisible={showHelp} />
|
|
44
|
+
*/
|
|
45
|
+
export default function HelpOverlay({ isVisible }) {
|
|
46
|
+
if (!isVisible) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const keyWidth = 14;
|
|
50
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Help \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }) }), helpSections.map((section) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: section.title }), section.items.map((item, itemIndex) => (_jsxs(Box, { children: [_jsx(Box, { width: keyWidth, children: _jsx(Text, { color: "green", children: item.key.padEnd(keyWidth - 2) }) }), _jsx(Text, { dimColor: true, children: item.description })] }, itemIndex)))] }, section.title))), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press h or Esc to close" }) })] }));
|
|
51
|
+
}
|
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
* KeyHints component
|
|
3
3
|
* Displays keyboard shortcuts footer at the bottom of the dashboard
|
|
4
4
|
*/
|
|
5
|
+
import type { FocusZone } from '../types/index.js';
|
|
5
6
|
interface KeyHintsProps {
|
|
6
7
|
/** Additional context-specific hints */
|
|
7
8
|
extraHints?: Array<{
|
|
8
9
|
key: string;
|
|
9
10
|
action: string;
|
|
10
11
|
}>;
|
|
12
|
+
/** Current focus zone for context-aware hints */
|
|
13
|
+
focusZone?: FocusZone;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
16
|
* Displays keyboard shortcut hints in the footer
|
|
14
17
|
* @example
|
|
15
|
-
* <KeyHints />
|
|
16
|
-
* <KeyHints extraHints={[{ key: 'i', action: 'install' }]} />
|
|
18
|
+
* <KeyHints focusZone="list" />
|
|
19
|
+
* <KeyHints focusZone="search" extraHints={[{ key: 'i', action: 'install' }]} />
|
|
17
20
|
*/
|
|
18
|
-
export default function KeyHints({ extraHints }: KeyHintsProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export default function KeyHints({ extraHints, focusZone, }: KeyHintsProps): import("react/jsx-runtime").JSX.Element;
|
|
19
22
|
export {};
|
|
@@ -4,20 +4,49 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
* Displays keyboard shortcuts footer at the bottom of the dashboard
|
|
5
5
|
*/
|
|
6
6
|
import { Box, Text } from 'ink';
|
|
7
|
+
/**
|
|
8
|
+
* Get base hints based on current focus zone
|
|
9
|
+
* @param focusZone - Current focus zone
|
|
10
|
+
* @returns Array of hint objects
|
|
11
|
+
*/
|
|
12
|
+
function getBaseHints(focusZone) {
|
|
13
|
+
switch (focusZone) {
|
|
14
|
+
case 'tabbar':
|
|
15
|
+
return [
|
|
16
|
+
{ key: '←/→', action: 'switch tabs' },
|
|
17
|
+
{ key: '↓', action: 'search/list' },
|
|
18
|
+
{ key: 'Tab', action: 'next tab' },
|
|
19
|
+
{ key: 'h', action: 'help' },
|
|
20
|
+
{ key: 'q or ^C', action: 'quit' },
|
|
21
|
+
];
|
|
22
|
+
case 'search':
|
|
23
|
+
return [
|
|
24
|
+
{ key: '↑', action: 'tabs' },
|
|
25
|
+
{ key: '↓/Enter', action: 'list' },
|
|
26
|
+
{ key: 'ESC', action: 'clear/exit' },
|
|
27
|
+
{ key: 'h', action: 'help' },
|
|
28
|
+
{ key: 'q or ^C', action: 'quit' },
|
|
29
|
+
];
|
|
30
|
+
case 'list':
|
|
31
|
+
default:
|
|
32
|
+
return [
|
|
33
|
+
{ key: '↑/↓', action: 'navigate' },
|
|
34
|
+
{ key: '↑(top)', action: 'search' },
|
|
35
|
+
{ key: 'Space', action: 'toggle' },
|
|
36
|
+
{ key: 'Tab', action: 'next tab' },
|
|
37
|
+
{ key: 'h', action: 'help' },
|
|
38
|
+
{ key: 'q or ^C', action: 'quit' },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
7
42
|
/**
|
|
8
43
|
* Displays keyboard shortcut hints in the footer
|
|
9
44
|
* @example
|
|
10
|
-
* <KeyHints />
|
|
11
|
-
* <KeyHints extraHints={[{ key: 'i', action: 'install' }]} />
|
|
45
|
+
* <KeyHints focusZone="list" />
|
|
46
|
+
* <KeyHints focusZone="search" extraHints={[{ key: 'i', action: 'install' }]} />
|
|
12
47
|
*/
|
|
13
|
-
export default function KeyHints({ extraHints }) {
|
|
14
|
-
const baseHints =
|
|
15
|
-
{ key: '←/→', action: 'tabs' },
|
|
16
|
-
{ key: '↑/↓', action: 'navigate' },
|
|
17
|
-
{ key: 'Space', action: 'toggle' },
|
|
18
|
-
{ key: '/', action: 'search' },
|
|
19
|
-
{ key: 'q', action: 'quit' },
|
|
20
|
-
];
|
|
48
|
+
export default function KeyHints({ extraHints, focusZone = 'list', }) {
|
|
49
|
+
const baseHints = getBaseHints(focusZone);
|
|
21
50
|
const allHints = extraHints ? [...baseHints, ...extraHints] : baseHints;
|
|
22
51
|
return (_jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: _jsx(Box, { gap: 2, flexWrap: "wrap", children: allHints.map((hint, index) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: "white", children: hint.key }), _jsx(Text, { dimColor: true, children: hint.action })] }, index))) }) }));
|
|
23
52
|
}
|
|
@@ -6,11 +6,13 @@ import type { Marketplace } from '../types/index.js';
|
|
|
6
6
|
interface MarketplaceListProps {
|
|
7
7
|
marketplaces: Marketplace[];
|
|
8
8
|
selectedIndex: number;
|
|
9
|
+
/** Whether the list has keyboard focus */
|
|
10
|
+
isFocused?: boolean;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Displays a list of marketplaces
|
|
12
14
|
* @example
|
|
13
|
-
* <MarketplaceList marketplaces={marketplaces} selectedIndex={0} />
|
|
15
|
+
* <MarketplaceList marketplaces={marketplaces} selectedIndex={0} isFocused={true} />
|
|
14
16
|
*/
|
|
15
|
-
export default function MarketplaceList({ marketplaces, selectedIndex, }: MarketplaceListProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export default function MarketplaceList({ marketplaces, selectedIndex, isFocused, }: MarketplaceListProps): import("react/jsx-runtime").JSX.Element;
|
|
16
18
|
export {};
|
|
@@ -7,15 +7,19 @@ import { Box, Text } from 'ink';
|
|
|
7
7
|
/**
|
|
8
8
|
* Displays a list of marketplaces
|
|
9
9
|
* @example
|
|
10
|
-
* <MarketplaceList marketplaces={marketplaces} selectedIndex={0} />
|
|
10
|
+
* <MarketplaceList marketplaces={marketplaces} selectedIndex={0} isFocused={true} />
|
|
11
11
|
*/
|
|
12
|
-
export default function MarketplaceList({ marketplaces, selectedIndex, }) {
|
|
12
|
+
export default function MarketplaceList({ marketplaces, selectedIndex, isFocused = true, }) {
|
|
13
13
|
if (marketplaces.length === 0) {
|
|
14
14
|
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "No marketplaces found" }) }));
|
|
15
15
|
}
|
|
16
16
|
return (_jsx(Box, { flexDirection: "column", children: marketplaces.map((marketplace, index) => {
|
|
17
17
|
const isSelected = index === selectedIndex;
|
|
18
|
-
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected ? _jsx(Text, { color: "cyan", children: '>' }) : _jsx(Text, { children: " " }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected
|
|
18
|
+
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: " " })) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected && isFocused
|
|
19
|
+
? 'cyan'
|
|
20
|
+
: isSelected
|
|
21
|
+
? 'gray'
|
|
22
|
+
: 'white', children: marketplace.name || marketplace.id }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [marketplace.pluginCount || 0, " plugins"] })] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: getSourceDisplay(marketplace) })] })] }, marketplace.id));
|
|
19
23
|
}) }));
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
*/
|
|
6
6
|
import { Box, Text } from 'ink';
|
|
7
7
|
import StatusIcon from './StatusIcon.js';
|
|
8
|
+
import ComponentBadges from './ComponentBadges.js';
|
|
8
9
|
/**
|
|
9
10
|
* Displays detailed information about a selected plugin
|
|
10
11
|
* @example
|
|
@@ -14,7 +15,7 @@ export default function PluginDetail({ plugin }) {
|
|
|
14
15
|
if (!plugin) {
|
|
15
16
|
return (_jsx(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: "Select a plugin to view details" }) }));
|
|
16
17
|
}
|
|
17
|
-
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 }))] }), _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"] })) })] }));
|
|
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"] })) })] }));
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Single detail row with label and value
|
|
@@ -9,11 +9,38 @@ interface PluginListProps {
|
|
|
9
9
|
selectedIndex: number;
|
|
10
10
|
/** Maximum visible items (for virtual scrolling) */
|
|
11
11
|
visibleCount?: number;
|
|
12
|
+
/** Whether the list has keyboard focus */
|
|
13
|
+
isFocused?: boolean;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Scrollable plugin list with selection
|
|
15
17
|
* @example
|
|
16
|
-
* <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} />
|
|
18
|
+
* <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} isFocused={true} />
|
|
17
19
|
*/
|
|
18
|
-
export default function PluginList({ plugins, selectedIndex, visibleCount, }: PluginListProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export default function PluginList({ plugins, selectedIndex, visibleCount, isFocused, }: PluginListProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
/**
|
|
22
|
+
* Truncate text to max length with ellipsis
|
|
23
|
+
* @param text - The text to truncate
|
|
24
|
+
* @param maxLength - Maximum length including ellipsis
|
|
25
|
+
* @returns
|
|
26
|
+
* - Original text if within maxLength
|
|
27
|
+
* - Truncated text with "..." suffix if exceeds maxLength
|
|
28
|
+
* @example
|
|
29
|
+
* truncate('Hello', 10) // => 'Hello'
|
|
30
|
+
* truncate('Hello World', 8) // => 'Hello...'
|
|
31
|
+
*/
|
|
32
|
+
export declare function truncate(text: string, maxLength: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* Format large numbers with K/M suffix
|
|
35
|
+
* @param count - The number to format
|
|
36
|
+
* @returns
|
|
37
|
+
* - "X.XM" for millions (>= 1,000,000)
|
|
38
|
+
* - "X.XK" for thousands (>= 1,000)
|
|
39
|
+
* - String representation for smaller numbers
|
|
40
|
+
* @example
|
|
41
|
+
* formatCount(1500) // => '1.5K'
|
|
42
|
+
* formatCount(1200000) // => '1.2M'
|
|
43
|
+
* formatCount(500) // => '500'
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatCount(count: number): string;
|
|
19
46
|
export {};
|
|
@@ -9,9 +9,9 @@ import StatusIcon from './StatusIcon.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* Scrollable plugin list with selection
|
|
11
11
|
* @example
|
|
12
|
-
* <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} />
|
|
12
|
+
* <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} isFocused={true} />
|
|
13
13
|
*/
|
|
14
|
-
export default function PluginList({ plugins, selectedIndex, visibleCount = 15, }) {
|
|
14
|
+
export default function PluginList({ plugins, selectedIndex, visibleCount = 15, isFocused = true, }) {
|
|
15
15
|
if (plugins.length === 0) {
|
|
16
16
|
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "No plugins found" }) }));
|
|
17
17
|
}
|
|
@@ -29,21 +29,42 @@ export default function PluginList({ plugins, selectedIndex, visibleCount = 15,
|
|
|
29
29
|
return (_jsxs(Box, { flexDirection: "column", children: [hasPrevious && _jsxs(Text, { dimColor: true, children: ["\u2191 ", startIndex, " more above"] }), visiblePlugins.map((plugin, index) => {
|
|
30
30
|
const actualIndex = startIndex + index;
|
|
31
31
|
const isSelected = actualIndex === selectedIndex;
|
|
32
|
-
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected ? _jsx(Text, { color: "cyan", 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
|
|
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));
|
|
33
37
|
}), hasMore && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", plugins.length - endIndex, " more below"] }))] }));
|
|
34
38
|
}
|
|
35
39
|
/**
|
|
36
40
|
* Truncate text to max length with ellipsis
|
|
41
|
+
* @param text - The text to truncate
|
|
42
|
+
* @param maxLength - Maximum length including ellipsis
|
|
43
|
+
* @returns
|
|
44
|
+
* - Original text if within maxLength
|
|
45
|
+
* - Truncated text with "..." suffix if exceeds maxLength
|
|
46
|
+
* @example
|
|
47
|
+
* truncate('Hello', 10) // => 'Hello'
|
|
48
|
+
* truncate('Hello World', 8) // => 'Hello...'
|
|
37
49
|
*/
|
|
38
|
-
function truncate(text, maxLength) {
|
|
50
|
+
export function truncate(text, maxLength) {
|
|
39
51
|
if (text.length <= maxLength)
|
|
40
52
|
return text;
|
|
41
53
|
return text.slice(0, maxLength - 3) + '...';
|
|
42
54
|
}
|
|
43
55
|
/**
|
|
44
56
|
* Format large numbers with K/M suffix
|
|
57
|
+
* @param count - The number to format
|
|
58
|
+
* @returns
|
|
59
|
+
* - "X.XM" for millions (>= 1,000,000)
|
|
60
|
+
* - "X.XK" for thousands (>= 1,000)
|
|
61
|
+
* - String representation for smaller numbers
|
|
62
|
+
* @example
|
|
63
|
+
* formatCount(1500) // => '1.5K'
|
|
64
|
+
* formatCount(1200000) // => '1.2M'
|
|
65
|
+
* formatCount(500) // => '500'
|
|
45
66
|
*/
|
|
46
|
-
function formatCount(count) {
|
|
67
|
+
export function formatCount(count) {
|
|
47
68
|
if (count >= 1000000) {
|
|
48
69
|
return `${(count / 1000000).toFixed(1)}M`;
|
|
49
70
|
}
|
|
@@ -10,5 +10,5 @@ import { Box, Text } from 'ink';
|
|
|
10
10
|
* <SearchInput query={searchQuery} isActive={isSearchMode} />
|
|
11
11
|
*/
|
|
12
12
|
export default function SearchInput({ query, isActive = false, placeholder = 'Type to search...', }) {
|
|
13
|
-
return (_jsxs(Box, { borderStyle: isActive ? 'round' : 'single', borderColor: isActive ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(Text, { color: isActive ? 'cyan' : 'gray', children: "
|
|
13
|
+
return (_jsxs(Box, { borderStyle: isActive ? 'round' : 'single', borderColor: isActive ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(Text, { color: isActive ? 'cyan' : 'gray', children: "\uD83D\uDD0D " }), query ? _jsx(Text, { children: query }) : _jsx(Text, { dimColor: true, children: placeholder }), isActive && _jsx(Text, { color: "cyan", children: "\u258C" })] }));
|
|
14
14
|
}
|
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
* Horizontal tab navigation for the dashboard
|
|
4
4
|
* Supports ← → arrow key navigation
|
|
5
5
|
*/
|
|
6
|
-
type Tab = '
|
|
6
|
+
type Tab = 'enabled' | 'installed' | 'discover' | 'marketplaces' | 'errors';
|
|
7
7
|
interface TabBarProps {
|
|
8
8
|
activeTab: Tab;
|
|
9
9
|
onTabChange?: (tab: Tab) => void;
|
|
10
|
+
/** Whether the tab bar has keyboard focus */
|
|
11
|
+
isFocused?: boolean;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
14
|
* Horizontal tab bar component
|
|
13
15
|
* @example
|
|
14
|
-
* <TabBar activeTab="discover"
|
|
16
|
+
* <TabBar activeTab="discover" isFocused={true} />
|
|
15
17
|
*/
|
|
16
|
-
export default function TabBar({ activeTab }: TabBarProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export default function TabBar({ activeTab, isFocused }: TabBarProps): import("react/jsx-runtime").JSX.Element;
|
|
17
19
|
/**
|
|
18
20
|
* Get the next tab in the cycle
|
|
19
21
|
* @param currentTab - Current active tab
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* TabBar component
|
|
4
4
|
* Horizontal tab navigation for the dashboard
|
|
@@ -6,21 +6,33 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
6
6
|
*/
|
|
7
7
|
import { Box, Text } from 'ink';
|
|
8
8
|
const TABS = [
|
|
9
|
-
{ id: '
|
|
9
|
+
{ id: 'enabled', label: 'Enabled' },
|
|
10
10
|
{ id: 'installed', label: 'Installed' },
|
|
11
|
+
{ id: 'discover', label: 'Discover' },
|
|
11
12
|
{ id: 'marketplaces', label: 'Marketplaces' },
|
|
12
13
|
{ id: 'errors', label: 'Errors' },
|
|
13
14
|
];
|
|
15
|
+
/** Color constants for consistent theming */
|
|
16
|
+
const COLORS = {
|
|
17
|
+
/** Background color when tab bar is focused */
|
|
18
|
+
FOCUS_BG: '#1a3a4a',
|
|
19
|
+
/** Background color for active tab (not focused) */
|
|
20
|
+
ACTIVE_BG: '#333333',
|
|
21
|
+
/** Foreground color for active/focused elements */
|
|
22
|
+
ACTIVE_FG: 'cyan',
|
|
23
|
+
/** Foreground color for inactive elements */
|
|
24
|
+
INACTIVE_FG: 'gray',
|
|
25
|
+
};
|
|
14
26
|
/**
|
|
15
27
|
* Horizontal tab bar component
|
|
16
28
|
* @example
|
|
17
|
-
* <TabBar activeTab="discover"
|
|
29
|
+
* <TabBar activeTab="discover" isFocused={true} />
|
|
18
30
|
*/
|
|
19
|
-
export default function TabBar({ activeTab }) {
|
|
20
|
-
return (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
export default function TabBar({ activeTab, isFocused = false }) {
|
|
32
|
+
return (_jsxs(Box, { gap: 2, marginBottom: 1, children: [isFocused && _jsx(Text, { color: COLORS.ACTIVE_FG, children: "\u25B6" }), TABS.map((tab) => {
|
|
33
|
+
const isActive = tab.id === activeTab;
|
|
34
|
+
return (_jsx(Box, { children: isActive ? (_jsx(Text, { bold: true, color: COLORS.ACTIVE_FG, backgroundColor: isFocused ? COLORS.FOCUS_BG : COLORS.ACTIVE_BG, children: isFocused ? `[${tab.label}]` : ` ${tab.label} ` })) : (_jsx(Text, { color: COLORS.INACTIVE_FG, children: ` ${tab.label} ` })) }, tab.id));
|
|
35
|
+
})] }));
|
|
24
36
|
}
|
|
25
37
|
/**
|
|
26
38
|
* Get the next tab in the cycle
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component service for detecting plugin component types
|
|
3
|
+
* Parses plugin.json and scans plugin directory structure to identify
|
|
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;
|