@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.
@@ -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 16 project health checks
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
+ }