@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,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
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: SAST (Static Application Security Testing)
|
|
3
|
+
*
|
|
4
|
+
* Checks for SAST tooling like Semgrep, CodeQL, Snyk Code, or SonarQube.
|
|
5
|
+
* Enterprise security requirement — catches OWASP patterns statically.
|
|
6
|
+
* Score: up to 2 points:
|
|
7
|
+
* +1 for SAST tool configured (config file or CI workflow)
|
|
8
|
+
* +1 for SAST running in CI (GitHub Actions workflow)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
12
|
+
import { join } from 'path'
|
|
13
|
+
import { createCheck } from './types.js'
|
|
14
|
+
|
|
15
|
+
export async function check(projectPath) {
|
|
16
|
+
const result = createCheck('sast', 2, {
|
|
17
|
+
tool: null,
|
|
18
|
+
configFound: false,
|
|
19
|
+
configPath: null,
|
|
20
|
+
ciIntegration: false,
|
|
21
|
+
ciWorkflow: null,
|
|
22
|
+
message: ''
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Check for Semgrep config
|
|
26
|
+
const semgrepConfigs = [
|
|
27
|
+
'.semgrep.yml',
|
|
28
|
+
'.semgrep.yaml',
|
|
29
|
+
'.semgrep/',
|
|
30
|
+
'semgrep.yml',
|
|
31
|
+
'semgrep.yaml'
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
for (const config of semgrepConfigs) {
|
|
35
|
+
if (existsSync(join(projectPath, config))) {
|
|
36
|
+
result.details.tool = 'semgrep'
|
|
37
|
+
result.details.configFound = true
|
|
38
|
+
result.details.configPath = config
|
|
39
|
+
result.score += 1
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check for CodeQL config
|
|
45
|
+
if (!result.details.configFound) {
|
|
46
|
+
const codeqlConfigs = [
|
|
47
|
+
'.github/codeql/codeql-config.yml',
|
|
48
|
+
'.github/codeql-config.yml',
|
|
49
|
+
'codeql-config.yml'
|
|
50
|
+
]
|
|
51
|
+
for (const config of codeqlConfigs) {
|
|
52
|
+
if (existsSync(join(projectPath, config))) {
|
|
53
|
+
result.details.tool = 'codeql'
|
|
54
|
+
result.details.configFound = true
|
|
55
|
+
result.details.configPath = config
|
|
56
|
+
result.score += 1
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for SonarQube/SonarCloud config
|
|
63
|
+
if (!result.details.configFound) {
|
|
64
|
+
const sonarConfigs = [
|
|
65
|
+
'sonar-project.properties',
|
|
66
|
+
'.sonarcloud.properties'
|
|
67
|
+
]
|
|
68
|
+
for (const config of sonarConfigs) {
|
|
69
|
+
if (existsSync(join(projectPath, config))) {
|
|
70
|
+
result.details.tool = 'sonarqube'
|
|
71
|
+
result.details.configFound = true
|
|
72
|
+
result.details.configPath = config
|
|
73
|
+
result.score += 1
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for Snyk config
|
|
80
|
+
if (!result.details.configFound) {
|
|
81
|
+
if (existsSync(join(projectPath, '.snyk'))) {
|
|
82
|
+
result.details.tool = 'snyk'
|
|
83
|
+
result.details.configFound = true
|
|
84
|
+
result.details.configPath = '.snyk'
|
|
85
|
+
result.score += 1
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check CI workflows for SAST integration
|
|
90
|
+
const workflowDir = join(projectPath, '.github/workflows')
|
|
91
|
+
if (existsSync(workflowDir)) {
|
|
92
|
+
try {
|
|
93
|
+
const files = readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
try {
|
|
96
|
+
const content = readFileSync(join(workflowDir, file), 'utf-8')
|
|
97
|
+
const lower = content.toLowerCase()
|
|
98
|
+
|
|
99
|
+
if (lower.includes('semgrep') || lower.includes('codeql') ||
|
|
100
|
+
lower.includes('sonarcloud') || lower.includes('sonarqube') ||
|
|
101
|
+
lower.includes('snyk') || lower.includes('returntocorp/semgrep') ||
|
|
102
|
+
lower.includes('github/codeql-action') || lower.includes('sonarsource')) {
|
|
103
|
+
result.details.ciIntegration = true
|
|
104
|
+
result.details.ciWorkflow = file
|
|
105
|
+
result.score += 1
|
|
106
|
+
|
|
107
|
+
// Detect tool from CI if not already found
|
|
108
|
+
if (!result.details.tool) {
|
|
109
|
+
if (lower.includes('semgrep')) result.details.tool = 'semgrep'
|
|
110
|
+
else if (lower.includes('codeql')) result.details.tool = 'codeql'
|
|
111
|
+
else if (lower.includes('sonar')) result.details.tool = 'sonarqube'
|
|
112
|
+
else if (lower.includes('snyk')) result.details.tool = 'snyk'
|
|
113
|
+
result.details.configFound = true
|
|
114
|
+
}
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
} catch { /* ignore individual file errors */ }
|
|
118
|
+
}
|
|
119
|
+
} catch { /* ignore */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check package.json for SAST-related dependencies or scripts
|
|
123
|
+
if (!result.details.configFound) {
|
|
124
|
+
const pkgPath = join(projectPath, 'package.json')
|
|
125
|
+
if (existsSync(pkgPath)) {
|
|
126
|
+
try {
|
|
127
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
128
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
129
|
+
const scripts = pkg.scripts || {}
|
|
130
|
+
|
|
131
|
+
if (allDeps['snyk']) {
|
|
132
|
+
result.details.tool = 'snyk'
|
|
133
|
+
result.details.configFound = true
|
|
134
|
+
result.score += 1
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
138
|
+
if (typeof cmd !== 'string') continue
|
|
139
|
+
if (cmd.includes('semgrep') || cmd.includes('snyk code') ||
|
|
140
|
+
name.includes('sast') || name.includes('security:scan')) {
|
|
141
|
+
result.details.configFound = true
|
|
142
|
+
result.details.configPath = `package.json → ${name}`
|
|
143
|
+
if (!result.details.tool) result.details.tool = 'custom'
|
|
144
|
+
result.score += 1
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch { /* ignore */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
result.score = Math.min(result.score, result.maxScore)
|
|
153
|
+
|
|
154
|
+
// Determine status
|
|
155
|
+
if (result.score === 0) {
|
|
156
|
+
result.status = 'warning'
|
|
157
|
+
result.details.message = 'No SAST tooling — OWASP patterns not detected statically'
|
|
158
|
+
} else if (result.score < 2) {
|
|
159
|
+
result.status = 'warning'
|
|
160
|
+
result.details.message = `${result.details.tool} configured but not integrated in CI`
|
|
161
|
+
} else {
|
|
162
|
+
result.details.message = `SAST enabled via ${result.details.tool} (CI integrated)`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result
|
|
166
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project Health Scanner
|
|
3
3
|
*
|
|
4
|
-
* Orchestrates all
|
|
4
|
+
* Orchestrates all 28 health checks and produces a HealthReport.
|
|
5
5
|
* This is the main entry point — consumers call scanProjectHealth().
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -22,6 +22,17 @@ 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'
|
|
31
|
+
import { check as checkKnip } from './knip.js'
|
|
32
|
+
import { check as checkDependencyAutomation } from './dependency-automation.js'
|
|
33
|
+
import { check as checkLicenseAudit } from './license-audit.js'
|
|
34
|
+
import { check as checkSast } from './sast.js'
|
|
35
|
+
import { check as checkBundleSize } from './bundle-size.js'
|
|
25
36
|
import { calculateHealthStatus } from './types.js'
|
|
26
37
|
|
|
27
38
|
/**
|
|
@@ -52,7 +63,18 @@ export async function scanProjectHealth(projectPath, projectName, options = {})
|
|
|
52
63
|
checkDopplerCompliance(projectPath),
|
|
53
64
|
checkInfrastructureYml(projectPath),
|
|
54
65
|
checkFileOrganization(projectPath),
|
|
55
|
-
checkRpcParamMismatch(projectPath)
|
|
66
|
+
checkRpcParamMismatch(projectPath),
|
|
67
|
+
checkTypescriptStrict(projectPath),
|
|
68
|
+
checkPrettier(projectPath),
|
|
69
|
+
checkCoverageThresholds(projectPath),
|
|
70
|
+
checkEslintSecurity(projectPath),
|
|
71
|
+
checkDependencyCruiser(projectPath),
|
|
72
|
+
checkConventionalCommits(projectPath),
|
|
73
|
+
checkKnip(projectPath),
|
|
74
|
+
checkDependencyAutomation(projectPath),
|
|
75
|
+
checkLicenseAudit(projectPath),
|
|
76
|
+
checkSast(projectPath),
|
|
77
|
+
checkBundleSize(projectPath)
|
|
56
78
|
])
|
|
57
79
|
|
|
58
80
|
const totalScore = checks.reduce((sum, c) => sum + c.score, 0)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* @typedef {'plugins'|'mcps'|'git'|'tests'|'secrets'|'quality-toolkit'|'naming-conventions'|'rls-audit'|'rpc-param-mismatch'|'gitignore'|'repo-visibility'|'vincifox-widget'|'stella-integration'|'claude-md'|'doppler-compliance'|'infrastructure-yml'|'file-organization'} HealthCheckType
|
|
8
|
+
* @typedef {'plugins'|'mcps'|'git'|'tests'|'secrets'|'quality-toolkit'|'naming-conventions'|'rls-audit'|'rpc-param-mismatch'|'typescript-strict'|'prettier'|'coverage-thresholds'|'eslint-security'|'dependency-cruiser'|'conventional-commits'|'knip'|'dependency-automation'|'license-audit'|'sast'|'bundle-size'|'gitignore'|'repo-visibility'|'vincifox-widget'|'stella-integration'|'claude-md'|'doppler-compliance'|'infrastructure-yml'|'file-organization'} HealthCheckType
|
|
9
9
|
*
|
|
10
10
|
* @typedef {'ok'|'warning'|'error'} HealthStatus
|
|
11
11
|
*
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: TypeScript Strictness
|
|
3
|
+
*
|
|
4
|
+
* Checks TypeScript configuration for strict mode and type safety.
|
|
5
|
+
* Score: up to 3 points:
|
|
6
|
+
* +1 for strict: true in tsconfig.json
|
|
7
|
+
* +1 for tsc --noEmit script in package.json (typecheck command)
|
|
8
|
+
* +1 for noImplicitAny + strictNullChecks (if not using strict: true)
|
|
9
|
+
* OR bonus for strict: true + noUncheckedIndexedAccess
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync } from 'fs'
|
|
13
|
+
import { join } from 'path'
|
|
14
|
+
import { createCheck } from './types.js'
|
|
15
|
+
|
|
16
|
+
export async function check(projectPath) {
|
|
17
|
+
const result = createCheck('typescript-strict', 3, {
|
|
18
|
+
tsconfigFound: false,
|
|
19
|
+
strict: false,
|
|
20
|
+
noImplicitAny: false,
|
|
21
|
+
strictNullChecks: false,
|
|
22
|
+
noUncheckedIndexedAccess: false,
|
|
23
|
+
hasTypecheckScript: false,
|
|
24
|
+
typecheckScript: null,
|
|
25
|
+
tsconfigPaths: [],
|
|
26
|
+
message: ''
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Find tsconfig files in root + common sub-packages
|
|
30
|
+
const tsconfigLocations = [
|
|
31
|
+
'tsconfig.json',
|
|
32
|
+
'backend/tsconfig.json',
|
|
33
|
+
'frontend/tsconfig.json',
|
|
34
|
+
'frontend-user/tsconfig.json',
|
|
35
|
+
'src/tsconfig.json',
|
|
36
|
+
'mcp/tsconfig.json'
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
let bestConfig = null
|
|
40
|
+
|
|
41
|
+
for (const loc of tsconfigLocations) {
|
|
42
|
+
const tsconfigPath = join(projectPath, loc)
|
|
43
|
+
if (!existsSync(tsconfigPath)) continue
|
|
44
|
+
|
|
45
|
+
result.details.tsconfigPaths.push(loc)
|
|
46
|
+
result.details.tsconfigFound = true
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Strip JSON comments (// and /* */) before parsing
|
|
50
|
+
const raw = readFileSync(tsconfigPath, 'utf-8')
|
|
51
|
+
const stripped = raw
|
|
52
|
+
.replace(/\/\/.*$/gm, '')
|
|
53
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
54
|
+
.replace(/,\s*([}\]])/g, '$1')
|
|
55
|
+
const tsconfig = JSON.parse(stripped)
|
|
56
|
+
const opts = tsconfig.compilerOptions || {}
|
|
57
|
+
|
|
58
|
+
if (opts.strict && !bestConfig) {
|
|
59
|
+
bestConfig = { path: loc, strict: true, opts }
|
|
60
|
+
} else if (!bestConfig) {
|
|
61
|
+
bestConfig = { path: loc, strict: false, opts }
|
|
62
|
+
}
|
|
63
|
+
} catch { /* invalid tsconfig, skip */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!result.details.tsconfigFound) {
|
|
67
|
+
result.status = 'warning'
|
|
68
|
+
result.details.message = 'No tsconfig.json found'
|
|
69
|
+
return result
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (bestConfig) {
|
|
73
|
+
const opts = bestConfig.opts
|
|
74
|
+
|
|
75
|
+
if (opts.strict) {
|
|
76
|
+
result.score += 1
|
|
77
|
+
result.details.strict = true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (opts.noImplicitAny) result.details.noImplicitAny = true
|
|
81
|
+
if (opts.strictNullChecks) result.details.strictNullChecks = true
|
|
82
|
+
if (opts.noUncheckedIndexedAccess) result.details.noUncheckedIndexedAccess = true
|
|
83
|
+
|
|
84
|
+
// Bonus point: strict + extra strictness OR individual flags
|
|
85
|
+
if (opts.strict && opts.noUncheckedIndexedAccess) {
|
|
86
|
+
result.score += 1
|
|
87
|
+
} else if (!opts.strict && opts.noImplicitAny && opts.strictNullChecks) {
|
|
88
|
+
result.score += 1
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for typecheck script in package.json files
|
|
93
|
+
const packageLocations = [
|
|
94
|
+
'package.json',
|
|
95
|
+
'backend/package.json',
|
|
96
|
+
'frontend/package.json',
|
|
97
|
+
'frontend-user/package.json'
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
for (const loc of packageLocations) {
|
|
101
|
+
const pkgPath = join(projectPath, loc)
|
|
102
|
+
if (!existsSync(pkgPath)) continue
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
106
|
+
const scripts = pkg.scripts || {}
|
|
107
|
+
|
|
108
|
+
// Look for typecheck or tsc --noEmit scripts
|
|
109
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
110
|
+
if (typeof cmd !== 'string') continue
|
|
111
|
+
if (name === 'typecheck' || name === 'type-check' ||
|
|
112
|
+
cmd.includes('tsc --noEmit') || cmd.includes('tsc -noEmit') ||
|
|
113
|
+
(name === 'check' && cmd.includes('tsc'))) {
|
|
114
|
+
result.details.hasTypecheckScript = true
|
|
115
|
+
result.details.typecheckScript = `${loc} → ${name}: ${cmd}`
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (result.details.hasTypecheckScript) break
|
|
120
|
+
} catch { /* ignore */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (result.details.hasTypecheckScript) {
|
|
124
|
+
result.score += 1
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Determine status
|
|
128
|
+
if (result.score === 0) {
|
|
129
|
+
result.status = 'error'
|
|
130
|
+
result.details.message = 'TypeScript not strict and no typecheck script found'
|
|
131
|
+
} else if (result.score < 2) {
|
|
132
|
+
result.status = 'warning'
|
|
133
|
+
result.details.message = result.details.strict
|
|
134
|
+
? 'strict: true but no typecheck script'
|
|
135
|
+
: result.details.hasTypecheckScript
|
|
136
|
+
? 'Typecheck script exists but strict mode disabled'
|
|
137
|
+
: 'Partial TypeScript strictness'
|
|
138
|
+
} else {
|
|
139
|
+
result.details.message = `TypeScript well-configured (${result.details.tsconfigPaths.length} tsconfig(s))`
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulbatical/tetra-dev-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "restricted"
|
|
6
6
|
},
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"templates/"
|
|
36
36
|
],
|
|
37
37
|
"scripts": {
|
|
38
|
-
"test": "
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
39
40
|
"lint": "eslint src/ lib/ bin/",
|
|
40
41
|
"build": "echo 'No build step needed'"
|
|
41
42
|
},
|
|
@@ -52,7 +53,8 @@
|
|
|
52
53
|
"yaml": "^2.8.2"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"eslint": "^9.0.0"
|
|
56
|
+
"eslint": "^9.0.0",
|
|
57
|
+
"vitest": "^4.0.18"
|
|
56
58
|
},
|
|
57
59
|
"peerDependencies": {
|
|
58
60
|
"eslint": ">=8.0.0",
|