@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.
Files changed (50) hide show
  1. package/config-templates/minimal.yaml +19 -8
  2. package/config-templates/typescript-library.yaml +19 -8
  3. package/config-templates/typescript-nodejs.yaml +19 -8
  4. package/config-templates/typescript-react.yaml +19 -8
  5. package/dist/bin.js +6 -0
  6. package/dist/bin.js.map +1 -1
  7. package/dist/commands/create-extractor.d.ts +13 -0
  8. package/dist/commands/create-extractor.d.ts.map +1 -0
  9. package/dist/commands/create-extractor.js +790 -0
  10. package/dist/commands/create-extractor.js.map +1 -0
  11. package/dist/commands/doctor.d.ts.map +1 -1
  12. package/dist/commands/doctor.js +140 -109
  13. package/dist/commands/doctor.js.map +1 -1
  14. package/dist/commands/generate-workflow.d.ts.map +1 -1
  15. package/dist/commands/generate-workflow.js +8 -0
  16. package/dist/commands/generate-workflow.js.map +1 -1
  17. package/dist/commands/pre-commit.d.ts.map +1 -1
  18. package/dist/commands/pre-commit.js +128 -56
  19. package/dist/commands/pre-commit.js.map +1 -1
  20. package/dist/commands/run.d.ts.map +1 -1
  21. package/dist/commands/run.js +107 -44
  22. package/dist/commands/run.js.map +1 -1
  23. package/dist/commands/validate.d.ts.map +1 -1
  24. package/dist/commands/validate.js +25 -9
  25. package/dist/commands/validate.js.map +1 -1
  26. package/dist/services/ci-providers/github-actions.d.ts.map +1 -1
  27. package/dist/services/ci-providers/github-actions.js +3 -2
  28. package/dist/services/ci-providers/github-actions.js.map +1 -1
  29. package/dist/utils/config-loader.d.ts +26 -3
  30. package/dist/utils/config-loader.d.ts.map +1 -1
  31. package/dist/utils/config-loader.js +80 -11
  32. package/dist/utils/config-loader.js.map +1 -1
  33. package/dist/utils/git-detection.d.ts.map +1 -1
  34. package/dist/utils/git-detection.js +18 -18
  35. package/dist/utils/git-detection.js.map +1 -1
  36. package/dist/utils/project-id.d.ts +1 -2
  37. package/dist/utils/project-id.d.ts.map +1 -1
  38. package/dist/utils/project-id.js +6 -11
  39. package/dist/utils/project-id.js.map +1 -1
  40. package/dist/utils/runner-adapter.d.ts.map +1 -1
  41. package/dist/utils/runner-adapter.js +1 -0
  42. package/dist/utils/runner-adapter.js.map +1 -1
  43. package/dist/utils/secret-scanning.d.ts +72 -0
  44. package/dist/utils/secret-scanning.d.ts.map +1 -0
  45. package/dist/utils/secret-scanning.js +205 -0
  46. package/dist/utils/secret-scanning.js.map +1 -0
  47. package/dist/utils/validate-workflow.d.ts.map +1 -1
  48. package/dist/utils/validate-workflow.js +9 -1
  49. package/dist/utils/validate-workflow.js.map +1 -1
  50. 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 { execSync } from 'node:child_process';
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 && secretScanning?.scanCommand) {
58
+ if (secretScanning?.enabled) {
59
59
  console.log(chalk.blue('\nšŸ”’ Running secret scanning...'));
60
- try {
61
- const result = execSync(secretScanning.scanCommand, {
62
- encoding: 'utf8',
63
- stdio: 'pipe',
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
- catch (error) {
72
- // Secret scanning failed (either tool missing or secrets found)
73
- if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
74
- // Tool not found
75
- const toolName = secretScanning.scanCommand.split(' ')[0];
76
- console.error(chalk.red('\nāŒ Secret scanning tool not found'));
77
- console.error(chalk.yellow(` Command: ${chalk.white(secretScanning.scanCommand)}`));
78
- console.error(chalk.yellow(` Tool '${toolName}' is not installed or not in PATH`));
79
- console.error(chalk.blue('\nšŸ’” Fix options:'));
80
- console.error(chalk.gray(' 1. Install the tool (e.g., brew install gitleaks)'));
81
- console.error(chalk.gray(' 2. Disable scanning: set hooks.preCommit.secretScanning.enabled=false'));
82
- process.exit(1);
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 (stderr) {
94
- console.error(stderr);
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
- else {
103
- // Unknown error
104
- console.error(chalk.red('\nāŒ Secret scanning failed with unknown error'));
105
- console.error(chalk.gray(String(error)));
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
- // Show agent-friendly error details
129
- console.error(chalk.blue('\nšŸ“‹ Error details:'), chalk.white('vibe-validate state'));
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 sync-check (fails if branch behind origin/main)
161
- 2. Runs validate (with caching)
162
- 3. Reports git status (warns about unstaged files)
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,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,OAAO;SACJ,OAAO,CAAC,YAAY,CAAC;SACrB,WAAW,CAAC,gEAAgE,CAAC;SAC7E,MAAM,CAAC,aAAa,EAAE,wBAAwB,CAAC;SAC/C,MAAM,CAAC,eAAe,EAAE,mCAAmC,CAAC;QAC7D,iMAAiM;SAChM,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QACxB,IAAI,CAAC;YACH,uDAAuD;YACvD,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;YAClC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,CAAC;gBACrD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC,CAAC;gBACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC;YAED,6CAA6C;YAC7C,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACtB,iDAAiD;gBACjD,MAAM,YAAY,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAEjD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,gCAAgC,YAAY,KAAK,CAAC,CAAC,CAAC;gBAE3E,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC;oBACvC,YAAY;iBACb,CAAC,CAAC;gBAEH,IAAI,CAAC,UAAU,CAAC,UAAU,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;oBACnD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,sBAAsB,YAAY,EAAE,CAAC,CAAC,CAAC;oBAC/D,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,gBAAgB,UAAU,CAAC,QAAQ,YAAY,CAAC,CAAC,CAAC;oBAC7E,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,mBAAmB,YAAY,qBAAqB,CAAC,CAAC,CAAC;oBAClF,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,YAAY,EAAE,CAAC,CAAC,CAAC;oBAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAClB,CAAC;gBAED,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;oBACzB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,+BAA+B,YAAY,EAAE,CAAC,CAAC,CAAC;gBAC1E,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC,CAAC;gBACrF,CAAC;YACH,CAAC;YAED,yBAAyB;YACzB,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;YAEhC,mEAAmE;YACnE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC;YAEzC,yCAAyC;YACzC,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC;YAC/D,IAAI,cAAc,EAAE,OAAO,IAAI,cAAc,EAAE,WAAW,EAAE,CAAC;gBAC3D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC,CAAC;gBAE3D,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,WAAW,EAAE;wBAClD,QAAQ,EAAE,MAAM;wBAChB,KAAK,EAAE,MAAM;qBACd,CAAC,CAAC;oBAEH,8BAA8B;oBAC9B,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;wBACtB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;oBAClC,CAAC;oBAED,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC,CAAC;gBACpD,CAAC;gBAAC,OAAO,KAAc,EAAE,CAAC;oBACxB,gEAAgE;oBAChE,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;wBACrF,iBAAiB;wBACjB,MAAM,QAAQ,GAAG,cAAc,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;wBAC1D,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC,CAAC;wBAC/D,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,eAAe,KAAK,CAAC,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;wBACtF,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,QAAQ,mCAAmC,CAAC,CAAC,CAAC;wBACrF,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC;wBAC/C,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC,CAAC;wBAClF,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC,CAAC;wBACtG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;oBAClB,CAAC;yBAAM,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,QAAQ,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK,EAAE,CAAC;wBACxF,6BAA6B;wBAC7B,MAAM,MAAM,GAAG,QAAQ,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;wBAC7E,MAAM,MAAM,GAAG,QAAQ,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;wBAE7E,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,kEAAkE,CAAC,CAAC,CAAC;wBAE7F,mBAAmB;wBACnB,IAAI,MAAM,EAAE,CAAC;4BACX,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;wBACxB,CAAC;wBACD,IAAI,MAAM,EAAE,CAAC;4BACX,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;wBACxB,CAAC;wBAED,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC;wBAC/C,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC,CAAC;wBACpE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC,CAAC;wBACnG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC,CAAC;wBACtG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;oBAClB,CAAC;yBAAM,CAAC;wBACN,gBAAgB;wBAChB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,+CAA+C,CAAC,CAAC,CAAC;wBAC1E,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;wBACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;oBAClB,CAAC;gBACH,CAAC;YACH,CAAC;YAED,sCAAsC;YACtC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC,CAAC;YAEtD,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,MAAM,EAAE;gBAC/C,KAAK,EAAE,KAAK,EAAE,2BAA2B;gBACzC,OAAO;gBACP,IAAI,EAAE,KAAK,EAAE,wCAAwC;gBACrD,KAAK,EAAE,KAAK;gBACZ,OAAO;aACR,CAAC,CAAC;YAEH,yBAAyB;YACzB,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBAClB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC,CAAC;gBAC1D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC;gBAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC,CAAC;gBACzD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,kCAAkC,CAAC,CAAC,CAAC;gBAEhE,oCAAoC;gBACpC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,qBAAqB,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,CAAC;gBAErF,oFAAoF;gBACpF,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM;oBAC9B,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC;qBAC9B,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,UAAU,CAAC,CAAC;gBAEjD,IAAI,UAAU,EAAE,OAAO,EAAE,CAAC;oBACxB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;gBAC7E,CAAC;gBAED,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,wCAAwC,CAAC,EAAE,KAAK,CAAC,CAAC;YAC1E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Fb,CAAC,CAAC;AACH,CAAC"}
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;AAczC,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgLjD;AAwbD;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAiSzC"}
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"}
@@ -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 = execSync('git rev-parse --show-toplevel', {
189
- encoding: 'utf8',
190
- stdio: ['ignore', 'pipe', 'ignore'],
191
- }).trim();
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
- // If cwd is git root, return empty string
194
- if (cwd === gitRoot) {
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
- // Return relative path from git root
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 = execSync(`git notes --ref=${refPath} show HEAD 2>/dev/null || true`, {
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 heredoc to avoid quote escaping issues
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
- 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
- });
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((resolve, reject) => {
348
+ async function executeAndExtract(commandString, explicitCwd) {
349
+ return new Promise((resolvePromise, rejectPromise) => {
323
350
  const startTime = Date.now();
324
- const child = spawnCommand(commandString);
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
- resolve({ result: mergedResult, context: contextOutput });
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
- resolve({ result, context: { preamble: '', stderr: '' } });
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
- resolve({ result, context: { preamble: '', stderr: '' } });
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
- reject(error);
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