@nclamvn/vibecode-cli 1.6.0 → 1.7.0

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.
Files changed (63) hide show
  1. package/bin/vibecode.js +48 -1
  2. package/docs-site/README.md +41 -0
  3. package/docs-site/blog/2019-05-28-first-blog-post.md +12 -0
  4. package/docs-site/blog/2019-05-29-long-blog-post.md +44 -0
  5. package/docs-site/blog/2021-08-01-mdx-blog-post.mdx +24 -0
  6. package/docs-site/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  7. package/docs-site/blog/2021-08-26-welcome/index.md +29 -0
  8. package/docs-site/blog/authors.yml +25 -0
  9. package/docs-site/blog/tags.yml +19 -0
  10. package/docs-site/docs/commands/agent.md +162 -0
  11. package/docs-site/docs/commands/assist.md +71 -0
  12. package/docs-site/docs/commands/build.md +53 -0
  13. package/docs-site/docs/commands/config.md +30 -0
  14. package/docs-site/docs/commands/debug.md +173 -0
  15. package/docs-site/docs/commands/doctor.md +34 -0
  16. package/docs-site/docs/commands/go.md +128 -0
  17. package/docs-site/docs/commands/index.md +79 -0
  18. package/docs-site/docs/commands/init.md +42 -0
  19. package/docs-site/docs/commands/learn.md +82 -0
  20. package/docs-site/docs/commands/lock.md +33 -0
  21. package/docs-site/docs/commands/plan.md +29 -0
  22. package/docs-site/docs/commands/review.md +31 -0
  23. package/docs-site/docs/commands/snapshot.md +34 -0
  24. package/docs-site/docs/commands/start.md +32 -0
  25. package/docs-site/docs/commands/status.md +37 -0
  26. package/docs-site/docs/commands/undo.md +83 -0
  27. package/docs-site/docs/configuration.md +72 -0
  28. package/docs-site/docs/faq.md +83 -0
  29. package/docs-site/docs/getting-started.md +119 -0
  30. package/docs-site/docs/guides/agent-mode.md +94 -0
  31. package/docs-site/docs/guides/debug-mode.md +83 -0
  32. package/docs-site/docs/guides/magic-mode.md +107 -0
  33. package/docs-site/docs/installation.md +98 -0
  34. package/docs-site/docs/intro.md +67 -0
  35. package/docs-site/docusaurus.config.ts +141 -0
  36. package/docs-site/package-lock.json +18039 -0
  37. package/docs-site/package.json +48 -0
  38. package/docs-site/sidebars.ts +70 -0
  39. package/docs-site/src/components/HomepageFeatures/index.tsx +72 -0
  40. package/docs-site/src/components/HomepageFeatures/styles.module.css +16 -0
  41. package/docs-site/src/css/custom.css +30 -0
  42. package/docs-site/src/pages/index.module.css +23 -0
  43. package/docs-site/src/pages/index.tsx +44 -0
  44. package/docs-site/src/pages/markdown-page.md +7 -0
  45. package/docs-site/src/theme/Footer/index.tsx +127 -0
  46. package/docs-site/src/theme/Footer/styles.module.css +285 -0
  47. package/docs-site/static/.nojekyll +0 -0
  48. package/docs-site/static/img/docusaurus-social-card.jpg +0 -0
  49. package/docs-site/static/img/docusaurus.png +0 -0
  50. package/docs-site/static/img/favicon.ico +0 -0
  51. package/docs-site/static/img/logo.svg +1 -0
  52. package/docs-site/static/img/undraw_docusaurus_mountain.svg +171 -0
  53. package/docs-site/static/img/undraw_docusaurus_react.svg +170 -0
  54. package/docs-site/static/img/undraw_docusaurus_tree.svg +40 -0
  55. package/docs-site/tsconfig.json +8 -0
  56. package/package.json +2 -1
  57. package/src/commands/debug.js +109 -1
  58. package/src/commands/git.js +923 -0
  59. package/src/commands/shell.js +486 -0
  60. package/src/commands/watch.js +556 -0
  61. package/src/debug/image-analyzer.js +304 -0
  62. package/src/index.js +19 -0
  63. package/src/utils/image.js +222 -0
