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