@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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/app.d.ts +8 -0
  4. package/dist/app.js +481 -0
  5. package/dist/cli.d.ts +16 -0
  6. package/dist/cli.js +316 -0
  7. package/dist/components/ConfirmDialog.d.ts +14 -0
  8. package/dist/components/ConfirmDialog.js +14 -0
  9. package/dist/components/KeyHints.d.ts +19 -0
  10. package/dist/components/KeyHints.js +23 -0
  11. package/dist/components/MarketplaceDetail.d.ts +15 -0
  12. package/dist/components/MarketplaceDetail.js +39 -0
  13. package/dist/components/MarketplaceList.d.ts +16 -0
  14. package/dist/components/MarketplaceList.js +32 -0
  15. package/dist/components/PluginDetail.d.ts +15 -0
  16. package/dist/components/PluginDetail.js +52 -0
  17. package/dist/components/PluginList.d.ts +19 -0
  18. package/dist/components/PluginList.js +54 -0
  19. package/dist/components/SearchInput.d.ts +16 -0
  20. package/dist/components/SearchInput.js +14 -0
  21. package/dist/components/SortDropdown.d.ts +21 -0
  22. package/dist/components/SortDropdown.js +29 -0
  23. package/dist/components/StatusIcon.d.ts +20 -0
  24. package/dist/components/StatusIcon.js +25 -0
  25. package/dist/components/TabBar.d.ts +24 -0
  26. package/dist/components/TabBar.js +38 -0
  27. package/dist/services/fileService.d.ts +41 -0
  28. package/dist/services/fileService.js +104 -0
  29. package/dist/services/pluginActionsService.d.ts +21 -0
  30. package/dist/services/pluginActionsService.js +65 -0
  31. package/dist/services/pluginService.d.ts +66 -0
  32. package/dist/services/pluginService.js +188 -0
  33. package/dist/services/settingsService.d.ts +82 -0
  34. package/dist/services/settingsService.js +117 -0
  35. package/dist/tabs/DiscoverTab.d.ts +26 -0
  36. package/dist/tabs/DiscoverTab.js +25 -0
  37. package/dist/tabs/ErrorsTab.d.ts +16 -0
  38. package/dist/tabs/ErrorsTab.js +39 -0
  39. package/dist/tabs/InstalledTab.d.ts +16 -0
  40. package/dist/tabs/InstalledTab.js +24 -0
  41. package/dist/tabs/MarketplacesTab.d.ts +16 -0
  42. package/dist/tabs/MarketplacesTab.js +21 -0
  43. package/dist/types/index.d.ts +250 -0
  44. package/dist/types/index.js +5 -0
  45. package/dist/utils/paths.d.ts +40 -0
  46. package/dist/utils/paths.js +50 -0
  47. 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
+ }