about summary refs log tree commit diff stats
path: root/tree-sitter/dsk/dsk-cli/src/commands/dev.ts
blob: 92b2867dc7d785e9b4bb39e03448384420917081 (plain) (blame)
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
/**
 * Dev Command - Watch grammar.js and rebuild/test on change
 */

import { Command } from 'commander';
import chokidar from 'chokidar';
import chalk from 'chalk';
import { existsSync } from 'fs';
import { execa } from 'execa';

/**
 * Create the dev command
 */
export function createDevCommand(): Command {
  const devCommand = new Command('dev');

  devCommand
    .description('Watch grammar.js and run build + test on changes')
    .option('-v, --verbose', 'Show detailed build output')
    .option('--quiet', 'Suppress non-error logs')
    .option('--debounce <ms>', 'Debounce delay in milliseconds', '150')
    .action(async (options) => {
      if (!existsSync('grammar.js')) {
        console.error(chalk.red('❌ No grammar.js found. Are you in a DSL project directory?'));
        process.exit(1);
      }

      const verbose: boolean = Boolean(options.verbose);
      const quiet: boolean = Boolean(options.quiet);
      const debounceMs: number = Number.parseInt(options.debounce, 10) || 150;

      if (!quiet) {
        console.log(chalk.blue('👀 Watching grammar.js for changes...'));
      }

      // Initial build + test
      await runBuildAndTest({ verbose, quiet });

      // Watcher with debounced rebuilds
      const watcher = chokidar.watch('grammar.js', { ignoreInitial: true });
      let isRunning = false;
      let rerunRequested = false;
      let debounceTimer: NodeJS.Timeout | null = null;

      const runOnce = async () => {
        if (isRunning) {
          rerunRequested = true;
          return;
        }
        isRunning = true;
        if (!quiet) {
          console.log(chalk.yellow('↻ Change detected. Rebuilding...'));
        }
        try {
          await runBuildAndTest({ verbose, quiet });
          if (!quiet) {
            console.log(chalk.green('✅ Rebuild and tests completed.'));
          }
        } catch (e) {
          // Errors already printed by build/test
        } finally {
          isRunning = false;
          if (rerunRequested) {
            rerunRequested = false;
            runOnce();
          }
        }
      };

      const debouncedTrigger = () => {
        if (debounceTimer) clearTimeout(debounceTimer);
        debounceTimer = setTimeout(runOnce, debounceMs);
      };

      watcher.on('change', debouncedTrigger);
    });

  return devCommand;
}

async function runBuildAndTest(opts: { verbose?: boolean; quiet?: boolean }): Promise<void> {
  const { verbose, quiet } = opts;
  if (!quiet) {
    console.log(chalk.blue('🏗️  Building...'));
  }
  try {
    const buildArgs = ['build', ...(verbose ? ['--verbose'] : [])];
    await execa('dsk', buildArgs, { stdio: 'inherit' });
  } catch (error: any) {
    console.error(chalk.red('❌ Build failed. Fix errors and save again.'));
    throw error;
  }

  if (!quiet) {
    console.log(chalk.blue('🧪 Testing...'));
  }
  try {
    await execa('dsk', ['test'], { stdio: 'inherit' });
  } catch (error: any) {
    console.error(chalk.red('❌ Tests failed. Fix tests and save again.'));
    throw error;
  }
}