@vibe-validate/cli 0.14.2 → 0.15.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -6
- package/bin/vibe-validate +131 -0
- package/config-templates/minimal.yaml +1 -1
- package/config-templates/typescript-library.yaml +1 -1
- package/config-templates/typescript-nodejs.yaml +1 -1
- package/config-templates/typescript-react.yaml +1 -1
- package/dist/bin.js +41 -21
- package/dist/bin.js.map +1 -1
- package/dist/commands/cleanup.d.ts +1 -1
- package/dist/commands/cleanup.d.ts.map +1 -1
- package/dist/commands/cleanup.js +135 -16
- package/dist/commands/cleanup.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +13 -10
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +253 -189
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-workflow.d.ts.map +1 -1
- package/dist/commands/generate-workflow.js +15 -13
- package/dist/commands/generate-workflow.js.map +1 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +305 -98
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pre-commit.d.ts.map +1 -1
- package/dist/commands/pre-commit.js +8 -3
- package/dist/commands/pre-commit.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +620 -217
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/state.d.ts.map +1 -1
- package/dist/commands/state.js +4 -7
- package/dist/commands/state.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +7 -7
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/watch-pr.d.ts.map +1 -1
- package/dist/commands/watch-pr.js +73 -49
- package/dist/commands/watch-pr.js.map +1 -1
- package/dist/schemas/run-result-schema-export.d.ts +32 -0
- package/dist/schemas/run-result-schema-export.d.ts.map +1 -0
- package/dist/schemas/run-result-schema-export.js +40 -0
- package/dist/schemas/run-result-schema-export.js.map +1 -0
- package/dist/schemas/run-result-schema.d.ts +850 -0
- package/dist/schemas/run-result-schema.d.ts.map +1 -0
- package/dist/schemas/run-result-schema.js +67 -0
- package/dist/schemas/run-result-schema.js.map +1 -0
- package/dist/schemas/watch-pr-schema.d.ts +431 -33
- package/dist/schemas/watch-pr-schema.d.ts.map +1 -1
- package/dist/schemas/watch-pr-schema.js +2 -2
- package/dist/schemas/watch-pr-schema.js.map +1 -1
- package/dist/scripts/generate-run-result-schema.d.ts +10 -0
- package/dist/scripts/generate-run-result-schema.d.ts.map +1 -0
- package/dist/scripts/generate-run-result-schema.js +20 -0
- package/dist/scripts/generate-run-result-schema.js.map +1 -0
- package/dist/scripts/generate-watch-pr-schema.js +3 -3
- package/dist/scripts/generate-watch-pr-schema.js.map +1 -1
- package/dist/services/ci-provider.d.ts +21 -6
- package/dist/services/ci-provider.d.ts.map +1 -1
- package/dist/services/ci-providers/github-actions.d.ts +21 -0
- package/dist/services/ci-providers/github-actions.d.ts.map +1 -1
- package/dist/services/ci-providers/github-actions.js +65 -49
- package/dist/services/ci-providers/github-actions.js.map +1 -1
- package/dist/utils/check-validation.d.ts.map +1 -1
- package/dist/utils/check-validation.js +9 -5
- package/dist/utils/check-validation.js.map +1 -1
- package/dist/utils/config-error-reporter.js +9 -7
- package/dist/utils/config-error-reporter.js.map +1 -1
- package/dist/utils/config-loader.js +5 -5
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/git-detection.d.ts +0 -22
- package/dist/utils/git-detection.d.ts.map +1 -1
- package/dist/utils/git-detection.js +64 -56
- package/dist/utils/git-detection.js.map +1 -1
- package/dist/utils/pid-lock.js +7 -7
- package/dist/utils/pid-lock.js.map +1 -1
- package/dist/utils/project-id.d.ts.map +1 -1
- package/dist/utils/project-id.js +8 -6
- package/dist/utils/project-id.js.map +1 -1
- package/dist/utils/runner-adapter.js +4 -3
- package/dist/utils/runner-adapter.js.map +1 -1
- package/dist/utils/setup-checks/hooks-check.js +3 -3
- package/dist/utils/setup-checks/hooks-check.js.map +1 -1
- package/dist/utils/setup-checks/workflow-check.js +3 -3
- package/dist/utils/setup-checks/workflow-check.js.map +1 -1
- package/dist/utils/temp-files.d.ts +67 -0
- package/dist/utils/temp-files.d.ts.map +1 -0
- package/dist/utils/temp-files.js +202 -0
- package/dist/utils/temp-files.js.map +1 -0
- package/dist/utils/template-discovery.d.ts.map +1 -1
- package/dist/utils/template-discovery.js +5 -4
- package/dist/utils/template-discovery.js.map +1 -1
- package/dist/utils/validate-workflow.d.ts.map +1 -1
- package/dist/utils/validate-workflow.js +169 -150
- package/dist/utils/validate-workflow.js.map +1 -1
- package/dist/vibe-validate +131 -0
- package/package.json +11 -9
- package/run-result.schema.json +186 -0
- package/watch-pr-result.schema.json +128 -6
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 (
|
|
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:
|
|
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 (
|
|
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:
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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',
|
|
@@ -150,7 +145,7 @@ async function checkConfigValid(config, configWithErrors) {
|
|
|
150
145
|
suggestion: [
|
|
151
146
|
`Fix syntax/validation errors in ${fileName}`,
|
|
152
147
|
'See configuration docs: https://github.com/jdutton/vibe-validate/blob/main/docs/configuration-reference.md',
|
|
153
|
-
'JSON Schema for IDE validation: https://raw.githubusercontent.com/jdutton/vibe-validate/main/packages/config/
|
|
148
|
+
'JSON Schema for IDE validation: https://raw.githubusercontent.com/jdutton/vibe-validate/main/packages/config/config.schema.json',
|
|
154
149
|
'Example YAML configs: https://github.com/jdutton/vibe-validate/tree/main/packages/cli/config-templates'
|
|
155
150
|
].join('\n '),
|
|
156
151
|
};
|
|
@@ -174,7 +169,7 @@ async function checkConfigValid(config, configWithErrors) {
|
|
|
174
169
|
' https://raw.githubusercontent.com/jdutton/vibe-validate/main/config-templates/typescript-nodejs.yaml',
|
|
175
170
|
'',
|
|
176
171
|
'JSON Schema for IDE validation:',
|
|
177
|
-
'https://raw.githubusercontent.com/jdutton/vibe-validate/main/packages/config/
|
|
172
|
+
'https://raw.githubusercontent.com/jdutton/vibe-validate/main/packages/config/config.schema.json',
|
|
178
173
|
].join('\n '),
|
|
179
174
|
};
|
|
180
175
|
}
|
|
@@ -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 (
|
|
183
|
+
catch (error) {
|
|
189
184
|
return {
|
|
190
185
|
name: 'Configuration valid',
|
|
191
186
|
passed: false,
|
|
192
|
-
message: `Invalid configuration: ${
|
|
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 (
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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 (
|
|
271
|
+
catch (error) {
|
|
280
272
|
return {
|
|
281
273
|
name: 'GitHub Actions workflow',
|
|
282
274
|
passed: false,
|
|
283
|
-
message: `Failed to check workflow sync: ${
|
|
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 (
|
|
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:
|
|
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 (
|
|
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 (
|
|
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:
|
|
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 (
|
|
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:
|
|
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
|
}
|
|
@@ -477,6 +479,45 @@ function checkValidationState() {
|
|
|
477
479
|
};
|
|
478
480
|
}
|
|
479
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Check for v0.15.0 cache migration recommendation
|
|
484
|
+
*
|
|
485
|
+
* v0.15.0 introduced run command caching with a new schema.
|
|
486
|
+
* Old cached results may be missing new fields (e.g., suggestedDirectCommand).
|
|
487
|
+
* Recommend clearing cache on upgrade to avoid stale data.
|
|
488
|
+
*/
|
|
489
|
+
function checkCacheMigration() {
|
|
490
|
+
try {
|
|
491
|
+
// Check if any run cache notes exist
|
|
492
|
+
const result = execSync('git for-each-ref --count=1 refs/notes/vibe-validate/run/', {
|
|
493
|
+
encoding: 'utf-8',
|
|
494
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
495
|
+
});
|
|
496
|
+
if (result.trim()) {
|
|
497
|
+
// Cache exists - recommend clearing for clean v0.15.0+ schema
|
|
498
|
+
return {
|
|
499
|
+
name: 'Run cache migration (v0.15.0+)',
|
|
500
|
+
passed: true, // Not a failure, just informational
|
|
501
|
+
message: 'Run cache detected from earlier version',
|
|
502
|
+
suggestion: `Clear old cache for v0.15.0+ schema:\n git for-each-ref refs/notes/vibe-validate --format='%(refname)' | xargs -n 1 git update-ref -d\n ℹ️ Clears both run cache and validation history (will rebuild on next run)`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
name: 'Run cache migration',
|
|
507
|
+
passed: true,
|
|
508
|
+
message: 'No run cache found (clean state)',
|
|
509
|
+
};
|
|
510
|
+
// eslint-disable-next-line sonarjs/no-ignored-exceptions -- Git command failure is non-critical for migration check
|
|
511
|
+
}
|
|
512
|
+
catch (_error) {
|
|
513
|
+
// Git command failed or no git notes - that's fine
|
|
514
|
+
return {
|
|
515
|
+
name: 'Run cache migration',
|
|
516
|
+
passed: true,
|
|
517
|
+
message: 'No run cache to migrate',
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
480
521
|
/**
|
|
481
522
|
* Check validation history health
|
|
482
523
|
*/
|
|
@@ -498,12 +539,13 @@ async function checkHistoryHealth() {
|
|
|
498
539
|
suggestion: 'Prune old history: vibe-validate history prune --older-than "90 days"',
|
|
499
540
|
};
|
|
500
541
|
}
|
|
501
|
-
catch (
|
|
542
|
+
catch (error) {
|
|
502
543
|
// Git notes not available or other error - not critical
|
|
544
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
503
545
|
return {
|
|
504
546
|
name: 'Validation history',
|
|
505
547
|
passed: true,
|
|
506
|
-
message:
|
|
548
|
+
message: `History unavailable (not in git repo or no validation runs yet): ${errorMessage}`,
|
|
507
549
|
};
|
|
508
550
|
}
|
|
509
551
|
}
|
|
@@ -530,8 +572,9 @@ async function checkMainBranch(config) {
|
|
|
530
572
|
message: `Branch '${mainBranch}' exists locally`,
|
|
531
573
|
};
|
|
532
574
|
}
|
|
533
|
-
catch (
|
|
575
|
+
catch (localError) {
|
|
534
576
|
// Local branch doesn't exist, check for remote branch
|
|
577
|
+
const localErrorMsg = localError instanceof Error ? localError.message : String(localError);
|
|
535
578
|
try {
|
|
536
579
|
execSync(`git rev-parse --verify ${remoteOrigin}/${mainBranch}`, { stdio: 'pipe' });
|
|
537
580
|
return {
|
|
@@ -540,21 +583,23 @@ async function checkMainBranch(config) {
|
|
|
540
583
|
message: `Branch '${mainBranch}' exists on remote '${remoteOrigin}' (fetch-depth: 0 required in CI)`,
|
|
541
584
|
};
|
|
542
585
|
}
|
|
543
|
-
catch (
|
|
586
|
+
catch (remoteError) {
|
|
587
|
+
const remoteErrorMsg = remoteError instanceof Error ? remoteError.message : String(remoteError);
|
|
544
588
|
return {
|
|
545
589
|
name: 'Git main branch',
|
|
546
590
|
passed: false,
|
|
547
|
-
message: `Configured main branch '${mainBranch}' does not exist locally or on remote '${remoteOrigin}'`,
|
|
591
|
+
message: `Configured main branch '${mainBranch}' does not exist locally (${localErrorMsg}) or on remote '${remoteOrigin}' (${remoteErrorMsg})`,
|
|
548
592
|
suggestion: `Create branch: git checkout -b ${mainBranch} OR update config to use existing branch (e.g., 'master', 'develop')`,
|
|
549
593
|
};
|
|
550
594
|
}
|
|
551
595
|
}
|
|
552
596
|
}
|
|
553
|
-
catch (
|
|
597
|
+
catch (error) {
|
|
598
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
554
599
|
return {
|
|
555
600
|
name: 'Git main branch',
|
|
556
601
|
passed: true,
|
|
557
|
-
message:
|
|
602
|
+
message: `Skipped (config or git error): ${errorMessage}`,
|
|
558
603
|
};
|
|
559
604
|
}
|
|
560
605
|
}
|
|
@@ -593,20 +638,22 @@ async function checkRemoteOrigin(config) {
|
|
|
593
638
|
};
|
|
594
639
|
}
|
|
595
640
|
}
|
|
596
|
-
catch (
|
|
641
|
+
catch (error) {
|
|
642
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
597
643
|
return {
|
|
598
644
|
name: 'Git remote origin',
|
|
599
645
|
passed: false,
|
|
600
|
-
message:
|
|
646
|
+
message: `Failed to list git remotes: ${errorMessage}`,
|
|
601
647
|
suggestion: 'Verify git repository is initialized',
|
|
602
648
|
};
|
|
603
649
|
}
|
|
604
650
|
}
|
|
605
|
-
catch (
|
|
651
|
+
catch (error) {
|
|
652
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
606
653
|
return {
|
|
607
654
|
name: 'Git remote origin',
|
|
608
655
|
passed: true,
|
|
609
|
-
message:
|
|
656
|
+
message: `Skipped (config or git error): ${errorMessage}`,
|
|
610
657
|
};
|
|
611
658
|
}
|
|
612
659
|
}
|
|
@@ -645,20 +692,64 @@ async function checkRemoteMainBranch(config) {
|
|
|
645
692
|
message: `Branch '${mainBranch}' exists on remote '${remoteOrigin}'`,
|
|
646
693
|
};
|
|
647
694
|
}
|
|
648
|
-
catch (
|
|
695
|
+
catch (error) {
|
|
696
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
649
697
|
return {
|
|
650
698
|
name: 'Git remote main branch',
|
|
651
699
|
passed: false,
|
|
652
|
-
message: `Branch '${mainBranch}' does not exist on remote '${remoteOrigin}'`,
|
|
700
|
+
message: `Branch '${mainBranch}' does not exist on remote '${remoteOrigin}': ${errorMessage}`,
|
|
653
701
|
suggestion: `Push branch: git push ${remoteOrigin} ${mainBranch} OR update config to match remote branch name`,
|
|
654
702
|
};
|
|
655
703
|
}
|
|
656
704
|
}
|
|
657
|
-
catch (
|
|
705
|
+
catch (error) {
|
|
706
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
658
707
|
return {
|
|
659
708
|
name: 'Git remote main branch',
|
|
660
709
|
passed: true,
|
|
661
|
-
message:
|
|
710
|
+
message: `Skipped (config or git error): ${errorMessage}`,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Get tool version by trying multiple version flag variants
|
|
716
|
+
*/
|
|
717
|
+
function getToolVersion(toolName) {
|
|
718
|
+
try {
|
|
719
|
+
return execSync(`${toolName} version`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
// Fallback to --version flag
|
|
723
|
+
return execSync(`${toolName} --version`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Check if scanning tool is available
|
|
728
|
+
*/
|
|
729
|
+
function checkScanningToolAvailable(toolName) {
|
|
730
|
+
try {
|
|
731
|
+
const version = getToolVersion(toolName);
|
|
732
|
+
return {
|
|
733
|
+
name: 'Pre-commit secret scanning',
|
|
734
|
+
passed: true,
|
|
735
|
+
message: `Secret scanning enabled with ${toolName} ${version}`,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
740
|
+
const isCI = process.env.CI === 'true' || process.env.CI === '1';
|
|
741
|
+
if (isCI) {
|
|
742
|
+
return {
|
|
743
|
+
name: 'Pre-commit secret scanning',
|
|
744
|
+
passed: true,
|
|
745
|
+
message: `Secret scanning enabled (pre-commit only, not needed in CI)`,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
name: 'Pre-commit secret scanning',
|
|
750
|
+
passed: true,
|
|
751
|
+
message: `Secret scanning enabled but '${toolName}' not found: ${errorMessage}`,
|
|
752
|
+
suggestion: `Install ${toolName}:\n • gitleaks: brew install gitleaks\n • Or disable: set hooks.preCommit.secretScanning.enabled=false in config`,
|
|
662
753
|
};
|
|
663
754
|
}
|
|
664
755
|
}
|
|
@@ -675,7 +766,6 @@ async function checkSecretScanning(config) {
|
|
|
675
766
|
};
|
|
676
767
|
}
|
|
677
768
|
const secretScanning = config.hooks?.preCommit?.secretScanning;
|
|
678
|
-
// If not configured at all, recommend enabling it
|
|
679
769
|
if (!secretScanning) {
|
|
680
770
|
return {
|
|
681
771
|
name: 'Pre-commit secret scanning',
|
|
@@ -684,7 +774,6 @@ async function checkSecretScanning(config) {
|
|
|
684
774
|
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
775
|
};
|
|
686
776
|
}
|
|
687
|
-
// If explicitly disabled, acknowledge user choice
|
|
688
777
|
if (secretScanning.enabled === false) {
|
|
689
778
|
return {
|
|
690
779
|
name: 'Pre-commit secret scanning',
|
|
@@ -692,58 +781,23 @@ async function checkSecretScanning(config) {
|
|
|
692
781
|
message: 'Secret scanning disabled in config (user preference)',
|
|
693
782
|
};
|
|
694
783
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
}
|
|
784
|
+
if (!secretScanning.scanCommand) {
|
|
785
|
+
return {
|
|
786
|
+
name: 'Pre-commit secret scanning',
|
|
787
|
+
passed: true,
|
|
788
|
+
message: 'Secret scanning enabled but no scanCommand configured',
|
|
789
|
+
suggestion: 'Add hooks.preCommit.secretScanning.scanCommand to config',
|
|
790
|
+
};
|
|
733
791
|
}
|
|
734
|
-
|
|
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
|
-
};
|
|
792
|
+
const toolName = secretScanning.scanCommand.split(' ')[0];
|
|
793
|
+
return checkScanningToolAvailable(toolName);
|
|
741
794
|
}
|
|
742
|
-
catch (
|
|
795
|
+
catch (error) {
|
|
796
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
743
797
|
return {
|
|
744
798
|
name: 'Pre-commit secret scanning',
|
|
745
799
|
passed: true,
|
|
746
|
-
message:
|
|
800
|
+
message: `Skipped (config or execution error): ${errorMessage}`,
|
|
747
801
|
};
|
|
748
802
|
}
|
|
749
803
|
}
|
|
@@ -760,8 +814,9 @@ export async function runDoctor(options = {}) {
|
|
|
760
814
|
config = await loadConfig();
|
|
761
815
|
configWithErrors = await loadConfigWithErrors();
|
|
762
816
|
}
|
|
763
|
-
catch
|
|
817
|
+
catch {
|
|
764
818
|
// Config load error will be caught by checkConfigValid
|
|
819
|
+
// Intentionally suppressing error here as it will be reported by checkConfigValid
|
|
765
820
|
config = null;
|
|
766
821
|
configWithErrors = { config: null, errors: null, filePath: null };
|
|
767
822
|
}
|
|
@@ -781,6 +836,7 @@ export async function runDoctor(options = {}) {
|
|
|
781
836
|
allChecks.push(await checkSecretScanning(config));
|
|
782
837
|
allChecks.push(checkGitignoreStateFile());
|
|
783
838
|
allChecks.push(checkValidationState());
|
|
839
|
+
allChecks.push(checkCacheMigration());
|
|
784
840
|
allChecks.push(await checkHistoryHealth());
|
|
785
841
|
// Collect suggestions from failed checks
|
|
786
842
|
const suggestions = allChecks
|
|
@@ -801,6 +857,61 @@ export async function runDoctor(options = {}) {
|
|
|
801
857
|
passedChecks,
|
|
802
858
|
};
|
|
803
859
|
}
|
|
860
|
+
/**
|
|
861
|
+
* Output doctor results in YAML format
|
|
862
|
+
*/
|
|
863
|
+
async function outputDoctorYaml(result) {
|
|
864
|
+
// Small delay to ensure stderr is flushed
|
|
865
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
866
|
+
// RFC 4627 separator
|
|
867
|
+
process.stdout.write('---\n');
|
|
868
|
+
// Write pure YAML
|
|
869
|
+
process.stdout.write(stringifyYaml(result));
|
|
870
|
+
// CRITICAL: Wait for stdout to flush before exiting
|
|
871
|
+
await new Promise(resolve => {
|
|
872
|
+
if (process.stdout.write('')) {
|
|
873
|
+
resolve();
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
process.stdout.once('drain', resolve);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Display doctor results in human-friendly format
|
|
882
|
+
*/
|
|
883
|
+
function displayDoctorResults(result) {
|
|
884
|
+
console.log('🩺 vibe-validate Doctor\n');
|
|
885
|
+
const modeMessage = result.verboseMode
|
|
886
|
+
? 'Running diagnostic checks (verbose mode)...\n'
|
|
887
|
+
: 'Running diagnostic checks...\n';
|
|
888
|
+
console.log(modeMessage);
|
|
889
|
+
// Print each check
|
|
890
|
+
for (const check of result.checks) {
|
|
891
|
+
const icon = check.passed ? '✅' : '❌';
|
|
892
|
+
console.log(`${icon} ${check.name}`);
|
|
893
|
+
console.log(` ${check.message}`);
|
|
894
|
+
if (check.suggestion) {
|
|
895
|
+
console.log(` 💡 ${check.suggestion}`);
|
|
896
|
+
}
|
|
897
|
+
console.log('');
|
|
898
|
+
}
|
|
899
|
+
// Summary
|
|
900
|
+
console.log(`📊 Results: ${result.passedChecks}/${result.totalChecks} checks passed\n`);
|
|
901
|
+
if (result.allPassed) {
|
|
902
|
+
console.log('✨ All checks passed! Your vibe-validate setup looks healthy.');
|
|
903
|
+
if (!result.verboseMode) {
|
|
904
|
+
console.log(' (Use --verbose to see all checks)');
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
console.log('⚠️ Some checks failed. See suggestions above to fix.');
|
|
909
|
+
if (!result.verboseMode) {
|
|
910
|
+
console.log(' (Use --verbose to see all checks including passing ones)');
|
|
911
|
+
}
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
804
915
|
/**
|
|
805
916
|
* Main command handler for Commander.js
|
|
806
917
|
*/
|
|
@@ -815,62 +926,15 @@ export function doctorCommand(program) {
|
|
|
815
926
|
try {
|
|
816
927
|
const result = await runDoctor({ verbose });
|
|
817
928
|
if (options.yaml) {
|
|
818
|
-
|
|
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
|
-
});
|
|
929
|
+
await outputDoctorYaml(result);
|
|
834
930
|
}
|
|
835
931
|
else {
|
|
836
|
-
|
|
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
|
-
}
|
|
932
|
+
displayDoctorResults(result);
|
|
869
933
|
}
|
|
870
934
|
}
|
|
871
|
-
catch (
|
|
935
|
+
catch (error) {
|
|
872
936
|
console.error('❌ Doctor check failed:');
|
|
873
|
-
console.error(
|
|
937
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
874
938
|
process.exit(1);
|
|
875
939
|
}
|
|
876
940
|
});
|