@soulbatical/tetra-dev-toolkit 1.22.1 → 1.22.2
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/README.md +3 -0
- package/lib/checks/health/deploy-readiness.js +90 -112
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,9 @@ LAYER 8: DB HELPERS adminDB/userDB/publicDB/systemDB enforce correct
|
|
|
74
74
|
| `tetra-setup` | Install hooks, CI, and config |
|
|
75
75
|
| `tetra-init` | Initialize project config files |
|
|
76
76
|
| `tetra-dev-token` | Generate development tokens |
|
|
77
|
+
| `tetra-license generate` | Generate a Tetra license key |
|
|
78
|
+
| `tetra-license verify` | Verify a license key (or `TETRA_LICENSE_KEY` env) |
|
|
79
|
+
| `tetra-license decode` | Decode key payload without validation |
|
|
77
80
|
|
|
78
81
|
Exit codes: `0` = passed, `1` = failed (CRITICAL/HIGH), `2` = error. No middle ground.
|
|
79
82
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Health Check: Deploy Readiness for
|
|
2
|
+
* Health Check: Deploy Readiness for @soulbatical packages
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* All @soulbatical packages are PUBLIC on npm since April 2026.
|
|
5
|
+
* No .npmrc, no NPM_TOKEN, no Dockerfile auth needed.
|
|
6
6
|
*
|
|
7
7
|
* Checks:
|
|
8
|
-
* 1.
|
|
9
|
-
* 2. No
|
|
10
|
-
* 3.
|
|
11
|
-
* 4.
|
|
8
|
+
* 1. No file: or link: references to @soulbatical packages (breaks CI/CD)
|
|
9
|
+
* 2. No leftover .npmrc with authToken (should be removed)
|
|
10
|
+
* 3. No leftover Dockerfile ARG NPM_TOKEN (not needed for public packages)
|
|
11
|
+
* 4. Version minimums: core >= 0.4.0, ui >= 0.10.0, dev-toolkit >= 1.22.0
|
|
12
12
|
*
|
|
13
13
|
* Score: 0-4 (1 per aspect)
|
|
14
14
|
* Skipped if project has no @soulbatical dependencies.
|
|
@@ -18,12 +18,11 @@ import { existsSync, readFileSync } from 'fs'
|
|
|
18
18
|
import { join } from 'path'
|
|
19
19
|
import { createCheck } from './types.js'
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
'@soulbatical/tetra-core',
|
|
23
|
-
'@soulbatical/tetra-ui',
|
|
24
|
-
'@soulbatical/tetra-dev-toolkit',
|
|
25
|
-
|
|
26
|
-
]
|
|
21
|
+
const MIN_VERSIONS = {
|
|
22
|
+
'@soulbatical/tetra-core': '0.4.0',
|
|
23
|
+
'@soulbatical/tetra-ui': '0.10.0',
|
|
24
|
+
'@soulbatical/tetra-dev-toolkit': '1.22.0',
|
|
25
|
+
}
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* Collect all @soulbatical dependency references from a project's package.json files
|
|
@@ -39,6 +38,7 @@ function findSoulbaticalDeps(projectPath) {
|
|
|
39
38
|
|
|
40
39
|
const deps = []
|
|
41
40
|
const fileRefs = []
|
|
41
|
+
const outdated = []
|
|
42
42
|
|
|
43
43
|
for (const { path: pkgPath, label } of pkgPaths) {
|
|
44
44
|
if (!existsSync(pkgPath)) continue
|
|
@@ -53,86 +53,81 @@ function findSoulbaticalDeps(projectPath) {
|
|
|
53
53
|
if (version.startsWith('file:') || version.startsWith('link:')) {
|
|
54
54
|
fileRefs.push({ name, version, location: label })
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
// Check version minimums
|
|
58
|
+
const minVersion = MIN_VERSIONS[name]
|
|
59
|
+
if (minVersion && version.startsWith('^')) {
|
|
60
|
+
const specified = version.slice(1) // remove ^
|
|
61
|
+
if (compareVersions(specified, minVersion) < 0) {
|
|
62
|
+
outdated.push({ name, version, minRequired: `^${minVersion}`, location: label })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
56
65
|
}
|
|
57
66
|
} catch { /* ignore */ }
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
return { deps, fileRefs }
|
|
69
|
+
return { deps, fileRefs, outdated }
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
/**
|
|
64
|
-
*
|
|
73
|
+
* Simple semver comparison (major.minor.patch)
|
|
74
|
+
* Returns: -1 if a < b, 0 if equal, 1 if a > b
|
|
65
75
|
*/
|
|
66
|
-
function
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
exists: true,
|
|
73
|
-
hasAuth: content.includes('_authToken=${NPM_TOKEN}') || content.includes('_authToken=$NPM_TOKEN'),
|
|
74
|
-
hasScope: content.includes('@soulbatical:registry='),
|
|
75
|
-
content
|
|
76
|
+
function compareVersions(a, b) {
|
|
77
|
+
const pa = a.split('.').map(Number)
|
|
78
|
+
const pb = b.split('.').map(Number)
|
|
79
|
+
for (let i = 0; i < 3; i++) {
|
|
80
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1
|
|
81
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1
|
|
76
82
|
}
|
|
83
|
+
return 0
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
/**
|
|
80
|
-
* Check
|
|
87
|
+
* Check for leftover .npmrc files with authToken (should be removed)
|
|
81
88
|
*/
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
join(projectPath, '
|
|
86
|
-
join(projectPath, '
|
|
87
|
-
join(projectPath, 'backend', '
|
|
89
|
+
function checkLeftoverNpmrc(projectPath) {
|
|
90
|
+
const locations = [
|
|
91
|
+
join(projectPath, '.npmrc'),
|
|
92
|
+
join(projectPath, 'backend', '.npmrc'),
|
|
93
|
+
join(projectPath, 'frontend', '.npmrc'),
|
|
94
|
+
join(projectPath, 'backend-mcp', '.npmrc'),
|
|
88
95
|
]
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
let orderCorrect = false
|
|
100
|
-
if (hasNpmrcCreation && hasNpmInstall) {
|
|
101
|
-
const npmrcPos = content.indexOf('.npmrc')
|
|
102
|
-
const installPos = content.search(/npm\s+(ci|install)/i)
|
|
103
|
-
orderCorrect = npmrcPos < installPos
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
exists: true,
|
|
108
|
-
path: dfPath,
|
|
109
|
-
hasArgNpmToken,
|
|
110
|
-
hasNpmrcCreation,
|
|
111
|
-
hasNpmInstall,
|
|
112
|
-
orderCorrect
|
|
113
|
-
}
|
|
97
|
+
const leftovers = []
|
|
98
|
+
for (const npmrcPath of locations) {
|
|
99
|
+
if (!existsSync(npmrcPath)) continue
|
|
100
|
+
try {
|
|
101
|
+
const content = readFileSync(npmrcPath, 'utf-8')
|
|
102
|
+
if (content.includes('authToken') || content.includes('_authToken')) {
|
|
103
|
+
leftovers.push(npmrcPath.replace(projectPath + '/', ''))
|
|
104
|
+
}
|
|
105
|
+
} catch { /* ignore */ }
|
|
114
106
|
}
|
|
115
|
-
|
|
116
|
-
return { exists: false }
|
|
107
|
+
return leftovers
|
|
117
108
|
}
|
|
118
109
|
|
|
119
110
|
/**
|
|
120
|
-
*
|
|
111
|
+
* Check for leftover Dockerfile ARG NPM_TOKEN
|
|
121
112
|
*/
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
function checkLeftoverDockerfileAuth(projectPath) {
|
|
114
|
+
const dockerfiles = [
|
|
115
|
+
join(projectPath, 'Dockerfile'),
|
|
116
|
+
join(projectPath, 'Dockerfile.prod'),
|
|
117
|
+
join(projectPath, 'backend', 'Dockerfile')
|
|
118
|
+
]
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
120
|
+
const leftovers = []
|
|
121
|
+
for (const dfPath of dockerfiles) {
|
|
122
|
+
if (!existsSync(dfPath)) continue
|
|
129
123
|
try {
|
|
130
|
-
const content = readFileSync(
|
|
131
|
-
if (
|
|
124
|
+
const content = readFileSync(dfPath, 'utf-8')
|
|
125
|
+
if (/ARG\s+NPM_TOKEN/i.test(content)) {
|
|
126
|
+
leftovers.push(dfPath.replace(projectPath + '/', ''))
|
|
127
|
+
}
|
|
132
128
|
} catch { /* ignore */ }
|
|
133
129
|
}
|
|
134
|
-
|
|
135
|
-
return false
|
|
130
|
+
return leftovers
|
|
136
131
|
}
|
|
137
132
|
|
|
138
133
|
export async function check(projectPath) {
|
|
@@ -140,66 +135,49 @@ export async function check(projectPath) {
|
|
|
140
135
|
hasSoulbaticalDeps: false,
|
|
141
136
|
soulbaticalDeps: [],
|
|
142
137
|
fileRefs: [],
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
outdated: [],
|
|
139
|
+
leftoverNpmrc: [],
|
|
140
|
+
leftoverDockerfileAuth: [],
|
|
146
141
|
skipped: false
|
|
147
142
|
})
|
|
148
143
|
|
|
149
144
|
// Find @soulbatical dependencies
|
|
150
|
-
const { deps, fileRefs } = findSoulbaticalDeps(projectPath)
|
|
145
|
+
const { deps, fileRefs, outdated } = findSoulbaticalDeps(projectPath)
|
|
151
146
|
result.details.soulbaticalDeps = deps
|
|
152
147
|
result.details.fileRefs = fileRefs
|
|
148
|
+
result.details.outdated = outdated
|
|
153
149
|
|
|
154
150
|
// Skip if no @soulbatical dependencies
|
|
155
151
|
if (deps.length === 0) {
|
|
156
152
|
result.details.skipped = true
|
|
157
153
|
result.details.message = 'No @soulbatical dependencies — check skipped'
|
|
158
|
-
result.score = result.maxScore
|
|
154
|
+
result.score = result.maxScore
|
|
159
155
|
return result
|
|
160
156
|
}
|
|
161
157
|
|
|
162
158
|
result.details.hasSoulbaticalDeps = true
|
|
163
|
-
const isRailway = isRailwayProject(projectPath)
|
|
164
|
-
result.details.isRailway = isRailway
|
|
165
|
-
|
|
166
|
-
// --- Check 1: .npmrc exists with auth + scope (+1 point) ---
|
|
167
|
-
const npmrc = checkNpmrc(projectPath)
|
|
168
|
-
result.details.npmrc = npmrc
|
|
169
159
|
|
|
170
|
-
|
|
160
|
+
// --- Check 1: No file: references (+1 point) ---
|
|
161
|
+
if (fileRefs.length === 0) {
|
|
171
162
|
result.score += 1
|
|
172
|
-
} else if (npmrc.exists && npmrc.hasAuth) {
|
|
173
|
-
result.score += 0.5 // Auth OK but missing scope
|
|
174
163
|
}
|
|
175
164
|
|
|
176
|
-
// --- Check 2: No
|
|
177
|
-
|
|
165
|
+
// --- Check 2: No leftover .npmrc with authToken (+1 point) ---
|
|
166
|
+
const leftoverNpmrc = checkLeftoverNpmrc(projectPath)
|
|
167
|
+
result.details.leftoverNpmrc = leftoverNpmrc
|
|
168
|
+
if (leftoverNpmrc.length === 0) {
|
|
178
169
|
result.score += 1
|
|
179
170
|
}
|
|
180
171
|
|
|
181
|
-
// --- Check 3:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (df.exists && df.hasArgNpmToken && df.hasNpmrcCreation && df.orderCorrect) {
|
|
187
|
-
result.score += 1
|
|
188
|
-
} else if (df.exists && df.hasNpmInstall) {
|
|
189
|
-
result.score += 0.5 // Dockerfile exists but missing npm auth
|
|
190
|
-
}
|
|
191
|
-
} else {
|
|
192
|
-
result.score += 1 // Not Railway — full score for this check
|
|
172
|
+
// --- Check 3: No leftover Dockerfile ARG NPM_TOKEN (+1 point) ---
|
|
173
|
+
const leftoverDockerfile = checkLeftoverDockerfileAuth(projectPath)
|
|
174
|
+
result.details.leftoverDockerfileAuth = leftoverDockerfile
|
|
175
|
+
if (leftoverDockerfile.length === 0) {
|
|
176
|
+
result.score += 1
|
|
193
177
|
}
|
|
194
178
|
|
|
195
|
-
// --- Check 4:
|
|
196
|
-
|
|
197
|
-
const nonDevFileRefs = fileRefs.filter(r => r.location !== 'root') // root devDeps are OK locally
|
|
198
|
-
const allSemver = nonDevFileRefs.length === 0
|
|
199
|
-
const npmrcReady = npmrc.exists && npmrc.hasAuth
|
|
200
|
-
const dockerReady = !isRailway || (result.details.dockerfile.exists && result.details.dockerfile.hasArgNpmToken)
|
|
201
|
-
|
|
202
|
-
if (allSemver && npmrcReady && dockerReady) {
|
|
179
|
+
// --- Check 4: Version minimums met (+1 point) ---
|
|
180
|
+
if (outdated.length === 0) {
|
|
203
181
|
result.score += 1
|
|
204
182
|
}
|
|
205
183
|
|
|
@@ -209,13 +187,13 @@ export async function check(projectPath) {
|
|
|
209
187
|
if (result.score < result.maxScore) {
|
|
210
188
|
result.status = 'warning'
|
|
211
189
|
const issues = []
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
result.details.message =
|
|
190
|
+
if (fileRefs.length > 0) issues.push(`${fileRefs.length} file: ref(s) — will break CI/CD`)
|
|
191
|
+
if (leftoverNpmrc.length > 0) issues.push(`leftover .npmrc with authToken: ${leftoverNpmrc.join(', ')} — remove it (packages are public)`)
|
|
192
|
+
if (leftoverDockerfile.length > 0) issues.push(`leftover ARG NPM_TOKEN in: ${leftoverDockerfile.join(', ')} — remove it (packages are public)`)
|
|
193
|
+
if (outdated.length > 0) issues.push(`outdated: ${outdated.map(d => `${d.name}@${d.version} (need ${d.minRequired})`).join(', ')}`)
|
|
194
|
+
result.details.message = issues.join('; ')
|
|
195
|
+
} else {
|
|
196
|
+
result.details.message = `${deps.length} @soulbatical package(s) — all public, no auth needed, versions OK`
|
|
219
197
|
}
|
|
220
198
|
|
|
221
199
|
return result
|