@vibe-validate/cli 0.14.3 → 0.15.0-rc.1
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/README.md +26 -6
- package/bin/vibe-validate +131 -0
- package/config-templates/minimal.yaml +1 -1
- package/config-templates/typescript-library.yaml +1 -1
- package/config-templates/typescript-nodejs.yaml +1 -1
- package/config-templates/typescript-react.yaml +1 -1
- package/dist/bin.js +18 -4
- package/dist/bin.js.map +1 -1
- package/dist/commands/cleanup.d.ts +1 -1
- package/dist/commands/cleanup.d.ts.map +1 -1
- package/dist/commands/cleanup.js +108 -2
- package/dist/commands/cleanup.js.map +1 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +45 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-workflow.d.ts.map +1 -1
- package/dist/commands/generate-workflow.js +3 -11
- package/dist/commands/generate-workflow.js.map +1 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +263 -68
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pre-commit.d.ts.map +1 -1
- package/dist/commands/pre-commit.js +6 -2
- package/dist/commands/pre-commit.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +618 -214
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/state.d.ts.map +1 -1
- package/dist/commands/state.js +4 -7
- package/dist/commands/state.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +5 -6
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/watch-pr.d.ts.map +1 -1
- package/dist/commands/watch-pr.js +33 -16
- package/dist/commands/watch-pr.js.map +1 -1
- package/dist/schemas/run-result-schema-export.d.ts +32 -0
- package/dist/schemas/run-result-schema-export.d.ts.map +1 -0
- package/dist/schemas/run-result-schema-export.js +40 -0
- package/dist/schemas/run-result-schema-export.js.map +1 -0
- package/dist/schemas/run-result-schema.d.ts +850 -0
- package/dist/schemas/run-result-schema.d.ts.map +1 -0
- package/dist/schemas/run-result-schema.js +67 -0
- package/dist/schemas/run-result-schema.js.map +1 -0
- package/dist/schemas/watch-pr-schema.d.ts +420 -22
- package/dist/schemas/watch-pr-schema.d.ts.map +1 -1
- package/dist/schemas/watch-pr-schema.js +2 -2
- package/dist/schemas/watch-pr-schema.js.map +1 -1
- package/dist/scripts/generate-run-result-schema.d.ts +10 -0
- package/dist/scripts/generate-run-result-schema.d.ts.map +1 -0
- package/dist/scripts/generate-run-result-schema.js +20 -0
- package/dist/scripts/generate-run-result-schema.js.map +1 -0
- package/dist/services/ci-provider.d.ts +21 -6
- package/dist/services/ci-provider.d.ts.map +1 -1
- package/dist/services/ci-providers/github-actions.d.ts +1 -5
- package/dist/services/ci-providers/github-actions.d.ts.map +1 -1
- package/dist/services/ci-providers/github-actions.js +9 -30
- package/dist/services/ci-providers/github-actions.js.map +1 -1
- package/dist/utils/config-error-reporter.js +1 -1
- package/dist/utils/config-error-reporter.js.map +1 -1
- package/dist/utils/pid-lock.d.ts.map +1 -1
- package/dist/utils/pid-lock.js +3 -6
- package/dist/utils/pid-lock.js.map +1 -1
- package/dist/utils/project-id.d.ts.map +1 -1
- package/dist/utils/project-id.js +3 -4
- package/dist/utils/project-id.js.map +1 -1
- package/dist/utils/runner-adapter.js +3 -2
- package/dist/utils/runner-adapter.js.map +1 -1
- package/dist/utils/temp-files.d.ts +67 -0
- package/dist/utils/temp-files.d.ts.map +1 -0
- package/dist/utils/temp-files.js +202 -0
- package/dist/utils/temp-files.js.map +1 -0
- package/dist/utils/validate-workflow.d.ts.map +1 -1
- package/dist/utils/validate-workflow.js +17 -12
- package/dist/utils/validate-workflow.js.map +1 -1
- package/dist/vibe-validate +131 -0
- package/package.json +11 -9
- package/run-result.schema.json +186 -0
- package/watch-pr-result.schema.json +128 -6
package/dist/commands/run.js
CHANGED
|
@@ -4,23 +4,142 @@
|
|
|
4
4
|
* Executes a command and extracts LLM-friendly error output using vibe-validate extractors.
|
|
5
5
|
* Provides concise, structured error information to save AI agent context windows.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { writeFile, readFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
8
10
|
import { autoDetectAndExtract } from '@vibe-validate/extractors';
|
|
11
|
+
import { getRunOutputDir, ensureDir } from '../utils/temp-files.js';
|
|
12
|
+
import { getGitTreeHash, encodeRunCacheKey, extractYamlWithPreamble } from '@vibe-validate/git';
|
|
13
|
+
import { spawnCommand, parseVibeValidateOutput } from '@vibe-validate/core';
|
|
9
14
|
import yaml from 'yaml';
|
|
15
|
+
import chalk from 'chalk';
|
|
10
16
|
export function runCommand(program) {
|
|
11
17
|
program
|
|
12
18
|
.command('run')
|
|
13
|
-
.description('Run a command and extract LLM-friendly errors
|
|
14
|
-
.argument('<command
|
|
15
|
-
.
|
|
19
|
+
.description('Run a command and extract LLM-friendly errors (with smart caching)')
|
|
20
|
+
.argument('<command...>', 'Command to execute (multiple words supported)')
|
|
21
|
+
.option('--check', 'Check if cached result exists without executing')
|
|
22
|
+
.option('--force', 'Force execution and update cache (bypass cache read)')
|
|
23
|
+
.option('--head <lines>', 'Display first N lines of output after YAML (on stderr)', Number.parseInt)
|
|
24
|
+
.option('--tail <lines>', 'Display last N lines of output after YAML (on stderr)', Number.parseInt)
|
|
25
|
+
.option('--verbose', 'Display all output after YAML (on stderr)')
|
|
26
|
+
.helpOption(false) // Disable automatic help to avoid conflicts with commands that use --help
|
|
27
|
+
.allowUnknownOption() // Allow unknown options in the command
|
|
28
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Complex command handling logic, refactoring would reduce readability
|
|
29
|
+
.action(async (commandParts, options) => {
|
|
30
|
+
// WORKAROUND: Commander.js with allowUnknownOption() strips ALL options from commandParts
|
|
31
|
+
// Parse directly from process.argv to get the actual command with our options parsed correctly
|
|
32
|
+
const argv = process.argv;
|
|
33
|
+
const runIndex = argv.findIndex(arg => arg === 'run' || arg.endsWith('/vv') || arg.endsWith('/vibe-validate'));
|
|
34
|
+
let actualOptions;
|
|
35
|
+
let commandString;
|
|
36
|
+
if (runIndex === -1) {
|
|
37
|
+
// Test environment or non-standard invocation - fallback to Commander's parsing
|
|
38
|
+
actualOptions = options;
|
|
39
|
+
commandString = commandParts.join(' ');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Real CLI environment - manually parse argv to get correct options
|
|
43
|
+
actualOptions = {};
|
|
44
|
+
const actualCommand = [];
|
|
45
|
+
let i = runIndex + 1;
|
|
46
|
+
while (i < argv.length) {
|
|
47
|
+
const arg = argv[i];
|
|
48
|
+
if (arg === '--verbose') {
|
|
49
|
+
actualOptions.verbose = true;
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
else if (arg === '--check') {
|
|
53
|
+
actualOptions.check = true;
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
else if (arg === '--force') {
|
|
57
|
+
actualOptions.force = true;
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
else if (arg === '--head' && i + 1 < argv.length) {
|
|
61
|
+
actualOptions.head = Number.parseInt(argv[i + 1], 10);
|
|
62
|
+
i += 2;
|
|
63
|
+
}
|
|
64
|
+
else if (arg === '--tail' && i + 1 < argv.length) {
|
|
65
|
+
actualOptions.tail = Number.parseInt(argv[i + 1], 10);
|
|
66
|
+
i += 2;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Not a known option - rest is the command
|
|
70
|
+
actualCommand.push(...argv.slice(i));
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
commandString = actualCommand.join(' ');
|
|
75
|
+
}
|
|
76
|
+
// If command is empty or starts with a flag (like --help), show help for run command
|
|
77
|
+
// This handles cases like: vv run --help, vv run --verbose --help, vv run --help bob
|
|
78
|
+
const trimmedCommand = commandString.trim();
|
|
79
|
+
if (!trimmedCommand || trimmedCommand.startsWith('-')) {
|
|
80
|
+
// No command or command starts with a flag - show run command help
|
|
81
|
+
showRunHelp();
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
16
84
|
try {
|
|
17
|
-
|
|
85
|
+
// Handle --check flag (cache status check only)
|
|
86
|
+
if (actualOptions.check) {
|
|
87
|
+
const cachedResult = await tryGetCachedResult(commandString);
|
|
88
|
+
if (cachedResult) {
|
|
89
|
+
// Cache hit - output cached result and exit with code 0
|
|
90
|
+
process.stdout.write('---\n');
|
|
91
|
+
process.stdout.write(yaml.stringify(cachedResult));
|
|
92
|
+
process.stdout.write('\n');
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Cache miss - output message and exit with code 1
|
|
97
|
+
process.stderr.write('No cached result found for command.\n');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Try to get cached result (unless --force)
|
|
103
|
+
let result;
|
|
104
|
+
let context = { preamble: '', stderr: '' };
|
|
105
|
+
if (!actualOptions.force) {
|
|
106
|
+
const cachedResult = await tryGetCachedResult(commandString);
|
|
107
|
+
if (cachedResult) {
|
|
108
|
+
result = cachedResult;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Cache miss - execute command
|
|
112
|
+
const executeResult = await executeAndExtract(commandString);
|
|
113
|
+
result = executeResult.result;
|
|
114
|
+
context = executeResult.context;
|
|
115
|
+
// Store result in cache
|
|
116
|
+
await storeCacheResult(commandString, result);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Force flag - bypass cache and execute
|
|
121
|
+
const executeResult = await executeAndExtract(commandString);
|
|
122
|
+
result = executeResult.result;
|
|
123
|
+
context = executeResult.context;
|
|
124
|
+
// Update cache with fresh result
|
|
125
|
+
await storeCacheResult(commandString, result);
|
|
126
|
+
}
|
|
18
127
|
// CRITICAL: Write complete YAML to stdout and flush BEFORE any stderr
|
|
19
128
|
// This ensures even if callers use 2>&1, YAML completes first
|
|
129
|
+
// Format as YAML front matter with opening delimiter
|
|
20
130
|
process.stdout.write('---\n');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
131
|
+
// Add YAML comment for non-git repositories to inform LLMs
|
|
132
|
+
let yamlOutput = yaml.stringify(result);
|
|
133
|
+
if (result.treeHash === 'unknown') {
|
|
134
|
+
yamlOutput = yamlOutput.replace(/^treeHash: unknown$/m, 'treeHash: unknown # Not in git repository - caching disabled');
|
|
135
|
+
}
|
|
136
|
+
process.stdout.write(yamlOutput);
|
|
137
|
+
// Only write closing delimiter if there will be output displayed
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Using || to check truthy values (0 is falsy, which is correct)
|
|
139
|
+
const willDisplayOutput = !!(actualOptions.head || actualOptions.tail || actualOptions.verbose);
|
|
140
|
+
if (willDisplayOutput) {
|
|
141
|
+
process.stdout.write('---\n');
|
|
142
|
+
}
|
|
24
143
|
// Flush stdout to guarantee all YAML is written before any stderr
|
|
25
144
|
// This prevents interleaving when streams are combined with 2>&1
|
|
26
145
|
await new Promise((resolve) => {
|
|
@@ -38,6 +157,10 @@ export function runCommand(program) {
|
|
|
38
157
|
if (context.stderr) {
|
|
39
158
|
process.stderr.write(context.stderr);
|
|
40
159
|
}
|
|
160
|
+
// Display output based on --head, --tail, or --verbose flags
|
|
161
|
+
if (willDisplayOutput) {
|
|
162
|
+
await displayCommandOutput(result, actualOptions);
|
|
163
|
+
}
|
|
41
164
|
// Exit with same code as the command
|
|
42
165
|
process.exit(result.exitCode);
|
|
43
166
|
}
|
|
@@ -56,38 +179,197 @@ export function runCommand(program) {
|
|
|
56
179
|
}
|
|
57
180
|
});
|
|
58
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Get working directory relative to git root
|
|
184
|
+
* Returns empty string for root, "packages/cli" for subdirectory
|
|
185
|
+
*/
|
|
186
|
+
function getWorkingDirectory() {
|
|
187
|
+
try {
|
|
188
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', {
|
|
189
|
+
encoding: 'utf8',
|
|
190
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
191
|
+
}).trim();
|
|
192
|
+
const cwd = process.cwd();
|
|
193
|
+
// If cwd is git root, return empty string
|
|
194
|
+
if (cwd === gitRoot) {
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
// Return relative path from git root
|
|
198
|
+
return cwd.substring(gitRoot.length + 1); // +1 to remove leading slash
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Not in a git repository - return empty string
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Try to get cached result for a command
|
|
207
|
+
* Returns null if no cache hit or if not in a git repository
|
|
208
|
+
*/
|
|
209
|
+
async function tryGetCachedResult(commandString) {
|
|
210
|
+
try {
|
|
211
|
+
// Get tree hash
|
|
212
|
+
const treeHash = await getGitTreeHash();
|
|
213
|
+
// Skip caching if not in git repository
|
|
214
|
+
if (treeHash === 'unknown') {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
// Get working directory
|
|
218
|
+
const workdir = getWorkingDirectory();
|
|
219
|
+
// Encode cache key
|
|
220
|
+
const cacheKey = encodeRunCacheKey(commandString, workdir);
|
|
221
|
+
// Construct git notes ref path: refs/notes/vibe-validate/run/{treeHash}/{cacheKey}
|
|
222
|
+
const refPath = `vibe-validate/run/${treeHash}/${cacheKey}`;
|
|
223
|
+
// Try to read git note
|
|
224
|
+
const noteContent = execSync(`git notes --ref=${refPath} show HEAD 2>/dev/null || true`, {
|
|
225
|
+
encoding: 'utf8',
|
|
226
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
227
|
+
}).trim();
|
|
228
|
+
if (!noteContent) {
|
|
229
|
+
// Cache miss
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
// Parse cached note
|
|
233
|
+
const cachedNote = yaml.parse(noteContent);
|
|
234
|
+
// Migration: v0.14.x cached notes used 'duration' (ms), v0.15.0+ uses 'durationSecs' (s)
|
|
235
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
236
|
+
const durationSecs = cachedNote.durationSecs ?? (cachedNote.duration ? cachedNote.duration / 1000 : 0);
|
|
237
|
+
// Convert to RunResult format (mark as cached)
|
|
238
|
+
const result = {
|
|
239
|
+
command: cachedNote.command, // Use command from cache (may be unwrapped)
|
|
240
|
+
exitCode: cachedNote.exitCode,
|
|
241
|
+
durationSecs,
|
|
242
|
+
timestamp: cachedNote.timestamp,
|
|
243
|
+
treeHash: cachedNote.treeHash,
|
|
244
|
+
extraction: cachedNote.extraction,
|
|
245
|
+
...(cachedNote.outputFiles ? { outputFiles: cachedNote.outputFiles } : {}),
|
|
246
|
+
isCachedResult: true, // Mark as cache hit
|
|
247
|
+
};
|
|
248
|
+
return result;
|
|
249
|
+
// eslint-disable-next-line sonarjs/no-ignored-exceptions -- Cache lookup failure is non-critical, proceed with execution
|
|
250
|
+
}
|
|
251
|
+
catch (_error) {
|
|
252
|
+
// Cache lookup failed - proceed with execution
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Store result in cache (only successful runs - exitCode === 0)
|
|
258
|
+
*/
|
|
259
|
+
async function storeCacheResult(commandString, result) {
|
|
260
|
+
try {
|
|
261
|
+
// Only cache successful runs (v0.15.0+)
|
|
262
|
+
// Failed runs may be transient or environment-specific
|
|
263
|
+
if (result.exitCode !== 0) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// Get tree hash
|
|
267
|
+
const treeHash = await getGitTreeHash();
|
|
268
|
+
// Skip caching if not in git repository
|
|
269
|
+
if (treeHash === 'unknown') {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// Get working directory
|
|
273
|
+
const workdir = getWorkingDirectory();
|
|
274
|
+
// Encode cache key
|
|
275
|
+
const cacheKey = encodeRunCacheKey(commandString, workdir);
|
|
276
|
+
// Construct git notes ref path
|
|
277
|
+
const refPath = `vibe-validate/run/${treeHash}/${cacheKey}`;
|
|
278
|
+
// Build cache note (extraction already cleaned in runner)
|
|
279
|
+
// Token optimization: Only include extraction when exitCode !== 0 OR there are actual errors
|
|
280
|
+
// Note: result.command may be unwrapped (actual command) if nested vibe-validate was detected
|
|
281
|
+
const cacheNote = {
|
|
282
|
+
treeHash,
|
|
283
|
+
command: result.command, // Store unwrapped command (e.g., "eslint ..." not "pnpm lint")
|
|
284
|
+
workdir,
|
|
285
|
+
timestamp: result.timestamp,
|
|
286
|
+
exitCode: result.exitCode,
|
|
287
|
+
durationSecs: result.durationSecs,
|
|
288
|
+
...(result.extraction ? { extraction: result.extraction } : {}), // Conditionally include extraction
|
|
289
|
+
...(result.outputFiles ? { outputFiles: result.outputFiles } : {}),
|
|
290
|
+
};
|
|
291
|
+
// Store in git notes using heredoc to avoid quote escaping issues
|
|
292
|
+
const noteYaml = yaml.stringify(cacheNote);
|
|
293
|
+
// Use heredoc format for multi-line YAML
|
|
294
|
+
try {
|
|
295
|
+
execSync(`cat <<'EOF' | git notes --ref=${refPath} add -f -F - HEAD\n${noteYaml}\nEOF`, {
|
|
296
|
+
stdio: 'ignore',
|
|
297
|
+
shell: '/bin/bash', // Ensure bash for heredoc support
|
|
298
|
+
});
|
|
299
|
+
// eslint-disable-next-line sonarjs/no-ignored-exceptions -- Cache storage failure is non-critical
|
|
300
|
+
}
|
|
301
|
+
catch (_error) {
|
|
302
|
+
// Cache storage failed - not critical, continue
|
|
303
|
+
}
|
|
304
|
+
// eslint-disable-next-line sonarjs/no-ignored-exceptions -- Cache storage failure is non-critical, continue execution
|
|
305
|
+
}
|
|
306
|
+
catch (_error) {
|
|
307
|
+
// Cache storage failed - not critical, continue
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Strip ANSI escape codes from text
|
|
312
|
+
*/
|
|
313
|
+
function stripAnsiCodes(text) {
|
|
314
|
+
// Control character \x1b is intentionally used to match ANSI escape codes
|
|
315
|
+
// eslint-disable-next-line no-control-regex, sonarjs/no-control-regex
|
|
316
|
+
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
317
|
+
}
|
|
59
318
|
/**
|
|
60
319
|
* Execute a command and extract errors from its output
|
|
61
320
|
*/
|
|
62
321
|
async function executeAndExtract(commandString) {
|
|
63
322
|
return new Promise((resolve, reject) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// NOSONAR - Intentional shell execution of user-defined commands
|
|
67
|
-
const child = spawn(commandString, {
|
|
68
|
-
shell: true,
|
|
69
|
-
stdio: ['inherit', 'pipe', 'pipe'], // inherit stdin, pipe stdout/stderr
|
|
70
|
-
});
|
|
323
|
+
const startTime = Date.now();
|
|
324
|
+
const child = spawnCommand(commandString);
|
|
71
325
|
let stdout = '';
|
|
72
326
|
let stderr = '';
|
|
73
|
-
|
|
327
|
+
const combinedLines = [];
|
|
328
|
+
// Capture stdout (spawnCommand always sets stdio: ['ignore', 'pipe', 'pipe'], so stdout/stderr are guaranteed non-null)
|
|
329
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- spawnCommand always pipes stdout/stderr
|
|
74
330
|
child.stdout.on('data', (data) => {
|
|
75
|
-
|
|
331
|
+
const chunk = data.toString();
|
|
332
|
+
stdout += chunk;
|
|
333
|
+
// Add to combined output (ANSI-stripped)
|
|
334
|
+
const lines = chunk.split('\n');
|
|
335
|
+
for (const line of lines) {
|
|
336
|
+
if (line) {
|
|
337
|
+
combinedLines.push({
|
|
338
|
+
ts: new Date().toISOString(),
|
|
339
|
+
stream: 'stdout',
|
|
340
|
+
line: stripAnsiCodes(line),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
76
344
|
});
|
|
77
|
-
// Capture stderr
|
|
345
|
+
// Capture stderr
|
|
346
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- spawnCommand always pipes stdout/stderr
|
|
78
347
|
child.stderr.on('data', (data) => {
|
|
79
|
-
|
|
348
|
+
const chunk = data.toString();
|
|
349
|
+
stderr += chunk;
|
|
350
|
+
// Add to combined output (ANSI-stripped)
|
|
351
|
+
const lines = chunk.split('\n');
|
|
352
|
+
for (const line of lines) {
|
|
353
|
+
if (line) {
|
|
354
|
+
combinedLines.push({
|
|
355
|
+
ts: new Date().toISOString(),
|
|
356
|
+
stream: 'stderr',
|
|
357
|
+
line: stripAnsiCodes(line),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
80
361
|
});
|
|
81
362
|
// Handle process exit
|
|
82
363
|
child.on('close', (exitCode = 1) => {
|
|
364
|
+
const durationSecs = (Date.now() - startTime) / 1000;
|
|
83
365
|
// CRITICAL: Check ONLY stdout for YAML (not stderr)
|
|
84
366
|
// This prevents stderr warnings from corrupting nested YAML output
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const mergedResult = mergeNestedYaml(commandString, yaml, exitCode);
|
|
367
|
+
const yamlResult = extractYamlWithPreamble(stdout);
|
|
368
|
+
if (yamlResult) {
|
|
369
|
+
const mergedResult = mergeNestedYaml(commandString, yamlResult.yaml, exitCode, durationSecs);
|
|
88
370
|
// Include preamble and stderr for context
|
|
89
371
|
const contextOutput = {
|
|
90
|
-
preamble: preamble
|
|
372
|
+
preamble: yamlResult.preamble,
|
|
91
373
|
stderr: stderr.trim(),
|
|
92
374
|
};
|
|
93
375
|
resolve({ result: mergedResult, context: contextOutput });
|
|
@@ -95,20 +377,101 @@ async function executeAndExtract(commandString) {
|
|
|
95
377
|
}
|
|
96
378
|
// For extraction, combine both streams (stderr has useful error context)
|
|
97
379
|
const combinedOutput = stdout + stderr;
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
380
|
+
// Extract errors using smart extractor (output-based detection)
|
|
381
|
+
// Token optimization: Only extract when exitCode !== 0 OR there are actual errors
|
|
382
|
+
const rawExtraction = autoDetectAndExtract({
|
|
383
|
+
stdout,
|
|
384
|
+
stderr,
|
|
385
|
+
combined: combinedOutput,
|
|
386
|
+
});
|
|
387
|
+
const extraction = (exitCode !== 0 || rawExtraction.totalErrors > 0) ? rawExtraction : undefined;
|
|
388
|
+
// Get tree hash for result (async operation needs to be awaited)
|
|
389
|
+
getGitTreeHash()
|
|
390
|
+
.then(async (treeHash) => {
|
|
391
|
+
// Write output files to organized temp directory
|
|
392
|
+
const outputDir = getRunOutputDir(treeHash);
|
|
393
|
+
await ensureDir(outputDir);
|
|
394
|
+
const writePromises = [];
|
|
395
|
+
// Write stdout.log (only if non-empty)
|
|
396
|
+
let stdoutFile;
|
|
397
|
+
if (stdout.trim()) {
|
|
398
|
+
stdoutFile = join(outputDir, 'stdout.log');
|
|
399
|
+
writePromises.push(writeFile(stdoutFile, stdout, 'utf-8'));
|
|
400
|
+
}
|
|
401
|
+
// Write stderr.log (only if non-empty)
|
|
402
|
+
let stderrFile;
|
|
403
|
+
if (stderr.trim()) {
|
|
404
|
+
stderrFile = join(outputDir, 'stderr.log');
|
|
405
|
+
writePromises.push(writeFile(stderrFile, stderr, 'utf-8'));
|
|
406
|
+
}
|
|
407
|
+
// Write combined.jsonl (always)
|
|
408
|
+
const combinedFile = join(outputDir, 'combined.jsonl');
|
|
409
|
+
const combinedContent = combinedLines
|
|
410
|
+
// eslint-disable-next-line sonarjs/no-nested-functions -- Array.map callback is standard functional programming pattern
|
|
411
|
+
.map(line => JSON.stringify(line))
|
|
412
|
+
.join('\n');
|
|
413
|
+
writePromises.push(writeFile(combinedFile, combinedContent, 'utf-8'));
|
|
414
|
+
// Wait for all writes to complete
|
|
415
|
+
await Promise.all(writePromises);
|
|
416
|
+
const result = {
|
|
417
|
+
command: commandString,
|
|
418
|
+
exitCode,
|
|
419
|
+
durationSecs,
|
|
420
|
+
timestamp: new Date().toISOString(),
|
|
421
|
+
treeHash,
|
|
422
|
+
...(extraction ? { extraction } : {}), // Only include extraction if needed
|
|
423
|
+
outputFiles: {
|
|
424
|
+
...(stdoutFile ? { stdout: stdoutFile } : {}),
|
|
425
|
+
...(stderrFile ? { stderr: stderrFile } : {}),
|
|
426
|
+
combined: combinedFile,
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
resolve({ result, context: { preamble: '', stderr: '' } });
|
|
430
|
+
})
|
|
431
|
+
.catch(async () => {
|
|
432
|
+
// If tree hash fails, use timestamp-based fallback
|
|
433
|
+
const timestamp = new Date().toISOString();
|
|
434
|
+
const fallbackHash = `nogit-${Date.now()}`;
|
|
435
|
+
// Write output files even without git
|
|
436
|
+
const outputDir = getRunOutputDir(fallbackHash);
|
|
437
|
+
await ensureDir(outputDir);
|
|
438
|
+
const writePromises = [];
|
|
439
|
+
// Write stdout.log (only if non-empty)
|
|
440
|
+
let stdoutFile;
|
|
441
|
+
if (stdout.trim()) {
|
|
442
|
+
stdoutFile = join(outputDir, 'stdout.log');
|
|
443
|
+
writePromises.push(writeFile(stdoutFile, stdout, 'utf-8'));
|
|
444
|
+
}
|
|
445
|
+
// Write stderr.log (only if non-empty)
|
|
446
|
+
let stderrFile;
|
|
447
|
+
if (stderr.trim()) {
|
|
448
|
+
stderrFile = join(outputDir, 'stderr.log');
|
|
449
|
+
writePromises.push(writeFile(stderrFile, stderr, 'utf-8'));
|
|
450
|
+
}
|
|
451
|
+
// Write combined.jsonl (always)
|
|
452
|
+
const combinedFile = join(outputDir, 'combined.jsonl');
|
|
453
|
+
const combinedContent = combinedLines
|
|
454
|
+
// eslint-disable-next-line sonarjs/no-nested-functions -- Array.map callback is standard functional programming pattern
|
|
455
|
+
.map(line => JSON.stringify(line))
|
|
456
|
+
.join('\n');
|
|
457
|
+
writePromises.push(writeFile(combinedFile, combinedContent, 'utf-8'));
|
|
458
|
+
// Wait for all writes to complete
|
|
459
|
+
await Promise.all(writePromises);
|
|
460
|
+
const result = {
|
|
461
|
+
command: commandString,
|
|
462
|
+
exitCode,
|
|
463
|
+
durationSecs,
|
|
464
|
+
timestamp,
|
|
465
|
+
treeHash: fallbackHash,
|
|
466
|
+
...(extraction ? { extraction } : {}), // Only include extraction if needed
|
|
467
|
+
outputFiles: {
|
|
468
|
+
...(stdoutFile ? { stdout: stdoutFile } : {}),
|
|
469
|
+
...(stderrFile ? { stderr: stderrFile } : {}),
|
|
470
|
+
combined: combinedFile,
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
resolve({ result, context: { preamble: '', stderr: '' } });
|
|
474
|
+
});
|
|
112
475
|
});
|
|
113
476
|
// Handle spawn errors (e.g., command not found)
|
|
114
477
|
child.on('error', (error) => {
|
|
@@ -125,188 +488,146 @@ async function executeAndExtract(commandString) {
|
|
|
125
488
|
* - "pnpm lint" → "lint"
|
|
126
489
|
* - "pnpm --filter @pkg test" → "test"
|
|
127
490
|
*/
|
|
128
|
-
function inferStepName(commandString) {
|
|
129
|
-
const lower = commandString.toLowerCase();
|
|
130
|
-
// TypeScript/tsc
|
|
131
|
-
if (lower.includes('tsc') || lower.includes('typecheck')) {
|
|
132
|
-
return 'typecheck';
|
|
133
|
-
}
|
|
134
|
-
// Linting
|
|
135
|
-
if (lower.includes('eslint') || lower.includes('lint')) {
|
|
136
|
-
return 'lint';
|
|
137
|
-
}
|
|
138
|
-
// Testing (vitest, jest, mocha, etc.)
|
|
139
|
-
if (lower.includes('vitest') || lower.includes('jest') ||
|
|
140
|
-
lower.includes('mocha') || lower.includes('test') ||
|
|
141
|
-
lower.includes('jasmine')) {
|
|
142
|
-
return 'test';
|
|
143
|
-
}
|
|
144
|
-
// OpenAPI
|
|
145
|
-
if (lower.includes('openapi')) {
|
|
146
|
-
return 'openapi';
|
|
147
|
-
}
|
|
148
|
-
// Generic fallback
|
|
149
|
-
return 'run';
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Check if output contains YAML format (may have preamble before ---)
|
|
153
|
-
*
|
|
154
|
-
* IMPORTANT: This function detects YAML anywhere in the output, not just at the start.
|
|
155
|
-
* This allows us to handle package manager preambles (pnpm, npm, yarn) that appear
|
|
156
|
-
* before the actual YAML content.
|
|
157
|
-
*
|
|
158
|
-
* Example with preamble:
|
|
159
|
-
* ```
|
|
160
|
-
* > vibe-validate@0.13.0 validate
|
|
161
|
-
* > node packages/cli/dist/bin.js validate
|
|
162
|
-
*
|
|
163
|
-
* ---
|
|
164
|
-
* command: "npm test"
|
|
165
|
-
* exitCode: 0
|
|
166
|
-
* ```
|
|
167
|
-
*
|
|
168
|
-
* The preamble will be extracted and routed to stderr, keeping stdout clean.
|
|
169
|
-
*/
|
|
170
|
-
function isYamlOutput(output) {
|
|
171
|
-
const trimmed = output.trim();
|
|
172
|
-
// Check if starts with --- (no preamble)
|
|
173
|
-
if (trimmed.startsWith('---\n') || trimmed.startsWith('---\r\n')) {
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
// Check if contains --- with newlines (has preamble)
|
|
177
|
-
return output.includes('\n---\n') || output.includes('\n---\r\n');
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Extract YAML content and separate preamble/postamble
|
|
181
|
-
*
|
|
182
|
-
* STREAM ROUTING STRATEGY:
|
|
183
|
-
* - stdout (returned 'yaml'): Clean YAML for piping and LLM consumption
|
|
184
|
-
* - stderr (returned 'preamble'): Package manager noise, preserved for human context
|
|
185
|
-
*
|
|
186
|
-
* This separation follows Unix philosophy: stdout = data, stderr = human messages.
|
|
187
|
-
*
|
|
188
|
-
* Example input:
|
|
189
|
-
* ```
|
|
190
|
-
* > package@1.0.0 test ← preamble (goes to stderr)
|
|
191
|
-
* > vitest run ← preamble (goes to stderr)
|
|
192
|
-
*
|
|
193
|
-
* --- ← yaml (goes to stdout)
|
|
194
|
-
* command: "vitest run"
|
|
195
|
-
* exitCode: 0
|
|
196
|
-
* extraction: {...}
|
|
197
|
-
* ```
|
|
198
|
-
*
|
|
199
|
-
* Benefits:
|
|
200
|
-
* 1. `run "pnpm test" > file.yaml` writes pure YAML
|
|
201
|
-
* 2. `run "pnpm test" 2>/dev/null` suppresses noise
|
|
202
|
-
* 3. Terminal shows both streams (full context)
|
|
203
|
-
*
|
|
204
|
-
* @param stdout - Raw stdout from the executed command
|
|
205
|
-
* @returns Object with separated yaml, preamble, and postamble
|
|
206
|
-
*/
|
|
207
|
-
function extractYamlAndPreamble(stdout) {
|
|
208
|
-
// Check if it starts with --- (no preamble)
|
|
209
|
-
const trimmed = stdout.trim();
|
|
210
|
-
if (trimmed.startsWith('---\n') || trimmed.startsWith('---\r\n')) {
|
|
211
|
-
return { yaml: trimmed, preamble: '', postamble: '' };
|
|
212
|
-
}
|
|
213
|
-
// Find first YAML separator with newline before it
|
|
214
|
-
const patterns = [
|
|
215
|
-
{ pattern: '\n---\n', offset: 1 },
|
|
216
|
-
{ pattern: '\n---\r\n', offset: 1 },
|
|
217
|
-
];
|
|
218
|
-
let earliestIndex = -1;
|
|
219
|
-
let selectedOffset = 0;
|
|
220
|
-
for (const { pattern, offset } of patterns) {
|
|
221
|
-
const idx = stdout.indexOf(pattern);
|
|
222
|
-
if (idx !== -1 && (earliestIndex === -1 || idx < earliestIndex)) {
|
|
223
|
-
earliestIndex = idx;
|
|
224
|
-
selectedOffset = offset;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (earliestIndex === -1) {
|
|
228
|
-
// No YAML found
|
|
229
|
-
return { yaml: '', preamble: stdout, postamble: '' };
|
|
230
|
-
}
|
|
231
|
-
// Extract preamble (everything before ---)
|
|
232
|
-
const preamble = stdout.substring(0, earliestIndex).trim();
|
|
233
|
-
// Extract YAML content (from --- onward)
|
|
234
|
-
const yamlContent = stdout.substring(earliestIndex + selectedOffset).trim();
|
|
235
|
-
return { yaml: yamlContent, preamble, postamble: '' };
|
|
236
|
-
}
|
|
237
491
|
/**
|
|
238
492
|
* Merge nested YAML output with outer run metadata
|
|
239
493
|
*
|
|
240
494
|
* When vibe-validate run wraps another vibe-validate command (run or validate),
|
|
241
495
|
* we merge the inner YAML with outer metadata instead of double-extracting.
|
|
242
496
|
*/
|
|
243
|
-
function mergeNestedYaml(outerCommand, yamlOutput, outerExitCode) {
|
|
497
|
+
function mergeNestedYaml(outerCommand, yamlOutput, outerExitCode, outerDurationSecs) {
|
|
244
498
|
try {
|
|
245
|
-
//
|
|
499
|
+
// Always parse the raw YAML first to preserve all fields
|
|
246
500
|
const innerResult = yaml.parse(yamlOutput);
|
|
247
|
-
//
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
501
|
+
// Try parsing as vibe-validate output using shared parser
|
|
502
|
+
const parsed = parseVibeValidateOutput(yamlOutput);
|
|
503
|
+
if (parsed) {
|
|
504
|
+
// Successfully parsed nested vibe-validate output
|
|
505
|
+
// Spread all inner fields to preserve custom fields (rawOutput, customField, etc.)
|
|
506
|
+
// Then override with outer metadata and parsed information
|
|
507
|
+
// Use command from inner result (already unwrapped by nested vibe-validate call)
|
|
508
|
+
const unwrappedCommand = innerResult.command ?? outerCommand;
|
|
509
|
+
return {
|
|
510
|
+
...innerResult, // Preserve ALL inner fields
|
|
511
|
+
command: unwrappedCommand, // Use unwrapped command (e.g., "eslint ..." instead of "pnpm lint")
|
|
512
|
+
exitCode: outerExitCode, // Override with outer exit code
|
|
513
|
+
durationSecs: outerDurationSecs, // Override with outer duration
|
|
514
|
+
timestamp: parsed.timestamp ?? innerResult.timestamp ?? new Date().toISOString(),
|
|
515
|
+
treeHash: parsed.treeHash ?? innerResult.treeHash ?? 'unknown',
|
|
516
|
+
extraction: parsed.extraction, // Use parsed extraction
|
|
517
|
+
...(parsed.isCachedResult !== undefined ? { isCachedResult: parsed.isCachedResult } : {}),
|
|
518
|
+
...(parsed.fullOutputFile ? { fullOutputFile: parsed.fullOutputFile } : {}),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
// Not a recognized vibe-validate format - use inner command if available
|
|
522
|
+
// This handles cases where the wrapper executes a command that produces non-YAML output
|
|
523
|
+
const unwrappedCommand = (innerResult.command && typeof innerResult.command === 'string')
|
|
524
|
+
? innerResult.command
|
|
525
|
+
: outerCommand;
|
|
526
|
+
return {
|
|
527
|
+
...innerResult,
|
|
528
|
+
command: unwrappedCommand, // Use inner command (unwrapped)
|
|
529
|
+
exitCode: outerExitCode,
|
|
530
|
+
durationSecs: outerDurationSecs,
|
|
531
|
+
treeHash: innerResult.treeHash ?? 'unknown', // Use inner treeHash or fallback to unknown
|
|
255
532
|
};
|
|
256
|
-
return mergedResult;
|
|
257
533
|
}
|
|
258
534
|
catch (error) {
|
|
259
|
-
//
|
|
535
|
+
// YAML parsing completely failed - treat as regular output
|
|
260
536
|
console.error('Warning: Failed to parse nested YAML output:', error);
|
|
261
|
-
|
|
262
|
-
const
|
|
537
|
+
// Token optimization: Only extract when exitCode !== 0 OR there are actual errors
|
|
538
|
+
const rawExtraction = autoDetectAndExtract(yamlOutput);
|
|
539
|
+
const extraction = (outerExitCode !== 0 || rawExtraction.totalErrors > 0) ? rawExtraction : undefined;
|
|
263
540
|
return {
|
|
264
541
|
command: outerCommand,
|
|
265
542
|
exitCode: outerExitCode,
|
|
266
|
-
|
|
267
|
-
|
|
543
|
+
durationSecs: outerDurationSecs,
|
|
544
|
+
timestamp: new Date().toISOString(),
|
|
545
|
+
treeHash: 'unknown',
|
|
546
|
+
...(extraction ? { extraction } : {}), // Only include extraction if needed
|
|
268
547
|
};
|
|
269
548
|
}
|
|
270
549
|
}
|
|
271
|
-
/**
|
|
272
|
-
* Extract the innermost command from nested run results
|
|
273
|
-
*
|
|
274
|
-
* Examples:
|
|
275
|
-
* - { command: "npm test" } → "npm test"
|
|
276
|
-
* - { command: "...", suggestedDirectCommand: "npm test" } → "npm test"
|
|
277
|
-
* - { command: "vibe-validate validate" } → "vibe-validate validate"
|
|
278
|
-
*/
|
|
279
|
-
function extractInnermostCommand(result) {
|
|
280
|
-
// If already has suggestedDirectCommand, use it (handles 3+ levels)
|
|
281
|
-
if (result.suggestedDirectCommand && typeof result.suggestedDirectCommand === 'string') {
|
|
282
|
-
return result.suggestedDirectCommand;
|
|
283
|
-
}
|
|
284
|
-
// Otherwise, use the command from the inner result
|
|
285
|
-
if (result.command && typeof result.command === 'string') {
|
|
286
|
-
return result.command;
|
|
287
|
-
}
|
|
288
|
-
return 'unknown';
|
|
289
|
-
}
|
|
290
550
|
/**
|
|
291
551
|
* Show verbose help with detailed documentation
|
|
292
552
|
*/
|
|
293
553
|
export function showRunVerboseHelp() {
|
|
294
554
|
console.log(`# run Command Reference
|
|
295
555
|
|
|
296
|
-
> Run a command and extract LLM-friendly errors
|
|
556
|
+
> Run a command and extract LLM-friendly errors (with smart caching)
|
|
297
557
|
|
|
298
558
|
## Overview
|
|
299
559
|
|
|
300
560
|
The \`run\` command executes any shell command and extracts errors using vibe-validate's smart extractors. This provides concise, structured error information to save AI agent context windows.
|
|
301
561
|
|
|
562
|
+
**NEW in v0.15.0**: Automatic caching based on git tree hash - repeat commands are instant (<200ms) when code hasn't changed.
|
|
563
|
+
|
|
302
564
|
## How It Works
|
|
303
565
|
|
|
304
|
-
1. **
|
|
305
|
-
2. **
|
|
306
|
-
3. **
|
|
307
|
-
4. **
|
|
308
|
-
5. **
|
|
309
|
-
6. **
|
|
566
|
+
1. **Checks cache** - If git tree unchanged, returns cached result instantly
|
|
567
|
+
2. **Executes command** (on cache miss) in a shell subprocess
|
|
568
|
+
3. **Captures output** (stdout + stderr)
|
|
569
|
+
4. **Auto-detects format** (vitest, jest, tsc, eslint, etc.)
|
|
570
|
+
5. **Extracts errors** using appropriate extractor
|
|
571
|
+
6. **Stores in cache** - Future runs with same tree hash are instant
|
|
572
|
+
7. **Outputs YAML** with structured error information
|
|
573
|
+
8. **Passes through exit code** from original command
|
|
574
|
+
|
|
575
|
+
## Caching
|
|
576
|
+
|
|
577
|
+
The \`run\` command automatically caches results based on:
|
|
578
|
+
- **Git tree hash** - Content-based identifier (same code = same hash)
|
|
579
|
+
- **Command string** - Different commands have separate caches
|
|
580
|
+
- **Working directory** - Subdirectory runs are tracked separately
|
|
581
|
+
|
|
582
|
+
### Cache Behavior
|
|
583
|
+
|
|
584
|
+
**First run** (cache miss):
|
|
585
|
+
\`\`\`bash
|
|
586
|
+
$ vibe-validate run "pnpm test"
|
|
587
|
+
# Executes test suite, extracts errors, stores in cache
|
|
588
|
+
# Duration: ~30 seconds
|
|
589
|
+
\`\`\`
|
|
590
|
+
|
|
591
|
+
**Repeat run** (cache hit):
|
|
592
|
+
\`\`\`bash
|
|
593
|
+
$ vibe-validate run "pnpm test"
|
|
594
|
+
# Returns cached result instantly (no execution)
|
|
595
|
+
# Duration: <200ms
|
|
596
|
+
\`\`\`
|
|
597
|
+
|
|
598
|
+
**After code change**:
|
|
599
|
+
\`\`\`bash
|
|
600
|
+
# Edit a file, tree hash changes
|
|
601
|
+
$ vibe-validate run "pnpm test"
|
|
602
|
+
# Cache miss - executes and caches with new tree hash
|
|
603
|
+
\`\`\`
|
|
604
|
+
|
|
605
|
+
### Cache Flags
|
|
606
|
+
|
|
607
|
+
**Check cache status** (--check):
|
|
608
|
+
\`\`\`bash
|
|
609
|
+
$ vibe-validate run --check "pnpm test"
|
|
610
|
+
# Exit 0 if cached (outputs cached result)
|
|
611
|
+
# Exit 1 if not cached (no execution)
|
|
612
|
+
\`\`\`
|
|
613
|
+
|
|
614
|
+
**Force execution** (--force):
|
|
615
|
+
\`\`\`bash
|
|
616
|
+
$ vibe-validate run --force "pnpm test"
|
|
617
|
+
# Always executes, updates cache (ignores existing cache)
|
|
618
|
+
# Useful for flaky tests or time-sensitive commands
|
|
619
|
+
\`\`\`
|
|
620
|
+
|
|
621
|
+
### Cache Storage
|
|
622
|
+
|
|
623
|
+
Cache is stored in git notes at:
|
|
624
|
+
\`\`\`
|
|
625
|
+
refs/notes/vibe-validate/run/{treeHash}/{encodedCommand}
|
|
626
|
+
\`\`\`
|
|
627
|
+
|
|
628
|
+
- **Local only** - Not pushed with git (each dev has their own cache)
|
|
629
|
+
- **Automatic cleanup** - Run \`vibe-validate doctor\` to check cache health
|
|
630
|
+
- **No configuration required** - Works out of the box
|
|
310
631
|
|
|
311
632
|
## Use Cases
|
|
312
633
|
|
|
@@ -316,29 +637,33 @@ Instead of parsing verbose test output:
|
|
|
316
637
|
# Verbose (wastes context window)
|
|
317
638
|
npx vitest packages/extractors/test/vitest-extractor.test.ts
|
|
318
639
|
|
|
319
|
-
# Concise (LLM-friendly)
|
|
320
|
-
vibe-validate run
|
|
640
|
+
# Concise (LLM-friendly) - NEW: No quotes needed!
|
|
641
|
+
vibe-validate run npx vitest packages/extractors/test/vitest-extractor.test.ts
|
|
321
642
|
\`\`\`
|
|
322
643
|
|
|
323
644
|
### Debugging Specific Tests
|
|
324
645
|
\`\`\`bash
|
|
325
|
-
# Run single test file with extraction
|
|
326
|
-
vibe-validate run
|
|
646
|
+
# Run single test file with extraction (NEW: natural syntax)
|
|
647
|
+
vibe-validate run npx vitest -t 'should extract failed tests'
|
|
327
648
|
|
|
328
649
|
# Run package tests with extraction
|
|
329
|
-
vibe-validate run
|
|
650
|
+
vibe-validate run pnpm --filter @vibe-validate/extractors test
|
|
651
|
+
|
|
652
|
+
# Quoted syntax still works for compatibility
|
|
653
|
+
vibe-validate run "npx vitest test.ts"
|
|
330
654
|
\`\`\`
|
|
331
655
|
|
|
332
656
|
### Type Checking
|
|
333
657
|
\`\`\`bash
|
|
334
658
|
# Extract TypeScript errors
|
|
335
|
-
vibe-validate run
|
|
659
|
+
vibe-validate run npx tsc --noEmit
|
|
336
660
|
\`\`\`
|
|
337
661
|
|
|
338
662
|
### Linting
|
|
339
663
|
\`\`\`bash
|
|
340
|
-
# Extract ESLint errors
|
|
341
|
-
vibe-validate run
|
|
664
|
+
# Extract ESLint errors (options pass through correctly)
|
|
665
|
+
vibe-validate run pnpm lint
|
|
666
|
+
vibe-validate run eslint --max-warnings 0 src/
|
|
342
667
|
\`\`\`
|
|
343
668
|
|
|
344
669
|
## Output Format
|
|
@@ -355,7 +680,7 @@ extraction:
|
|
|
355
680
|
message: "expected 5 to equal 3"
|
|
356
681
|
summary: "1 test failed"
|
|
357
682
|
guidance: "Review test assertions and expected values"
|
|
358
|
-
|
|
683
|
+
errorSummary: |
|
|
359
684
|
test.ts:42 - expected 5 to equal 3
|
|
360
685
|
rawOutput: "... (truncated)"
|
|
361
686
|
\`\`\`
|
|
@@ -403,28 +728,27 @@ The \`run\` command automatically detects and handles package manager preambles:
|
|
|
403
728
|
|
|
404
729
|
This means you can safely use:
|
|
405
730
|
\`\`\`bash
|
|
406
|
-
vibe-validate run
|
|
407
|
-
vibe-validate run
|
|
408
|
-
vibe-validate run
|
|
731
|
+
vibe-validate run pnpm validate --yaml # Works!
|
|
732
|
+
vibe-validate run npm test # Works!
|
|
733
|
+
vibe-validate run yarn build # Works!
|
|
409
734
|
\`\`\`
|
|
410
735
|
|
|
411
736
|
The YAML output on stdout remains clean and parseable, while the preamble is preserved on stderr for debugging.
|
|
412
737
|
|
|
413
738
|
## Nested Run Detection
|
|
414
739
|
|
|
415
|
-
When \`run\` wraps another vibe-validate command that outputs YAML, it
|
|
740
|
+
When \`run\` wraps another vibe-validate command that outputs YAML, it automatically unwraps to show the actual command:
|
|
416
741
|
|
|
417
742
|
\`\`\`bash
|
|
418
743
|
# 2-level nesting
|
|
419
744
|
$ vibe-validate run "vibe-validate run 'npm test'"
|
|
420
745
|
---
|
|
421
|
-
command:
|
|
746
|
+
command: npm test # ← Automatically unwrapped!
|
|
422
747
|
exitCode: 0
|
|
423
748
|
extraction: {...}
|
|
424
|
-
suggestedDirectCommand: npm test # ← Unwrapped!
|
|
425
749
|
\`\`\`
|
|
426
750
|
|
|
427
|
-
The \`
|
|
751
|
+
The \`command\` field shows the innermost command that actually executed, helping you avoid unnecessary nesting.
|
|
428
752
|
|
|
429
753
|
## Exit Codes
|
|
430
754
|
|
|
@@ -434,29 +758,40 @@ The \`run\` command passes through the exit code from the executed command:
|
|
|
434
758
|
|
|
435
759
|
## Examples
|
|
436
760
|
|
|
437
|
-
###
|
|
761
|
+
### Python Testing
|
|
438
762
|
\`\`\`bash
|
|
439
|
-
|
|
763
|
+
vv run pytest tests/ --cov=src
|
|
764
|
+
vv run pytest -k test_auth --verbose
|
|
765
|
+
vv run python -m unittest discover
|
|
440
766
|
\`\`\`
|
|
441
767
|
|
|
442
|
-
###
|
|
768
|
+
### Rust Testing
|
|
443
769
|
\`\`\`bash
|
|
444
|
-
|
|
770
|
+
vv run cargo test
|
|
771
|
+
vv run cargo test --all-features
|
|
772
|
+
vv run cargo clippy -- -D warnings
|
|
445
773
|
\`\`\`
|
|
446
774
|
|
|
447
|
-
###
|
|
775
|
+
### Go Testing
|
|
448
776
|
\`\`\`bash
|
|
449
|
-
|
|
777
|
+
vv run go test ./...
|
|
778
|
+
vv run go test -v -race ./pkg/...
|
|
779
|
+
vv run go vet ./...
|
|
450
780
|
\`\`\`
|
|
451
781
|
|
|
452
|
-
###
|
|
782
|
+
### Ruby Testing
|
|
453
783
|
\`\`\`bash
|
|
454
|
-
|
|
784
|
+
vv run bundle exec rspec
|
|
785
|
+
vv run bundle exec rspec spec/models/
|
|
786
|
+
vv run bundle exec rubocop
|
|
455
787
|
\`\`\`
|
|
456
788
|
|
|
457
|
-
###
|
|
789
|
+
### Node.js/TypeScript
|
|
458
790
|
\`\`\`bash
|
|
459
|
-
|
|
791
|
+
vv run npm test
|
|
792
|
+
vv run npx vitest packages/cli/test/commands/run.test.ts
|
|
793
|
+
vv run npx tsc --noEmit
|
|
794
|
+
vv run pnpm lint
|
|
460
795
|
\`\`\`
|
|
461
796
|
|
|
462
797
|
## Supported Extractors
|
|
@@ -505,4 +840,73 @@ extraction:
|
|
|
505
840
|
**Result**: Same information, 90% smaller!
|
|
506
841
|
`);
|
|
507
842
|
}
|
|
843
|
+
/**
|
|
844
|
+
* Show help for the run command
|
|
845
|
+
*/
|
|
846
|
+
function showRunHelp() {
|
|
847
|
+
console.log(`
|
|
848
|
+
Usage: vibe-validate run [options] <command...>
|
|
849
|
+
|
|
850
|
+
Run a command and extract LLM-friendly errors (with smart caching)
|
|
851
|
+
|
|
852
|
+
Arguments:
|
|
853
|
+
command... Command to execute (multiple words supported)
|
|
854
|
+
|
|
855
|
+
Options:
|
|
856
|
+
--check Check if cached result exists without executing
|
|
857
|
+
--force Force execution and update cache (bypass cache read)
|
|
858
|
+
--head <lines> Display first N lines of output after YAML (on stderr)
|
|
859
|
+
--tail <lines> Display last N lines of output after YAML (on stderr)
|
|
860
|
+
--verbose Display all output after YAML (on stderr)
|
|
861
|
+
-h, --help Display this help message
|
|
862
|
+
|
|
863
|
+
Examples:
|
|
864
|
+
vv run pytest tests/ --cov=src # Python
|
|
865
|
+
vv run cargo test --all-features # Rust
|
|
866
|
+
vv run go test ./... # Go
|
|
867
|
+
vv run npm test # Node.js
|
|
868
|
+
vv run --verbose npm test # With output display
|
|
869
|
+
|
|
870
|
+
For detailed documentation, use: vibe-validate run --help --verbose
|
|
871
|
+
`.trim());
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Display command output based on --head, --tail, or --verbose flags
|
|
875
|
+
*/
|
|
876
|
+
async function displayCommandOutput(result, options) {
|
|
877
|
+
if (!result.outputFiles?.combined) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
const combinedContent = await readFile(result.outputFiles.combined, 'utf-8');
|
|
882
|
+
const lines = combinedContent.trim().split('\n');
|
|
883
|
+
const outputLines = lines
|
|
884
|
+
.filter(line => line.trim())
|
|
885
|
+
.map(line => JSON.parse(line));
|
|
886
|
+
let linesToDisplay;
|
|
887
|
+
if (options.verbose) {
|
|
888
|
+
linesToDisplay = outputLines;
|
|
889
|
+
}
|
|
890
|
+
else if (options.head) {
|
|
891
|
+
linesToDisplay = outputLines.slice(0, options.head);
|
|
892
|
+
}
|
|
893
|
+
else if (options.tail) {
|
|
894
|
+
linesToDisplay = outputLines.slice(-options.tail);
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
// Display lines with formatting (no timestamp - available in JSONL if needed)
|
|
900
|
+
// No header - YAML front matter delimiter serves as separator
|
|
901
|
+
for (const line of linesToDisplay) {
|
|
902
|
+
const streamColor = line.stream === 'stdout' ? chalk.gray : chalk.yellow;
|
|
903
|
+
const stream = streamColor(`[${line.stream}]`);
|
|
904
|
+
process.stderr.write(`${stream} ${line.line}\n`);
|
|
905
|
+
}
|
|
906
|
+
// eslint-disable-next-line sonarjs/no-ignored-exceptions -- Display errors are non-critical, YAML already written
|
|
907
|
+
}
|
|
908
|
+
catch (_err) {
|
|
909
|
+
// Silently ignore errors in display - YAML output is already written
|
|
910
|
+
}
|
|
911
|
+
}
|
|
508
912
|
//# sourceMappingURL=run.js.map
|