claude-code-templates 1.27.0 → 1.28.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,441 @@
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
+ const yaml = require('js-yaml');
8
+
9
+ class SkillDashboard {
10
+ constructor(options = {}) {
11
+ this.options = options;
12
+ this.app = express();
13
+ this.port = 3337;
14
+ this.httpServer = null;
15
+ this.homeDir = os.homedir();
16
+ this.claudeDir = path.join(this.homeDir, '.claude');
17
+ this.personalSkillsDir = path.join(this.claudeDir, 'skills');
18
+ }
19
+
20
+ async initialize() {
21
+ // Load skills data
22
+ await this.loadSkillsData();
23
+ this.setupWebServer();
24
+ }
25
+
26
+ async loadSkillsData() {
27
+ try {
28
+ // Load personal skills
29
+ this.personalSkills = await this.loadSkillsFromDirectory(this.personalSkillsDir, 'Personal');
30
+
31
+ // Load project skills (if in a project directory)
32
+ const projectSkillsDir = path.join(process.cwd(), '.claude', 'skills');
33
+ this.projectSkills = await this.loadSkillsFromDirectory(projectSkillsDir, 'Project');
34
+
35
+ // Combine all skills
36
+ this.skills = [...this.personalSkills, ...this.projectSkills];
37
+
38
+ } catch (error) {
39
+ console.error(chalk.red('Error loading skills data:'), error.message);
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ async loadSkillsFromDirectory(skillsDir, source) {
45
+ const skills = [];
46
+
47
+ try {
48
+ if (!(await fs.pathExists(skillsDir))) {
49
+ if (this.options.verbose) {
50
+ console.log(chalk.yellow(`Skills directory not found: ${skillsDir}`));
51
+ }
52
+ return skills;
53
+ }
54
+
55
+ const skillDirs = await fs.readdir(skillsDir);
56
+
57
+ for (const skillDir of skillDirs) {
58
+ const skillPath = path.join(skillsDir, skillDir);
59
+
60
+ try {
61
+ const stat = await fs.stat(skillPath);
62
+ if (!stat.isDirectory()) continue;
63
+
64
+ // Look for SKILL.md
65
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
66
+
67
+ if (await fs.pathExists(skillMdPath)) {
68
+ const skillData = await this.parseSkill(skillMdPath, skillPath, skillDir, source);
69
+ if (skillData) {
70
+ skills.push(skillData);
71
+ }
72
+ }
73
+ } catch (error) {
74
+ console.warn(chalk.yellow(`Warning: Error loading skill ${skillDir}`), error.message);
75
+ }
76
+ }
77
+
78
+ return skills;
79
+ } catch (error) {
80
+ console.warn(chalk.yellow(`Warning: Error loading skills from ${skillsDir}`), error.message);
81
+ return skills;
82
+ }
83
+ }
84
+
85
+ async parseSkill(skillMdPath, skillPath, skillDirName, source) {
86
+ try {
87
+ const content = await fs.readFile(skillMdPath, 'utf8');
88
+
89
+ // Parse YAML frontmatter
90
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
91
+ let frontmatter = {};
92
+ let markdownContent = content;
93
+
94
+ if (frontmatterMatch) {
95
+ try {
96
+ frontmatter = yaml.load(frontmatterMatch[1]);
97
+ markdownContent = content.substring(frontmatterMatch[0].length).trim();
98
+ } catch (error) {
99
+ console.warn(chalk.yellow(`Warning: Could not parse YAML frontmatter for ${skillDirName}`));
100
+ }
101
+ }
102
+
103
+ // Get file stats
104
+ const stats = await fs.stat(skillMdPath);
105
+
106
+ // Scan for supporting files
107
+ const supportingFiles = await this.scanSupportingFiles(skillPath);
108
+
109
+ // Categorize files by loading strategy
110
+ const categorizedFiles = this.categorizeFiles(supportingFiles, markdownContent);
111
+
112
+ return {
113
+ name: frontmatter.name || skillDirName,
114
+ description: frontmatter.description || 'No description available',
115
+ allowedTools: frontmatter['allowed-tools'] || frontmatter.allowedTools || null,
116
+ source,
117
+ path: skillPath,
118
+ mainFile: 'SKILL.md',
119
+ mainFilePath: skillMdPath,
120
+ mainFileSize: this.formatFileSize(stats.size),
121
+ lastModified: stats.mtime,
122
+ fileCount: supportingFiles.length + 1, // +1 for SKILL.md
123
+ supportingFiles: categorizedFiles,
124
+ rawContent: content,
125
+ markdownContent
126
+ };
127
+ } catch (error) {
128
+ console.warn(chalk.yellow(`Warning: Error parsing skill ${skillDirName}`), error.message);
129
+ return null;
130
+ }
131
+ }
132
+
133
+ async scanSupportingFiles(skillPath) {
134
+ const files = [];
135
+ const self = this; // Preserve 'this' context
136
+
137
+ async function scanDirectory(dir, relativePath = '') {
138
+ const entries = await fs.readdir(dir);
139
+
140
+ for (const entry of entries) {
141
+ const fullPath = path.join(dir, entry);
142
+ const relPath = relativePath ? path.join(relativePath, entry) : entry;
143
+
144
+ try {
145
+ const stat = await fs.stat(fullPath);
146
+
147
+ if (stat.isDirectory()) {
148
+ await scanDirectory(fullPath, relPath);
149
+ } else if (entry !== 'SKILL.md') {
150
+ files.push({
151
+ name: entry,
152
+ path: fullPath,
153
+ relativePath: relPath,
154
+ size: stat.size,
155
+ isDirectory: false,
156
+ type: self.getFileType(entry) // Use self instead of this
157
+ });
158
+ }
159
+ } catch (error) {
160
+ // Skip files we can't read
161
+ }
162
+ }
163
+ }
164
+
165
+ try {
166
+ await scanDirectory(skillPath);
167
+ } catch (error) {
168
+ console.warn(chalk.yellow(`Warning: Error scanning skill directory ${skillPath}`), error.message);
169
+ }
170
+
171
+ return files;
172
+ }
173
+
174
+ categorizeFiles(files, markdownContent) {
175
+ const categorized = {
176
+ alwaysLoaded: ['SKILL.md'],
177
+ onDemand: [],
178
+ progressive: []
179
+ };
180
+
181
+ // Parse referenced files from markdown content
182
+ const referencedFiles = this.extractReferencedFiles(markdownContent);
183
+
184
+ for (const file of files) {
185
+ const ext = path.extname(file.name).toLowerCase();
186
+
187
+ // Check if file is referenced in SKILL.md
188
+ const isReferenced = referencedFiles.some(ref =>
189
+ file.relativePath.includes(ref) || ref.includes(file.name)
190
+ );
191
+
192
+ // Categorize based on file type and references
193
+ if (isReferenced && ext === '.md') {
194
+ categorized.onDemand.push(file);
195
+ } else if (ext === '.md') {
196
+ categorized.onDemand.push(file);
197
+ } else if (file.relativePath.startsWith('scripts/') ||
198
+ file.relativePath.startsWith('templates/') ||
199
+ ext === '.py' || ext === '.js' || ext === '.sh') {
200
+ categorized.progressive.push(file);
201
+ } else {
202
+ categorized.onDemand.push(file);
203
+ }
204
+ }
205
+
206
+ return categorized;
207
+ }
208
+
209
+ extractReferencedFiles(markdownContent) {
210
+ const references = [];
211
+
212
+ // Match markdown links: [text](file.md)
213
+ const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
214
+ let match;
215
+
216
+ while ((match = linkPattern.exec(markdownContent)) !== null) {
217
+ const href = match[2];
218
+ // Only include relative file references (not URLs)
219
+ if (!href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('#')) {
220
+ references.push(href);
221
+ }
222
+ }
223
+
224
+ return references;
225
+ }
226
+
227
+ getFileType(filename) {
228
+ const ext = path.extname(filename).toLowerCase();
229
+ const typeMap = {
230
+ '.md': 'markdown',
231
+ '.py': 'python',
232
+ '.js': 'javascript',
233
+ '.ts': 'typescript',
234
+ '.sh': 'shell',
235
+ '.json': 'json',
236
+ '.yaml': 'yaml',
237
+ '.yml': 'yaml',
238
+ '.txt': 'text',
239
+ '.html': 'html',
240
+ '.css': 'css'
241
+ };
242
+ return typeMap[ext] || 'unknown';
243
+ }
244
+
245
+ formatFileSize(bytes) {
246
+ if (bytes < 1024) return `${bytes} B`;
247
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
248
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
249
+ }
250
+
251
+ setupWebServer() {
252
+ // Add CORS middleware
253
+ this.app.use((req, res, next) => {
254
+ res.header('Access-Control-Allow-Origin', '*');
255
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
256
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
257
+
258
+ if (req.method === 'OPTIONS') {
259
+ res.sendStatus(200);
260
+ return;
261
+ }
262
+
263
+ next();
264
+ });
265
+
266
+ // JSON middleware
267
+ this.app.use(express.json());
268
+
269
+ // Serve static files
270
+ this.app.use(express.static(path.join(__dirname, 'skill-dashboard-web')));
271
+
272
+ // API endpoints - reload data on each request
273
+ this.app.get('/api/skills', async (req, res) => {
274
+ try {
275
+ await this.loadSkillsData();
276
+ res.json({
277
+ skills: this.skills || [],
278
+ count: (this.skills || []).length,
279
+ timestamp: new Date().toISOString()
280
+ });
281
+ } catch (error) {
282
+ console.error('Error loading skills:', error);
283
+ res.status(500).json({ error: 'Failed to load skills' });
284
+ }
285
+ });
286
+
287
+ this.app.get('/api/skills/:name', async (req, res) => {
288
+ try {
289
+ await this.loadSkillsData();
290
+ const skillName = req.params.name;
291
+ const skill = this.skills.find(s =>
292
+ s.name === skillName ||
293
+ s.name.toLowerCase().replace(/\s+/g, '-') === skillName.toLowerCase()
294
+ );
295
+
296
+ if (!skill) {
297
+ return res.status(404).json({ error: 'Skill not found' });
298
+ }
299
+
300
+ res.json({
301
+ skill,
302
+ timestamp: new Date().toISOString()
303
+ });
304
+ } catch (error) {
305
+ console.error('Error loading skill:', error);
306
+ res.status(500).json({ error: 'Failed to load skill' });
307
+ }
308
+ });
309
+
310
+ this.app.get('/api/skills/:name/file/*', async (req, res) => {
311
+ try {
312
+ const skillName = req.params.name;
313
+ const filePath = req.params[0]; // Capture the wildcard path
314
+
315
+ await this.loadSkillsData();
316
+ const skill = this.skills.find(s =>
317
+ s.name === skillName ||
318
+ s.name.toLowerCase().replace(/\s+/g, '-') === skillName.toLowerCase()
319
+ );
320
+
321
+ if (!skill) {
322
+ return res.status(404).json({ error: 'Skill not found' });
323
+ }
324
+
325
+ const fullPath = path.join(skill.path, filePath);
326
+
327
+ // Security check: ensure the file is within the skill directory
328
+ const normalizedPath = path.normalize(fullPath);
329
+ if (!normalizedPath.startsWith(skill.path)) {
330
+ return res.status(403).json({ error: 'Access denied' });
331
+ }
332
+
333
+ if (!(await fs.pathExists(fullPath))) {
334
+ return res.status(404).json({ error: 'File not found' });
335
+ }
336
+
337
+ const content = await fs.readFile(fullPath, 'utf8');
338
+ const stats = await fs.stat(fullPath);
339
+
340
+ res.json({
341
+ content,
342
+ path: filePath,
343
+ size: this.formatFileSize(stats.size),
344
+ lastModified: stats.mtime,
345
+ timestamp: new Date().toISOString()
346
+ });
347
+ } catch (error) {
348
+ console.error('Error loading file:', error);
349
+ res.status(500).json({ error: 'Failed to load file' });
350
+ }
351
+ });
352
+
353
+ this.app.get('/api/summary', async (req, res) => {
354
+ try {
355
+ await this.loadSkillsData();
356
+ const personalCount = this.skills.filter(s => s.source === 'Personal').length;
357
+ const projectCount = this.skills.filter(s => s.source === 'Project').length;
358
+ const pluginCount = this.skills.filter(s => s.source === 'Plugin').length;
359
+
360
+ res.json({
361
+ total: this.skills.length,
362
+ personal: personalCount,
363
+ project: projectCount,
364
+ plugin: pluginCount,
365
+ timestamp: new Date().toISOString()
366
+ });
367
+ } catch (error) {
368
+ console.error('Error loading summary:', error);
369
+ res.status(500).json({ error: 'Failed to load summary' });
370
+ }
371
+ });
372
+
373
+ // Main route
374
+ this.app.get('/', (req, res) => {
375
+ res.sendFile(path.join(__dirname, 'skill-dashboard-web', 'index.html'));
376
+ });
377
+ }
378
+
379
+ async startServer() {
380
+ return new Promise((resolve) => {
381
+ this.httpServer = this.app.listen(this.port, async () => {
382
+ console.log(chalk.green(`šŸŽÆ Skills dashboard started at http://localhost:${this.port}`));
383
+ resolve();
384
+ });
385
+ });
386
+ }
387
+
388
+ async openBrowser() {
389
+ const url = `http://localhost:${this.port}`;
390
+ console.log(chalk.blue('🌐 Opening browser to Skills Dashboard...'));
391
+
392
+ try {
393
+ await open(url);
394
+ } catch (error) {
395
+ console.log(chalk.yellow('Could not open browser automatically. Please visit:'));
396
+ console.log(chalk.cyan(url));
397
+ }
398
+ }
399
+
400
+ stop() {
401
+ if (this.httpServer) {
402
+ this.httpServer.close();
403
+ }
404
+ console.log(chalk.yellow('Skills dashboard stopped'));
405
+ }
406
+ }
407
+
408
+ async function runSkillDashboard(options = {}) {
409
+ console.log(chalk.blue('šŸŽÆ Starting Claude Code Skills Dashboard...'));
410
+
411
+ const dashboard = new SkillDashboard(options);
412
+
413
+ try {
414
+ await dashboard.initialize();
415
+ await dashboard.startServer();
416
+ await dashboard.openBrowser();
417
+
418
+ console.log(chalk.green('āœ… Skills dashboard is running!'));
419
+ console.log(chalk.cyan(`🌐 Access at: http://localhost:${dashboard.port}`));
420
+ console.log(chalk.gray('Press Ctrl+C to stop the server'));
421
+
422
+ // Handle graceful shutdown
423
+ process.on('SIGINT', () => {
424
+ console.log(chalk.yellow('\nšŸ›‘ Shutting down skills dashboard...'));
425
+ dashboard.stop();
426
+ process.exit(0);
427
+ });
428
+
429
+ // Keep the process running
430
+ await new Promise(() => {});
431
+
432
+ } catch (error) {
433
+ console.error(chalk.red('āŒ Failed to start skills dashboard:'), error.message);
434
+ process.exit(1);
435
+ }
436
+ }
437
+
438
+ module.exports = {
439
+ runSkillDashboard,
440
+ SkillDashboard
441
+ };