create-claudeportal 0.1.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.
Files changed (39) hide show
  1. package/bin/cli.js +37 -0
  2. package/dist/assets/index-BBU5K5iA.js +132 -0
  3. package/dist/assets/index-fNmv07eE.css +1 -0
  4. package/dist/index.html +13 -0
  5. package/index.html +12 -0
  6. package/mockups/01-chat-conversation-v2.html +803 -0
  7. package/mockups/01-chat-conversation.html +592 -0
  8. package/mockups/02-activity-feed.html +648 -0
  9. package/mockups/03-focused-workspace.html +680 -0
  10. package/mockups/04-documents-mode.html +1556 -0
  11. package/package.json +54 -0
  12. package/server/index.js +140 -0
  13. package/server/lib/detect-tools.js +93 -0
  14. package/server/lib/file-scanner.js +46 -0
  15. package/server/lib/file-watcher.js +45 -0
  16. package/server/lib/fix-npm-prefix.js +61 -0
  17. package/server/lib/folder-scanner.js +43 -0
  18. package/server/lib/install-tools.js +122 -0
  19. package/server/lib/platform.js +18 -0
  20. package/server/lib/sse-manager.js +36 -0
  21. package/server/lib/terminal.js +95 -0
  22. package/server/lib/validate-folder-path.js +17 -0
  23. package/server/lib/validate-path.js +13 -0
  24. package/server/routes/detect.js +64 -0
  25. package/server/routes/doc-events.js +94 -0
  26. package/server/routes/events.js +37 -0
  27. package/server/routes/folder.js +195 -0
  28. package/server/routes/github.js +21 -0
  29. package/server/routes/health.js +16 -0
  30. package/server/routes/install.js +102 -0
  31. package/server/routes/project.js +18 -0
  32. package/server/routes/scaffold.js +45 -0
  33. package/skills-lock.json +15 -0
  34. package/tsconfig.app.json +17 -0
  35. package/tsconfig.node.json +11 -0
  36. package/tsconfig.tsbuildinfo +1 -0
  37. package/ui/app.js +747 -0
  38. package/ui/index.html +272 -0
  39. package/ui/styles.css +788 -0
