@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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for Claude Code Plugin Dashboard
|
|
5
|
+
* Supports both interactive (TUI) and non-interactive (command) modes
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* claude-plugin-dashboard # Interactive mode
|
|
9
|
+
* claude-plugin-dashboard status # Show summary
|
|
10
|
+
* claude-plugin-dashboard list # List all plugins
|
|
11
|
+
* claude-plugin-dashboard list --installed # List installed plugins
|
|
12
|
+
* claude-plugin-dashboard enable <plugin-id> # Enable plugin
|
|
13
|
+
* claude-plugin-dashboard disable <plugin-id># Disable plugin
|
|
14
|
+
* claude-plugin-dashboard toggle <plugin-id> # Toggle plugin
|
|
15
|
+
* claude-plugin-dashboard help # Show help
|
|
16
|
+
*/
|
|
17
|
+
import { render } from 'ink';
|
|
18
|
+
import App from './app.js';
|
|
19
|
+
import { loadAllPlugins, loadInstalledPlugins, loadMarketplaces, getPluginStatistics, getPluginById, } from './services/pluginService.js';
|
|
20
|
+
import { enablePlugin, disablePlugin, togglePlugin, } from './services/settingsService.js';
|
|
21
|
+
import { fileExists } from './services/fileService.js';
|
|
22
|
+
import { PATHS } from './utils/paths.js';
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const command = args[0];
|
|
25
|
+
const subCommand = args[1];
|
|
26
|
+
/**
|
|
27
|
+
* Show status summary
|
|
28
|
+
*/
|
|
29
|
+
function showStatus() {
|
|
30
|
+
try {
|
|
31
|
+
const stats = getPluginStatistics();
|
|
32
|
+
const marketplaces = loadMarketplaces();
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log('⚡ Claude Code Plugin Dashboard');
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log('📊 Summary:');
|
|
37
|
+
console.log(` Total plugins: ${stats.total}`);
|
|
38
|
+
console.log(` Installed: ${stats.installed}`);
|
|
39
|
+
console.log(` Enabled: ${stats.enabled}`);
|
|
40
|
+
console.log(` Marketplaces: ${marketplaces.length}`);
|
|
41
|
+
console.log('');
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* List plugins
|
|
50
|
+
*/
|
|
51
|
+
function listPlugins(options) {
|
|
52
|
+
try {
|
|
53
|
+
let plugins = options.installed ? loadInstalledPlugins() : loadAllPlugins();
|
|
54
|
+
if (options.marketplace) {
|
|
55
|
+
plugins = plugins.filter((p) => p.marketplace === options.marketplace);
|
|
56
|
+
}
|
|
57
|
+
if (plugins.length === 0) {
|
|
58
|
+
console.log('No plugins found');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(`Found ${plugins.length} plugins:`);
|
|
63
|
+
console.log('');
|
|
64
|
+
for (const plugin of plugins) {
|
|
65
|
+
const status = plugin.isInstalled ? (plugin.isEnabled ? '●' : '◐') : '○';
|
|
66
|
+
const statusColor = plugin.isInstalled
|
|
67
|
+
? plugin.isEnabled
|
|
68
|
+
? '\x1b[32m'
|
|
69
|
+
: '\x1b[33m'
|
|
70
|
+
: '\x1b[90m';
|
|
71
|
+
const reset = '\x1b[0m';
|
|
72
|
+
console.log(`${statusColor}${status}${reset} ${plugin.id}`);
|
|
73
|
+
console.log(` ${plugin.description.slice(0, 60)}${plugin.description.length > 60 ? '...' : ''}`);
|
|
74
|
+
}
|
|
75
|
+
console.log('');
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Enable a plugin
|
|
84
|
+
*/
|
|
85
|
+
function handleEnable(pluginId) {
|
|
86
|
+
try {
|
|
87
|
+
const plugin = getPluginById(pluginId);
|
|
88
|
+
if (!plugin) {
|
|
89
|
+
console.error(`Plugin not found: ${pluginId}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
if (!plugin.isInstalled) {
|
|
93
|
+
console.error(`Plugin not installed: ${pluginId}`);
|
|
94
|
+
console.log('Install it first with: /plugin install in Claude Code');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
enablePlugin(pluginId);
|
|
98
|
+
console.log(`✅ ${plugin.name} enabled`);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Disable a plugin
|
|
107
|
+
*/
|
|
108
|
+
function handleDisable(pluginId) {
|
|
109
|
+
try {
|
|
110
|
+
const plugin = getPluginById(pluginId);
|
|
111
|
+
if (!plugin) {
|
|
112
|
+
console.error(`Plugin not found: ${pluginId}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
disablePlugin(pluginId);
|
|
116
|
+
console.log(`❌ ${plugin.name} disabled`);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Toggle a plugin
|
|
125
|
+
*/
|
|
126
|
+
function handleToggle(pluginId) {
|
|
127
|
+
try {
|
|
128
|
+
const plugin = getPluginById(pluginId);
|
|
129
|
+
if (!plugin) {
|
|
130
|
+
console.error(`Plugin not found: ${pluginId}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
if (!plugin.isInstalled) {
|
|
134
|
+
console.error(`Plugin not installed: ${pluginId}`);
|
|
135
|
+
console.log('Install it first with: /plugin install in Claude Code');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
const newState = togglePlugin(pluginId);
|
|
139
|
+
console.log(`${newState ? '✅' : '❌'} ${plugin.name} ${newState ? 'enabled' : 'disabled'}`);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Show plugin info
|
|
148
|
+
*/
|
|
149
|
+
function showPluginInfo(pluginId) {
|
|
150
|
+
try {
|
|
151
|
+
const plugin = getPluginById(pluginId);
|
|
152
|
+
if (!plugin) {
|
|
153
|
+
console.error(`Plugin not found: ${pluginId}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(`📦 ${plugin.name}`);
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log(`ID: ${plugin.id}`);
|
|
160
|
+
console.log(`Marketplace: ${plugin.marketplace}`);
|
|
161
|
+
console.log(`Version: ${plugin.version}`);
|
|
162
|
+
console.log(`Installs: ${plugin.installCount.toLocaleString()}`);
|
|
163
|
+
console.log(`Status: ${plugin.isInstalled ? (plugin.isEnabled ? 'Installed & Enabled' : 'Installed & Disabled') : 'Not Installed'}`);
|
|
164
|
+
if (plugin.category)
|
|
165
|
+
console.log(`Category: ${plugin.category}`);
|
|
166
|
+
if (plugin.author)
|
|
167
|
+
console.log(`Author: ${plugin.author.name}`);
|
|
168
|
+
if (plugin.homepage)
|
|
169
|
+
console.log(`Homepage: ${plugin.homepage}`);
|
|
170
|
+
console.log('');
|
|
171
|
+
console.log('Description:');
|
|
172
|
+
console.log(` ${plugin.description}`);
|
|
173
|
+
console.log('');
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Show help
|
|
182
|
+
*/
|
|
183
|
+
function showHelp() {
|
|
184
|
+
console.log(`
|
|
185
|
+
⚡ Claude Code Plugin Dashboard
|
|
186
|
+
|
|
187
|
+
An interactive CLI tool to browse and manage Claude Code plugins.
|
|
188
|
+
|
|
189
|
+
USAGE:
|
|
190
|
+
claude-plugin-dashboard [command] [options]
|
|
191
|
+
|
|
192
|
+
COMMANDS:
|
|
193
|
+
(none) Open interactive dashboard
|
|
194
|
+
status Show summary statistics
|
|
195
|
+
list List all available plugins
|
|
196
|
+
list --installed List installed plugins only
|
|
197
|
+
list --marketplace <id> List plugins from specific marketplace
|
|
198
|
+
info <plugin-id> Show plugin details
|
|
199
|
+
enable <plugin-id> Enable an installed plugin
|
|
200
|
+
disable <plugin-id> Disable a plugin
|
|
201
|
+
toggle <plugin-id> Toggle plugin enabled state
|
|
202
|
+
help Show this help message
|
|
203
|
+
|
|
204
|
+
EXAMPLES:
|
|
205
|
+
claude-plugin-dashboard
|
|
206
|
+
claude-plugin-dashboard status
|
|
207
|
+
claude-plugin-dashboard list --installed
|
|
208
|
+
claude-plugin-dashboard info context7@claude-plugins-official
|
|
209
|
+
claude-plugin-dashboard toggle context7@claude-plugins-official
|
|
210
|
+
|
|
211
|
+
INTERACTIVE MODE:
|
|
212
|
+
← → Switch tabs
|
|
213
|
+
↑ ↓ Navigate list
|
|
214
|
+
^P ^N Navigate list (Emacs-style)
|
|
215
|
+
i Install selected plugin
|
|
216
|
+
u Uninstall selected plugin
|
|
217
|
+
Space Toggle plugin enable/disable
|
|
218
|
+
/ Search plugins
|
|
219
|
+
s Cycle sort options
|
|
220
|
+
q Quit
|
|
221
|
+
|
|
222
|
+
For more information, visit:
|
|
223
|
+
https://github.com/laststance/claude-code-plugin-dashboard
|
|
224
|
+
`);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Check if Claude Code is installed
|
|
228
|
+
*/
|
|
229
|
+
function checkClaudeCodeInstalled() {
|
|
230
|
+
if (!fileExists(PATHS.settings)) {
|
|
231
|
+
console.error('');
|
|
232
|
+
console.error('❌ Claude Code not found');
|
|
233
|
+
console.error('');
|
|
234
|
+
console.error('This tool requires Claude Code to be installed.');
|
|
235
|
+
console.error('Expected settings file at: ' + PATHS.settings);
|
|
236
|
+
console.error('');
|
|
237
|
+
console.error('Install Claude Code: https://claude.ai/code');
|
|
238
|
+
console.error('');
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Main execution
|
|
243
|
+
if (command) {
|
|
244
|
+
// Non-interactive mode
|
|
245
|
+
checkClaudeCodeInstalled();
|
|
246
|
+
switch (command) {
|
|
247
|
+
case 'status':
|
|
248
|
+
showStatus();
|
|
249
|
+
break;
|
|
250
|
+
case 'list':
|
|
251
|
+
if (subCommand === '--installed' || args.includes('--installed')) {
|
|
252
|
+
listPlugins({ installed: true });
|
|
253
|
+
}
|
|
254
|
+
else if (subCommand === '--marketplace' ||
|
|
255
|
+
args.includes('--marketplace')) {
|
|
256
|
+
const marketplaceIndex = args.indexOf('--marketplace');
|
|
257
|
+
const marketplace = args[marketplaceIndex + 1];
|
|
258
|
+
listPlugins({ marketplace });
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
listPlugins({});
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
case 'info':
|
|
265
|
+
if (!subCommand) {
|
|
266
|
+
console.error('Usage: claude-plugin-dashboard info <plugin-id>');
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
showPluginInfo(subCommand);
|
|
270
|
+
break;
|
|
271
|
+
case 'enable':
|
|
272
|
+
if (!subCommand) {
|
|
273
|
+
console.error('Usage: claude-plugin-dashboard enable <plugin-id>');
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
handleEnable(subCommand);
|
|
277
|
+
break;
|
|
278
|
+
case 'disable':
|
|
279
|
+
if (!subCommand) {
|
|
280
|
+
console.error('Usage: claude-plugin-dashboard disable <plugin-id>');
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
handleDisable(subCommand);
|
|
284
|
+
break;
|
|
285
|
+
case 'toggle':
|
|
286
|
+
if (!subCommand) {
|
|
287
|
+
console.error('Usage: claude-plugin-dashboard toggle <plugin-id>');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
handleToggle(subCommand);
|
|
291
|
+
break;
|
|
292
|
+
case 'help':
|
|
293
|
+
case '-h':
|
|
294
|
+
case '--help':
|
|
295
|
+
showHelp();
|
|
296
|
+
break;
|
|
297
|
+
case '-v':
|
|
298
|
+
case '--version':
|
|
299
|
+
console.log('claude-plugin-dashboard v0.1.0');
|
|
300
|
+
break;
|
|
301
|
+
default:
|
|
302
|
+
console.error(`Unknown command: ${command}`);
|
|
303
|
+
console.log('Run "claude-plugin-dashboard help" for usage information.');
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// Interactive mode
|
|
309
|
+
checkClaudeCodeInstalled();
|
|
310
|
+
if (!process.stdin.isTTY) {
|
|
311
|
+
console.log('Interactive mode requires a TTY.');
|
|
312
|
+
console.log('Use "claude-plugin-dashboard help" for non-interactive commands.');
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
render(_jsx(App, {}));
|
|
316
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfirmDialog component
|
|
3
|
+
* Displays a Y/N confirmation prompt for destructive actions
|
|
4
|
+
*/
|
|
5
|
+
interface ConfirmDialogProps {
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A simple confirmation dialog that prompts Y/N
|
|
10
|
+
* @example
|
|
11
|
+
* <ConfirmDialog message="Uninstall plugin-name@marketplace?" />
|
|
12
|
+
*/
|
|
13
|
+
export default function ConfirmDialog({ message }: ConfirmDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ConfirmDialog component
|
|
4
|
+
* Displays a Y/N confirmation prompt for destructive actions
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
/**
|
|
8
|
+
* A simple confirmation dialog that prompts Y/N
|
|
9
|
+
* @example
|
|
10
|
+
* <ConfirmDialog message="Uninstall plugin-name@marketplace?" />
|
|
11
|
+
*/
|
|
12
|
+
export default function ConfirmDialog({ message }) {
|
|
13
|
+
return (_jsxs(Box, { marginTop: 1, paddingX: 2, paddingY: 1, borderStyle: "double", borderColor: "yellow", children: [_jsxs(Text, { children: [message, " "] }), _jsx(Text, { bold: true, color: "green", children: "Y" }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { bold: true, color: "red", children: "N" })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KeyHints component
|
|
3
|
+
* Displays keyboard shortcuts footer at the bottom of the dashboard
|
|
4
|
+
*/
|
|
5
|
+
interface KeyHintsProps {
|
|
6
|
+
/** Additional context-specific hints */
|
|
7
|
+
extraHints?: Array<{
|
|
8
|
+
key: string;
|
|
9
|
+
action: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Displays keyboard shortcut hints in the footer
|
|
14
|
+
* @example
|
|
15
|
+
* <KeyHints />
|
|
16
|
+
* <KeyHints extraHints={[{ key: 'i', action: 'install' }]} />
|
|
17
|
+
*/
|
|
18
|
+
export default function KeyHints({ extraHints }: KeyHintsProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* KeyHints component
|
|
4
|
+
* Displays keyboard shortcuts footer at the bottom of the dashboard
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
/**
|
|
8
|
+
* Displays keyboard shortcut hints in the footer
|
|
9
|
+
* @example
|
|
10
|
+
* <KeyHints />
|
|
11
|
+
* <KeyHints extraHints={[{ key: 'i', action: 'install' }]} />
|
|
12
|
+
*/
|
|
13
|
+
export default function KeyHints({ extraHints }) {
|
|
14
|
+
const baseHints = [
|
|
15
|
+
{ key: '←/→', action: 'tabs' },
|
|
16
|
+
{ key: '↑/↓', action: 'navigate' },
|
|
17
|
+
{ key: 'Space', action: 'toggle' },
|
|
18
|
+
{ key: '/', action: 'search' },
|
|
19
|
+
{ key: 'q', action: 'quit' },
|
|
20
|
+
];
|
|
21
|
+
const allHints = extraHints ? [...baseHints, ...extraHints] : baseHints;
|
|
22
|
+
return (_jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: _jsx(Box, { gap: 2, flexWrap: "wrap", children: allHints.map((hint, index) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: "white", children: hint.key }), _jsx(Text, { dimColor: true, children: hint.action })] }, index))) }) }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarketplaceDetail component
|
|
3
|
+
* Right panel showing marketplace information
|
|
4
|
+
*/
|
|
5
|
+
import type { Marketplace } from '../types/index.js';
|
|
6
|
+
interface MarketplaceDetailProps {
|
|
7
|
+
marketplace: Marketplace | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Displays detailed information about a selected marketplace
|
|
11
|
+
* @example
|
|
12
|
+
* <MarketplaceDetail marketplace={selectedMarketplace} />
|
|
13
|
+
*/
|
|
14
|
+
export default function MarketplaceDetail({ marketplace, }: MarketplaceDetailProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* MarketplaceDetail component
|
|
4
|
+
* Right panel showing marketplace information
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
/**
|
|
8
|
+
* Displays detailed information about a selected marketplace
|
|
9
|
+
* @example
|
|
10
|
+
* <MarketplaceDetail marketplace={selectedMarketplace} />
|
|
11
|
+
*/
|
|
12
|
+
export default function MarketplaceDetail({ marketplace, }) {
|
|
13
|
+
if (!marketplace) {
|
|
14
|
+
return (_jsx(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: "Select a marketplace to view details" }) }));
|
|
15
|
+
}
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: marketplace.name || marketplace.id }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(DetailRow, { label: "ID", value: marketplace.id }), _jsx(DetailRow, { label: "Plugins", value: `${marketplace.pluginCount || 0}` }), _jsx(DetailRow, { label: "Source", value: marketplace.source.source }), marketplace.source.url && (_jsx(DetailRow, { label: "URL", value: marketplace.source.url })), marketplace.source.repo && (_jsx(DetailRow, { label: "Repo", value: marketplace.source.repo })), _jsx(DetailRow, { label: "Last Updated", value: formatDate(marketplace.lastUpdated) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "Install Location:" }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: marketplace.installLocation })] })] }));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Single detail row with label and value
|
|
20
|
+
*/
|
|
21
|
+
function DetailRow({ label, value }) {
|
|
22
|
+
return (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: "gray", children: [label, ":"] }), _jsx(Text, { children: value })] }));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Format ISO date string to readable format
|
|
26
|
+
*/
|
|
27
|
+
function formatDate(isoString) {
|
|
28
|
+
try {
|
|
29
|
+
const date = new Date(isoString);
|
|
30
|
+
return date.toLocaleDateString('en-US', {
|
|
31
|
+
year: 'numeric',
|
|
32
|
+
month: 'short',
|
|
33
|
+
day: 'numeric',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return isoString;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarketplaceList component
|
|
3
|
+
* List of marketplace sources
|
|
4
|
+
*/
|
|
5
|
+
import type { Marketplace } from '../types/index.js';
|
|
6
|
+
interface MarketplaceListProps {
|
|
7
|
+
marketplaces: Marketplace[];
|
|
8
|
+
selectedIndex: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Displays a list of marketplaces
|
|
12
|
+
* @example
|
|
13
|
+
* <MarketplaceList marketplaces={marketplaces} selectedIndex={0} />
|
|
14
|
+
*/
|
|
15
|
+
export default function MarketplaceList({ marketplaces, selectedIndex, }: MarketplaceListProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* MarketplaceList component
|
|
4
|
+
* List of marketplace sources
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
/**
|
|
8
|
+
* Displays a list of marketplaces
|
|
9
|
+
* @example
|
|
10
|
+
* <MarketplaceList marketplaces={marketplaces} selectedIndex={0} />
|
|
11
|
+
*/
|
|
12
|
+
export default function MarketplaceList({ marketplaces, selectedIndex, }) {
|
|
13
|
+
if (marketplaces.length === 0) {
|
|
14
|
+
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "No marketplaces found" }) }));
|
|
15
|
+
}
|
|
16
|
+
return (_jsx(Box, { flexDirection: "column", children: marketplaces.map((marketplace, index) => {
|
|
17
|
+
const isSelected = index === selectedIndex;
|
|
18
|
+
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected ? _jsx(Text, { color: "cyan", children: '>' }) : _jsx(Text, { children: " " }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected ? 'cyan' : 'white', children: marketplace.name || marketplace.id }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [marketplace.pluginCount || 0, " plugins"] })] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: getSourceDisplay(marketplace) })] })] }, marketplace.id));
|
|
19
|
+
}) }));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get display string for marketplace source
|
|
23
|
+
*/
|
|
24
|
+
function getSourceDisplay(marketplace) {
|
|
25
|
+
if (marketplace.source.url) {
|
|
26
|
+
return marketplace.source.url;
|
|
27
|
+
}
|
|
28
|
+
if (marketplace.source.repo) {
|
|
29
|
+
return `github.com/${marketplace.source.repo}`;
|
|
30
|
+
}
|
|
31
|
+
return marketplace.source.source;
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginDetail component
|
|
3
|
+
* Right panel showing detailed plugin information
|
|
4
|
+
*/
|
|
5
|
+
import type { Plugin } from '../types/index.js';
|
|
6
|
+
interface PluginDetailProps {
|
|
7
|
+
plugin: Plugin | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Displays detailed information about a selected plugin
|
|
11
|
+
* @example
|
|
12
|
+
* <PluginDetail plugin={selectedPlugin} />
|
|
13
|
+
*/
|
|
14
|
+
export default function PluginDetail({ plugin }: PluginDetailProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* PluginDetail component
|
|
4
|
+
* Right panel showing detailed plugin information
|
|
5
|
+
*/
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
import StatusIcon from './StatusIcon.js';
|
|
8
|
+
/**
|
|
9
|
+
* Displays detailed information about a selected plugin
|
|
10
|
+
* @example
|
|
11
|
+
* <PluginDetail plugin={selectedPlugin} />
|
|
12
|
+
*/
|
|
13
|
+
export default function PluginDetail({ plugin }) {
|
|
14
|
+
if (!plugin) {
|
|
15
|
+
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
|
+
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
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Single detail row with label and value
|
|
21
|
+
*/
|
|
22
|
+
function DetailRow({ label, value }) {
|
|
23
|
+
return (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: "gray", children: [label, ":"] }), _jsx(Text, { children: value })] }));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Format large numbers with K/M suffix
|
|
27
|
+
*/
|
|
28
|
+
function formatCount(count) {
|
|
29
|
+
if (count >= 1000000) {
|
|
30
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
31
|
+
}
|
|
32
|
+
if (count >= 1000) {
|
|
33
|
+
return `${(count / 1000).toFixed(1)}K`;
|
|
34
|
+
}
|
|
35
|
+
return count.toString();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Format ISO date string to readable format
|
|
39
|
+
*/
|
|
40
|
+
function formatDate(isoString) {
|
|
41
|
+
try {
|
|
42
|
+
const date = new Date(isoString);
|
|
43
|
+
return date.toLocaleDateString('en-US', {
|
|
44
|
+
year: 'numeric',
|
|
45
|
+
month: 'short',
|
|
46
|
+
day: 'numeric',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return isoString;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginList component
|
|
3
|
+
* Scrollable list of plugins with selection highlighting
|
|
4
|
+
* Supports ↑ ↓ arrow key navigation
|
|
5
|
+
*/
|
|
6
|
+
import type { Plugin } from '../types/index.js';
|
|
7
|
+
interface PluginListProps {
|
|
8
|
+
plugins: Plugin[];
|
|
9
|
+
selectedIndex: number;
|
|
10
|
+
/** Maximum visible items (for virtual scrolling) */
|
|
11
|
+
visibleCount?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Scrollable plugin list with selection
|
|
15
|
+
* @example
|
|
16
|
+
* <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} />
|
|
17
|
+
*/
|
|
18
|
+
export default function PluginList({ plugins, selectedIndex, visibleCount, }: PluginListProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* PluginList component
|
|
4
|
+
* Scrollable list of plugins with selection highlighting
|
|
5
|
+
* Supports ↑ ↓ arrow key navigation
|
|
6
|
+
*/
|
|
7
|
+
import { Box, Text } from 'ink';
|
|
8
|
+
import StatusIcon from './StatusIcon.js';
|
|
9
|
+
/**
|
|
10
|
+
* Scrollable plugin list with selection
|
|
11
|
+
* @example
|
|
12
|
+
* <PluginList plugins={plugins} selectedIndex={0} visibleCount={15} />
|
|
13
|
+
*/
|
|
14
|
+
export default function PluginList({ plugins, selectedIndex, visibleCount = 15, }) {
|
|
15
|
+
if (plugins.length === 0) {
|
|
16
|
+
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "No plugins found" }) }));
|
|
17
|
+
}
|
|
18
|
+
// Calculate visible window
|
|
19
|
+
const halfVisible = Math.floor(visibleCount / 2);
|
|
20
|
+
let startIndex = Math.max(0, selectedIndex - halfVisible);
|
|
21
|
+
const endIndex = Math.min(plugins.length, startIndex + visibleCount);
|
|
22
|
+
// Adjust start if we're near the end
|
|
23
|
+
if (endIndex - startIndex < visibleCount) {
|
|
24
|
+
startIndex = Math.max(0, endIndex - visibleCount);
|
|
25
|
+
}
|
|
26
|
+
const visiblePlugins = plugins.slice(startIndex, endIndex);
|
|
27
|
+
const hasMore = endIndex < plugins.length;
|
|
28
|
+
const hasPrevious = startIndex > 0;
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", children: [hasPrevious && _jsxs(Text, { dimColor: true, children: ["\u2191 ", startIndex, " more above"] }), visiblePlugins.map((plugin, index) => {
|
|
30
|
+
const actualIndex = startIndex + index;
|
|
31
|
+
const isSelected = actualIndex === selectedIndex;
|
|
32
|
+
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, children: isSelected ? _jsx(Text, { color: "cyan", children: '>' }) : _jsx(Text, { children: " " }) }), _jsx(Box, { width: 2, children: _jsx(StatusIcon, { isInstalled: plugin.isInstalled, isEnabled: plugin.isEnabled }) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, color: isSelected ? 'cyan' : 'white', children: plugin.name }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { color: "gray", children: truncate(plugin.marketplace, 20) }), plugin.installCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), _jsxs(Text, { color: "gray", children: [formatCount(plugin.installCount), " installs"] })] }))] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: truncate(plugin.description, 60) })] })] }, plugin.id));
|
|
33
|
+
}), hasMore && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", plugins.length - endIndex, " more below"] }))] }));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Truncate text to max length with ellipsis
|
|
37
|
+
*/
|
|
38
|
+
function truncate(text, maxLength) {
|
|
39
|
+
if (text.length <= maxLength)
|
|
40
|
+
return text;
|
|
41
|
+
return text.slice(0, maxLength - 3) + '...';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Format large numbers with K/M suffix
|
|
45
|
+
*/
|
|
46
|
+
function formatCount(count) {
|
|
47
|
+
if (count >= 1000000) {
|
|
48
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
49
|
+
}
|
|
50
|
+
if (count >= 1000) {
|
|
51
|
+
return `${(count / 1000).toFixed(1)}K`;
|
|
52
|
+
}
|
|
53
|
+
return count.toString();
|
|
54
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchInput component
|
|
3
|
+
* Filter/search box for the plugin list
|
|
4
|
+
*/
|
|
5
|
+
interface SearchInputProps {
|
|
6
|
+
query: string;
|
|
7
|
+
isActive?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Search input display (read-only display, actual input handled by useInput)
|
|
12
|
+
* @example
|
|
13
|
+
* <SearchInput query={searchQuery} isActive={isSearchMode} />
|
|
14
|
+
*/
|
|
15
|
+
export default function SearchInput({ query, isActive, placeholder, }: SearchInputProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|