@vibe-validate/cli 0.14.2 → 0.14.3

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 (69) hide show
  1. package/dist/bin.js +23 -17
  2. package/dist/bin.js.map +1 -1
  3. package/dist/commands/cleanup.d.ts.map +1 -1
  4. package/dist/commands/cleanup.js +32 -19
  5. package/dist/commands/cleanup.js.map +1 -1
  6. package/dist/commands/config.d.ts.map +1 -1
  7. package/dist/commands/config.js +12 -9
  8. package/dist/commands/config.js.map +1 -1
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +212 -187
  11. package/dist/commands/doctor.js.map +1 -1
  12. package/dist/commands/generate-workflow.d.ts.map +1 -1
  13. package/dist/commands/generate-workflow.js +22 -12
  14. package/dist/commands/generate-workflow.js.map +1 -1
  15. package/dist/commands/history.d.ts.map +1 -1
  16. package/dist/commands/history.js +46 -34
  17. package/dist/commands/history.js.map +1 -1
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +7 -6
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/pre-commit.d.ts.map +1 -1
  22. package/dist/commands/pre-commit.js +2 -1
  23. package/dist/commands/pre-commit.js.map +1 -1
  24. package/dist/commands/run.d.ts.map +1 -1
  25. package/dist/commands/run.js +8 -9
  26. package/dist/commands/run.js.map +1 -1
  27. package/dist/commands/validate.d.ts.map +1 -1
  28. package/dist/commands/validate.js +2 -1
  29. package/dist/commands/validate.js.map +1 -1
  30. package/dist/commands/watch-pr.d.ts.map +1 -1
  31. package/dist/commands/watch-pr.js +43 -36
  32. package/dist/commands/watch-pr.js.map +1 -1
  33. package/dist/schemas/watch-pr-schema.d.ts +31 -31
  34. package/dist/scripts/generate-watch-pr-schema.js +3 -3
  35. package/dist/scripts/generate-watch-pr-schema.js.map +1 -1
  36. package/dist/services/ci-providers/github-actions.d.ts +25 -0
  37. package/dist/services/ci-providers/github-actions.d.ts.map +1 -1
  38. package/dist/services/ci-providers/github-actions.js +83 -46
  39. package/dist/services/ci-providers/github-actions.js.map +1 -1
  40. package/dist/utils/check-validation.d.ts.map +1 -1
  41. package/dist/utils/check-validation.js +9 -5
  42. package/dist/utils/check-validation.js.map +1 -1
  43. package/dist/utils/config-error-reporter.js +8 -6
  44. package/dist/utils/config-error-reporter.js.map +1 -1
  45. package/dist/utils/config-loader.js +5 -5
  46. package/dist/utils/config-loader.js.map +1 -1
  47. package/dist/utils/git-detection.d.ts +0 -22
  48. package/dist/utils/git-detection.d.ts.map +1 -1
  49. package/dist/utils/git-detection.js +64 -56
  50. package/dist/utils/git-detection.js.map +1 -1
  51. package/dist/utils/pid-lock.d.ts.map +1 -1
  52. package/dist/utils/pid-lock.js +10 -7
  53. package/dist/utils/pid-lock.js.map +1 -1
  54. package/dist/utils/project-id.d.ts.map +1 -1
  55. package/dist/utils/project-id.js +9 -6
  56. package/dist/utils/project-id.js.map +1 -1
  57. package/dist/utils/runner-adapter.js +1 -1
  58. package/dist/utils/runner-adapter.js.map +1 -1
  59. package/dist/utils/setup-checks/hooks-check.js +3 -3
  60. package/dist/utils/setup-checks/hooks-check.js.map +1 -1
  61. package/dist/utils/setup-checks/workflow-check.js +3 -3
  62. package/dist/utils/setup-checks/workflow-check.js.map +1 -1
  63. package/dist/utils/template-discovery.d.ts.map +1 -1
  64. package/dist/utils/template-discovery.js +5 -4
  65. package/dist/utils/template-discovery.js.map +1 -1
  66. package/dist/utils/validate-workflow.d.ts.map +1 -1
  67. package/dist/utils/validate-workflow.js +164 -150
  68. package/dist/utils/validate-workflow.js.map +1 -1
  69. package/package.json +6 -6
@@ -10,8 +10,8 @@
10
10
  *
11
11
  * @packageDocumentation
