@nclamvn/vibecode-cli 2.0.0 → 2.2.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.
- package/.vibecode/learning/fixes.json +1 -0
- package/.vibecode/learning/preferences.json +1 -0
- package/README.md +310 -49
- package/SESSION_NOTES.md +154 -0
- package/bin/vibecode.js +235 -2
- package/package.json +5 -2
- package/src/agent/decomposition.js +476 -0
- package/src/agent/index.js +391 -0
- package/src/agent/memory.js +542 -0
- package/src/agent/orchestrator.js +917 -0
- package/src/agent/self-healing.js +516 -0
- package/src/commands/agent.js +349 -0
- package/src/commands/ask.js +230 -0
- package/src/commands/assist.js +413 -0
- package/src/commands/build.js +345 -4
- package/src/commands/debug.js +565 -0
- package/src/commands/docs.js +167 -0
- package/src/commands/git.js +1024 -0
- package/src/commands/go.js +635 -0
- package/src/commands/learn.js +294 -0
- package/src/commands/migrate.js +341 -0
- package/src/commands/plan.js +8 -2
- package/src/commands/refactor.js +205 -0
- package/src/commands/review.js +126 -1
- package/src/commands/security.js +229 -0
- package/src/commands/shell.js +486 -0
- package/src/commands/templates.js +397 -0
- package/src/commands/test.js +194 -0
- package/src/commands/undo.js +281 -0
- package/src/commands/watch.js +556 -0
- package/src/commands/wizard.js +322 -0
- package/src/config/constants.js +5 -1
- package/src/config/templates.js +146 -15
- package/src/core/backup.js +325 -0
- package/src/core/error-analyzer.js +237 -0
- package/src/core/fix-generator.js +195 -0
- package/src/core/iteration.js +226 -0
- package/src/core/learning.js +295 -0
- package/src/core/session.js +18 -2
- package/src/core/test-runner.js +281 -0
- package/src/debug/analyzer.js +329 -0
- package/src/debug/evidence.js +228 -0
- package/src/debug/fixer.js +348 -0
- package/src/debug/image-analyzer.js +304 -0
- package/src/debug/index.js +378 -0
- package/src/debug/verifier.js +346 -0
- package/src/index.js +102 -0
- package/src/providers/claude-code.js +12 -7
- package/src/templates/index.js +724 -0
- package/src/ui/__tests__/error-translator.test.js +390 -0
- package/src/ui/dashboard.js +364 -0
- package/src/ui/error-translator.js +775 -0
- 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;
|