@@ -0,0 +1,556 @@
1
+ /**
2
+ * File Watcher for Vibecode CLI
3
+ * Real-time monitoring with auto-test, lint, build on file changes
4
+ */
5
+
6
+ import chokidar from 'chokidar';
7
+ import chalk from 'chalk';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+ import path from 'path';
11
+ import fs from 'fs/promises';
12
+ import readline from 'readline';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ /**
17
+ * Main watch command handler
18
+ */
19
+ export async function watchCommand(options) {
20
+ const cwd = process.cwd();
21
+ const watchDirs = options.dir
22
+ ? [options.dir]
23
+ : await getDefaultWatchDirs(cwd);
24
+
25
+ // Determine what to run
26
+ const checks = {
27
+ test: options.test || options.all,
28
+ lint: options.lint || options.all,
29
+ build: options.build,
30
+ typecheck: options.typecheck || options.all
31
+ };
32
+
33
+ // If no specific checks, default to test
34
+ if (!checks.test && !checks.lint && !checks.build && !checks.typecheck) {
35
+ checks.test = true;
36
+ }
37
+
38
+ const state = {
39
+ events: 0,
40
+ lastRun: null,
41
+ running: false,
42
+ results: {
43
+ test: null,
44
+ lint: null,
45
+ build: null,
46
+ typecheck: null
47
+ },
48
+ errors: [],
49
+ lastEvent: null
50
+ };
51
+
52
+ // Setup console UI
53
+ console.clear();
54
+ renderUI(state, watchDirs, checks);
55
+
56
+ // Setup watcher
57
+ const ignored = [
58
+ '**/node_modules/**',
59
+ '**/.git/**',
60
+ '**/.next/**',
61
+ '**/dist/**',
62
+ '**/build/**',
63
+ '**/.vibecode/**',
64
+ '**/coverage/**',
65
+ '**/*.log',
66
+ '**/.DS_Store'
67
+ ];
68
+
69
+ const watcher = chokidar.watch(watchDirs, {
70
+ ignored,
71
+ persistent: true,
72
+ ignoreInitial: true,
73
+ awaitWriteFinish: {
74
+ stabilityThreshold: 300,
75
+ pollInterval: 100
76
+ }
77
+ });
78
+
79
+ // Debounce function
80
+ let debounceTimer = null;
81
+ const debounce = (fn, delay = 500) => {
82
+ return (...args) => {
83
+ if (debounceTimer) clearTimeout(debounceTimer);
84
+ debounceTimer = setTimeout(() => fn(...args), delay);
85
+ };
86
+ };
87
+
88
+ // Run checks
89
+ const runChecks = debounce(async (filePath, eventType) => {
90
+ if (state.running) return;
91
+
92
+ state.running = true;
93
+ state.events++;
94
+ state.lastRun = new Date();
95
+ state.errors = [];
96
+
97
+ const fileName = path.relative(cwd, filePath);
98
+ state.lastEvent = `[${formatTime()}] ${eventType}: ${fileName}`;
99
+
100
+ // Update UI
101
+ renderUI(state, watchDirs, checks);
102
+
103
+ // Run enabled checks
104
+ if (checks.typecheck) {
105
+ state.results.typecheck = await runTypecheck(cwd, state);
106
+ renderUI(state, watchDirs, checks);
107
+ }
108
+
109
+ if (checks.lint) {
110
+ state.results.lint = await runLint(cwd, state);
111
+ renderUI(state, watchDirs, checks);
112
+ }
113
+
114
+ if (checks.test) {
115
+ state.results.test = await runTest(cwd, state);
116
+ renderUI(state, watchDirs, checks);
117
+ }
118
+
119
+ if (checks.build) {
120
+ state.results.build = await runBuild(cwd, state);
121
+ renderUI(state, watchDirs, checks);
122
+ }
123
+
124
+ // Notify if enabled
125
+ if (options.notify && state.errors.length > 0) {
126
+ notify('Vibecode Watch', `${state.errors.length} errors found`);
127
+ } else if (options.notify && state.errors.length === 0) {
128
+ notify('Vibecode Watch', 'All checks passed!');
129
+ }
130
+
131
+ state.running = false;
132
+ renderUI(state, watchDirs, checks);
133
+ }, 500);
134
+
135
+ // Watch events
136
+ watcher
137
+ .on('change', (filePath) => runChecks(filePath, 'changed'))
138
+ .on('add', (filePath) => runChecks(filePath, 'added'))
139
+ .on('unlink', (filePath) => runChecks(filePath, 'deleted'));
140
+
141
+ // Keyboard shortcuts
142
+ setupKeyboardShortcuts(state, checks, cwd, watchDirs, watcher, runChecks);
143
+
144
+ // Initial run
145
+ if (options.immediate) {
146
+ setTimeout(() => runChecks(watchDirs[0], 'initial'), 100);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Get default directories to watch
152
+ */
153
+ async function getDefaultWatchDirs(cwd) {
154
+ const dirs = [];
155
+ const candidates = ['src', 'lib', 'app', 'pages', 'components', 'test', 'tests', '__tests__'];
156
+
157
+ for (const dir of candidates) {
158
+ try {
159
+ const stat = await fs.stat(path.join(cwd, dir));
160
+ if (stat.isDirectory()) {
161
+ dirs.push(dir);
162
+ }
163
+ } catch {
164
+ // Directory doesn't exist
165
+ }
166
+ }
167
+
168
+ // If no common dirs found, watch current directory
169
+ if (dirs.length === 0) {
170
+ dirs.push('.');
171
+ }
172
+
173
+ return dirs;
174
+ }
175
+
176
+ /**
177
+ * Run tests
178
+ */
179
+ async function runTest(cwd, state) {
180
+ try {
181
+ const pkgPath = path.join(cwd, 'package.json');
182
+ let pkg;
183
+ try {
184
+ pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
185
+ } catch {
186
+ return { status: 'skip', message: 'No package.json' };
187
+ }
188
+
189
+ if (!pkg.scripts?.test) {
190
+ return { status: 'skip', message: 'No test script' };
191
+ }
192
+
193
+ const { stdout, stderr } = await execAsync('npm test 2>&1', { cwd, timeout: 120000 });
194
+ const output = stdout + stderr;
195
+
196
+ // Parse test results - try different patterns
197
+ let passed = output.match(/(\d+)\s*(?:passing|passed|pass)/i);
198
+ let failed = output.match(/(\d+)\s*(?:failing|failed|fail)/i);
199
+
200
+ // Jest pattern
201
+ if (!passed) {
202
+ const jestMatch = output.match(/Tests:\s*(\d+)\s*passed/i);
203
+ if (jestMatch) passed = jestMatch;
204
+ }
205
+ if (!failed) {
206
+ const jestFail = output.match(/Tests:\s*(\d+)\s*failed/i);
207
+ if (jestFail) failed = jestFail;
208
+ }
209
+
210
+ if (failed && parseInt(failed[1]) > 0) {
211
+ state.errors.push({ type: 'test', message: `${failed[1]} tests failed` });
212
+ return { status: 'fail', passed: passed?.[1] || 0, failed: failed[1] };
213
+ }
214
+
215
+ return { status: 'pass', passed: passed?.[1] || '?', failed: 0 };
216
+ } catch (error) {
217
+ const output = error.stdout || error.message;
218
+ const failed = output.match(/(\d+)\s*(?:failing|failed|fail)/i);
219
+
220
+ if (failed) {
221
+ state.errors.push({ type: 'test', message: `${failed[1]} tests failed` });
222
+ return { status: 'fail', passed: 0, failed: failed[1] };
223
+ }
224
+
225
+ state.errors.push({ type: 'test', message: 'Tests failed' });
226
+ return { status: 'error', message: 'Tests failed' };
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Run lint
232
+ */
233
+ async function runLint(cwd, state) {
234
+ try {
235
+ const pkgPath = path.join(cwd, 'package.json');
236
+ let pkg;
237
+ try {
238
+ pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
239
+ } catch {
240
+ return { status: 'skip', message: 'No package.json' };
241
+ }
242
+
243
+ if (!pkg.scripts?.lint) {
244
+ return { status: 'skip', message: 'No lint script' };
245
+ }
246
+
247
+ await execAsync('npm run lint 2>&1', { cwd, timeout: 60000 });
248
+ return { status: 'pass' };
249
+ } catch (error) {
250
+ const output = error.stdout || error.message || '';
251
+ const warnings = (output.match(/warning/gi) || []).length;
252
+ const errors = (output.match(/error(?!\s*TS)/gi) || []).length;
253
+
254
+ if (errors > 0) {
255
+ state.errors.push({ type: 'lint', message: `${errors} lint errors` });
256
+ return { status: 'fail', errors, warnings };
257
+ }
258
+
259
+ if (warnings > 0) {
260
+ return { status: 'warn', warnings };
261
+ }
262
+
263
+ state.errors.push({ type: 'lint', message: 'Lint failed' });
264
+ return { status: 'fail', message: 'Lint failed' };
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Run build
270
+ */
271
+ async function runBuild(cwd, state) {
272
+ try {
273
+ const pkgPath = path.join(cwd, 'package.json');
274
+ let pkg;
275
+ try {
276
+ pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
277
+ } catch {
278
+ return { status: 'skip', message: 'No package.json' };
279
+ }
280
+
281
+ if (!pkg.scripts?.build) {
282
+ return { status: 'skip', message: 'No build script' };
283
+ }
284
+
285
+ await execAsync('npm run build 2>&1', { cwd, timeout: 180000 });
286
+ return { status: 'pass' };
287
+ } catch (error) {
288
+ state.errors.push({ type: 'build', message: 'Build failed' });
289
+ return { status: 'fail', message: 'Build failed' };
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Run TypeScript type checking
295
+ */
296
+ async function runTypecheck(cwd, state) {
297
+ try {
298
+ // Check if TypeScript project
299
+ try {
300
+ await fs.stat(path.join(cwd, 'tsconfig.json'));
301
+ } catch {
302
+ return { status: 'skip', message: 'No tsconfig.json' };
303
+ }
304
+
305
+ await execAsync('npx tsc --noEmit 2>&1', { cwd, timeout: 120000 });
306
+ return { status: 'pass' };
307
+ } catch (error) {
308
+ const output = error.stdout || error.message || '';
309
+ const errorCount = (output.match(/error TS/gi) || []).length;
310
+
311
+ if (errorCount > 0) {
312
+ state.errors.push({ type: 'typecheck', message: `${errorCount} type errors` });
313
+ return { status: 'fail', errors: errorCount };
314
+ }
315
+
316
+ state.errors.push({ type: 'typecheck', message: 'Type check failed' });
317
+ return { status: 'fail', message: 'Type check failed' };
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Render the watch UI
323
+ */
324
+ function renderUI(state, watchDirs, checks) {
325
+ // Move cursor to top
326
+ readline.cursorTo(process.stdout, 0, 0);
327
+ readline.clearScreenDown(process.stdout);
328
+
329
+ const lines = [];
330
+ const width = 70;
331
+
332
+ // Header
333
+ lines.push(chalk.cyan('+' + '-'.repeat(width) + '+'));
334
+ lines.push(chalk.cyan('|') + ' ' + chalk.bold('VIBECODE WATCH') + ' '.repeat(width - 18) + chalk.cyan('|'));
335
+ lines.push(chalk.cyan('|') + ' '.repeat(width) + chalk.cyan('|'));
336
+
337
+ // Status info
338
+ const watchInfo = `Watching: ${watchDirs.join(', ')}`.substring(0, width - 4);
339
+ lines.push(chalk.cyan('|') + ` ${chalk.white(watchInfo)}`.padEnd(width + 9) + chalk.cyan('|'));
340
+
341
+ const eventsInfo = `Events: ${state.events}`;
342
+ lines.push(chalk.cyan('|') + ` ${chalk.yellow(eventsInfo)}`.padEnd(width + 9) + chalk.cyan('|'));
343
+
344
+ const statusText = state.running ? chalk.yellow('Running...') : chalk.green('Watching');
345
+ lines.push(chalk.cyan('|') + ` Status: ${statusText}`.padEnd(width + 9) + chalk.cyan('|'));
346
+
347
+ lines.push(chalk.cyan('|') + ' '.repeat(width) + chalk.cyan('|'));
348
+
349
+ // Separator
350
+ lines.push(chalk.cyan('|') + ' ' + chalk.gray('-'.repeat(width - 4)) + ' ' + chalk.cyan('|'));
351
+
352
+ // Results
353
+ if (checks.typecheck) {
354
+ const result = formatResult('TypeScript', state.results.typecheck);
355
+ lines.push(chalk.cyan('|') + ` ${result}`.padEnd(width + 9) + chalk.cyan('|'));
356
+ }
357
+ if (checks.lint) {
358
+ const result = formatResult('Lint', state.results.lint);
359
+ lines.push(chalk.cyan('|') + ` ${result}`.padEnd(width + 9) + chalk.cyan('|'));
360
+ }
361
+ if (checks.test) {
362
+ const result = formatResult('Tests', state.results.test);
363
+ lines.push(chalk.cyan('|') + ` ${result}`.padEnd(width + 9) + chalk.cyan('|'));
364
+ }
365
+ if (checks.build) {
366
+ const result = formatResult('Build', state.results.build);
367
+ lines.push(chalk.cyan('|') + ` ${result}`.padEnd(width + 9) + chalk.cyan('|'));
368
+ }
369
+
370
+ lines.push(chalk.cyan('|') + ' '.repeat(width) + chalk.cyan('|'));
371
+
372
+ // Last event
373
+ if (state.lastEvent) {
374
+ lines.push(chalk.cyan('|') + ' ' + chalk.gray('-'.repeat(width - 4)) + ' ' + chalk.cyan('|'));
375
+ const eventText = state.lastEvent.substring(0, width - 4);
376
+ lines.push(chalk.cyan('|') + ` ${chalk.gray(eventText)}`.padEnd(width + 9) + chalk.cyan('|'));
377
+ lines.push(chalk.cyan('|') + ' '.repeat(width) + chalk.cyan('|'));
378
+ }
379
+
380
+ // Errors
381
+ if (state.errors.length > 0) {
382
+ lines.push(chalk.cyan('|') + ' ' + chalk.red('Errors:') + ' '.repeat(width - 11) + chalk.cyan('|'));
383
+ for (const error of state.errors.slice(0, 3)) {
384
+ const errorText = ` ${error.message}`.substring(0, width - 2);
385
+ lines.push(chalk.cyan('|') + ` ${chalk.red('*')} ${errorText}`.padEnd(width + 9) + chalk.cyan('|'));
386
+ }
387
+ if (state.errors.length > 3) {
388
+ lines.push(chalk.cyan('|') + ` ${chalk.gray(`... and ${state.errors.length - 3} more`)}`.padEnd(width + 9) + chalk.cyan('|'));
389
+ }
390
+ lines.push(chalk.cyan('|') + ' '.repeat(width) + chalk.cyan('|'));
391
+ }
392
+
393
+ // Keyboard shortcuts
394
+ lines.push(chalk.cyan('|') + ' ' + chalk.gray('-'.repeat(width - 4)) + ' ' + chalk.cyan('|'));
395
+ lines.push(chalk.cyan('|') + ` ${chalk.gray('[q] Quit [t] Test [l] Lint [b] Build [a] All [r] Reset')}`.padEnd(width + 9) + chalk.cyan('|'));
396
+ lines.push(chalk.cyan('|') + ' '.repeat(width) + chalk.cyan('|'));
397
+ lines.push(chalk.cyan('+' + '-'.repeat(width) + '+'));
398
+
399
+ console.log(lines.join('\n'));
400
+ }
401
+
402
+ /**
403
+ * Format result for display
404
+ */
405
+ function formatResult(name, result) {
406
+ const label = name.padEnd(12);
407
+
408
+ if (!result) {
409
+ return `${label} ${chalk.gray('--')}`;
410
+ }
411
+
412
+ switch (result.status) {
413
+ case 'pass':
414
+ if (result.passed !== undefined) {
415
+ return `${label} ${chalk.green('PASS')} ${chalk.gray(`(${result.passed} passed)`)}`;
416
+ }
417
+ return `${label} ${chalk.green('PASS')}`;
418
+
419
+ case 'fail':
420
+ if (result.failed !== undefined) {
421
+ return `${label} ${chalk.red('FAIL')} ${chalk.gray(`(${result.failed} failed)`)}`;
422
+ }
423
+ if (result.errors !== undefined) {
424
+ return `${label} ${chalk.red('FAIL')} ${chalk.gray(`(${result.errors} errors)`)}`;
425
+ }
426
+ return `${label} ${chalk.red('FAIL')}`;
427
+
428
+ case 'warn':
429
+ return `${label} ${chalk.yellow('WARN')} ${chalk.gray(`(${result.warnings || 0} warnings)`)}`;
430
+
431
+ case 'skip':
432
+ return `${label} ${chalk.gray('SKIP')} ${chalk.gray(`(${result.message || 'skipped'})`)}`;
433
+
434
+ case 'error':
435
+ return `${label} ${chalk.red('ERROR')}`;
436
+
437
+ default:
438
+ return `${label} ${chalk.gray('--')}`;
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Format current time
444
+ */
445
+ function formatTime() {
446
+ return new Date().toLocaleTimeString('en-US', { hour12: false });
447
+ }
448
+
449
+ /**
450
+ * Setup keyboard shortcuts
451
+ */
452
+ function setupKeyboardShortcuts(state, checks, cwd, watchDirs, watcher, runChecks) {
453
+ readline.emitKeypressEvents(process.stdin);
454
+ if (process.stdin.isTTY) {
455
+ process.stdin.setRawMode(true);
456
+ }
457
+
458
+ process.stdin.on('keypress', async (str, key) => {
459
+ if (!key) return;
460
+
461
+ // Quit
462
+ if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
463
+ console.clear();
464
+ console.log(chalk.cyan('\n Watch stopped.\n'));
465
+ await watcher.close();
466
+ process.exit(0);
467
+ }
468
+
469
+ // Run tests
470
+ if (key.name === 't' && !state.running) {
471
+ state.running = true;
472
+ state.lastEvent = `[${formatTime()}] Manual: Running tests...`;
473
+ renderUI(state, watchDirs, checks);
474
+ state.results.test = await runTest(cwd, state);
475
+ state.running = false;
476
+ renderUI(state, watchDirs, checks);
477
+ }
478
+
479
+ // Run lint
480
+ if (key.name === 'l' && !state.running) {
481
+ state.running = true;
482
+ state.lastEvent = `[${formatTime()}] Manual: Running lint...`;
483
+ renderUI(state, watchDirs, checks);
484
+ state.results.lint = await runLint(cwd, state);
485
+ state.running = false;
486
+ renderUI(state, watchDirs, checks);
487
+ }
488
+
489
+ // Run build
490
+ if (key.name === 'b' && !state.running) {
491
+ state.running = true;
492
+ state.lastEvent = `[${formatTime()}] Manual: Running build...`;
493
+ renderUI(state, watchDirs, checks);
494
+ state.results.build = await runBuild(cwd, state);
495
+ state.running = false;
496
+ renderUI(state, watchDirs, checks);
497
+ }
498
+
499
+ // Run all checks
500
+ if (key.name === 'a' && !state.running) {
501
+ state.running = true;
502
+ state.errors = [];
503
+ state.lastEvent = `[${formatTime()}] Manual: Running all checks...`;
504
+ renderUI(state, watchDirs, checks);
505
+
506
+ if (checks.typecheck || true) {
507
+ state.results.typecheck = await runTypecheck(cwd, state);
508
+ renderUI(state, watchDirs, checks);
509
+ }
510
+ if (checks.lint || true) {
511
+ state.results.lint = await runLint(cwd, state);
512
+ renderUI(state, watchDirs, checks);
513
+ }
514
+ if (checks.test || true) {
515
+ state.results.test = await runTest(cwd, state);
516
+ renderUI(state, watchDirs, checks);
517
+ }
518
+ if (checks.build) {
519
+ state.results.build = await runBuild(cwd, state);
520
+ renderUI(state, watchDirs, checks);
521
+ }
522
+
523
+ state.running = false;
524
+ renderUI(state, watchDirs, checks);
525
+ }
526
+
527
+ // Reset
528
+ if (key.name === 'r') {
529
+ state.events = 0;
530
+ state.results = { test: null, lint: null, build: null, typecheck: null };
531
+ state.errors = [];
532
+ state.lastEvent = `[${formatTime()}] Reset`;
533
+ renderUI(state, watchDirs, checks);
534
+ }
535
+ });
536
+ }
537
+
538
+ /**
539
+ * Send desktop notification
540
+ */
541
+ function notify(title, message) {
542
+ const platform = process.platform;
543
+
544
+ try {
545
+ if (platform === 'darwin') {
546
+ exec(`osascript -e 'display notification "${message}" with title "${title}"'`);
547
+ } else if (platform === 'linux') {
548
+ exec(`notify-send "${title}" "${message}"`);
549
+ }
550
+ // Windows would need different approach (powershell or node-notifier)
551
+ } catch {
552
+ // Silently fail - notifications are optional
553
+ }
554
+ }
555
+
556
+ export default watchCommand;