@vibe-validate/cli 0.16.0 ā 0.17.0-rc.10
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/config-templates/minimal.yaml +19 -8
- package/config-templates/typescript-library.yaml +19 -8
- package/config-templates/typescript-nodejs.yaml +19 -8
- package/config-templates/typescript-react.yaml +19 -8
- package/dist/bin.js +6 -0
- package/dist/bin.js.map +1 -1
- package/dist/commands/create-extractor.d.ts +13 -0
- package/dist/commands/create-extractor.d.ts.map +1 -0
- package/dist/commands/create-extractor.js +790 -0
- package/dist/commands/create-extractor.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +140 -109
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-workflow.d.ts.map +1 -1
- package/dist/commands/generate-workflow.js +8 -0
- package/dist/commands/generate-workflow.js.map +1 -1
- package/dist/commands/pre-commit.d.ts.map +1 -1
- package/dist/commands/pre-commit.js +128 -56
- package/dist/commands/pre-commit.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +107 -44
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +25 -9
- package/dist/commands/validate.js.map +1 -1
- package/dist/services/ci-providers/github-actions.d.ts.map +1 -1
- package/dist/services/ci-providers/github-actions.js +3 -2
- package/dist/services/ci-providers/github-actions.js.map +1 -1
- package/dist/utils/config-loader.d.ts +26 -3
- package/dist/utils/config-loader.d.ts.map +1 -1
- package/dist/utils/config-loader.js +80 -11
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/git-detection.d.ts.map +1 -1
- package/dist/utils/git-detection.js +18 -18
- package/dist/utils/git-detection.js.map +1 -1
- package/dist/utils/project-id.d.ts +1 -2
- package/dist/utils/project-id.d.ts.map +1 -1
- package/dist/utils/project-id.js +6 -11
- package/dist/utils/project-id.js.map +1 -1
- package/dist/utils/runner-adapter.d.ts.map +1 -1
- package/dist/utils/runner-adapter.js +1 -0
- package/dist/utils/runner-adapter.js.map +1 -1
- package/dist/utils/secret-scanning.d.ts +72 -0
- package/dist/utils/secret-scanning.d.ts.map +1 -0
- package/dist/utils/secret-scanning.js +205 -0
- package/dist/utils/secret-scanning.js.map +1 -0
- package/dist/utils/validate-workflow.d.ts.map +1 -1
- package/dist/utils/validate-workflow.js +9 -1
- package/dist/utils/validate-workflow.js.map +1 -1
- package/package.json +8 -6
|
@@ -9,7 +9,7 @@ import { getRemoteBranch } from '@vibe-validate/config';
|
|
|
9
9
|
import { loadConfig } from '../utils/config-loader.js';
|
|
10
10
|
import { detectContext } from '../utils/context-detector.js';
|
|
11
11
|
import { runValidateWorkflow } from '../utils/validate-workflow.js';
|
|
12
|
-
import {
|
|
12
|
+
import { selectToolsToRun, runSecretScan, showPerformanceWarning, showSecretsDetectedError, formatToolName, hasGitleaksConfig, isGitleaksAvailable, } from '../utils/secret-scanning.js';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
export function preCommitCommand(program) {
|
|
15
15
|
program
|
|
@@ -55,56 +55,50 @@ export function preCommitCommand(program) {
|
|
|
55
55
|
const verbose = options.verbose ?? false;
|
|
56
56
|
// Step 5: Run secret scanning if enabled
|
|
57
57
|
const secretScanning = config.hooks?.preCommit?.secretScanning;
|
|
58
|
-
if (secretScanning?.enabled
|
|
58
|
+
if (secretScanning?.enabled) {
|
|
59
59
|
console.log(chalk.blue('\nš Running secret scanning...'));
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Show scan output if verbose
|
|
66
|
-
if (verbose && result) {
|
|
67
|
-
console.log(chalk.gray(result));
|
|
68
|
-
}
|
|
69
|
-
console.log(chalk.green('ā
No secrets detected'));
|
|
60
|
+
// Determine which tools to run (autodetect or explicit command)
|
|
61
|
+
const toolsToRun = selectToolsToRun(secretScanning.scanCommand);
|
|
62
|
+
if (toolsToRun.length === 0) {
|
|
63
|
+
console.warn(chalk.yellow('ā ļø No secret scanning tools configured or available'));
|
|
64
|
+
console.warn(chalk.gray(' Install gitleaks or add .secretlintrc.json'));
|
|
70
65
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
else if (error && typeof error === 'object' && 'stderr' in error && 'stdout' in error) {
|
|
85
|
-
// Tool ran but found secrets
|
|
86
|
-
const stderr = 'stderr' in error && error.stderr ? String(error.stderr) : '';
|
|
87
|
-
const stdout = 'stdout' in error && error.stdout ? String(error.stdout) : '';
|
|
88
|
-
console.error(chalk.red('\nā Secret scanning detected potential secrets in staged files\n'));
|
|
89
|
-
// Show scan output
|
|
90
|
-
if (stdout) {
|
|
91
|
-
console.error(stdout);
|
|
66
|
+
else {
|
|
67
|
+
const results = [];
|
|
68
|
+
// Run each tool
|
|
69
|
+
for (const { tool, command } of toolsToRun) {
|
|
70
|
+
const result = runSecretScan(tool, command, verbose);
|
|
71
|
+
results.push(result);
|
|
72
|
+
// Handle skipped scans (e.g., gitleaks not available but config exists)
|
|
73
|
+
if (result.skipped) {
|
|
74
|
+
if (hasGitleaksConfig() && !isGitleaksAvailable()) {
|
|
75
|
+
console.warn(chalk.yellow(`ā ļø Found .gitleaks.toml but gitleaks command not available, skipping`));
|
|
76
|
+
console.warn(chalk.gray(' Install gitleaks: brew install gitleaks'));
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
92
79
|
}
|
|
93
|
-
if
|
|
94
|
-
|
|
80
|
+
// Show verbose output if requested
|
|
81
|
+
if (verbose && result.output) {
|
|
82
|
+
console.log(chalk.gray(result.output));
|
|
83
|
+
}
|
|
84
|
+
// Show performance warning if scan was slow (hardcoded 5s threshold)
|
|
85
|
+
if (result.passed) {
|
|
86
|
+
showPerformanceWarning(tool, result.duration, 5000);
|
|
95
87
|
}
|
|
96
|
-
console.error(chalk.blue('\nš” Fix options:'));
|
|
97
|
-
console.error(chalk.gray(' 1. Remove secrets from staged files'));
|
|
98
|
-
console.error(chalk.gray(' 2. Use .gitleaksignore to mark false positives (if using gitleaks)'));
|
|
99
|
-
console.error(chalk.gray(' 3. Disable scanning: set hooks.preCommit.secretScanning.enabled=false'));
|
|
100
|
-
process.exit(1);
|
|
101
88
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
89
|
+
// Check if any scans failed
|
|
90
|
+
const failedScans = results.filter(r => !r.passed && !r.skipped);
|
|
91
|
+
if (failedScans.length > 0) {
|
|
92
|
+
showSecretsDetectedError(failedScans);
|
|
106
93
|
process.exit(1);
|
|
107
94
|
}
|
|
95
|
+
// Success message
|
|
96
|
+
const ranTools = results.filter(r => !r.skipped);
|
|
97
|
+
if (ranTools.length > 0) {
|
|
98
|
+
const toolNames = ranTools.map(r => formatToolName(r.tool)).join(', ');
|
|
99
|
+
const totalDuration = ranTools.reduce((sum, r) => sum + r.duration, 0);
|
|
100
|
+
console.log(chalk.green(`ā
No secrets detected (${toolNames}, ${totalDuration}ms)`));
|
|
101
|
+
}
|
|
108
102
|
}
|
|
109
103
|
}
|
|
110
104
|
// Step 6: Run validation with caching
|
|
@@ -125,15 +119,8 @@ export function preCommitCommand(program) {
|
|
|
125
119
|
else {
|
|
126
120
|
console.error(chalk.red('\nā Pre-commit checks failed'));
|
|
127
121
|
console.error(chalk.yellow(' Fix errors before committing.'));
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
// Find the failed step's command (v0.15.0+: rerunCommand removed, use step.command)
|
|
131
|
-
const failedStep = result.phases
|
|
132
|
-
?.flatMap(phase => phase.steps)
|
|
133
|
-
.find(step => step.name === result.failedStep);
|
|
134
|
-
if (failedStep?.command) {
|
|
135
|
-
console.error(chalk.blue('š To retry:'), chalk.white(failedStep.command));
|
|
136
|
-
}
|
|
122
|
+
// Note: Error details and YAML output are already shown by runValidateWorkflow
|
|
123
|
+
// No need to duplicate the error display here
|
|
137
124
|
process.exit(1);
|
|
138
125
|
}
|
|
139
126
|
}
|
|
@@ -157,9 +144,10 @@ The \`pre-commit\` command runs a comprehensive pre-commit workflow to ensure yo
|
|
|
157
144
|
|
|
158
145
|
## How It Works
|
|
159
146
|
|
|
160
|
-
1. Runs
|
|
161
|
-
2. Runs
|
|
162
|
-
3.
|
|
147
|
+
1. Runs secret scanning (if enabled in config)
|
|
148
|
+
2. Runs sync-check (fails if branch behind origin/main)
|
|
149
|
+
3. Runs validate (with caching)
|
|
150
|
+
4. Reports git status (warns about unstaged files)
|
|
163
151
|
|
|
164
152
|
## Options
|
|
165
153
|
|
|
@@ -207,6 +195,90 @@ echo "npx vibe-validate pre-commit" > .husky/pre-commit
|
|
|
207
195
|
git commit -m "Your message"
|
|
208
196
|
\`\`\`
|
|
209
197
|
|
|
198
|
+
## Secret Scanning
|
|
199
|
+
|
|
200
|
+
Secret scanning prevents accidental commits of credentials (API keys, tokens, passwords).
|
|
201
|
+
|
|
202
|
+
### Autodetect Mode (Recommended)
|
|
203
|
+
|
|
204
|
+
Enable in config without specifying \`scanCommand\`:
|
|
205
|
+
|
|
206
|
+
\`\`\`yaml
|
|
207
|
+
hooks:
|
|
208
|
+
preCommit:
|
|
209
|
+
secretScanning:
|
|
210
|
+
enabled: true
|
|
211
|
+
\`\`\`
|
|
212
|
+
|
|
213
|
+
Automatically runs tools based on config files:
|
|
214
|
+
- \`.gitleaks.toml\` or \`.gitleaksignore\` ā runs gitleaks
|
|
215
|
+
- \`.secretlintrc.json\` ā runs secretlint (via npx)
|
|
216
|
+
- Both files ā runs both tools (defense-in-depth)
|
|
217
|
+
|
|
218
|
+
### Tool Setup
|
|
219
|
+
|
|
220
|
+
**Option 1: gitleaks (recommended - fast, 160+ secret types)**
|
|
221
|
+
\`\`\`bash
|
|
222
|
+
# Install
|
|
223
|
+
macOS: brew install gitleaks
|
|
224
|
+
Linux: https://github.com/gitleaks/gitleaks#installation
|
|
225
|
+
Windows: winget install gitleaks
|
|
226
|
+
|
|
227
|
+
# Create config (empty file enables autodetect)
|
|
228
|
+
touch .gitleaksignore
|
|
229
|
+
|
|
230
|
+
# Handle false positives (add fingerprints from gitleaks output)
|
|
231
|
+
echo "path/to/file.txt:generic-api-key:123" >> .gitleaksignore
|
|
232
|
+
\`\`\`
|
|
233
|
+
|
|
234
|
+
**Option 2: secretlint (npm-based, always available)**
|
|
235
|
+
\`\`\`bash
|
|
236
|
+
# Install
|
|
237
|
+
npm install --save-dev @secretlint/secretlint-rule-preset-recommend secretlint
|
|
238
|
+
|
|
239
|
+
# Create config
|
|
240
|
+
cat > .secretlintrc.json << 'EOF'
|
|
241
|
+
{
|
|
242
|
+
"rules": [
|
|
243
|
+
{"id": "@secretlint/secretlint-rule-preset-recommend"}
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
EOF
|
|
247
|
+
|
|
248
|
+
# Handle false positives
|
|
249
|
+
cat > .secretlintignore << 'EOF'
|
|
250
|
+
.jscpd/
|
|
251
|
+
**/dist/**
|
|
252
|
+
**/node_modules/**
|
|
253
|
+
EOF
|
|
254
|
+
\`\`\`
|
|
255
|
+
|
|
256
|
+
**Option 3: Both (defense-in-depth)**
|
|
257
|
+
\`\`\`bash
|
|
258
|
+
# Set up both tools - autodetect runs both automatically
|
|
259
|
+
# gitleaks: fast native binary
|
|
260
|
+
# secretlint: npm-based with different detection patterns
|
|
261
|
+
\`\`\`
|
|
262
|
+
|
|
263
|
+
### Explicit Command Mode
|
|
264
|
+
|
|
265
|
+
For custom tools or specific flags:
|
|
266
|
+
|
|
267
|
+
\`\`\`yaml
|
|
268
|
+
hooks:
|
|
269
|
+
preCommit:
|
|
270
|
+
secretScanning:
|
|
271
|
+
enabled: true
|
|
272
|
+
scanCommand: "gitleaks protect --staged --verbose --config .gitleaks.toml"
|
|
273
|
+
\`\`\`
|
|
274
|
+
|
|
275
|
+
### Troubleshooting
|
|
276
|
+
|
|
277
|
+
- **"No secrets detected"** - Working correctly, no secrets found
|
|
278
|
+
- **"Secret scanning enabled but no tools available"** - Install gitleaks or create .secretlintrc.json
|
|
279
|
+
- **False positives** - Add to .gitleaksignore or .secretlintignore
|
|
280
|
+
- **Slow scans** - Warning shown if scan takes >5 seconds
|
|
281
|
+
|
|
210
282
|
## Error Recovery
|
|
211
283
|
|
|
212
284
|
### If sync check fails
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pre-commit.js","sourceRoot":"","sources":["../../src/commands/pre-commit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,
|
|
1
|
+
{"version":3,"file":"pre-commit.js","sourceRoot":"","sources":["../../src/commands/pre-commit.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAazC,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwLjD;AAyeD;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CA0SzC"}
|
package/dist/commands/run.js
CHANGED
|
@@ -4,13 +4,12 @@
|
|
|
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 { execSync } from 'node:child_process';
|
|
8
7
|
import { writeFile, readFile } from 'node:fs/promises';
|
|
9
|
-
import { join } from 'node:path';
|
|
8
|
+
import { join, resolve, relative } from 'node:path';
|
|
10
9
|
import { autoDetectAndExtract } from '@vibe-validate/extractors';
|
|
11
10
|
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';
|
|
11
|
+
import { getGitTreeHash, encodeRunCacheKey, extractYamlWithPreamble, addNote, readNote } from '@vibe-validate/git';
|
|
12
|
+
import { spawnCommand, parseVibeValidateOutput, getGitRoot } from '@vibe-validate/core';
|
|
14
13
|
import yaml from 'yaml';
|
|
15
14
|
import chalk from 'chalk';
|
|
16
15
|
export function runCommand(program) {
|
|
@@ -20,6 +19,7 @@ export function runCommand(program) {
|
|
|
20
19
|
.argument('<command...>', 'Command to execute (multiple words supported)')
|
|
21
20
|
.option('--check', 'Check if cached result exists without executing')
|
|
22
21
|
.option('--force', 'Force execution and update cache (bypass cache read)')
|
|
22
|
+
.option('--cwd <directory>', 'Working directory relative to git root (default: git root)')
|
|
23
23
|
.option('--head <lines>', 'Display first N lines of output after YAML (on stderr)', Number.parseInt)
|
|
24
24
|
.option('--tail <lines>', 'Display last N lines of output after YAML (on stderr)', Number.parseInt)
|
|
25
25
|
.option('--verbose', 'Display all output after YAML (on stderr)')
|
|
@@ -57,6 +57,10 @@ export function runCommand(program) {
|
|
|
57
57
|
actualOptions.force = true;
|
|
58
58
|
i++;
|
|
59
59
|
}
|
|
60
|
+
else if (arg === '--cwd' && i + 1 < argv.length) {
|
|
61
|
+
actualOptions.cwd = argv[i + 1];
|
|
62
|
+
i += 2;
|
|
63
|
+
}
|
|
60
64
|
else if (arg === '--head' && i + 1 < argv.length) {
|
|
61
65
|
actualOptions.head = Number.parseInt(argv[i + 1], 10);
|
|
62
66
|
i += 2;
|
|
@@ -82,9 +86,12 @@ export function runCommand(program) {
|
|
|
82
86
|
process.exit(0);
|
|
83
87
|
}
|
|
84
88
|
try {
|
|
89
|
+
// Note: Plugin loading is handled by the runner (in @vibe-validate/core)
|
|
90
|
+
// when running via validate command. For standalone run commands, plugins
|
|
91
|
+
// are not needed since run is primarily for caching/extraction, not validation.
|
|
85
92
|
// Handle --check flag (cache status check only)
|
|
86
93
|
if (actualOptions.check) {
|
|
87
|
-
const cachedResult = await tryGetCachedResult(commandString);
|
|
94
|
+
const cachedResult = await tryGetCachedResult(commandString, actualOptions.cwd);
|
|
88
95
|
if (cachedResult) {
|
|
89
96
|
// Cache hit - output cached result and exit with code 0
|
|
90
97
|
process.stdout.write('---\n');
|
|
@@ -103,26 +110,26 @@ export function runCommand(program) {
|
|
|
103
110
|
let result;
|
|
104
111
|
let context = { preamble: '', stderr: '' };
|
|
105
112
|
if (!actualOptions.force) {
|
|
106
|
-
const cachedResult = await tryGetCachedResult(commandString);
|
|
113
|
+
const cachedResult = await tryGetCachedResult(commandString, actualOptions.cwd);
|
|
107
114
|
if (cachedResult) {
|
|
108
115
|
result = cachedResult;
|
|
109
116
|
}
|
|
110
117
|
else {
|
|
111
118
|
// Cache miss - execute command
|
|
112
|
-
const executeResult = await executeAndExtract(commandString);
|
|
119
|
+
const executeResult = await executeAndExtract(commandString, actualOptions.cwd);
|
|
113
120
|
result = executeResult.result;
|
|
114
121
|
context = executeResult.context;
|
|
115
122
|
// Store result in cache
|
|
116
|
-
await storeCacheResult(commandString, result);
|
|
123
|
+
await storeCacheResult(commandString, result, actualOptions.cwd);
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
else {
|
|
120
127
|
// Force flag - bypass cache and execute
|
|
121
|
-
const executeResult = await executeAndExtract(commandString);
|
|
128
|
+
const executeResult = await executeAndExtract(commandString, actualOptions.cwd);
|
|
122
129
|
result = executeResult.result;
|
|
123
130
|
context = executeResult.context;
|
|
124
131
|
// Update cache with fresh result
|
|
125
|
-
await storeCacheResult(commandString, result);
|
|
132
|
+
await storeCacheResult(commandString, result, actualOptions.cwd);
|
|
126
133
|
}
|
|
127
134
|
// CRITICAL: Write complete YAML to stdout and flush BEFORE any stderr
|
|
128
135
|
// This ensures even if callers use 2>&1, YAML completes first
|
|
@@ -180,24 +187,49 @@ export function runCommand(program) {
|
|
|
180
187
|
});
|
|
181
188
|
}
|
|
182
189
|
/**
|
|
183
|
-
* Get working directory relative to git root
|
|
190
|
+
* Get working directory relative to git root for cache key
|
|
184
191
|
* Returns empty string for root, "packages/cli" for subdirectory
|
|
192
|
+
*
|
|
193
|
+
* IMPORTANT: For cache key generation, we use the ACTUAL directory where the command
|
|
194
|
+
* is invoked from (process.cwd()), not where it runs. This ensures cache keys are
|
|
195
|
+
* accurate - running "npm test" from packages/cli is different than from git root.
|
|
196
|
+
*
|
|
197
|
+
* @param explicitCwd - Optional explicit cwd from --cwd flag (relative to git root)
|
|
198
|
+
* @returns Working directory path relative to git root (empty string for root)
|
|
185
199
|
*/
|
|
186
|
-
function getWorkingDirectory() {
|
|
200
|
+
function getWorkingDirectory(explicitCwd) {
|
|
187
201
|
try {
|
|
188
|
-
const gitRoot =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
202
|
+
const gitRoot = getGitRoot();
|
|
203
|
+
if (!gitRoot) {
|
|
204
|
+
throw new Error('Not in a git repository');
|
|
205
|
+
}
|
|
206
|
+
// Use explicit --cwd if provided
|
|
207
|
+
if (explicitCwd) {
|
|
208
|
+
// Resolve path relative to git root
|
|
209
|
+
const resolved = resolve(gitRoot, explicitCwd);
|
|
210
|
+
// Security: Prevent directory traversal outside git root
|
|
211
|
+
if (!resolved.startsWith(gitRoot)) {
|
|
212
|
+
throw new Error(`Invalid --cwd: "${explicitCwd}" - must be within git repository`);
|
|
213
|
+
}
|
|
214
|
+
// Return normalized relative path
|
|
215
|
+
const relativePath = relative(gitRoot, resolved);
|
|
216
|
+
return relativePath || ''; // Empty string if resolved to git root
|
|
217
|
+
}
|
|
218
|
+
// Use actual current directory for cache key (process.cwd() relative to git root)
|
|
219
|
+
// This ensures cache keys reflect WHERE the command was invoked from, not where it runs
|
|
192
220
|
const cwd = process.cwd();
|
|
193
|
-
|
|
194
|
-
|
|
221
|
+
const relativePath = relative(gitRoot, cwd);
|
|
222
|
+
// If outside git repo or at git root, return empty string
|
|
223
|
+
if (relativePath.startsWith('..') || !relativePath) {
|
|
195
224
|
return '';
|
|
196
225
|
}
|
|
197
|
-
|
|
198
|
-
return cwd.substring(gitRoot.length + 1); // +1 to remove leading slash
|
|
226
|
+
return relativePath;
|
|
199
227
|
}
|
|
200
|
-
catch {
|
|
228
|
+
catch (error) {
|
|
229
|
+
// Re-throw validation errors
|
|
230
|
+
if (error instanceof Error && error.message.includes('Invalid --cwd')) {
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
201
233
|
// Not in a git repository - return empty string
|
|
202
234
|
return '';
|
|
203
235
|
}
|
|
@@ -206,7 +238,7 @@ function getWorkingDirectory() {
|
|
|
206
238
|
* Try to get cached result for a command
|
|
207
239
|
* Returns null if no cache hit or if not in a git repository
|
|
208
240
|
*/
|
|
209
|
-
async function tryGetCachedResult(commandString) {
|
|
241
|
+
async function tryGetCachedResult(commandString, explicitCwd) {
|
|
210
242
|
try {
|
|
211
243
|
// Get tree hash
|
|
212
244
|
const treeHash = await getGitTreeHash();
|
|
@@ -215,16 +247,13 @@ async function tryGetCachedResult(commandString) {
|
|
|
215
247
|
return null;
|
|
216
248
|
}
|
|
217
249
|
// Get working directory
|
|
218
|
-
const workdir = getWorkingDirectory();
|
|
250
|
+
const workdir = getWorkingDirectory(explicitCwd);
|
|
219
251
|
// Encode cache key
|
|
220
252
|
const cacheKey = encodeRunCacheKey(commandString, workdir);
|
|
221
253
|
// Construct git notes ref path: refs/notes/vibe-validate/run/{treeHash}/{cacheKey}
|
|
222
254
|
const refPath = `vibe-validate/run/${treeHash}/${cacheKey}`;
|
|
223
|
-
// Try to read git note
|
|
224
|
-
const noteContent =
|
|
225
|
-
encoding: 'utf8',
|
|
226
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
227
|
-
}).trim();
|
|
255
|
+
// Try to read git note using secure readNote function
|
|
256
|
+
const noteContent = readNote(refPath, 'HEAD');
|
|
228
257
|
if (!noteContent) {
|
|
229
258
|
// Cache miss
|
|
230
259
|
return null;
|
|
@@ -256,7 +285,7 @@ async function tryGetCachedResult(commandString) {
|
|
|
256
285
|
/**
|
|
257
286
|
* Store result in cache (only successful runs - exitCode === 0)
|
|
258
287
|
*/
|
|
259
|
-
async function storeCacheResult(commandString, result) {
|
|
288
|
+
async function storeCacheResult(commandString, result, explicitCwd) {
|
|
260
289
|
try {
|
|
261
290
|
// Only cache successful runs (v0.15.0+)
|
|
262
291
|
// Failed runs may be transient or environment-specific
|
|
@@ -270,7 +299,7 @@ async function storeCacheResult(commandString, result) {
|
|
|
270
299
|
return;
|
|
271
300
|
}
|
|
272
301
|
// Get working directory
|
|
273
|
-
const workdir = getWorkingDirectory();
|
|
302
|
+
const workdir = getWorkingDirectory(explicitCwd);
|
|
274
303
|
// Encode cache key
|
|
275
304
|
const cacheKey = encodeRunCacheKey(commandString, workdir);
|
|
276
305
|
// Construct git notes ref path
|
|
@@ -288,14 +317,12 @@ async function storeCacheResult(commandString, result) {
|
|
|
288
317
|
...(result.extraction ? { extraction: result.extraction } : {}), // Conditionally include extraction
|
|
289
318
|
...(result.outputFiles ? { outputFiles: result.outputFiles } : {}),
|
|
290
319
|
};
|
|
291
|
-
// Store in git notes using
|
|
320
|
+
// Store in git notes using secure addNote function
|
|
321
|
+
// SECURITY FIX: Eliminates heredoc injection vulnerability
|
|
292
322
|
const noteYaml = yaml.stringify(cacheNote);
|
|
293
|
-
// Use heredoc format for multi-line YAML
|
|
294
323
|
try {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
shell: '/bin/bash', // Ensure bash for heredoc support
|
|
298
|
-
});
|
|
324
|
+
// Use secure addNote with stdin piping (no shell, no heredoc)
|
|
325
|
+
addNote(refPath, 'HEAD', noteYaml, true);
|
|
299
326
|
// eslint-disable-next-line sonarjs/no-ignored-exceptions -- Cache storage failure is non-critical
|
|
300
327
|
}
|
|
301
328
|
catch (_error) {
|
|
@@ -318,10 +345,31 @@ function stripAnsiCodes(text) {
|
|
|
318
345
|
/**
|
|
319
346
|
* Execute a command and extract errors from its output
|
|
320
347
|
*/
|
|
321
|
-
async function executeAndExtract(commandString) {
|
|
322
|
-
return new Promise((
|
|
348
|
+
async function executeAndExtract(commandString, explicitCwd) {
|
|
349
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
323
350
|
const startTime = Date.now();
|
|
324
|
-
|
|
351
|
+
// Resolve cwd if provided (relative to git root)
|
|
352
|
+
let resolvedCwd;
|
|
353
|
+
if (explicitCwd) {
|
|
354
|
+
try {
|
|
355
|
+
const gitRoot = getGitRoot();
|
|
356
|
+
if (!gitRoot) {
|
|
357
|
+
rejectPromise(new Error('Not in a git repository'));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
resolvedCwd = resolve(gitRoot, explicitCwd);
|
|
361
|
+
// Security: Validate path is within git root
|
|
362
|
+
if (!resolvedCwd.startsWith(gitRoot)) {
|
|
363
|
+
rejectPromise(new Error(`Invalid --cwd: "${explicitCwd}" - must be within git repository`));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
rejectPromise(new Error(`Failed to resolve --cwd: ${error instanceof Error ? error.message : 'unknown error'}`));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const child = spawnCommand(commandString, { cwd: resolvedCwd });
|
|
325
373
|
let stdout = '';
|
|
326
374
|
let stderr = '';
|
|
327
375
|
const combinedLines = [];
|
|
@@ -372,7 +420,7 @@ async function executeAndExtract(commandString) {
|
|
|
372
420
|
preamble: yamlResult.preamble,
|
|
373
421
|
stderr: stderr.trim(),
|
|
374
422
|
};
|
|
375
|
-
|
|
423
|
+
resolvePromise({ result: mergedResult, context: contextOutput });
|
|
376
424
|
return;
|
|
377
425
|
}
|
|
378
426
|
// For extraction, combine both streams (stderr has useful error context)
|
|
@@ -383,7 +431,7 @@ async function executeAndExtract(commandString) {
|
|
|
383
431
|
stdout,
|
|
384
432
|
stderr,
|
|
385
433
|
combined: combinedOutput,
|
|
386
|
-
});
|
|
434
|
+
}, exitCode);
|
|
387
435
|
const extraction = (exitCode !== 0 || rawExtraction.totalErrors > 0) ? rawExtraction : undefined;
|
|
388
436
|
// Get tree hash for result (async operation needs to be awaited)
|
|
389
437
|
getGitTreeHash()
|
|
@@ -426,7 +474,7 @@ async function executeAndExtract(commandString) {
|
|
|
426
474
|
combined: combinedFile,
|
|
427
475
|
},
|
|
428
476
|
};
|
|
429
|
-
|
|
477
|
+
resolvePromise({ result, context: { preamble: '', stderr: '' } });
|
|
430
478
|
})
|
|
431
479
|
.catch(async () => {
|
|
432
480
|
// If tree hash fails, use timestamp-based fallback
|
|
@@ -470,12 +518,12 @@ async function executeAndExtract(commandString) {
|
|
|
470
518
|
combined: combinedFile,
|
|
471
519
|
},
|
|
472
520
|
};
|
|
473
|
-
|
|
521
|
+
resolvePromise({ result, context: { preamble: '', stderr: '' } });
|
|
474
522
|
});
|
|
475
523
|
});
|
|
476
524
|
// Handle spawn errors (e.g., command not found)
|
|
477
525
|
child.on('error', (error) => {
|
|
478
|
-
|
|
526
|
+
rejectPromise(error);
|
|
479
527
|
});
|
|
480
528
|
});
|
|
481
529
|
}
|
|
@@ -548,6 +596,10 @@ function mergeNestedYaml(outerCommand, yamlOutput, outerExitCode, outerDurationS
|
|
|
548
596
|
};
|
|
549
597
|
}
|
|
550
598
|
}
|
|
599
|
+
// Note: loadPluginsIfConfigured() has been removed.
|
|
600
|
+
// Plugin loading is now handled exclusively by the runner (in @vibe-validate/core)
|
|
601
|
+
// when executing validation. This prevents redundant plugin loading on every
|
|
602
|
+
// `vv run` invocation, which was causing 2.4x+ performance regression during tests.
|
|
551
603
|
/**
|
|
552
604
|
* Show verbose help with detailed documentation
|
|
553
605
|
*/
|
|
@@ -619,6 +671,15 @@ $ vibe-validate run --force "pnpm test"
|
|
|
619
671
|
# Useful for flaky tests or time-sensitive commands
|
|
620
672
|
\`\`\`
|
|
621
673
|
|
|
674
|
+
**Working directory** (--cwd):
|
|
675
|
+
\`\`\`bash
|
|
676
|
+
$ vibe-validate run --cwd packages/cli "npm test"
|
|
677
|
+
# Runs command in packages/cli directory (relative to git root)
|
|
678
|
+
# Cache keys include working directory for correct cache hits
|
|
679
|
+
\`\`\`
|
|
680
|
+
|
|
681
|
+
**NEW in v0.17.0**: The \`--cwd\` flag allows running commands in subdirectories while maintaining consistent cache behavior. Paths are relative to git root, ensuring cache hits regardless of where you invoke the command.
|
|
682
|
+
|
|
622
683
|
### Cache Storage
|
|
623
684
|
|
|
624
685
|
Cache is stored in git notes at:
|
|
@@ -856,6 +917,7 @@ Arguments:
|
|
|
856
917
|
Options:
|
|
857
918
|
--check Check if cached result exists without executing
|
|
858
919
|
--force Force execution and update cache (bypass cache read)
|
|
920
|
+
--cwd <directory> Working directory relative to git root (default: git root)
|
|
859
921
|
--head <lines> Display first N lines of output after YAML (on stderr)
|
|
860
922
|
--tail <lines> Display last N lines of output after YAML (on stderr)
|
|
861
923
|
--verbose Display all output after YAML (on stderr)
|
|
@@ -866,6 +928,7 @@ Examples:
|
|
|
866
928
|
vv run cargo test --all-features # Rust
|
|
867
929
|
vv run go test ./... # Go
|
|
868
930
|
vv run npm test # Node.js
|
|
931
|
+
vv run --cwd packages/cli npm test # Run in subdirectory
|
|
869
932
|
vv run --verbose npm test # With output display
|
|
870
933
|
|
|
871
934
|
For detailed documentation, use: vibe-validate run --help --verbose
|