code-quality-lib 2.0.2 → 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/.code-quality/.prettierignore +87 -0
- package/.code-quality/.prettierrc +17 -0
- package/.code-quality/README.md +32 -0
- package/.code-quality/config.json +34 -0
- package/.code-quality/eslint.config.mjs +311 -0
- package/.code-quality/knip.json +30 -0
- package/.code-quality/tsconfig.json +32 -0
- package/README.md +300 -29
- package/index.d.ts +32 -21
- package/index.js +1303 -166
- package/package.json +19 -15
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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,714 @@ 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
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
{
|
|
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
186
|
loadEnv: options.loadEnv !== false,
|
|
165
187
|
useProjectConfig: options.useProjectConfig !== false,
|
|
166
|
-
tools: options.tools
|
|
188
|
+
tools: this._resolveToolsForEnvironment(options.tools, options.environments, env),
|
|
167
189
|
commands: options.commands || {},
|
|
168
190
|
descriptions: options.descriptions || {},
|
|
169
191
|
packageManager: options.packageManager || detectPackageManager(),
|
|
170
|
-
|
|
192
|
+
environment: env,
|
|
193
|
+
environments: options.environments || {},
|
|
194
|
+
}
|
|
171
195
|
|
|
172
|
-
if (this.options.loadEnv) loadEnvFile()
|
|
196
|
+
if (this.options.loadEnv) loadEnvFile()
|
|
173
197
|
|
|
174
198
|
// Detect project configs if useProjectConfig is enabled
|
|
175
|
-
this.projectConfigs = this.options.useProjectConfig ? detectProjectConfigs() : {}
|
|
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
|
+
}
|
|
176
221
|
}
|
|
177
222
|
|
|
178
223
|
_getChecks() {
|
|
179
|
-
const binDir = resolveToolBinDir()
|
|
180
|
-
const checks = []
|
|
224
|
+
const binDir = resolveToolBinDir()
|
|
225
|
+
const checks = []
|
|
181
226
|
|
|
182
227
|
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
|
|
228
|
+
const defaultTool = DEFAULT_TOOLS.find((t) => t.name === toolName)
|
|
229
|
+
if (!defaultTool && !this.options.commands[toolName]) continue
|
|
185
230
|
|
|
186
|
-
let cmd = this.options.commands[toolName]
|
|
231
|
+
let cmd = this.options.commands[toolName]
|
|
187
232
|
|
|
188
|
-
// If
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
192
241
|
|
|
193
242
|
// Add config flags if project configs exist and useProjectConfig is true
|
|
194
243
|
if (this.options.useProjectConfig) {
|
|
195
244
|
if (toolName === 'ESLint' && this.projectConfigs.eslint) {
|
|
196
|
-
args = `${args} --config ${this.projectConfigs.eslint}
|
|
245
|
+
args = `${args} --config ${this.projectConfigs.eslint}`
|
|
197
246
|
} else if (toolName === 'Prettier' && this.projectConfigs.prettier) {
|
|
198
|
-
args = `${args} --config ${this.projectConfigs.prettier}
|
|
247
|
+
args = `${args} --config ${this.projectConfigs.prettier}`
|
|
199
248
|
} else if (toolName === 'TypeScript' && this.projectConfigs.typescript) {
|
|
200
|
-
args = `--project ${this.projectConfigs.typescript} --noEmit
|
|
249
|
+
args = `--project ${this.projectConfigs.typescript} --noEmit`
|
|
201
250
|
} else if (toolName === 'Knip' && this.projectConfigs.knip) {
|
|
202
|
-
args = `--config ${this.projectConfigs.knip}
|
|
251
|
+
args = `--config ${this.projectConfigs.knip}`
|
|
203
252
|
}
|
|
204
253
|
}
|
|
205
254
|
|
|
206
|
-
cmd = `${binPath}${args ? ' ' + args : ''}
|
|
255
|
+
cmd = `${binPath}${args ? ' ' + args : ''}`
|
|
207
256
|
}
|
|
208
257
|
|
|
209
258
|
const description =
|
|
210
|
-
this.options.descriptions[toolName] ||
|
|
211
|
-
(defaultTool ? defaultTool.description : toolName);
|
|
259
|
+
this.options.descriptions[toolName] || (defaultTool ? defaultTool.description : toolName)
|
|
212
260
|
|
|
213
|
-
checks.push({ name: toolName, cmd, description })
|
|
261
|
+
checks.push({ name: toolName, cmd, description })
|
|
214
262
|
}
|
|
215
263
|
|
|
216
|
-
return checks
|
|
264
|
+
return checks
|
|
217
265
|
}
|
|
218
266
|
|
|
219
|
-
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
|
+
|
|
220
323
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
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() }
|
|
223
331
|
} catch (error) {
|
|
224
|
-
const output = error.stdout || error.stderr || error.message || 'Unknown error'
|
|
225
|
-
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
|
|
226
546
|
}
|
|
547
|
+
|
|
548
|
+
return errorLines
|
|
227
549
|
}
|
|
228
550
|
|
|
229
551
|
async run(options = {}) {
|
|
230
|
-
const showLogs = options.showLogs || false
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
552
|
+
const showLogs = options.showLogs || false
|
|
553
|
+
const useFix = options.useFix || false
|
|
554
|
+
|
|
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`)
|
|
241
594
|
|
|
242
595
|
if (showLogs) {
|
|
243
|
-
console.log('📋
|
|
596
|
+
console.log('📋 Verbose logging enabled\n')
|
|
244
597
|
}
|
|
245
598
|
|
|
599
|
+
// Run each check with step-by-step output
|
|
246
600
|
for (const { name, cmd, description } of checks) {
|
|
247
|
-
|
|
248
|
-
const
|
|
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)
|
|
622
|
+
|
|
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
|
+
}
|
|
249
628
|
|
|
250
|
-
|
|
251
|
-
|
|
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`)
|
|
252
639
|
} else {
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
}
|
|
255
675
|
}
|
|
256
676
|
|
|
677
|
+
// Show details if logs enabled
|
|
257
678
|
if (showLogs && result.output) {
|
|
258
|
-
|
|
259
|
-
console.log(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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))
|
|
264
690
|
}
|
|
265
691
|
|
|
266
|
-
results.push(
|
|
692
|
+
results.push(finalResult)
|
|
693
|
+
step++
|
|
267
694
|
}
|
|
268
695
|
|
|
269
696
|
// Generate report
|
|
270
|
-
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}`)
|
|
730
|
+
|
|
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
|
+
}
|
|
271
739
|
|
|
272
|
-
console.log('\n' + '─'.repeat(50))
|
|
740
|
+
console.log('\n' + '─'.repeat(50))
|
|
273
741
|
|
|
274
742
|
if (allPassed) {
|
|
275
|
-
console.log('
|
|
743
|
+
console.log('🎉 Success! All quality checks passed.\n')
|
|
744
|
+
console.log('✅ Your code is ready for production!\n')
|
|
276
745
|
} else {
|
|
277
|
-
console.log('
|
|
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
|
+
|
|
278
761
|
if (!showLogs) {
|
|
279
|
-
console.log('💡 Run with --logs flag to see
|
|
762
|
+
console.log('💡 Run with --logs flag to see full tool output')
|
|
280
763
|
}
|
|
281
|
-
console.log('📄 See .quality-report.md for
|
|
764
|
+
console.log('📄 See .quality-report.md for complete details\n')
|
|
282
765
|
}
|
|
283
766
|
|
|
284
767
|
return {
|
|
285
768
|
success: allPassed,
|
|
286
769
|
message: allPassed ? 'All quality checks passed' : 'Some quality checks failed',
|
|
287
770
|
results,
|
|
288
|
-
}
|
|
771
|
+
}
|
|
289
772
|
}
|
|
290
773
|
|
|
291
774
|
_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)
|
|
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)
|
|
296
779
|
|
|
297
|
-
let report = `# Code Quality Report\n\n
|
|
298
|
-
report += `**Generated**: ${timestamp}\n
|
|
299
|
-
report += `**Package Manager**: ${pm}\n\n
|
|
300
|
-
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`
|
|
301
784
|
|
|
302
785
|
for (const r of results) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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`
|
|
306
792
|
if (r.output) {
|
|
307
|
-
report += `**Output**:\n\`\`\`\n${r.output}\n\`\`\`\n\n
|
|
793
|
+
report += `**Output**:\n\`\`\`\n${r.output}\n\`\`\`\n\n`
|
|
308
794
|
}
|
|
309
|
-
report += `---\n\n
|
|
795
|
+
report += `---\n\n`
|
|
310
796
|
}
|
|
311
797
|
|
|
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
|
|
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`
|
|
316
802
|
|
|
317
803
|
if (allPassed) {
|
|
318
|
-
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`
|
|
319
805
|
} 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
|
|
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`
|
|
324
810
|
}
|
|
325
811
|
|
|
326
|
-
report += `---\n\n## For AI Agents\n\n
|
|
327
|
-
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`
|
|
328
814
|
if (!allPassed) {
|
|
329
|
-
report += `**Action Required**:\n
|
|
815
|
+
report += `**Action Required**:\n`
|
|
330
816
|
for (const r of failed) {
|
|
331
|
-
report += `- Fix ${r.name} errors\n
|
|
817
|
+
report += `- Fix ${r.name} errors\n`
|
|
332
818
|
}
|
|
333
|
-
report += `\n
|
|
819
|
+
report += `\n`
|
|
334
820
|
}
|
|
335
821
|
|
|
336
822
|
try {
|
|
337
|
-
fs.writeFileSync(reportPath, report, 'utf8')
|
|
338
|
-
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`)
|
|
339
825
|
} catch (err) {
|
|
340
|
-
console.error(`\n⚠️ Failed to write report: ${err.message}`)
|
|
826
|
+
console.error(`\n⚠️ Failed to write report: ${err.message}`)
|
|
341
827
|
}
|
|
342
828
|
}
|
|
343
829
|
}
|
|
@@ -345,39 +831,690 @@ class CodeQualityChecker {
|
|
|
345
831
|
// ─── Convenience Function ───────────────────────────────────────────────────
|
|
346
832
|
|
|
347
833
|
async function runQualityCheck(options = {}) {
|
|
348
|
-
const checker = new CodeQualityChecker(options)
|
|
349
|
-
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
|
+
})
|
|
350
1261
|
}
|
|
351
1262
|
|
|
352
1263
|
// ─── CLI Entry Point ────────────────────────────────────────────────────────
|
|
353
1264
|
|
|
354
1265
|
if (require.main === module) {
|
|
355
|
-
const args = process.argv.slice(2)
|
|
1266
|
+
const args = process.argv.slice(2)
|
|
356
1267
|
|
|
357
1268
|
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('
|
|
366
|
-
|
|
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)
|
|
367
1310
|
}
|
|
368
1311
|
|
|
369
1312
|
if (args.includes('--version') || args.includes('-v')) {
|
|
370
|
-
const pkg = require('./package.json')
|
|
371
|
-
console.log(pkg.version)
|
|
372
|
-
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
|
|
373
1505
|
}
|
|
374
1506
|
|
|
375
|
-
const checker = new CodeQualityChecker()
|
|
376
|
-
checker
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
})
|
|
379
1516
|
}
|
|
380
1517
|
|
|
381
1518
|
// ─── Exports ────────────────────────────────────────────────────────────────
|
|
382
1519
|
|
|
383
|
-
module.exports = { CodeQualityChecker, runQualityCheck }
|
|
1520
|
+
module.exports = { CodeQualityChecker, runQualityCheck }
|