@soulbatical/tetra-dev-toolkit 1.3.3 → 1.4.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/bin/tetra-setup.js +463 -2
- package/lib/checks/health/conventional-commits.js +164 -0
- package/lib/checks/health/coverage-thresholds.js +199 -0
- package/lib/checks/health/dependency-cruiser.js +113 -0
- package/lib/checks/health/eslint-security.js +172 -0
- package/lib/checks/health/index.js +7 -1
- package/lib/checks/health/prettier.js +162 -0
- package/lib/checks/health/scanner.js +14 -2
- package/lib/checks/health/types.js +1 -1
- package/lib/checks/health/typescript-strict.js +143 -0
- package/package.json +5 -3
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: Test Coverage Thresholds
|
|
3
|
+
*
|
|
4
|
+
* Checks that test coverage thresholds are configured and enforced.
|
|
5
|
+
* Score: up to 3 points:
|
|
6
|
+
* +1 for coverage config in test framework (jest/vitest)
|
|
7
|
+
* +1 for threshold values defined (statements/branches/lines)
|
|
8
|
+
* +1 for coverage script in package.json or CI enforcement
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from 'fs'
|
|
12
|
+
import { join } from 'path'
|
|
13
|
+
import { createCheck } from './types.js'
|
|
14
|
+
|
|
15
|
+
export async function check(projectPath) {
|
|
16
|
+
const result = createCheck('coverage-thresholds', 3, {
|
|
17
|
+
framework: null,
|
|
18
|
+
hasCoverageConfig: false,
|
|
19
|
+
hasThresholds: false,
|
|
20
|
+
thresholds: null,
|
|
21
|
+
hasCoverageScript: false,
|
|
22
|
+
coverageScript: null,
|
|
23
|
+
hasCiCoverage: false,
|
|
24
|
+
message: ''
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Check Jest configs
|
|
28
|
+
const jestConfigs = [
|
|
29
|
+
'jest.config.js',
|
|
30
|
+
'jest.config.ts',
|
|
31
|
+
'jest.config.cjs',
|
|
32
|
+
'jest.config.mjs',
|
|
33
|
+
'backend/jest.config.js',
|
|
34
|
+
'backend/jest.config.ts'
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
for (const config of jestConfigs) {
|
|
38
|
+
const configPath = join(projectPath, config)
|
|
39
|
+
if (!existsSync(configPath)) continue
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
43
|
+
result.details.framework = 'jest'
|
|
44
|
+
|
|
45
|
+
// Check for coverage configuration
|
|
46
|
+
if (content.includes('collectCoverage') || content.includes('coverageDirectory') ||
|
|
47
|
+
content.includes('coverageReporters') || content.includes('collectCoverageFrom')) {
|
|
48
|
+
result.details.hasCoverageConfig = true
|
|
49
|
+
result.score += 1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check for thresholds
|
|
53
|
+
if (content.includes('coverageThreshold')) {
|
|
54
|
+
result.details.hasThresholds = true
|
|
55
|
+
result.score += 1
|
|
56
|
+
|
|
57
|
+
// Try to extract threshold values
|
|
58
|
+
const thresholdMatch = content.match(/coverageThreshold\s*:\s*\{[\s\S]*?global\s*:\s*\{([^}]+)\}/)
|
|
59
|
+
if (thresholdMatch) {
|
|
60
|
+
result.details.thresholds = thresholdMatch[1].trim()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch { /* ignore */ }
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check Vitest configs
|
|
68
|
+
if (!result.details.framework) {
|
|
69
|
+
const vitestConfigs = [
|
|
70
|
+
'vitest.config.ts',
|
|
71
|
+
'vitest.config.js',
|
|
72
|
+
'vitest.config.mjs',
|
|
73
|
+
'vite.config.ts',
|
|
74
|
+
'vite.config.js',
|
|
75
|
+
'backend/vitest.config.ts',
|
|
76
|
+
'backend/vite.config.ts'
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
for (const config of vitestConfigs) {
|
|
80
|
+
const configPath = join(projectPath, config)
|
|
81
|
+
if (!existsSync(configPath)) continue
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
85
|
+
|
|
86
|
+
// Only count as vitest if it has test config
|
|
87
|
+
if (!content.includes('test')) continue
|
|
88
|
+
|
|
89
|
+
result.details.framework = 'vitest'
|
|
90
|
+
|
|
91
|
+
if (content.includes('coverage')) {
|
|
92
|
+
result.details.hasCoverageConfig = true
|
|
93
|
+
result.score += 1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (content.includes('thresholds') || content.includes('threshold')) {
|
|
97
|
+
result.details.hasThresholds = true
|
|
98
|
+
result.score += 1
|
|
99
|
+
}
|
|
100
|
+
} catch { /* ignore */ }
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Also check package.json for jest coverage config
|
|
106
|
+
if (!result.details.hasCoverageConfig) {
|
|
107
|
+
const pkgPath = join(projectPath, 'package.json')
|
|
108
|
+
if (existsSync(pkgPath)) {
|
|
109
|
+
try {
|
|
110
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
111
|
+
if (pkg.jest) {
|
|
112
|
+
result.details.framework = result.details.framework || 'jest'
|
|
113
|
+
if (pkg.jest.collectCoverage || pkg.jest.coverageReporters) {
|
|
114
|
+
result.details.hasCoverageConfig = true
|
|
115
|
+
result.score += 1
|
|
116
|
+
}
|
|
117
|
+
if (pkg.jest.coverageThreshold) {
|
|
118
|
+
result.details.hasThresholds = true
|
|
119
|
+
result.score += 1
|
|
120
|
+
const global = pkg.jest.coverageThreshold.global
|
|
121
|
+
if (global) {
|
|
122
|
+
result.details.thresholds = global
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for coverage script
|
|
131
|
+
const packageLocations = [
|
|
132
|
+
'package.json',
|
|
133
|
+
'backend/package.json',
|
|
134
|
+
'frontend/package.json'
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
for (const loc of packageLocations) {
|
|
138
|
+
const pkgPath = join(projectPath, loc)
|
|
139
|
+
if (!existsSync(pkgPath)) continue
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
143
|
+
const scripts = pkg.scripts || {}
|
|
144
|
+
|
|
145
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
146
|
+
if (typeof cmd !== 'string') continue
|
|
147
|
+
if (name.includes('coverage') || cmd.includes('--coverage') ||
|
|
148
|
+
cmd.includes('--coverageThreshold')) {
|
|
149
|
+
result.details.hasCoverageScript = true
|
|
150
|
+
result.details.coverageScript = `${loc} → ${name}`
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (result.details.hasCoverageScript) break
|
|
155
|
+
} catch { /* ignore */ }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check CI workflows for coverage enforcement
|
|
159
|
+
const ciFiles = [
|
|
160
|
+
'.github/workflows/quality.yml',
|
|
161
|
+
'.github/workflows/test.yml',
|
|
162
|
+
'.github/workflows/ci.yml',
|
|
163
|
+
'.github/workflows/admin-mode-tests.yml'
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
for (const ci of ciFiles) {
|
|
167
|
+
const ciPath = join(projectPath, ci)
|
|
168
|
+
if (!existsSync(ciPath)) continue
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const content = readFileSync(ciPath, 'utf-8')
|
|
172
|
+
if (content.includes('coverage') || content.includes('--coverageThreshold')) {
|
|
173
|
+
result.details.hasCiCoverage = true
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
} catch { /* ignore */ }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (result.details.hasCoverageScript || result.details.hasCiCoverage) {
|
|
180
|
+
result.score += 1
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
result.score = Math.min(result.score, result.maxScore)
|
|
184
|
+
|
|
185
|
+
// Determine status
|
|
186
|
+
if (result.score === 0) {
|
|
187
|
+
result.status = result.details.framework ? 'warning' : 'error'
|
|
188
|
+
result.details.message = result.details.framework
|
|
189
|
+
? `${result.details.framework} detected but no coverage configuration`
|
|
190
|
+
: 'No test framework or coverage configuration detected'
|
|
191
|
+
} else if (result.score < 2) {
|
|
192
|
+
result.status = 'warning'
|
|
193
|
+
result.details.message = 'Coverage partially configured — add thresholds for enforcement'
|
|
194
|
+
} else {
|
|
195
|
+
result.details.message = `Coverage well-configured (${result.details.framework})`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: Dependency Cruiser
|
|
3
|
+
*
|
|
4
|
+
* Checks for dependency analysis and circular dependency prevention.
|
|
5
|
+
* Score: up to 2 points:
|
|
6
|
+
* +1 for dependency-cruiser config file exists
|
|
7
|
+
* +1 for no-circular rule defined or dep:check script
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'fs'
|
|
11
|
+
import { join } from 'path'
|
|
12
|
+
import { createCheck } from './types.js'
|
|
13
|
+
|
|
14
|
+
export async function check(projectPath) {
|
|
15
|
+
const result = createCheck('dependency-cruiser', 2, {
|
|
16
|
+
configFound: false,
|
|
17
|
+
configPath: null,
|
|
18
|
+
hasNoCircularRule: false,
|
|
19
|
+
hasCheckScript: false,
|
|
20
|
+
checkScript: null,
|
|
21
|
+
installed: false,
|
|
22
|
+
message: ''
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Check for dependency-cruiser config files
|
|
26
|
+
const depCruiserConfigs = [
|
|
27
|
+
'.dependency-cruiser.js',
|
|
28
|
+
'.dependency-cruiser.cjs',
|
|
29
|
+
'.dependency-cruiser.mjs',
|
|
30
|
+
'.dependency-cruiser.json',
|
|
31
|
+
'dependency-cruiser.config.js',
|
|
32
|
+
'dependency-cruiser.config.cjs',
|
|
33
|
+
// Sub-packages
|
|
34
|
+
'backend/.dependency-cruiser.js',
|
|
35
|
+
'backend/.dependency-cruiser.cjs'
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
let configContent = null
|
|
39
|
+
|
|
40
|
+
for (const config of depCruiserConfigs) {
|
|
41
|
+
const configPath = join(projectPath, config)
|
|
42
|
+
if (!existsSync(configPath)) continue
|
|
43
|
+
|
|
44
|
+
result.details.configFound = true
|
|
45
|
+
result.details.configPath = config
|
|
46
|
+
result.score += 1
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
configContent = readFileSync(configPath, 'utf-8')
|
|
50
|
+
} catch { /* ignore */ }
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for dependency-cruiser in dependencies
|
|
55
|
+
const packageLocations = [
|
|
56
|
+
'package.json',
|
|
57
|
+
'backend/package.json'
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
for (const loc of packageLocations) {
|
|
61
|
+
const pkgPath = join(projectPath, loc)
|
|
62
|
+
if (!existsSync(pkgPath)) continue
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
66
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
67
|
+
|
|
68
|
+
if (allDeps['dependency-cruiser']) {
|
|
69
|
+
result.details.installed = true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for dep check scripts
|
|
73
|
+
const scripts = pkg.scripts || {}
|
|
74
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
75
|
+
if (typeof cmd !== 'string') continue
|
|
76
|
+
if (cmd.includes('dependency-cruiser') || cmd.includes('depcruise') ||
|
|
77
|
+
name === 'check:deps' || name === 'check:circular') {
|
|
78
|
+
result.details.hasCheckScript = true
|
|
79
|
+
result.details.checkScript = `${loc} → ${name}`
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch { /* ignore */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check config content for no-circular rule
|
|
87
|
+
if (configContent) {
|
|
88
|
+
if (configContent.includes('no-circular') || configContent.includes('circular')) {
|
|
89
|
+
result.details.hasNoCircularRule = true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (result.details.hasNoCircularRule || result.details.hasCheckScript) {
|
|
94
|
+
result.score += 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
result.score = Math.min(result.score, result.maxScore)
|
|
98
|
+
|
|
99
|
+
// Determine status
|
|
100
|
+
if (result.score === 0) {
|
|
101
|
+
result.status = 'warning'
|
|
102
|
+
result.details.message = 'No dependency analysis configured — circular dependencies go undetected'
|
|
103
|
+
} else if (result.score < 2) {
|
|
104
|
+
result.status = 'warning'
|
|
105
|
+
result.details.message = result.details.configFound
|
|
106
|
+
? 'Dependency cruiser configured but no circular dependency rule'
|
|
107
|
+
: 'Dependency check script found but no config file'
|
|
108
|
+
} else {
|
|
109
|
+
result.details.message = 'Dependency analysis well-configured'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result
|
|
113
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: ESLint Security Plugins
|
|
3
|
+
*
|
|
4
|
+
* Checks for security-focused ESLint plugins.
|
|
5
|
+
* Score: up to 3 points:
|
|
6
|
+
* +1 for eslint-plugin-security installed
|
|
7
|
+
* +1 for eslint-plugin-sonarjs installed
|
|
8
|
+
* +1 for custom security rules (no-restricted-imports/syntax for dangerous patterns)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from 'fs'
|
|
12
|
+
import { join } from 'path'
|
|
13
|
+
import { createCheck } from './types.js'
|
|
14
|
+
|
|
15
|
+
export async function check(projectPath) {
|
|
16
|
+
const result = createCheck('eslint-security', 3, {
|
|
17
|
+
eslintConfigFound: false,
|
|
18
|
+
eslintConfigPath: null,
|
|
19
|
+
hasSecurityPlugin: false,
|
|
20
|
+
hasSonarPlugin: false,
|
|
21
|
+
hasCustomSecurityRules: false,
|
|
22
|
+
securityPlugins: [],
|
|
23
|
+
message: ''
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Check if ESLint is configured at all
|
|
27
|
+
const eslintConfigs = [
|
|
28
|
+
'.eslintrc.js',
|
|
29
|
+
'.eslintrc.cjs',
|
|
30
|
+
'.eslintrc.json',
|
|
31
|
+
'.eslintrc.yml',
|
|
32
|
+
'.eslintrc.yaml',
|
|
33
|
+
'.eslintrc',
|
|
34
|
+
'eslint.config.js',
|
|
35
|
+
'eslint.config.cjs',
|
|
36
|
+
'eslint.config.mjs',
|
|
37
|
+
'eslint.config.ts',
|
|
38
|
+
// Sub-packages
|
|
39
|
+
'backend/.eslintrc.js',
|
|
40
|
+
'backend/.eslintrc.json',
|
|
41
|
+
'backend/eslint.config.js',
|
|
42
|
+
'backend/eslint.config.mjs',
|
|
43
|
+
'frontend/.eslintrc.js',
|
|
44
|
+
'frontend/.eslintrc.json',
|
|
45
|
+
'frontend/eslint.config.js',
|
|
46
|
+
'frontend/eslint.config.mjs'
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
let eslintContent = null
|
|
50
|
+
|
|
51
|
+
for (const config of eslintConfigs) {
|
|
52
|
+
const configPath = join(projectPath, config)
|
|
53
|
+
if (!existsSync(configPath)) continue
|
|
54
|
+
|
|
55
|
+
result.details.eslintConfigFound = true
|
|
56
|
+
result.details.eslintConfigPath = config
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
eslintContent = readFileSync(configPath, 'utf-8')
|
|
60
|
+
} catch { /* ignore */ }
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!result.details.eslintConfigFound) {
|
|
65
|
+
result.status = 'error'
|
|
66
|
+
result.details.message = 'No ESLint configuration found'
|
|
67
|
+
return result
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check package.json for security plugins in dependencies
|
|
71
|
+
const packageLocations = [
|
|
72
|
+
'package.json',
|
|
73
|
+
'backend/package.json',
|
|
74
|
+
'frontend/package.json'
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
const allDeps = {}
|
|
78
|
+
for (const loc of packageLocations) {
|
|
79
|
+
const pkgPath = join(projectPath, loc)
|
|
80
|
+
if (!existsSync(pkgPath)) continue
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
84
|
+
Object.assign(allDeps, pkg.dependencies || {}, pkg.devDependencies || {})
|
|
85
|
+
} catch { /* ignore */ }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for security plugins
|
|
89
|
+
const securityPlugins = [
|
|
90
|
+
'eslint-plugin-security',
|
|
91
|
+
'@eslint/plugin-security',
|
|
92
|
+
'eslint-plugin-no-secrets'
|
|
93
|
+
]
|
|
94
|
+
for (const plugin of securityPlugins) {
|
|
95
|
+
if (allDeps[plugin]) {
|
|
96
|
+
result.details.hasSecurityPlugin = true
|
|
97
|
+
result.details.securityPlugins.push(plugin)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Also check ESLint config content for security plugin references
|
|
102
|
+
if (eslintContent) {
|
|
103
|
+
if (eslintContent.includes('plugin:security') || eslintContent.includes("'security'") ||
|
|
104
|
+
eslintContent.includes('"security"') || eslintContent.includes('eslint-plugin-security')) {
|
|
105
|
+
result.details.hasSecurityPlugin = true
|
|
106
|
+
if (!result.details.securityPlugins.includes('eslint-plugin-security')) {
|
|
107
|
+
result.details.securityPlugins.push('eslint-plugin-security (in config)')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (result.details.hasSecurityPlugin) {
|
|
113
|
+
result.score += 1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for SonarJS plugin
|
|
117
|
+
const sonarPlugins = [
|
|
118
|
+
'eslint-plugin-sonarjs',
|
|
119
|
+
'@eslint/plugin-sonarjs'
|
|
120
|
+
]
|
|
121
|
+
for (const plugin of sonarPlugins) {
|
|
122
|
+
if (allDeps[plugin]) {
|
|
123
|
+
result.details.hasSonarPlugin = true
|
|
124
|
+
result.details.securityPlugins.push(plugin)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (eslintContent) {
|
|
129
|
+
if (eslintContent.includes('sonarjs') || eslintContent.includes('plugin:sonarjs')) {
|
|
130
|
+
result.details.hasSonarPlugin = true
|
|
131
|
+
if (!result.details.securityPlugins.some(p => p.includes('sonarjs'))) {
|
|
132
|
+
result.details.securityPlugins.push('eslint-plugin-sonarjs (in config)')
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (result.details.hasSonarPlugin) {
|
|
138
|
+
result.score += 1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check for custom security rules (no-restricted-imports/syntax)
|
|
142
|
+
if (eslintContent) {
|
|
143
|
+
const hasRestrictedImports = eslintContent.includes('no-restricted-imports') ||
|
|
144
|
+
eslintContent.includes('no-restricted-syntax')
|
|
145
|
+
const hasSecurityKeywords = eslintContent.includes('SupabaseService') ||
|
|
146
|
+
eslintContent.includes('service_role') ||
|
|
147
|
+
eslintContent.includes('SERVICE_ROLE') ||
|
|
148
|
+
eslintContent.includes('@anthropic-ai') ||
|
|
149
|
+
eslintContent.includes('eval') ||
|
|
150
|
+
eslintContent.includes('DANGEROUS') ||
|
|
151
|
+
eslintContent.includes('detect-unsafe-regex') ||
|
|
152
|
+
eslintContent.includes('detect-eval')
|
|
153
|
+
|
|
154
|
+
if (hasRestrictedImports && hasSecurityKeywords) {
|
|
155
|
+
result.details.hasCustomSecurityRules = true
|
|
156
|
+
result.score += 1
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Determine status
|
|
161
|
+
if (result.score === 0) {
|
|
162
|
+
result.status = 'error'
|
|
163
|
+
result.details.message = 'ESLint configured but no security plugins detected'
|
|
164
|
+
} else if (result.score < 2) {
|
|
165
|
+
result.status = 'warning'
|
|
166
|
+
result.details.message = `${result.details.securityPlugins.length} security plugin(s) — consider adding more`
|
|
167
|
+
} else {
|
|
168
|
+
result.details.message = `ESLint security well-configured (${result.details.securityPlugins.join(', ')})`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result
|
|
172
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Health Checks — All
|
|
2
|
+
* Health Checks — All 23 project health checks
|
|
3
3
|
*
|
|
4
4
|
* Main entry: scanProjectHealth(projectPath, projectName, options?)
|
|
5
5
|
* Individual checks available via named imports.
|
|
@@ -26,3 +26,9 @@ export { check as checkDopplerCompliance } from './doppler-compliance.js'
|
|
|
26
26
|
export { check as checkInfrastructureYml } from './infrastructure-yml.js'
|
|
27
27
|
export { check as checkFileOrganization } from './file-organization.js'
|
|
28
28
|
export { check as checkRpcParamMismatch } from './rpc-param-mismatch.js'
|
|
29
|
+
export { check as checkTypescriptStrict } from './typescript-strict.js'
|
|
30
|
+
export { check as checkPrettier } from './prettier.js'
|
|
31
|
+
export { check as checkCoverageThresholds } from './coverage-thresholds.js'
|
|
32
|
+
export { check as checkEslintSecurity } from './eslint-security.js'
|
|
33
|
+
export { check as checkDependencyCruiser } from './dependency-cruiser.js'
|
|
34
|
+
export { check as checkConventionalCommits } from './conventional-commits.js'
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: Prettier / Code Formatting
|
|
3
|
+
*
|
|
4
|
+
* Checks for consistent code formatting setup.
|
|
5
|
+
* Score: up to 3 points:
|
|
6
|
+
* +1 for Prettier config file exists
|
|
7
|
+
* +1 for format:check or format script in package.json
|
|
8
|
+
* +1 for lint-staged config (auto-format on commit)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from 'fs'
|
|
12
|
+
import { join } from 'path'
|
|
13
|
+
import { createCheck } from './types.js'
|
|
14
|
+
|
|
15
|
+
export async function check(projectPath) {
|
|
16
|
+
const result = createCheck('prettier', 3, {
|
|
17
|
+
configFound: false,
|
|
18
|
+
configPath: null,
|
|
19
|
+
hasFormatScript: false,
|
|
20
|
+
formatScript: null,
|
|
21
|
+
hasLintStaged: false,
|
|
22
|
+
lintStagedConfig: null,
|
|
23
|
+
message: ''
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Check for Prettier config files
|
|
27
|
+
const prettierConfigs = [
|
|
28
|
+
'.prettierrc',
|
|
29
|
+
'.prettierrc.json',
|
|
30
|
+
'.prettierrc.js',
|
|
31
|
+
'.prettierrc.cjs',
|
|
32
|
+
'.prettierrc.mjs',
|
|
33
|
+
'.prettierrc.yml',
|
|
34
|
+
'.prettierrc.yaml',
|
|
35
|
+
'.prettierrc.toml',
|
|
36
|
+
'prettier.config.js',
|
|
37
|
+
'prettier.config.cjs',
|
|
38
|
+
'prettier.config.mjs',
|
|
39
|
+
// Also check in sub-packages
|
|
40
|
+
'frontend/.prettierrc',
|
|
41
|
+
'frontend/.prettierrc.json',
|
|
42
|
+
'backend/.prettierrc',
|
|
43
|
+
'backend/.prettierrc.json'
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
for (const config of prettierConfigs) {
|
|
47
|
+
if (existsSync(join(projectPath, config))) {
|
|
48
|
+
result.details.configFound = true
|
|
49
|
+
result.details.configPath = config
|
|
50
|
+
result.score += 1
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Also check for prettier key in package.json
|
|
56
|
+
if (!result.details.configFound) {
|
|
57
|
+
const pkgPath = join(projectPath, 'package.json')
|
|
58
|
+
if (existsSync(pkgPath)) {
|
|
59
|
+
try {
|
|
60
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
61
|
+
if (pkg.prettier) {
|
|
62
|
+
result.details.configFound = true
|
|
63
|
+
result.details.configPath = 'package.json (prettier key)'
|
|
64
|
+
result.score += 1
|
|
65
|
+
}
|
|
66
|
+
} catch { /* ignore */ }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check for format scripts
|
|
71
|
+
const packageLocations = [
|
|
72
|
+
'package.json',
|
|
73
|
+
'backend/package.json',
|
|
74
|
+
'frontend/package.json',
|
|
75
|
+
'frontend-user/package.json'
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
for (const loc of packageLocations) {
|
|
79
|
+
const pkgPath = join(projectPath, loc)
|
|
80
|
+
if (!existsSync(pkgPath)) continue
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
84
|
+
const scripts = pkg.scripts || {}
|
|
85
|
+
|
|
86
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
87
|
+
if (typeof cmd !== 'string') continue
|
|
88
|
+
if (name === 'format' || name === 'format:check' || name === 'prettier' ||
|
|
89
|
+
name === 'prettier:check' || cmd.includes('prettier')) {
|
|
90
|
+
result.details.hasFormatScript = true
|
|
91
|
+
result.details.formatScript = `${loc} → ${name}`
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (result.details.hasFormatScript) break
|
|
96
|
+
} catch { /* ignore */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (result.details.hasFormatScript) {
|
|
100
|
+
result.score += 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check for lint-staged config
|
|
104
|
+
const lintStagedConfigs = [
|
|
105
|
+
'.lintstagedrc',
|
|
106
|
+
'.lintstagedrc.json',
|
|
107
|
+
'.lintstagedrc.js',
|
|
108
|
+
'.lintstagedrc.cjs',
|
|
109
|
+
'.lintstagedrc.mjs',
|
|
110
|
+
'.lintstagedrc.yml',
|
|
111
|
+
'lint-staged.config.js',
|
|
112
|
+
'lint-staged.config.cjs',
|
|
113
|
+
'lint-staged.config.mjs',
|
|
114
|
+
// Sub-packages
|
|
115
|
+
'frontend/.lintstagedrc',
|
|
116
|
+
'frontend/.lintstagedrc.json',
|
|
117
|
+
'backend/.lintstagedrc',
|
|
118
|
+
'backend/.lintstagedrc.json'
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
for (const config of lintStagedConfigs) {
|
|
122
|
+
if (existsSync(join(projectPath, config))) {
|
|
123
|
+
result.details.hasLintStaged = true
|
|
124
|
+
result.details.lintStagedConfig = config
|
|
125
|
+
result.score += 1
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Also check package.json for lint-staged key
|
|
131
|
+
if (!result.details.hasLintStaged) {
|
|
132
|
+
for (const loc of packageLocations) {
|
|
133
|
+
const pkgPath = join(projectPath, loc)
|
|
134
|
+
if (!existsSync(pkgPath)) continue
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
138
|
+
if (pkg['lint-staged']) {
|
|
139
|
+
result.details.hasLintStaged = true
|
|
140
|
+
result.details.lintStagedConfig = `${loc} (lint-staged key)`
|
|
141
|
+
result.score += 1
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
} catch { /* ignore */ }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Determine status
|
|
149
|
+
if (result.score === 0) {
|
|
150
|
+
result.status = 'error'
|
|
151
|
+
result.details.message = 'No code formatting setup detected'
|
|
152
|
+
} else if (result.score < 2) {
|
|
153
|
+
result.status = 'warning'
|
|
154
|
+
result.details.message = result.details.configFound
|
|
155
|
+
? 'Prettier config exists but no format script or lint-staged'
|
|
156
|
+
: 'Format script exists but no Prettier config'
|
|
157
|
+
} else {
|
|
158
|
+
result.details.message = `Code formatting well-configured`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result
|
|
162
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project Health Scanner
|
|
3
3
|
*
|
|
4
|
-
* Orchestrates all
|
|
4
|
+
* Orchestrates all 23 health checks and produces a HealthReport.
|
|
5
5
|
* This is the main entry point — consumers call scanProjectHealth().
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -22,6 +22,12 @@ import { check as checkDopplerCompliance } from './doppler-compliance.js'
|
|
|
22
22
|
import { check as checkInfrastructureYml } from './infrastructure-yml.js'
|
|
23
23
|
import { check as checkFileOrganization } from './file-organization.js'
|
|
24
24
|
import { check as checkRpcParamMismatch } from './rpc-param-mismatch.js'
|
|
25
|
+
import { check as checkTypescriptStrict } from './typescript-strict.js'
|
|
26
|
+
import { check as checkPrettier } from './prettier.js'
|
|
27
|
+
import { check as checkCoverageThresholds } from './coverage-thresholds.js'
|
|
28
|
+
import { check as checkEslintSecurity } from './eslint-security.js'
|
|
29
|
+
import { check as checkDependencyCruiser } from './dependency-cruiser.js'
|
|
30
|
+
import { check as checkConventionalCommits } from './conventional-commits.js'
|
|
25
31
|
import { calculateHealthStatus } from './types.js'
|
|
26
32
|
|
|
27
33
|
/**
|
|
@@ -52,7 +58,13 @@ export async function scanProjectHealth(projectPath, projectName, options = {})
|
|
|
52
58
|
checkDopplerCompliance(projectPath),
|
|
53
59
|
checkInfrastructureYml(projectPath),
|
|
54
60
|
checkFileOrganization(projectPath),
|
|
55
|
-
checkRpcParamMismatch(projectPath)
|
|
61
|
+
checkRpcParamMismatch(projectPath),
|
|
62
|
+
checkTypescriptStrict(projectPath),
|
|
63
|
+
checkPrettier(projectPath),
|
|
64
|
+
checkCoverageThresholds(projectPath),
|
|
65
|
+
checkEslintSecurity(projectPath),
|
|
66
|
+
checkDependencyCruiser(projectPath),
|
|
67
|
+
checkConventionalCommits(projectPath)
|
|
56
68
|
])
|
|
57
69
|
|
|
58
70
|
const totalScore = checks.reduce((sum, c) => sum + c.score, 0)
|