faster-vibecraft 0.2.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/bin/cli.js ADDED
@@ -0,0 +1,824 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Vibeshop CLI - 3D visualization for Claude Code
5
+ *
6
+ * Usage:
7
+ * npx vibecraft # Start the server
8
+ * npx vibecraft --help # Show help
9
+ */
10
+
11
+ // Check if cwd exists (common issue when running from deleted directory)
12
+ try {
13
+ process.cwd()
14
+ } catch (e) {
15
+ console.error('Error: Current directory no longer exists.')
16
+ console.error('This happens when the directory you ran the command from was deleted.')
17
+ console.error('\nFix: cd to a valid directory first:')
18
+ console.error(' cd ~')
19
+ console.error(' npx vibecraft setup')
20
+ process.exit(1)
21
+ }
22
+
23
+ import { spawn, execSync } from 'child_process'
24
+ import { dirname, resolve, join, basename } from 'path'
25
+ import { fileURLToPath } from 'url'
26
+ import { existsSync, mkdirSync, readFileSync } from 'fs'
27
+ import { homedir } from 'os'
28
+
29
+ const __dirname = dirname(fileURLToPath(import.meta.url))
30
+ const ROOT = resolve(__dirname, '..')
31
+
32
+ // ============================================================================
33
+ // Health Checks
34
+ // ============================================================================
35
+
36
+ function checkJq() {
37
+ try {
38
+ execSync('which jq', { stdio: 'ignore' })
39
+ return true
40
+ } catch {
41
+ return false
42
+ }
43
+ }
44
+
45
+ function checkTmux() {
46
+ try {
47
+ execSync('which tmux', { stdio: 'ignore' })
48
+ return true
49
+ } catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ function checkHooksConfigured() {
55
+ const settingsPath = join(homedir(), '.claude', 'settings.json')
56
+ if (!existsSync(settingsPath)) {
57
+ return { configured: false, reason: 'no settings file' }
58
+ }
59
+
60
+ try {
61
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'))
62
+ const hooks = settings.hooks || {}
63
+ const hasPreToolUse = hooks.PreToolUse?.some(h =>
64
+ h.hooks?.some(hh => hh.command?.includes('vibecraft-hook'))
65
+ )
66
+ const hasPostToolUse = hooks.PostToolUse?.some(h =>
67
+ h.hooks?.some(hh => hh.command?.includes('vibecraft-hook'))
68
+ )
69
+
70
+ if (hasPreToolUse && hasPostToolUse) {
71
+ return { configured: true }
72
+ }
73
+ return { configured: false, reason: 'hooks not found in settings' }
74
+ } catch (e) {
75
+ return { configured: false, reason: 'failed to parse settings' }
76
+ }
77
+ }
78
+
79
+ function printHealthCheck() {
80
+ const jqOk = checkJq()
81
+ const tmuxOk = checkTmux()
82
+ const hooksResult = checkHooksConfigured()
83
+
84
+ let warnings = []
85
+
86
+ if (!jqOk) {
87
+ warnings.push(` [!] jq not found - hooks won't work without it
88
+ Install: brew install jq (macOS) or apt install jq (Linux)`)
89
+ }
90
+
91
+ if (!tmuxOk) {
92
+ warnings.push(` [!] tmux not found - session management won't work
93
+ Install: brew install tmux (macOS) or apt install tmux (Linux)`)
94
+ }
95
+
96
+ if (!hooksResult.configured) {
97
+ warnings.push(` [!] Hooks not configured - events won't be captured
98
+ Run: npx vibecraft setup
99
+ Then restart Claude Code`)
100
+ }
101
+
102
+ if (warnings.length > 0) {
103
+ console.log('\n Warnings:')
104
+ warnings.forEach(w => console.log(w))
105
+ console.log()
106
+ }
107
+ }
108
+
109
+ // Parse arguments
110
+ const args = process.argv.slice(2)
111
+
112
+ if (args.includes('--help') || args.includes('-h')) {
113
+ console.log(`
114
+ vibecraft - 3D visualization for Claude Code
115
+
116
+ Usage:
117
+ vibecraft [options]
118
+ vibecraft setup Configure Claude Code hooks automatically
119
+ vibecraft uninstall Remove vibecraft hooks (keeps your data)
120
+ vibecraft doctor Diagnose common issues
121
+
122
+ Options:
123
+ --port, -p <port> WebSocket server port (default: 4003)
124
+ --help, -h Show this help message
125
+ --version, -v Show version
126
+ --hook-path Print path to hook script (for manual setup)
127
+
128
+ Environment Variables:
129
+ VIBECRAFT_PORT WebSocket server port (default: 4003)
130
+ VIBECRAFT_DEBUG Enable debug logging (true/false)
131
+
132
+ Setup:
133
+ 1. Run: vibecraft setup
134
+ 2. Start server: vibecraft
135
+ 3. Open frontend in browser
136
+
137
+ Website: https://vibecraft.sh
138
+ GitHub: https://github.com/nearcyan/vibecraft
139
+ `)
140
+ process.exit(0)
141
+ }
142
+
143
+ // Hook path command
144
+ if (args.includes('--hook-path')) {
145
+ console.log(resolve(ROOT, 'hooks/vibecraft-hook.sh'))
146
+ process.exit(0)
147
+ }
148
+
149
+ // Setup command
150
+ if (args[0] === 'setup') {
151
+ const { writeFileSync, copyFileSync, chmodSync } = await import('fs')
152
+
153
+ console.log('Setting up vibecraft hooks...\n')
154
+
155
+ // ==========================================================================
156
+ // Step 1: Find Claude Code settings
157
+ // ==========================================================================
158
+
159
+ // Possible locations for Claude settings (in order of preference)
160
+ const possibleSettingsPaths = [
161
+ join(homedir(), '.claude', 'settings.json'), // Standard location
162
+ join(homedir(), '.config', 'claude', 'settings.json'), // XDG config
163
+ ]
164
+
165
+ let settingsPath = null
166
+ let settingsDir = null
167
+
168
+ // Find existing settings file
169
+ for (const path of possibleSettingsPaths) {
170
+ if (existsSync(path)) {
171
+ settingsPath = path
172
+ settingsDir = dirname(path)
173
+ break
174
+ }
175
+ }
176
+
177
+ // If no settings file found, use default location
178
+ if (!settingsPath) {
179
+ settingsPath = possibleSettingsPaths[0]
180
+ settingsDir = dirname(settingsPath)
181
+ }
182
+
183
+ console.log(`Claude settings: ${settingsPath}`)
184
+
185
+ // ==========================================================================
186
+ // Step 2: Install hook script to ~/.vibecraft/hooks/
187
+ // ==========================================================================
188
+
189
+ const vibecraftHooksDir = join(homedir(), '.vibecraft', 'hooks')
190
+ const installedHookPath = join(vibecraftHooksDir, 'vibecraft-hook.sh')
191
+ const sourceHookPath = resolve(ROOT, 'hooks/vibecraft-hook.sh')
192
+
193
+ // Ensure hooks directory exists
194
+ if (!existsSync(vibecraftHooksDir)) {
195
+ mkdirSync(vibecraftHooksDir, { recursive: true })
196
+ console.log(`Created ${vibecraftHooksDir}`)
197
+ }
198
+
199
+ // Copy hook script
200
+ if (!existsSync(sourceHookPath)) {
201
+ console.error(`ERROR: Hook script not found at ${sourceHookPath}`)
202
+ console.error('This is a bug - please report it.')
203
+ process.exit(1)
204
+ }
205
+
206
+ try {
207
+ copyFileSync(sourceHookPath, installedHookPath)
208
+ chmodSync(installedHookPath, 0o755) // Make executable
209
+ console.log(`Installed hook: ${installedHookPath}`)
210
+ } catch (e) {
211
+ console.error(`ERROR: Failed to install hook script: ${e.message}`)
212
+ process.exit(1)
213
+ }
214
+
215
+ // ==========================================================================
216
+ // Step 3: Ensure data directory exists
217
+ // ==========================================================================
218
+
219
+ const dataDir = join(homedir(), '.vibecraft', 'data')
220
+ if (!existsSync(dataDir)) {
221
+ mkdirSync(dataDir, { recursive: true })
222
+ console.log(`Created ${dataDir}`)
223
+ }
224
+
225
+ // ==========================================================================
226
+ // Step 4: Configure Claude Code settings
227
+ // ==========================================================================
228
+
229
+ // Ensure settings directory exists
230
+ if (!existsSync(settingsDir)) {
231
+ mkdirSync(settingsDir, { recursive: true })
232
+ console.log(`Created ${settingsDir}`)
233
+ }
234
+
235
+ // Load or create settings
236
+ let settings = {}
237
+ if (existsSync(settingsPath)) {
238
+ try {
239
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'))
240
+ // Backup existing settings
241
+ const backupPath = `${settingsPath}.backup-${Date.now()}`
242
+ writeFileSync(backupPath, JSON.stringify(settings, null, 2))
243
+ console.log(`Backed up settings: ${backupPath}`)
244
+ } catch (e) {
245
+ console.error(`ERROR: Failed to parse ${settingsPath}: ${e.message}`)
246
+ console.error('Please fix the JSON syntax and try again.')
247
+ process.exit(1)
248
+ }
249
+ }
250
+
251
+ // Hook configurations - use installed path (stable location)
252
+ const toolHookEntry = {
253
+ matcher: '*',
254
+ hooks: [{ type: 'command', command: installedHookPath, timeout: 5 }]
255
+ }
256
+ const genericHookEntry = {
257
+ hooks: [{ type: 'command', command: installedHookPath, timeout: 5 }]
258
+ }
259
+
260
+ // Initialize hooks object
261
+ settings.hooks = settings.hooks || {}
262
+
263
+ // Helper to add/update hooks (removes old vibecraft hooks first)
264
+ const addHook = (eventType, entry) => {
265
+ settings.hooks[eventType] = settings.hooks[eventType] || []
266
+ // Remove any existing vibecraft hooks (from any location)
267
+ settings.hooks[eventType] = settings.hooks[eventType].filter(h =>
268
+ !h.hooks?.some(hh => hh.command?.includes('vibecraft-hook'))
269
+ )
270
+ // Add new hook
271
+ settings.hooks[eventType].push(entry)
272
+ }
273
+
274
+ // Configure ALL hooks
275
+ addHook('PreToolUse', toolHookEntry)
276
+ addHook('PostToolUse', toolHookEntry)
277
+ addHook('Stop', genericHookEntry)
278
+ addHook('SubagentStop', genericHookEntry)
279
+ addHook('SessionStart', genericHookEntry)
280
+ addHook('SessionEnd', genericHookEntry)
281
+ addHook('UserPromptSubmit', genericHookEntry)
282
+ addHook('Notification', genericHookEntry)
283
+
284
+ // Write settings
285
+ try {
286
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
287
+ console.log(`Updated settings: ${settingsPath}`)
288
+ } catch (e) {
289
+ console.error(`ERROR: Failed to write settings: ${e.message}`)
290
+ process.exit(1)
291
+ }
292
+
293
+ // ==========================================================================
294
+ // Step 5: Verify and report
295
+ // ==========================================================================
296
+
297
+ console.log('\n' + '='.repeat(50))
298
+ console.log('Setup complete!')
299
+ console.log('='.repeat(50))
300
+
301
+ console.log('\nHooks configured:')
302
+ console.log(' - PreToolUse')
303
+ console.log(' - PostToolUse')
304
+ console.log(' - Stop')
305
+ console.log(' - SubagentStop')
306
+ console.log(' - SessionStart')
307
+ console.log(' - SessionEnd')
308
+ console.log(' - UserPromptSubmit')
309
+ console.log(' - Notification')
310
+
311
+ // Check dependencies
312
+ let hasWarnings = false
313
+
314
+ if (!checkJq()) {
315
+ hasWarnings = true
316
+ console.log('\n[!] Warning: jq not found')
317
+ console.log(' Install: brew install jq (macOS) or apt install jq (Linux)')
318
+ }
319
+
320
+ if (!checkTmux()) {
321
+ hasWarnings = true
322
+ console.log('\n[!] Warning: tmux not found')
323
+ console.log(' Install: brew install tmux (macOS) or apt install tmux (Linux)')
324
+ }
325
+
326
+ if (!hasWarnings) {
327
+ console.log('\nAll dependencies found!')
328
+ }
329
+
330
+ // Check if server is already running (likely an update)
331
+ let serverRunning = false
332
+ try {
333
+ const res = execSync('curl -s http://localhost:4003/health', { timeout: 2000 })
334
+ if (res.toString().includes('"ok":true')) {
335
+ serverRunning = true
336
+ }
337
+ } catch {}
338
+
339
+ if (serverRunning) {
340
+ // Update scenario
341
+ console.log('\nTo complete the update:')
342
+ console.log(' 1. Restart vibecraft server (Ctrl+C, then run: npx vibecraft)')
343
+ console.log(' 2. Restart Claude Code (for hook changes to take effect)')
344
+ console.log(' 3. Refresh browser\n')
345
+ } else {
346
+ // Fresh install scenario
347
+ console.log('\nNext steps:')
348
+ console.log(' 1. Restart Claude Code (required for hooks to take effect)')
349
+ console.log(' 2. Run: npx vibecraft')
350
+ console.log(' 3. Open http://localhost:4003 in your browser\n')
351
+ }
352
+
353
+ process.exit(0)
354
+ }
355
+
356
+ // Uninstall command
357
+ if (args[0] === 'uninstall') {
358
+ const { writeFileSync, rmSync } = await import('fs')
359
+
360
+ console.log('Uninstalling vibecraft hooks...\n')
361
+
362
+ // ==========================================================================
363
+ // Step 1: Find Claude Code settings
364
+ // ==========================================================================
365
+
366
+ const possibleSettingsPaths = [
367
+ join(homedir(), '.claude', 'settings.json'),
368
+ join(homedir(), '.config', 'claude', 'settings.json'),
369
+ ]
370
+
371
+ let settingsPath = null
372
+ for (const path of possibleSettingsPaths) {
373
+ if (existsSync(path)) {
374
+ settingsPath = path
375
+ break
376
+ }
377
+ }
378
+
379
+ if (!settingsPath) {
380
+ console.log('No Claude settings file found - nothing to uninstall.')
381
+ process.exit(0)
382
+ }
383
+
384
+ console.log(`Claude settings: ${settingsPath}`)
385
+
386
+ // ==========================================================================
387
+ // Step 2: Remove vibecraft hooks from settings
388
+ // ==========================================================================
389
+
390
+ let settings
391
+ try {
392
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'))
393
+ } catch (e) {
394
+ console.error(`ERROR: Failed to parse ${settingsPath}: ${e.message}`)
395
+ process.exit(1)
396
+ }
397
+
398
+ if (!settings.hooks) {
399
+ console.log('No hooks configured - nothing to uninstall.')
400
+ process.exit(0)
401
+ }
402
+
403
+ // Backup before modifying
404
+ const backupPath = `${settingsPath}.backup-${Date.now()}`
405
+ writeFileSync(backupPath, JSON.stringify(settings, null, 2))
406
+ console.log(`Backed up settings: ${backupPath}`)
407
+
408
+ // Remove vibecraft hooks from each event type
409
+ const hookTypes = [
410
+ 'PreToolUse', 'PostToolUse', 'Stop', 'SubagentStop',
411
+ 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'Notification'
412
+ ]
413
+
414
+ let removedCount = 0
415
+ for (const hookType of hookTypes) {
416
+ if (!settings.hooks[hookType]) continue
417
+
418
+ const before = settings.hooks[hookType].length
419
+ settings.hooks[hookType] = settings.hooks[hookType].filter(h =>
420
+ !h.hooks?.some(hh => hh.command?.includes('vibecraft-hook'))
421
+ )
422
+ const after = settings.hooks[hookType].length
423
+
424
+ if (before !== after) {
425
+ removedCount += (before - after)
426
+ console.log(` Removed vibecraft hook from ${hookType}`)
427
+ }
428
+
429
+ // Clean up empty arrays
430
+ if (settings.hooks[hookType].length === 0) {
431
+ delete settings.hooks[hookType]
432
+ }
433
+ }
434
+
435
+ // Clean up empty hooks object
436
+ if (Object.keys(settings.hooks).length === 0) {
437
+ delete settings.hooks
438
+ }
439
+
440
+ if (removedCount === 0) {
441
+ console.log('No vibecraft hooks found - nothing to remove.')
442
+ } else {
443
+ // Write updated settings
444
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
445
+ console.log(`\nRemoved ${removedCount} hook(s) from settings.`)
446
+ }
447
+
448
+ // ==========================================================================
449
+ // Step 3: Remove hook script (but keep data)
450
+ // ==========================================================================
451
+
452
+ const hookScript = join(homedir(), '.vibecraft', 'hooks', 'vibecraft-hook.sh')
453
+ if (existsSync(hookScript)) {
454
+ rmSync(hookScript)
455
+ console.log(`Removed: ${hookScript}`)
456
+ }
457
+
458
+ // Remove hooks directory if empty
459
+ const hooksDir = join(homedir(), '.vibecraft', 'hooks')
460
+ if (existsSync(hooksDir)) {
461
+ try {
462
+ const { readdirSync } = await import('fs')
463
+ if (readdirSync(hooksDir).length === 0) {
464
+ rmSync(hooksDir, { recursive: true })
465
+ console.log(`Removed empty directory: ${hooksDir}`)
466
+ }
467
+ } catch {}
468
+ }
469
+
470
+ // ==========================================================================
471
+ // Done
472
+ // ==========================================================================
473
+
474
+ console.log('\n' + '='.repeat(50))
475
+ console.log('Uninstall complete!')
476
+ console.log('='.repeat(50))
477
+
478
+ console.log('\nVibecraft hooks have been removed.')
479
+ console.log('Your data is preserved in ~/.vibecraft/data/')
480
+ console.log('\nTo remove all data:')
481
+ console.log(' rm -rf ~/.vibecraft')
482
+ console.log('\nRestart Claude Code for changes to take effect.\n')
483
+
484
+ process.exit(0)
485
+ }
486
+
487
+ // Doctor command - diagnose common issues
488
+ if (args[0] === 'doctor') {
489
+ console.log('='.repeat(50))
490
+ console.log('Vibecraft Doctor - Diagnosing your setup...')
491
+ console.log('='.repeat(50))
492
+ console.log()
493
+
494
+ let issues = []
495
+ let warnings = []
496
+
497
+ // -------------------------------------------------------------------------
498
+ // 1. Check dependencies
499
+ // -------------------------------------------------------------------------
500
+ console.log('[1/6] Checking dependencies...')
501
+
502
+ // Node version
503
+ const nodeVersion = process.version
504
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0])
505
+ if (nodeMajor >= 18) {
506
+ console.log(` ✓ Node.js ${nodeVersion}`)
507
+ } else {
508
+ console.log(` ✗ Node.js ${nodeVersion} (need 18+)`)
509
+ issues.push('Node.js 18+ required')
510
+ }
511
+
512
+ // jq
513
+ if (checkJq()) {
514
+ try {
515
+ const jqVersion = execSync('jq --version 2>&1', { encoding: 'utf-8' }).trim()
516
+ console.log(` ✓ jq (${jqVersion})`)
517
+ } catch {
518
+ console.log(' ✓ jq')
519
+ }
520
+ } else {
521
+ console.log(' ✗ jq not found')
522
+ issues.push('jq not installed - hooks will not work')
523
+ }
524
+
525
+ // tmux
526
+ if (checkTmux()) {
527
+ try {
528
+ const tmuxVersion = execSync('tmux -V 2>&1', { encoding: 'utf-8' }).trim()
529
+ console.log(` ✓ tmux (${tmuxVersion})`)
530
+ } catch {
531
+ console.log(' ✓ tmux')
532
+ }
533
+ } else {
534
+ console.log(' ⚠ tmux not found (optional - needed for browser prompts)')
535
+ warnings.push('tmux not installed - browser prompt feature won\'t work')
536
+ }
537
+
538
+ // curl
539
+ try {
540
+ execSync('which curl', { stdio: 'ignore' })
541
+ console.log(' ✓ curl')
542
+ } catch {
543
+ console.log(' ✗ curl not found')
544
+ issues.push('curl not installed - hooks cannot send events to server')
545
+ }
546
+
547
+ // -------------------------------------------------------------------------
548
+ // 2. Check hook script
549
+ // -------------------------------------------------------------------------
550
+ console.log('\n[2/6] Checking hook script...')
551
+
552
+ const hookScript = join(homedir(), '.vibecraft', 'hooks', 'vibecraft-hook.sh')
553
+ if (existsSync(hookScript)) {
554
+ console.log(` ✓ Hook script exists: ${hookScript}`)
555
+
556
+ // Check if executable
557
+ try {
558
+ const { accessSync, constants } = await import('fs')
559
+ accessSync(hookScript, constants.X_OK)
560
+ console.log(' ✓ Hook script is executable')
561
+ } catch {
562
+ console.log(' ✗ Hook script is not executable')
563
+ issues.push(`Hook script not executable. Run: chmod +x ${hookScript}`)
564
+ }
565
+ } else {
566
+ console.log(` ✗ Hook script not found: ${hookScript}`)
567
+ issues.push('Hook script not installed. Run: npx vibecraft setup')
568
+ }
569
+
570
+ // -------------------------------------------------------------------------
571
+ // 3. Check Claude settings
572
+ // -------------------------------------------------------------------------
573
+ console.log('\n[3/6] Checking Claude Code settings...')
574
+
575
+ const settingsPaths = [
576
+ join(homedir(), '.claude', 'settings.json'),
577
+ join(homedir(), '.config', 'claude', 'settings.json'),
578
+ ]
579
+
580
+ let settingsPath = null
581
+ for (const p of settingsPaths) {
582
+ if (existsSync(p)) {
583
+ settingsPath = p
584
+ break
585
+ }
586
+ }
587
+
588
+ if (!settingsPath) {
589
+ console.log(' ✗ No Claude settings file found')
590
+ issues.push('Claude settings not found. Run: npx vibecraft setup')
591
+ } else {
592
+ console.log(` ✓ Settings file: ${settingsPath}`)
593
+
594
+ try {
595
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'))
596
+ const hooks = settings.hooks || {}
597
+
598
+ const hookTypes = ['PreToolUse', 'PostToolUse', 'Stop', 'SubagentStop',
599
+ 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'Notification']
600
+
601
+ let configuredHooks = []
602
+ let missingHooks = []
603
+
604
+ for (const hookType of hookTypes) {
605
+ const hasVibecraft = hooks[hookType]?.some(h =>
606
+ h.hooks?.some(hh => hh.command?.includes('vibecraft-hook'))
607
+ )
608
+ if (hasVibecraft) {
609
+ configuredHooks.push(hookType)
610
+ } else {
611
+ missingHooks.push(hookType)
612
+ }
613
+ }
614
+
615
+ if (configuredHooks.length === hookTypes.length) {
616
+ console.log(` ✓ All ${hookTypes.length} hooks configured`)
617
+ } else if (configuredHooks.length > 0) {
618
+ console.log(` ⚠ ${configuredHooks.length}/${hookTypes.length} hooks configured`)
619
+ console.log(` Missing: ${missingHooks.join(', ')}`)
620
+ warnings.push(`Some hooks not configured: ${missingHooks.join(', ')}`)
621
+ } else {
622
+ console.log(' ✗ No vibecraft hooks configured')
623
+ issues.push('Hooks not configured. Run: npx vibecraft setup')
624
+ }
625
+ } catch (e) {
626
+ console.log(` ✗ Failed to parse settings: ${e.message}`)
627
+ issues.push('Claude settings file has invalid JSON')
628
+ }
629
+ }
630
+
631
+ // -------------------------------------------------------------------------
632
+ // 4. Check data directory
633
+ // -------------------------------------------------------------------------
634
+ console.log('\n[4/6] Checking data directory...')
635
+
636
+ const dataDir = join(homedir(), '.vibecraft', 'data')
637
+ if (existsSync(dataDir)) {
638
+ console.log(` ✓ Data directory exists: ${dataDir}`)
639
+
640
+ // Check events file
641
+ const eventsFile = join(dataDir, 'events.jsonl')
642
+ if (existsSync(eventsFile)) {
643
+ const { statSync } = await import('fs')
644
+ const stats = statSync(eventsFile)
645
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(2)
646
+ const modifiedAgo = Math.round((Date.now() - stats.mtimeMs) / 1000)
647
+
648
+ let timeAgo
649
+ if (modifiedAgo < 60) timeAgo = `${modifiedAgo}s ago`
650
+ else if (modifiedAgo < 3600) timeAgo = `${Math.round(modifiedAgo/60)}m ago`
651
+ else if (modifiedAgo < 86400) timeAgo = `${Math.round(modifiedAgo/3600)}h ago`
652
+ else timeAgo = `${Math.round(modifiedAgo/86400)}d ago`
653
+
654
+ console.log(` ✓ Events file: ${sizeMB} MB, last modified ${timeAgo}`)
655
+
656
+ if (modifiedAgo > 86400) {
657
+ warnings.push('No events in 24+ hours - hooks may not be firing')
658
+ }
659
+ } else {
660
+ console.log(' ⚠ No events file yet (will be created when hooks fire)')
661
+ }
662
+ } else {
663
+ console.log(` ✗ Data directory not found: ${dataDir}`)
664
+ issues.push('Data directory not created. Run: npx vibecraft setup')
665
+ }
666
+
667
+ // -------------------------------------------------------------------------
668
+ // 5. Check server status
669
+ // -------------------------------------------------------------------------
670
+ console.log('\n[5/6] Checking server status...')
671
+
672
+ try {
673
+ const healthRes = execSync('curl -s http://localhost:4003/health', {
674
+ timeout: 3000,
675
+ encoding: 'utf-8'
676
+ })
677
+ const health = JSON.parse(healthRes)
678
+ if (health.ok) {
679
+ console.log(` ✓ Server running on port 4003`)
680
+ console.log(` Version: ${health.version || 'unknown'}`)
681
+ console.log(` Clients: ${health.clients || 0}`)
682
+ console.log(` Events: ${health.events || 0}`)
683
+ }
684
+ } catch {
685
+ console.log(' ⚠ Server not running on port 4003')
686
+ warnings.push('Server not running. Start with: npx vibecraft')
687
+ }
688
+
689
+ // -------------------------------------------------------------------------
690
+ // 6. Check tmux sessions
691
+ // -------------------------------------------------------------------------
692
+ console.log('\n[6/6] Checking tmux sessions...')
693
+
694
+ if (checkTmux()) {
695
+ try {
696
+ const sessions = execSync('tmux list-sessions 2>/dev/null', {
697
+ encoding: 'utf-8',
698
+ timeout: 2000
699
+ }).trim()
700
+
701
+ if (sessions) {
702
+ const sessionList = sessions.split('\n')
703
+ console.log(` ✓ ${sessionList.length} tmux session(s) found:`)
704
+ sessionList.forEach(s => console.log(` - ${s.split(':')[0]}`))
705
+
706
+ const hasClaude = sessionList.some(s => s.startsWith('claude:'))
707
+ if (!hasClaude) {
708
+ console.log(' ⚠ No "claude" session (browser prompts target this by default)')
709
+ }
710
+ } else {
711
+ console.log(' ⚠ No tmux sessions running')
712
+ }
713
+ } catch {
714
+ console.log(' ⚠ No tmux sessions running')
715
+ }
716
+ } else {
717
+ console.log(' - Skipped (tmux not installed)')
718
+ }
719
+
720
+ // -------------------------------------------------------------------------
721
+ // Summary
722
+ // -------------------------------------------------------------------------
723
+ console.log('\n' + '='.repeat(50))
724
+
725
+ if (issues.length === 0 && warnings.length === 0) {
726
+ console.log('✓ All checks passed! Vibecraft should be working.')
727
+ } else {
728
+ if (issues.length > 0) {
729
+ console.log(`✗ ${issues.length} issue(s) found:\n`)
730
+ issues.forEach((issue, i) => console.log(` ${i + 1}. ${issue}`))
731
+ }
732
+ if (warnings.length > 0) {
733
+ console.log(`\n⚠ ${warnings.length} warning(s):\n`)
734
+ warnings.forEach((warning, i) => console.log(` ${i + 1}. ${warning}`))
735
+ }
736
+ }
737
+
738
+ console.log('\n' + '='.repeat(50))
739
+ console.log()
740
+
741
+ process.exit(issues.length > 0 ? 1 : 0)
742
+ }
743
+
744
+ if (args.includes('--version') || args.includes('-v')) {
745
+ const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'))
746
+ console.log(`vibecraft v${pkg.version}`)
747
+ process.exit(0)
748
+ }
749
+
750
+ // Parse port from args
751
+ let port = process.env.VIBECRAFT_PORT || '4003'
752
+ const portIdx = args.findIndex(a => a === '--port' || a === '-p')
753
+ if (portIdx !== -1 && args[portIdx + 1]) {
754
+ port = args[portIdx + 1]
755
+ }
756
+
757
+ // Ensure data directory exists
758
+ const dataDir = resolve(ROOT, 'data')
759
+ if (!existsSync(dataDir)) {
760
+ mkdirSync(dataDir, { recursive: true })
761
+ }
762
+
763
+ // Banner
764
+ console.log(`
765
+ ╭─────────────────────────────────────╮
766
+ │ │
767
+ │ vibecraft │
768
+ │ 3D visualization for Claude Code │
769
+ │ │
770
+ ╰─────────────────────────────────────╯
771
+ `)
772
+
773
+ // Run health checks
774
+ printHealthCheck()
775
+
776
+ console.log(`Starting server on port ${port}...`)
777
+ console.log(`Open http://localhost:${port} in your browser`)
778
+ console.log()
779
+
780
+ // Check for compiled JS (npm package) or fall back to tsx (dev)
781
+ const compiledPath = resolve(ROOT, 'dist/server/server/index.js')
782
+ const sourcePath = resolve(ROOT, 'server/index.ts')
783
+
784
+ let server
785
+ if (existsSync(compiledPath)) {
786
+ // Use compiled JS (production/npm install)
787
+ server = spawn('node', [compiledPath], {
788
+ cwd: ROOT,
789
+ env: {
790
+ ...process.env,
791
+ VIBECRAFT_PORT: port,
792
+ },
793
+ stdio: 'inherit',
794
+ })
795
+ } else {
796
+ // Fall back to tsx (development)
797
+ console.log('(dev mode - using tsx)')
798
+ server = spawn('npx', ['tsx', sourcePath], {
799
+ cwd: ROOT,
800
+ env: {
801
+ ...process.env,
802
+ VIBECRAFT_PORT: port,
803
+ },
804
+ stdio: 'inherit',
805
+ })
806
+ }
807
+
808
+ server.on('error', (err) => {
809
+ console.error('Failed to start server:', err.message)
810
+ process.exit(1)
811
+ })
812
+
813
+ server.on('close', (code) => {
814
+ process.exit(code || 0)
815
+ })
816
+
817
+ // Handle signals
818
+ process.on('SIGINT', () => {
819
+ server.kill('SIGINT')
820
+ })
821
+
822
+ process.on('SIGTERM', () => {
823
+ server.kill('SIGTERM')
824
+ })