@laststance/claude-plugin-dashboard 0.2.0 → 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 +6 -1
- package/dist/app.js +270 -30
- 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/PluginDetail.js +2 -1
- 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.js +6 -0
- package/dist/types/index.d.ts +56 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Claude Code Plugin Dashboard
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@laststance/claude-plugin-dashboard)
|
|
4
|
+
[](https://github.com/laststance/claude-plugin-dashboard/actions/workflows/ci.yml)
|
|
5
|
+
[](https://codecov.io/gh/laststance/claude-plugin-dashboard)
|
|
4
6
|
[](https://opensource.org/licenses/MIT)
|
|
5
7
|
|
|
6
8
|
An interactive CLI dashboard to browse, install, and manage [Claude Code](https://claude.ai/code) plugins.
|
|
@@ -12,12 +14,14 @@ Built with [Ink](https://github.com/vadimdemedes/ink) (React for CLI).
|
|
|
12
14
|
|
|
13
15
|
## Features
|
|
14
16
|
|
|
15
|
-
-
|
|
17
|
+
- ✅ **Enabled** - Default view showing active plugins (installed AND enabled)
|
|
16
18
|
- 📦 **Install/Uninstall** - Install and uninstall plugins directly from the dashboard
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
+
- 🔄 **Enable/Disable** - Toggle plugins on/off with a single key
|
|
20
|
+
- 🔍 **Discover** - Browse 100+ plugins from multiple marketplaces
|
|
21
|
+
- 🏪 **Marketplaces** - Add, remove, and update plugin sources
|
|
19
22
|
- ⚠️ **Errors** - Debug plugin issues
|
|
20
|
-
- ⌨️ **
|
|
23
|
+
- ⌨️ **Focus Zone Navigation** - 3-zone keyboard model (TabBar → Search → List)
|
|
24
|
+
- 🆘 **Help Overlay** - Press `h` for full keyboard shortcuts
|
|
21
25
|
- 🎨 **Beautiful TUI** - Terminal UI that matches Claude Code's design
|
|
22
26
|
|
|
23
27
|
## Prerequisites
|
|
@@ -43,21 +47,26 @@ This opens the interactive TUI dashboard.
|
|
|
43
47
|
|
|
44
48
|
**Keyboard Shortcuts:**
|
|
45
49
|
|
|
46
|
-
| Key | Action
|
|
47
|
-
| --------- |
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
| `^P` `^N` | Navigate list (Emacs-style)
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
|
|
|
56
|
-
| `
|
|
57
|
-
|
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
50
|
+
| Key | Action |
|
|
51
|
+
| --------- | --------------------------------------------- |
|
|
52
|
+
| `↑` `↓` | Navigate within zone / Move between zones |
|
|
53
|
+
| `←` `→` | Switch tabs (when TabBar focused) |
|
|
54
|
+
| `^P` `^N` | Navigate list (Emacs-style vertical) |
|
|
55
|
+
| `^B` `^F` | Switch tabs (Emacs-style horizontal) |
|
|
56
|
+
| `Tab` | Next tab |
|
|
57
|
+
| `i` | Install selected plugin |
|
|
58
|
+
| `u` | Uninstall selected plugin (with confirm) |
|
|
59
|
+
| `Space` | Toggle plugin enable/disable |
|
|
60
|
+
| `Enter` | Install (Discover) / Toggle (Installed) |
|
|
61
|
+
| `/` | Search (works on all plugin/marketplace tabs) |
|
|
62
|
+
| `s` | Cycle sort options (Installs → Name → Date) |
|
|
63
|
+
| `S` | Toggle sort order (Asc/Desc) |
|
|
64
|
+
| `a` | Add marketplace (Marketplaces tab) |
|
|
65
|
+
| `d` | Remove marketplace (Marketplaces tab) |
|
|
66
|
+
| `r` | Update marketplace (Marketplaces tab) |
|
|
67
|
+
| `h` | Show help overlay |
|
|
68
|
+
| `Esc` | Clear search / Close dialog / Cancel |
|
|
69
|
+
| `q`/`^C` | Quit |
|
|
61
70
|
|
|
62
71
|
### Command Line Mode
|
|
63
72
|
|
|
@@ -92,14 +101,14 @@ claude-plugin-dashboard help
|
|
|
92
101
|
|
|
93
102
|
## Dashboard Tabs
|
|
94
103
|
|
|
95
|
-
###
|
|
104
|
+
### Enabled (Default)
|
|
96
105
|
|
|
97
|
-
|
|
106
|
+
View your currently active plugins (installed AND enabled):
|
|
98
107
|
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
108
|
+
- Shows only plugins that are ready to use
|
|
109
|
+
- Quick status overview of your Claude Code setup
|
|
110
|
+
- Toggle plugins on/off with `Space`
|
|
111
|
+
- Search with `/` to filter enabled plugins
|
|
103
112
|
|
|
104
113
|
### Installed
|
|
105
114
|
|
|
@@ -108,15 +117,26 @@ Manage your installed plugins:
|
|
|
108
117
|
- See enabled (●) and disabled (◐) status at a glance
|
|
109
118
|
- Toggle plugins on/off with `Space`
|
|
110
119
|
- Uninstall plugins with `u` (with Y/N confirmation)
|
|
111
|
-
-
|
|
120
|
+
- Search installed plugins with `/`
|
|
121
|
+
|
|
122
|
+
### Discover
|
|
123
|
+
|
|
124
|
+
Browse all available plugins from all configured marketplaces:
|
|
125
|
+
|
|
126
|
+
- Search by name, description, or tags
|
|
127
|
+
- Sort by install count, name, or date
|
|
128
|
+
- Install plugins with `i` key or `Enter`
|
|
129
|
+
- View plugin details including install count and description
|
|
112
130
|
|
|
113
131
|
### Marketplaces
|
|
114
132
|
|
|
115
|
-
|
|
133
|
+
Manage plugin sources:
|
|
116
134
|
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
135
|
+
- **Add** marketplace with `a` key (supports GitHub shorthand, Git URLs, local paths)
|
|
136
|
+
- **Remove** marketplace with `d` key (with confirmation)
|
|
137
|
+
- **Update** marketplace catalog with `r` key
|
|
138
|
+
- Search marketplaces with `/`
|
|
139
|
+
- View plugin counts and last update times
|
|
120
140
|
|
|
121
141
|
### Errors
|
|
122
142
|
|
|
@@ -198,8 +218,8 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
198
218
|
## Local Development
|
|
199
219
|
|
|
200
220
|
```bash
|
|
201
|
-
git clone https://github.com/laststance/claude-
|
|
202
|
-
cd claude-
|
|
221
|
+
git clone https://github.com/laststance/claude-plugin-dashboard.git
|
|
222
|
+
cd claude-plugin-dashboard
|
|
203
223
|
pnpm install
|
|
204
224
|
pnpm build
|
|
205
225
|
pnpm start
|
|
@@ -224,6 +244,24 @@ MIT © [Laststance.io](https://github.com/laststance)
|
|
|
224
244
|
|
|
225
245
|
## Changelog
|
|
226
246
|
|
|
247
|
+
### v0.2.0
|
|
248
|
+
|
|
249
|
+
- **Enabled tab**: New default view showing active plugins (installed AND enabled)
|
|
250
|
+
- **Focus zone navigation**: 3-zone keyboard model (TabBar → Search → List)
|
|
251
|
+
- **Emacs keybindings**: `Ctrl+B`/`Ctrl+F` for horizontal tab switching
|
|
252
|
+
- **Marketplace management**: Add (`a`), remove (`d`), update (`r`) plugin sources
|
|
253
|
+
- **Search on all tabs**: `/` now works on Enabled, Installed, and Marketplaces tabs
|
|
254
|
+
- **Help overlay**: Press `h` to show all keyboard shortcuts
|
|
255
|
+
- **Context-aware Enter**: Installs on Discover, toggles on Installed/Enabled
|
|
256
|
+
- **Plugin component types**: Show component types (skill, hook, agent) in detail view
|
|
257
|
+
- **CI/CD**: GitHub Actions workflow for lint, test, and build
|
|
258
|
+
- **96.67% test coverage**: Comprehensive Vitest and E2E test suites
|
|
259
|
+
|
|
260
|
+
### v0.1.1
|
|
261
|
+
|
|
262
|
+
- Fix: Clear terminal screen when exiting dashboard
|
|
263
|
+
- Fix: Repository URL in package.json
|
|
264
|
+
|
|
227
265
|
### v0.1.0
|
|
228
266
|
|
|
229
267
|
- Initial release
|
package/dist/app.d.ts
CHANGED
|
@@ -19,7 +19,12 @@ export declare function getAvailableZones(activeTab: AppState['activeTab']): Foc
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function appReducer(state: AppState, action: Action): AppState;
|
|
21
21
|
/**
|
|
22
|
-
* Get items array for current tab
|
|
22
|
+
* Get items array for current tab with search filter applied
|
|
23
|
+
* @param state - Current app state
|
|
24
|
+
* @returns Filtered array of items for the active tab
|
|
25
|
+
* @example
|
|
26
|
+
* getItemsForTab({ activeTab: 'installed', searchQuery: 'su', plugins: [...] })
|
|
27
|
+
* // => Only installed plugins matching 'su'
|
|
23
28
|
*/
|
|
24
29
|
export declare function getItemsForTab(state: AppState): unknown[];
|
|
25
30
|
/**
|
package/dist/app.js
CHANGED
|
@@ -15,6 +15,8 @@ import ErrorsTab from './tabs/ErrorsTab.js';
|
|
|
15
15
|
import { loadAllPlugins, loadMarketplaces, searchPlugins, searchMarketplaces, sortPlugins, } from './services/pluginService.js';
|
|
16
16
|
import { togglePlugin } from './services/settingsService.js';
|
|
17
17
|
import { installPlugin, uninstallPlugin, } from './services/pluginActionsService.js';
|
|
18
|
+
import { addMarketplace, removeMarketplace, updateMarketplace, } from './services/marketplaceActionsService.js';
|
|
19
|
+
import AddMarketplaceDialog from './components/AddMarketplaceDialog.js';
|
|
18
20
|
import ConfirmDialog from './components/ConfirmDialog.js';
|
|
19
21
|
import HelpOverlay from './components/HelpOverlay.js';
|
|
20
22
|
import packageJson from '../package.json' with { type: 'json' };
|
|
@@ -38,6 +40,11 @@ export const initialState = {
|
|
|
38
40
|
operationPluginId: null,
|
|
39
41
|
confirmUninstall: false,
|
|
40
42
|
showHelp: false,
|
|
43
|
+
marketplaceOperation: 'idle',
|
|
44
|
+
operationMarketplaceId: null,
|
|
45
|
+
confirmRemoveMarketplace: false,
|
|
46
|
+
showAddMarketplaceDialog: false,
|
|
47
|
+
addMarketplaceError: null,
|
|
41
48
|
};
|
|
42
49
|
/**
|
|
43
50
|
* Get available focus zones for the current tab
|
|
@@ -51,6 +58,24 @@ export function getAvailableZones(activeTab) {
|
|
|
51
58
|
}
|
|
52
59
|
return ['tabbar', 'search', 'list'];
|
|
53
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Get display message for marketplace operation status
|
|
63
|
+
* @param operation - The marketplace operation type
|
|
64
|
+
* @param marketplaceId - Optional marketplace identifier
|
|
65
|
+
* @returns Display message for the operation
|
|
66
|
+
*/
|
|
67
|
+
function getMarketplaceOperationMessage(operation, marketplaceId) {
|
|
68
|
+
switch (operation) {
|
|
69
|
+
case 'adding':
|
|
70
|
+
return 'Adding marketplace...';
|
|
71
|
+
case 'removing':
|
|
72
|
+
return `Removing ${marketplaceId}...`;
|
|
73
|
+
case 'updating':
|
|
74
|
+
return `Updating ${marketplaceId || 'marketplaces'}...`;
|
|
75
|
+
default:
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
54
79
|
/**
|
|
55
80
|
* State reducer for application state management
|
|
56
81
|
*/
|
|
@@ -210,19 +235,76 @@ export function appReducer(state, action) {
|
|
|
210
235
|
...state,
|
|
211
236
|
showHelp: !state.showHelp,
|
|
212
237
|
};
|
|
238
|
+
case 'SHOW_CONFIRM_REMOVE_MARKETPLACE':
|
|
239
|
+
return {
|
|
240
|
+
...state,
|
|
241
|
+
confirmRemoveMarketplace: true,
|
|
242
|
+
operationMarketplaceId: action.payload,
|
|
243
|
+
};
|
|
244
|
+
case 'HIDE_CONFIRM_REMOVE_MARKETPLACE':
|
|
245
|
+
return {
|
|
246
|
+
...state,
|
|
247
|
+
confirmRemoveMarketplace: false,
|
|
248
|
+
operationMarketplaceId: null,
|
|
249
|
+
};
|
|
250
|
+
case 'SHOW_ADD_MARKETPLACE_DIALOG':
|
|
251
|
+
return {
|
|
252
|
+
...state,
|
|
253
|
+
showAddMarketplaceDialog: true,
|
|
254
|
+
searchQuery: '', // Reuse searchQuery for dialog input
|
|
255
|
+
addMarketplaceError: null, // Clear previous error
|
|
256
|
+
};
|
|
257
|
+
case 'HIDE_ADD_MARKETPLACE_DIALOG':
|
|
258
|
+
return {
|
|
259
|
+
...state,
|
|
260
|
+
showAddMarketplaceDialog: false,
|
|
261
|
+
searchQuery: '',
|
|
262
|
+
addMarketplaceError: null,
|
|
263
|
+
};
|
|
264
|
+
case 'SET_ADD_MARKETPLACE_ERROR':
|
|
265
|
+
return {
|
|
266
|
+
...state,
|
|
267
|
+
addMarketplaceError: action.payload,
|
|
268
|
+
};
|
|
269
|
+
case 'START_MARKETPLACE_OPERATION':
|
|
270
|
+
return {
|
|
271
|
+
...state,
|
|
272
|
+
marketplaceOperation: action.payload.operation,
|
|
273
|
+
operationMarketplaceId: action.payload.marketplaceId ?? null,
|
|
274
|
+
message: getMarketplaceOperationMessage(action.payload.operation, action.payload.marketplaceId),
|
|
275
|
+
};
|
|
276
|
+
case 'END_MARKETPLACE_OPERATION':
|
|
277
|
+
return {
|
|
278
|
+
...state,
|
|
279
|
+
marketplaceOperation: 'idle',
|
|
280
|
+
operationMarketplaceId: null,
|
|
281
|
+
};
|
|
213
282
|
default:
|
|
214
283
|
return state;
|
|
215
284
|
}
|
|
216
285
|
}
|
|
217
286
|
/**
|
|
218
|
-
* Get items array for current tab
|
|
287
|
+
* Get items array for current tab with search filter applied
|
|
288
|
+
* @param state - Current app state
|
|
289
|
+
* @returns Filtered array of items for the active tab
|
|
290
|
+
* @example
|
|
291
|
+
* getItemsForTab({ activeTab: 'installed', searchQuery: 'su', plugins: [...] })
|
|
292
|
+
* // => Only installed plugins matching 'su'
|
|
219
293
|
*/
|
|
220
294
|
export function getItemsForTab(state) {
|
|
221
295
|
switch (state.activeTab) {
|
|
222
|
-
case 'enabled':
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
296
|
+
case 'enabled': {
|
|
297
|
+
const enabledPlugins = state.plugins.filter((p) => p.isInstalled && p.isEnabled);
|
|
298
|
+
return state.searchQuery
|
|
299
|
+
? searchPlugins(state.searchQuery, enabledPlugins)
|
|
300
|
+
: enabledPlugins;
|
|
301
|
+
}
|
|
302
|
+
case 'installed': {
|
|
303
|
+
const installedPlugins = state.plugins.filter((p) => p.isInstalled);
|
|
304
|
+
return state.searchQuery
|
|
305
|
+
? searchPlugins(state.searchQuery, installedPlugins)
|
|
306
|
+
: installedPlugins;
|
|
307
|
+
}
|
|
226
308
|
case 'discover':
|
|
227
309
|
return getFilteredPlugins(state);
|
|
228
310
|
case 'marketplaces':
|
|
@@ -313,10 +395,103 @@ export default function App() {
|
|
|
313
395
|
});
|
|
314
396
|
}
|
|
315
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Handle adding a new marketplace
|
|
400
|
+
* @param source - Marketplace source (e.g., "owner/repo", URL, or local path)
|
|
401
|
+
*/
|
|
402
|
+
async function handleAddMarketplace(source) {
|
|
403
|
+
dispatch({
|
|
404
|
+
type: 'START_MARKETPLACE_OPERATION',
|
|
405
|
+
payload: { operation: 'adding' },
|
|
406
|
+
});
|
|
407
|
+
const result = await addMarketplace(source);
|
|
408
|
+
dispatch({ type: 'END_MARKETPLACE_OPERATION' });
|
|
409
|
+
if (result.success) {
|
|
410
|
+
dispatch({ type: 'HIDE_ADD_MARKETPLACE_DIALOG' });
|
|
411
|
+
// Reload marketplaces to get fresh state
|
|
412
|
+
const marketplaces = loadMarketplaces();
|
|
413
|
+
dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
|
|
414
|
+
// Also reload plugins as new marketplace may have plugins
|
|
415
|
+
const plugins = loadAllPlugins();
|
|
416
|
+
dispatch({ type: 'SET_PLUGINS', payload: plugins });
|
|
417
|
+
// Reset selection to avoid pointing to a different marketplace after re-sort
|
|
418
|
+
dispatch({ type: 'SET_SELECTED_INDEX', payload: 0 });
|
|
419
|
+
dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
// Keep dialog open and show error inline
|
|
423
|
+
dispatch({
|
|
424
|
+
type: 'SET_ADD_MARKETPLACE_ERROR',
|
|
425
|
+
payload: result.error || result.message,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Handle removing a marketplace
|
|
431
|
+
* @param marketplaceId - Marketplace identifier to remove
|
|
432
|
+
*/
|
|
433
|
+
async function handleRemoveMarketplace(marketplaceId) {
|
|
434
|
+
dispatch({
|
|
435
|
+
type: 'START_MARKETPLACE_OPERATION',
|
|
436
|
+
payload: { operation: 'removing', marketplaceId },
|
|
437
|
+
});
|
|
438
|
+
const result = await removeMarketplace(marketplaceId);
|
|
439
|
+
dispatch({ type: 'END_MARKETPLACE_OPERATION' });
|
|
440
|
+
if (result.success) {
|
|
441
|
+
// Reload marketplaces to get fresh state
|
|
442
|
+
const marketplaces = loadMarketplaces();
|
|
443
|
+
dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
|
|
444
|
+
// Also reload plugins as removed marketplace's plugins should be gone
|
|
445
|
+
const plugins = loadAllPlugins();
|
|
446
|
+
dispatch({ type: 'SET_PLUGINS', payload: plugins });
|
|
447
|
+
dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
|
|
448
|
+
// Reset selection if needed
|
|
449
|
+
if (state.selectedIndex >= marketplaces.length) {
|
|
450
|
+
dispatch({
|
|
451
|
+
type: 'SET_SELECTED_INDEX',
|
|
452
|
+
payload: Math.max(0, marketplaces.length - 1),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
dispatch({
|
|
458
|
+
type: 'SET_MESSAGE',
|
|
459
|
+
payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Handle updating a marketplace (or all marketplaces)
|
|
465
|
+
* @param marketplaceId - Optional marketplace identifier. If omitted, updates all.
|
|
466
|
+
*/
|
|
467
|
+
async function handleUpdateMarketplace(marketplaceId) {
|
|
468
|
+
dispatch({
|
|
469
|
+
type: 'START_MARKETPLACE_OPERATION',
|
|
470
|
+
payload: { operation: 'updating', marketplaceId },
|
|
471
|
+
});
|
|
472
|
+
const result = await updateMarketplace(marketplaceId);
|
|
473
|
+
dispatch({ type: 'END_MARKETPLACE_OPERATION' });
|
|
474
|
+
if (result.success) {
|
|
475
|
+
// Reload marketplaces and plugins to get fresh state
|
|
476
|
+
const marketplaces = loadMarketplaces();
|
|
477
|
+
dispatch({ type: 'SET_MARKETPLACES', payload: marketplaces });
|
|
478
|
+
const plugins = loadAllPlugins();
|
|
479
|
+
dispatch({ type: 'SET_PLUGINS', payload: plugins });
|
|
480
|
+
// Reset selection to avoid pointing to a different marketplace after re-sort
|
|
481
|
+
dispatch({ type: 'SET_SELECTED_INDEX', payload: 0 });
|
|
482
|
+
dispatch({ type: 'SET_MESSAGE', payload: `✅ ${result.message}` });
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
dispatch({
|
|
486
|
+
type: 'SET_MESSAGE',
|
|
487
|
+
payload: `❌ ${result.message}${result.error ? `: ${result.error}` : ''}`,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
316
491
|
// Keyboard input handling
|
|
317
492
|
useInput((input, key) => {
|
|
318
|
-
// Block all input during operations
|
|
319
|
-
if (state.operation !== 'idle') {
|
|
493
|
+
// Block all input during operations (plugin or marketplace)
|
|
494
|
+
if (state.operation !== 'idle' || state.marketplaceOperation !== 'idle') {
|
|
320
495
|
return;
|
|
321
496
|
}
|
|
322
497
|
// Handle help overlay
|
|
@@ -336,7 +511,7 @@ export default function App() {
|
|
|
336
511
|
dispatch({ type: 'TOGGLE_HELP' });
|
|
337
512
|
return;
|
|
338
513
|
}
|
|
339
|
-
// Handle confirmation dialog
|
|
514
|
+
// Handle plugin uninstall confirmation dialog
|
|
340
515
|
if (state.confirmUninstall && state.operationPluginId) {
|
|
341
516
|
if (input === 'y' || input === 'Y') {
|
|
342
517
|
dispatch({ type: 'HIDE_CONFIRM_UNINSTALL' });
|
|
@@ -350,6 +525,51 @@ export default function App() {
|
|
|
350
525
|
}
|
|
351
526
|
return;
|
|
352
527
|
}
|
|
528
|
+
// Handle marketplace remove confirmation dialog
|
|
529
|
+
if (state.confirmRemoveMarketplace && state.operationMarketplaceId) {
|
|
530
|
+
if (input === 'y' || input === 'Y') {
|
|
531
|
+
dispatch({ type: 'HIDE_CONFIRM_REMOVE_MARKETPLACE' });
|
|
532
|
+
handleRemoveMarketplace(state.operationMarketplaceId);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
536
|
+
dispatch({ type: 'HIDE_CONFIRM_REMOVE_MARKETPLACE' });
|
|
537
|
+
dispatch({ type: 'SET_MESSAGE', payload: 'Remove cancelled' });
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
// Handle add marketplace dialog input
|
|
543
|
+
if (state.showAddMarketplaceDialog) {
|
|
544
|
+
// Submit on Enter
|
|
545
|
+
if (key.return && state.searchQuery.trim()) {
|
|
546
|
+
handleAddMarketplace(state.searchQuery.trim());
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Cancel on Escape
|
|
550
|
+
if (key.escape) {
|
|
551
|
+
dispatch({ type: 'HIDE_ADD_MARKETPLACE_DIALOG' });
|
|
552
|
+
dispatch({ type: 'SET_MESSAGE', payload: 'Add marketplace cancelled' });
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// Backspace
|
|
556
|
+
if (key.backspace || key.delete) {
|
|
557
|
+
dispatch({
|
|
558
|
+
type: 'SET_SEARCH_QUERY',
|
|
559
|
+
payload: state.searchQuery.slice(0, -1),
|
|
560
|
+
});
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
// Character input
|
|
564
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
565
|
+
dispatch({
|
|
566
|
+
type: 'SET_SEARCH_QUERY',
|
|
567
|
+
payload: state.searchQuery + input,
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
353
573
|
// Search mode input (when focusZone is 'search')
|
|
354
574
|
if (state.focusZone === 'search') {
|
|
355
575
|
// Up arrow: move focus to tabbar
|
|
@@ -453,11 +673,7 @@ export default function App() {
|
|
|
453
673
|
(state.activeTab === 'enabled' ||
|
|
454
674
|
state.activeTab === 'installed' ||
|
|
455
675
|
state.activeTab === 'discover')) {
|
|
456
|
-
const items = state
|
|
457
|
-
? state.plugins.filter((p) => p.isInstalled && p.isEnabled)
|
|
458
|
-
: state.activeTab === 'installed'
|
|
459
|
-
? state.plugins.filter((p) => p.isInstalled)
|
|
460
|
-
: getFilteredPlugins(state);
|
|
676
|
+
const items = getItemsForTab(state);
|
|
461
677
|
const selectedPlugin = items[state.selectedIndex];
|
|
462
678
|
if (selectedPlugin) {
|
|
463
679
|
if (!selectedPlugin.isInstalled) {
|
|
@@ -494,11 +710,7 @@ export default function App() {
|
|
|
494
710
|
(state.activeTab === 'enabled' ||
|
|
495
711
|
state.activeTab === 'installed' ||
|
|
496
712
|
state.activeTab === 'discover')) {
|
|
497
|
-
const items = state
|
|
498
|
-
? state.plugins.filter((p) => p.isInstalled && p.isEnabled)
|
|
499
|
-
: state.activeTab === 'installed'
|
|
500
|
-
? state.plugins.filter((p) => p.isInstalled)
|
|
501
|
-
: getFilteredPlugins(state);
|
|
713
|
+
const items = getItemsForTab(state);
|
|
502
714
|
const selectedPlugin = items[state.selectedIndex];
|
|
503
715
|
if (selectedPlugin && selectedPlugin.isInstalled) {
|
|
504
716
|
try {
|
|
@@ -552,16 +764,43 @@ export default function App() {
|
|
|
552
764
|
dispatch({ type: 'SET_SEARCH_QUERY', payload: '' });
|
|
553
765
|
return;
|
|
554
766
|
}
|
|
767
|
+
// Marketplace-specific key bindings
|
|
768
|
+
if (state.activeTab === 'marketplaces' && state.focusZone === 'list') {
|
|
769
|
+
// Add marketplace (a key)
|
|
770
|
+
if (input === 'a') {
|
|
771
|
+
dispatch({ type: 'SHOW_ADD_MARKETPLACE_DIALOG' });
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
// Remove marketplace (d key or Backspace)
|
|
775
|
+
if (input === 'd' || key.backspace || key.delete) {
|
|
776
|
+
const selectedMarketplace = filteredMarketplaces[state.selectedIndex];
|
|
777
|
+
if (selectedMarketplace) {
|
|
778
|
+
dispatch({
|
|
779
|
+
type: 'SHOW_CONFIRM_REMOVE_MARKETPLACE',
|
|
780
|
+
payload: selectedMarketplace.id,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
// Update marketplace (u key)
|
|
786
|
+
if (input === 'u') {
|
|
787
|
+
const selectedMarketplace = filteredMarketplaces[state.selectedIndex];
|
|
788
|
+
if (selectedMarketplace) {
|
|
789
|
+
handleUpdateMarketplace(selectedMarketplace.id);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
// No marketplace selected, update all
|
|
793
|
+
handleUpdateMarketplace();
|
|
794
|
+
}
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
555
798
|
// Install (i key) - only on enabled/installed/discover tabs
|
|
556
799
|
if (input === 'i' &&
|
|
557
800
|
(state.activeTab === 'enabled' ||
|
|
558
801
|
state.activeTab === 'installed' ||
|
|
559
802
|
state.activeTab === 'discover')) {
|
|
560
|
-
const items = state
|
|
561
|
-
? state.plugins.filter((p) => p.isInstalled && p.isEnabled)
|
|
562
|
-
: state.activeTab === 'installed'
|
|
563
|
-
? state.plugins.filter((p) => p.isInstalled)
|
|
564
|
-
: getFilteredPlugins(state);
|
|
803
|
+
const items = getItemsForTab(state);
|
|
565
804
|
const selectedPlugin = items[state.selectedIndex];
|
|
566
805
|
if (selectedPlugin && !selectedPlugin.isInstalled) {
|
|
567
806
|
handleInstall(selectedPlugin.id);
|
|
@@ -579,11 +818,7 @@ export default function App() {
|
|
|
579
818
|
(state.activeTab === 'enabled' ||
|
|
580
819
|
state.activeTab === 'installed' ||
|
|
581
820
|
state.activeTab === 'discover')) {
|
|
582
|
-
const items = state
|
|
583
|
-
? state.plugins.filter((p) => p.isInstalled && p.isEnabled)
|
|
584
|
-
: state.activeTab === 'installed'
|
|
585
|
-
? state.plugins.filter((p) => p.isInstalled)
|
|
586
|
-
: getFilteredPlugins(state);
|
|
821
|
+
const items = getItemsForTab(state);
|
|
587
822
|
const selectedPlugin = items[state.selectedIndex];
|
|
588
823
|
if (selectedPlugin && selectedPlugin.isInstalled) {
|
|
589
824
|
dispatch({ type: 'SHOW_CONFIRM_UNINSTALL', payload: selectedPlugin.id });
|
|
@@ -618,7 +853,7 @@ export default function App() {
|
|
|
618
853
|
const filteredMarketplaces = state.searchQuery
|
|
619
854
|
? searchMarketplaces(state.searchQuery, state.marketplaces)
|
|
620
855
|
: state.marketplaces;
|
|
621
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, color: "magenta", children: "\u26A1 Claude Code Plugin Dashboard" }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: ["v", packageJson.version] })] }), _jsx(TabBar, { activeTab: state.activeTab, isFocused: state.focusZone === 'tabbar' }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [state.activeTab === 'enabled' && (_jsx(EnabledTab, { plugins: enabledPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'installed' && (_jsx(InstalledTab, { plugins: installedPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'discover' && (_jsx(DiscoverTab, { plugins: filteredPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, sortBy: state.sortBy, sortOrder: state.sortOrder, focusZone: state.focusZone })), state.activeTab === 'marketplaces' && (_jsx(MarketplacesTab, { marketplaces: filteredMarketplaces, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'errors' && (_jsx(ErrorsTab, { errors: state.errors, selectedIndex: state.selectedIndex }))] }), state.confirmUninstall && state.operationPluginId && (_jsx(ConfirmDialog, { message: `Uninstall ${state.operationPluginId}?` })), _jsx(HelpOverlay, { isVisible: state.showHelp }), state.message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: state.message }) })), _jsx(KeyHints, { focusZone: state.focusZone, extraHints: (() => {
|
|
856
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, color: "magenta", children: "\u26A1 Claude Code Plugin Dashboard" }), _jsx(Box, { flexGrow: 1 }), _jsxs(Text, { dimColor: true, children: ["v", packageJson.version] })] }), _jsx(TabBar, { activeTab: state.activeTab, isFocused: state.focusZone === 'tabbar' }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [state.activeTab === 'enabled' && (_jsx(EnabledTab, { plugins: enabledPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'installed' && (_jsx(InstalledTab, { plugins: installedPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'discover' && (_jsx(DiscoverTab, { plugins: filteredPlugins, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, sortBy: state.sortBy, sortOrder: state.sortOrder, focusZone: state.focusZone })), state.activeTab === 'marketplaces' && (_jsx(MarketplacesTab, { marketplaces: filteredMarketplaces, selectedIndex: state.selectedIndex, searchQuery: state.searchQuery, focusZone: state.focusZone })), state.activeTab === 'errors' && (_jsx(ErrorsTab, { errors: state.errors, selectedIndex: state.selectedIndex }))] }), state.confirmUninstall && state.operationPluginId && (_jsx(ConfirmDialog, { message: `Uninstall ${state.operationPluginId}?` })), state.confirmRemoveMarketplace && state.operationMarketplaceId && (_jsx(ConfirmDialog, { message: `Remove marketplace ${state.operationMarketplaceId}?` })), state.showAddMarketplaceDialog && (_jsx(AddMarketplaceDialog, { value: state.searchQuery, error: state.addMarketplaceError ?? undefined })), _jsx(HelpOverlay, { isVisible: state.showHelp }), state.message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: state.message }) })), _jsx(KeyHints, { focusZone: state.focusZone, extraHints: (() => {
|
|
622
857
|
// Search mode - no extra hints (base hints cover it)
|
|
623
858
|
if (state.focusZone === 'search') {
|
|
624
859
|
return undefined;
|
|
@@ -661,7 +896,12 @@ export default function App() {
|
|
|
661
896
|
}
|
|
662
897
|
// Marketplaces tab hints
|
|
663
898
|
if (state.activeTab === 'marketplaces') {
|
|
664
|
-
return [
|
|
899
|
+
return [
|
|
900
|
+
{ key: '/', action: 'search' },
|
|
901
|
+
{ key: 'a', action: 'add' },
|
|
902
|
+
{ key: 'd', action: 'remove' },
|
|
903
|
+
{ key: 'u', action: 'update' },
|
|
904
|
+
];
|
|
665
905
|
}
|
|
666
906
|
return undefined;
|
|
667
907
|
})() })] }));
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -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;
|
|
@@ -0,0 +1,178 @@
|
|
|
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 * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { readJsonFile, directoryExists, fileExists } from './fileService.js';
|
|
9
|
+
/**
|
|
10
|
+
* Detect all component types for a plugin at the given install path
|
|
11
|
+
* @param installPath - Absolute path to the installed plugin directory
|
|
12
|
+
* @returns PluginComponents object with detected component counts
|
|
13
|
+
* - Returns undefined values for components that are not present
|
|
14
|
+
* - Returns counts > 0 for components that exist
|
|
15
|
+
* @example
|
|
16
|
+
* const components = detectPluginComponents('/path/to/plugin')
|
|
17
|
+
* // => { skills: 5, commands: 2, mcpServers: 1 }
|
|
18
|
+
*/
|
|
19
|
+
export function detectPluginComponents(installPath) {
|
|
20
|
+
if (!directoryExists(installPath)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const components = {};
|
|
24
|
+
// Detect skills (count directories in skills/ folder)
|
|
25
|
+
const skillsCount = countSkills(installPath);
|
|
26
|
+
if (skillsCount > 0) {
|
|
27
|
+
components.skills = skillsCount;
|
|
28
|
+
}
|
|
29
|
+
// Detect commands (count .md files in commands/ folder)
|
|
30
|
+
const commandsCount = countMarkdownFiles(installPath, 'commands');
|
|
31
|
+
if (commandsCount > 0) {
|
|
32
|
+
components.commands = commandsCount;
|
|
33
|
+
}
|
|
34
|
+
// Detect agents (count .md files in agents/ folder)
|
|
35
|
+
const agentsCount = countMarkdownFiles(installPath, 'agents');
|
|
36
|
+
if (agentsCount > 0) {
|
|
37
|
+
components.agents = agentsCount;
|
|
38
|
+
}
|
|
39
|
+
// Detect hooks
|
|
40
|
+
const hasHooks = detectHooks(installPath);
|
|
41
|
+
if (hasHooks) {
|
|
42
|
+
components.hooks = true;
|
|
43
|
+
}
|
|
44
|
+
// Detect MCP servers from plugin.json
|
|
45
|
+
const mcpCount = countMcpServers(installPath);
|
|
46
|
+
if (mcpCount > 0) {
|
|
47
|
+
components.mcpServers = mcpCount;
|
|
48
|
+
}
|
|
49
|
+
// Detect LSP servers from .lsp.json
|
|
50
|
+
const lspCount = countLspServers(installPath);
|
|
51
|
+
if (lspCount > 0) {
|
|
52
|
+
components.lspServers = lspCount;
|
|
53
|
+
}
|
|
54
|
+
// Return undefined if no components detected
|
|
55
|
+
if (Object.keys(components).length === 0) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return components;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Count skill directories in the skills/ folder
|
|
62
|
+
* Skills are stored as subdirectories with SKILL.md files
|
|
63
|
+
* @param installPath - Plugin install path
|
|
64
|
+
* @returns Number of skill directories
|
|
65
|
+
*/
|
|
66
|
+
function countSkills(installPath) {
|
|
67
|
+
const skillsPath = path.join(installPath, 'skills');
|
|
68
|
+
if (!directoryExists(skillsPath)) {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const entries = fs.readdirSync(skillsPath, { withFileTypes: true });
|
|
73
|
+
return entries.filter((entry) => entry.isDirectory()).length;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Count .md files in a specific directory
|
|
81
|
+
* @param installPath - Plugin install path
|
|
82
|
+
* @param subdir - Subdirectory name ('commands' or 'agents')
|
|
83
|
+
* @returns Number of .md files
|
|
84
|
+
*/
|
|
85
|
+
function countMarkdownFiles(installPath, subdir) {
|
|
86
|
+
const dirPath = path.join(installPath, subdir);
|
|
87
|
+
if (!directoryExists(dirPath)) {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const files = fs.readdirSync(dirPath);
|
|
92
|
+
return files.filter((file) => file.endsWith('.md')).length;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Detect if plugin has hooks configured
|
|
100
|
+
* Checks for hooks/ directory or hooks.json file
|
|
101
|
+
* @param installPath - Plugin install path
|
|
102
|
+
* @returns true if hooks are configured
|
|
103
|
+
*/
|
|
104
|
+
function detectHooks(installPath) {
|
|
105
|
+
const hooksDir = path.join(installPath, 'hooks');
|
|
106
|
+
const hooksJson = path.join(installPath, 'hooks.json');
|
|
107
|
+
return directoryExists(hooksDir) || fileExists(hooksJson);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Count MCP servers defined in plugin.json
|
|
111
|
+
* @param installPath - Plugin install path
|
|
112
|
+
* @returns Number of MCP server configurations
|
|
113
|
+
*/
|
|
114
|
+
function countMcpServers(installPath) {
|
|
115
|
+
// Check both .claude-plugin/plugin.json and plugin.json at root
|
|
116
|
+
const pluginJsonPaths = [
|
|
117
|
+
path.join(installPath, '.claude-plugin', 'plugin.json'),
|
|
118
|
+
path.join(installPath, 'plugin.json'),
|
|
119
|
+
];
|
|
120
|
+
for (const pluginJsonPath of pluginJsonPaths) {
|
|
121
|
+
const pluginJson = readJsonFile(pluginJsonPath);
|
|
122
|
+
if (pluginJson?.mcpServers) {
|
|
123
|
+
return Object.keys(pluginJson.mcpServers).length;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Count LSP servers defined in .lsp.json
|
|
130
|
+
* @param installPath - Plugin install path
|
|
131
|
+
* @returns Number of LSP server configurations
|
|
132
|
+
*/
|
|
133
|
+
function countLspServers(installPath) {
|
|
134
|
+
const lspJsonPath = path.join(installPath, '.lsp.json');
|
|
135
|
+
const lspConfig = readJsonFile(lspJsonPath);
|
|
136
|
+
if (!lspConfig) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
return Object.keys(lspConfig).length;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Check if a plugin has any components
|
|
143
|
+
* @param components - PluginComponents object
|
|
144
|
+
* @returns true if at least one component type is present
|
|
145
|
+
* @example
|
|
146
|
+
* hasAnyComponents({ skills: 2 }) // => true
|
|
147
|
+
* hasAnyComponents({}) // => false
|
|
148
|
+
* hasAnyComponents(undefined) // => false
|
|
149
|
+
*/
|
|
150
|
+
export function hasAnyComponents(components) {
|
|
151
|
+
if (!components) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return ((components.skills ?? 0) > 0 ||
|
|
155
|
+
(components.commands ?? 0) > 0 ||
|
|
156
|
+
(components.agents ?? 0) > 0 ||
|
|
157
|
+
components.hooks === true ||
|
|
158
|
+
(components.mcpServers ?? 0) > 0 ||
|
|
159
|
+
(components.lspServers ?? 0) > 0);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get total component count for a plugin
|
|
163
|
+
* @param components - PluginComponents object
|
|
164
|
+
* @returns Total number of components (hooks count as 1)
|
|
165
|
+
* @example
|
|
166
|
+
* getTotalComponentCount({ skills: 3, commands: 2, hooks: true }) // => 6
|
|
167
|
+
*/
|
|
168
|
+
export function getTotalComponentCount(components) {
|
|
169
|
+
if (!components) {
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
return ((components.skills ?? 0) +
|
|
173
|
+
(components.commands ?? 0) +
|
|
174
|
+
(components.agents ?? 0) +
|
|
175
|
+
(components.hooks ? 1 : 0) +
|
|
176
|
+
(components.mcpServers ?? 0) +
|
|
177
|
+
(components.lspServers ?? 0));
|
|
178
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace actions service for add/remove/update operations
|
|
3
|
+
* Executes `claude plugin marketplace <action>` as subprocess
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Result of a marketplace CLI action
|
|
7
|
+
*/
|
|
8
|
+
export interface MarketplaceActionResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
message: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Add a new marketplace via Claude CLI
|
|
15
|
+
* @param source - Marketplace source (e.g., "owner/repo", "https://...", "./local-path")
|
|
16
|
+
* @returns Promise resolving to action result
|
|
17
|
+
* @example
|
|
18
|
+
* // GitHub shorthand
|
|
19
|
+
* addMarketplace('anthropics/claude-plugins')
|
|
20
|
+
* // Git URL
|
|
21
|
+
* addMarketplace('https://github.com/org/plugins.git')
|
|
22
|
+
* // Local path
|
|
23
|
+
* addMarketplace('./my-marketplace')
|
|
24
|
+
*/
|
|
25
|
+
export declare function addMarketplace(source: string): Promise<MarketplaceActionResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Remove an existing marketplace via Claude CLI
|
|
28
|
+
* @param name - Marketplace name/identifier to remove
|
|
29
|
+
* @returns Promise resolving to action result
|
|
30
|
+
* @example
|
|
31
|
+
* removeMarketplace('my-marketplace')
|
|
32
|
+
*/
|
|
33
|
+
export declare function removeMarketplace(name: string): Promise<MarketplaceActionResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Update marketplace catalog(s) via Claude CLI
|
|
36
|
+
* @param name - Optional marketplace name. If omitted, updates all marketplaces.
|
|
37
|
+
* @returns Promise resolving to action result
|
|
38
|
+
* @example
|
|
39
|
+
* // Update specific marketplace
|
|
40
|
+
* updateMarketplace('claude-plugins-official')
|
|
41
|
+
* // Update all marketplaces
|
|
42
|
+
* updateMarketplace()
|
|
43
|
+
*/
|
|
44
|
+
export declare function updateMarketplace(name?: string): Promise<MarketplaceActionResult>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace actions service for add/remove/update operations
|
|
3
|
+
* Executes `claude plugin marketplace <action>` as subprocess
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
/**
|
|
7
|
+
* Add a new marketplace via Claude CLI
|
|
8
|
+
* @param source - Marketplace source (e.g., "owner/repo", "https://...", "./local-path")
|
|
9
|
+
* @returns Promise resolving to action result
|
|
10
|
+
* @example
|
|
11
|
+
* // GitHub shorthand
|
|
12
|
+
* addMarketplace('anthropics/claude-plugins')
|
|
13
|
+
* // Git URL
|
|
14
|
+
* addMarketplace('https://github.com/org/plugins.git')
|
|
15
|
+
* // Local path
|
|
16
|
+
* addMarketplace('./my-marketplace')
|
|
17
|
+
*/
|
|
18
|
+
export function addMarketplace(source) {
|
|
19
|
+
return executeMarketplaceCommand(['plugin', 'marketplace', 'add', source], `Added marketplace: ${source}`, `Failed to add marketplace: ${source}`);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Remove an existing marketplace via Claude CLI
|
|
23
|
+
* @param name - Marketplace name/identifier to remove
|
|
24
|
+
* @returns Promise resolving to action result
|
|
25
|
+
* @example
|
|
26
|
+
* removeMarketplace('my-marketplace')
|
|
27
|
+
*/
|
|
28
|
+
export function removeMarketplace(name) {
|
|
29
|
+
return executeMarketplaceCommand(['plugin', 'marketplace', 'remove', name], `Removed marketplace: ${name}`, `Failed to remove marketplace: ${name}`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Update marketplace catalog(s) via Claude CLI
|
|
33
|
+
* @param name - Optional marketplace name. If omitted, updates all marketplaces.
|
|
34
|
+
* @returns Promise resolving to action result
|
|
35
|
+
* @example
|
|
36
|
+
* // Update specific marketplace
|
|
37
|
+
* updateMarketplace('claude-plugins-official')
|
|
38
|
+
* // Update all marketplaces
|
|
39
|
+
* updateMarketplace()
|
|
40
|
+
*/
|
|
41
|
+
export function updateMarketplace(name) {
|
|
42
|
+
const args = ['plugin', 'marketplace', 'update'];
|
|
43
|
+
if (name) {
|
|
44
|
+
args.push(name);
|
|
45
|
+
}
|
|
46
|
+
return executeMarketplaceCommand(args, name ? `Updated ${name}` : 'Updated all marketplaces', `Failed to update ${name || 'marketplaces'}`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Execute a marketplace CLI command with generic args and messages
|
|
50
|
+
* @param args - CLI arguments to pass to claude command
|
|
51
|
+
* @param successMessage - Message to return on success
|
|
52
|
+
* @param failureMessage - Message to return on failure
|
|
53
|
+
* @returns Promise resolving to action result
|
|
54
|
+
*/
|
|
55
|
+
function executeMarketplaceCommand(args, successMessage, failureMessage) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const child = spawn('claude', args, {
|
|
58
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
59
|
+
shell: false,
|
|
60
|
+
});
|
|
61
|
+
let stdout = '';
|
|
62
|
+
let stderr = '';
|
|
63
|
+
child.stdout?.on('data', (data) => {
|
|
64
|
+
stdout += data.toString();
|
|
65
|
+
});
|
|
66
|
+
child.stderr?.on('data', (data) => {
|
|
67
|
+
stderr += data.toString();
|
|
68
|
+
});
|
|
69
|
+
child.on('close', (code) => {
|
|
70
|
+
if (code === 0) {
|
|
71
|
+
resolve({
|
|
72
|
+
success: true,
|
|
73
|
+
message: successMessage,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
resolve({
|
|
78
|
+
success: false,
|
|
79
|
+
message: failureMessage,
|
|
80
|
+
error: stderr || stdout || `Exit code: ${code}`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
child.on('error', (err) => {
|
|
85
|
+
resolve({
|
|
86
|
+
success: false,
|
|
87
|
+
message: 'Failed to execute claude command',
|
|
88
|
+
error: err.message,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readJsonFile, directoryExists, listDirectories, } from './fileService.js';
|
|
6
6
|
import { getEnabledPlugins } from './settingsService.js';
|
|
7
|
+
import { detectPluginComponents } from './componentService.js';
|
|
7
8
|
import { PATHS, getMarketplaceJsonPath } from '../utils/paths.js';
|
|
8
9
|
/**
|
|
9
10
|
* Load all plugins from all marketplaces
|
|
@@ -44,6 +45,10 @@ export function loadAllPlugins() {
|
|
|
44
45
|
for (const plugin of manifest.plugins) {
|
|
45
46
|
const pluginId = `${plugin.name}@${marketplace}`;
|
|
46
47
|
const installedEntry = installedMap.get(pluginId);
|
|
48
|
+
// Detect components for installed plugins
|
|
49
|
+
const components = installedEntry
|
|
50
|
+
? detectPluginComponents(installedEntry.installPath)
|
|
51
|
+
: undefined;
|
|
47
52
|
plugins.push({
|
|
48
53
|
id: pluginId,
|
|
49
54
|
name: plugin.name,
|
|
@@ -61,6 +66,7 @@ export function loadAllPlugins() {
|
|
|
61
66
|
tags: plugin.tags || plugin.keywords,
|
|
62
67
|
isLocal: installedEntry?.isLocal,
|
|
63
68
|
gitCommitSha: installedEntry?.gitCommitSha,
|
|
69
|
+
components,
|
|
64
70
|
});
|
|
65
71
|
}
|
|
66
72
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
* TypeScript interfaces for Claude Code Plugin Dashboard
|
|
3
3
|
* These types aggregate data from multiple Claude Code configuration files
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Component types provided by a plugin
|
|
7
|
+
* Detected by scanning plugin directory structure and plugin.json
|
|
8
|
+
* @example
|
|
9
|
+
* { skills: 5, commands: 2, mcpServers: 1 } // Plugin with skills, commands, and MCP
|
|
10
|
+
*/
|
|
11
|
+
export interface PluginComponents {
|
|
12
|
+
/** Count of skill directories in skills/ */
|
|
13
|
+
skills?: number;
|
|
14
|
+
/** Count of slash command .md files in commands/ */
|
|
15
|
+
commands?: number;
|
|
16
|
+
/** Count of subagent .md files in agents/ */
|
|
17
|
+
agents?: number;
|
|
18
|
+
/** Whether hooks are configured (hooks/ dir or hooks.json exists) */
|
|
19
|
+
hooks?: boolean;
|
|
20
|
+
/** Count of MCP server configurations in plugin.json mcpServers field */
|
|
21
|
+
mcpServers?: number;
|
|
22
|
+
/** Count of LSP server configurations in .lsp.json */
|
|
23
|
+
lspServers?: number;
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* Aggregated plugin data from multiple sources
|
|
7
27
|
* Combines data from settings.json, installed_plugins.json, marketplace.json, etc.
|
|
@@ -42,6 +62,8 @@ export interface Plugin {
|
|
|
42
62
|
isLocal?: boolean;
|
|
43
63
|
/** Git commit SHA (if installed) */
|
|
44
64
|
gitCommitSha?: string;
|
|
65
|
+
/** Component types provided by this plugin (skills, commands, MCP, etc.) */
|
|
66
|
+
components?: PluginComponents;
|
|
45
67
|
}
|
|
46
68
|
/**
|
|
47
69
|
* Raw installed plugin data from installed_plugins.json
|
|
@@ -157,6 +179,10 @@ export interface Settings {
|
|
|
157
179
|
* Defines which UI area currently has keyboard focus
|
|
158
180
|
*/
|
|
159
181
|
export type FocusZone = 'tabbar' | 'search' | 'list';
|
|
182
|
+
/**
|
|
183
|
+
* Marketplace operation types
|
|
184
|
+
*/
|
|
185
|
+
export type MarketplaceOperation = 'idle' | 'adding' | 'removing' | 'updating';
|
|
160
186
|
/**
|
|
161
187
|
* Application state for useReducer
|
|
162
188
|
*/
|
|
@@ -193,6 +219,16 @@ export interface AppState {
|
|
|
193
219
|
confirmUninstall: boolean;
|
|
194
220
|
/** Whether help overlay is showing */
|
|
195
221
|
showHelp: boolean;
|
|
222
|
+
/** Current marketplace operation */
|
|
223
|
+
marketplaceOperation: MarketplaceOperation;
|
|
224
|
+
/** Marketplace ID being operated on */
|
|
225
|
+
operationMarketplaceId: string | null;
|
|
226
|
+
/** Whether remove marketplace confirmation dialog is showing */
|
|
227
|
+
confirmRemoveMarketplace: boolean;
|
|
228
|
+
/** Whether add marketplace dialog is showing */
|
|
229
|
+
showAddMarketplaceDialog: boolean;
|
|
230
|
+
/** Error message for add marketplace dialog */
|
|
231
|
+
addMarketplaceError: string | null;
|
|
196
232
|
}
|
|
197
233
|
/**
|
|
198
234
|
* Action types for useReducer
|
|
@@ -261,4 +297,24 @@ export type Action = {
|
|
|
261
297
|
} | {
|
|
262
298
|
type: 'SET_FOCUS_ZONE';
|
|
263
299
|
payload: FocusZone;
|
|
300
|
+
} | {
|
|
301
|
+
type: 'SHOW_CONFIRM_REMOVE_MARKETPLACE';
|
|
302
|
+
payload: string;
|
|
303
|
+
} | {
|
|
304
|
+
type: 'HIDE_CONFIRM_REMOVE_MARKETPLACE';
|
|
305
|
+
} | {
|
|
306
|
+
type: 'SHOW_ADD_MARKETPLACE_DIALOG';
|
|
307
|
+
} | {
|
|
308
|
+
type: 'HIDE_ADD_MARKETPLACE_DIALOG';
|
|
309
|
+
} | {
|
|
310
|
+
type: 'START_MARKETPLACE_OPERATION';
|
|
311
|
+
payload: {
|
|
312
|
+
operation: MarketplaceOperation;
|
|
313
|
+
marketplaceId?: string;
|
|
314
|
+
};
|
|
315
|
+
} | {
|
|
316
|
+
type: 'END_MARKETPLACE_OPERATION';
|
|
317
|
+
} | {
|
|
318
|
+
type: 'SET_ADD_MARKETPLACE_ERROR';
|
|
319
|
+
payload: string | null;
|
|
264
320
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@laststance/claude-plugin-dashboard",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Interactive CLI dashboard to manage Claude Code plugins",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryota Murakami <ryota.murakami@laststance.io> (https://github.com/ryota-murakami)",
|
|
@@ -65,4 +65,4 @@
|
|
|
65
65
|
"typescript": "^5.7.2",
|
|
66
66
|
"vitest": "^4.0.16"
|
|
67
67
|
}
|
|
68
|
-
}
|
|
68
|
+
}
|