12
12
  */
13
- import { existsSync, readFileSync } from 'fs';
14
- import { execSync } from 'child_process';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { execSync } from 'node:child_process';
15
15
  import { stringify as stringifyYaml } from 'yaml';
16
16
  import { loadConfig, findConfigPath, loadConfigWithErrors } from '../utils/config-loader.js';
17
17
  import { formatDoctorConfigError } from '../utils/config-error-reporter.js';
@@ -27,28 +27,24 @@ const DEPRECATED_STATE_FILE = '.vibe-validate-state.yaml';
27
27
  function checkNodeVersion() {
28
28
  try {
29
29
  const version = execSync('node --version', { encoding: 'utf8' }).trim();
30
- const majorVersion = parseInt(version.replace('v', '').split('.')[0]);
31
- if (majorVersion >= 20) {
32
- return {
33
- name: 'Node.js version',
34
- passed: true,
35
- message: `${version} (meets requirement: >=20.0.0)`,
36
- };
37
- }
38
- else {
39
- return {
40
- name: 'Node.js version',
41
- passed: false,
42
- message: `${version} is too old. Node.js 20+ required.`,
43
- suggestion: 'Upgrade Node.js: https://nodejs.org/ or use nvm',
44
- };
45
- }
30
+ const majorVersion = Number.parseInt(version.replace('v', '').split('.')[0]);
31
+ return majorVersion >= 20 ? {
32
+ name: 'Node.js version',
33
+ passed: true,
34
+ message: `${version} (meets requirement: >=20.0.0)`,
35
+ } : {
36
+ name: 'Node.js version',
37
+ passed: false,
38
+ message: `${version} is too old. Node.js 20+ required.`,
39
+ suggestion: 'Upgrade Node.js: https://nodejs.org/ or use nvm',
40
+ };
46
41
  }
47
- catch (_error) {
42
+ catch (error) {
43
+ const errorMessage = error instanceof Error ? error.message : String(error);
48
44
  return {
49
45
  name: 'Node.js version',
50
46
  passed: false,
51
- message: 'Failed to detect Node.js version',
47
+ message: `Failed to detect Node.js version: ${errorMessage}`,
52
48
  suggestion: 'Install Node.js: https://nodejs.org/',
53
49
  };
54
50
  }
@@ -65,11 +61,12 @@ function checkGitInstalled() {
65
61
  message: version,
66
62
  };
67
63
  }
68
- catch (_error) {
64
+ catch (error) {
65
+ const errorMessage = error instanceof Error ? error.message : String(error);
69
66
  return {
70
67
  name: 'Git installed',
71
68
  passed: false,
72
- message: 'Git is not installed',
69
+ message: `Git is not installed: ${errorMessage}`,
73
70
  suggestion: 'Install Git: https://git-scm.com/',
74
71
  };
75
72
  }
@@ -86,11 +83,12 @@ function checkGitRepository() {
86
83
  message: 'Current directory is a git repository',
87
84
  };
88
85
  }
89
- catch (_error) {
86
+ catch (error) {
87
+ const errorMessage = error instanceof Error ? error.message : String(error);
90
88
  return {
91
89
  name: 'Git repository',
92
90
  passed: false,
93
- message: 'Current directory is not a git repository',
91
+ message: `Current directory is not a git repository: ${errorMessage}`,
94
92
  suggestion: 'Run: git init',
95
93
  };
96
94
  }
@@ -100,21 +98,16 @@ function checkGitRepository() {
100
98
  */
101
99
  function checkConfigFile() {
102
100
  const yamlConfig = 'vibe-validate.config.yaml';
103
- if (existsSync(yamlConfig)) {
104
- return {
105
- name: 'Configuration file',
106
- passed: true,
107
- message: `Found: ${yamlConfig}`,
108
- };
109
- }
110
- else {
111
- return {
112
- name: 'Configuration file',
113
- passed: false,
114
- message: 'Configuration file not found',
115
- suggestion: 'Run: npx vibe-validate init',
116
- };
117
- }
101
+ return existsSync(yamlConfig) ? {
102
+ name: 'Configuration file',
103
+ passed: true,
104
+ message: `Found: ${yamlConfig}`,
105
+ } : {
106
+ name: 'Configuration file',
107
+ passed: false,
108
+ message: 'Configuration file not found',
109
+ suggestion: 'Run: npx vibe-validate init',
110
+ };
118
111
  }
119
112
  /**
120
113
  * Check if configuration is valid
@@ -127,6 +120,7 @@ async function checkConfigValid(config, configWithErrors) {
127
120
  if (!config) {
128
121
  // Check if we have detailed error information
129
122
  if (configWithErrors?.errors && configWithErrors.filePath) {
123
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Need to filter empty strings, not just null/undefined
130
124
  const fileName = configWithErrors.filePath.split('/').pop() || 'vibe-validate.config.yaml';
131
125
  const { message, suggestion } = formatDoctorConfigError({
132
126
  fileName,
@@ -142,6 +136,7 @@ async function checkConfigValid(config, configWithErrors) {
142
136
  // Fallback: try to find config file path
143
137
  const configPath = findConfigPath();
144
138
  if (configPath) {
139
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Need to filter empty strings, not just null/undefined
145
140
  const fileName = configPath.split('/').pop() || 'vibe-validate.config.yaml';
146
141
  return {
147
142
  name: 'Configuration valid',
@@ -185,11 +180,11 @@ async function checkConfigValid(config, configWithErrors) {
185
180
  message: `Loaded successfully (${config.validation.phases.length} phases)`,
186
181
  };
187
182
  }
188
- catch (_error) {
183
+ catch (error) {
189
184
  return {
190
185
  name: 'Configuration valid',
191
186
  passed: false,
192
- message: `Invalid configuration: ${_error instanceof Error ? _error.message : String(_error)}`,
187
+ message: `Invalid configuration: ${error instanceof Error ? error.message : String(error)}`,
193
188
  suggestion: 'Check syntax in vibe-validate.config.yaml',
194
189
  };
195
190
  }
@@ -208,7 +203,7 @@ async function checkPackageManager(config) {
208
203
  };
209
204
  }
210
205
  // Detect package manager from config commands
211
- const firstCommand = config.validation.phases[0]?.steps[0]?.command || '';
206
+ const firstCommand = config.validation.phases[0]?.steps[0]?.command ?? '';
212
207
  const pm = firstCommand.startsWith('pnpm ') ? 'pnpm' : 'npm';
213
208
  try {
214
209
  const version = execSync(`${pm} --version`, { encoding: 'utf8' }).trim();
@@ -218,22 +213,24 @@ async function checkPackageManager(config) {
218
213
  message: `${pm} ${version} is available`,
219
214
  };
220
215
  }
221
- catch (_error) {
216
+ catch (error) {
217
+ const errorMessage = error instanceof Error ? error.message : String(error);
222
218
  return {
223
219
  name: 'Package manager',
224
220
  passed: false,
225
- message: `${pm} not found (required by config commands)`,
221
+ message: `${pm} not found (required by config commands): ${errorMessage}`,
226
222
  suggestion: pm === 'pnpm'
227
223
  ? 'Install pnpm: npm install -g pnpm'
228
224
  : 'npm should be installed with Node.js',
229
225
  };
230
226
  }
231
227
  }
232
- catch (_error) {
228
+ catch (error) {
229
+ const errorMessage = error instanceof Error ? error.message : String(error);
233
230
  return {
234
231
  name: 'Package manager',
235
232
  passed: true,
236
- message: 'Skipped (config check failed)',
233
+ message: `Skipped (config check failed): ${errorMessage}`,
237
234
  };
238
235
  }
239
236
  }
@@ -260,27 +257,22 @@ async function checkWorkflowSync(config) {
260
257
  // Use CI config from vibe-validate config
261
258
  const generateOptions = ciConfigToWorkflowOptions(config);
262
259
  const { inSync, diff } = checkSync(config, generateOptions);
263
- if (inSync) {
264
- return {
265
- name: 'GitHub Actions workflow',
266
- passed: true,
267
- message: 'Workflow is in sync with config',
268
- };
269
- }
270
- else {
271
- return {
272
- name: 'GitHub Actions workflow',
273
- passed: false,
274
- message: `Workflow is out of sync: ${diff || 'differs from config'}`,
275
- suggestion: 'Manual: npx vibe-validate generate-workflow\n 💡 Or run: vibe-validate init --setup-workflow',
276
- };
277
- }
260
+ return inSync ? {
261
+ name: 'GitHub Actions workflow',
262
+ passed: true,
263
+ message: 'Workflow is in sync with config',
264
+ } : {
265
+ name: 'GitHub Actions workflow',
266
+ passed: false,
267
+ message: `Workflow is out of sync: ${diff ?? 'differs from config'}`,
268
+ suggestion: 'Manual: npx vibe-validate generate-workflow\n 💡 Or run: vibe-validate init --setup-workflow',
269
+ };
278
270
  }
279
- catch (_error) {
271
+ catch (error) {
280
272
  return {
281
273
  name: 'GitHub Actions workflow',
282
274
  passed: false,
283
- message: `Failed to check workflow sync: ${_error instanceof Error ? _error.message : String(_error)}`,
275
+ message: `Failed to check workflow sync: ${error instanceof Error ? error.message : String(error)}`,
284
276
  suggestion: 'Verify workflow file syntax',
285
277
  };
286
278
  }
@@ -334,11 +326,12 @@ async function checkPreCommitHook(config) {
334
326
  };
335
327
  }
336
328
  }
337
- catch (_error) {
329
+ catch (error) {
330
+ const errorMessage = error instanceof Error ? error.message : String(error);
338
331
  return {
339
332
  name: 'Pre-commit hook',
340
333
  passed: false,
341
- message: 'Pre-commit hook exists but unreadable',
334
+ message: `Pre-commit hook exists but unreadable: ${errorMessage}`,
342
335
  suggestion: 'Fix file permissions or set hooks.preCommit.enabled=false',
343
336
  };
344
337
  }
@@ -396,20 +389,22 @@ async function checkVersion() {
396
389
  };
397
390
  }
398
391
  }
399
- catch (_npmError) {
392
+ catch (npmError) {
400
393
  // npm registry unavailable - not a critical error
394
+ const errorMessage = npmError instanceof Error ? npmError.message : String(npmError);
401
395
  return {
402
396
  name: 'vibe-validate version',
403
397
  passed: true,
404
- message: `Current version: ${currentVersion} (unable to check for updates)`,
398
+ message: `Current version: ${currentVersion} (unable to check for updates: ${errorMessage})`,
405
399
  };
406
400
  }
407
401
  }
408
- catch (_error) {
402
+ catch (error) {
403
+ const errorMessage = error instanceof Error ? error.message : String(error);
409
404
  return {
410
405
  name: 'vibe-validate version',
411
406
  passed: true,
412
- message: 'Unable to determine version',
407
+ message: `Unable to determine version: ${errorMessage}`,
413
408
  };
414
409
  }
415
410
  }
@@ -418,6 +413,7 @@ async function checkVersion() {
418
413
  */
419
414
  function checkGitignoreStateFile() {
420
415
  const gitignorePath = '.gitignore';
416
+ // eslint-disable-next-line sonarjs/deprecation -- Intentionally checking deprecated file location for migration guidance
421
417
  const stateFileName = DEPRECATED_STATE_FILE;
422
418
  // Check if .gitignore exists
423
419
  if (!existsSync(gitignorePath)) {
@@ -434,7 +430,9 @@ function checkGitignoreStateFile() {
434
430
  return {
435
431
  name: 'Gitignore state file (deprecated)',
436
432
  passed: false,
433
+ // eslint-disable-next-line sonarjs/deprecation -- Intentionally checking deprecated file location for migration guidance
437
434
  message: `${DEPRECATED_STATE_FILE} in .gitignore (deprecated - can be removed)`,
435
+ // eslint-disable-next-line sonarjs/deprecation -- Intentionally checking deprecated file location for migration guidance
438
436
  suggestion: `Remove from .gitignore: sed -i.bak '/${DEPRECATED_STATE_FILE}/d' .gitignore && rm .gitignore.bak\n ℹ️ Validation now uses git notes instead of state file`,
439
437
  };
440
438
  }
@@ -446,11 +444,12 @@ function checkGitignoreStateFile() {
446
444
  };
447
445
  }
448
446
  }
449
- catch (_error) {
447
+ catch (error) {
448
+ const errorMessage = error instanceof Error ? error.message : String(error);
450
449
  return {
451
450
  name: 'Gitignore state file',
452
451
  passed: false,
453
- message: '.gitignore exists but is unreadable',
452
+ message: `.gitignore exists but is unreadable: ${errorMessage}`,
454
453
  suggestion: 'Fix file permissions: chmod 644 .gitignore',
455
454
  };
456
455
  }
@@ -459,13 +458,16 @@ function checkGitignoreStateFile() {
459
458
  * Check if deprecated validation state file exists
460
459
  */
461
460
  function checkValidationState() {
461
+ // eslint-disable-next-line sonarjs/deprecation -- Intentionally checking deprecated file location for migration guidance
462
462
  const statePath = DEPRECATED_STATE_FILE;
463
463
  // Check if deprecated state file exists
464
464
  if (existsSync(statePath)) {
465
465
  return {
466
466
  name: 'Validation state (deprecated)',
467
467
  passed: false,
468
+ // eslint-disable-next-line sonarjs/deprecation -- Intentionally checking deprecated file location for migration guidance
468
469
  message: `${DEPRECATED_STATE_FILE} found (deprecated file - safe to remove)`,
470
+ // eslint-disable-next-line sonarjs/deprecation -- Intentionally checking deprecated file location for migration guidance
469
471
  suggestion: `Remove deprecated state file: rm ${DEPRECATED_STATE_FILE}\n ℹ️ Validation now uses git notes for improved caching`,
470
472
  };
471
473
  }
@@ -498,12 +500,13 @@ async function checkHistoryHealth() {
498
500
  suggestion: 'Prune old history: vibe-validate history prune --older-than "90 days"',
499
501
  };
500
502
  }
501
- catch (_error) {
503
+ catch (error) {
502
504
  // Git notes not available or other error - not critical
505
+ const errorMessage = error instanceof Error ? error.message : String(error);
503
506
  return {
504
507
  name: 'Validation history',
505
508
  passed: true,
506
- message: 'History unavailable (not in git repo or no validation runs yet)',
509
+ message: `History unavailable (not in git repo or no validation runs yet): ${errorMessage}`,
507
510
  };
508
511
  }
509
512
  }
@@ -530,8 +533,9 @@ async function checkMainBranch(config) {
530
533
  message: `Branch '${mainBranch}' exists locally`,
531
534
  };
532
535
  }
533
- catch (_localError) {
536
+ catch (localError) {
534
537
  // Local branch doesn't exist, check for remote branch
538
+ const localErrorMsg = localError instanceof Error ? localError.message : String(localError);
535
539
  try {
536
540
  execSync(`git rev-parse --verify ${remoteOrigin}/${mainBranch}`, { stdio: 'pipe' });
537
541
  return {
@@ -540,21 +544,23 @@ async function checkMainBranch(config) {
540
544
  message: `Branch '${mainBranch}' exists on remote '${remoteOrigin}' (fetch-depth: 0 required in CI)`,
541
545
  };
542
546
  }
543
- catch (_remoteError) {
547
+ catch (remoteError) {
548
+ const remoteErrorMsg = remoteError instanceof Error ? remoteError.message : String(remoteError);
544
549
  return {
545
550
  name: 'Git main branch',
546
551
  passed: false,
547
- message: `Configured main branch '${mainBranch}' does not exist locally or on remote '${remoteOrigin}'`,
552
+ message: `Configured main branch '${mainBranch}' does not exist locally (${localErrorMsg}) or on remote '${remoteOrigin}' (${remoteErrorMsg})`,
548
553
  suggestion: `Create branch: git checkout -b ${mainBranch} OR update config to use existing branch (e.g., 'master', 'develop')`,
549
554
  };
550
555
  }
551
556
  }
552
557
  }
553
- catch (_error) {
558
+ catch (error) {
559
+ const errorMessage = error instanceof Error ? error.message : String(error);
554
560
  return {
555
561
  name: 'Git main branch',
556
562
  passed: true,
557
- message: 'Skipped (config or git error)',
563
+ message: `Skipped (config or git error): ${errorMessage}`,
558
564
  };
559
565
  }
560
566
  }
@@ -593,20 +599,22 @@ async function checkRemoteOrigin(config) {
593
599
  };
594
600
  }
595
601
  }
596
- catch (_error) {
602
+ catch (error) {
603
+ const errorMessage = error instanceof Error ? error.message : String(error);
597
604
  return {
598
605
  name: 'Git remote origin',
599
606
  passed: false,
600
- message: 'Failed to list git remotes',
607
+ message: `Failed to list git remotes: ${errorMessage}`,
601
608
  suggestion: 'Verify git repository is initialized',
602
609
  };
603
610
  }
604
611
  }
605
- catch (_error) {
612
+ catch (error) {
613
+ const errorMessage = error instanceof Error ? error.message : String(error);
606
614
  return {
607
615
  name: 'Git remote origin',
608
616
  passed: true,
609
- message: 'Skipped (config or git error)',
617
+ message: `Skipped (config or git error): ${errorMessage}`,
610
618
  };
611
619
  }
612
620
  }
@@ -645,20 +653,64 @@ async function checkRemoteMainBranch(config) {
645
653
  message: `Branch '${mainBranch}' exists on remote '${remoteOrigin}'`,
646
654
  };
647
655
  }
648
- catch (_error) {
656
+ catch (error) {
657
+ const errorMessage = error instanceof Error ? error.message : String(error);
649
658
  return {
650
659
  name: 'Git remote main branch',
651
660
  passed: false,
652
- message: `Branch '${mainBranch}' does not exist on remote '${remoteOrigin}'`,
661
+ message: `Branch '${mainBranch}' does not exist on remote '${remoteOrigin}': ${errorMessage}`,
653
662
  suggestion: `Push branch: git push ${remoteOrigin} ${mainBranch} OR update config to match remote branch name`,
654
663
  };
655
664
  }
656
665
  }
657
- catch (_error) {
666
+ catch (error) {
667
+ const errorMessage = error instanceof Error ? error.message : String(error);
658
668
  return {
659
669
  name: 'Git remote main branch',
660
670
  passed: true,
661
- message: 'Skipped (config or git error)',
671
+ message: `Skipped (config or git error): ${errorMessage}`,
672
+ };
673
+ }
674
+ }
675
+ /**
676
+ * Get tool version by trying multiple version flag variants
677
+ */
678
+ function getToolVersion(toolName) {
679
+ try {
680
+ return execSync(`${toolName} version`, { encoding: 'utf8', stdio: 'pipe' }).trim();
681
+ }
682
+ catch (versionError) {
683
+ console.debug(`${toolName} version failed: ${versionError instanceof Error ? versionError.message : String(versionError)}`);
684
+ return execSync(`${toolName} --version`, { encoding: 'utf8', stdio: 'pipe' }).trim();
685
+ }
686
+ }
687
+ /**
688
+ * Check if scanning tool is available
689
+ */
690
+ function checkScanningToolAvailable(toolName) {
691
+ try {
692
+ const version = getToolVersion(toolName);
693
+ return {
694
+ name: 'Pre-commit secret scanning',
695
+ passed: true,
696
+ message: `Secret scanning enabled with ${toolName} ${version}`,
697
+ };
698
+ }
699
+ catch (error) {
700
+ const errorMessage = error instanceof Error ? error.message : String(error);
701
+ const isCI = process.env.CI === 'true' || process.env.CI === '1';
702
+ if (isCI) {
703
+ return {
704
+ name: 'Pre-commit secret scanning',
705
+ passed: true,
706
+ message: `Secret scanning enabled (pre-commit only, not needed in CI)`,
707
+ };
708
+ }
709
+ return {
710
+ name: 'Pre-commit secret scanning',
711
+ passed: true,
712
+ message: `Secret scanning enabled but '${toolName}' not found: ${errorMessage}`,
713
+ suggestion: `Install ${toolName}:\n • gitleaks: brew install gitleaks\n • Or disable: set hooks.preCommit.secretScanning.enabled=false in config`,
662
714
  };
663
715
  }
664
716
  }
@@ -675,7 +727,6 @@ async function checkSecretScanning(config) {
675
727
  };
676
728
  }
677
729
  const secretScanning = config.hooks?.preCommit?.secretScanning;
678
- // If not configured at all, recommend enabling it
679
730
  if (!secretScanning) {
680
731
  return {
681
732
  name: 'Pre-commit secret scanning',
@@ -684,7 +735,6 @@ async function checkSecretScanning(config) {
684
735
  suggestion: 'Recommended: Enable secret scanning to prevent credential leaks\n • Add to config: hooks.preCommit.secretScanning.enabled=true\n • scanCommand: "gitleaks protect --staged --verbose"\n • Install gitleaks: brew install gitleaks',
685
736
  };
686
737
  }
687
- // If explicitly disabled, acknowledge user choice
688
738
  if (secretScanning.enabled === false) {
689
739
  return {
690
740
  name: 'Pre-commit secret scanning',
@@ -692,58 +742,23 @@ async function checkSecretScanning(config) {
692
742
  message: 'Secret scanning disabled in config (user preference)',
693
743
  };
694
744
  }
695
- // If enabled, check if tool is available
696
- if (secretScanning.scanCommand) {
697
- // Extract tool name (first word of command)
698
- const toolName = secretScanning.scanCommand.split(' ')[0];
699
- try {
700
- // Try to get tool version
701
- let version;
702
- try {
703
- version = execSync(`${toolName} version`, { encoding: 'utf8', stdio: 'pipe' }).trim();
704
- }
705
- catch (_versionError) {
706
- // Try --version flag
707
- version = execSync(`${toolName} --version`, { encoding: 'utf8', stdio: 'pipe' }).trim();
708
- }
709
- return {
710
- name: 'Pre-commit secret scanning',
711
- passed: true,
712
- message: `Secret scanning enabled with ${toolName} ${version}`,
713
- };
714
- }
715
- catch (_error) {
716
- // Tool not found
717
- // In CI, this is expected (secret scanning is pre-commit only, not needed in CI)
718
- const isCI = process.env.CI === 'true' || process.env.CI === '1';
719
- if (isCI) {
720
- return {
721
- name: 'Pre-commit secret scanning',
722
- passed: true,
723
- message: `Secret scanning enabled (pre-commit only, not needed in CI)`,
724
- };
725
- }
726
- return {
727
- name: 'Pre-commit secret scanning',
728
- passed: true, // Advisory only, never fails
729
- message: `Secret scanning enabled but '${toolName}' not found`,
730
- suggestion: `Install ${toolName}:\n • gitleaks: brew install gitleaks\n • Or disable: set hooks.preCommit.secretScanning.enabled=false in config`,
731
- };
732
- }
745
+ if (!secretScanning.scanCommand) {
746
+ return {
747
+ name: 'Pre-commit secret scanning',
748
+ passed: true,
749
+ message: 'Secret scanning enabled but no scanCommand configured',
750
+ suggestion: 'Add hooks.preCommit.secretScanning.scanCommand to config',
751
+ };
733
752
  }
734
- // Enabled but no scanCommand (should be caught by schema validation)
735
- return {
736
- name: 'Pre-commit secret scanning',
737
- passed: true,
738
- message: 'Secret scanning enabled but no scanCommand configured',
739
- suggestion: 'Add hooks.preCommit.secretScanning.scanCommand to config',
740
- };
753
+ const toolName = secretScanning.scanCommand.split(' ')[0];
754
+ return checkScanningToolAvailable(toolName);
741
755
  }
742
- catch (_error) {
756
+ catch (error) {
757
+ const errorMessage = error instanceof Error ? error.message : String(error);
743
758
  return {
744
759
  name: 'Pre-commit secret scanning',
745
760
  passed: true,
746
- message: 'Skipped (config or execution error)',
761
+ message: `Skipped (config or execution error): ${errorMessage}`,
747
762
  };
748
763
  }
749
764
  }
@@ -760,8 +775,10 @@ export async function runDoctor(options = {}) {
760
775
  config = await loadConfig();
761
776
  configWithErrors = await loadConfigWithErrors();
762
777
  }
763
- catch (_error) {
778
+ catch (error) {
764
779
  // Config load error will be caught by checkConfigValid
780
+ // Intentionally suppressing error here as it will be reported by checkConfigValid
781
+ console.debug(`Config load failed: ${error instanceof Error ? error.message : String(error)}`);
765
782
  config = null;
766
783
  configWithErrors = { config: null, errors: null, filePath: null };
767
784
  }
@@ -801,6 +818,61 @@ export async function runDoctor(options = {}) {
801
818
  passedChecks,
802
819
  };
803
820
  }
821
+ /**
822
+ * Output doctor results in YAML format
823
+ */
824
+ async function outputDoctorYaml(result) {
825
+ // Small delay to ensure stderr is flushed
826
+ await new Promise(resolve => setTimeout(resolve, 10));
827
+ // RFC 4627 separator
828
+ process.stdout.write('---\n');
829
+ // Write pure YAML
830
+ process.stdout.write(stringifyYaml(result));
831
+ // CRITICAL: Wait for stdout to flush before exiting
832
+ await new Promise(resolve => {
833
+ if (process.stdout.write('')) {
834
+ resolve();
835
+ }
836
+ else {
837
+ process.stdout.once('drain', resolve);
838
+ }
839
+ });
840
+ }
841
+ /**
842
+ * Display doctor results in human-friendly format
843
+ */
844
+ function displayDoctorResults(result) {
845
+ console.log('🩺 vibe-validate Doctor\n');
846
+ const modeMessage = result.verboseMode
847
+ ? 'Running diagnostic checks (verbose mode)...\n'
848
+ : 'Running diagnostic checks...\n';
849
+ console.log(modeMessage);
850
+ // Print each check
851
+ for (const check of result.checks) {
852
+ const icon = check.passed ? '✅' : '❌';
853
+ console.log(`${icon} ${check.name}`);
854
+ console.log(` ${check.message}`);
855
+ if (check.suggestion) {
856
+ console.log(` 💡 ${check.suggestion}`);
857
+ }
858
+ console.log('');
859
+ }
860
+ // Summary
861
+ console.log(`📊 Results: ${result.passedChecks}/${result.totalChecks} checks passed\n`);
862
+ if (result.allPassed) {
863
+ console.log('✨ All checks passed! Your vibe-validate setup looks healthy.');
864
+ if (!result.verboseMode) {
865
+ console.log(' (Use --verbose to see all checks)');
866
+ }
867
+ }
868
+ else {
869
+ console.log('⚠️ Some checks failed. See suggestions above to fix.');
870
+ if (!result.verboseMode) {
871
+ console.log(' (Use --verbose to see all checks including passing ones)');
872
+ }
873
+ process.exit(1);
874
+ }
875
+ }
804
876
  /**
805
877
  * Main command handler for Commander.js
806
878
  */
@@ -815,62 +887,15 @@ export function doctorCommand(program) {
815
887
  try {
816
888
  const result = await runDoctor({ verbose });
817
889
  if (options.yaml) {
818
- // YAML mode: Output structured result to stdout
819
- // Small delay to ensure stderr is flushed
820
- await new Promise(resolve => setTimeout(resolve, 10));
821
- // RFC 4627 separator
822
- process.stdout.write('---\n');
823
- // Write pure YAML
824
- process.stdout.write(stringifyYaml(result));
825
- // CRITICAL: Wait for stdout to flush before exiting
826
- await new Promise(resolve => {
827
- if (process.stdout.write('')) {
828
- resolve();
829
- }
830
- else {
831
- process.stdout.once('drain', resolve);
832
- }
833
- });
890
+ await outputDoctorYaml(result);
834
891
  }
835
892
  else {
836
- // Human-friendly output
837
- console.log('🩺 vibe-validate Doctor\n');
838
- if (result.verboseMode) {
839
- console.log('Running diagnostic checks (verbose mode)...\n');
840
- }
841
- else {
842
- console.log('Running diagnostic checks...\n');
843
- }
844
- // Print each check
845
- for (const check of result.checks) {
846
- const icon = check.passed ? '✅' : '❌';
847
- console.log(`${icon} ${check.name}`);
848
- console.log(` ${check.message}`);
849
- if (check.suggestion) {
850
- console.log(` 💡 ${check.suggestion}`);
851
- }
852
- console.log('');
853
- }
854
- // Summary
855
- console.log(`📊 Results: ${result.passedChecks}/${result.totalChecks} checks passed\n`);
856
- if (result.allPassed) {
857
- console.log('✨ All checks passed! Your vibe-validate setup looks healthy.');
858
- if (!result.verboseMode) {
859
- console.log(' (Use --verbose to see all checks)');
860
- }
861
- }
862
- else {
863
- console.log('⚠️ Some checks failed. See suggestions above to fix.');
864
- if (!result.verboseMode) {
865
- console.log(' (Use --verbose to see all checks including passing ones)');
866
- }
867
- process.exit(1);
868
- }
893
+ displayDoctorResults(result);
869
894
  }
870
895
  }
871
- catch (_error) {
896
+ catch (error) {
872
897
  console.error('❌ Doctor check failed:');
873
- console.error(_error instanceof Error ? _error.message : String(_error));
898
+ console.error(error instanceof Error ? error.message : String(error));
874
899
  process.exit(1);
875
900
  }
876
901
  });