package/ui/app.js ADDED
@@ -0,0 +1,747 @@
1
+ // ============================================
2
+ // GREENHOUSE SETUP WIZARD — Client
3
+ // ============================================
4
+
5
+ let appState = {
6
+ currentScreen: 'setup',
7
+ tools: [],
8
+ platform: null,
9
+ ptyAvailable: false,
10
+ projectDir: null,
11
+ projectName: null,
12
+ terminal: null,
13
+ ws: null,
14
+ intentDescription: null,
15
+ claudeReady: false,
16
+ pendingPrompt: null,
17
+ firstBuildDone: false,
18
+ }
19
+
20
+ // ============================================
21
+ // INIT
22
+ // ============================================
23
+
24
+ document.addEventListener('DOMContentLoaded', async () => {
25
+ // Get app info
26
+ const info = await fetchJson('/api/info')
27
+ if (info) {
28
+ appState.ptyAvailable = info.ptyAvailable
29
+ }
30
+
31
+ // Restore saved state
32
+ const saved = localStorage.getItem('greenhouse-setup')
33
+ let savedState = null
34
+ if (saved) {
35
+ try {
36
+ savedState = JSON.parse(saved)
37
+ appState.projectDir = savedState.projectDir || null
38
+ } catch {}
39
+ }
40
+
41
+ // Set up event listeners
42
+ setupListeners()
43
+
44
+ // If setup was already completed, skip straight to build screen
45
+ if (savedState?.setupComplete) {
46
+ showScreen('build')
47
+ return
48
+ }
49
+
50
+ // Otherwise run tool detection
51
+ await detectTools()
52
+ })
53
+
54
+ // ============================================
55
+ // SCREEN NAVIGATION
56
+ // ============================================
57
+
58
+ function showScreen(name) {
59
+ document.querySelectorAll('.screen').forEach((s) => s.classList.remove('active'))
60
+ document.getElementById(`screen-${name}`).classList.add('active')
61
+ appState.currentScreen = name
62
+
63
+ if (name === 'build') {
64
+ saveState(true)
65
+ // Show sidebar immediately for existing projects
66
+ if (appState.firstBuildDone) {
67
+ document.getElementById('build-sidebar').classList.remove('hidden')
68
+ document.getElementById('sidebar-reopen').classList.add('hidden')
69
+ }
70
+ initTerminal()
71
+ }
72
+ }
73
+
74
+ function saveState(setupComplete = false) {
75
+ localStorage.setItem('greenhouse-setup', JSON.stringify({
76
+ setupComplete,
77
+ projectDir: appState.projectDir,
78
+ }))
79
+ }
80
+
81
+ // ============================================
82
+ // SCREEN 1: TOOL DETECTION
83
+ // ============================================
84
+
85
+ async function detectTools() {
86
+ const list = document.getElementById('tools-list')
87
+ list.innerHTML = '<div class="tool-item loading"><span class="tool-status">⏳</span><span class="tool-name">Scanning your machine...</span></div>'
88
+
89
+ const data = await fetchJson('/api/detect')
90
+ if (!data) {
91
+ list.innerHTML = '<div class="tool-item error"><span class="tool-status">❌</span><span class="tool-name">Failed to connect to the setup server. Please refresh.</span></div>'
92
+ return
93
+ }
94
+
95
+ appState.tools = data.tools
96
+ appState.platform = data.platform
97
+ appState.drives = data.drives || []
98
+
99
+ renderToolList()
100
+ renderDriveShortcuts()
101
+ }
102
+
103
+ function renderToolList() {
104
+ const container = document.getElementById('tools-list')
105
+ const claudeInstalled = appState.tools.find((t) => t.id === 'claude')?.installed
106
+
107
+ // Compact inline display
108
+ const items = appState.tools.map((tool) => {
109
+ const icon = tool.installed ? '✅' : (tool.required ? '❌' : '⚠️')
110
+ const label = tool.installed ? `${tool.name} ${tool.version}` : `${tool.name} — missing`
111
+ return `<span class="tool-check">${icon} ${label}</span>`
112
+ })
113
+
114
+ container.innerHTML = items.join('')
115
+
116
+ // Show install button if Claude Code is missing
117
+ if (!claudeInstalled) {
118
+ container.innerHTML += `<span class="tool-install">
119
+ <button class="btn btn-small btn-secondary" onclick="installSingle('claude')">Install Claude Code</button>
120
+ </span>`
121
+ }
122
+
123
+ // Update build button state
124
+ updateBuildButton()
125
+ }
126
+
127
+ function updateBuildButton() {
128
+ const buildBtn = document.getElementById('btn-start-building')
129
+ const descInput = document.getElementById('intent-description')
130
+ const hasDescription = descInput && descInput.value.trim().length > 0
131
+ const claudeOk = appState.tools.find((t) => t.id === 'claude')?.installed
132
+ const hasName = appState.projectName && appState.projectName.length > 0
133
+ buildBtn.disabled = !(hasDescription && claudeOk && hasName)
134
+ }
135
+
136
+ async function installSingle(toolId) {
137
+ const container = document.getElementById('tools-list')
138
+ const tool = appState.tools.find((t) => t.id === toolId)
139
+ const toolName = tool ? tool.name : toolId
140
+
141
+ // Show installing state inline
142
+ container.innerHTML = `<span class="tool-check"><span class="spinner-small"></span> Installing ${toolName}...</span>`
143
+
144
+ try {
145
+ const response = await fetch(`/api/install/${toolId}`, { method: 'POST' })
146
+ if (!response.ok || !response.body) {
147
+ throw new Error(`Install request failed (HTTP ${response.status})`)
148
+ }
149
+
150
+ const reader = response.body.getReader()
151
+ const decoder = new TextDecoder()
152
+
153
+ let buffer = ''
154
+ let failed = false
155
+ while (true) {
156
+ const { done, value } = await reader.read()
157
+ if (done) break
158
+
159
+ buffer += decoder.decode(value, { stream: true })
160
+ const lines = buffer.split('\n\n')
161
+ buffer = lines.pop() || ''
162
+
163
+ for (const line of lines) {
164
+ if (line.startsWith('data: ')) {
165
+ try {
166
+ const data = JSON.parse(line.slice(6))
167
+ if (data.status === 'error') {
168
+ failed = true
169
+ showError(toolId, data.error)
170
+ } else if (data.status === 'progress' && data.message) {
171
+ container.innerHTML = `<span class="tool-check"><span class="spinner-small"></span> ${data.message}</span>`
172
+ }
173
+ } catch (e) {
174
+ console.error('Failed to parse install event:', line, e)
175
+ }
176
+ }
177
+ }
178
+ }
179
+ } catch (err) {
180
+ console.error(`Install failed for ${toolId}:`, err)
181
+ showError(toolId, err.message)
182
+ return
183
+ }
184
+
185
+ // Re-detect to update versions and rebuild tool display
186
+ await detectTools()
187
+ }
188
+
189
+ function showError(toolId, error) {
190
+ const recovery = document.getElementById('error-recovery')
191
+ const message = document.getElementById('error-message')
192
+ const technical = document.getElementById('error-technical')
193
+
194
+ const tool = appState.tools.find((t) => t.id === toolId)
195
+ message.textContent = `${tool ? tool.name : toolId} failed to install.`
196
+ const osInfo = appState.platform ? `${appState.platform.os} ${appState.platform.arch}` : 'unknown'
197
+ technical.textContent = `Tool: ${toolId}\nError: ${error}\nOS: ${osInfo}\nTime: ${new Date().toISOString()}`
198
+
199
+ recovery.style.display = 'block'
200
+
201
+ document.getElementById('btn-retry').onclick = () => {
202
+ recovery.style.display = 'none'
203
+ installSingle(toolId)
204
+ }
205
+
206
+ document.getElementById('btn-skip').onclick = () => {
207
+ recovery.style.display = 'none'
208
+ // Mark tool as skipped so the Next button can appear
209
+ const tool = appState.tools.find((t) => t.id === toolId)
210
+ if (tool) tool.installed = true
211
+ renderToolList()
212
+ }
213
+ }
214
+
215
+ // ============================================
216
+ // START BUILDING
217
+ // ============================================
218
+
219
+ async function startBuilding() {
220
+ const projectName = appState.projectName
221
+ const description = document.getElementById('intent-description').value.trim()
222
+ if (!projectName || !description) return
223
+
224
+ const claudeInstalled = appState.tools.find((t) => t.id === 'claude')?.installed
225
+ if (!claudeInstalled) {
226
+ alert('Claude Code must be installed before you can build. Click "Install Claude Code" above.')
227
+ return
228
+ }
229
+
230
+ // Disable button while scaffolding
231
+ const buildBtn = document.getElementById('btn-start-building')
232
+ buildBtn.disabled = true
233
+ buildBtn.textContent = 'Setting up...'
234
+
235
+ // Scaffold the starter template into ~/Claude/<name>/
236
+ try {
237
+ const res = await fetch('/api/scaffold', {
238
+ method: 'POST',
239
+ headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify({ projectName }),
241
+ })
242
+ const data = await res.json()
243
+ if (!res.ok) throw new Error(data.error || 'Scaffold failed')
244
+ appState.projectDir = data.projectDir
245
+ } catch (err) {
246
+ alert('Failed to create project: ' + err.message)
247
+ buildBtn.disabled = false
248
+ buildBtn.textContent = "Let's Build"
249
+ return
250
+ }
251
+
252
+ appState.intentDescription = [
253
+ `I want to build: ${description}.`,
254
+ 'There is already a starter template in this directory with CLAUDE.md, src/ folder with React components, hooks, and utilities.',
255
+ 'Read the CLAUDE.md first to understand the architecture rules.',
256
+ 'Set up the project with Vite + React, install dependencies, and build the app using the provided component structure.',
257
+ 'Make it look polished and professional.',
258
+ 'When done, tell me to run npm run dev to preview it.',
259
+ ].join(' ')
260
+
261
+ // Update sidebar project path
262
+ document.getElementById('current-project-path').textContent = `~/Claude/${projectName}`
263
+
264
+ showScreen('build')
265
+ }
266
+
267
+ function renderDriveShortcuts() {
268
+ const container = document.getElementById('drive-shortcuts')
269
+ const list = document.getElementById('drive-list')
270
+ if (!appState.drives || !appState.drives.length) return
271
+
272
+ const icons = {
273
+ icloud: '☁️',
274
+ gdrive: '📁',
275
+ onedrive: '☁️',
276
+ dropbox: '📦',
277
+ documents: '📄',
278
+ desktop: '🖥️',
279
+ }
280
+
281
+ list.innerHTML = appState.drives.map((drive) => {
282
+ const icon = icons[drive.id] || '📂'
283
+ return `<button class="drive-btn" data-path="${drive.path}">
284
+ <span class="drive-icon">${icon}</span>${drive.name}
285
+ </button>`
286
+ }).join('')
287
+
288
+ // Click to fill the path input
289
+ list.querySelectorAll('.drive-btn').forEach((btn) => {
290
+ btn.addEventListener('click', () => {
291
+ const pathInput = document.getElementById('existing-path')
292
+ pathInput.value = btn.dataset.path
293
+ pathInput.dispatchEvent(new Event('input'))
294
+ pathInput.focus()
295
+ })
296
+ })
297
+
298
+ container.style.display = 'block'
299
+ }
300
+
301
+ function openExisting() {
302
+ const folderPath = document.getElementById('existing-path').value.trim()
303
+ const pain = document.getElementById('existing-pain').value.trim()
304
+ const outcome = document.getElementById('existing-outcome').value.trim()
305
+ if (!folderPath || !pain || !outcome) return
306
+
307
+ appState.projectDir = folderPath
308
+ appState.projectName = null
309
+
310
+ // Build a prompt from pain + outcome
311
+ appState.intentDescription = [
312
+ 'First, explore this project — read the key files and understand the codebase structure.',
313
+ `The problem I need help with: ${pain}.`,
314
+ `The ideal outcome: ${outcome}.`,
315
+ 'Start by summarizing what you found in the project, then suggest a plan to achieve the outcome.',
316
+ 'Ask me before making any changes.',
317
+ ].join(' ')
318
+
319
+ // Update sidebar project path
320
+ document.getElementById('current-project-path').textContent = folderPath
321
+
322
+ // Show sidebar immediately for existing projects
323
+ appState.firstBuildDone = true
324
+
325
+ showScreen('build')
326
+ }
327
+
328
+ // ============================================
329
+ // SCREEN 3: TERMINAL
330
+ // ============================================
331
+
332
+ function initTerminal() {
333
+ // Clean up existing terminal if any
334
+ if (appState.terminal) {
335
+ appState.terminal.dispose()
336
+ appState.terminal = null
337
+ }
338
+ if (appState.ws) {
339
+ appState.ws.close()
340
+ appState.ws = null
341
+ }
342
+ if (appState.fallbackTimer) {
343
+ clearTimeout(appState.fallbackTimer)
344
+ appState.fallbackTimer = null
345
+ }
346
+ if (appState.resizeObserver) {
347
+ appState.resizeObserver.disconnect()
348
+ appState.resizeObserver = null
349
+ }
350
+ appState.firstBuildDone = false
351
+ appState.claudeReady = false
352
+ appState.claudeLaunchedAt = null
353
+ // Clear the container
354
+ const container = document.getElementById('terminal-container')
355
+ container.innerHTML = ''
356
+
357
+ if (!appState.ptyAvailable) {
358
+ container.style.display = 'none'
359
+ document.getElementById('terminal-fallback').style.display = 'flex'
360
+
361
+ document.getElementById('btn-open-native').onclick = openNativeTerminal
362
+ return
363
+ }
364
+
365
+ // Check if xterm CDN scripts loaded
366
+ if (!window.Terminal || !window.FitAddon || !window.WebLinksAddon) {
367
+ container.style.display = 'none'
368
+ document.getElementById('terminal-fallback').style.display = 'flex'
369
+ document.getElementById('terminal-fallback').querySelector('p').textContent =
370
+ 'Terminal libraries failed to load (check your internet connection).'
371
+ document.getElementById('btn-open-native').onclick = openNativeTerminal
372
+ return
373
+ }
374
+
375
+ const Terminal = window.Terminal
376
+ const FitAddon = window.FitAddon.FitAddon
377
+ const WebLinksAddon = window.WebLinksAddon.WebLinksAddon
378
+
379
+ const term = new Terminal({
380
+ theme: {
381
+ background: '#1a1a2e',
382
+ foreground: '#e0e0e0',
383
+ cursor: '#4ade80',
384
+ selectionBackground: 'rgba(74, 222, 128, 0.25)',
385
+ black: '#1a1a2e',
386
+ red: '#ef4444',
387
+ green: '#22c55e',
388
+ yellow: '#f59e0b',
389
+ blue: '#3b82f6',
390
+ magenta: '#a855f7',
391
+ cyan: '#06b6d4',
392
+ white: '#e0e0e0',
393
+ },
394
+ fontFamily: '"JetBrains Mono", "Fira Code", "SF Mono", monospace',
395
+ fontSize: 14,
396
+ cursorBlink: true,
397
+ scrollback: 5000,
398
+ })
399
+
400
+ const fitAddon = new FitAddon()
401
+ term.loadAddon(fitAddon)
402
+ term.loadAddon(new WebLinksAddon())
403
+
404
+ term.open(container)
405
+ fitAddon.fit()
406
+
407
+ appState.terminal = term
408
+
409
+ // Connect WebSocket
410
+ const cwd = encodeURIComponent(appState.projectDir || '')
411
+ const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
412
+ const ws = new WebSocket(`${wsProtocol}//${location.host}/terminal?cwd=${cwd}`)
413
+ appState.ws = ws
414
+
415
+ ws.onopen = () => {
416
+ appState.claudeReady = false
417
+ appState.claudeLaunchedAt = null
418
+ appState.pendingPrompt = appState.intentDescription || null
419
+ appState.intentDescription = null
420
+
421
+ // Launch Claude Code — project folder already created by /api/scaffold
422
+ // and the WebSocket cwd param sets the working directory
423
+ setTimeout(() => {
424
+ ws.send(JSON.stringify({ type: 'input', data: 'claude --dangerously-skip-permissions\r' }))
425
+ appState.claudeLaunchedAt = Date.now()
426
+ }, 500)
427
+ }
428
+
429
+ ws.onmessage = (e) => {
430
+ let msg
431
+ try {
432
+ msg = JSON.parse(e.data)
433
+ } catch {
434
+ return
435
+ }
436
+ switch (msg.type) {
437
+ case 'output':
438
+ term.write(msg.data)
439
+
440
+ // Detect Claude Code readiness — look for the interactive prompt.
441
+ // Ignore output in the first 2s after launch to avoid false positives
442
+ // from shell echo, npm warnings, etc. that may contain ">".
443
+ if (!appState.claudeReady && appState.pendingPrompt && appState.claudeLaunchedAt) {
444
+ const elapsed = Date.now() - appState.claudeLaunchedAt
445
+ if (elapsed > 2000) {
446
+ const clean = msg.data.replace(/\x1b(?:\[[0-9;]*[a-zA-Z]|\][^\x07]*\x07|.)/g, '')
447
+ if (/>\s*$/.test(clean) || clean.includes('What can I help')) {
448
+ appState.claudeReady = true
449
+ setTimeout(() => {
450
+ if (appState.pendingPrompt && ws.readyState === WebSocket.OPEN) {
451
+ ws.send(JSON.stringify({ type: 'input', data: appState.pendingPrompt + '\r' }))
452
+ appState.pendingPrompt = null
453
+ }
454
+ }, 300)
455
+ }
456
+ }
457
+ }
458
+
459
+ // After the first prompt is sent, watch for Claude finishing its response.
460
+ // Require at least 10s after prompt delivery to avoid mid-response triggers.
461
+ if (appState.claudeReady && !appState.firstBuildDone && !appState.pendingPrompt) {
462
+ const elapsed = Date.now() - appState.claudeLaunchedAt
463
+ if (elapsed > 10000) {
464
+ const clean = msg.data.replace(/\x1b(?:\[[0-9;]*[a-zA-Z]|\][^\x07]*\x07|.)/g, '')
465
+ if (/>\s*$/.test(clean)) {
466
+ appState.firstBuildDone = true
467
+ const sidebar = document.getElementById('build-sidebar')
468
+ const reopenTab = document.getElementById('sidebar-reopen')
469
+ sidebar.classList.remove('hidden')
470
+ reopenTab.classList.add('hidden')
471
+ }
472
+ }
473
+ }
474
+ break
475
+ case 'exit':
476
+ term.writeln('\r\n[Process exited. Press any key to restart]')
477
+ break
478
+ case 'error':
479
+ document.getElementById('terminal-container').style.display = 'none'
480
+ document.getElementById('terminal-fallback').style.display = 'flex'
481
+ break
482
+ }
483
+ }
484
+
485
+ ws.onclose = () => {
486
+ term.writeln('\r\n[Connection lost. Refresh to reconnect]')
487
+ }
488
+
489
+ // Send keystrokes to server
490
+ term.onData((data) => {
491
+ if (ws.readyState === WebSocket.OPEN) {
492
+ ws.send(JSON.stringify({ type: 'input', data }))
493
+ }
494
+ })
495
+
496
+ // Handle resize
497
+ appState.resizeObserver = new ResizeObserver(() => {
498
+ fitAddon.fit()
499
+ if (ws.readyState === WebSocket.OPEN) {
500
+ ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }))
501
+ }
502
+ })
503
+ appState.resizeObserver.observe(container)
504
+
505
+ // Fallback: if readiness detection doesn't trigger in 15s, send anyway
506
+ appState.fallbackTimer = setTimeout(() => {
507
+ if (appState.pendingPrompt && ws.readyState === WebSocket.OPEN) {
508
+ ws.send(JSON.stringify({ type: 'input', data: appState.pendingPrompt + '\r' }))
509
+ appState.pendingPrompt = null
510
+ appState.claudeReady = true
511
+ }
512
+ appState.fallbackTimer = null
513
+ }, 15000)
514
+ }
515
+
516
+ function openNativeTerminal() {
517
+ const dir = appState.projectDir || '~'
518
+ alert(`Open your terminal app and run:\n\ncd ${dir}\nclaude --dangerously-skip-permissions`)
519
+ }
520
+
521
+ // ============================================
522
+ // PANEL TOGGLES
523
+ // ============================================
524
+
525
+ function toggleSidebar() {
526
+ const sidebar = document.getElementById('build-sidebar')
527
+ const reopenTab = document.getElementById('sidebar-reopen')
528
+ const isHidden = sidebar.classList.toggle('hidden')
529
+ reopenTab.classList.toggle('hidden', !isHidden)
530
+ }
531
+
532
+ function togglePreview() {
533
+ const preview = document.getElementById('build-preview')
534
+ const iframe = document.getElementById('preview-iframe')
535
+ const placeholder = document.getElementById('preview-placeholder')
536
+ const isHidden = preview.classList.toggle('hidden')
537
+
538
+ if (!isHidden) {
539
+ iframe.src = 'http://localhost:5173'
540
+ iframe.onload = () => {
541
+ placeholder.style.display = 'none'
542
+ iframe.style.display = 'block'
543
+ }
544
+ iframe.onerror = () => {
545
+ placeholder.style.display = 'flex'
546
+ iframe.style.display = 'none'
547
+ }
548
+ } else {
549
+ iframe.src = ''
550
+ placeholder.style.display = 'flex'
551
+ iframe.style.display = 'none'
552
+ }
553
+ }
554
+
555
+ // ============================================
556
+ // GITHUB CLONE
557
+ // ============================================
558
+
559
+ async function showRepoList() {
560
+ const list = document.getElementById('clone-repo-list')
561
+ const items = document.getElementById('repo-items')
562
+ list.style.display = 'block'
563
+ items.innerHTML = '<div class="loading">Loading repos...</div>'
564
+
565
+ const data = await fetchJson('/api/github/repos')
566
+ if (!data || !data.repos || !data.repos.length) {
567
+ items.innerHTML = '<div class="text-muted">No repos found</div>'
568
+ return
569
+ }
570
+
571
+ items.innerHTML = ''
572
+ data.repos.forEach((repo) => {
573
+ const btn = document.createElement('button')
574
+ btn.className = 'repo-item'
575
+ btn.textContent = repo.name
576
+ btn.onclick = () => cloneRepo(repo.name, repo.url)
577
+ items.appendChild(btn)
578
+ })
579
+ }
580
+
581
+ function shellEscape(str) {
582
+ return "'" + str.replace(/'/g, "'\\''") + "'"
583
+ }
584
+
585
+ function cloneRepo(name, url) {
586
+ if (!appState.ws || appState.ws.readyState !== WebSocket.OPEN) return
587
+ document.getElementById('clone-repo-list').style.display = 'none'
588
+ appState.ws.send(JSON.stringify({ type: 'input', data: `gh repo clone ${shellEscape(url)} && cd ${shellEscape(name)}\r` }))
589
+ document.getElementById('current-project-path').textContent = `~/Claude/${name}`
590
+ }
591
+
592
+ // ============================================
593
+ // EVENT LISTENERS
594
+ // ============================================
595
+
596
+ function setupListeners() {
597
+ // Path selection
598
+ document.getElementById('path-new').addEventListener('click', () => showScreen('new'))
599
+ document.getElementById('path-existing').addEventListener('click', () => showScreen('existing'))
600
+
601
+ // Back buttons
602
+ document.getElementById('btn-back-new').addEventListener('click', () => showScreen('setup'))
603
+ document.getElementById('btn-back-existing').addEventListener('click', () => showScreen('setup'))
604
+
605
+ // New project: project name input
606
+ const nameInput = document.getElementById('project-name')
607
+ const descStep = document.getElementById('description-step')
608
+ const descInput = document.getElementById('intent-description')
609
+ const buildBtn = document.getElementById('btn-start-building')
610
+
611
+ nameInput.addEventListener('input', () => {
612
+ const raw = nameInput.value
613
+ const clean = raw.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9._-]/g, '')
614
+ if (raw !== clean) nameInput.value = clean
615
+ appState.projectName = clean
616
+
617
+ if (clean.length > 0) {
618
+ descStep.style.display = 'block'
619
+ } else {
620
+ descStep.style.display = 'none'
621
+ }
622
+ updateBuildButton()
623
+ })
624
+
625
+ descInput.addEventListener('input', updateBuildButton)
626
+
627
+ descInput.addEventListener('keydown', (e) => {
628
+ if (e.key === 'Enter' && !buildBtn.disabled) {
629
+ e.preventDefault()
630
+ startBuilding()
631
+ }
632
+ })
633
+
634
+ buildBtn.addEventListener('click', startBuilding)
635
+
636
+ // Example buttons fill the project name input
637
+ document.querySelectorAll('.example-btn').forEach((btn) => {
638
+ btn.addEventListener('click', () => {
639
+ nameInput.value = btn.dataset.example
640
+ nameInput.dispatchEvent(new Event('input'))
641
+ descInput.focus()
642
+ })
643
+ })
644
+
645
+ // Existing project: progressive reveal (path → pain → outcome → button)
646
+ const pathInput = document.getElementById('existing-path')
647
+ const contextStep = document.getElementById('existing-context')
648
+ const painInput = document.getElementById('existing-pain')
649
+ const outcomeStep = document.getElementById('existing-outcome-step')
650
+ const outcomeInput = document.getElementById('existing-outcome')
651
+ const openBtn = document.getElementById('btn-open-existing')
652
+
653
+ pathInput.addEventListener('input', () => {
654
+ if (pathInput.value.trim()) {
655
+ contextStep.style.display = 'block'
656
+ } else {
657
+ contextStep.style.display = 'none'
658
+ outcomeStep.style.display = 'none'
659
+ }
660
+ updateOpenButton()
661
+ })
662
+
663
+ painInput.addEventListener('input', () => {
664
+ if (painInput.value.trim()) {
665
+ outcomeStep.style.display = 'block'
666
+ } else {
667
+ outcomeStep.style.display = 'none'
668
+ }
669
+ updateOpenButton()
670
+ })
671
+
672
+ outcomeInput.addEventListener('input', updateOpenButton)
673
+
674
+ outcomeInput.addEventListener('keydown', (e) => {
675
+ if (e.key === 'Enter' && !openBtn.disabled) {
676
+ e.preventDefault()
677
+ openExisting()
678
+ }
679
+ })
680
+
681
+ openBtn.addEventListener('click', openExisting)
682
+
683
+ function updateOpenButton() {
684
+ const hasPath = pathInput.value.trim().length > 0
685
+ const hasPain = painInput.value.trim().length > 0
686
+ const hasOutcome = outcomeInput.value.trim().length > 0
687
+ openBtn.disabled = !(hasPath && hasPain && hasOutcome)
688
+ }
689
+
690
+ // Screen 3: Project management
691
+ document.getElementById('btn-clone-github').addEventListener('click', showRepoList)
692
+ document.getElementById('btn-close-repos').addEventListener('click', () => {
693
+ document.getElementById('clone-repo-list').style.display = 'none'
694
+ })
695
+ document.getElementById('btn-new-project').addEventListener('click', () => {
696
+ const name = prompt('Project folder name:')
697
+ if (!name) return
698
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
699
+ alert('Folder name can only contain letters, numbers, hyphens, dots, and underscores.')
700
+ return
701
+ }
702
+ if (appState.ws && appState.ws.readyState === WebSocket.OPEN) {
703
+ appState.ws.send(JSON.stringify({ type: 'input', data: `mkdir -p ${shellEscape(name)} && cd ${shellEscape(name)}\r` }))
704
+ }
705
+ document.getElementById('current-project-path').textContent = `~/Claude/${name}`
706
+ })
707
+
708
+ // Screen 3: Panel toggles
709
+ document.getElementById('btn-toggle-sidebar').addEventListener('click', toggleSidebar)
710
+ document.getElementById('sidebar-reopen').addEventListener('click', toggleSidebar)
711
+ document.getElementById('btn-toggle-preview').addEventListener('click', togglePreview)
712
+ document.getElementById('btn-close-preview').addEventListener('click', togglePreview)
713
+ document.getElementById('btn-refresh-preview').addEventListener('click', () => {
714
+ const iframe = document.getElementById('preview-iframe')
715
+ iframe.src = iframe.src
716
+ })
717
+
718
+ // Prompt buttons — click to type into terminal
719
+ document.addEventListener('click', (e) => {
720
+ const promptBtn = e.target.closest('.prompt-btn')
721
+ if (!promptBtn) return
722
+
723
+ const text = promptBtn.dataset.prompt
724
+ if (!text || !appState.ws || appState.ws.readyState !== WebSocket.OPEN) return
725
+
726
+ const autoExec = promptBtn.dataset.auto === 'true'
727
+ appState.ws.send(JSON.stringify({ type: 'input', data: autoExec ? text + '\r' : text }))
728
+
729
+ promptBtn.classList.add('prompt-sent')
730
+ setTimeout(() => promptBtn.classList.remove('prompt-sent'), 600)
731
+ })
732
+ }
733
+
734
+ // ============================================
735
+ // HELPERS
736
+ // ============================================
737
+
738
+ async function fetchJson(url, options = {}) {
739
+ try {
740
+ const res = await fetch(url, options)
741
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
742
+ return res.json()
743
+ } catch (err) {
744
+ console.error(`Failed to fetch ${url}:`, err)
745
+ return null
746
+ }
747
+ }