@laststance/claude-plugin-dashboard 0.1.0
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/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/app.d.ts +8 -0
- package/dist/app.js +481 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +316 -0
- package/dist/components/ConfirmDialog.d.ts +14 -0
- package/dist/components/ConfirmDialog.js +14 -0
- package/dist/components/KeyHints.d.ts +19 -0
- package/dist/components/KeyHints.js +23 -0
- package/dist/components/MarketplaceDetail.d.ts +15 -0
- package/dist/components/MarketplaceDetail.js +39 -0
- package/dist/components/MarketplaceList.d.ts +16 -0
- package/dist/components/MarketplaceList.js +32 -0
- package/dist/components/PluginDetail.d.ts +15 -0
- package/dist/components/PluginDetail.js +52 -0
- package/dist/components/PluginList.d.ts +19 -0
- package/dist/components/PluginList.js +54 -0
- package/dist/components/SearchInput.d.ts +16 -0
- package/dist/components/SearchInput.js +14 -0
- package/dist/components/SortDropdown.d.ts +21 -0
- package/dist/components/SortDropdown.js +29 -0
- package/dist/components/StatusIcon.d.ts +20 -0
- package/dist/components/StatusIcon.js +25 -0
- package/dist/components/TabBar.d.ts +24 -0
- package/dist/components/TabBar.js +38 -0
- package/dist/services/fileService.d.ts +41 -0
- package/dist/services/fileService.js +104 -0
- package/dist/services/pluginActionsService.d.ts +21 -0
- package/dist/services/pluginActionsService.js +65 -0
- package/dist/services/pluginService.d.ts +66 -0
- package/dist/services/pluginService.js +188 -0
- package/dist/services/settingsService.d.ts +82 -0
- package/dist/services/settingsService.js +117 -0
- package/dist/tabs/DiscoverTab.d.ts +26 -0
- package/dist/tabs/DiscoverTab.js +25 -0
- package/dist/tabs/ErrorsTab.d.ts +16 -0
- package/dist/tabs/ErrorsTab.js +39 -0
- package/dist/tabs/InstalledTab.d.ts +16 -0
- package/dist/tabs/InstalledTab.js +24 -0
- package/dist/tabs/MarketplacesTab.d.ts +16 -0
- package/dist/tabs/MarketplacesTab.js +21 -0
- package/dist/types/index.d.ts +250 -0
- package/dist/types/index.js +5 -0
- package/dist/utils/paths.d.ts +40 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +60 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* SearchInput component
|
|
4
|
+
* Filter/search box for the plugin list
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
/**
|
|
8
|
+
* Search input display (read-only display, actual input handled by useInput)
|
|
9
|
+
* @example
|
|
10
|
+
* <SearchInput query={searchQuery} isActive={isSearchMode} />
|
|
11
|
+
*/
|
|
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: "Q " }), query ? _jsx(Text, { children: query }) : _jsx(Text, { dimColor: true, children: placeholder }), isActive && _jsx(Text, { color: "cyan", children: "\u258C" })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SortDropdown component
|
|
3
|
+
* Displays current sort option
|
|
4
|
+
*/
|
|
5
|
+
type SortBy = 'installs' | 'name' | 'date';
|
|
6
|
+
type SortOrder = 'asc' | 'desc';
|
|
7
|
+
interface SortDropdownProps {
|
|
8
|
+
sortBy: SortBy;
|
|
9
|
+
sortOrder: SortOrder;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Displays the current sort option
|
|
13
|
+
* @example
|
|
14
|
+
* <SortDropdown sortBy="installs" sortOrder="desc" />
|
|
15
|
+
*/
|
|
16
|
+
export default function SortDropdown({ sortBy, sortOrder }: SortDropdownProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* Get next sort option in cycle
|
|
19
|
+
*/
|
|
20
|
+
export declare function getNextSort(current: SortBy): SortBy;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* SortDropdown component
|
|
4
|
+
* Displays current sort option
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
const SORT_LABELS = {
|
|
8
|
+
installs: 'Installs',
|
|
9
|
+
name: 'Name',
|
|
10
|
+
date: 'Date',
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Displays the current sort option
|
|
14
|
+
* @example
|
|
15
|
+
* <SortDropdown sortBy="installs" sortOrder="desc" />
|
|
16
|
+
*/
|
|
17
|
+
export default function SortDropdown({ sortBy, sortOrder }) {
|
|
18
|
+
const label = SORT_LABELS[sortBy];
|
|
19
|
+
const arrow = sortOrder === 'desc' ? '▼' : '▲';
|
|
20
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "Sort:" }), _jsx(Text, { color: "cyan", children: label }), _jsx(Text, { color: "cyan", children: arrow })] }));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get next sort option in cycle
|
|
24
|
+
*/
|
|
25
|
+
export function getNextSort(current) {
|
|
26
|
+
const options = ['installs', 'name', 'date'];
|
|
27
|
+
const currentIndex = options.indexOf(current);
|
|
28
|
+
return options[(currentIndex + 1) % options.length];
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusIcon component
|
|
3
|
+
* Displays plugin status with colored icons:
|
|
4
|
+
* - ● (green) = Installed & Enabled
|
|
5
|
+
* - ◐ (yellow) = Installed & Disabled
|
|
6
|
+
* - ○ (gray) = Not installed
|
|
7
|
+
*/
|
|
8
|
+
interface StatusIconProps {
|
|
9
|
+
isInstalled: boolean;
|
|
10
|
+
isEnabled: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Displays a status icon based on plugin installation and enabled state
|
|
14
|
+
* @example
|
|
15
|
+
* <StatusIcon isInstalled={true} isEnabled={true} /> // ● (green)
|
|
16
|
+
* <StatusIcon isInstalled={true} isEnabled={false} /> // ◐ (yellow)
|
|
17
|
+
* <StatusIcon isInstalled={false} isEnabled={false} /> // ○ (gray)
|
|
18
|
+
*/
|
|
19
|
+
export default function StatusIcon({ isInstalled, isEnabled, }: StatusIconProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* StatusIcon component
|
|
4
|
+
* Displays plugin status with colored icons:
|
|
5
|
+
* - ● (green) = Installed & Enabled
|
|
6
|
+
* - ◐ (yellow) = Installed & Disabled
|
|
7
|
+
* - ○ (gray) = Not installed
|
|
8
|
+
*/
|
|
9
|
+
import { Text } from 'ink';
|
|
10
|
+
/**
|
|
11
|
+
* Displays a status icon based on plugin installation and enabled state
|
|
12
|
+
* @example
|
|
13
|
+
* <StatusIcon isInstalled={true} isEnabled={true} /> // ● (green)
|
|
14
|
+
* <StatusIcon isInstalled={true} isEnabled={false} /> // ◐ (yellow)
|
|
15
|
+
* <StatusIcon isInstalled={false} isEnabled={false} /> // ○ (gray)
|
|
16
|
+
*/
|
|
17
|
+
export default function StatusIcon({ isInstalled, isEnabled, }) {
|
|
18
|
+
if (isInstalled && isEnabled) {
|
|
19
|
+
return _jsx(Text, { color: "green", children: "\u25CF" });
|
|
20
|
+
}
|
|
21
|
+
if (isInstalled && !isEnabled) {
|
|
22
|
+
return _jsx(Text, { color: "yellow", children: "\u25D0" });
|
|
23
|
+
}
|
|
24
|
+
return _jsx(Text, { color: "gray", children: "\u25CB" });
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TabBar component
|
|
3
|
+
* Horizontal tab navigation for the dashboard
|
|
4
|
+
* Supports ← → arrow key navigation
|
|
5
|
+
*/
|
|
6
|
+
type Tab = 'discover' | 'installed' | 'marketplaces' | 'errors';
|
|
7
|
+
interface TabBarProps {
|
|
8
|
+
activeTab: Tab;
|
|
9
|
+
onTabChange?: (tab: Tab) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Horizontal tab bar component
|
|
13
|
+
* @example
|
|
14
|
+
* <TabBar activeTab="discover" onTabChange={setActiveTab} />
|
|
15
|
+
*/
|
|
16
|
+
export default function TabBar({ activeTab }: TabBarProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* Get the next tab in the cycle
|
|
19
|
+
* @param currentTab - Current active tab
|
|
20
|
+
* @param direction - Navigation direction
|
|
21
|
+
* @returns Next tab ID
|
|
22
|
+
*/
|
|
23
|
+
export declare function getNextTab(currentTab: Tab, direction: 'next' | 'prev'): Tab;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* TabBar component
|
|
4
|
+
* Horizontal tab navigation for the dashboard
|
|
5
|
+
* Supports ← → arrow key navigation
|
|
6
|
+
*/
|
|
7
|
+
import { Box, Text } from 'ink';
|
|
8
|
+
const TABS = [
|
|
9
|
+
{ id: 'discover', label: 'Discover' },
|
|
10
|
+
{ id: 'installed', label: 'Installed' },
|
|
11
|
+
{ id: 'marketplaces', label: 'Marketplaces' },
|
|
12
|
+
{ id: 'errors', label: 'Errors' },
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Horizontal tab bar component
|
|
16
|
+
* @example
|
|
17
|
+
* <TabBar activeTab="discover" onTabChange={setActiveTab} />
|
|
18
|
+
*/
|
|
19
|
+
export default function TabBar({ activeTab }) {
|
|
20
|
+
return (_jsx(Box, { gap: 2, marginBottom: 1, children: TABS.map((tab) => {
|
|
21
|
+
const isActive = tab.id === activeTab;
|
|
22
|
+
return (_jsx(Box, { children: isActive ? (_jsx(Text, { bold: true, color: "cyan", backgroundColor: "#333333", children: ` ${tab.label} ` })) : (_jsx(Text, { color: "gray", children: ` ${tab.label} ` })) }, tab.id));
|
|
23
|
+
}) }));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the next tab in the cycle
|
|
27
|
+
* @param currentTab - Current active tab
|
|
28
|
+
* @param direction - Navigation direction
|
|
29
|
+
* @returns Next tab ID
|
|
30
|
+
*/
|
|
31
|
+
export function getNextTab(currentTab, direction) {
|
|
32
|
+
const currentIndex = TABS.findIndex((t) => t.id === currentTab);
|
|
33
|
+
const tabCount = TABS.length;
|
|
34
|
+
const newIndex = direction === 'next'
|
|
35
|
+
? (currentIndex + 1) % tabCount
|
|
36
|
+
: (currentIndex - 1 + tabCount) % tabCount;
|
|
37
|
+
return TABS[newIndex].id;
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File service for safe JSON read/write operations
|
|
3
|
+
* Provides atomic writes and proper error handling
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Safely read a JSON file with error handling
|
|
7
|
+
* @param filePath - Absolute path to JSON file
|
|
8
|
+
* @returns Parsed JSON or null if file doesn't exist
|
|
9
|
+
* @throws Error if file exists but contains invalid JSON
|
|
10
|
+
* @example
|
|
11
|
+
* const data = readJsonFile<Settings>('/path/to/settings.json');
|
|
12
|
+
* // Returns parsed Settings or null
|
|
13
|
+
*/
|
|
14
|
+
export declare function readJsonFile<T>(filePath: string): T | null;
|
|
15
|
+
/**
|
|
16
|
+
* Safely write a JSON file with atomic write (write to temp, then rename)
|
|
17
|
+
* @param filePath - Absolute path to JSON file
|
|
18
|
+
* @param data - Data to write
|
|
19
|
+
* @throws Error if write fails
|
|
20
|
+
* @example
|
|
21
|
+
* writeJsonFile('/path/to/settings.json', { enabledPlugins: {} });
|
|
22
|
+
*/
|
|
23
|
+
export declare function writeJsonFile<T>(filePath: string, data: T): void;
|
|
24
|
+
/**
|
|
25
|
+
* Check if a directory exists
|
|
26
|
+
* @param dirPath - Absolute path to directory
|
|
27
|
+
* @returns true if directory exists
|
|
28
|
+
*/
|
|
29
|
+
export declare function directoryExists(dirPath: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Check if a file exists
|
|
32
|
+
* @param filePath - Absolute path to file
|
|
33
|
+
* @returns true if file exists
|
|
34
|
+
*/
|
|
35
|
+
export declare function fileExists(filePath: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* List directories in a directory
|
|
38
|
+
* @param dirPath - Absolute path to directory
|
|
39
|
+
* @returns Array of directory names (not full paths)
|
|
40
|
+
*/
|
|
41
|
+
export declare function listDirectories(dirPath: string): string[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File service for safe JSON read/write operations
|
|
3
|
+
* Provides atomic writes and proper error handling
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
/**
|
|
7
|
+
* Safely read a JSON file with error handling
|
|
8
|
+
* @param filePath - Absolute path to JSON file
|
|
9
|
+
* @returns Parsed JSON or null if file doesn't exist
|
|
10
|
+
* @throws Error if file exists but contains invalid JSON
|
|
11
|
+
* @example
|
|
12
|
+
* const data = readJsonFile<Settings>('/path/to/settings.json');
|
|
13
|
+
* // Returns parsed Settings or null
|
|
14
|
+
*/
|
|
15
|
+
export function readJsonFile(filePath) {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.existsSync(filePath)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
21
|
+
return JSON.parse(content);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof SyntaxError) {
|
|
25
|
+
throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Safely write a JSON file with atomic write (write to temp, then rename)
|
|
32
|
+
* @param filePath - Absolute path to JSON file
|
|
33
|
+
* @param data - Data to write
|
|
34
|
+
* @throws Error if write fails
|
|
35
|
+
* @example
|
|
36
|
+
* writeJsonFile('/path/to/settings.json', { enabledPlugins: {} });
|
|
37
|
+
*/
|
|
38
|
+
export function writeJsonFile(filePath, data) {
|
|
39
|
+
const tempPath = `${filePath}.tmp`;
|
|
40
|
+
try {
|
|
41
|
+
const content = JSON.stringify(data, null, 2) + '\n';
|
|
42
|
+
// Write to temp file first
|
|
43
|
+
fs.writeFileSync(tempPath, content, 'utf-8');
|
|
44
|
+
// Atomic rename
|
|
45
|
+
fs.renameSync(tempPath, filePath);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// Cleanup temp file on error
|
|
49
|
+
if (fs.existsSync(tempPath)) {
|
|
50
|
+
try {
|
|
51
|
+
fs.unlinkSync(tempPath);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Ignore cleanup errors
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Failed to write ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if a directory exists
|
|
62
|
+
* @param dirPath - Absolute path to directory
|
|
63
|
+
* @returns true if directory exists
|
|
64
|
+
*/
|
|
65
|
+
export function directoryExists(dirPath) {
|
|
66
|
+
try {
|
|
67
|
+
return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check if a file exists
|
|
75
|
+
* @param filePath - Absolute path to file
|
|
76
|
+
* @returns true if file exists
|
|
77
|
+
*/
|
|
78
|
+
export function fileExists(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* List directories in a directory
|
|
88
|
+
* @param dirPath - Absolute path to directory
|
|
89
|
+
* @returns Array of directory names (not full paths)
|
|
90
|
+
*/
|
|
91
|
+
export function listDirectories(dirPath) {
|
|
92
|
+
try {
|
|
93
|
+
if (!directoryExists(dirPath)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
return fs.readdirSync(dirPath).filter((name) => {
|
|
97
|
+
const fullPath = `${dirPath}/${name}`;
|
|
98
|
+
return fs.statSync(fullPath).isDirectory();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin actions service for install/uninstall operations
|
|
3
|
+
* Executes `claude plugin install/uninstall` as subprocess
|
|
4
|
+
*/
|
|
5
|
+
export interface PluginActionResult {
|
|
6
|
+
success: boolean;
|
|
7
|
+
message: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Install a plugin via Claude CLI
|
|
12
|
+
* @param pluginId - Plugin identifier (e.g., "context7@claude-plugins-official")
|
|
13
|
+
* @returns Promise resolving to action result
|
|
14
|
+
*/
|
|
15
|
+
export declare function installPlugin(pluginId: string): Promise<PluginActionResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Uninstall a plugin via Claude CLI
|
|
18
|
+
* @param pluginId - Plugin identifier
|
|
19
|
+
* @returns Promise resolving to action result
|
|
20
|
+
*/
|
|
21
|
+
export declare function uninstallPlugin(pluginId: string): Promise<PluginActionResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin actions service for install/uninstall operations
|
|
3
|
+
* Executes `claude plugin install/uninstall` as subprocess
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
/**
|
|
7
|
+
* Install a plugin via Claude CLI
|
|
8
|
+
* @param pluginId - Plugin identifier (e.g., "context7@claude-plugins-official")
|
|
9
|
+
* @returns Promise resolving to action result
|
|
10
|
+
*/
|
|
11
|
+
export function installPlugin(pluginId) {
|
|
12
|
+
return executePluginAction('install', pluginId);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Uninstall a plugin via Claude CLI
|
|
16
|
+
* @param pluginId - Plugin identifier
|
|
17
|
+
* @returns Promise resolving to action result
|
|
18
|
+
*/
|
|
19
|
+
export function uninstallPlugin(pluginId) {
|
|
20
|
+
return executePluginAction('uninstall', pluginId);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Execute a plugin command (install/uninstall)
|
|
24
|
+
* @param action - 'install' or 'uninstall'
|
|
25
|
+
* @param pluginId - Plugin identifier
|
|
26
|
+
* @returns Promise resolving to action result
|
|
27
|
+
*/
|
|
28
|
+
function executePluginAction(action, pluginId) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const child = spawn('claude', ['plugin', action, pluginId], {
|
|
31
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
32
|
+
shell: false,
|
|
33
|
+
});
|
|
34
|
+
let stdout = '';
|
|
35
|
+
let stderr = '';
|
|
36
|
+
child.stdout?.on('data', (data) => {
|
|
37
|
+
stdout += data.toString();
|
|
38
|
+
});
|
|
39
|
+
child.stderr?.on('data', (data) => {
|
|
40
|
+
stderr += data.toString();
|
|
41
|
+
});
|
|
42
|
+
child.on('close', (code) => {
|
|
43
|
+
if (code === 0) {
|
|
44
|
+
resolve({
|
|
45
|
+
success: true,
|
|
46
|
+
message: `${action === 'install' ? 'Installed' : 'Uninstalled'} ${pluginId}`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
resolve({
|
|
51
|
+
success: false,
|
|
52
|
+
message: `Failed to ${action} ${pluginId}`,
|
|
53
|
+
error: stderr || stdout || `Exit code: ${code}`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
child.on('error', (err) => {
|
|
58
|
+
resolve({
|
|
59
|
+
success: false,
|
|
60
|
+
message: 'Failed to execute claude command',
|
|
61
|
+
error: err.message,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin service for aggregating plugin data from multiple sources
|
|
3
|
+
* Combines data from marketplace.json, installed_plugins.json, settings.json, etc.
|
|
4
|
+
*/
|
|
5
|
+
import type { Plugin, Marketplace } from '../types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Load all plugins from all marketplaces
|
|
8
|
+
* Aggregates data from marketplace.json, installed_plugins.json, settings.json, and install-counts-cache.json
|
|
9
|
+
* @returns Array of aggregated Plugin objects sorted by install count (descending)
|
|
10
|
+
* @example
|
|
11
|
+
* const plugins = await loadAllPlugins();
|
|
12
|
+
* console.log(`Found ${plugins.length} plugins`);
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadAllPlugins(): Plugin[];
|
|
15
|
+
/**
|
|
16
|
+
* Load installed plugins only
|
|
17
|
+
* @returns Array of installed Plugin objects
|
|
18
|
+
* @example
|
|
19
|
+
* const installed = loadInstalledPlugins();
|
|
20
|
+
* console.log(`${installed.length} plugins installed`);
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadInstalledPlugins(): Plugin[];
|
|
23
|
+
/**
|
|
24
|
+
* Load enabled plugins only
|
|
25
|
+
* @returns Array of enabled Plugin objects
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadEnabledPlugins(): Plugin[];
|
|
28
|
+
/**
|
|
29
|
+
* Load all known marketplaces
|
|
30
|
+
* @returns Array of Marketplace objects
|
|
31
|
+
* @example
|
|
32
|
+
* const marketplaces = loadMarketplaces();
|
|
33
|
+
* console.log(`Found ${marketplaces.length} marketplaces`);
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadMarketplaces(): Marketplace[];
|
|
36
|
+
/**
|
|
37
|
+
* Get a single plugin by ID
|
|
38
|
+
* @param pluginId - Plugin identifier (e.g., "context7@claude-plugins-official")
|
|
39
|
+
* @returns Plugin object or undefined if not found
|
|
40
|
+
*/
|
|
41
|
+
export declare function getPluginById(pluginId: string): Plugin | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Search plugins by query
|
|
44
|
+
* @param query - Search query
|
|
45
|
+
* @param plugins - Plugins to search (defaults to all plugins)
|
|
46
|
+
* @returns Filtered plugins matching the query
|
|
47
|
+
*/
|
|
48
|
+
export declare function searchPlugins(query: string, plugins?: Plugin[]): Plugin[];
|
|
49
|
+
/**
|
|
50
|
+
* Sort plugins by field
|
|
51
|
+
* @param plugins - Plugins to sort
|
|
52
|
+
* @param sortBy - Field to sort by
|
|
53
|
+
* @param order - Sort order
|
|
54
|
+
* @returns Sorted plugins array
|
|
55
|
+
*/
|
|
56
|
+
export declare function sortPlugins(plugins: Plugin[], sortBy: 'installs' | 'name' | 'date', order: 'asc' | 'desc'): Plugin[];
|
|
57
|
+
/**
|
|
58
|
+
* Get plugin statistics
|
|
59
|
+
* @returns Object with various plugin counts
|
|
60
|
+
*/
|
|
61
|
+
export declare function getPluginStatistics(): {
|
|
62
|
+
total: number;
|
|
63
|
+
installed: number;
|
|
64
|
+
enabled: number;
|
|
65
|
+
marketplaces: number;
|
|
66
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin service for aggregating plugin data from multiple sources
|
|
3
|
+
* Combines data from marketplace.json, installed_plugins.json, settings.json, etc.
|
|
4
|
+
*/
|
|
5
|
+
import { readJsonFile, directoryExists, listDirectories, } from './fileService.js';
|
|
6
|
+
import { getEnabledPlugins } from './settingsService.js';
|
|
7
|
+
import { PATHS, getMarketplaceJsonPath } from '../utils/paths.js';
|
|
8
|
+
/**
|
|
9
|
+
* Load all plugins from all marketplaces
|
|
10
|
+
* Aggregates data from marketplace.json, installed_plugins.json, settings.json, and install-counts-cache.json
|
|
11
|
+
* @returns Array of aggregated Plugin objects sorted by install count (descending)
|
|
12
|
+
* @example
|
|
13
|
+
* const plugins = await loadAllPlugins();
|
|
14
|
+
* console.log(`Found ${plugins.length} plugins`);
|
|
15
|
+
*/
|
|
16
|
+
export function loadAllPlugins() {
|
|
17
|
+
// Load data from all sources
|
|
18
|
+
const installed = readJsonFile(PATHS.installedPlugins);
|
|
19
|
+
const counts = readJsonFile(PATHS.installCountsCache);
|
|
20
|
+
const enabledPlugins = getEnabledPlugins();
|
|
21
|
+
// Build lookup maps
|
|
22
|
+
const installedMap = new Map();
|
|
23
|
+
if (installed?.plugins) {
|
|
24
|
+
for (const [pluginId, entries] of Object.entries(installed.plugins)) {
|
|
25
|
+
if (entries[0]) {
|
|
26
|
+
installedMap.set(pluginId, entries[0]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const countsMap = new Map();
|
|
31
|
+
if (counts?.counts) {
|
|
32
|
+
for (const entry of counts.counts) {
|
|
33
|
+
countsMap.set(entry.plugin, entry.unique_installs);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Scan all marketplaces
|
|
37
|
+
const plugins = [];
|
|
38
|
+
if (directoryExists(PATHS.marketplacesDir)) {
|
|
39
|
+
const marketplaces = listDirectories(PATHS.marketplacesDir);
|
|
40
|
+
for (const marketplace of marketplaces) {
|
|
41
|
+
const manifestPath = getMarketplaceJsonPath(marketplace);
|
|
42
|
+
const manifest = readJsonFile(manifestPath);
|
|
43
|
+
if (manifest?.plugins) {
|
|
44
|
+
for (const plugin of manifest.plugins) {
|
|
45
|
+
const pluginId = `${plugin.name}@${marketplace}`;
|
|
46
|
+
const installedEntry = installedMap.get(pluginId);
|
|
47
|
+
plugins.push({
|
|
48
|
+
id: pluginId,
|
|
49
|
+
name: plugin.name,
|
|
50
|
+
marketplace,
|
|
51
|
+
description: plugin.description || '',
|
|
52
|
+
version: installedEntry?.version || plugin.version || 'unknown',
|
|
53
|
+
installCount: countsMap.get(pluginId) || 0,
|
|
54
|
+
isInstalled: installedMap.has(pluginId),
|
|
55
|
+
isEnabled: enabledPlugins[pluginId] ?? false,
|
|
56
|
+
installedAt: installedEntry?.installedAt,
|
|
57
|
+
lastUpdated: installedEntry?.lastUpdated,
|
|
58
|
+
category: plugin.category,
|
|
59
|
+
author: plugin.author,
|
|
60
|
+
homepage: plugin.homepage,
|
|
61
|
+
tags: plugin.tags || plugin.keywords,
|
|
62
|
+
isLocal: installedEntry?.isLocal,
|
|
63
|
+
gitCommitSha: installedEntry?.gitCommitSha,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Sort by install count (descending)
|
|
70
|
+
plugins.sort((a, b) => b.installCount - a.installCount);
|
|
71
|
+
return plugins;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Load installed plugins only
|
|
75
|
+
* @returns Array of installed Plugin objects
|
|
76
|
+
* @example
|
|
77
|
+
* const installed = loadInstalledPlugins();
|
|
78
|
+
* console.log(`${installed.length} plugins installed`);
|
|
79
|
+
*/
|
|
80
|
+
export function loadInstalledPlugins() {
|
|
81
|
+
const allPlugins = loadAllPlugins();
|
|
82
|
+
return allPlugins.filter((p) => p.isInstalled);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Load enabled plugins only
|
|
86
|
+
* @returns Array of enabled Plugin objects
|
|
87
|
+
*/
|
|
88
|
+
export function loadEnabledPlugins() {
|
|
89
|
+
const allPlugins = loadAllPlugins();
|
|
90
|
+
return allPlugins.filter((p) => p.isEnabled);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Load all known marketplaces
|
|
94
|
+
* @returns Array of Marketplace objects
|
|
95
|
+
* @example
|
|
96
|
+
* const marketplaces = loadMarketplaces();
|
|
97
|
+
* console.log(`Found ${marketplaces.length} marketplaces`);
|
|
98
|
+
*/
|
|
99
|
+
export function loadMarketplaces() {
|
|
100
|
+
const known = readJsonFile(PATHS.knownMarketplaces);
|
|
101
|
+
if (!known) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const marketplaces = [];
|
|
105
|
+
for (const [id, data] of Object.entries(known)) {
|
|
106
|
+
// Count plugins in this marketplace
|
|
107
|
+
const manifestPath = getMarketplaceJsonPath(id);
|
|
108
|
+
const manifest = readJsonFile(manifestPath);
|
|
109
|
+
const pluginCount = manifest?.plugins?.length || 0;
|
|
110
|
+
marketplaces.push({
|
|
111
|
+
id,
|
|
112
|
+
name: manifest?.name || id,
|
|
113
|
+
source: data.source,
|
|
114
|
+
installLocation: data.installLocation,
|
|
115
|
+
lastUpdated: data.lastUpdated,
|
|
116
|
+
pluginCount,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Sort by plugin count (descending)
|
|
120
|
+
marketplaces.sort((a, b) => (b.pluginCount || 0) - (a.pluginCount || 0));
|
|
121
|
+
return marketplaces;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get a single plugin by ID
|
|
125
|
+
* @param pluginId - Plugin identifier (e.g., "context7@claude-plugins-official")
|
|
126
|
+
* @returns Plugin object or undefined if not found
|
|
127
|
+
*/
|
|
128
|
+
export function getPluginById(pluginId) {
|
|
129
|
+
const allPlugins = loadAllPlugins();
|
|
130
|
+
return allPlugins.find((p) => p.id === pluginId);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Search plugins by query
|
|
134
|
+
* @param query - Search query
|
|
135
|
+
* @param plugins - Plugins to search (defaults to all plugins)
|
|
136
|
+
* @returns Filtered plugins matching the query
|
|
137
|
+
*/
|
|
138
|
+
export function searchPlugins(query, plugins) {
|
|
139
|
+
const allPlugins = plugins || loadAllPlugins();
|
|
140
|
+
const lowerQuery = query.toLowerCase();
|
|
141
|
+
return allPlugins.filter((p) => p.name.toLowerCase().includes(lowerQuery) ||
|
|
142
|
+
p.description.toLowerCase().includes(lowerQuery) ||
|
|
143
|
+
p.marketplace.toLowerCase().includes(lowerQuery) ||
|
|
144
|
+
p.category?.toLowerCase().includes(lowerQuery) ||
|
|
145
|
+
p.tags?.some((t) => t.toLowerCase().includes(lowerQuery)));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Sort plugins by field
|
|
149
|
+
* @param plugins - Plugins to sort
|
|
150
|
+
* @param sortBy - Field to sort by
|
|
151
|
+
* @param order - Sort order
|
|
152
|
+
* @returns Sorted plugins array
|
|
153
|
+
*/
|
|
154
|
+
export function sortPlugins(plugins, sortBy, order) {
|
|
155
|
+
const sorted = [...plugins];
|
|
156
|
+
sorted.sort((a, b) => {
|
|
157
|
+
let comparison = 0;
|
|
158
|
+
switch (sortBy) {
|
|
159
|
+
case 'installs':
|
|
160
|
+
comparison = a.installCount - b.installCount;
|
|
161
|
+
break;
|
|
162
|
+
case 'name':
|
|
163
|
+
comparison = a.name.localeCompare(b.name);
|
|
164
|
+
break;
|
|
165
|
+
case 'date':
|
|
166
|
+
const dateA = a.installedAt ? new Date(a.installedAt).getTime() : 0;
|
|
167
|
+
const dateB = b.installedAt ? new Date(b.installedAt).getTime() : 0;
|
|
168
|
+
comparison = dateA - dateB;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
return order === 'asc' ? comparison : -comparison;
|
|
172
|
+
});
|
|
173
|
+
return sorted;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get plugin statistics
|
|
177
|
+
* @returns Object with various plugin counts
|
|
178
|
+
*/
|
|
179
|
+
export function getPluginStatistics() {
|
|
180
|
+
const allPlugins = loadAllPlugins();
|
|
181
|
+
const marketplaces = loadMarketplaces();
|
|
182
|
+
return {
|
|
183
|
+
total: allPlugins.length,
|
|
184
|
+
installed: allPlugins.filter((p) => p.isInstalled).length,
|
|
185
|
+
enabled: allPlugins.filter((p) => p.isEnabled).length,
|
|
186
|
+
marketplaces: marketplaces.length,
|
|
187
|
+
};
|
|
188
|
+
}
|