@soulbatical/tetra-dev-toolkit 1.20.2 → 2.0.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/README.md +235 -238
- package/bin/tetra-setup.js +2 -172
- package/lib/checks/health/index.js +0 -1
- package/lib/checks/health/scanner.js +1 -3
- package/lib/checks/health/types.js +1 -1
- package/lib/checks/hygiene/stella-compliance.js +2 -2
- package/lib/checks/security/deprecated-supabase-admin.js +6 -15
- package/lib/checks/security/direct-supabase-client.js +4 -22
- package/lib/checks/security/frontend-supabase-queries.js +1 -1
- package/lib/checks/security/hardcoded-secrets.js +2 -5
- package/lib/checks/security/systemdb-whitelist.js +27 -116
- package/lib/config.js +1 -7
- package/lib/runner.js +7 -120
- package/package.json +2 -7
- package/bin/tetra-check-peers.js +0 -359
- package/bin/tetra-db-push.js +0 -91
- package/bin/tetra-migration-lint.js +0 -317
- package/bin/tetra-security-gate.js +0 -293
- package/bin/tetra-smoke.js +0 -532
- package/lib/checks/health/smoke-readiness.js +0 -150
- package/lib/checks/security/config-rls-alignment.js +0 -637
- package/lib/checks/security/mixed-db-usage.js +0 -204
- package/lib/checks/security/rls-live-audit.js +0 -255
- package/lib/checks/security/route-config-alignment.js +0 -342
- package/lib/checks/security/rpc-security-mode.js +0 -194
- package/lib/checks/security/tetra-core-compliance.js +0 -197
package/lib/runner.js
CHANGED
|
@@ -12,10 +12,6 @@ import * as serviceKeyExposure from './checks/security/service-key-exposure.js'
|
|
|
12
12
|
import * as deprecatedSupabaseAdmin from './checks/security/deprecated-supabase-admin.js'
|
|
13
13
|
import * as directSupabaseClient from './checks/security/direct-supabase-client.js'
|
|
14
14
|
import * as frontendSupabaseQueries from './checks/security/frontend-supabase-queries.js'
|
|
15
|
-
import * as tetraCoreCompliance from './checks/security/tetra-core-compliance.js'
|
|
16
|
-
import * as mixedDbUsage from './checks/security/mixed-db-usage.js'
|
|
17
|
-
import * as configRlsAlignment from './checks/security/config-rls-alignment.js'
|
|
18
|
-
import * as rpcSecurityMode from './checks/security/rpc-security-mode.js'
|
|
19
15
|
import * as systemdbWhitelist from './checks/security/systemdb-whitelist.js'
|
|
20
16
|
import * as huskyHooks from './checks/stability/husky-hooks.js'
|
|
21
17
|
import * as ciPipeline from './checks/stability/ci-pipeline.js'
|
|
@@ -25,65 +21,12 @@ import * as fileSize from './checks/codeQuality/file-size.js'
|
|
|
25
21
|
import * as namingConventions from './checks/codeQuality/naming-conventions.js'
|
|
26
22
|
import * as routeSeparation from './checks/codeQuality/route-separation.js'
|
|
27
23
|
import * as gitignoreValidation from './checks/security/gitignore-validation.js'
|
|
28
|
-
import * as routeConfigAlignment from './checks/security/route-config-alignment.js'
|
|
29
|
-
import * as rlsLiveAudit from './checks/security/rls-live-audit.js'
|
|
30
24
|
import * as rlsPolicyAudit from './checks/supabase/rls-policy-audit.js'
|
|
31
25
|
import * as rpcParamMismatch from './checks/supabase/rpc-param-mismatch.js'
|
|
32
26
|
import * as rpcGeneratorOrigin from './checks/supabase/rpc-generator-origin.js'
|
|
33
27
|
import * as fileOrganization from './checks/hygiene/file-organization.js'
|
|
34
28
|
import * as stellaCompliance from './checks/hygiene/stella-compliance.js'
|
|
35
29
|
|
|
36
|
-
// Health checks (score-based) — wrapped as runner checks via adapter
|
|
37
|
-
import { check as checkTests } from './checks/health/tests.js'
|
|
38
|
-
import { check as checkEslintSecurity } from './checks/health/eslint-security.js'
|
|
39
|
-
import { check as checkTypescriptStrict } from './checks/health/typescript-strict.js'
|
|
40
|
-
import { check as checkCoverageThresholds } from './checks/health/coverage-thresholds.js'
|
|
41
|
-
import { check as checkKnip } from './checks/health/knip.js'
|
|
42
|
-
import { check as checkDependencyCruiser } from './checks/health/dependency-cruiser.js'
|
|
43
|
-
import { check as checkDependencyAutomation } from './checks/health/dependency-automation.js'
|
|
44
|
-
import { check as checkPrettier } from './checks/health/prettier.js'
|
|
45
|
-
import { check as checkConventionalCommits } from './checks/health/conventional-commits.js'
|
|
46
|
-
import { check as checkBundleSize } from './checks/health/bundle-size.js'
|
|
47
|
-
import { check as checkSast } from './checks/health/sast.js'
|
|
48
|
-
import { check as checkLicenseAudit } from './checks/health/license-audit.js'
|
|
49
|
-
import { check as checkSecurityLayers } from './checks/health/security-layers.js'
|
|
50
|
-
import { check as checkSmokeReadiness } from './checks/health/smoke-readiness.js'
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Adapt a health check (score-based) to the runner format (meta + run).
|
|
54
|
-
* A health check passes if it scores > 0 (has at least some infrastructure).
|
|
55
|
-
*/
|
|
56
|
-
function adaptHealthCheck(id, name, severity, healthCheckFn) {
|
|
57
|
-
return {
|
|
58
|
-
meta: { id, name, severity },
|
|
59
|
-
async run(config, projectRoot) {
|
|
60
|
-
const result = await healthCheckFn(projectRoot)
|
|
61
|
-
const passed = result.score > 0
|
|
62
|
-
const findings = []
|
|
63
|
-
|
|
64
|
-
if (!passed && result.details?.message) {
|
|
65
|
-
findings.push({
|
|
66
|
-
file: 'project',
|
|
67
|
-
line: 0,
|
|
68
|
-
severity,
|
|
69
|
-
message: result.details.message
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
passed,
|
|
75
|
-
findings,
|
|
76
|
-
summary: {
|
|
77
|
-
total: 1,
|
|
78
|
-
[severity]: passed ? 0 : 1,
|
|
79
|
-
score: result.score,
|
|
80
|
-
maxScore: result.maxScore
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
30
|
// Register all checks
|
|
88
31
|
const ALL_CHECKS = {
|
|
89
32
|
security: [
|
|
@@ -92,33 +35,13 @@ const ALL_CHECKS = {
|
|
|
92
35
|
deprecatedSupabaseAdmin,
|
|
93
36
|
directSupabaseClient,
|
|
94
37
|
frontendSupabaseQueries,
|
|
95
|
-
tetraCoreCompliance,
|
|
96
|
-
mixedDbUsage,
|
|
97
|
-
rpcSecurityMode,
|
|
98
38
|
systemdbWhitelist,
|
|
99
|
-
gitignoreValidation
|
|
100
|
-
routeConfigAlignment,
|
|
101
|
-
rlsLiveAudit, // Must run BEFORE config-rls-alignment (provides live DB data)
|
|
102
|
-
configRlsAlignment, // Uses live DB data from rls-live-audit when available
|
|
39
|
+
gitignoreValidation
|
|
103
40
|
],
|
|
104
41
|
stability: [
|
|
105
42
|
huskyHooks,
|
|
106
43
|
ciPipeline,
|
|
107
|
-
npmAudit
|
|
108
|
-
adaptHealthCheck('tests', 'Test Infrastructure', 'high', checkTests),
|
|
109
|
-
adaptHealthCheck('eslint-security', 'ESLint Security Plugins', 'high', checkEslintSecurity),
|
|
110
|
-
adaptHealthCheck('typescript-strict', 'TypeScript Strictness', 'medium', checkTypescriptStrict),
|
|
111
|
-
adaptHealthCheck('coverage-thresholds', 'Test Coverage Thresholds', 'medium', checkCoverageThresholds),
|
|
112
|
-
adaptHealthCheck('knip', 'Dead Code Detection (Knip)', 'medium', checkKnip),
|
|
113
|
-
adaptHealthCheck('dependency-cruiser', 'Dependency Architecture', 'medium', checkDependencyCruiser),
|
|
114
|
-
adaptHealthCheck('dependency-automation', 'Dependency Updates (Dependabot/Renovate)', 'medium', checkDependencyAutomation),
|
|
115
|
-
adaptHealthCheck('prettier', 'Code Formatting (Prettier)', 'low', checkPrettier),
|
|
116
|
-
adaptHealthCheck('conventional-commits', 'Conventional Commits', 'low', checkConventionalCommits),
|
|
117
|
-
adaptHealthCheck('bundle-size', 'Bundle Size Monitoring', 'low', checkBundleSize),
|
|
118
|
-
adaptHealthCheck('sast', 'Static Application Security Testing', 'medium', checkSast),
|
|
119
|
-
adaptHealthCheck('license-audit', 'License Compliance', 'low', checkLicenseAudit),
|
|
120
|
-
adaptHealthCheck('security-layers', 'Security Layer Coverage', 'high', checkSecurityLayers),
|
|
121
|
-
adaptHealthCheck('smoke-readiness', 'Smoke Test Readiness', 'medium', checkSmokeReadiness)
|
|
44
|
+
npmAudit
|
|
122
45
|
],
|
|
123
46
|
codeQuality: [
|
|
124
47
|
apiResponseFormat,
|
|
@@ -164,10 +87,6 @@ export async function runAllChecks(options = {}) {
|
|
|
164
87
|
}
|
|
165
88
|
}
|
|
166
89
|
|
|
167
|
-
// Track rls-live-audit result across suites so migration-based RLS checks can be skipped.
|
|
168
|
-
// rls-live-audit runs in 'security' suite, rls-policy-audit runs in 'supabase' suite.
|
|
169
|
-
let rlsLiveData = null
|
|
170
|
-
|
|
171
90
|
for (const suite of suites) {
|
|
172
91
|
if (!config.suites[suite]) {
|
|
173
92
|
continue
|
|
@@ -180,43 +99,11 @@ export async function runAllChecks(options = {}) {
|
|
|
180
99
|
}
|
|
181
100
|
|
|
182
101
|
for (const check of checks) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
id: check.meta.id,
|
|
189
|
-
name: `${check.meta.name} (skipped — live DB is source of truth)`,
|
|
190
|
-
severity: check.meta.severity,
|
|
191
|
-
passed: true,
|
|
192
|
-
skipped: true,
|
|
193
|
-
skipReason: 'Skipped: rls-live-audit succeeded — live DB is the source of truth for current RLS state.',
|
|
194
|
-
findings: [],
|
|
195
|
-
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// config-rls-alignment: always runs, but uses live DB data when available
|
|
199
|
-
else if (rlsLiveData && check.meta.id === 'config-rls-alignment') {
|
|
200
|
-
checkResult = {
|
|
201
|
-
id: check.meta.id,
|
|
202
|
-
name: `${check.meta.name} (live DB)`,
|
|
203
|
-
severity: check.meta.severity,
|
|
204
|
-
...await check.run(config, projectRoot, { liveState: rlsLiveData.liveState })
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
checkResult = {
|
|
209
|
-
id: check.meta.id,
|
|
210
|
-
name: check.meta.name,
|
|
211
|
-
severity: check.meta.severity,
|
|
212
|
-
...await check.run(config, projectRoot)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Capture live data from rls-live-audit for downstream checks
|
|
217
|
-
if (check.meta.id === 'rls-live-audit' && !checkResult.skipped && checkResult._liveData) {
|
|
218
|
-
rlsLiveData = checkResult._liveData
|
|
219
|
-
delete checkResult._liveData // Don't leak internal data into output
|
|
102
|
+
const checkResult = {
|
|
103
|
+
id: check.meta.id,
|
|
104
|
+
name: check.meta.name,
|
|
105
|
+
severity: check.meta.severity,
|
|
106
|
+
...await check.run(config, projectRoot)
|
|
220
107
|
}
|
|
221
108
|
|
|
222
109
|
results.suites[suite].checks.push(checkResult)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulbatical/tetra-dev-toolkit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "restricted"
|
|
6
6
|
},
|
|
@@ -29,12 +29,7 @@
|
|
|
29
29
|
"tetra-init": "./bin/tetra-init.js",
|
|
30
30
|
"tetra-setup": "./bin/tetra-setup.js",
|
|
31
31
|
"tetra-dev-token": "./bin/tetra-dev-token.js",
|
|
32
|
-
"tetra-check-rls": "./bin/tetra-check-rls.js"
|
|
33
|
-
"tetra-migration-lint": "./bin/tetra-migration-lint.js",
|
|
34
|
-
"tetra-db-push": "./bin/tetra-db-push.js",
|
|
35
|
-
"tetra-check-peers": "./bin/tetra-check-peers.js",
|
|
36
|
-
"tetra-security-gate": "./bin/tetra-security-gate.js",
|
|
37
|
-
"tetra-smoke": "./bin/tetra-smoke.js"
|
|
32
|
+
"tetra-check-rls": "./bin/tetra-check-rls.js"
|
|
38
33
|
},
|
|
39
34
|
"files": [
|
|
40
35
|
"bin/",
|
package/bin/tetra-check-peers.js
DELETED
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tetra Check Peers — Validate peer dependency compatibility across consumer projects
|
|
5
|
-
*
|
|
6
|
-
* Scans all known consumer projects and checks if their installed versions
|
|
7
|
-
* are compatible with tetra packages' peerDependencies.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* tetra-check-peers # Check all consumers
|
|
11
|
-
* tetra-check-peers --fix # Show npm commands to fix mismatches
|
|
12
|
-
* tetra-check-peers --strict # Fail on any mismatch (for CI/prepublish)
|
|
13
|
-
* tetra-check-peers --json # JSON output
|
|
14
|
-
*
|
|
15
|
-
* Add to prepublishOnly to catch breaking peer dep changes before publish.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { readFileSync, existsSync } from 'fs'
|
|
19
|
-
import { join, basename, dirname } from 'path'
|
|
20
|
-
import { execSync } from 'child_process'
|
|
21
|
-
|
|
22
|
-
// ─── Config ──────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
// Resolve paths dynamically:
|
|
25
|
-
// 1. TETRA_PROJECTS_ROOT env var (explicit override)
|
|
26
|
-
// 2. Detect from tetra repo: this script lives in tetra/packages/dev-toolkit/bin/
|
|
27
|
-
// so tetra root = 4 levels up, and projects root = 5 levels up (sibling dirs)
|
|
28
|
-
// 3. Fallback: ~/projecten
|
|
29
|
-
function resolveProjectsRoot() {
|
|
30
|
-
if (process.env.TETRA_PROJECTS_ROOT) return process.env.TETRA_PROJECTS_ROOT
|
|
31
|
-
|
|
32
|
-
// This file: <projects>/<tetra>/packages/dev-toolkit/bin/tetra-check-peers.js
|
|
33
|
-
const scriptDir = dirname(new URL(import.meta.url).pathname)
|
|
34
|
-
const tetraRoot = join(scriptDir, '..', '..', '..') // → tetra/
|
|
35
|
-
const possibleProjectsRoot = join(tetraRoot, '..') // → projects/
|
|
36
|
-
|
|
37
|
-
// Verify: does this directory contain a 'tetra' subdirectory?
|
|
38
|
-
if (existsSync(join(possibleProjectsRoot, 'tetra', 'packages'))) {
|
|
39
|
-
return possibleProjectsRoot
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return join(process.env.HOME || '~', 'projecten')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const PROJECTS_ROOT = resolveProjectsRoot()
|
|
46
|
-
const TETRA_ROOT = join(PROJECTS_ROOT, 'tetra', 'packages')
|
|
47
|
-
|
|
48
|
-
// Tetra packages that have peerDependencies
|
|
49
|
-
const TETRA_PACKAGES = ['core', 'ui', 'dev-toolkit', 'schemas']
|
|
50
|
-
|
|
51
|
-
// ─── Helpers ─────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
function readJson(path) {
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(readFileSync(path, 'utf-8'))
|
|
56
|
-
} catch {
|
|
57
|
-
return null
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function satisfiesRange(installed, range) {
|
|
62
|
-
// Check if a consumer's dependency range can satisfy a peer dep range.
|
|
63
|
-
// Both `installed` and `range` can be semver ranges (^2.48.0, ^2.93.3, etc.)
|
|
64
|
-
// We check if the ranges CAN overlap — i.e., there exists a version that satisfies both.
|
|
65
|
-
if (!installed || !range) return false
|
|
66
|
-
if (range === '*' || installed === '*') return true
|
|
67
|
-
|
|
68
|
-
const cleanVersion = (v) => v.replace(/^[\^~>=<\s]+/, '').trim()
|
|
69
|
-
const parseVersion = (v) => {
|
|
70
|
-
const cleaned = cleanVersion(v)
|
|
71
|
-
const parts = cleaned.split('.').map(Number)
|
|
72
|
-
return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 }
|
|
73
|
-
}
|
|
74
|
-
const versionGte = (a, b) => {
|
|
75
|
-
if (a.major !== b.major) return a.major > b.major
|
|
76
|
-
if (a.minor !== b.minor) return a.minor > b.minor
|
|
77
|
-
return a.patch >= b.patch
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// For || ranges, check each part independently
|
|
81
|
-
const rangeParts = range.split('||').map(p => p.trim())
|
|
82
|
-
const installedParts = installed.split('||').map(p => p.trim())
|
|
83
|
-
|
|
84
|
-
return rangeParts.some(rPart => {
|
|
85
|
-
return installedParts.some(iPart => {
|
|
86
|
-
return rangesOverlap(iPart, rPart, parseVersion, versionGte)
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function rangesOverlap(installedRange, requiredRange, parseVersion, versionGte) {
|
|
92
|
-
const isCaret = (r) => r.startsWith('^')
|
|
93
|
-
const isTilde = (r) => r.startsWith('~')
|
|
94
|
-
const isGte = (r) => r.startsWith('>=')
|
|
95
|
-
|
|
96
|
-
const inst = parseVersion(installedRange)
|
|
97
|
-
const req = parseVersion(requiredRange)
|
|
98
|
-
|
|
99
|
-
// Both caret ranges with same major: they overlap if their ranges intersect
|
|
100
|
-
// ^2.48.0 allows 2.48.0 - 2.x.x, ^2.93.3 allows 2.93.3 - 2.x.x
|
|
101
|
-
// They overlap because ^2.48.0 includes 2.93.3
|
|
102
|
-
if ((isCaret(installedRange) || !installedRange.match(/^[\^~>=]/)) &&
|
|
103
|
-
(isCaret(requiredRange) || !requiredRange.match(/^[\^~>=]/))) {
|
|
104
|
-
// Same major = ranges can overlap
|
|
105
|
-
if (inst.major === req.major) {
|
|
106
|
-
// The higher minimum must be reachable from the lower range
|
|
107
|
-
// ^2.48.0 (allows up to <3.0.0) can reach 2.93.3 ✓
|
|
108
|
-
// ^3.0.0 cannot reach 2.93.3 ✗
|
|
109
|
-
return true // Same major with caret = always overlapping
|
|
110
|
-
}
|
|
111
|
-
// Different major with exact versions: only if equal
|
|
112
|
-
if (!isCaret(installedRange) && !isCaret(requiredRange)) {
|
|
113
|
-
return inst.major === req.major && inst.minor === req.minor && inst.patch === req.patch
|
|
114
|
-
}
|
|
115
|
-
return false
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// >= range checks
|
|
119
|
-
if (isGte(requiredRange)) {
|
|
120
|
-
// Required >= X.Y.Z: consumer's range must be able to produce a version >= X.Y.Z
|
|
121
|
-
// ^9.0.0 can produce 9.0.0+ which is >= 8.0.0 ✓
|
|
122
|
-
// ^5.3.3 can produce 5.3.3+ which is >= 5.0.0 ✓
|
|
123
|
-
// The max version of consumer's caret range is <(major+1).0.0
|
|
124
|
-
// So: consumer max >= required min
|
|
125
|
-
const consumerMax = isCaret(installedRange) ? { major: inst.major + 1, minor: 0, patch: 0 } : inst
|
|
126
|
-
return versionGte(consumerMax, req)
|
|
127
|
-
}
|
|
128
|
-
if (isGte(installedRange)) {
|
|
129
|
-
// Consumer has >= X, required has ^Y.Z.W — any version >= X could satisfy ^Y if X <= Y
|
|
130
|
-
return true // >= is open-ended, always overlaps with bounded ranges
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Tilde: ~X.Y.Z allows X.Y.Z - X.Y+1.0
|
|
134
|
-
if (isTilde(installedRange) || isTilde(requiredRange)) {
|
|
135
|
-
if (inst.major !== req.major) return false
|
|
136
|
-
// Tilde ranges on same major.minor overlap
|
|
137
|
-
if (isTilde(installedRange) && isTilde(requiredRange)) {
|
|
138
|
-
return inst.minor === req.minor
|
|
139
|
-
}
|
|
140
|
-
// Tilde + caret: tilde range must include or be included in caret range
|
|
141
|
-
return inst.minor === req.minor || (isCaret(requiredRange) && inst.minor >= req.minor)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Fallback: exact match
|
|
145
|
-
return inst.major === req.major && inst.minor === req.minor && inst.patch === req.patch
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ─── Discovery ───────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
function discoverTetraPeerDeps() {
|
|
151
|
-
const result = {}
|
|
152
|
-
|
|
153
|
-
for (const pkg of TETRA_PACKAGES) {
|
|
154
|
-
const pkgJson = readJson(join(TETRA_ROOT, pkg, 'package.json'))
|
|
155
|
-
if (!pkgJson?.peerDependencies) continue
|
|
156
|
-
|
|
157
|
-
result[pkgJson.name] = {
|
|
158
|
-
version: pkgJson.version,
|
|
159
|
-
peerDependencies: pkgJson.peerDependencies,
|
|
160
|
-
peerDependenciesMeta: pkgJson.peerDependenciesMeta || {}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return result
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function discoverConsumers() {
|
|
168
|
-
const consumers = []
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const dirs = execSync(`ls -d ${PROJECTS_ROOT}/*/`, { encoding: 'utf-8' })
|
|
172
|
-
.trim().split('\n').filter(Boolean)
|
|
173
|
-
|
|
174
|
-
for (const dir of dirs) {
|
|
175
|
-
const projectName = basename(dir.replace(/\/$/, ''))
|
|
176
|
-
if (projectName === 'tetra' || projectName.startsWith('.') || projectName.startsWith('_')) continue
|
|
177
|
-
|
|
178
|
-
// Check root, backend/, frontend/ package.json
|
|
179
|
-
const locations = [
|
|
180
|
-
{ path: join(dir, 'package.json'), label: projectName },
|
|
181
|
-
{ path: join(dir, 'backend', 'package.json'), label: `${projectName}/backend` },
|
|
182
|
-
{ path: join(dir, 'frontend', 'package.json'), label: `${projectName}/frontend` },
|
|
183
|
-
]
|
|
184
|
-
|
|
185
|
-
for (const loc of locations) {
|
|
186
|
-
const pkg = readJson(loc.path)
|
|
187
|
-
if (!pkg) continue
|
|
188
|
-
|
|
189
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
190
|
-
|
|
191
|
-
// Check if this package.json uses any tetra package
|
|
192
|
-
const usesTetra = Object.keys(allDeps).some(d => d.startsWith('@soulbatical/tetra-'))
|
|
193
|
-
if (!usesTetra) continue
|
|
194
|
-
|
|
195
|
-
consumers.push({
|
|
196
|
-
label: loc.label,
|
|
197
|
-
path: loc.path,
|
|
198
|
-
dependencies: allDeps,
|
|
199
|
-
tetraDeps: Object.fromEntries(
|
|
200
|
-
Object.entries(allDeps).filter(([k]) => k.startsWith('@soulbatical/tetra-'))
|
|
201
|
-
)
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
} catch {
|
|
206
|
-
// ignore discovery errors
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return consumers
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ─── Check ───────────────────────────────────────────────
|
|
213
|
-
|
|
214
|
-
function checkCompatibility(tetraPeers, consumers) {
|
|
215
|
-
const issues = []
|
|
216
|
-
|
|
217
|
-
for (const consumer of consumers) {
|
|
218
|
-
for (const [tetraPkg, tetraInfo] of Object.entries(tetraPeers)) {
|
|
219
|
-
// Does this consumer use this tetra package?
|
|
220
|
-
if (!consumer.tetraDeps[tetraPkg]) continue
|
|
221
|
-
|
|
222
|
-
// Check each peer dependency
|
|
223
|
-
for (const [peerDep, requiredRange] of Object.entries(tetraInfo.peerDependencies)) {
|
|
224
|
-
const isOptional = tetraInfo.peerDependenciesMeta[peerDep]?.optional
|
|
225
|
-
const installedVersion = consumer.dependencies[peerDep]
|
|
226
|
-
|
|
227
|
-
if (!installedVersion) {
|
|
228
|
-
if (!isOptional) {
|
|
229
|
-
issues.push({
|
|
230
|
-
consumer: consumer.label,
|
|
231
|
-
tetraPackage: tetraPkg,
|
|
232
|
-
dependency: peerDep,
|
|
233
|
-
required: requiredRange,
|
|
234
|
-
installed: 'MISSING',
|
|
235
|
-
severity: 'error',
|
|
236
|
-
fix: `npm install ${peerDep}@"${requiredRange}"`
|
|
237
|
-
})
|
|
238
|
-
}
|
|
239
|
-
continue
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Extract version from range (consumer might have "^2.93.3")
|
|
243
|
-
const cleanInstalled = installedVersion.replace(/^[\^~>=<\s]+/, '')
|
|
244
|
-
|
|
245
|
-
if (!satisfiesRange(cleanInstalled, requiredRange)) {
|
|
246
|
-
// Check if it's an exact pin vs range issue
|
|
247
|
-
const isExactPin = !requiredRange.startsWith('^') && !requiredRange.startsWith('~') && !requiredRange.startsWith('>')
|
|
248
|
-
const severity = isExactPin ? 'warning' : 'error'
|
|
249
|
-
|
|
250
|
-
issues.push({
|
|
251
|
-
consumer: consumer.label,
|
|
252
|
-
tetraPackage: tetraPkg,
|
|
253
|
-
dependency: peerDep,
|
|
254
|
-
required: requiredRange,
|
|
255
|
-
installed: installedVersion,
|
|
256
|
-
severity,
|
|
257
|
-
isExactPin,
|
|
258
|
-
fix: `npm install ${peerDep}@"${requiredRange}"`,
|
|
259
|
-
suggestion: isExactPin
|
|
260
|
-
? `Consider using "^${requiredRange}" in ${tetraPkg} peerDependencies for flexibility`
|
|
261
|
-
: null
|
|
262
|
-
})
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return issues
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ─── Output ──────────────────────────────────────────────
|
|
272
|
-
|
|
273
|
-
function formatTerminal(issues, consumers, tetraPeers, options) {
|
|
274
|
-
const lines = []
|
|
275
|
-
|
|
276
|
-
lines.push('')
|
|
277
|
-
lines.push('═══════════════════════════════════════════════════════════════')
|
|
278
|
-
lines.push(' 🔗 Tetra Check Peers — Peer Dependency Compatibility')
|
|
279
|
-
lines.push('═══════════════════════════════════════════════════════════════')
|
|
280
|
-
lines.push('')
|
|
281
|
-
|
|
282
|
-
// Summary
|
|
283
|
-
const tetraPackages = Object.entries(tetraPeers)
|
|
284
|
-
.map(([name, info]) => `${name}@${info.version}`)
|
|
285
|
-
.join(', ')
|
|
286
|
-
lines.push(` Tetra packages: ${tetraPackages}`)
|
|
287
|
-
lines.push(` Consumers found: ${consumers.length}`)
|
|
288
|
-
lines.push(` Issues found: ${issues.length}`)
|
|
289
|
-
lines.push('')
|
|
290
|
-
|
|
291
|
-
if (issues.length === 0) {
|
|
292
|
-
lines.push(' ✅ All consumer projects are compatible with current peer dependencies')
|
|
293
|
-
lines.push('')
|
|
294
|
-
lines.push('═══════════════════════════════════════════════════════════════')
|
|
295
|
-
return lines.join('\n')
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Group by consumer
|
|
299
|
-
const byConsumer = {}
|
|
300
|
-
for (const issue of issues) {
|
|
301
|
-
if (!byConsumer[issue.consumer]) byConsumer[issue.consumer] = []
|
|
302
|
-
byConsumer[issue.consumer].push(issue)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
for (const [consumer, consumerIssues] of Object.entries(byConsumer)) {
|
|
306
|
-
lines.push(` 📦 ${consumer}`)
|
|
307
|
-
for (const issue of consumerIssues) {
|
|
308
|
-
const icon = issue.severity === 'error' ? '❌' : '⚠️'
|
|
309
|
-
lines.push(` ${icon} ${issue.dependency}: installed ${issue.installed}, needs ${issue.required}`)
|
|
310
|
-
if (issue.suggestion) {
|
|
311
|
-
lines.push(` 💡 ${issue.suggestion}`)
|
|
312
|
-
}
|
|
313
|
-
if (options.fix) {
|
|
314
|
-
lines.push(` → ${issue.fix}`)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
lines.push('')
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Exact pin warnings
|
|
321
|
-
const exactPins = issues.filter(i => i.isExactPin)
|
|
322
|
-
if (exactPins.length > 0) {
|
|
323
|
-
const uniquePins = [...new Set(exactPins.map(i => `${i.dependency} (${i.required} in ${i.tetraPackage})`))]
|
|
324
|
-
lines.push(' 💡 EXACT VERSION PINS detected — these cause most compatibility issues:')
|
|
325
|
-
for (const pin of uniquePins) {
|
|
326
|
-
lines.push(` → ${pin}`)
|
|
327
|
-
}
|
|
328
|
-
lines.push(' Consider using "^x.y.z" ranges instead of exact versions in peerDependencies')
|
|
329
|
-
lines.push('')
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
lines.push('═══════════════════════════════════════════════════════════════')
|
|
333
|
-
return lines.join('\n')
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// ─── Main ────────────────────────────────────────────────
|
|
337
|
-
|
|
338
|
-
const args = process.argv.slice(2)
|
|
339
|
-
const options = {
|
|
340
|
-
fix: args.includes('--fix'),
|
|
341
|
-
strict: args.includes('--strict'),
|
|
342
|
-
json: args.includes('--json'),
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const tetraPeers = discoverTetraPeerDeps()
|
|
346
|
-
const consumers = discoverConsumers()
|
|
347
|
-
const issues = checkCompatibility(tetraPeers, consumers)
|
|
348
|
-
|
|
349
|
-
if (options.json) {
|
|
350
|
-
console.log(JSON.stringify({ tetraPeers, consumers: consumers.map(c => c.label), issues }, null, 2))
|
|
351
|
-
} else {
|
|
352
|
-
console.log(formatTerminal(issues, consumers, tetraPeers, options))
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Exit code
|
|
356
|
-
const errors = issues.filter(i => i.severity === 'error')
|
|
357
|
-
if (options.strict && errors.length > 0) {
|
|
358
|
-
process.exit(1)
|
|
359
|
-
}
|
package/bin/tetra-db-push.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tetra DB Push — Safe wrapper around `supabase db push`
|
|
5
|
-
*
|
|
6
|
-
* Runs tetra-migration-lint FIRST. If any CRITICAL or HIGH issues
|
|
7
|
-
* are found, the push is BLOCKED. No exceptions.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* tetra-db-push # Lint + push
|
|
11
|
-
* tetra-db-push --force # Skip lint (DANGEROUS — requires explicit flag)
|
|
12
|
-
* tetra-db-push --dry-run # Lint only, don't push
|
|
13
|
-
* tetra-db-push -- --linked # Pass flags to supabase db push
|
|
14
|
-
*
|
|
15
|
-
* Replace in your workflow:
|
|
16
|
-
* BEFORE: supabase db push
|
|
17
|
-
* AFTER: tetra-db-push
|
|
18
|
-
*
|
|
19
|
-
* Or add alias: alias supabase-push='tetra-db-push'
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { execSync, spawnSync } from 'child_process'
|
|
23
|
-
import chalk from 'chalk'
|
|
24
|
-
import { resolve } from 'path'
|
|
25
|
-
|
|
26
|
-
const args = process.argv.slice(2)
|
|
27
|
-
const force = args.includes('--force')
|
|
28
|
-
const dryRun = args.includes('--dry-run')
|
|
29
|
-
const supabaseArgs = args.filter(a => a !== '--force' && a !== '--dry-run')
|
|
30
|
-
|
|
31
|
-
const projectRoot = resolve(process.cwd())
|
|
32
|
-
|
|
33
|
-
// ─── Step 1: Migration Lint ────────────────────────────────────────
|
|
34
|
-
console.log('')
|
|
35
|
-
console.log(chalk.bold(' 🔒 Tetra DB Push — Security Gate'))
|
|
36
|
-
console.log('')
|
|
37
|
-
|
|
38
|
-
if (force) {
|
|
39
|
-
console.log(chalk.red.bold(' ⚠️ --force flag: SKIPPING security lint'))
|
|
40
|
-
console.log(chalk.red(' You are pushing migrations WITHOUT security validation.'))
|
|
41
|
-
console.log('')
|
|
42
|
-
} else {
|
|
43
|
-
console.log(chalk.gray(' Step 1: Running migration lint...'))
|
|
44
|
-
console.log('')
|
|
45
|
-
|
|
46
|
-
const lintResult = spawnSync('node', [
|
|
47
|
-
resolve(import.meta.dirname, 'tetra-migration-lint.js'),
|
|
48
|
-
'--project', projectRoot
|
|
49
|
-
], {
|
|
50
|
-
cwd: projectRoot,
|
|
51
|
-
stdio: 'inherit',
|
|
52
|
-
env: process.env
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
if (lintResult.status !== 0) {
|
|
56
|
-
console.log('')
|
|
57
|
-
console.log(chalk.red.bold(' ❌ Migration lint FAILED — push blocked'))
|
|
58
|
-
console.log(chalk.gray(' Fix the issues above, then run tetra-db-push again.'))
|
|
59
|
-
console.log('')
|
|
60
|
-
process.exit(1)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
console.log(chalk.green(' ✅ Migration lint passed'))
|
|
64
|
-
console.log('')
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ─── Step 2: Supabase DB Push ──────────────────────────────────────
|
|
68
|
-
if (dryRun) {
|
|
69
|
-
console.log(chalk.gray(' --dry-run: skipping actual push'))
|
|
70
|
-
console.log('')
|
|
71
|
-
process.exit(0)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
console.log(chalk.gray(' Step 2: Running supabase db push...'))
|
|
75
|
-
console.log('')
|
|
76
|
-
|
|
77
|
-
const pushResult = spawnSync('npx', ['supabase', 'db', 'push', ...supabaseArgs], {
|
|
78
|
-
cwd: projectRoot,
|
|
79
|
-
stdio: 'inherit',
|
|
80
|
-
env: process.env
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
if (pushResult.status !== 0) {
|
|
84
|
-
console.log('')
|
|
85
|
-
console.log(chalk.red(' ❌ supabase db push failed'))
|
|
86
|
-
process.exit(pushResult.status || 1)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
console.log('')
|
|
90
|
-
console.log(chalk.green.bold(' ✅ Migrations pushed successfully (security validated)'))
|
|
91
|
-
console.log('')
|