forked from garrytan/gstack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathskill-check.ts
More file actions
139 lines (119 loc) · 5.15 KB
/
Copy pathskill-check.ts
File metadata and controls
139 lines (119 loc) · 5.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env bun
/**
* skill:check — Health summary for all SKILL.md files.
*
* Reports:
* - Command validation (valid/invalid/snapshot errors)
* - Template coverage (which SKILL.md files have .tmpl sources)
* - Freshness check (generated files match committed files)
*/
import { validateSkill } from '../test/helpers/skill-parser';
import { discoverTemplates, discoverSkillFiles } from './discover-skills';
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
const ROOT = path.resolve(import.meta.dir, '..');
// Find all SKILL.md files (dynamic discovery — no hardcoded list)
const SKILL_FILES = discoverSkillFiles(ROOT);
let hasErrors = false;
// ─── Skills ─────────────────────────────────────────────────
console.log(' Skills:');
for (const file of SKILL_FILES) {
const fullPath = path.join(ROOT, file);
const result = validateSkill(fullPath);
if (result.warnings.length > 0) {
console.log(` \u26a0\ufe0f ${file.padEnd(30)} — ${result.warnings.join(', ')}`);
continue;
}
const totalValid = result.valid.length;
const totalInvalid = result.invalid.length;
const totalSnapErrors = result.snapshotFlagErrors.length;
if (totalInvalid > 0 || totalSnapErrors > 0) {
hasErrors = true;
console.log(` \u274c ${file.padEnd(30)} — ${totalValid} valid, ${totalInvalid} invalid, ${totalSnapErrors} snapshot errors`);
for (const inv of result.invalid) {
console.log(` line ${inv.line}: unknown command '${inv.command}'`);
}
for (const se of result.snapshotFlagErrors) {
console.log(` line ${se.command.line}: ${se.error}`);
}
} else {
console.log(` \u2705 ${file.padEnd(30)} — ${totalValid} commands, all valid`);
}
}
// ─── Templates ──────────────────────────────────────────────
console.log('\n Templates:');
const TEMPLATES = discoverTemplates(ROOT);
for (const { tmpl, output } of TEMPLATES) {
const tmplPath = path.join(ROOT, tmpl);
const outPath = path.join(ROOT, output);
if (!fs.existsSync(tmplPath)) {
console.log(` \u26a0\ufe0f ${output.padEnd(30)} — no template`);
continue;
}
if (!fs.existsSync(outPath)) {
hasErrors = true;
console.log(` \u274c ${output.padEnd(30)} — generated file missing! Run: bun run gen:skill-docs`);
continue;
}
console.log(` \u2705 ${tmpl.padEnd(30)} \u2192 ${output}`);
}
// Skills without templates
for (const file of SKILL_FILES) {
const tmplPath = path.join(ROOT, file + '.tmpl');
if (!fs.existsSync(tmplPath) && !TEMPLATES.some(t => t.output === file)) {
console.log(` \u26a0\ufe0f ${file.padEnd(30)} — no template (OK if no $B commands)`);
}
}
// ─── External Host Skills (config-driven) ───────────────────
import { getExternalHosts } from '../hosts/index';
for (const hostConfig of getExternalHosts()) {
const hostDir = path.join(ROOT, hostConfig.hostSubdir, 'skills');
if (fs.existsSync(hostDir)) {
console.log(`\n ${hostConfig.displayName} Skills (${hostConfig.hostSubdir}/skills/):`);
const dirs = fs.readdirSync(hostDir).sort();
let count = 0;
let missing = 0;
for (const dir of dirs) {
const skillMd = path.join(hostDir, dir, 'SKILL.md');
if (fs.existsSync(skillMd)) {
count++;
const content = fs.readFileSync(skillMd, 'utf-8');
const hasClaude = content.includes('.claude/skills');
if (hasClaude) {
hasErrors = true;
console.log(` \u274c ${dir.padEnd(30)} — contains .claude/skills reference`);
} else {
console.log(` \u2705 ${dir.padEnd(30)} — OK`);
}
} else {
missing++;
hasErrors = true;
console.log(` \u274c ${dir.padEnd(30)} — SKILL.md missing`);
}
}
console.log(` Total: ${count} skills, ${missing} missing`);
} else {
console.log(`\n ${hostConfig.displayName} Skills: ${hostConfig.hostSubdir}/skills/ not found (run: bun run gen:skill-docs --host ${hostConfig.name})`);
}
}
// ─── Freshness (config-driven) ──────────────────────────────
import { ALL_HOST_CONFIGS } from '../hosts/index';
for (const hostConfig of ALL_HOST_CONFIGS) {
const hostFlag = hostConfig.name === 'claude' ? '' : ` --host ${hostConfig.name}`;
console.log(`\n Freshness (${hostConfig.displayName}):`);
try {
execSync(`bun run scripts/gen-skill-docs.ts${hostFlag} --dry-run`, { cwd: ROOT, stdio: 'pipe' });
console.log(` \u2705 All ${hostConfig.displayName} generated files are fresh`);
} catch (err: any) {
hasErrors = true;
const output = err.stdout?.toString() || '';
console.log(` \u274c ${hostConfig.displayName} generated files are stale:`);
for (const line of output.split('\n').filter((l: string) => l.startsWith('STALE'))) {
console.log(` ${line}`);
}
console.log(` Run: bun run gen:skill-docs${hostFlag}`);
}
}
console.log('');
process.exit(hasErrors ? 1 : 0);