@soulbatical/tetra-dev-toolkit 1.6.0 → 1.6.1

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.
Files changed (2) hide show
  1. package/bin/tetra-init.js +623 -0
  2. package/package.json +2 -1
@@ -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()
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.6.1",
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
  },