claude-code-templates 1.21.12 → 1.22.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.
@@ -0,0 +1,689 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const express = require('express');
5
+ const open = require('open');
6
+ const os = require('os');
7
+
8
+ class PluginDashboard {
9
+ constructor(options = {}) {
10
+ this.options = options;
11
+ this.app = express();
12
+ this.port = 3336;
13
+ this.httpServer = null;
14
+ this.homeDir = os.homedir();
15
+ this.claudeDir = path.join(this.homeDir, '.claude');
16
+ this.settingsFile = path.join(this.claudeDir, 'settings.json');
17
+ }
18
+
19
+ async initialize() {
20
+ // Check if Claude directory exists
21
+ if (!(await fs.pathExists(this.claudeDir))) {
22
+ throw new Error(`Claude Code directory not found at ${this.claudeDir}`);
23
+ }
24
+
25
+ // Load plugin data
26
+ await this.loadPluginData();
27
+ this.setupWebServer();
28
+ }
29
+
30
+ async loadPluginData() {
31
+ try {
32
+ // Read Claude settings to get marketplace and plugin info
33
+ const settings = await this.readSettings();
34
+
35
+ // Load marketplaces
36
+ this.marketplaces = await this.loadMarketplaces(settings);
37
+
38
+ // Load installed plugins
39
+ this.plugins = await this.loadInstalledPlugins();
40
+
41
+ // Load permissions (agents, commands, hooks, MCPs)
42
+ this.permissions = await this.loadPermissions();
43
+
44
+ } catch (error) {
45
+ console.error(chalk.red('Error loading plugin data:'), error.message);
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ async readSettings() {
51
+ try {
52
+ if (await fs.pathExists(this.settingsFile)) {
53
+ const content = await fs.readFile(this.settingsFile, 'utf8');
54
+ const settings = JSON.parse(content);
55
+
56
+ // Extract enabled plugins from settings
57
+ // Plugins are stored in settings.enabledPlugins as "plugin-name@marketplace-name": true
58
+ this.enabledPlugins = new Set();
59
+ if (settings.enabledPlugins && typeof settings.enabledPlugins === 'object') {
60
+ for (const [key, value] of Object.entries(settings.enabledPlugins)) {
61
+ if (value === true) {
62
+ this.enabledPlugins.add(key);
63
+ }
64
+ }
65
+ }
66
+
67
+ return settings;
68
+ }
69
+ this.enabledPlugins = new Set();
70
+ return {};
71
+ } catch (error) {
72
+ console.warn(chalk.yellow('Warning: Could not read settings file'), error.message);
73
+ this.enabledPlugins = new Set();
74
+ return {};
75
+ }
76
+ }
77
+
78
+ async loadMarketplaces(settings) {
79
+ const marketplaces = [];
80
+
81
+ try {
82
+ // Read known_marketplaces.json from plugins directory
83
+ const knownMarketplacesFile = path.join(this.claudeDir, 'plugins', 'known_marketplaces.json');
84
+
85
+ if (!(await fs.pathExists(knownMarketplacesFile))) {
86
+ console.warn(chalk.yellow('Warning: known_marketplaces.json not found'));
87
+ return [];
88
+ }
89
+
90
+ const content = await fs.readFile(knownMarketplacesFile, 'utf8');
91
+ const knownMarketplacesData = JSON.parse(content);
92
+
93
+ // Parse the marketplace configuration
94
+ for (const [name, config] of Object.entries(knownMarketplacesData)) {
95
+ // Load marketplace details including plugin count
96
+ const marketplaceInfo = await this.loadMarketplaceDetails(name, config);
97
+
98
+ // Check if marketplace is enabled (exists in the filesystem)
99
+ const marketplacePath = path.join(this.claudeDir, 'plugins', 'marketplaces', name);
100
+ const enabled = await fs.pathExists(marketplacePath);
101
+
102
+ marketplaces.push({
103
+ name,
104
+ source: config,
105
+ type: this.getMarketplaceType(config),
106
+ enabled,
107
+ pluginCount: marketplaceInfo.pluginCount || 0,
108
+ lastUpdated: config.lastUpdated || null,
109
+ url: this.getMarketplaceUrl(config)
110
+ });
111
+ }
112
+
113
+ return marketplaces;
114
+ } catch (error) {
115
+ console.warn(chalk.yellow('Warning: Error loading marketplaces'), error.message);
116
+ return [];
117
+ }
118
+ }
119
+
120
+ getMarketplaceUrl(config) {
121
+ const source = config.source || config;
122
+ if (source.url) return source.url;
123
+ if (source.repo) return `https://github.com/${source.repo}`;
124
+ return null;
125
+ }
126
+
127
+ async loadMarketplaceDetails(name, config) {
128
+ try {
129
+ // Try to read marketplace.json from the marketplace source
130
+ const marketplacePath = path.join(this.claudeDir, 'plugins', 'marketplaces', name);
131
+ const marketplaceJsonPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
132
+
133
+ if (await fs.pathExists(marketplaceJsonPath)) {
134
+ const content = await fs.readFile(marketplaceJsonPath, 'utf8');
135
+ const marketplaceData = JSON.parse(content);
136
+ return {
137
+ pluginCount: marketplaceData.plugins ? marketplaceData.plugins.length : 0,
138
+ marketplaceData
139
+ };
140
+ }
141
+ } catch (error) {
142
+ console.warn(chalk.yellow(`Warning: Could not load marketplace details for ${name}`));
143
+ }
144
+
145
+ return { pluginCount: 0 };
146
+ }
147
+
148
+ getMarketplaceType(config) {
149
+ // Handle nested source structure
150
+ const source = config.source || config;
151
+
152
+ if (source.source === 'github') return 'GitHub';
153
+ if (source.source === 'git') return 'Git';
154
+ if (source.source === 'local') return 'Local';
155
+ return 'Unknown';
156
+ }
157
+
158
+ async loadInstalledPlugins() {
159
+ const plugins = [];
160
+ const pluginsMarketplacesDir = path.join(this.claudeDir, 'plugins', 'marketplaces');
161
+
162
+ try {
163
+ if (!(await fs.pathExists(pluginsMarketplacesDir))) {
164
+ console.warn(chalk.yellow('Warning: plugins/marketplaces directory not found'));
165
+ return [];
166
+ }
167
+
168
+ const marketplaceDirs = await fs.readdir(pluginsMarketplacesDir);
169
+
170
+ for (const marketplaceDir of marketplaceDirs) {
171
+ const marketplacePath = path.join(pluginsMarketplacesDir, marketplaceDir);
172
+ const stat = await fs.stat(marketplacePath);
173
+
174
+ if (!stat.isDirectory()) continue;
175
+
176
+ // Check if this is a marketplace directory (contains .claude-plugin/marketplace.json)
177
+ const marketplaceJsonPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
178
+
179
+ if (await fs.pathExists(marketplaceJsonPath)) {
180
+ // Load plugins from marketplace.json
181
+ const marketplacePlugins = await this.loadPluginsFromMarketplace(marketplacePath, marketplaceDir);
182
+ plugins.push(...marketplacePlugins);
183
+ continue;
184
+ }
185
+
186
+ // Scan for plugin directories (legacy support)
187
+ const pluginDirs = await fs.readdir(marketplacePath);
188
+
189
+ for (const pluginDir of pluginDirs) {
190
+ const pluginPath = path.join(marketplacePath, pluginDir);
191
+
192
+ try {
193
+ const pluginStat = await fs.stat(pluginPath);
194
+ if (!pluginStat.isDirectory()) continue;
195
+
196
+ // Read plugin.json
197
+ const pluginJsonPath = path.join(pluginPath, '.claude-plugin', 'plugin.json');
198
+
199
+ if (await fs.pathExists(pluginJsonPath)) {
200
+ const pluginJson = JSON.parse(await fs.readFile(pluginJsonPath, 'utf8'));
201
+
202
+ // Count components
203
+ const components = await this.countPluginComponents(pluginPath);
204
+
205
+ plugins.push({
206
+ name: pluginJson.name,
207
+ version: pluginJson.version || '1.0.0',
208
+ description: pluginJson.description || 'No description',
209
+ marketplace: marketplaceDir,
210
+ path: pluginPath,
211
+ components,
212
+ author: pluginJson.author,
213
+ homepage: pluginJson.homepage,
214
+ license: pluginJson.license
215
+ });
216
+ }
217
+ } catch (error) {
218
+ console.warn(chalk.yellow(`Warning: Error loading plugin ${pluginDir}`), error.message);
219
+ }
220
+ }
221
+ }
222
+
223
+ return plugins;
224
+ } catch (error) {
225
+ console.warn(chalk.yellow('Warning: Error loading plugins'), error.message);
226
+ return [];
227
+ }
228
+ }
229
+
230
+ async loadPluginsFromMarketplace(marketplacePath, marketplaceName) {
231
+ const plugins = [];
232
+
233
+ try {
234
+ const marketplaceJsonPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
235
+ const content = await fs.readFile(marketplaceJsonPath, 'utf8');
236
+ const marketplaceData = JSON.parse(content);
237
+
238
+ if (!marketplaceData.plugins || !Array.isArray(marketplaceData.plugins)) {
239
+ return [];
240
+ }
241
+
242
+ // Process each plugin definition
243
+ for (const pluginDef of marketplaceData.plugins) {
244
+ try {
245
+ let components = {
246
+ agents: 0,
247
+ commands: 0,
248
+ hooks: 0,
249
+ mcps: 0
250
+ };
251
+
252
+ const pluginSourcePath = pluginDef.source ? path.join(marketplacePath, pluginDef.source) : marketplacePath;
253
+
254
+ // Check if plugin has inline component definitions (claude-code-templates style)
255
+ if (pluginDef.agents || pluginDef.commands || pluginDef.mcpServers) {
256
+ components = {
257
+ agents: pluginDef.agents ? pluginDef.agents.length : 0,
258
+ commands: pluginDef.commands ? pluginDef.commands.length : 0,
259
+ hooks: pluginDef.hooks ? (Array.isArray(pluginDef.hooks) ? pluginDef.hooks.length : Object.keys(pluginDef.hooks).length) : 0,
260
+ mcps: pluginDef.mcpServers ? pluginDef.mcpServers.length : 0
261
+ };
262
+ }
263
+ // Otherwise, try to count from source directory (claude-code-plugins style)
264
+ else if (pluginDef.source) {
265
+ if (await fs.pathExists(pluginSourcePath)) {
266
+ components = await this.countPluginComponents(pluginSourcePath);
267
+ }
268
+ }
269
+
270
+ // Check if plugin is enabled in settings.json
271
+ const enabled = await this.isPluginEnabled(pluginDef.name, marketplaceName);
272
+
273
+ plugins.push({
274
+ name: pluginDef.name,
275
+ version: pluginDef.version || '1.0.0',
276
+ description: pluginDef.description || 'No description',
277
+ marketplace: marketplaceName,
278
+ path: pluginSourcePath,
279
+ components,
280
+ author: pluginDef.author,
281
+ homepage: pluginDef.homepage,
282
+ license: pluginDef.license,
283
+ keywords: pluginDef.keywords || [],
284
+ category: pluginDef.category,
285
+ enabled
286
+ });
287
+ } catch (error) {
288
+ console.warn(chalk.yellow(`Warning: Error processing plugin ${pluginDef.name}`), error.message);
289
+ }
290
+ }
291
+ } catch (error) {
292
+ console.warn(chalk.yellow(`Warning: Error loading plugins from marketplace ${marketplaceName}`), error.message);
293
+ }
294
+
295
+ return plugins;
296
+ }
297
+
298
+ async isPluginEnabled(pluginName, marketplace) {
299
+ // Check if plugin is enabled in settings.json
300
+ // Plugins are stored as "plugin-name@marketplace-name": true
301
+ const pluginKey = `${pluginName}@${marketplace}`;
302
+ return this.enabledPlugins && this.enabledPlugins.has(pluginKey);
303
+ }
304
+
305
+ async countPluginComponents(pluginPath) {
306
+ const components = {
307
+ agents: 0,
308
+ commands: 0,
309
+ hooks: 0,
310
+ mcps: 0
311
+ };
312
+
313
+ try {
314
+ // Count agents
315
+ const agentsDir = path.join(pluginPath, 'agents');
316
+ if (await fs.pathExists(agentsDir)) {
317
+ const agentFiles = await fs.readdir(agentsDir);
318
+ components.agents = agentFiles.filter(f => f.endsWith('.md')).length;
319
+ }
320
+
321
+ // Count commands
322
+ const commandsDir = path.join(pluginPath, 'commands');
323
+ if (await fs.pathExists(commandsDir)) {
324
+ const commandFiles = await fs.readdir(commandsDir);
325
+ components.commands = commandFiles.filter(f => f.endsWith('.md')).length;
326
+ }
327
+
328
+ // Count hooks
329
+ const hooksFile = path.join(pluginPath, 'hooks', 'hooks.json');
330
+ if (await fs.pathExists(hooksFile)) {
331
+ const hooksData = JSON.parse(await fs.readFile(hooksFile, 'utf8'));
332
+ components.hooks = Object.values(hooksData.hooks || {}).flat().length;
333
+ }
334
+
335
+ // Count MCPs
336
+ const mcpFile = path.join(pluginPath, '.mcp.json');
337
+ if (await fs.pathExists(mcpFile)) {
338
+ const mcpData = JSON.parse(await fs.readFile(mcpFile, 'utf8'));
339
+ components.mcps = Object.keys(mcpData.mcpServers || {}).length;
340
+ }
341
+ } catch (error) {
342
+ console.warn(chalk.yellow(`Warning: Error counting components for plugin at ${pluginPath}`), error.message);
343
+ }
344
+
345
+ return components;
346
+ }
347
+
348
+ async loadPermissions() {
349
+ const permissions = {
350
+ agents: [],
351
+ commands: [],
352
+ hooks: [],
353
+ mcps: []
354
+ };
355
+
356
+ try {
357
+ // Load user-level permissions
358
+ const userPermissions = await this.loadUserPermissions();
359
+
360
+ // Load plugin permissions
361
+ for (const plugin of this.plugins || []) {
362
+ const pluginPermissions = await this.loadPluginPermissions(plugin);
363
+
364
+ permissions.agents.push(...pluginPermissions.agents);
365
+ permissions.commands.push(...pluginPermissions.commands);
366
+ permissions.hooks.push(...pluginPermissions.hooks);
367
+ permissions.mcps.push(...pluginPermissions.mcps);
368
+ }
369
+
370
+ // Add user permissions
371
+ permissions.agents.push(...userPermissions.agents);
372
+ permissions.commands.push(...userPermissions.commands);
373
+ permissions.hooks.push(...userPermissions.hooks);
374
+ permissions.mcps.push(...userPermissions.mcps);
375
+
376
+ return permissions;
377
+ } catch (error) {
378
+ console.warn(chalk.yellow('Warning: Error loading permissions'), error.message);
379
+ return permissions;
380
+ }
381
+ }
382
+
383
+ async loadUserPermissions() {
384
+ const permissions = {
385
+ agents: [],
386
+ commands: [],
387
+ hooks: [],
388
+ mcps: []
389
+ };
390
+
391
+ try {
392
+ // Load user-level agents
393
+ const userAgentsDir = path.join(this.claudeDir, 'agents');
394
+ if (await fs.pathExists(userAgentsDir)) {
395
+ const agentFiles = await fs.readdir(userAgentsDir);
396
+ for (const file of agentFiles.filter(f => f.endsWith('.md'))) {
397
+ permissions.agents.push({
398
+ name: file.replace('.md', ''),
399
+ source: 'User',
400
+ plugin: null,
401
+ path: path.join(userAgentsDir, file)
402
+ });
403
+ }
404
+ }
405
+
406
+ // Load user-level commands
407
+ const userCommandsDir = path.join(this.claudeDir, 'commands');
408
+ if (await fs.pathExists(userCommandsDir)) {
409
+ const commandFiles = await fs.readdir(userCommandsDir);
410
+ for (const file of commandFiles.filter(f => f.endsWith('.md'))) {
411
+ permissions.commands.push({
412
+ name: file.replace('.md', ''),
413
+ source: 'User',
414
+ plugin: null,
415
+ path: path.join(userCommandsDir, file)
416
+ });
417
+ }
418
+ }
419
+
420
+ // Load user-level hooks
421
+ const userHooksFile = path.join(this.claudeDir, 'hooks', 'hooks.json');
422
+ if (await fs.pathExists(userHooksFile)) {
423
+ const hooksData = JSON.parse(await fs.readFile(userHooksFile, 'utf8'));
424
+ for (const [event, hooks] of Object.entries(hooksData.hooks || {})) {
425
+ for (const hook of hooks) {
426
+ permissions.hooks.push({
427
+ name: `${event} hook`,
428
+ event,
429
+ source: 'User',
430
+ plugin: null,
431
+ config: hook
432
+ });
433
+ }
434
+ }
435
+ }
436
+
437
+ // Load user-level MCPs
438
+ const userMcpFile = path.join(this.claudeDir, '.mcp.json');
439
+ if (await fs.pathExists(userMcpFile)) {
440
+ const mcpData = JSON.parse(await fs.readFile(userMcpFile, 'utf8'));
441
+ for (const [name, config] of Object.entries(mcpData.mcpServers || {})) {
442
+ permissions.mcps.push({
443
+ name,
444
+ source: 'User',
445
+ plugin: null,
446
+ config
447
+ });
448
+ }
449
+ }
450
+ } catch (error) {
451
+ console.warn(chalk.yellow('Warning: Error loading user permissions'), error.message);
452
+ }
453
+
454
+ return permissions;
455
+ }
456
+
457
+ async loadPluginPermissions(plugin) {
458
+ const permissions = {
459
+ agents: [],
460
+ commands: [],
461
+ hooks: [],
462
+ mcps: []
463
+ };
464
+
465
+ try {
466
+ // Load plugin agents
467
+ const agentsDir = path.join(plugin.path, 'agents');
468
+ if (await fs.pathExists(agentsDir)) {
469
+ const agentFiles = await fs.readdir(agentsDir);
470
+ for (const file of agentFiles.filter(f => f.endsWith('.md'))) {
471
+ permissions.agents.push({
472
+ name: file.replace('.md', ''),
473
+ source: 'Plugin',
474
+ plugin: plugin.name,
475
+ path: path.join(agentsDir, file)
476
+ });
477
+ }
478
+ }
479
+
480
+ // Load plugin commands
481
+ const commandsDir = path.join(plugin.path, 'commands');
482
+ if (await fs.pathExists(commandsDir)) {
483
+ const commandFiles = await fs.readdir(commandsDir);
484
+ for (const file of commandFiles.filter(f => f.endsWith('.md'))) {
485
+ permissions.commands.push({
486
+ name: file.replace('.md', ''),
487
+ source: 'Plugin',
488
+ plugin: plugin.name,
489
+ path: path.join(commandsDir, file)
490
+ });
491
+ }
492
+ }
493
+
494
+ // Load plugin hooks
495
+ const hooksFile = path.join(plugin.path, 'hooks', 'hooks.json');
496
+ if (await fs.pathExists(hooksFile)) {
497
+ const hooksData = JSON.parse(await fs.readFile(hooksFile, 'utf8'));
498
+ for (const [event, hooks] of Object.entries(hooksData.hooks || {})) {
499
+ for (const hook of hooks) {
500
+ permissions.hooks.push({
501
+ name: `${event} hook`,
502
+ event,
503
+ source: 'Plugin',
504
+ plugin: plugin.name,
505
+ config: hook
506
+ });
507
+ }
508
+ }
509
+ }
510
+
511
+ // Load plugin MCPs
512
+ const mcpFile = path.join(plugin.path, '.mcp.json');
513
+ if (await fs.pathExists(mcpFile)) {
514
+ const mcpData = JSON.parse(await fs.readFile(mcpFile, 'utf8'));
515
+ for (const [name, config] of Object.entries(mcpData.mcpServers || {})) {
516
+ permissions.mcps.push({
517
+ name,
518
+ source: 'Plugin',
519
+ plugin: plugin.name,
520
+ config
521
+ });
522
+ }
523
+ }
524
+ } catch (error) {
525
+ console.warn(chalk.yellow(`Warning: Error loading plugin permissions for ${plugin.name}`), error.message);
526
+ }
527
+
528
+ return permissions;
529
+ }
530
+
531
+ setupWebServer() {
532
+ // Add CORS middleware
533
+ this.app.use((req, res, next) => {
534
+ res.header('Access-Control-Allow-Origin', '*');
535
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
536
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
537
+
538
+ if (req.method === 'OPTIONS') {
539
+ res.sendStatus(200);
540
+ return;
541
+ }
542
+
543
+ next();
544
+ });
545
+
546
+ // Serve static files
547
+ this.app.use(express.static(path.join(__dirname, 'plugin-dashboard-web')));
548
+
549
+ // API endpoints - reload data on each request
550
+ this.app.get('/api/marketplaces', async (req, res) => {
551
+ try {
552
+ await this.loadPluginData();
553
+ res.json({
554
+ marketplaces: this.marketplaces || [],
555
+ count: (this.marketplaces || []).length,
556
+ timestamp: new Date().toISOString()
557
+ });
558
+ } catch (error) {
559
+ console.error('Error loading marketplaces:', error);
560
+ res.status(500).json({ error: 'Failed to load marketplaces' });
561
+ }
562
+ });
563
+
564
+ this.app.get('/api/plugins', async (req, res) => {
565
+ try {
566
+ await this.loadPluginData();
567
+ res.json({
568
+ plugins: this.plugins || [],
569
+ count: (this.plugins || []).length,
570
+ timestamp: new Date().toISOString()
571
+ });
572
+ } catch (error) {
573
+ console.error('Error loading plugins:', error);
574
+ res.status(500).json({ error: 'Failed to load plugins' });
575
+ }
576
+ });
577
+
578
+ this.app.get('/api/permissions', async (req, res) => {
579
+ try {
580
+ await this.loadPluginData();
581
+ res.json({
582
+ permissions: this.permissions || {},
583
+ counts: {
584
+ agents: (this.permissions?.agents || []).length,
585
+ commands: (this.permissions?.commands || []).length,
586
+ hooks: (this.permissions?.hooks || []).length,
587
+ mcps: (this.permissions?.mcps || []).length
588
+ },
589
+ timestamp: new Date().toISOString()
590
+ });
591
+ } catch (error) {
592
+ console.error('Error loading permissions:', error);
593
+ res.status(500).json({ error: 'Failed to load permissions' });
594
+ }
595
+ });
596
+
597
+ this.app.get('/api/summary', async (req, res) => {
598
+ try {
599
+ await this.loadPluginData();
600
+ res.json({
601
+ marketplaces: (this.marketplaces || []).length,
602
+ plugins: (this.plugins || []).length,
603
+ permissions: {
604
+ agents: (this.permissions?.agents || []).length,
605
+ commands: (this.permissions?.commands || []).length,
606
+ hooks: (this.permissions?.hooks || []).length,
607
+ mcps: (this.permissions?.mcps || []).length,
608
+ total: (this.permissions?.agents || []).length +
609
+ (this.permissions?.commands || []).length +
610
+ (this.permissions?.hooks || []).length +
611
+ (this.permissions?.mcps || []).length
612
+ },
613
+ timestamp: new Date().toISOString()
614
+ });
615
+ } catch (error) {
616
+ console.error('Error loading summary:', error);
617
+ res.status(500).json({ error: 'Failed to load summary' });
618
+ }
619
+ });
620
+
621
+ // Main route
622
+ this.app.get('/', (req, res) => {
623
+ res.sendFile(path.join(__dirname, 'plugin-dashboard-web', 'index.html'));
624
+ });
625
+ }
626
+
627
+ async startServer() {
628
+ return new Promise((resolve) => {
629
+ this.httpServer = this.app.listen(this.port, async () => {
630
+ console.log(chalk.green(`šŸ”Œ Plugin dashboard started at http://localhost:${this.port}`));
631
+ resolve();
632
+ });
633
+ });
634
+ }
635
+
636
+ async openBrowser() {
637
+ const url = `http://localhost:${this.port}`;
638
+ console.log(chalk.blue('🌐 Opening browser to Plugin Dashboard...'));
639
+
640
+ try {
641
+ await open(url);
642
+ } catch (error) {
643
+ console.log(chalk.yellow('Could not open browser automatically. Please visit:'));
644
+ console.log(chalk.cyan(url));
645
+ }
646
+ }
647
+
648
+ stop() {
649
+ if (this.httpServer) {
650
+ this.httpServer.close();
651
+ }
652
+ console.log(chalk.yellow('Plugin dashboard stopped'));
653
+ }
654
+ }
655
+
656
+ async function runPluginDashboard(options = {}) {
657
+ console.log(chalk.blue('šŸ”Œ Starting Claude Code Plugin Dashboard...'));
658
+
659
+ const dashboard = new PluginDashboard(options);
660
+
661
+ try {
662
+ await dashboard.initialize();
663
+ await dashboard.startServer();
664
+ await dashboard.openBrowser();
665
+
666
+ console.log(chalk.green('āœ… Plugin dashboard is running!'));
667
+ console.log(chalk.cyan(`🌐 Access at: http://localhost:${dashboard.port}`));
668
+ console.log(chalk.gray('Press Ctrl+C to stop the server'));
669
+
670
+ // Handle graceful shutdown
671
+ process.on('SIGINT', () => {
672
+ console.log(chalk.yellow('\nšŸ›‘ Shutting down plugin dashboard...'));
673
+ dashboard.stop();
674
+ process.exit(0);
675
+ });
676
+
677
+ // Keep the process running
678
+ await new Promise(() => {});
679
+
680
+ } catch (error) {
681
+ console.error(chalk.red('āŒ Failed to start plugin dashboard:'), error.message);
682
+ process.exit(1);
683
+ }
684
+ }
685
+
686
+ module.exports = {
687
+ runPluginDashboard,
688
+ PluginDashboard
689
+ };