@soulbatical/tetra-dev-toolkit 1.3.3 → 1.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/bin/tetra-setup.js +555 -2
- package/lib/checks/health/bundle-size.js +156 -0
- package/lib/checks/health/conventional-commits.js +164 -0
- package/lib/checks/health/coverage-thresholds.js +199 -0
- package/lib/checks/health/dependency-automation.js +116 -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 +12 -1
- package/lib/checks/health/knip.js +135 -0
- package/lib/checks/health/license-audit.js +133 -0
- package/lib/checks/health/prettier.js +162 -0
- package/lib/checks/health/sast.js +166 -0
- package/lib/checks/health/scanner.js +24 -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,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 28 project health checks
|
|
3
3
|
*
|
|
4
4
|
* Main entry: scanProjectHealth(projectPath, projectName, options?)
|
|
5
5
|
* Individual checks available via named imports.
|
|
@@ -26,3 +26,14 @@ 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'
|
|
35
|
+
export { check as checkKnip } from './knip.js'
|
|
36
|
+
export { check as checkDependencyAutomation } from './dependency-automation.js'
|
|
37
|
+
export { check as checkLicenseAudit } from './license-audit.js'
|
|
38
|
+
export { check as checkSast } from './sast.js'
|
|
39
|
+
export { check as checkBundleSize } from './bundle-size.js'
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: Knip (Dead Code Detection)
|
|
3
|
+
*
|
|
4
|
+
* Checks for unused files, exports, and dependencies via Knip.
|
|
5
|
+
* Score: up to 3 points:
|
|
6
|
+
* +1 for knip installed (in dependencies)
|
|
7
|
+
* +1 for knip config file exists (knip.json, knip.config.ts, etc.)
|
|
8
|
+
* +1 for knip script in package.json (e.g. check:unused, knip)
|
|
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('knip', 3, {
|
|
17
|
+
installed: false,
|
|
18
|
+
configFound: false,
|
|
19
|
+
configPath: null,
|
|
20
|
+
hasScript: false,
|
|
21
|
+
scriptName: null,
|
|
22
|
+
hasAlternative: false,
|
|
23
|
+
alternativeTool: null,
|
|
24
|
+
message: ''
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Check for knip config files
|
|
28
|
+
const knipConfigs = [
|
|
29
|
+
'knip.json',
|
|
30
|
+
'knip.jsonc',
|
|
31
|
+
'knip.config.ts',
|
|
32
|
+
'knip.config.js',
|
|
33
|
+
'knip.config.mjs',
|
|
34
|
+
'knip.config.cjs',
|
|
35
|
+
'.knip.json',
|
|
36
|
+
'.knip.jsonc',
|
|
37
|
+
// Sub-packages
|
|
38
|
+
'backend/knip.json',
|
|
39
|
+
'backend/knip.config.ts',
|
|
40
|
+
'frontend/knip.json',
|
|
41
|
+
'frontend/knip.config.ts'
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
for (const config of knipConfigs) {
|
|
45
|
+
if (existsSync(join(projectPath, config))) {
|
|
46
|
+
result.details.configFound = true
|
|
47
|
+
result.details.configPath = config
|
|
48
|
+
result.score += 1
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check package.json for knip in dependencies and scripts
|
|
54
|
+
const packageLocations = [
|
|
55
|
+
'package.json',
|
|
56
|
+
'backend/package.json',
|
|
57
|
+
'frontend/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.knip) {
|
|
69
|
+
result.details.installed = true
|
|
70
|
+
result.score += 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Also check for knip key in package.json (inline config)
|
|
74
|
+
if (pkg.knip && !result.details.configFound) {
|
|
75
|
+
result.details.configFound = true
|
|
76
|
+
result.details.configPath = `${loc} (knip key)`
|
|
77
|
+
result.score += 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for knip script
|
|
81
|
+
const scripts = pkg.scripts || {}
|
|
82
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
83
|
+
if (typeof cmd !== 'string') continue
|
|
84
|
+
if (cmd.includes('knip') || name === 'knip') {
|
|
85
|
+
result.details.hasScript = true
|
|
86
|
+
result.details.scriptName = `${loc} → ${name}`
|
|
87
|
+
result.score += 1
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for alternative dead code tools
|
|
93
|
+
if (allDeps['ts-prune'] || allDeps['unimported']) {
|
|
94
|
+
result.details.hasAlternative = true
|
|
95
|
+
result.details.alternativeTool = allDeps['ts-prune'] ? 'ts-prune' : 'unimported'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check for custom unused code scripts (partial credit)
|
|
99
|
+
if (!result.details.hasScript) {
|
|
100
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
101
|
+
if (typeof cmd !== 'string') continue
|
|
102
|
+
if (name.includes('unused') || name.includes('dead-code') ||
|
|
103
|
+
cmd.includes('unused') || cmd.includes('check-unused')) {
|
|
104
|
+
result.details.hasScript = true
|
|
105
|
+
result.details.scriptName = `${loc} → ${name} (custom)`
|
|
106
|
+
result.score += 1
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch { /* ignore */ }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
result.score = Math.min(result.score, result.maxScore)
|
|
115
|
+
|
|
116
|
+
// Determine status
|
|
117
|
+
if (result.score === 0) {
|
|
118
|
+
if (result.details.hasAlternative) {
|
|
119
|
+
result.status = 'warning'
|
|
120
|
+
result.details.message = `Using ${result.details.alternativeTool} — consider migrating to Knip (more comprehensive)`
|
|
121
|
+
} else {
|
|
122
|
+
result.status = 'error'
|
|
123
|
+
result.details.message = 'No dead code detection configured — unused files and exports go undetected'
|
|
124
|
+
}
|
|
125
|
+
} else if (result.score < 2) {
|
|
126
|
+
result.status = 'warning'
|
|
127
|
+
result.details.message = result.details.installed
|
|
128
|
+
? 'Knip installed but no config or script'
|
|
129
|
+
: 'Dead code script exists but Knip not installed'
|
|
130
|
+
} else {
|
|
131
|
+
result.details.message = 'Dead code detection well-configured'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result
|
|
135
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: License Audit
|
|
3
|
+
*
|
|
4
|
+
* Checks for dependency license compliance tooling.
|
|
5
|
+
* Score: up to 2 points:
|
|
6
|
+
* +1 for license-checker or license-report tool configured
|
|
7
|
+
* +1 for forbidden license list or license 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('license-audit', 2, {
|
|
16
|
+
toolInstalled: false,
|
|
17
|
+
toolName: null,
|
|
18
|
+
hasConfig: false,
|
|
19
|
+
configPath: null,
|
|
20
|
+
hasScript: false,
|
|
21
|
+
scriptName: null,
|
|
22
|
+
hasForbiddenList: false,
|
|
23
|
+
message: ''
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// License tools to check for
|
|
27
|
+
const licenseTools = [
|
|
28
|
+
'license-checker',
|
|
29
|
+
'license-checker-rspack',
|
|
30
|
+
'license-report',
|
|
31
|
+
'license-webpack-plugin',
|
|
32
|
+
'license-compliance',
|
|
33
|
+
'legally',
|
|
34
|
+
'nlf'
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
// Check package.json files for license tools and scripts
|
|
38
|
+
const packageLocations = [
|
|
39
|
+
'package.json',
|
|
40
|
+
'backend/package.json',
|
|
41
|
+
'frontend/package.json'
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
for (const loc of packageLocations) {
|
|
45
|
+
const pkgPath = join(projectPath, loc)
|
|
46
|
+
if (!existsSync(pkgPath)) continue
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
50
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
51
|
+
|
|
52
|
+
// Check for license tools
|
|
53
|
+
for (const tool of licenseTools) {
|
|
54
|
+
if (allDeps[tool]) {
|
|
55
|
+
result.details.toolInstalled = true
|
|
56
|
+
result.details.toolName = tool
|
|
57
|
+
result.score += 1
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for license scripts
|
|
63
|
+
const scripts = pkg.scripts || {}
|
|
64
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
65
|
+
if (typeof cmd !== 'string') continue
|
|
66
|
+
if (name.includes('license') || cmd.includes('license-checker') ||
|
|
67
|
+
cmd.includes('license-report') || cmd.includes('license-compliance')) {
|
|
68
|
+
result.details.hasScript = true
|
|
69
|
+
result.details.scriptName = `${loc} → ${name}`
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for forbidden license patterns in scripts
|
|
75
|
+
for (const [, cmd] of Object.entries(scripts)) {
|
|
76
|
+
if (typeof cmd !== 'string') continue
|
|
77
|
+
if (cmd.includes('--failOn') || cmd.includes('--excludeLicenses') ||
|
|
78
|
+
cmd.includes('--production') || cmd.includes('forbidden')) {
|
|
79
|
+
result.details.hasForbiddenList = true
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch { /* ignore */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for license config files
|
|
87
|
+
const licenseConfigs = [
|
|
88
|
+
'.licensechecker.json',
|
|
89
|
+
'.license-checker.json',
|
|
90
|
+
'license-checker-config.json',
|
|
91
|
+
'.license-report-config.json'
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for (const config of licenseConfigs) {
|
|
95
|
+
const configPath = join(projectPath, config)
|
|
96
|
+
if (!existsSync(configPath)) continue
|
|
97
|
+
|
|
98
|
+
result.details.hasConfig = true
|
|
99
|
+
result.details.configPath = config
|
|
100
|
+
result.score += 1
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
104
|
+
if (content.includes('GPL') || content.includes('forbidden') ||
|
|
105
|
+
content.includes('excludeLicenses') || content.includes('failOn')) {
|
|
106
|
+
result.details.hasForbiddenList = true
|
|
107
|
+
}
|
|
108
|
+
} catch { /* ignore */ }
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Script or forbidden list counts toward second point
|
|
113
|
+
if (!result.details.hasConfig && (result.details.hasScript || result.details.hasForbiddenList)) {
|
|
114
|
+
result.score += 1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
result.score = Math.min(result.score, result.maxScore)
|
|
118
|
+
|
|
119
|
+
// Determine status
|
|
120
|
+
if (result.score === 0) {
|
|
121
|
+
result.status = 'warning'
|
|
122
|
+
result.details.message = 'No license compliance tooling — GPL dependencies could go unnoticed'
|
|
123
|
+
} else if (result.score < 2) {
|
|
124
|
+
result.status = 'warning'
|
|
125
|
+
result.details.message = result.details.toolInstalled
|
|
126
|
+
? `${result.details.toolName} installed but no forbidden license enforcement`
|
|
127
|
+
: 'License check script exists but no dedicated tool'
|
|
128
|
+
} else {
|
|
129
|
+
result.details.message = `License compliance enforced via ${result.details.toolName || 'config'}`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
}
|