vibeteam 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,832 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Vibeshop CLI - 3D visualization for Claude Code
5
+ *
6
+ * Usage:
7
+ * npx vibeteam # Start the server
8
+ * npx vibeteam --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 vibeteam 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('vibeteam-hook'))
65
+ )
66
+ const hasPostToolUse = hooks.PostToolUse?.some(h =>
67
+ h.hooks?.some(hh => hh.command?.includes('vibeteam-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 vibeteam 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
+ vibeteam - 3D visualization for Claude Code
115
+
116
+ Usage:
117
+ vibeteam [options]
118
+ vibeteam setup Configure Claude Code hooks automatically
119
+ vibeteam uninstall Remove vibeteam hooks (keeps your data)
120
+ vibeteam 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
+ VIBETEAM_PORT WebSocket server port (default: 4003)
130
+ VIBETEAM_DEBUG Enable debug logging (true/false)
131
+
132
+ Setup:
133
+ 1. Run: vibeteam setup
134
+ 2. Start server: vibeteam
135
+ 3. Open frontend in browser
136
+
137
+ Website: https://vibeteam.sh
138
+ GitHub: https://github.com/Elysian-Labs/vibeteam
139
+ `)
140
+ process.exit(0)
141
+ }
142
+
143
+ // Hook path command
144
+ if (args.includes('--hook-path')) {
145
+ console.log(resolve(ROOT, 'hooks/vibeteam-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 vibeteam 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 ~/.vibeteam/hooks/
187
+ // ==========================================================================
188
+
189
+ const vibeteamHooksDir = join(homedir(), '.vibeteam', 'hooks')
190
+ const installedHookPath = join(vibeteamHooksDir, 'vibeteam-hook.sh')
191
+ const sourceHookPath = resolve(ROOT, 'hooks/vibeteam-hook.sh')
192
+
193
+ // Ensure hooks directory exists
194
+ if (!existsSync(vibeteamHooksDir)) {
195
+ mkdirSync(vibeteamHooksDir, { recursive: true })
196
+ console.log(`Created ${vibeteamHooksDir}`)
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(), '.vibeteam', '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 vibeteam hooks first)
264
+ const addHook = (eventType, entry) => {
265
+ settings.hooks[eventType] = settings.hooks[eventType] || []
266
+ // Remove any existing vibeteam hooks (from any location)
267
+ settings.hooks[eventType] = settings.hooks[eventType].filter(h =>
268
+ !h.hooks?.some(hh => hh.command?.includes('vibeteam-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 vibeteam server (Ctrl+C, then run: npx vibeteam)')
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 vibeteam')
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 vibeteam 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 vibeteam 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 vibeteam 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('vibeteam-hook'))
421
+ )
422
+ const after = settings.hooks[hookType].length
423
+
424
+ if (before !== after) {
425
+ removedCount += (before - after)
426
+ console.log(` Removed vibeteam 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 vibeteam 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(), '.vibeteam', 'hooks', 'vibeteam-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(), '.vibeteam', '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('\nVibTeam hooks have been removed.')
479
+ console.log('Your data is preserved in ~/.vibeteam/data/')
480
+ console.log('\nTo remove all data:')
481
+ console.log(' rm -rf ~/.vibeteam')
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('VibTeam 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(), '.vibeteam', 'hooks', 'vibeteam-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 vibeteam 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 vibeteam 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 hasVibeteam = hooks[hookType]?.some(h =>
606
+ h.hooks?.some(hh => hh.command?.includes('vibeteam-hook'))
607
+ )
608
+ if (hasVibeteam) {
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 vibeteam hooks configured')
623
+ issues.push('Hooks not configured. Run: npx vibeteam 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(), '.vibeteam', '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 vibeteam 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 vibeteam')
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! VibTeam 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(`vibeteam v${pkg.version}`)
747
+ process.exit(0)
748
+ }
749
+
750
+ // Parse port from args
751
+ let port = process.env.VIBETEAM_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
+ // Check if bundled frontend exists
764
+ const hasBundledFrontend = existsSync(resolve(ROOT, 'public/index.html'))
765
+
766
+ // Banner
767
+ console.log(`
768
+ ╭─────────────────────────────────────╮
769
+ │ │
770
+ │ vibeteam │
771
+ │ 3D visualization for Claude Code │
772
+ │ │
773
+ ╰─────────────────────────────────────╯
774
+ `)
775
+
776
+ // Run health checks
777
+ printHealthCheck()
778
+
779
+ console.log(`Starting server on port ${port}...`)
780
+ if (hasBundledFrontend) {
781
+ console.log(`Open http://localhost:${port} in your browser`)
782
+ } else {
783
+ console.log(`Backend API: http://localhost:${port}`)
784
+ console.log(`Frontend: http://localhost:5173 (start with: npm run dev)`)
785
+ }
786
+ console.log()
787
+
788
+ // Check for compiled JS (npm package) or fall back to tsx (dev)
789
+ const compiledPath = resolve(ROOT, 'dist/server/server/index.js')
790
+ const sourcePath = resolve(ROOT, 'server/index.ts')
791
+
792
+ let server
793
+ if (existsSync(compiledPath)) {
794
+ // Use compiled JS (production/npm install)
795
+ server = spawn('node', [compiledPath], {
796
+ cwd: ROOT,
797
+ env: {
798
+ ...process.env,
799
+ VIBETEAM_PORT: port,
800
+ },
801
+ stdio: 'inherit',
802
+ })
803
+ } else {
804
+ // Fall back to tsx (development)
805
+ console.log('(dev mode - using tsx)')
806
+ server = spawn('npx', ['tsx', sourcePath], {
807
+ cwd: ROOT,
808
+ env: {
809
+ ...process.env,
810
+ VIBETEAM_PORT: port,
811
+ },
812
+ stdio: 'inherit',
813
+ })
814
+ }
815
+
816
+ server.on('error', (err) => {
817
+ console.error('Failed to start server:', err.message)
818
+ process.exit(1)
819
+ })
820
+
821
+ server.on('close', (code) => {
822
+ process.exit(code || 0)
823
+ })
824
+
825
+ // Handle signals
826
+ process.on('SIGINT', () => {
827
+ server.kill('SIGINT')
828
+ })
829
+
830
+ process.on('SIGTERM', () => {
831
+ server.kill('SIGTERM')
832
+ })