@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/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
- let checkResult
184
-
185
- // rls-policy-audit is pure migration parsing — skip when live DB is available
186
- if (rlsLiveData && check.meta.id === 'rls-policy-audit') {
187
- checkResult = {
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": "1.20.2",
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/",
@@ -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
- }
@@ -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('')