@soulbatical/tetra-dev-toolkit 1.22.0 → 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 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 Private Packages
2
+ * Health Check: Deploy Readiness for @soulbatical packages
3
3
  *
4
- * Verifies that a project is correctly configured to deploy with
5
- * @soulbatical private npm packages on Railway and Netlify.
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. .npmrc exists with @soulbatical scope + ${NPM_TOKEN} auth
9
- * 2. No file: references to @soulbatical packages (breaks CI/CD)
10
- * 3. Railway projects have a Dockerfile with npm auth step
11
- * 4. Dockerfile ARG NPM_TOKEN + .npmrc creation happens BEFORE npm install
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 SOULBATICAL_PACKAGES = [
22
- '@soulbatical/tetra-core',
23
- '@soulbatical/tetra-ui',
24
- '@soulbatical/tetra-dev-toolkit',
25
- '@soulbatical/stella'
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
- * Check if .npmrc has proper config for private packages
73
+ * Simple semver comparison (major.minor.patch)
74
+ * Returns: -1 if a < b, 0 if equal, 1 if a > b
65
75
  */
66
- function checkNpmrc(projectPath) {
67
- const npmrcPath = join(projectPath, '.npmrc')
68
- if (!existsSync(npmrcPath)) return { exists: false, hasAuth: false, hasScope: false, content: '' }
69
-
70
- const content = readFileSync(npmrcPath, 'utf-8')
71
- return {
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 if Dockerfile has proper npm auth for private packages
87
+ * Check for leftover .npmrc files with authToken (should be removed)
81
88
  */
82
- function checkDockerfile(projectPath) {
83
- // Check common Dockerfile locations
84
- const dockerfiles = [
85
- join(projectPath, 'Dockerfile'),
86
- join(projectPath, 'Dockerfile.prod'),
87
- join(projectPath, 'backend', 'Dockerfile')
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
- for (const dfPath of dockerfiles) {
91
- if (!existsSync(dfPath)) continue
92
- const content = readFileSync(dfPath, 'utf-8')
93
-
94
- const hasArgNpmToken = /ARG\s+NPM_TOKEN/i.test(content)
95
- const hasNpmrcCreation = content.includes('.npmrc') && content.includes('authToken')
96
- const hasNpmInstall = /npm\s+(ci|install)/i.test(content)
97
-
98
- // Check ordering: .npmrc creation should be BEFORE npm install
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
- * Detect if project deploys to Railway (has railway config or infrastructure hints)
111
+ * Check for leftover Dockerfile ARG NPM_TOKEN
121
112
  */
122
- function isRailwayProject(projectPath) {
123
- if (existsSync(join(projectPath, 'railway.json'))) return true
124
- if (existsSync(join(projectPath, 'railway.toml'))) return true
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
- // Check INFRASTRUCTURE.yml for Railway hosting
127
- const infraPath = join(projectPath, '.ralph', 'INFRASTRUCTURE.yml')
128
- if (existsSync(infraPath)) {
120
+ const leftovers = []
121
+ for (const dfPath of dockerfiles) {
122
+ if (!existsSync(dfPath)) continue
129
123
  try {
130
- const content = readFileSync(infraPath, 'utf-8')
131
- if (content.toLowerCase().includes('railway')) return true
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
- npmrc: {},
144
- dockerfile: {},
145
- isRailway: false,
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 // Full score if not applicable
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
- if (npmrc.exists && npmrc.hasAuth && npmrc.hasScope) {
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 file: references (+1 point) ---
177
- if (fileRefs.length === 0) {
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: Railway projects need Dockerfile (+1 point) ---
182
- if (isRailway) {
183
- const df = checkDockerfile(projectPath)
184
- result.details.dockerfile = df
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: Overall deploy confidence (+1 point) ---
196
- // All non-dev @soulbatical deps use semver (not file:, not link:)
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 (!npmrc.exists) issues.push('missing .npmrc')
213
- else if (!npmrc.hasAuth) issues.push('.npmrc missing ${NPM_TOKEN} auth')
214
- else if (!npmrc.hasScope) issues.push('.npmrc missing @soulbatical:registry scope')
215
- if (fileRefs.length > 0) issues.push(`${fileRefs.length} file: ref(s) to @soulbatical packages — will break CI/CD`)
216
- if (isRailway && !result.details.dockerfile.exists) issues.push('Railway project without Dockerfile �� private packages will fail')
217
- else if (isRailway && !result.details.dockerfile.hasArgNpmToken) issues.push('Dockerfile missing ARG NPM_TOKEN')
218
- result.details.message = issues.join(', ')
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
@@ -28,7 +28,7 @@ const ALLOWED_ROOT_MD = new Set([
28
28
 
29
29
  const IGNORED_DIRS = new Set([
30
30
  'node_modules', 'dist', 'build', '.git', '.next', '.cache', '.turbo',
31
- 'coverage', '.nyc_output', '.playwright', 'test-results'
31
+ 'coverage', '.nyc_output', '.playwright', 'test-results', '.netlify'
32
32
  ])
33
33
 
34
34
  const ALLOWED_SCRIPT_DIRS = new Set([
@@ -37,7 +37,7 @@ const ALLOWED_SCRIPT_DIRS = new Set([
37
37
 
38
38
  /** Directories whose .md content is always allowed (tooling config) */
39
39
  const ALLOWED_MD_DIRS = new Set([
40
- 'docs', '.ralph', '.claude', '.agents', 'e2e', 'tests'
40
+ 'docs', '.ralph', '.claude', '.agents', 'e2e', 'tests', 'shell'
41
41
  ])
42
42
 
43
43
  /** Directories where .yml/.yaml config files are allowed */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.22.0",
3
+ "version": "1.22.2",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },