@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,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 16 health checks and produces a HealthReport.
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.3",
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": "node --test src/**/*.test.js",
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",