~cytrogen/gstack

gstack/scripts/skill-check.ts -rw-r--r-- 7.1 KiB
9c5f4797 — Cytrogen fork: 频率分级路由 + 触发式描述符重写 2 days ago
                                                                                
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#!/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)`);
  }
}

// ─── Codex Skills ───────────────────────────────────────────

const AGENTS_DIR = path.join(ROOT, '.agents', 'skills');
if (fs.existsSync(AGENTS_DIR)) {
  console.log('\n  Codex Skills (.agents/skills/):');
  const codexDirs = fs.readdirSync(AGENTS_DIR).sort();
  let codexCount = 0;
  let codexMissing = 0;
  for (const dir of codexDirs) {
    const skillMd = path.join(AGENTS_DIR, dir, 'SKILL.md');
    if (fs.existsSync(skillMd)) {
      codexCount++;
      const content = fs.readFileSync(skillMd, 'utf-8');
      // Quick validation: must have frontmatter with name + description only
      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 {
      codexMissing++;
      hasErrors = true;
      console.log(`  \u274c ${dir.padEnd(30)} — SKILL.md missing`);
    }
  }
  console.log(`  Total: ${codexCount} skills, ${codexMissing} missing`);
} else {
  console.log('\n  Codex Skills: .agents/skills/ not found (run: bun run gen:skill-docs --host codex)');
}

// ─── Factory Skills ─────────────────────────────────────────

const FACTORY_DIR = path.join(ROOT, '.factory', 'skills');
if (fs.existsSync(FACTORY_DIR)) {
  console.log('\n  Factory Skills (.factory/skills/):');
  const factoryDirs = fs.readdirSync(FACTORY_DIR).sort();
  let factoryCount = 0;
  let factoryMissing = 0;
  for (const dir of factoryDirs) {
    const skillMd = path.join(FACTORY_DIR, dir, 'SKILL.md');
    if (fs.existsSync(skillMd)) {
      factoryCount++;
      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 {
      factoryMissing++;
      hasErrors = true;
      console.log(`  \u274c ${dir.padEnd(30)} — SKILL.md missing`);
    }
  }
  console.log(`  Total: ${factoryCount} skills, ${factoryMissing} missing`);
} else {
  console.log('\n  Factory Skills: .factory/skills/ not found (run: bun run gen:skill-docs --host factory)');
}

// ─── Freshness ──────────────────────────────────────────────

console.log('\n  Freshness (Claude):');
try {
  execSync('bun run scripts/gen-skill-docs.ts --dry-run', { cwd: ROOT, stdio: 'pipe' });
  console.log('  \u2705 All Claude generated files are fresh');
} catch (err: any) {
  hasErrors = true;
  const output = err.stdout?.toString() || '';
  console.log('  \u274c Claude 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');
}

console.log('\n  Freshness (Codex):');
try {
  execSync('bun run scripts/gen-skill-docs.ts --host codex --dry-run', { cwd: ROOT, stdio: 'pipe' });
  console.log('  \u2705 All Codex generated files are fresh');
} catch (err: any) {
  hasErrors = true;
  const output = err.stdout?.toString() || '';
  console.log('  \u274c Codex 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 --host codex');
}

console.log('\n  Freshness (Factory):');
try {
  execSync('bun run scripts/gen-skill-docs.ts --host factory --dry-run', { cwd: ROOT, stdio: 'pipe' });
  console.log('  \u2705 All Factory generated files are fresh');
} catch (err: any) {
  hasErrors = true;
  const output = err.stdout?.toString() || '';
  console.log('  \u274c Factory 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 --host factory');
}

console.log('');
process.exit(hasErrors ? 1 : 0);