@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.
- package/bin/tetra-init.js +623 -0
- 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.
|
|
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
|
},
|