dev-skill.ts
101 lines · scripts/dev-skill.ts at 3f79d81
1#!/usr/bin/env -S bun --no-env-file --config=/dev/null
2/**
3 * dev:skill — Watch mode for SKILL.md template development.
4 *
5 * Watches .tmpl files, regenerates SKILL.md files on change,
6 * validates all $B commands immediately.
7 */
9import { validateSkill } from '../test/helpers/skill-parser';
10import { discoverTemplates } from './discover-skills';
11import { execSync } from 'child_process';
12import * as fs from 'fs';
13import * as path from 'path';
15const ROOT = path.resolve(import.meta.dir, '..');
17const TEMPLATES = discoverTemplates(ROOT).map(t => ({
18 tmpl: path.join(ROOT, t.tmpl),
19 output: t.output,
20}));
22function regenerateAndValidate() {
23 // Regenerate
24 try {
25 execSync('bun run scripts/gen-skill-docs.ts', { cwd: ROOT, stdio: 'pipe' });
26 } catch (err: any) {
27 console.log(` [gen] ERROR: ${err.stderr?.toString().trim() || err.message}`);
28 return;
29 }
31 // Validate each generated file
32 for (const { output } of TEMPLATES) {
33 const fullPath = path.join(ROOT, output);
34 if (!fs.existsSync(fullPath)) continue;
36 const result = validateSkill(fullPath);
37 const totalValid = result.valid.length;
38 const totalInvalid = result.invalid.length;
39 const totalSnapErrors = result.snapshotFlagErrors.length;
41 if (totalInvalid > 0 || totalSnapErrors > 0) {
42 console.log(` [check] \u274c ${output} (${totalValid} valid)`);
43 for (const inv of result.invalid) {
44 console.log(` Unknown command: '${inv.command}' at line ${inv.line}`);
45 }
46 for (const se of result.snapshotFlagErrors) {
47 console.log(` ${se.error} at line ${se.command.line}`);
48 }
49 } else {
50 console.log(` [check] \u2705 ${output} — ${totalValid} commands, all valid`);
51 }
52 }
54 // Dev workspace render isolation: the default in-place regen above keeps the
55 // worktree canonical. If bin/dev-setup set up an untracked brain-aware render
56 // (.claude/gstack-rendered), refresh it too so live template edits reflect at
57 // this workspace's runtime. Only runs when the render dir already exists — we
58 // never create it during plain template dev.
59 const RENDER_DIR = path.join(ROOT, '.claude', 'gstack-rendered');
60 if (fs.existsSync(RENDER_DIR)) {
61 try {
62 execSync(
63 `bun run scripts/gen-skill-docs.ts --respect-detection --host claude --out-dir ${JSON.stringify(RENDER_DIR)}`,
64 { cwd: ROOT, stdio: 'pipe' },
65 );
66 console.log(' [render] refreshed .claude/gstack-rendered (brain-aware workspace copy)');
67 } catch (err: any) {
68 console.log(` [render] ERROR: ${err.stderr?.toString().trim() || err.message}`);
69 }
70 }
71}
73// Initial run
74console.log(' [watch] Watching *.md.tmpl files...');
75regenerateAndValidate();
77// Watch for changes
78for (const { tmpl } of TEMPLATES) {
79 if (!fs.existsSync(tmpl)) continue;
80 fs.watch(tmpl, () => {
81 console.log(`\n [watch] ${path.relative(ROOT, tmpl)} changed`);
82 regenerateAndValidate();
83 });
84}
86// Also watch commands.ts and snapshot.ts (source of truth changes)
87const SOURCE_FILES = [
88 path.join(ROOT, 'browse', 'src', 'commands.ts'),
89 path.join(ROOT, 'browse', 'src', 'snapshot.ts'),
90];
92for (const src of SOURCE_FILES) {
93 if (!fs.existsSync(src)) continue;
94 fs.watch(src, () => {
95 console.log(`\n [watch] ${path.relative(ROOT, src)} changed`);
96 regenerateAndValidate();
97 });
98}
100// Keep alive
101console.log(' [watch] Press Ctrl+C to stop\n');