cc-dev-template 0.1.5 → 0.1.7

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,206 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * find-skills.js
5
+ *
6
+ * Discovers all Claude Code skills at user and project levels.
7
+ * Returns JSON with skill locations and basic metadata.
8
+ *
9
+ * Usage: node find-skills.js [--json] [--verbose]
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+
16
+ // Parse arguments
17
+ const args = process.argv.slice(2);
18
+ const jsonOutput = args.includes('--json');
19
+ const verbose = args.includes('--verbose');
20
+
21
+ // Skill locations to scan
22
+ const locations = [
23
+ {
24
+ name: 'user',
25
+ path: path.join(os.homedir(), '.claude', 'skills'),
26
+ description: 'User-level skills (~/.claude/skills/)'
27
+ },
28
+ {
29
+ name: 'project',
30
+ path: path.join(process.cwd(), '.claude', 'skills'),
31
+ description: 'Project-level skills (.claude/skills/)'
32
+ },
33
+ {
34
+ name: 'source',
35
+ path: path.join(process.cwd(), 'src', 'skills'),
36
+ description: 'Source skills (src/skills/)'
37
+ }
38
+ ];
39
+
40
+ /**
41
+ * Parse YAML frontmatter from SKILL.md content
42
+ */
43
+ function parseFrontmatter(content) {
44
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
45
+ if (!match) return null;
46
+
47
+ const frontmatter = {};
48
+ const lines = match[1].split('\n');
49
+
50
+ for (const line of lines) {
51
+ const colonIndex = line.indexOf(':');
52
+ if (colonIndex > 0) {
53
+ const key = line.slice(0, colonIndex).trim();
54
+ let value = line.slice(colonIndex + 1).trim();
55
+
56
+ // Remove quotes if present
57
+ if ((value.startsWith('"') && value.endsWith('"')) ||
58
+ (value.startsWith("'") && value.endsWith("'"))) {
59
+ value = value.slice(1, -1);
60
+ }
61
+
62
+ frontmatter[key] = value;
63
+ }
64
+ }
65
+
66
+ return frontmatter;
67
+ }
68
+
69
+ /**
70
+ * Get basic stats about a skill
71
+ */
72
+ function getSkillStats(skillPath) {
73
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
74
+
75
+ if (!fs.existsSync(skillMdPath)) {
76
+ return null;
77
+ }
78
+
79
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
80
+ const frontmatter = parseFrontmatter(content);
81
+
82
+ // Count words in body (after frontmatter)
83
+ const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
84
+ const body = bodyMatch ? bodyMatch[1] : content;
85
+ const wordCount = body.split(/\s+/).filter(w => w.length > 0).length;
86
+
87
+ // Check for subdirectories
88
+ const hasReferences = fs.existsSync(path.join(skillPath, 'references'));
89
+ const hasScripts = fs.existsSync(path.join(skillPath, 'scripts'));
90
+ const hasAssets = fs.existsSync(path.join(skillPath, 'assets'));
91
+
92
+ return {
93
+ name: frontmatter?.name || path.basename(skillPath),
94
+ description: frontmatter?.description || null,
95
+ wordCount,
96
+ hasReferences,
97
+ hasScripts,
98
+ hasAssets,
99
+ frontmatterValid: !!(frontmatter?.name && frontmatter?.description)
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Scan a location for skills
105
+ */
106
+ function scanLocation(location) {
107
+ const skills = [];
108
+
109
+ if (!fs.existsSync(location.path)) {
110
+ return skills;
111
+ }
112
+
113
+ const entries = fs.readdirSync(location.path, { withFileTypes: true });
114
+
115
+ for (const entry of entries) {
116
+ if (!entry.isDirectory()) continue;
117
+
118
+ const skillPath = path.join(location.path, entry.name);
119
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
120
+
121
+ if (fs.existsSync(skillMdPath)) {
122
+ const stats = getSkillStats(skillPath);
123
+
124
+ skills.push({
125
+ directory: entry.name,
126
+ path: skillPath,
127
+ location: location.name,
128
+ ...stats
129
+ });
130
+ }
131
+ }
132
+
133
+ return skills;
134
+ }
135
+
136
+ // Main execution
137
+ const results = {
138
+ timestamp: new Date().toISOString(),
139
+ cwd: process.cwd(),
140
+ locations: {},
141
+ skills: [],
142
+ summary: {
143
+ total: 0,
144
+ byLocation: {}
145
+ }
146
+ };
147
+
148
+ for (const location of locations) {
149
+ const skills = scanLocation(location);
150
+
151
+ results.locations[location.name] = {
152
+ path: location.path,
153
+ exists: fs.existsSync(location.path),
154
+ count: skills.length
155
+ };
156
+
157
+ results.skills.push(...skills);
158
+ results.summary.byLocation[location.name] = skills.length;
159
+ }
160
+
161
+ results.summary.total = results.skills.length;
162
+
163
+ // Output
164
+ if (jsonOutput) {
165
+ console.log(JSON.stringify(results, null, 2));
166
+ } else {
167
+ console.log('\n=== Claude Code Skills Discovery ===\n');
168
+
169
+ for (const location of locations) {
170
+ const info = results.locations[location.name];
171
+ console.log(`${location.description}`);
172
+ console.log(` Path: ${info.path}`);
173
+ console.log(` Exists: ${info.exists ? 'Yes' : 'No'}`);
174
+ console.log(` Skills found: ${info.count}`);
175
+ console.log();
176
+ }
177
+
178
+ if (results.skills.length === 0) {
179
+ console.log('No skills found.\n');
180
+ } else {
181
+ console.log('=== Skills ===\n');
182
+
183
+ for (const skill of results.skills) {
184
+ console.log(`[${skill.location}] ${skill.name}`);
185
+ console.log(` Path: ${skill.path}`);
186
+
187
+ if (verbose) {
188
+ console.log(` Words: ${skill.wordCount}`);
189
+ console.log(` Valid frontmatter: ${skill.frontmatterValid ? 'Yes' : 'No'}`);
190
+ console.log(` Has references/: ${skill.hasReferences ? 'Yes' : 'No'}`);
191
+ console.log(` Has scripts/: ${skill.hasScripts ? 'Yes' : 'No'}`);
192
+
193
+ if (skill.description) {
194
+ const truncated = skill.description.length > 80
195
+ ? skill.description.slice(0, 80) + '...'
196
+ : skill.description;
197
+ console.log(` Description: ${truncated}`);
198
+ }
199
+ }
200
+
201
+ console.log();
202
+ }
203
+
204
+ console.log(`Total: ${results.summary.total} skill(s)\n`);
205
+ }
206
+ }
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * validate-skill.js
5
+ *
6
+ * Validates a Claude Code skill against best practices.
7
+ * Returns detailed findings with severity levels.
8
+ *
9
+ * Usage: node validate-skill.js <skill-path> [--json] [--fix]
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // Parse arguments
16
+ const args = process.argv.slice(2);
17
+ const skillPath = args.find(a => !a.startsWith('--'));
18
+ const jsonOutput = args.includes('--json');
19
+ const autoFix = args.includes('--fix');
20
+
21
+ if (!skillPath) {
22
+ console.error('Usage: node validate-skill.js <skill-path> [--json] [--fix]');
23
+ process.exit(1);
24
+ }
25
+
26
+ // Resolve skill path
27
+ const resolvedPath = path.resolve(skillPath);
28
+ const skillMdPath = path.join(resolvedPath, 'SKILL.md');
29
+
30
+ if (!fs.existsSync(skillMdPath)) {
31
+ console.error(`Error: SKILL.md not found at ${skillMdPath}`);
32
+ process.exit(1);
33
+ }
34
+
35
+ // Findings storage
36
+ const findings = [];
37
+
38
+ function addFinding(category, severity, message, line = null, suggestion = null) {
39
+ findings.push({ category, severity, message, line, suggestion });
40
+ }
41
+
42
+ /**
43
+ * Parse YAML frontmatter
44
+ */
45
+ function parseFrontmatter(content) {
46
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
47
+ if (!match) return { valid: false, data: null, raw: null };
48
+
49
+ const raw = match[1];
50
+ const data = {};
51
+ const lines = raw.split('\n');
52
+
53
+ for (const line of lines) {
54
+ const colonIndex = line.indexOf(':');
55
+ if (colonIndex > 0) {
56
+ const key = line.slice(0, colonIndex).trim();
57
+ let value = line.slice(colonIndex + 1).trim();
58
+
59
+ // Handle multi-line values (basic support)
60
+ if (value === '' || value === '|' || value === '>') {
61
+ // Skip complex multi-line for now
62
+ continue;
63
+ }
64
+
65
+ // Remove quotes if present
66
+ if ((value.startsWith('"') && value.endsWith('"')) ||
67
+ (value.startsWith("'") && value.endsWith("'"))) {
68
+ value = value.slice(1, -1);
69
+ }
70
+
71
+ data[key] = value;
72
+ }
73
+ }
74
+
75
+ return { valid: true, data, raw };
76
+ }
77
+
78
+ /**
79
+ * Get body content (after frontmatter)
80
+ */
81
+ function getBody(content) {
82
+ const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
83
+ return match ? match[1] : content;
84
+ }
85
+
86
+ // Read skill content
87
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
88
+ const frontmatter = parseFrontmatter(content);
89
+ const body = getBody(content);
90
+ const lines = content.split('\n');
91
+
92
+ // === VALIDATION CHECKS ===
93
+
94
+ // Check 1: Frontmatter exists
95
+ if (!frontmatter.valid) {
96
+ addFinding('STRUCTURE', 'error', 'Missing or invalid YAML frontmatter');
97
+ } else {
98
+ // Check 2: Required fields
99
+ if (!frontmatter.data.name) {
100
+ addFinding('STRUCTURE', 'error', 'Missing required field: name');
101
+ } else {
102
+ // Check name format (hyphen-case)
103
+ const namePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
104
+ if (!namePattern.test(frontmatter.data.name)) {
105
+ addFinding('STRUCTURE', 'error',
106
+ `Invalid name format: "${frontmatter.data.name}" - must be hyphen-case (lowercase letters, numbers, hyphens)`);
107
+ }
108
+
109
+ // Check name matches directory
110
+ const dirName = path.basename(resolvedPath);
111
+ if (frontmatter.data.name !== dirName) {
112
+ addFinding('STRUCTURE', 'warning',
113
+ `Name "${frontmatter.data.name}" doesn't match directory "${dirName}"`);
114
+ }
115
+
116
+ // Check name length
117
+ if (frontmatter.data.name.length > 64) {
118
+ addFinding('STRUCTURE', 'error',
119
+ `Name exceeds 64 characters (${frontmatter.data.name.length})`);
120
+ }
121
+ }
122
+
123
+ if (!frontmatter.data.description) {
124
+ addFinding('STRUCTURE', 'error', 'Missing required field: description');
125
+ } else {
126
+ // Check description length
127
+ if (frontmatter.data.description.length > 1024) {
128
+ addFinding('DESCRIPTION', 'error',
129
+ `Description exceeds 1024 characters (${frontmatter.data.description.length})`);
130
+ }
131
+
132
+ // Check for third person
133
+ const desc = frontmatter.data.description.toLowerCase();
134
+ if (desc.startsWith('use this') || desc.startsWith('use when')) {
135
+ addFinding('DESCRIPTION', 'warning',
136
+ 'Description should use third person ("This skill should be used when...") not second person',
137
+ null,
138
+ 'Start with "This skill should be used when..." or similar third-person phrasing');
139
+ }
140
+
141
+ // Check for trigger phrases (quoted text)
142
+ const hasQuotedPhrases = /"[^"]+"|'[^']+'/.test(frontmatter.data.description);
143
+ if (!hasQuotedPhrases) {
144
+ addFinding('DESCRIPTION', 'warning',
145
+ 'Description lacks quoted trigger phrases',
146
+ null,
147
+ 'Add specific phrases users might say, e.g., "create a skill", "build a new skill"');
148
+ }
149
+
150
+ // Check if description is too detailed (might prevent activation)
151
+ if (frontmatter.data.description.length > 500 && !hasQuotedPhrases) {
152
+ addFinding('DESCRIPTION', 'info',
153
+ 'Long description without trigger phrases may reduce activation reliability');
154
+ }
155
+ }
156
+ }
157
+
158
+ // Check 3: Second person violations
159
+ const secondPersonPatterns = [
160
+ /\byou should\b/gi,
161
+ /\byou can\b/gi,
162
+ /\byou need\b/gi,
163
+ /\byou might\b/gi,
164
+ /\byou will\b/gi,
165
+ /\byou must\b/gi,
166
+ /\byour\b/gi
167
+ ];
168
+
169
+ let secondPersonCount = 0;
170
+ const secondPersonExamples = [];
171
+
172
+ for (let i = 0; i < lines.length; i++) {
173
+ const line = lines[i];
174
+ // Skip frontmatter
175
+ if (i < 3) continue;
176
+
177
+ for (const pattern of secondPersonPatterns) {
178
+ const matches = line.match(pattern);
179
+ if (matches) {
180
+ secondPersonCount += matches.length;
181
+ if (secondPersonExamples.length < 3) {
182
+ secondPersonExamples.push({ line: i + 1, text: line.trim().slice(0, 60) });
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ if (secondPersonCount > 0) {
189
+ addFinding('STYLE', 'error',
190
+ `Found ${secondPersonCount} instance(s) of second person language`,
191
+ secondPersonExamples[0]?.line,
192
+ 'Convert to imperative form: "You should parse" → "Parse"');
193
+ }
194
+
195
+ // Check 4: Negative framing
196
+ const negativePatterns = [
197
+ /\bdon't\b/gi,
198
+ /\bdo not\b/gi,
199
+ /\bnever\b/gi,
200
+ /\bavoid\b/gi,
201
+ /\bshould not\b/gi,
202
+ /\bshouldn't\b/gi,
203
+ /\bcan't\b/gi,
204
+ /\bcannot\b/gi
205
+ ];
206
+
207
+ let negativeCount = 0;
208
+ const negativeExamples = [];
209
+
210
+ for (let i = 0; i < lines.length; i++) {
211
+ const line = lines[i];
212
+ if (i < 3) continue;
213
+
214
+ for (const pattern of negativePatterns) {
215
+ const matches = line.match(pattern);
216
+ if (matches) {
217
+ negativeCount += matches.length;
218
+ if (negativeExamples.length < 3) {
219
+ negativeExamples.push({ line: i + 1, text: line.trim().slice(0, 60) });
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ if (negativeCount > 3) {
226
+ addFinding('STYLE', 'warning',
227
+ `Found ${negativeCount} instance(s) of negative framing`,
228
+ negativeExamples[0]?.line,
229
+ 'Use positive framing: "Don\'t do X" → "Do Y instead"');
230
+ }
231
+
232
+ // Check 5: Body size
233
+ const wordCount = body.split(/\s+/).filter(w => w.length > 0).length;
234
+ const lineCount = body.split('\n').length;
235
+
236
+ if (wordCount > 5000) {
237
+ addFinding('SIZE', 'error',
238
+ `SKILL.md body is ${wordCount} words - strongly exceeds 2,000 word recommendation`,
239
+ null,
240
+ 'Move detailed content to references/ directory');
241
+ } else if (wordCount > 3500) {
242
+ addFinding('SIZE', 'warning',
243
+ `SKILL.md body is ${wordCount} words - exceeds 2,000 word recommendation`,
244
+ null,
245
+ 'Consider moving detailed content to references/ directory');
246
+ } else if (wordCount > 2000) {
247
+ addFinding('SIZE', 'info',
248
+ `SKILL.md body is ${wordCount} words - at upper limit of recommendation`);
249
+ }
250
+
251
+ // Check 6: Progressive disclosure
252
+ const hasReferences = fs.existsSync(path.join(resolvedPath, 'references'));
253
+ const hasScripts = fs.existsSync(path.join(resolvedPath, 'scripts'));
254
+
255
+ if (wordCount > 2000 && !hasReferences) {
256
+ addFinding('DISCLOSURE', 'warning',
257
+ 'Large SKILL.md without references/ directory - not using progressive disclosure',
258
+ null,
259
+ 'Create references/ and move detailed content there');
260
+ }
261
+
262
+ // Check 7: Reference integrity
263
+ const referencePaths = body.match(/`references\/[^`]+`|references\/[\w.-]+/g) || [];
264
+ const scriptPaths = body.match(/`scripts\/[^`]+`|scripts\/[\w.-]+/g) || [];
265
+
266
+ for (const ref of referencePaths) {
267
+ const cleanPath = ref.replace(/`/g, '');
268
+ const fullPath = path.join(resolvedPath, cleanPath);
269
+
270
+ if (!fs.existsSync(fullPath)) {
271
+ addFinding('BROKEN', 'error',
272
+ `Referenced file doesn't exist: ${cleanPath}`);
273
+ }
274
+ }
275
+
276
+ for (const ref of scriptPaths) {
277
+ const cleanPath = ref.replace(/`/g, '');
278
+ const fullPath = path.join(resolvedPath, cleanPath);
279
+
280
+ if (!fs.existsSync(fullPath)) {
281
+ addFinding('BROKEN', 'error',
282
+ `Referenced script doesn't exist: ${cleanPath}`);
283
+ }
284
+ }
285
+
286
+ // Check 8: Unreferenced resources
287
+ if (hasReferences) {
288
+ const refDir = path.join(resolvedPath, 'references');
289
+ const refFiles = fs.readdirSync(refDir);
290
+
291
+ for (const file of refFiles) {
292
+ const isReferenced = body.includes(`references/${file}`) ||
293
+ body.includes(`references/${path.basename(file, '.md')}`);
294
+ if (!isReferenced && file.endsWith('.md')) {
295
+ addFinding('DISCLOSURE', 'info',
296
+ `File references/${file} exists but may not be referenced in SKILL.md`);
297
+ }
298
+ }
299
+ }
300
+
301
+ if (hasScripts) {
302
+ const scriptDir = path.join(resolvedPath, 'scripts');
303
+ const scriptFiles = fs.readdirSync(scriptDir);
304
+
305
+ for (const file of scriptFiles) {
306
+ const isReferenced = body.includes(`scripts/${file}`);
307
+ if (!isReferenced) {
308
+ addFinding('DISCLOSURE', 'info',
309
+ `File scripts/${file} exists but may not be referenced in SKILL.md`);
310
+ }
311
+ }
312
+ }
313
+
314
+ // === OUTPUT ===
315
+
316
+ const result = {
317
+ skill: {
318
+ path: resolvedPath,
319
+ name: frontmatter.data?.name || path.basename(resolvedPath),
320
+ wordCount,
321
+ lineCount,
322
+ hasReferences,
323
+ hasScripts
324
+ },
325
+ findings,
326
+ summary: {
327
+ errors: findings.filter(f => f.severity === 'error').length,
328
+ warnings: findings.filter(f => f.severity === 'warning').length,
329
+ info: findings.filter(f => f.severity === 'info').length,
330
+ passed: findings.filter(f => f.severity === 'error').length === 0
331
+ }
332
+ };
333
+
334
+ if (jsonOutput) {
335
+ console.log(JSON.stringify(result, null, 2));
336
+ } else {
337
+ console.log(`\n=== Skill Validation: ${result.skill.name} ===\n`);
338
+ console.log(`Path: ${result.skill.path}`);
339
+ console.log(`Words: ${result.skill.wordCount}`);
340
+ console.log(`Has references/: ${result.skill.hasReferences ? 'Yes' : 'No'}`);
341
+ console.log(`Has scripts/: ${result.skill.hasScripts ? 'Yes' : 'No'}`);
342
+ console.log();
343
+
344
+ if (findings.length === 0) {
345
+ console.log('✓ All checks passed!\n');
346
+ } else {
347
+ // Group by severity
348
+ const errors = findings.filter(f => f.severity === 'error');
349
+ const warnings = findings.filter(f => f.severity === 'warning');
350
+ const infos = findings.filter(f => f.severity === 'info');
351
+
352
+ if (errors.length > 0) {
353
+ console.log('ERRORS:');
354
+ for (const f of errors) {
355
+ console.log(` ✗ [${f.category}] ${f.message}`);
356
+ if (f.line) console.log(` Line ${f.line}`);
357
+ if (f.suggestion) console.log(` Suggestion: ${f.suggestion}`);
358
+ }
359
+ console.log();
360
+ }
361
+
362
+ if (warnings.length > 0) {
363
+ console.log('WARNINGS:');
364
+ for (const f of warnings) {
365
+ console.log(` ! [${f.category}] ${f.message}`);
366
+ if (f.line) console.log(` Line ${f.line}`);
367
+ if (f.suggestion) console.log(` Suggestion: ${f.suggestion}`);
368
+ }
369
+ console.log();
370
+ }
371
+
372
+ if (infos.length > 0) {
373
+ console.log('INFO:');
374
+ for (const f of infos) {
375
+ console.log(` i [${f.category}] ${f.message}`);
376
+ }
377
+ console.log();
378
+ }
379
+
380
+ console.log(`Summary: ${result.summary.errors} error(s), ${result.summary.warnings} warning(s), ${result.summary.info} info(s)`);
381
+ console.log(`Status: ${result.summary.passed ? 'PASSED' : 'FAILED'}\n`);
382
+ }
383
+ }
384
+
385
+ // Exit with error code if validation failed
386
+ process.exit(result.summary.passed ? 0 : 1);
@@ -23,7 +23,7 @@ Complete each step in order before proceeding to the next.
23
23
  Run the plan status script to find existing plans:
24
24
 
25
25
  ```bash
26
- node ~/.claude/scripts/plan-status.js
26
+ node ~/.claude/skills/orchestration/scripts/plan-status.js
27
27
  ```
28
28
 
29
29
  This returns a list of plans with their current status (draft, approved, in_progress, completed).
@@ -10,7 +10,7 @@
10
10
  * file scanning.
11
11
  *
12
12
  * Usage:
13
- * node ~/.claude/scripts/plan-status.js
13
+ * node ~/.claude/skills/orchestration/scripts/plan-status.js
14
14
  *
15
15
  * Output:
16
16
  * JSON object to stdout: