code-quality-lib 2.0.1 → 2.5.0

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/index.js CHANGED
@@ -1,64 +1,59 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync } = require('child_process');
4
- const path = require('path');
5
- const fs = require('fs');
3
+ const { execSync } = require('child_process')
4
+ const path = require('path')
5
+ const fs = require('fs')
6
6
 
7
7
  // ─── Package Manager Detection ──────────────────────────────────────────────
8
8
 
9
9
  function detectPackageManager() {
10
- const cwd = process.cwd();
10
+ const cwd = process.cwd()
11
11
  const lockFiles = [
12
12
  { file: 'bun.lock', pm: 'bun' },
13
13
  { file: 'bun.lockb', pm: 'bun' },
14
14
  { file: 'pnpm-lock.yaml', pm: 'pnpm' },
15
15
  { file: 'yarn.lock', pm: 'yarn' },
16
16
  { file: 'package-lock.json', pm: 'npm' },
17
- ];
17
+ ]
18
18
 
19
19
  for (const { file, pm } of lockFiles) {
20
- if (fs.existsSync(path.join(cwd, file))) return pm;
20
+ if (fs.existsSync(path.join(cwd, file))) return pm
21
21
  }
22
22
 
23
- const binaries = ['bun', 'pnpm', 'yarn'];
23
+ const binaries = ['bun', 'pnpm', 'yarn']
24
24
  for (const bin of binaries) {
25
25
  try {
26
- execSync(`which ${bin}`, { stdio: 'ignore' });
27
- return bin;
26
+ execSync(`which ${bin}`, { stdio: 'ignore' })
27
+ return bin
28
28
  } catch {
29
29
  // not found, continue
30
30
  }
31
31
  }
32
32
 
33
- return 'npm';
34
- }
35
-
36
- function getExecPrefix(pm) {
37
- const map = { bun: 'bunx', pnpm: 'pnpm dlx', yarn: 'yarn dlx', npm: 'npx' };
38
- return map[pm] || 'npx';
33
+ return 'npm'
39
34
  }
40
35
 
41
36
  function getRunPrefix(pm) {
42
- const map = { bun: 'bun run', pnpm: 'pnpm run', yarn: 'yarn', npm: 'npm run' };
43
- return map[pm] || 'npm run';
37
+ const map = { bun: 'bun run', pnpm: 'pnpm run', yarn: 'yarn', npm: 'npm run' }
38
+ return map[pm] || 'npm run'
44
39
  }
45
40
 
46
41
  // ─── Environment Loading ────────────────────────────────────────────────────
47
42
 
48
43
  function loadEnvFile() {
49
44
  try {
50
- const envPath = path.join(process.cwd(), '.env');
51
- if (!fs.existsSync(envPath)) return;
45
+ const envPath = path.join(process.cwd(), '.env')
46
+ if (!fs.existsSync(envPath)) return
52
47
 
53
- const content = fs.readFileSync(envPath, 'utf8');
48
+ const content = fs.readFileSync(envPath, 'utf8')
54
49
  for (const line of content.split('\n')) {
55
- const trimmed = line.trim();
56
- if (!trimmed || trimmed.startsWith('#')) continue;
57
- const eqIndex = trimmed.indexOf('=');
58
- if (eqIndex === -1) continue;
59
- const key = trimmed.slice(0, eqIndex);
60
- const value = trimmed.slice(eqIndex + 1);
61
- if (key) process.env[key] = value;
50
+ const trimmed = line.trim()
51
+ if (!trimmed || trimmed.startsWith('#')) continue
52
+ const eqIndex = trimmed.indexOf('=')
53
+ if (eqIndex === -1) continue
54
+ const key = trimmed.slice(0, eqIndex)
55
+ const value = trimmed.slice(eqIndex + 1)
56
+ if (key) process.env[key] = value
62
57
  }
63
58
  } catch {
64
59
  // silently continue without .env
@@ -68,185 +63,767 @@ function loadEnvFile() {
68
63
  // ─── Tool Path Resolution ───────────────────────────────────────────────────
69
64
 
70
65
  function resolveToolBinDir() {
71
- try {
72
- return path.join(
73
- path.dirname(require.resolve('code-quality-lib/package.json')),
74
- 'node_modules',
75
- '.bin'
76
- );
77
- } catch {
78
- return path.join(__dirname, 'node_modules', '.bin');
66
+ // Always use project's node_modules/.bin first (where code-quality is installed)
67
+ const projectBinDir = path.join(process.cwd(), 'node_modules', '.bin')
68
+
69
+ // Fallback to library's bundled binaries if project doesn't have them
70
+ const libBinDir = path.join(__dirname, 'node_modules', '.bin')
71
+
72
+ // Check if project has node_modules/.bin directory
73
+ if (fs.existsSync(projectBinDir)) {
74
+ return projectBinDir
75
+ }
76
+
77
+ return libBinDir
78
+ }
79
+
80
+ // ─── Config File Detection ──────────────────────────────────────────────────
81
+
82
+ function detectProjectConfigs() {
83
+ const cwd = process.cwd()
84
+ const configs = {
85
+ eslint: null,
86
+ prettier: null,
87
+ typescript: null,
88
+ knip: null,
89
+ }
90
+
91
+ // ESLint config
92
+ const eslintFiles = [
93
+ '.eslintrc.js',
94
+ '.eslintrc.cjs',
95
+ '.eslintrc.json',
96
+ '.eslintrc.yml',
97
+ '.eslintrc.yaml',
98
+ 'eslint.config.js',
99
+ 'eslint.config.mjs',
100
+ ]
101
+ for (const file of eslintFiles) {
102
+ const fullPath = path.join(cwd, file)
103
+ if (fs.existsSync(fullPath)) {
104
+ configs.eslint = fullPath
105
+ break
106
+ }
107
+ }
108
+
109
+ // Prettier config
110
+ const prettierFiles = [
111
+ '.prettierrc',
112
+ '.prettierrc.json',
113
+ '.prettierrc.yml',
114
+ '.prettierrc.yaml',
115
+ '.prettierrc.js',
116
+ '.prettierrc.cjs',
117
+ 'prettier.config.js',
118
+ 'prettier.config.cjs',
119
+ ]
120
+ for (const file of prettierFiles) {
121
+ const fullPath = path.join(cwd, file)
122
+ if (fs.existsSync(fullPath)) {
123
+ configs.prettier = fullPath
124
+ break
125
+ }
79
126
  }
127
+
128
+ // TypeScript config
129
+ const tsconfigPath = path.join(cwd, 'tsconfig.json')
130
+ if (fs.existsSync(tsconfigPath)) {
131
+ configs.typescript = tsconfigPath
132
+ }
133
+
134
+ // Knip config
135
+ const knipFiles = ['knip.json', 'knip.jsonc', '.knip.json', '.knip.jsonc']
136
+ for (const file of knipFiles) {
137
+ const fullPath = path.join(cwd, file)
138
+ if (fs.existsSync(fullPath)) {
139
+ configs.knip = fullPath
140
+ break
141
+ }
142
+ }
143
+
144
+ return configs
80
145
  }
81
146
 
82
147
  // ─── Default Checks ─────────────────────────────────────────────────────────
83
148
 
84
149
  const DEFAULT_TOOLS = [
85
- { name: 'TypeScript', bin: 'tsc', args: '--noEmit', description: 'Type checking and compilation' },
86
- { name: 'ESLint', bin: 'eslint', args: '. --ext .js,.jsx,.ts,.tsx', description: 'Code linting and style checking' },
87
- { name: 'Prettier', bin: 'prettier', args: '--check .', description: 'Code formatting validation' },
150
+ {
151
+ name: 'ESLint',
152
+ bin: 'eslint',
153
+ args: '. --ext .js,.jsx,.ts,.tsx',
154
+ description: 'Code linting and style checking',
155
+ },
156
+ {
157
+ name: 'TypeScript',
158
+ bin: 'tsc',
159
+ args: '--noEmit',
160
+ description: 'Type checking and compilation',
161
+ },
162
+ {
163
+ name: 'Prettier',
164
+ bin: 'prettier',
165
+ args: '--check .',
166
+ description: 'Code formatting validation',
167
+ },
88
168
  { name: 'Knip', bin: 'knip', args: '', description: 'Dead code detection' },
89
- { name: 'Snyk', bin: 'snyk', args: 'test --severity-threshold=high', description: 'Security vulnerability scanning' },
90
- ];
169
+ {
170
+ name: 'Snyk',
171
+ bin: 'snyk',
172
+ args: 'test --severity-threshold=high',
173
+ description: 'Security vulnerability scanning',
174
+ },
175
+ ]
91
176
 
92
177
  // ─── CodeQualityChecker Class ───────────────────────────────────────────────
93
178
 
94
179
  class CodeQualityChecker {
95
180
  constructor(options = {}) {
181
+ // Detect environment
182
+ const env =
183
+ options.environment || process.env.NODE_ENV || process.env.CODE_QUALITY_ENV || 'development'
184
+
96
185
  this.options = {
97
186
  loadEnv: options.loadEnv !== false,
98
- tools: options.tools || DEFAULT_TOOLS.map((t) => t.name),
187
+ useProjectConfig: options.useProjectConfig !== false,
188
+ tools: this._resolveToolsForEnvironment(options.tools, options.environments, env),
99
189
  commands: options.commands || {},
100
190
  descriptions: options.descriptions || {},
101
191
  packageManager: options.packageManager || detectPackageManager(),
102
- };
192
+ environment: env,
193
+ environments: options.environments || {},
194
+ }
195
+
196
+ if (this.options.loadEnv) loadEnvFile()
103
197
 
104
- if (this.options.loadEnv) loadEnvFile();
198
+ // Detect project configs if useProjectConfig is enabled
199
+ this.projectConfigs = this.options.useProjectConfig ? detectProjectConfigs() : {}
200
+ }
201
+
202
+ _resolveToolsForEnvironment(tools, environments, env) {
203
+ // If environment-specific tools are defined
204
+ if (environments && environments[env] && environments[env].tools) {
205
+ return environments[env].tools
206
+ }
207
+
208
+ // If default tools are provided
209
+ if (tools) {
210
+ return tools
211
+ }
212
+
213
+ // Fallback to default tools based on environment
214
+ if (env === 'ci' || env === 'production' || env === 'test') {
215
+ // CI/CD: Run all tools for comprehensive checks
216
+ return DEFAULT_TOOLS.map((t) => t.name)
217
+ } else {
218
+ // Development: Run essential tools only
219
+ return ['ESLint', 'TypeScript', 'Prettier']
220
+ }
105
221
  }
106
222
 
107
223
  _getChecks() {
108
- const binDir = resolveToolBinDir();
109
- const checks = [];
224
+ const binDir = resolveToolBinDir()
225
+ const checks = []
110
226
 
111
227
  for (const toolName of this.options.tools) {
112
- const defaultTool = DEFAULT_TOOLS.find((t) => t.name === toolName);
113
- if (!defaultTool && !this.options.commands[toolName]) continue;
228
+ const defaultTool = DEFAULT_TOOLS.find((t) => t.name === toolName)
229
+ if (!defaultTool && !this.options.commands[toolName]) continue
114
230
 
115
- const cmd =
116
- this.options.commands[toolName] ||
117
- `${path.join(binDir, defaultTool.bin)}${defaultTool.args ? ' ' + defaultTool.args : ''}`;
231
+ let cmd = this.options.commands[toolName]
232
+
233
+ // If custom command provided, use it directly (assumes tools are in PATH or project's node_modules)
234
+ if (cmd) {
235
+ // Custom command - use as-is, tools should be available in project's node_modules/.bin or PATH
236
+ // No need to prepend full path
237
+ } else {
238
+ // No custom command - build default
239
+ const binPath = path.join(binDir, defaultTool.bin)
240
+ let args = defaultTool.args
241
+
242
+ // Add config flags if project configs exist and useProjectConfig is true
243
+ if (this.options.useProjectConfig) {
244
+ if (toolName === 'ESLint' && this.projectConfigs.eslint) {
245
+ args = `${args} --config ${this.projectConfigs.eslint}`
246
+ } else if (toolName === 'Prettier' && this.projectConfigs.prettier) {
247
+ args = `${args} --config ${this.projectConfigs.prettier}`
248
+ } else if (toolName === 'TypeScript' && this.projectConfigs.typescript) {
249
+ args = `--project ${this.projectConfigs.typescript} --noEmit`
250
+ } else if (toolName === 'Knip' && this.projectConfigs.knip) {
251
+ args = `--config ${this.projectConfigs.knip}`
252
+ }
253
+ }
254
+
255
+ cmd = `${binPath}${args ? ' ' + args : ''}`
256
+ }
118
257
 
119
258
  const description =
120
- this.options.descriptions[toolName] ||
121
- (defaultTool ? defaultTool.description : toolName);
259
+ this.options.descriptions[toolName] || (defaultTool ? defaultTool.description : toolName)
122
260
 
123
- checks.push({ name: toolName, cmd, description });
261
+ checks.push({ name: toolName, cmd, description })
124
262
  }
125
263
 
126
- return checks;
264
+ return checks
127
265
  }
128
266
 
129
- runCommand(command, description) {
267
+ runCommand(command, description, skipSnykValidation = false) {
268
+ // Check for Snyk token before running Snyk
269
+ if (description.includes('Security vulnerability scanning') && !skipSnykValidation) {
270
+ if (!process.env.SNYK_TOKEN) {
271
+ return {
272
+ success: false,
273
+ output:
274
+ '⚠️ SNYK_TOKEN not found in environment variables. Please set SNYK_TOKEN in your .env file or run:\n export SNYK_TOKEN=your_token_here\n\nYou can get a free token at: https://snyk.io/login',
275
+ }
276
+ }
277
+
278
+ // Validate Snyk token by clearing cache and testing
279
+ try {
280
+ // Clear Snyk cache to force token validation
281
+ const homeDir = require('os').homedir()
282
+ const snykConfigPath = path.join(homeDir, '.config', 'snyk')
283
+ if (fs.existsSync(snykConfigPath)) {
284
+ fs.rmSync(snykConfigPath, { recursive: true, force: true })
285
+ }
286
+
287
+ const authCheck = execSync('npx snyk test --severity-threshold=high', {
288
+ stdio: 'pipe',
289
+ encoding: 'utf8',
290
+ env: process.env,
291
+ timeout: 15000, // 15 second timeout
292
+ })
293
+
294
+ // If test succeeds, token is valid - return the result directly to avoid double run
295
+ if (authCheck.includes('✔ Tested') || authCheck.includes('No vulnerable paths found')) {
296
+ return { success: true, output: authCheck.trim() }
297
+ }
298
+ } catch (authError) {
299
+ // If auth check fails, check if it's authentication error
300
+ const authOutput = authError.stdout || authError.stderr || authError.message || ''
301
+ if (
302
+ authOutput.includes('Authentication error') ||
303
+ authOutput.includes('401') ||
304
+ authOutput.includes('SNYK-0005')
305
+ ) {
306
+ return {
307
+ success: false,
308
+ output:
309
+ '❌ Snyk token validation failed. Token may be expired or invalid.\n\n' +
310
+ 'To fix:\n' +
311
+ '1. Get a new token at: https://snyk.io/login\n' +
312
+ '2. Set SNYK_TOKEN in your .env file\n' +
313
+ '3. Or run: npx snyk auth\n\n' +
314
+ `Error: ${authOutput.trim()}`,
315
+ }
316
+ } else {
317
+ // Other errors - show warning but continue with main scan
318
+ console.log('⚠️ Snyk token validation inconclusive - proceeding with scan...')
319
+ }
320
+ }
321
+ }
322
+
130
323
  try {
131
- const output = execSync(command, { stdio: 'pipe', encoding: 'utf8' });
132
- return { success: true, output: (output || '').trim() };
324
+ // Pass all environment variables to child process
325
+ const output = execSync(command, {
326
+ stdio: 'pipe',
327
+ encoding: 'utf8',
328
+ env: process.env,
329
+ })
330
+ return { success: true, output: (output || '').trim() }
133
331
  } catch (error) {
134
- const output = error.stdout || error.stderr || error.message || 'Unknown error';
135
- return { success: false, output: output.trim() };
332
+ const output = error.stdout || error.stderr || error.message || 'Unknown error'
333
+ return { success: false, output: output.trim() }
334
+ }
335
+ }
336
+
337
+ _parseErrorCounts(toolName, output) {
338
+ if (!output) return { errors: 0, warnings: 0 }
339
+
340
+ let errors = 0
341
+ let warnings = 0
342
+
343
+ switch (toolName) {
344
+ case 'TypeScript': {
345
+ // Parse individual TypeScript errors in tsc format
346
+ const errorLines = output.split('\n').filter((line) => line.includes(' - error TS'))
347
+ errors = errorLines.length
348
+
349
+ // Also try the "Found X errors" format as fallback
350
+ if (errors === 0) {
351
+ const errorMatch = output.match(/Found (\d+) errors?/)
352
+ if (errorMatch) errors = parseInt(errorMatch[1], 10)
353
+ }
354
+ break
355
+ }
356
+ case 'ESLint': {
357
+ // ESLint format: "✖ X problems (Y errors, Z warnings)" or individual error lines
358
+ const problemMatch = output.match(/(\d+) errors?, (\d+) warnings?/)
359
+ if (problemMatch) {
360
+ errors = parseInt(problemMatch[1], 10)
361
+ warnings = parseInt(problemMatch[2], 10)
362
+ } else {
363
+ // Count individual error/warning lines as fallback
364
+ const lines = output.split('\n')
365
+ for (const line of lines) {
366
+ if (line.includes('error ')) errors++
367
+ if (line.includes('warning ')) warnings++
368
+ }
369
+ }
370
+ break
371
+ }
372
+ case 'Prettier': {
373
+ // Check if Prettier reports success
374
+ if (output.includes('All matched files use Prettier code style!')) {
375
+ errors = 0
376
+ } else {
377
+ // Count files that need formatting
378
+ const lines = output.split('\n')
379
+ const filesNeedingFormatting = lines.filter((line) => {
380
+ const trimmed = line.trim()
381
+ return (
382
+ trimmed &&
383
+ !trimmed.includes('Code style issues') &&
384
+ !trimmed.includes('formatted') &&
385
+ !trimmed.includes('issues found') &&
386
+ !trimmed.includes('files listed') &&
387
+ !trimmed.includes('Checking formatting') &&
388
+ !trimmed.startsWith('[') &&
389
+ !trimmed.startsWith('{') &&
390
+ !trimmed.startsWith('{') &&
391
+ trimmed.includes('.') // Must be a file path
392
+ )
393
+ })
394
+ errors = filesNeedingFormatting.length
395
+ }
396
+ break
397
+ }
398
+ case 'Knip': {
399
+ // Knip shows issues per category with detailed counts - treat as warnings
400
+ const issueMatches = output.match(/\d+\s+(unused|unlisted|unresolved|duplicate)/gi)
401
+ if (issueMatches) {
402
+ warnings = issueMatches.reduce((sum, match) => {
403
+ const num = parseInt(match.match(/\d+/)[0], 10)
404
+ return sum + num
405
+ }, 0)
406
+ } else {
407
+ // Fallback: count lines with issue keywords as warnings
408
+ const lines = output.split('\n')
409
+ for (const line of lines) {
410
+ if (
411
+ line.includes('unused') ||
412
+ line.includes('unlisted') ||
413
+ line.includes('unresolved') ||
414
+ line.includes('duplicate') ||
415
+ line.includes('Unused') ||
416
+ line.includes('Unlisted') ||
417
+ line.includes('Unresolved') ||
418
+ line.includes('Duplicate')
419
+ ) {
420
+ warnings++
421
+ }
422
+ }
423
+ }
424
+ break
425
+ }
426
+ default:
427
+ break
428
+ }
429
+
430
+ return { errors, warnings }
431
+ }
432
+
433
+ _parseErrorLines(toolName, output) {
434
+ if (!output) return []
435
+
436
+ const errorLines = []
437
+
438
+ switch (toolName) {
439
+ case 'TypeScript': {
440
+ // Extract individual TypeScript error lines in tsc format
441
+ const lines = output.split('\n')
442
+ for (let i = 0; i < lines.length; i++) {
443
+ const line = lines[i].trim()
444
+
445
+ // Match tsc error format: "src/file.ts:58:17 - error TS2552: Cannot find name"
446
+ if (line.includes(' - error TS') && line.includes(':')) {
447
+ errorLines.push(line)
448
+ }
449
+ }
450
+ break
451
+ }
452
+ case 'ESLint': {
453
+ // Extract ESLint error lines in standard format
454
+ const lines = output.split('\n')
455
+ for (const line of lines) {
456
+ const trimmedLine = line.trim()
457
+ // Match ESLint format: "path/to/file:line:column: error message (rule)"
458
+ if (
459
+ trimmedLine.includes(':') &&
460
+ (trimmedLine.includes('error ') || trimmedLine.includes('warning '))
461
+ ) {
462
+ // Remove any ANSI color codes and clean up
463
+ const cleanLine = trimmedLine.replace(/\u001b\[[0-9;]*m/g, '')
464
+ errorLines.push(cleanLine)
465
+ }
466
+ }
467
+ break
468
+ }
469
+ case 'Prettier': {
470
+ // Extract files that need formatting in clean format
471
+ const lines = output.split('\n')
472
+ for (const line of lines) {
473
+ const trimmedLine = line.trim()
474
+
475
+ // Skip generic messages and JSON output
476
+ if (
477
+ !trimmedLine ||
478
+ trimmedLine.includes('Code style issues') ||
479
+ trimmedLine.includes('formatted') ||
480
+ trimmedLine.includes('issues found') ||
481
+ trimmedLine.includes('files listed') ||
482
+ trimmedLine.includes('Checking formatting...') ||
483
+ trimmedLine.startsWith('{') ||
484
+ trimmedLine.match(/^\d+ files?/) || // "1 file" or "2 files"
485
+ trimmedLine.match(/^.*\d+ files? checked/) // "2 files checked"
486
+ ) {
487
+ continue
488
+ }
489
+
490
+ // Match Prettier [warn] filename format
491
+ if (trimmedLine.startsWith('[warn]')) {
492
+ const fileName = trimmedLine.replace(/^\[warn]\s*/, '').trim()
493
+ if (fileName && fileName !== 'Code style issues found in the above file') {
494
+ errorLines.push(`File needs formatting: ${fileName}`)
495
+ }
496
+ }
497
+ // Match other file path formats
498
+ else if (trimmedLine.includes('/') || trimmedLine.includes('.')) {
499
+ // Clean up the file path and add context
500
+ let filePath = trimmedLine
501
+
502
+ // Remove any leading symbols or brackets (but not [warn] which we handled above)
503
+ filePath = filePath.replace(/^[\[\]\s]+/, '').replace(/[\[\]\s]+$/, '')
504
+
505
+ // Skip if it looks like a directory or non-file
506
+ if (filePath.endsWith('/') || !filePath.includes('.')) {
507
+ continue
508
+ }
509
+
510
+ errorLines.push(`File needs formatting: ${filePath}`)
511
+ }
512
+ }
513
+
514
+ // If no specific files were found but Prettier failed, add a generic message
515
+ if (errorLines.length === 0 && output.includes('issues')) {
516
+ errorLines.push('Code formatting issues detected - run prettier --write . to fix')
517
+ }
518
+
519
+ break
520
+ }
521
+ case 'Knip': {
522
+ // Extract Knip issues in structured format
523
+ const lines = output.split('\n')
524
+ for (const line of lines) {
525
+ const trimmedLine = line.trim()
526
+ // Match Knip issue patterns
527
+ if (
528
+ trimmedLine.includes('unused') ||
529
+ trimmedLine.includes('unlisted') ||
530
+ trimmedLine.includes('unresolved') ||
531
+ trimmedLine.includes('duplicate') ||
532
+ trimmedLine.includes('Unused') ||
533
+ trimmedLine.includes('Unlisted') ||
534
+ trimmedLine.includes('Unresolved') ||
535
+ trimmedLine.includes('Duplicate')
536
+ ) {
537
+ // Clean up and format for AI readability
538
+ const cleanLine = trimmedLine.replace(/\u001b\[[0-9;]*m/g, '') // Remove ANSI codes
539
+ errorLines.push(cleanLine)
540
+ }
541
+ }
542
+ break
543
+ }
544
+ default:
545
+ break
136
546
  }
547
+
548
+ return errorLines
137
549
  }
138
550
 
139
551
  async run(options = {}) {
140
- const showLogs = options.showLogs || false;
141
- const checks = this._getChecks();
142
- const pm = this.options.packageManager;
143
- const runCmd = getRunPrefix(pm);
144
- const results = [];
145
- let allPassed = true;
552
+ const showLogs = options.showLogs || false
553
+ const useFix = options.useFix || false
146
554
 
147
- console.log('\n� Professional Code Quality Check\n');
148
- console.log('─'.repeat(50));
149
- console.log(`📦 Using ${pm} package manager\n`);
555
+ // Load .env file if enabled
556
+ if (this.options.loadEnv) {
557
+ try {
558
+ const dotenvPath = path.join(process.cwd(), '.env')
559
+ if (fs.existsSync(dotenvPath)) {
560
+ // Try to load dotenv as optional dependency
561
+ let dotenv
562
+ try {
563
+ dotenv = require('dotenv')
564
+ } catch (_dotenvError) {
565
+ // dotenv not installed, skip .env loading
566
+ console.log('⚠️ dotenv not installed. Install with: npm install dotenv')
567
+ }
568
+
569
+ if (dotenv) {
570
+ dotenv.config({ path: dotenvPath })
571
+ }
572
+ }
573
+ } catch (_error) {
574
+ // .env file missing or other error, continue without it
575
+ }
576
+ }
577
+
578
+ const checks = this._getChecks()
579
+ const pm = this.options.packageManager
580
+ const runCmd = getRunPrefix(pm)
581
+ const results = []
582
+ let allPassed = true
583
+ let step = 1
584
+
585
+ // Header
586
+ console.log('\n🔧 Code Quality Setup')
587
+ console.log('─'.repeat(50))
588
+ console.log(`📦 Package Manager: ${pm}`)
589
+ console.log(`🌍 Environment: ${this.options.environment}`)
590
+ console.log(
591
+ `⚙️ Config: ${this.options.useProjectConfig ? 'Project configs' : 'Bundled configs'}`
592
+ )
593
+ console.log(`🔧 Tools: ${checks.length} quality checks\n`)
150
594
 
151
595
  if (showLogs) {
152
- console.log('📋 Detailed error logging enabled (--logs flag)\n');
596
+ console.log('📋 Verbose logging enabled\n')
153
597
  }
154
598
 
599
+ // Run each check with step-by-step output
155
600
  for (const { name, cmd, description } of checks) {
156
- console.log(`Running ${name}...`);
157
- const result = this.runCommand(cmd, description);
601
+ const stepNum = String(step).padStart(2, ' ')
602
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
603
+ let spinIndex = 0
604
+
605
+ // Show starting message
606
+ process.stdout.write(`${stepNum}. ${name}... `)
607
+
608
+ // Simple spinner (simulate work)
609
+ const spinInterval = setInterval(() => {
610
+ process.stdout.write(`\r${stepNum}. ${name}... ${spinner[spinIndex]}`)
611
+ spinIndex = (spinIndex + 1) % spinner.length
612
+ }, 100)
613
+
614
+ // Run the actual check
615
+ const result = this.runCommand(cmd, description)
616
+
617
+ // Stop spinner
618
+ clearInterval(spinInterval)
619
+
620
+ const counts = this._parseErrorCounts(name, result.output)
621
+ const errorLines = this._parseErrorLines(name, result.output)
158
622
 
159
- if (result.success) {
160
- console.log(`✅ ${name}: Passed`);
623
+ // Special handling for Knip - allow passing with warnings only
624
+ const finalResult = { name, description, ...result, ...counts, errorLines }
625
+ if (name === 'Knip' && !result.success && counts.warnings > 0 && counts.errors === 0) {
626
+ finalResult.success = true // Override success for Knip warnings only
627
+ }
628
+
629
+ // Show result - show warning icon for any tool with warnings
630
+ const actualSuccess = finalResult.success
631
+ let displayIcon = finalResult.success ? '✅ Done' : '❌ Failed'
632
+
633
+ if (finalResult.success && counts.warnings > 0) {
634
+ displayIcon = '⚠️ Done' // Show warning icon for any tool with warnings
635
+ }
636
+
637
+ if (actualSuccess) {
638
+ process.stdout.write(`\r${stepNum}. ${name}... ${displayIcon}\n`)
161
639
  } else {
162
- allPassed = false;
163
- console.log(`❌ ${name}: Failed`);
640
+ // Auto-fix retry for ESLint and Prettier when --fix is used
641
+ if (useFix && (name === 'ESLint' || name === 'Prettier')) {
642
+ process.stdout.write(`\r${stepNum}. ${name}... ${displayIcon}\n`)
643
+
644
+ // Build fix command
645
+ let fixCommand = cmd
646
+ if (name === 'ESLint') {
647
+ fixCommand = cmd.replace('. --ext', ' --fix . --ext')
648
+ } else if (name === 'Prettier') {
649
+ fixCommand = cmd.replace('--check', '--write')
650
+ }
651
+
652
+ console.log(`\n🔧 Auto-fixing ${name}...`)
653
+ const fixResult = this.runCommand(fixCommand, description)
654
+
655
+ if (fixResult.success) {
656
+ console.log(`✅ ${name} fixed successfully!`)
657
+ // Re-run check to verify fix
658
+ const verifyResult = this.runCommand(cmd, description)
659
+ const verifyCounts = this._parseErrorCounts(name, verifyResult.output)
660
+
661
+ if (verifyResult.success || (name === 'Knip' && verifyCounts.errors === 0)) {
662
+ finalResult.success = true
663
+ finalResult.output = verifyResult.output
664
+ Object.assign(finalResult, verifyCounts)
665
+ } else {
666
+ console.log(`⚠️ ${name} fix applied but issues remain`)
667
+ }
668
+ } else {
669
+ console.log(`❌ Failed to auto-fix ${name}`)
670
+ }
671
+ } else {
672
+ allPassed = false
673
+ process.stdout.write(`\r${stepNum}. ${name}... ${displayIcon}\n`)
674
+ }
164
675
  }
165
676
 
677
+ // Show details if logs enabled
166
678
  if (showLogs && result.output) {
167
- const icon = result.success ? '📄' : '❌';
168
- console.log(`\n${icon} ${name} ${result.success ? 'Output' : 'Error Details'}:`);
169
- console.log(''.repeat(50));
170
- console.log(result.output);
171
- console.log('─'.repeat(50));
172
- console.log('');
679
+ console.log(` ${result.success ? '📄 Output:' : '❌ Error:'}`)
680
+ console.log(' ' + ''.repeat(48))
681
+ const lines = result.output.split('\n')
682
+ for (const line of lines.slice(0, 10)) {
683
+ // Limit output
684
+ console.log(` ${line}`)
685
+ }
686
+ if (lines.length > 10) {
687
+ console.log(` ... (${lines.length - 10} more lines)`)
688
+ }
689
+ console.log(' ' + '─'.repeat(48))
173
690
  }
174
691
 
175
- results.push({ name, description, ...result });
692
+ results.push(finalResult)
693
+ step++
176
694
  }
177
695
 
178
696
  // Generate report
179
- this._writeReport(results, allPassed, pm, runCmd);
697
+ this._writeReport(results, allPassed, pm, runCmd)
698
+
699
+ // Summary
700
+ console.log('\n' + '─'.repeat(50))
701
+ console.log('📊 Quality Check Summary\n')
702
+
703
+ const passed = results.filter((r) => r.success)
704
+ const failed = results.filter((r) => !r.success)
705
+
706
+ // Show results with checkmarks/strikes and error counts
707
+ for (const result of results) {
708
+ let icon = result.success ? '✅' : '❌'
709
+ const name = result.name.padEnd(12, ' ')
710
+ let status = result.success ? 'Passed' : 'Failed'
711
+
712
+ // Show warning icon for any tool that passed but has warnings
713
+ if (result.success && result.warnings > 0) {
714
+ icon = '⚠️'
715
+ }
716
+
717
+ // Build status message with counts
718
+ if (result.errors > 0 || result.warnings > 0) {
719
+ const parts = []
720
+ if (result.errors > 0) parts.push(`${result.errors} error${result.errors !== 1 ? 's' : ''}`)
721
+ if (result.warnings > 0) {
722
+ // Use "detected" for Knip warnings, "warnings" for others
723
+ const warningWord = result.name === 'Knip' ? 'detected' : 'warning'
724
+ parts.push(`${result.warnings} ${warningWord}${result.warnings !== 1 ? 's' : ''}`)
725
+ }
726
+ status = `${status} (${parts.join(', ')})`
727
+ }
728
+
729
+ console.log(`${icon} ${name}${status}`)
180
730
 
181
- console.log('\n' + '─'.repeat(50));
731
+ // Show individual error lines for failed tools only when --logs is used
732
+ if (!result.success && result.errorLines && result.errorLines.length > 0 && showLogs) {
733
+ console.log(` 📝 Error details:`)
734
+ for (const errorLine of result.errorLines) {
735
+ console.log(` • ${errorLine}`)
736
+ }
737
+ }
738
+ }
739
+
740
+ console.log('\n' + '─'.repeat(50))
182
741
 
183
742
  if (allPassed) {
184
- console.log('\n🎉 All quality checks passed! Code is ready for production.\n');
743
+ console.log('🎉 Success! All quality checks passed.\n')
744
+ console.log('✅ Your code is ready for production!\n')
185
745
  } else {
186
- console.log('\n❌ Some quality checks failed. Please fix the issues above.\n');
746
+ console.log('❌ Some quality checks failed.\n')
747
+ console.log(`📊 Results: ${passed.length} passed, ${failed.length} failed\n`)
748
+
749
+ // Show summary of all errors for AI analysis
750
+ console.log('🤖 For AI Analysis - Summary of All Errors:\n')
751
+ for (const result of failed) {
752
+ if (result.errorLines && result.errorLines.length > 0) {
753
+ console.log(`## ${result.name} Errors:`)
754
+ for (const errorLine of result.errorLines) {
755
+ console.log(errorLine)
756
+ }
757
+ console.log('') // Empty line between tools
758
+ }
759
+ }
760
+
187
761
  if (!showLogs) {
188
- console.log('💡 Run with --logs flag to see detailed errors in terminal');
762
+ console.log('💡 Run with --logs flag to see full tool output')
189
763
  }
190
- console.log('📄 See .quality-report.md for detailed error information\n');
764
+ console.log('📄 See .quality-report.md for complete details\n')
191
765
  }
192
766
 
193
767
  return {
194
768
  success: allPassed,
195
769
  message: allPassed ? 'All quality checks passed' : 'Some quality checks failed',
196
770
  results,
197
- };
771
+ }
198
772
  }
199
773
 
200
774
  _writeReport(results, allPassed, pm, runCmd) {
201
- const reportPath = path.join(process.cwd(), '.quality-report.md');
202
- const timestamp = new Date().toISOString();
203
- const passed = results.filter((r) => r.success);
204
- const failed = results.filter((r) => !r.success);
775
+ const reportPath = path.join(process.cwd(), '.quality-report.md')
776
+ const timestamp = new Date().toISOString()
777
+ const passed = results.filter((r) => r.success)
778
+ const failed = results.filter((r) => !r.success)
205
779
 
206
- let report = `# Code Quality Report\n\n`;
207
- report += `**Generated**: ${timestamp}\n`;
208
- report += `**Package Manager**: ${pm}\n\n`;
209
- report += `---\n\n`;
780
+ let report = `# Code Quality Report\n\n`
781
+ report += `**Generated**: ${timestamp}\n`
782
+ report += `**Package Manager**: ${pm}\n\n`
783
+ report += `---\n\n`
210
784
 
211
785
  for (const r of results) {
212
- report += `## ${r.name}\n\n`;
213
- report += `**Description**: ${r.description}\n\n`;
214
- report += `**Status**: ${r.success ? '✅ **PASSED**' : '❌ **FAILED**'}\n\n`;
786
+ const toolName = r.name || 'Unknown Tool'
787
+ const toolDesc = r.description || 'No description available'
788
+
789
+ report += `## ${toolName}\n\n`
790
+ report += `**Description**: ${toolDesc}\n\n`
791
+ report += `**Status**: ${r.success ? '✅ **PASSED**' : '❌ **FAILED**'}\n\n`
215
792
  if (r.output) {
216
- report += `**Output**:\n\`\`\`\n${r.output}\n\`\`\`\n\n`;
793
+ report += `**Output**:\n\`\`\`\n${r.output}\n\`\`\`\n\n`
217
794
  }
218
- report += `---\n\n`;
795
+ report += `---\n\n`
219
796
  }
220
797
 
221
- report += `## Summary\n\n`;
222
- report += `**Total Checks**: ${results.length}\n`;
223
- report += `**Passed**: ${passed.length}\n`;
224
- report += `**Failed**: ${failed.length}\n\n`;
798
+ report += `## Summary\n\n`
799
+ report += `**Total Checks**: ${results.length}\n`
800
+ report += `**Passed**: ${passed.length}\n`
801
+ report += `**Failed**: ${failed.length}\n\n`
225
802
 
226
803
  if (allPassed) {
227
- report += `### ✅ All quality checks passed!\n\nYour code is ready for production.\n\n`;
804
+ report += `### ✅ All quality checks passed!\n\nYour code is ready for production.\n\n`
228
805
  } else {
229
- report += `### ❌ Some quality checks failed\n\n`;
230
- report += `**Quick Fix Commands**:\n`;
231
- report += `- \`${runCmd} lint:fix\` — Auto-fix linting issues\n`;
232
- report += `- \`${runCmd} format:fix\` — Auto-format code\n\n`;
806
+ report += `### ❌ Some quality checks failed\n\n`
807
+ report += `**Quick Fix Commands**:\n`
808
+ report += `- \`${runCmd} lint:fix\` — Auto-fix linting issues\n`
809
+ report += `- \`${runCmd} format:fix\` — Auto-format code\n\n`
233
810
  }
234
811
 
235
- report += `---\n\n## For AI Agents\n\n`;
236
- report += `**Failed Checks**: ${failed.map((r) => r.name).join(', ') || 'None'}\n\n`;
812
+ report += `---\n\n## For AI Agents\n\n`
813
+ report += `**Failed Checks**: ${failed.map((r) => r.name).join(', ') || 'None'}\n\n`
237
814
  if (!allPassed) {
238
- report += `**Action Required**:\n`;
815
+ report += `**Action Required**:\n`
239
816
  for (const r of failed) {
240
- report += `- Fix ${r.name} errors\n`;
817
+ report += `- Fix ${r.name} errors\n`
241
818
  }
242
- report += `\n`;
819
+ report += `\n`
243
820
  }
244
821
 
245
822
  try {
246
- fs.writeFileSync(reportPath, report, 'utf8');
247
- console.log(`\n📄 Quality report saved to: .quality-report.md`);
823
+ fs.writeFileSync(reportPath, report, 'utf8')
824
+ console.log(`\n📄 Quality report saved to: .quality-report.md`)
248
825
  } catch (err) {
249
- console.error(`\n⚠️ Failed to write report: ${err.message}`);
826
+ console.error(`\n⚠️ Failed to write report: ${err.message}`)
250
827
  }
251
828
  }
252
829
  }
@@ -254,39 +831,690 @@ class CodeQualityChecker {
254
831
  // ─── Convenience Function ───────────────────────────────────────────────────
255
832
 
256
833
  async function runQualityCheck(options = {}) {
257
- const checker = new CodeQualityChecker(options);
258
- return checker.run({ showLogs: options.showLogs || false });
834
+ const checker = new CodeQualityChecker(options)
835
+ return checker.run({ showLogs: options.showLogs || false })
836
+ }
837
+
838
+ // ─── Config Generation ────────────────────────────────────────────────────
839
+
840
+ function initConfigFiles() {
841
+ const rootDir = process.cwd()
842
+ const libConfigDir = path.join(__dirname, 'config')
843
+
844
+ console.log('\n🚀 Initializing config files in root directory...\n')
845
+
846
+ const configFiles = [
847
+ { src: 'eslint.config.mjs', dest: 'eslint.config.mjs', desc: 'ESLint configuration' },
848
+ { src: 'tsconfig.json', dest: 'tsconfig.json', desc: 'TypeScript configuration' },
849
+ { src: '.prettierrc', dest: '.prettierrc', desc: 'Prettier configuration' },
850
+ { src: '.prettierignore', dest: '.prettierignore', desc: 'Prettier ignore patterns' },
851
+ ]
852
+
853
+ let copied = 0
854
+ let skipped = 0
855
+
856
+ for (const file of configFiles) {
857
+ const srcPath = path.join(libConfigDir, file.src)
858
+ const destPath = path.join(rootDir, file.dest)
859
+
860
+ if (fs.existsSync(destPath)) {
861
+ console.log(`⏭️ ${file.dest} already exists - skipped`)
862
+ skipped++
863
+ } else if (fs.existsSync(srcPath)) {
864
+ fs.copyFileSync(srcPath, destPath)
865
+ console.log(`✅ Created ${file.dest} - ${file.desc}`)
866
+ copied++
867
+ } else {
868
+ console.log(`⚠️ ${file.src} not found in library`)
869
+ }
870
+ }
871
+
872
+ console.log('\n' + '─'.repeat(50))
873
+ console.log(`📊 Summary: ${copied} created, ${skipped} skipped`)
874
+ console.log('─'.repeat(50))
875
+
876
+ if (copied > 0) {
877
+ console.log('\n✨ Config files initialized successfully!')
878
+ console.log('\n💡 Next steps:')
879
+ console.log(' 1. Review and customize the config files')
880
+ console.log(' 2. Run: code-quality')
881
+ console.log(' 3. Fix issues: code-quality --fix\n')
882
+ } else {
883
+ console.log('\n✅ All config files already exist!\n')
884
+ }
885
+ }
886
+
887
+ function generateConfigFile() {
888
+ const configDir = path.join(process.cwd(), '.code-quality')
889
+ const configPath = path.join(configDir, 'config.json')
890
+
891
+ // Create .code-quality directory if it doesn't exist
892
+ if (!fs.existsSync(configDir)) {
893
+ fs.mkdirSync(configDir, { recursive: true })
894
+ }
895
+
896
+ const config = {
897
+ version: '1.0.0',
898
+ tools: undefined, // Use environment-based tools
899
+ environments: {
900
+ development: {
901
+ tools: ['ESLint', 'TypeScript', 'Prettier'],
902
+ },
903
+ ci: {
904
+ tools: ['ESLint', 'TypeScript', 'Prettier', 'Knip', 'Snyk'],
905
+ },
906
+ production: {
907
+ tools: ['ESLint', 'TypeScript', 'Prettier', 'Knip', 'Snyk'],
908
+ },
909
+ },
910
+ packageManager: detectPackageManager(),
911
+ useProjectConfig: true,
912
+ loadEnv: true,
913
+ commands: {
914
+ ESLint: '. --ext .js,.jsx,.ts,.tsx',
915
+ TypeScript: 'tsc --noEmit',
916
+ Prettier: '--check .',
917
+ Knip: '',
918
+ Snyk: 'test --severity-threshold=high',
919
+ },
920
+ descriptions: {
921
+ ESLint: 'Code linting and style checking',
922
+ TypeScript: 'Type checking and compilation',
923
+ Prettier: 'Code formatting validation',
924
+ Knip: 'Dead code detection',
925
+ Snyk: 'Security vulnerability scanning',
926
+ },
927
+ generated: new Date().toISOString(),
928
+ }
929
+
930
+ try {
931
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8')
932
+
933
+ // Copy reference configs from library to .code-quality/
934
+ const libConfigDir = path.join(__dirname, 'config')
935
+ const referenceConfigs = [
936
+ 'tsconfig.json',
937
+ 'eslint.config.mjs',
938
+ '.prettierrc',
939
+ '.prettierignore',
940
+ 'knip.json',
941
+ 'README.md',
942
+ ]
943
+
944
+ for (const configFile of referenceConfigs) {
945
+ const srcPath = path.join(libConfigDir, configFile)
946
+ const destPath = path.join(configDir, configFile)
947
+
948
+ if (fs.existsSync(srcPath) && !fs.existsSync(destPath)) {
949
+ fs.copyFileSync(srcPath, destPath)
950
+ }
951
+ }
952
+
953
+ console.log(`✅ Configuration directory created: ${configDir}`)
954
+ console.log('📝 Reference config files copied to .code-quality/')
955
+ console.log('📖 See .code-quality/README.md for usage guidance')
956
+ } catch (error) {
957
+ console.error(`❌ Failed to create config: ${error.message}`)
958
+ process.exit(1)
959
+ }
960
+ }
961
+
962
+ function loadConfigFile() {
963
+ // Try new location first: .code-quality/config.json
964
+ const newConfigPath = path.join(process.cwd(), '.code-quality', 'config.json')
965
+
966
+ if (fs.existsSync(newConfigPath)) {
967
+ try {
968
+ const content = fs.readFileSync(newConfigPath, 'utf8')
969
+ const config = JSON.parse(content)
970
+
971
+ // Migrate old format to new environment-based structure
972
+ if (
973
+ config.tools &&
974
+ Array.isArray(config.tools) &&
975
+ (!config.environments || Object.keys(config.environments).length === 0)
976
+ ) {
977
+ console.log('🔄 Migrating configuration to environment-based format...')
978
+ console.log(`📝 Migrating tools: ${config.tools.join(', ')}`)
979
+
980
+ config.environments = {
981
+ development: {
982
+ tools: [...config.tools], // Copy the array
983
+ },
984
+ ci: {
985
+ tools: ['ESLint', 'TypeScript', 'Prettier', 'Knip', 'Snyk'],
986
+ },
987
+ production: {
988
+ tools: ['ESLint', 'TypeScript', 'Prettier', 'Knip', 'Snyk'],
989
+ },
990
+ }
991
+ config.tools = undefined // Remove old tools array
992
+
993
+ // Save migrated config
994
+ fs.writeFileSync(newConfigPath, JSON.stringify(config, null, 2), 'utf8')
995
+ console.log('✅ Configuration migrated successfully')
996
+ console.log(`📁 Updated config saved to: ${newConfigPath}`)
997
+ } else if (config.tools && Array.isArray(config.tools)) {
998
+ console.log('⚠️ Migration condition not met. Existing environments:', config.environments)
999
+ }
1000
+
1001
+ return config
1002
+ } catch (error) {
1003
+ console.warn(`⚠️ Failed to load .code-quality/config.json: ${error.message}`)
1004
+ }
1005
+ }
1006
+
1007
+ // Fallback to old location: .code-quality.json (for backward compatibility)
1008
+ const oldConfigPath = path.join(process.cwd(), '.code-quality.json')
1009
+
1010
+ if (fs.existsSync(oldConfigPath)) {
1011
+ try {
1012
+ const content = fs.readFileSync(oldConfigPath, 'utf8')
1013
+ const config = JSON.parse(content)
1014
+
1015
+ // Migrate old format to new environment-based structure
1016
+ if (config.tools && !config.environments) {
1017
+ console.log('🔄 Migrating configuration to environment-based format...')
1018
+ config.environments = {
1019
+ development: {
1020
+ tools: config.tools,
1021
+ },
1022
+ ci: {
1023
+ tools: ['ESLint', 'TypeScript', 'Prettier', 'Knip', 'Snyk'],
1024
+ },
1025
+ production: {
1026
+ tools: ['ESLint', 'TypeScript', 'Prettier', 'Knip', 'Snyk'],
1027
+ },
1028
+ }
1029
+ config.tools = undefined // Remove old tools array
1030
+
1031
+ // Save to new location and remove old file
1032
+ const configDir = path.join(process.cwd(), '.code-quality')
1033
+ if (!fs.existsSync(configDir)) {
1034
+ fs.mkdirSync(configDir, { recursive: true })
1035
+ }
1036
+ fs.writeFileSync(newConfigPath, JSON.stringify(config, null, 2), 'utf8')
1037
+ fs.unlinkSync(oldConfigPath)
1038
+ console.log('✅ Configuration migrated to .code-quality/config.json')
1039
+ }
1040
+
1041
+ console.log(
1042
+ 'ℹ️ Using legacy .code-quality.json (consider migrating to .code-quality/config.json)'
1043
+ )
1044
+ return config
1045
+ } catch (error) {
1046
+ console.warn(`⚠️ Failed to load .code-quality.json: ${error.message}`)
1047
+ }
1048
+ }
1049
+
1050
+ return null
1051
+ }
1052
+
1053
+ // ─── Interactive Wizard ───────────────────────────────────────────────────
1054
+
1055
+ async function runWizard() {
1056
+ const readline = require('readline')
1057
+ const rl = readline.createInterface({
1058
+ input: process.stdin,
1059
+ output: process.stdout,
1060
+ })
1061
+
1062
+ // Check if config already exists
1063
+ const existingConfig = loadConfigFile()
1064
+ if (existingConfig) {
1065
+ console.log('\n📋 Found existing configuration:')
1066
+ console.log(`📦 Package Manager: ${existingConfig.packageManager || 'auto-detected'}`)
1067
+ console.log(
1068
+ `⚙️ Config: ${existingConfig.useProjectConfig !== false ? 'Project configs' : 'Bundled configs'}`
1069
+ )
1070
+ console.log(
1071
+ `🔧 Tools: ${(existingConfig.tools || ['ESLint', 'TypeScript', 'Prettier', 'Knip', 'Snyk']).join(', ')}`
1072
+ )
1073
+ console.log(`🌍 Load .env: ${existingConfig.loadEnv !== false ? 'Yes' : 'No'}`)
1074
+
1075
+ const rerun = await askQuestion(rl, '\nReconfigure? (y/N): ')
1076
+ if (!rerun.toLowerCase().startsWith('y')) {
1077
+ rl.close()
1078
+ // Run with existing config
1079
+ const checker = new CodeQualityChecker(existingConfig)
1080
+ const result = await checker.run({ showLogs: false })
1081
+ process.exit(result.success ? 0 : 1)
1082
+ }
1083
+ }
1084
+
1085
+ console.log('\n🧙‍♂️ Code Quality Setup Wizard')
1086
+ console.log('─'.repeat(50))
1087
+ console.log("Let's configure your quality checks!\n")
1088
+
1089
+ // Step 1: Package Manager
1090
+ const pm = detectPackageManager()
1091
+ console.log(`📦 Detected package manager: ${pm}`)
1092
+ const pmAnswer = await askQuestion(rl, `Use ${pm}? (Y/n): `)
1093
+ const selectedPm = pmAnswer.toLowerCase().startsWith('n')
1094
+ ? await askQuestion(rl, 'Enter package manager (npm/bun/pnpm/yarn): ')
1095
+ : pm
1096
+
1097
+ // Step 2: Config Type
1098
+ console.log('\n⚙️ Use project config files (.eslintrc, .prettierrc, etc.)?')
1099
+ console.log('Answer "No" to use bundled configs from code-quality-lib')
1100
+ const configAnswer = await askQuestion(rl, 'Use project configs? (y/N): ')
1101
+ const useProjectConfig = configAnswer.toLowerCase().startsWith('y')
1102
+
1103
+ // Step 3: Tools Selection (Checkbox style)
1104
+ console.log('\n🔧 Select tools to run (default = all checked):')
1105
+ const allTools = ['ESLint', 'TypeScript', 'Prettier', 'Knip', 'Snyk']
1106
+ const selectedTools = []
1107
+
1108
+ for (const tool of allTools) {
1109
+ // Default to Yes for ESLint, TypeScript, Prettier
1110
+ const isDefaultYes = ['ESLint', 'TypeScript', 'Prettier'].includes(tool)
1111
+ const prompt = isDefaultYes ? `[✓] ${tool}? (Y/n): ` : `[ ] ${tool}? (y/N): `
1112
+ const answer = await askQuestion(rl, prompt)
1113
+
1114
+ // For default Yes tools, include unless user explicitly says 'n'
1115
+ // For others, include only if user explicitly says 'y'
1116
+ if (isDefaultYes) {
1117
+ if (!answer.toLowerCase().startsWith('n')) {
1118
+ selectedTools.push(tool)
1119
+ }
1120
+ } else {
1121
+ if (answer.toLowerCase().startsWith('y')) {
1122
+ selectedTools.push(tool)
1123
+ }
1124
+ }
1125
+ }
1126
+
1127
+ // Step 4: Environment Configuration
1128
+ console.log('\n🌍 Set up environment-specific tool sets?')
1129
+ console.log('This allows different tools for development vs CI/CD')
1130
+ const envConfigAnswer = await askQuestion(rl, 'Configure environments? (y/N): ')
1131
+ const configureEnvironments = envConfigAnswer.toLowerCase().startsWith('y')
1132
+
1133
+ let environments = {}
1134
+ if (configureEnvironments) {
1135
+ console.log('\n🔧 Configure development tools (default: ESLint, TypeScript, Prettier):')
1136
+ const devTools = []
1137
+ for (const tool of allTools) {
1138
+ const isDefaultYes = ['ESLint', 'TypeScript', 'Prettier'].includes(tool)
1139
+ const prompt = isDefaultYes ? `[✓] ${tool}? (Y/n): ` : `[ ] ${tool}? (y/N): `
1140
+ const answer = await askQuestion(rl, prompt)
1141
+
1142
+ if (isDefaultYes) {
1143
+ if (!answer.toLowerCase().startsWith('n')) {
1144
+ devTools.push(tool)
1145
+ }
1146
+ } else {
1147
+ if (answer.toLowerCase().startsWith('y')) {
1148
+ devTools.push(tool)
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ console.log('\n🚀 Configure CI/CD tools (default: all tools):')
1154
+ const ciTools = []
1155
+ for (const tool of allTools) {
1156
+ const answer = await askQuestion(rl, `[✓] ${tool}? (Y/n): `)
1157
+ if (!answer.toLowerCase().startsWith('n')) {
1158
+ ciTools.push(tool)
1159
+ }
1160
+ }
1161
+
1162
+ environments = {
1163
+ development: { tools: devTools },
1164
+ ci: { tools: ciTools },
1165
+ production: { tools: ciTools },
1166
+ }
1167
+ }
1168
+
1169
+ // Step 5: Load .env
1170
+ console.log('\n🌍 Load .env file before running checks?')
1171
+ const envAnswer = await askQuestion(rl, 'Load .env? (Y/n): ')
1172
+ const loadEnv = !envAnswer.toLowerCase().startsWith('n')
1173
+
1174
+ // Step 6: Show summary and confirm
1175
+ console.log('\n📋 Configuration Summary:')
1176
+ console.log('─'.repeat(50))
1177
+ console.log(`📦 Package Manager: ${selectedPm}`)
1178
+ console.log(`⚙️ Config: ${useProjectConfig ? 'Project configs' : 'Bundled configs'}`)
1179
+
1180
+ if (configureEnvironments) {
1181
+ console.log(`🌍 Environment Config: Enabled`)
1182
+ console.log(`🔧 Development: ${environments.development.tools.join(', ')}`)
1183
+ console.log(`🚀 CI/CD: ${environments.ci.tools.join(', ')}`)
1184
+ } else {
1185
+ console.log(`🔧 Tools: ${selectedTools.join(', ')}`)
1186
+ }
1187
+
1188
+ console.log(`🌍 Load .env: ${loadEnv ? 'Yes' : 'No'}`)
1189
+ console.log('─'.repeat(50))
1190
+
1191
+ const confirm = await askQuestion(rl, 'Run checks with these settings? (Y/n): ')
1192
+
1193
+ rl.close()
1194
+
1195
+ if (confirm.toLowerCase().startsWith('n')) {
1196
+ console.log('\n❌ Wizard cancelled. Use --config flag to generate config file manually.')
1197
+ process.exit(0)
1198
+ }
1199
+
1200
+ // Save config for next time in .code-quality/ directory
1201
+ const config = {
1202
+ version: '1.0.0',
1203
+ packageManager: selectedPm,
1204
+ useProjectConfig,
1205
+ tools: configureEnvironments ? undefined : selectedTools,
1206
+ environments: configureEnvironments ? environments : undefined,
1207
+ loadEnv,
1208
+ generated: new Date().toISOString(),
1209
+ }
1210
+
1211
+ try {
1212
+ const configDir = path.join(process.cwd(), '.code-quality')
1213
+ const configPath = path.join(configDir, 'config.json')
1214
+
1215
+ // Create .code-quality directory if it doesn't exist
1216
+ if (!fs.existsSync(configDir)) {
1217
+ fs.mkdirSync(configDir, { recursive: true })
1218
+ }
1219
+
1220
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8')
1221
+
1222
+ // Copy reference configs from library to .code-quality/
1223
+ const libConfigDir = path.join(__dirname, 'config')
1224
+ const referenceConfigs = [
1225
+ 'tsconfig.json',
1226
+ 'eslint.config.mjs',
1227
+ '.prettierrc',
1228
+ '.prettierignore',
1229
+ 'knip.json',
1230
+ 'README.md',
1231
+ ]
1232
+
1233
+ for (const configFile of referenceConfigs) {
1234
+ const srcPath = path.join(libConfigDir, configFile)
1235
+ const destPath = path.join(configDir, configFile)
1236
+
1237
+ if (fs.existsSync(srcPath) && !fs.existsSync(destPath)) {
1238
+ fs.copyFileSync(srcPath, destPath)
1239
+ }
1240
+ }
1241
+
1242
+ console.log(`\n💾 Configuration saved to: ${configPath}`)
1243
+ console.log('📝 Reference config files copied to .code-quality/')
1244
+ } catch (_error) {
1245
+ console.warn('\n⚠️ Could not save configuration file')
1246
+ }
1247
+
1248
+ // Run with wizard settings
1249
+ const checker = new CodeQualityChecker(config)
1250
+ const result = await checker.run({ showLogs: false })
1251
+
1252
+ process.exit(result.success ? 0 : 1)
1253
+ }
1254
+
1255
+ function askQuestion(rl, question) {
1256
+ return new Promise((resolve) => {
1257
+ rl.question(question, (answer) => {
1258
+ resolve(answer.trim())
1259
+ })
1260
+ })
259
1261
  }
260
1262
 
261
1263
  // ─── CLI Entry Point ────────────────────────────────────────────────────────
262
1264
 
263
1265
  if (require.main === module) {
264
- const args = process.argv.slice(2);
1266
+ const args = process.argv.slice(2)
265
1267
 
266
1268
  if (args.includes('--help') || args.includes('-h')) {
267
- console.log('Usage: code-quality [options]');
268
- console.log('');
269
- console.log('Options:');
270
- console.log(' --help, -h Show this help message');
271
- console.log(' --version, -v Show version number');
272
- console.log(' --logs Show detailed error logs');
273
- console.log('');
274
- console.log('Runs TypeScript, ESLint, Prettier, Knip, and Snyk checks.');
275
- process.exit(0);
1269
+ console.log('Usage: code-quality [options]')
1270
+ console.log('')
1271
+ console.log('Options:')
1272
+ console.log(' --help, -h Show this help message')
1273
+ console.log(' --version, -v Show version number')
1274
+ console.log(' --logs Show detailed error logs')
1275
+ console.log(' --init Initialize config files in root directory (ESLint v9+)')
1276
+ console.log(' --config Generate .code-quality.json configuration file')
1277
+ console.log(' --wizard Run interactive setup wizard')
1278
+ console.log(' --env <name> Set environment (development, ci, production)')
1279
+ console.log(' --ESLint Run only ESLint checks')
1280
+ console.log(' --TypeScript Run only TypeScript checks')
1281
+ console.log(' --Prettier Run only Prettier checks')
1282
+ console.log(' --Knip Run only Knip checks')
1283
+ console.log(' --Snyk Run only Snyk checks')
1284
+ console.log(' --fix Auto-fix issues when possible (ESLint, Prettier)')
1285
+ console.log('')
1286
+ console.log('Examples:')
1287
+ console.log(' code-quality # Run checks with defaults')
1288
+ console.log(' code-quality --init # Create config files in root (for ESLint v9+)')
1289
+ console.log(' code-quality --wizard # Run interactive wizard')
1290
+ console.log(' code-quality --config # Generate config file')
1291
+ console.log(' code-quality --logs # Run with verbose output')
1292
+ console.log(' code-quality --env ci # Run CI/CD checks (all tools)')
1293
+ console.log(' code-quality --env development # Run dev checks (ESLint, TS, Prettier)')
1294
+ console.log(' code-quality --ESLint # Run only ESLint')
1295
+ console.log(' code-quality --TypeScript # Run only TypeScript')
1296
+ console.log(' code-quality --Prettier # Run only Prettier')
1297
+ console.log(' code-quality --ESLint --logs # Run ESLint with verbose output')
1298
+ console.log(' code-quality --ESLint --fix # Fix ESLint issues automatically')
1299
+ console.log(' code-quality --Prettier --fix # Format code with Prettier')
1300
+ console.log(' code-quality --ESLint --Prettier --fix --logs # Fix both with logs')
1301
+ console.log('')
1302
+ console.log('Environment Variables:')
1303
+ console.log(' NODE_ENV # Set environment automatically')
1304
+ console.log(' CODE_QUALITY_ENV # Override environment detection')
1305
+ console.log('')
1306
+ console.log('Default behavior:')
1307
+ console.log(' - Development: ESLint, TypeScript, Prettier')
1308
+ console.log(' - CI/Production: All tools (including Knip, Snyk)')
1309
+ process.exit(0)
276
1310
  }
277
1311
 
278
1312
  if (args.includes('--version') || args.includes('-v')) {
279
- const pkg = require('./package.json');
280
- console.log(pkg.version);
281
- process.exit(0);
1313
+ const pkg = require('./package.json')
1314
+ console.log(pkg.version)
1315
+ process.exit(0)
1316
+ }
1317
+
1318
+ if (args.includes('--init')) {
1319
+ initConfigFiles()
1320
+ process.exit(0)
1321
+ }
1322
+
1323
+ if (args.includes('--config')) {
1324
+ generateConfigFile()
1325
+ process.exit(0)
1326
+ }
1327
+
1328
+ if (args.includes('--wizard')) {
1329
+ runWizard().catch((err) => {
1330
+ console.error('❌ Wizard error:', err.message)
1331
+ process.exit(1)
1332
+ })
1333
+ return
1334
+ }
1335
+
1336
+ // Parse tool-specific flags
1337
+ const toolFlags = ['--ESLint', '--TypeScript', '--Prettier', '--Knip', '--Snyk']
1338
+ const specificTools = args.filter((arg) => toolFlags.includes(arg))
1339
+
1340
+ // If specific tools are requested, run them directly
1341
+ if (specificTools.length > 0) {
1342
+ const config = loadConfigFile() || {}
1343
+ const results = []
1344
+ let allPassed = true
1345
+ const checker = new CodeQualityChecker(config)
1346
+ const useFix = args.includes('--fix')
1347
+
1348
+ for (const toolFlag of specificTools) {
1349
+ const toolName = toolFlag.replace('--', '')
1350
+ const defaultTool = DEFAULT_TOOLS.find((t) => t.name === toolName)
1351
+
1352
+ if (!defaultTool) {
1353
+ console.error(`❌ Unknown tool: ${toolName}`)
1354
+ process.exit(1)
1355
+ }
1356
+
1357
+ const action = useFix ? 'fixing' : 'checking'
1358
+ console.log(`\n🔧 Running ${toolName} ${action}...\n`)
1359
+
1360
+ // Use custom command from config if available, otherwise use default
1361
+ let command = defaultTool.args
1362
+ if (config.commands && config.commands[toolName]) {
1363
+ command = config.commands[toolName]
1364
+ }
1365
+
1366
+ // Add fix flag for tools that support it
1367
+ if (useFix) {
1368
+ if (toolName === 'ESLint') {
1369
+ command = command.replace('. --ext', ' --fix . --ext')
1370
+ } else if (toolName === 'Prettier') {
1371
+ command = command.replace('--check', '--write')
1372
+ } else if (toolName === 'Knip') {
1373
+ // Knip doesn't have a fix mode, so we warn user
1374
+ console.log(`⚠️ ${toolName} does not support auto-fix. Running checks only...`)
1375
+ } else if (toolName === 'TypeScript') {
1376
+ // TypeScript doesn't have a fix mode
1377
+ console.log(`⚠️ ${toolName} does not support auto-fix. Running checks only...`)
1378
+ } else if (toolName === 'Snyk') {
1379
+ // Snyk has fix but it's for security vulnerabilities, not style
1380
+ console.log(
1381
+ `⚠️ ${toolName} auto-fix not supported through --fix flag. Use 'snyk wizard' for security fixes.`
1382
+ )
1383
+ }
1384
+ }
1385
+
1386
+ // Build full command
1387
+ const binDir = resolveToolBinDir()
1388
+ const binPath = path.join(binDir, defaultTool.bin)
1389
+ const fullCommand = `${binPath}${command ? ' ' + command : ''}`
1390
+
1391
+ // Run the specific tool
1392
+ const result = checker.runCommand(fullCommand, defaultTool.description)
1393
+
1394
+ // Parse error counts and lines
1395
+ const counts = checker._parseErrorCounts(toolName, result.output)
1396
+ const errorLines = checker._parseErrorLines(toolName, result.output)
1397
+
1398
+ const resultData = {
1399
+ name: toolName,
1400
+ description: defaultTool.description,
1401
+ ...result,
1402
+ ...counts,
1403
+ errorLines,
1404
+ }
1405
+
1406
+ results.push(resultData)
1407
+
1408
+ const icon = result.success ? '✅' : '❌'
1409
+ const status = result.success ? (useFix ? 'Fixed' : 'Passed') : 'Failed'
1410
+
1411
+ console.log(`${icon} ${toolName}... ${status}`)
1412
+
1413
+ // Show error details if failed and logs enabled
1414
+ if (!result.success && args.includes('--logs')) {
1415
+ console.log(`\n❌ ${toolName} Error:`)
1416
+ console.log('─'.repeat(50))
1417
+
1418
+ if (errorLines.length > 0) {
1419
+ console.log('📝 Individual Errors:')
1420
+ for (const errorLine of errorLines) {
1421
+ console.log(`• ${errorLine}`)
1422
+ }
1423
+ console.log('') // Empty line before full output
1424
+ }
1425
+
1426
+ console.log('📄 Full Output:')
1427
+ console.log(result.output)
1428
+ console.log('─'.repeat(50))
1429
+ }
1430
+
1431
+ if (!result.success) {
1432
+ allPassed = false
1433
+ }
1434
+ }
1435
+
1436
+ // Generate report for AI analysis
1437
+ const pm = config.packageManager || detectPackageManager()
1438
+ const runCmd = getRunPrefix(pm)
1439
+ checker._writeReport(results, allPassed, pm, runCmd)
1440
+
1441
+ // Show summary with error details for AI
1442
+ console.log('\n' + '─'.repeat(50))
1443
+ console.log('📊 Tool-Specific Check Summary\n')
1444
+
1445
+ for (const result of results) {
1446
+ const icon = result.success ? '✅' : '❌'
1447
+ const name = result.name.padEnd(12, ' ')
1448
+ let status = result.success ? (useFix ? 'Fixed' : 'Passed') : 'Failed'
1449
+
1450
+ if (!result.success && (result.errors > 0 || result.warnings > 0)) {
1451
+ const parts = []
1452
+ if (result.errors > 0) parts.push(`${result.errors} error${result.errors !== 1 ? 's' : ''}`)
1453
+ if (result.warnings > 0)
1454
+ parts.push(`${result.warnings} warning${result.warnings !== 1 ? 's' : ''}`)
1455
+ status = `${status} (${parts.join(', ')})`
1456
+ }
1457
+
1458
+ console.log(`${icon} ${name}${status}`)
1459
+ }
1460
+
1461
+ console.log('\n' + '─'.repeat(50))
1462
+
1463
+ if (!allPassed) {
1464
+ console.log('🤖 For AI Analysis - Summary of All Errors:\n')
1465
+ for (const result of results) {
1466
+ if (!result.success && result.errorLines && result.errorLines.length > 0) {
1467
+ console.log(`## ${result.name} Errors:`)
1468
+ for (const errorLine of result.errorLines) {
1469
+ console.log(errorLine)
1470
+ }
1471
+ console.log('') // Empty line between tools
1472
+ }
1473
+ }
1474
+ console.log('📄 See .quality-report.md for complete details\n')
1475
+ } else {
1476
+ console.log('🎉 All specified tools passed!\n')
1477
+ }
1478
+
1479
+ process.exit(allPassed ? 0 : 1)
1480
+ }
1481
+
1482
+ // Parse environment flag
1483
+ let environment = undefined
1484
+ const envIndex = args.findIndex((arg) => arg === '--env')
1485
+ if (envIndex !== -1 && args[envIndex + 1]) {
1486
+ environment = args[envIndex + 1]
1487
+ // Remove --env and its value from args
1488
+ args.splice(envIndex, 2)
1489
+ }
1490
+
1491
+ // Load config file if exists, if not - run wizard automatically
1492
+ const config = loadConfigFile()
1493
+ if (!config) {
1494
+ console.log('🔧 No configuration found. Starting setup wizard...\n')
1495
+ runWizard().catch((err) => {
1496
+ console.error('❌ Wizard error:', err.message)
1497
+ process.exit(1)
1498
+ })
1499
+ return
1500
+ }
1501
+
1502
+ // Override environment if specified
1503
+ if (environment) {
1504
+ config.environment = environment
282
1505
  }
283
1506
 
284
- const checker = new CodeQualityChecker();
285
- checker.run({ showLogs: args.includes('--logs') }).then((result) => {
286
- process.exit(result.success ? 0 : 1);
287
- });
1507
+ const checker = new CodeQualityChecker(config)
1508
+ checker
1509
+ .run({
1510
+ showLogs: args.includes('--logs'),
1511
+ useFix: args.includes('--fix'),
1512
+ })
1513
+ .then((result) => {
1514
+ process.exit(result.success ? 0 : 1)
1515
+ })
288
1516
  }
289
1517
 
290
1518
  // ─── Exports ────────────────────────────────────────────────────────────────
291
1519
 
292
- module.exports = { CodeQualityChecker, runQualityCheck };
1520
+ module.exports = { CodeQualityChecker, runQualityCheck }