@vibe-validate/cli 0.14.2 → 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 +41 -21
- 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 +135 -16
- package/dist/commands/cleanup.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +13 -10
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +253 -189
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-workflow.d.ts.map +1 -1
- package/dist/commands/generate-workflow.js +15 -13
- package/dist/commands/generate-workflow.js.map +1 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +305 -98
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pre-commit.d.ts.map +1 -1
- package/dist/commands/pre-commit.js +8 -3
- package/dist/commands/pre-commit.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +620 -217
- 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 +7 -7
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/watch-pr.d.ts.map +1 -1
- package/dist/commands/watch-pr.js +73 -49
- 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 +431 -33
- 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/scripts/generate-watch-pr-schema.js +3 -3
- package/dist/scripts/generate-watch-pr-schema.js.map +1 -1
- 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 +21 -0
- package/dist/services/ci-providers/github-actions.d.ts.map +1 -1
- package/dist/services/ci-providers/github-actions.js +65 -49
- package/dist/services/ci-providers/github-actions.js.map +1 -1
- package/dist/utils/check-validation.d.ts.map +1 -1
- package/dist/utils/check-validation.js +9 -5
- package/dist/utils/check-validation.js.map +1 -1
- package/dist/utils/config-error-reporter.js +9 -7
- package/dist/utils/config-error-reporter.js.map +1 -1
- package/dist/utils/config-loader.js +5 -5
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/git-detection.d.ts +0 -22
- package/dist/utils/git-detection.d.ts.map +1 -1
- package/dist/utils/git-detection.js +64 -56
- package/dist/utils/git-detection.js.map +1 -1
- package/dist/utils/pid-lock.js +7 -7
- 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 +8 -6
- package/dist/utils/project-id.js.map +1 -1
- package/dist/utils/runner-adapter.js +4 -3
- package/dist/utils/runner-adapter.js.map +1 -1
- package/dist/utils/setup-checks/hooks-check.js +3 -3
- package/dist/utils/setup-checks/hooks-check.js.map +1 -1
- package/dist/utils/setup-checks/workflow-check.js +3 -3
- package/dist/utils/setup-checks/workflow-check.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/template-discovery.d.ts.map +1 -1
- package/dist/utils/template-discovery.js +5 -4
- package/dist/utils/template-discovery.js.map +1 -1
- package/dist/utils/validate-workflow.d.ts.map +1 -1
- package/dist/utils/validate-workflow.js +169 -150
- 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,39 +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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
330
|
+
child.stdout.on('data', (data) => {
|
|
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
345
|
// Capture stderr
|
|
78
|
-
|
|
79
|
-
|
|
346
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- spawnCommand always pipes stdout/stderr
|
|
347
|
+
child.stderr.on('data', (data) => {
|
|
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
|
-
child.on('close', (exitCode) => {
|
|
83
|
-
const
|
|
363
|
+
child.on('close', (exitCode = 1) => {
|
|
364
|
+
const durationSecs = (Date.now() - startTime) / 1000;
|
|
84
365
|
// CRITICAL: Check ONLY stdout for YAML (not stderr)
|
|
85
366
|
// This prevents stderr warnings from corrupting nested YAML output
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const mergedResult = mergeNestedYaml(commandString, yaml,
|
|
367
|
+
const yamlResult = extractYamlWithPreamble(stdout);
|
|
368
|
+
if (yamlResult) {
|
|
369
|
+
const mergedResult = mergeNestedYaml(commandString, yamlResult.yaml, exitCode, durationSecs);
|
|
89
370
|
// Include preamble and stderr for context
|
|
90
371
|
const contextOutput = {
|
|
91
|
-
preamble: preamble
|
|
372
|
+
preamble: yamlResult.preamble,
|
|
92
373
|
stderr: stderr.trim(),
|
|
93
374
|
};
|
|
94
375
|
resolve({ result: mergedResult, context: contextOutput });
|
|
@@ -96,20 +377,101 @@ async function executeAndExtract(commandString) {
|
|
|
96
377
|
}
|
|
97
378
|
// For extraction, combine both streams (stderr has useful error context)
|
|
98
379
|
const combinedOutput = stdout + stderr;
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
});
|
|
113
475
|
});
|
|
114
476
|
// Handle spawn errors (e.g., command not found)
|
|
115
477
|
child.on('error', (error) => {
|
|
@@ -126,188 +488,146 @@ async function executeAndExtract(commandString) {
|
|
|
126
488
|
* - "pnpm lint" → "lint"
|
|
127
489
|
* - "pnpm --filter @pkg test" → "test"
|
|
128
490
|
*/
|
|
129
|
-
function inferStepName(commandString) {
|
|
130
|
-
const lower = commandString.toLowerCase();
|
|
131
|
-
// TypeScript/tsc
|
|
132
|
-
if (lower.includes('tsc') || lower.includes('typecheck')) {
|
|
133
|
-
return 'typecheck';
|
|
134
|
-
}
|
|
135
|
-
// Linting
|
|
136
|
-
if (lower.includes('eslint') || lower.includes('lint')) {
|
|
137
|
-
return 'lint';
|
|
138
|
-
}
|
|
139
|
-
// Testing (vitest, jest, mocha, etc.)
|
|
140
|
-
if (lower.includes('vitest') || lower.includes('jest') ||
|
|
141
|
-
lower.includes('mocha') || lower.includes('test') ||
|
|
142
|
-
lower.includes('jasmine')) {
|
|
143
|
-
return 'test';
|
|
144
|
-
}
|
|
145
|
-
// OpenAPI
|
|
146
|
-
if (lower.includes('openapi')) {
|
|
147
|
-
return 'openapi';
|
|
148
|
-
}
|
|
149
|
-
// Generic fallback
|
|
150
|
-
return 'run';
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Check if output contains YAML format (may have preamble before ---)
|
|
154
|
-
*
|
|
155
|
-
* IMPORTANT: This function detects YAML anywhere in the output, not just at the start.
|
|
156
|
-
* This allows us to handle package manager preambles (pnpm, npm, yarn) that appear
|
|
157
|
-
* before the actual YAML content.
|
|
158
|
-
*
|
|
159
|
-
* Example with preamble:
|
|
160
|
-
* ```
|
|
161
|
-
* > vibe-validate@0.13.0 validate
|
|
162
|
-
* > node packages/cli/dist/bin.js validate
|
|
163
|
-
*
|
|
164
|
-
* ---
|
|
165
|
-
* command: "npm test"
|
|
166
|
-
* exitCode: 0
|
|
167
|
-
* ```
|
|
168
|
-
*
|
|
169
|
-
* The preamble will be extracted and routed to stderr, keeping stdout clean.
|
|
170
|
-
*/
|
|
171
|
-
function isYamlOutput(output) {
|
|
172
|
-
const trimmed = output.trim();
|
|
173
|
-
// Check if starts with --- (no preamble)
|
|
174
|
-
if (trimmed.startsWith('---\n') || trimmed.startsWith('---\r\n')) {
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
// Check if contains --- with newlines (has preamble)
|
|
178
|
-
return output.includes('\n---\n') || output.includes('\n---\r\n');
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Extract YAML content and separate preamble/postamble
|
|
182
|
-
*
|
|
183
|
-
* STREAM ROUTING STRATEGY:
|
|
184
|
-
* - stdout (returned 'yaml'): Clean YAML for piping and LLM consumption
|
|
185
|
-
* - stderr (returned 'preamble'): Package manager noise, preserved for human context
|
|
186
|
-
*
|
|
187
|
-
* This separation follows Unix philosophy: stdout = data, stderr = human messages.
|
|
188
|
-
*
|
|
189
|
-
* Example input:
|
|
190
|
-
* ```
|
|
191
|
-
* > package@1.0.0 test ← preamble (goes to stderr)
|
|
192
|
-
* > vitest run ← preamble (goes to stderr)
|
|
193
|
-
*
|
|
194
|
-
* --- ← yaml (goes to stdout)
|
|
195
|
-
* command: "vitest run"
|
|
196
|
-
* exitCode: 0
|
|
197
|
-
* extraction: {...}
|
|
198
|
-
* ```
|
|
199
|
-
*
|
|
200
|
-
* Benefits:
|
|
201
|
-
* 1. `run "pnpm test" > file.yaml` writes pure YAML
|
|
202
|
-
* 2. `run "pnpm test" 2>/dev/null` suppresses noise
|
|
203
|
-
* 3. Terminal shows both streams (full context)
|
|
204
|
-
*
|
|
205
|
-
* @param stdout - Raw stdout from the executed command
|
|
206
|
-
* @returns Object with separated yaml, preamble, and postamble
|
|
207
|
-
*/
|
|
208
|
-
function extractYamlAndPreamble(stdout) {
|
|
209
|
-
// Check if it starts with --- (no preamble)
|
|
210
|
-
const trimmed = stdout.trim();
|
|
211
|
-
if (trimmed.startsWith('---\n') || trimmed.startsWith('---\r\n')) {
|
|
212
|
-
return { yaml: trimmed, preamble: '', postamble: '' };
|
|
213
|
-
}
|
|
214
|
-
// Find first YAML separator with newline before it
|
|
215
|
-
const patterns = [
|
|
216
|
-
{ pattern: '\n---\n', offset: 1 },
|
|
217
|
-
{ pattern: '\n---\r\n', offset: 1 },
|
|
218
|
-
];
|
|
219
|
-
let earliestIndex = -1;
|
|
220
|
-
let selectedOffset = 0;
|
|
221
|
-
for (const { pattern, offset } of patterns) {
|
|
222
|
-
const idx = stdout.indexOf(pattern);
|
|
223
|
-
if (idx !== -1 && (earliestIndex === -1 || idx < earliestIndex)) {
|
|
224
|
-
earliestIndex = idx;
|
|
225
|
-
selectedOffset = offset;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (earliestIndex === -1) {
|
|
229
|
-
// No YAML found
|
|
230
|
-
return { yaml: '', preamble: stdout, postamble: '' };
|
|
231
|
-
}
|
|
232
|
-
// Extract preamble (everything before ---)
|
|
233
|
-
const preamble = stdout.substring(0, earliestIndex).trim();
|
|
234
|
-
// Extract YAML content (from --- onward)
|
|
235
|
-
const yamlContent = stdout.substring(earliestIndex + selectedOffset).trim();
|
|
236
|
-
return { yaml: yamlContent, preamble, postamble: '' };
|
|
237
|
-
}
|
|
238
491
|
/**
|
|
239
492
|
* Merge nested YAML output with outer run metadata
|
|
240
493
|
*
|
|
241
494
|
* When vibe-validate run wraps another vibe-validate command (run or validate),
|
|
242
495
|
* we merge the inner YAML with outer metadata instead of double-extracting.
|
|
243
496
|
*/
|
|
244
|
-
function mergeNestedYaml(outerCommand, yamlOutput, outerExitCode) {
|
|
497
|
+
function mergeNestedYaml(outerCommand, yamlOutput, outerExitCode, outerDurationSecs) {
|
|
245
498
|
try {
|
|
246
|
-
//
|
|
499
|
+
// Always parse the raw YAML first to preserve all fields
|
|
247
500
|
const innerResult = yaml.parse(yamlOutput);
|
|
248
|
-
//
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
256
532
|
};
|
|
257
|
-
return mergedResult;
|
|
258
533
|
}
|
|
259
534
|
catch (error) {
|
|
260
|
-
//
|
|
535
|
+
// YAML parsing completely failed - treat as regular output
|
|
261
536
|
console.error('Warning: Failed to parse nested YAML output:', error);
|
|
262
|
-
|
|
263
|
-
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;
|
|
264
540
|
return {
|
|
265
541
|
command: outerCommand,
|
|
266
542
|
exitCode: outerExitCode,
|
|
267
|
-
|
|
268
|
-
|
|
543
|
+
durationSecs: outerDurationSecs,
|
|
544
|
+
timestamp: new Date().toISOString(),
|
|
545
|
+
treeHash: 'unknown',
|
|
546
|
+
...(extraction ? { extraction } : {}), // Only include extraction if needed
|
|
269
547
|
};
|
|
270
548
|
}
|
|
271
549
|
}
|
|
272
|
-
/**
|
|
273
|
-
* Extract the innermost command from nested run results
|
|
274
|
-
*
|
|
275
|
-
* Examples:
|
|
276
|
-
* - { command: "npm test" } → "npm test"
|
|
277
|
-
* - { command: "...", suggestedDirectCommand: "npm test" } → "npm test"
|
|
278
|
-
* - { command: "vibe-validate validate" } → "vibe-validate validate"
|
|
279
|
-
*/
|
|
280
|
-
function extractInnermostCommand(result) {
|
|
281
|
-
// If already has suggestedDirectCommand, use it (handles 3+ levels)
|
|
282
|
-
if (result.suggestedDirectCommand && typeof result.suggestedDirectCommand === 'string') {
|
|
283
|
-
return result.suggestedDirectCommand;
|
|
284
|
-
}
|
|
285
|
-
// Otherwise, use the command from the inner result
|
|
286
|
-
if (result.command && typeof result.command === 'string') {
|
|
287
|
-
return result.command;
|
|
288
|
-
}
|
|
289
|
-
return 'unknown';
|
|
290
|
-
}
|
|
291
550
|
/**
|
|
292
551
|
* Show verbose help with detailed documentation
|
|
293
552
|
*/
|
|
294
553
|
export function showRunVerboseHelp() {
|
|
295
554
|
console.log(`# run Command Reference
|
|
296
555
|
|
|
297
|
-
> Run a command and extract LLM-friendly errors
|
|
556
|
+
> Run a command and extract LLM-friendly errors (with smart caching)
|
|
298
557
|
|
|
299
558
|
## Overview
|
|
300
559
|
|
|
301
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.
|
|
302
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
|
+
|
|
303
564
|
## How It Works
|
|
304
565
|
|
|
305
|
-
1. **
|
|
306
|
-
2. **
|
|
307
|
-
3. **
|
|
308
|
-
4. **
|
|
309
|
-
5. **
|
|
310
|
-
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
|
|
311
631
|
|
|
312
632
|
## Use Cases
|
|
313
633
|
|
|
@@ -317,29 +637,33 @@ Instead of parsing verbose test output:
|
|
|
317
637
|
# Verbose (wastes context window)
|
|
318
638
|
npx vitest packages/extractors/test/vitest-extractor.test.ts
|
|
319
639
|
|
|
320
|
-
# Concise (LLM-friendly)
|
|
321
|
-
vibe-validate run
|
|
640
|
+
# Concise (LLM-friendly) - NEW: No quotes needed!
|
|
641
|
+
vibe-validate run npx vitest packages/extractors/test/vitest-extractor.test.ts
|
|
322
642
|
\`\`\`
|
|
323
643
|
|
|
324
644
|
### Debugging Specific Tests
|
|
325
645
|
\`\`\`bash
|
|
326
|
-
# Run single test file with extraction
|
|
327
|
-
vibe-validate run
|
|
646
|
+
# Run single test file with extraction (NEW: natural syntax)
|
|
647
|
+
vibe-validate run npx vitest -t 'should extract failed tests'
|
|
328
648
|
|
|
329
649
|
# Run package tests with extraction
|
|
330
|
-
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"
|
|
331
654
|
\`\`\`
|
|
332
655
|
|
|
333
656
|
### Type Checking
|
|
334
657
|
\`\`\`bash
|
|
335
658
|
# Extract TypeScript errors
|
|
336
|
-
vibe-validate run
|
|
659
|
+
vibe-validate run npx tsc --noEmit
|
|
337
660
|
\`\`\`
|
|
338
661
|
|
|
339
662
|
### Linting
|
|
340
663
|
\`\`\`bash
|
|
341
|
-
# Extract ESLint errors
|
|
342
|
-
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/
|
|
343
667
|
\`\`\`
|
|
344
668
|
|
|
345
669
|
## Output Format
|
|
@@ -356,7 +680,7 @@ extraction:
|
|
|
356
680
|
message: "expected 5 to equal 3"
|
|
357
681
|
summary: "1 test failed"
|
|
358
682
|
guidance: "Review test assertions and expected values"
|
|
359
|
-
|
|
683
|
+
errorSummary: |
|
|
360
684
|
test.ts:42 - expected 5 to equal 3
|
|
361
685
|
rawOutput: "... (truncated)"
|
|
362
686
|
\`\`\`
|
|
@@ -404,28 +728,27 @@ The \`run\` command automatically detects and handles package manager preambles:
|
|
|
404
728
|
|
|
405
729
|
This means you can safely use:
|
|
406
730
|
\`\`\`bash
|
|
407
|
-
vibe-validate run
|
|
408
|
-
vibe-validate run
|
|
409
|
-
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!
|
|
410
734
|
\`\`\`
|
|
411
735
|
|
|
412
736
|
The YAML output on stdout remains clean and parseable, while the preamble is preserved on stderr for debugging.
|
|
413
737
|
|
|
414
738
|
## Nested Run Detection
|
|
415
739
|
|
|
416
|
-
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:
|
|
417
741
|
|
|
418
742
|
\`\`\`bash
|
|
419
743
|
# 2-level nesting
|
|
420
744
|
$ vibe-validate run "vibe-validate run 'npm test'"
|
|
421
745
|
---
|
|
422
|
-
command:
|
|
746
|
+
command: npm test # ← Automatically unwrapped!
|
|
423
747
|
exitCode: 0
|
|
424
748
|
extraction: {...}
|
|
425
|
-
suggestedDirectCommand: npm test # ← Unwrapped!
|
|
426
749
|
\`\`\`
|
|
427
750
|
|
|
428
|
-
The \`
|
|
751
|
+
The \`command\` field shows the innermost command that actually executed, helping you avoid unnecessary nesting.
|
|
429
752
|
|
|
430
753
|
## Exit Codes
|
|
431
754
|
|
|
@@ -435,29 +758,40 @@ The \`run\` command passes through the exit code from the executed command:
|
|
|
435
758
|
|
|
436
759
|
## Examples
|
|
437
760
|
|
|
438
|
-
###
|
|
761
|
+
### Python Testing
|
|
439
762
|
\`\`\`bash
|
|
440
|
-
|
|
763
|
+
vv run pytest tests/ --cov=src
|
|
764
|
+
vv run pytest -k test_auth --verbose
|
|
765
|
+
vv run python -m unittest discover
|
|
441
766
|
\`\`\`
|
|
442
767
|
|
|
443
|
-
###
|
|
768
|
+
### Rust Testing
|
|
444
769
|
\`\`\`bash
|
|
445
|
-
|
|
770
|
+
vv run cargo test
|
|
771
|
+
vv run cargo test --all-features
|
|
772
|
+
vv run cargo clippy -- -D warnings
|
|
446
773
|
\`\`\`
|
|
447
774
|
|
|
448
|
-
###
|
|
775
|
+
### Go Testing
|
|
449
776
|
\`\`\`bash
|
|
450
|
-
|
|
777
|
+
vv run go test ./...
|
|
778
|
+
vv run go test -v -race ./pkg/...
|
|
779
|
+
vv run go vet ./...
|
|
451
780
|
\`\`\`
|
|
452
781
|
|
|
453
|
-
###
|
|
782
|
+
### Ruby Testing
|
|
454
783
|
\`\`\`bash
|
|
455
|
-
|
|
784
|
+
vv run bundle exec rspec
|
|
785
|
+
vv run bundle exec rspec spec/models/
|
|
786
|
+
vv run bundle exec rubocop
|
|
456
787
|
\`\`\`
|
|
457
788
|
|
|
458
|
-
###
|
|
789
|
+
### Node.js/TypeScript
|
|
459
790
|
\`\`\`bash
|
|
460
|
-
|
|
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
|
|
461
795
|
\`\`\`
|
|
462
796
|
|
|
463
797
|
## Supported Extractors
|
|
@@ -506,4 +840,73 @@ extraction:
|
|
|
506
840
|
**Result**: Same information, 90% smaller!
|
|
507
841
|
`);
|
|
508
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
|
+
}
|
|
509
912
|
//# sourceMappingURL=run.js.map
|