@soulbatical/tetra-dev-toolkit 1.6.0 → 1.7.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,623 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Tetra Dev Toolkit - Project Init CLI
5
+ *
6
+ * Initializes a Tetra project with all required config files:
7
+ * - .ralph/ directory (ports.json, INFRASTRUCTURE.yml, MARKETING.yml, config.sh, status.json)
8
+ * - .tetra-quality.json
9
+ * - Verifies doppler.yaml and CLAUDE.md existence
10
+ *
11
+ * Usage:
12
+ * tetra-init # Interactive full init
13
+ * tetra-init ralph # Only .ralph/ config files
14
+ * tetra-init check # Verify project completeness (no changes)
15
+ * tetra-init --name myproject # Non-interactive with project name
16
+ */
17
+
18
+ import { program } from 'commander'
19
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'
20
+ import { join, basename } from 'path'
21
+ import { createInterface } from 'readline'
22
+
23
+ const projectRoot = process.cwd()
24
+
25
+ // ─── Helpers ────────────────────────────────────────────────────
26
+
27
+ function ask(question, defaultValue) {
28
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
29
+ const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `
30
+ return new Promise((resolve) => {
31
+ rl.question(prompt, (answer) => {
32
+ rl.close()
33
+ resolve(answer.trim() || defaultValue || '')
34
+ })
35
+ })
36
+ }
37
+
38
+ function writeIfMissing(filePath, content, options = {}) {
39
+ const relativePath = filePath.replace(projectRoot + '/', '')
40
+ if (existsSync(filePath) && !options.force) {
41
+ console.log(` ⏭️ ${relativePath} already exists`)
42
+ return false
43
+ }
44
+ const dir = filePath.substring(0, filePath.lastIndexOf('/'))
45
+ if (!existsSync(dir)) {
46
+ mkdirSync(dir, { recursive: true })
47
+ }
48
+ writeFileSync(filePath, content)
49
+ console.log(` ✅ Created ${relativePath}`)
50
+ return true
51
+ }
52
+
53
+ function detectProjectName() {
54
+ const packagePath = join(projectRoot, 'package.json')
55
+ if (existsSync(packagePath)) {
56
+ try {
57
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
58
+ return pkg.name?.replace(/^@[^/]+\//, '') || basename(projectRoot)
59
+ } catch { /* ignore */ }
60
+ }
61
+ return basename(projectRoot)
62
+ }
63
+
64
+ function detectPorts() {
65
+ // Try to find ports from doppler.yaml or package.json scripts
66
+ const packagePath = join(projectRoot, 'package.json')
67
+ if (existsSync(packagePath)) {
68
+ try {
69
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
70
+ const scripts = pkg.scripts || {}
71
+ const allScripts = Object.values(scripts).join(' ')
72
+
73
+ // Try to extract backend port from scripts
74
+ const backendPortMatch = allScripts.match(/PORT[=:](\d+)/) ||
75
+ allScripts.match(/-p\s+(\d+).*backend/) ||
76
+ allScripts.match(/backend.*-p\s+(\d+)/)
77
+
78
+ // Try to extract frontend port from scripts
79
+ const frontendPortMatch = allScripts.match(/next\s+dev\s+-p\s+(\d+)/) ||
80
+ allScripts.match(/-p\s+(\d+)/)
81
+
82
+ return {
83
+ backend: backendPortMatch ? parseInt(backendPortMatch[1]) : null,
84
+ frontend: frontendPortMatch ? parseInt(frontendPortMatch[1]) : null
85
+ }
86
+ } catch { /* ignore */ }
87
+ }
88
+ return { backend: null, frontend: null }
89
+ }
90
+
91
+ function detectSupabaseProjectId() {
92
+ // Check doppler.yaml or existing env references
93
+ const dopplerPath = join(projectRoot, 'doppler.yaml')
94
+ if (existsSync(dopplerPath)) {
95
+ return 'check-doppler' // User needs to fill in from Doppler
96
+ }
97
+ return null
98
+ }
99
+
100
+ function hasWorkspace(name) {
101
+ return existsSync(join(projectRoot, name))
102
+ }
103
+
104
+ // ─── Templates ──────────────────────────────────────────────────
105
+
106
+ function generateInfrastructureYml(config) {
107
+ return `# ${config.name} Infrastructure Configuration
108
+ # Single source of truth for all external services and infrastructure
109
+ # Machine-readable by ralph-manager health checks
110
+
111
+ project:
112
+ name: ${config.name}
113
+ phase: development
114
+ description: "${config.description}"
115
+ repo: null
116
+
117
+ hosting:
118
+ frontend:
119
+ type: null
120
+ site_name: null
121
+ url: null
122
+ branch: main
123
+ backend:
124
+ type: null
125
+ service_name: null
126
+ url: null
127
+ branch: main
128
+
129
+ domains:
130
+ registrar: null
131
+ primary: null
132
+ aliases: []
133
+ dns_provider: null
134
+ ssl: auto
135
+
136
+ database:
137
+ provider: supabase
138
+ region: eu-central-1
139
+ project_id: null
140
+
141
+ secrets:
142
+ manager: doppler
143
+ configs: []
144
+
145
+ email:
146
+ provider: null
147
+ type: null
148
+ addresses: []
149
+
150
+ monitoring:
151
+ uptime: null
152
+ error_tracking: null
153
+ analytics: null
154
+
155
+ services:
156
+ payment: null
157
+ email_delivery: null
158
+ cdn: null
159
+
160
+ security:
161
+ rls_enabled: true
162
+ auth_provider: supabase
163
+ rate_limiting: true
164
+ repo_visibility: private
165
+ open_issues: []
166
+
167
+ meta:
168
+ created_at: "${new Date().toISOString().split('T')[0]}"
169
+ updated_by: tetra-init
170
+ version: 1
171
+ `
172
+ }
173
+
174
+ function generateMarketingYml(config) {
175
+ return `# ${config.name} Marketing Stack Configuration
176
+ # Single source of truth for marketing infrastructure status
177
+ # Machine-readable by ralph-manager
178
+
179
+ project: ${config.name}
180
+ updated_at: "${new Date().toISOString().split('T')[0]}"
181
+ updated_by: tetra-init
182
+
183
+ # ─── Meta (Facebook/Instagram) ───────────────────────────────
184
+
185
+ meta:
186
+ business_portfolio: null
187
+ business_portfolio_id: null
188
+ ad_account:
189
+ name: null
190
+ id: null
191
+ status: not_configured
192
+ currency: EUR
193
+ pixel:
194
+ name: null
195
+ id: null
196
+ status: not_configured
197
+ consent_mode: true
198
+ events: []
199
+ pages: []
200
+
201
+ # ─── Google ──────────────────────────────────────────────────
202
+
203
+ google:
204
+ analytics:
205
+ measurement_id: null
206
+ stream_id: null
207
+ status: not_configured
208
+ consent_mode: true
209
+ ads:
210
+ customer_id: null
211
+ status: not_configured
212
+ search_console:
213
+ verified: false
214
+ status: not_configured
215
+ tag_manager:
216
+ container_id: null
217
+ status: not_configured
218
+
219
+ # ─── Consent & Privacy ──────────────────────────────────────
220
+
221
+ consent:
222
+ platform: null
223
+ cookie_banner: false
224
+ privacy_policy: false
225
+ terms_of_service: false
226
+ gdpr_compliant: false
227
+
228
+ # ─── SEO ────────────────────────────────────────────────────
229
+
230
+ seo:
231
+ sitemap: false
232
+ robots_txt: false
233
+ structured_data: false
234
+ canonical_urls: false
235
+ meta_tags: false
236
+
237
+ # ─── Email Marketing ────────────────────────────────────────
238
+
239
+ email_marketing:
240
+ provider: null
241
+ list_id: null
242
+ status: not_configured
243
+
244
+ meta_info:
245
+ created_at: "${new Date().toISOString().split('T')[0]}"
246
+ updated_by: tetra-init
247
+ version: 1
248
+ `
249
+ }
250
+
251
+ function generatePortsJson(config) {
252
+ const obj = {
253
+ backend_port: config.backendPort,
254
+ frontend_port: config.frontendPort,
255
+ api_url: `http://localhost:${config.backendPort}`,
256
+ synced_at: new Date().toISOString(),
257
+ source: 'tetra-init'
258
+ }
259
+ return JSON.stringify(obj, null, 2) + '\n'
260
+ }
261
+
262
+ function generateConfigSh(config) {
263
+ return `# ${config.name} project config overrides for Ralph
264
+ # Allowed tools for autonomous Ralph sessions
265
+ CLAUDE_ALLOWED_TOOLS="Write,Edit,Read,Bash,Glob,Grep,WebFetch,WebSearch"
266
+ `
267
+ }
268
+
269
+ function generateStatusJson() {
270
+ return JSON.stringify({
271
+ last_session: null,
272
+ last_task: null,
273
+ updated_at: new Date().toISOString()
274
+ }, null, 2) + '\n'
275
+ }
276
+
277
+ function generateFixPlan(config) {
278
+ return `# ${config.name} — Fix Plan
279
+
280
+ ## Active Tasks
281
+
282
+ - [ ] Project initialization — verify all config files are correct
283
+ - [ ] First feature implementation
284
+
285
+ ## Completed
286
+
287
+ (none yet)
288
+ `
289
+ }
290
+
291
+ function generatePromptMd(config) {
292
+ return `# Ralph Development Instructions - ${config.name}
293
+
294
+ ## Context
295
+ You are Ralph, an AUTONOMOUS AI development agent working on **${config.name}**.
296
+
297
+ ## KRITIEKE REGEL: NOOIT VRAGEN, ALTIJD DOEN
298
+
299
+ \`\`\`
300
+ VERBODEN:
301
+ - "Wat wil je als eerste oppakken?"
302
+ - "Waar wil je mee beginnen?"
303
+ - "Wil je dat ik X doe?"
304
+ - Elke vraag aan de gebruiker over wat je moet doen
305
+
306
+ VERPLICHT:
307
+ - Kies ZELF de eerste onafgeronde taak uit @fix_plan.md
308
+ - Voer die taak UIT in dezelfde loop
309
+ - Vraag alleen om hulp als iets technisch ONMOGELIJK is
310
+ \`\`\`
311
+
312
+ ## Sessie Opstart (loop 1)
313
+
314
+ 1. \`Read .ralph/@fix_plan.md\` — vind eerste \`- [ ]\` taak
315
+ 2. **BEGIN DIRECT met die taak** — niet samenvatten, niet vragen, DOEN
316
+ 3. Na afronding: vink af met [x], ga naar volgende taak
317
+
318
+ ## Na elke taak
319
+
320
+ 1. Vink de taak af in @fix_plan.md
321
+ 2. Ga naar de volgende \`- [ ]\` taak
322
+ 3. Als alles af is: schrijf samenvatting en stop
323
+ `
324
+ }
325
+
326
+ function generateProjectJson(config) {
327
+ return JSON.stringify({
328
+ ralph_id: null,
329
+ name: config.name,
330
+ created_at: new Date().toISOString()
331
+ }, null, 2) + '\n'
332
+ }
333
+
334
+ function generateTestConfigYml(config) {
335
+ return `# ${config.name} Test Configuration
336
+ # Used by Ralph for automated testing
337
+
338
+ test_suites:
339
+ unit:
340
+ command: "npm test"
341
+ timeout: 120
342
+ typecheck:
343
+ command: "npx tsc --noEmit"
344
+ timeout: 60
345
+ e2e:
346
+ command: "npx playwright test"
347
+ timeout: 300
348
+ enabled: false
349
+
350
+ health_checks:
351
+ backend:
352
+ url: "http://localhost:${config.backendPort}/api/health"
353
+ timeout: 5
354
+ frontend:
355
+ url: "http://localhost:${config.frontendPort}"
356
+ timeout: 10
357
+ `
358
+ }
359
+
360
+ function generateTetraQualityJson() {
361
+ return JSON.stringify({
362
+ "$schema": "https://tetra-tools.dev/schemas/quality-toolkit.json",
363
+ "suites": {
364
+ "security": true,
365
+ "stability": true,
366
+ "codeQuality": true,
367
+ "supabase": "auto",
368
+ "hygiene": true
369
+ },
370
+ "security": {
371
+ "checkHardcodedSecrets": true,
372
+ "checkServiceKeyExposure": true
373
+ },
374
+ "stability": {
375
+ "requireHusky": true,
376
+ "requireCiConfig": true,
377
+ "allowedVulnerabilities": {
378
+ "critical": 0,
379
+ "high": 0,
380
+ "moderate": 10
381
+ }
382
+ },
383
+ "ignore": [
384
+ "node_modules/**",
385
+ "dist/**",
386
+ "build/**",
387
+ ".next/**"
388
+ ]
389
+ }, null, 2) + '\n'
390
+ }
391
+
392
+ // ─── Commands ───────────────────────────────────────────────────
393
+
394
+ async function initRalph(config, options) {
395
+ console.log('')
396
+ console.log('📁 Initializing .ralph/ directory...')
397
+
398
+ const ralphDir = join(projectRoot, '.ralph')
399
+ if (!existsSync(ralphDir)) {
400
+ mkdirSync(ralphDir, { recursive: true })
401
+ console.log(' ✅ Created .ralph/')
402
+ }
403
+
404
+ // Specs directory
405
+ const specsDir = join(ralphDir, 'specs')
406
+ if (!existsSync(specsDir)) {
407
+ mkdirSync(specsDir, { recursive: true })
408
+ console.log(' ✅ Created .ralph/specs/')
409
+ }
410
+
411
+ writeIfMissing(join(ralphDir, 'ports.json'), generatePortsJson(config), options)
412
+ writeIfMissing(join(ralphDir, 'INFRASTRUCTURE.yml'), generateInfrastructureYml(config), options)
413
+ writeIfMissing(join(ralphDir, 'MARKETING.yml'), generateMarketingYml(config), options)
414
+ writeIfMissing(join(ralphDir, 'config.sh'), generateConfigSh(config), options)
415
+ writeIfMissing(join(ralphDir, 'status.json'), generateStatusJson(), options)
416
+ writeIfMissing(join(ralphDir, '@fix_plan.md'), generateFixPlan(config), options)
417
+ writeIfMissing(join(ralphDir, 'PROMPT.md'), generatePromptMd(config), options)
418
+ writeIfMissing(join(ralphDir, 'project.json'), generateProjectJson(config), options)
419
+ writeIfMissing(join(ralphDir, 'test-config.yml'), generateTestConfigYml(config), options)
420
+ }
421
+
422
+ async function initQuality(config, options) {
423
+ console.log('')
424
+ console.log('🔍 Initializing quality config...')
425
+
426
+ writeIfMissing(
427
+ join(projectRoot, '.tetra-quality.json'),
428
+ generateTetraQualityJson(),
429
+ options
430
+ )
431
+ }
432
+
433
+ function checkCompleteness() {
434
+ console.log('')
435
+ console.log('🔎 Checking project completeness...')
436
+ console.log('═'.repeat(50))
437
+
438
+ const checks = [
439
+ // Root files
440
+ { path: 'package.json', category: 'root', required: true },
441
+ { path: 'doppler.yaml', category: 'root', required: true },
442
+ { path: 'CLAUDE.md', category: 'root', required: true },
443
+ { path: '.tetra-quality.json', category: 'root', required: false },
444
+ { path: '.gitignore', category: 'root', required: true },
445
+
446
+ // .ralph/ files
447
+ { path: '.ralph/ports.json', category: 'ralph', required: true },
448
+ { path: '.ralph/INFRASTRUCTURE.yml', category: 'ralph', required: true },
449
+ { path: '.ralph/MARKETING.yml', category: 'ralph', required: false },
450
+ { path: '.ralph/config.sh', category: 'ralph', required: false },
451
+ { path: '.ralph/status.json', category: 'ralph', required: false },
452
+ { path: '.ralph/@fix_plan.md', category: 'ralph', required: true },
453
+ { path: '.ralph/PROMPT.md', category: 'ralph', required: true },
454
+ { path: '.ralph/project.json', category: 'ralph', required: true },
455
+ { path: '.ralph/test-config.yml', category: 'ralph', required: false },
456
+ { path: '.ralph/specs', category: 'ralph', required: false },
457
+
458
+ // Backend
459
+ { path: 'backend/package.json', category: 'backend', required: true },
460
+ { path: 'backend/tsconfig.json', category: 'backend', required: true },
461
+ { path: 'backend/src/index.ts', category: 'backend', required: true },
462
+ { path: 'backend/src/auth-config.ts', category: 'backend', required: true },
463
+ { path: 'backend/src/core/Application.ts', category: 'backend', required: true },
464
+ { path: 'backend/src/core/RouteManager.ts', category: 'backend', required: true },
465
+
466
+ // Frontend
467
+ { path: 'frontend/package.json', category: 'frontend', required: true },
468
+ { path: 'frontend/next.config.ts', category: 'frontend', required: true },
469
+ { path: 'frontend/src/app/layout.tsx', category: 'frontend', required: true },
470
+
471
+ // Database
472
+ { path: 'supabase/migrations', category: 'database', required: false },
473
+ ]
474
+
475
+ let currentCategory = ''
476
+ let passed = 0
477
+ let failed = 0
478
+ let warnings = 0
479
+
480
+ for (const check of checks) {
481
+ if (check.category !== currentCategory) {
482
+ currentCategory = check.category
483
+ console.log('')
484
+ console.log(` ${currentCategory.toUpperCase()}`)
485
+ }
486
+
487
+ const fullPath = join(projectRoot, check.path)
488
+ const exists = existsSync(fullPath)
489
+ const icon = exists ? '✅' : (check.required ? '❌' : '⚠️')
490
+
491
+ if (exists) {
492
+ passed++
493
+ } else if (check.required) {
494
+ failed++
495
+ } else {
496
+ warnings++
497
+ }
498
+
499
+ console.log(` ${icon} ${check.path}`)
500
+ }
501
+
502
+ console.log('')
503
+ console.log('═'.repeat(50))
504
+ console.log(` ✅ ${passed} present ❌ ${failed} missing (required) ⚠️ ${warnings} missing (optional)`)
505
+
506
+ if (failed > 0) {
507
+ console.log('')
508
+ console.log(' Run `tetra-init` to generate missing files.')
509
+ } else if (warnings > 0) {
510
+ console.log('')
511
+ console.log(' All required files present! Run `tetra-init` to generate optional files.')
512
+ } else {
513
+ console.log('')
514
+ console.log(' 🎉 Project is fully initialized!')
515
+ }
516
+
517
+ console.log('')
518
+ return failed === 0
519
+ }
520
+
521
+ // ─── Main ───────────────────────────────────────────────────────
522
+
523
+ program
524
+ .name('tetra-init')
525
+ .description('Initialize a Tetra project with all required config files')
526
+ .version('1.0.0')
527
+ .argument('[component]', 'Component to init: ralph, quality, check, or all (default)')
528
+ .option('-n, --name <name>', 'Project name (skips interactive prompt)')
529
+ .option('-d, --description <desc>', 'Project description')
530
+ .option('--backend-port <port>', 'Backend port number', parseInt)
531
+ .option('--frontend-port <port>', 'Frontend port number', parseInt)
532
+ .option('-f, --force', 'Overwrite existing files')
533
+ .option('-y, --yes', 'Accept all defaults (non-interactive)')
534
+ .action(async (component, options) => {
535
+ console.log('')
536
+ console.log('🚀 Tetra Project Init')
537
+ console.log('═'.repeat(50))
538
+
539
+ // Check-only mode
540
+ if (component === 'check') {
541
+ const ok = checkCompleteness()
542
+ process.exit(ok ? 0 : 1)
543
+ }
544
+
545
+ // Gather config
546
+ const detectedName = detectProjectName()
547
+ const detectedPorts = detectPorts()
548
+ const isInteractive = !options.yes && process.stdin.isTTY
549
+
550
+ let config = {
551
+ name: options.name || detectedName,
552
+ description: options.description || '',
553
+ backendPort: options.backendPort || detectedPorts.backend,
554
+ frontendPort: options.frontendPort || detectedPorts.frontend,
555
+ }
556
+
557
+ if (isInteractive) {
558
+ console.log('')
559
+ config.name = await ask('Project name', config.name)
560
+ config.description = await ask('Description', config.description || `${config.name} platform`)
561
+ config.backendPort = parseInt(await ask('Backend port', String(config.backendPort || 3000))) || 3000
562
+ config.frontendPort = parseInt(await ask('Frontend port', String(config.frontendPort || 3001))) || 3001
563
+ } else {
564
+ // Non-interactive defaults
565
+ config.description = config.description || `${config.name} platform`
566
+ config.backendPort = config.backendPort || 3000
567
+ config.frontendPort = config.frontendPort || 3001
568
+ }
569
+
570
+ console.log('')
571
+ console.log(` Project: ${config.name}`)
572
+ console.log(` Backend: localhost:${config.backendPort}`)
573
+ console.log(` Frontend: localhost:${config.frontendPort}`)
574
+
575
+ const components = component === 'all' || !component
576
+ ? ['ralph', 'quality']
577
+ : [component]
578
+
579
+ for (const comp of components) {
580
+ switch (comp) {
581
+ case 'ralph':
582
+ await initRalph(config, options)
583
+ break
584
+ case 'quality':
585
+ await initQuality(config, options)
586
+ break
587
+ default:
588
+ console.log(`Unknown component: ${comp}`)
589
+ console.log('Available: ralph, quality, check, or all (default)')
590
+ }
591
+ }
592
+
593
+ // Verify essential root files
594
+ console.log('')
595
+ console.log('📋 Checking essential root files...')
596
+
597
+ const essentials = [
598
+ { path: 'doppler.yaml', hint: 'Create doppler.yaml with your Doppler project config' },
599
+ { path: 'CLAUDE.md', hint: 'Create CLAUDE.md with project instructions for Claude Code' },
600
+ { path: '.gitignore', hint: 'Create .gitignore (node_modules, .next, dist, etc.)' },
601
+ ]
602
+
603
+ for (const item of essentials) {
604
+ const fullPath = join(projectRoot, item.path)
605
+ if (existsSync(fullPath)) {
606
+ console.log(` ✅ ${item.path}`)
607
+ } else {
608
+ console.log(` ⚠️ ${item.path} — ${item.hint}`)
609
+ }
610
+ }
611
+
612
+ console.log('')
613
+ console.log('✅ Init complete!')
614
+ console.log('')
615
+ console.log('Next steps:')
616
+ console.log(' 1. Review generated files in .ralph/')
617
+ console.log(' 2. Fill in INFRASTRUCTURE.yml with hosting/domain info')
618
+ console.log(' 3. Run `tetra-init check` to verify completeness')
619
+ console.log(' 4. Run `tetra-setup` to add quality hooks')
620
+ console.log('')
621
+ })
622
+
623
+ program.parse()
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Check: File Size / God File Detection
3
+ *
4
+ * Prevents "god files" — overly large source files that:
5
+ * - Are hard to review, test, and maintain
6
+ * - Indicate missing separation of concerns
7
+ * - Grow silently until they become unmovable
8
+ *
9
+ * Uses config.codeQuality.maxFileLines (default: 500).
10
+ * Scans backend + frontend source directories.
11
+ */
12
+
13
+ import { glob } from 'glob'
14
+ import { readFileSync } from 'fs'
15
+
16
+ export const meta = {
17
+ id: 'file-size',
18
+ name: 'File Size / God File Detection',
19
+ category: 'codeQuality',
20
+ severity: 'high',
21
+ description: 'Detects source files exceeding maxFileLines — god files that need splitting'
22
+ }
23
+
24
+ export async function run(config, projectRoot) {
25
+ const maxLines = config.codeQuality?.maxFileLines || 500
26
+
27
+ const results = {
28
+ passed: true,
29
+ findings: [],
30
+ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
31
+ info: {
32
+ maxFileLines: maxLines,
33
+ filesScanned: 0,
34
+ violations: 0,
35
+ largest: null
36
+ }
37
+ }
38
+
39
+ // Scan all TypeScript/JavaScript source files
40
+ const patterns = [
41
+ 'backend/src/**/*.ts',
42
+ 'frontend/src/**/*.ts',
43
+ 'frontend/src/**/*.tsx',
44
+ 'backend-mcp/src/**/*.ts',
45
+ 'src/**/*.ts',
46
+ 'src/**/*.tsx',
47
+ 'bot/**/*.ts',
48
+ ]
49
+
50
+ let files = []
51
+ for (const pattern of patterns) {
52
+ const found = await glob(pattern, {
53
+ cwd: projectRoot,
54
+ ignore: [
55
+ ...(config.ignore || []),
56
+ 'node_modules/**',
57
+ '**/node_modules/**',
58
+ 'dist/**',
59
+ '**/dist/**',
60
+ 'build/**',
61
+ '**/build/**',
62
+ '**/*.test.*',
63
+ '**/*.spec.*',
64
+ '**/*.d.ts',
65
+ '**/*.js.map',
66
+ '**/generated/**',
67
+ ]
68
+ })
69
+ files.push(...found)
70
+ }
71
+
72
+ // Deduplicate
73
+ files = [...new Set(files)]
74
+
75
+ if (files.length === 0) {
76
+ results.skipped = true
77
+ results.skipReason = 'No source files found'
78
+ return results
79
+ }
80
+
81
+ let largestFile = null
82
+ let largestLines = 0
83
+
84
+ for (const file of files) {
85
+ const filePath = `${projectRoot}/${file}`
86
+ let content
87
+ try {
88
+ content = readFileSync(filePath, 'utf-8')
89
+ } catch {
90
+ continue
91
+ }
92
+
93
+ results.info.filesScanned++
94
+
95
+ const lineCount = content.split('\n').length
96
+
97
+ // Track largest file
98
+ if (lineCount > largestLines) {
99
+ largestLines = lineCount
100
+ largestFile = file
101
+ }
102
+
103
+ if (lineCount <= maxLines) continue
104
+
105
+ // Determine severity based on how far over the limit
106
+ const ratio = lineCount / maxLines
107
+ let severity
108
+ if (ratio >= 5) {
109
+ severity = 'critical' // 5x over limit (e.g. 2500+ lines with 500 limit)
110
+ } else if (ratio >= 3) {
111
+ severity = 'high' // 3x over limit (e.g. 1500+ lines)
112
+ } else if (ratio >= 2) {
113
+ severity = 'medium' // 2x over limit (e.g. 1000+ lines)
114
+ } else {
115
+ severity = 'low' // Just over limit
116
+ }
117
+
118
+ results.findings.push({
119
+ file,
120
+ line: 1,
121
+ type: 'GOD_FILE',
122
+ severity,
123
+ message: `${file} has ${lineCount} lines (max: ${maxLines}) — split into smaller modules`,
124
+ fix: `Break this file into focused modules of <${maxLines} lines each`
125
+ })
126
+
127
+ results.summary[severity]++
128
+ results.summary.total++
129
+ results.info.violations++
130
+ }
131
+
132
+ results.info.largest = largestFile ? { file: largestFile, lines: largestLines } : null
133
+
134
+ // Fail on critical or high findings
135
+ results.passed = results.findings.filter(f =>
136
+ f.severity === 'critical' || f.severity === 'high'
137
+ ).length === 0
138
+
139
+ return results
140
+ }
package/lib/runner.js CHANGED
@@ -15,6 +15,7 @@ import * as huskyHooks from './checks/stability/husky-hooks.js'
15
15
  import * as ciPipeline from './checks/stability/ci-pipeline.js'
16
16
  import * as npmAudit from './checks/stability/npm-audit.js'
17
17
  import * as apiResponseFormat from './checks/codeQuality/api-response-format.js'
18
+ import * as fileSize from './checks/codeQuality/file-size.js'
18
19
  import * as gitignoreValidation from './checks/security/gitignore-validation.js'
19
20
  import * as rlsPolicyAudit from './checks/supabase/rls-policy-audit.js'
20
21
  import * as rpcParamMismatch from './checks/supabase/rpc-param-mismatch.js'
@@ -36,7 +37,8 @@ const ALL_CHECKS = {
36
37
  npmAudit
37
38
  ],
38
39
  codeQuality: [
39
- apiResponseFormat
40
+ apiResponseFormat,
41
+ fileSize
40
42
  ],
41
43
  supabase: [
42
44
  rlsPolicyAudit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },
@@ -26,6 +26,7 @@
26
26
  "main": "lib/index.js",
27
27
  "bin": {
28
28
  "tetra-audit": "./bin/tetra-audit.js",
29
+ "tetra-init": "./bin/tetra-init.js",
29
30
  "tetra-setup": "./bin/tetra-setup.js",
30
31
  "tetra-dev-token": "./bin/tetra-dev-token.js"
31
32
  },