'use client' import { useEffect, useRef, useState } from 'react' import { Terminal, ChevronRight, AlertTriangle } from 'lucide-react' interface HistoryEntry { command: string; output: string; exitCode: number; duration: number; cwd: string; ts: string } const PROMPT_COLOR = '#22c55e' const BANNER = `BlackRoad OS Terminal — localhost only Type 'help' for available commands, 'clear' to clear screen. ` const BUILT_INS: Record string> = { help: () => `Available commands: git status / log / diff — git operations br — BlackRoad CLI ls / cat / head / tail — file ops curl http://localhost:... — local API calls ping — network check ps aux | grep — process search clear — clear terminal`, clear: () => '__CLEAR__', } export default function TerminalPage() { const [history, setHistory] = useState([]) const [input, setInput] = useState('') const [cwd, setCwd] = useState('/Users/alexa/blackroad') const [running, setRunning] = useState(false) const [cmdHistory, setCmdHistory] = useState([]) const [histIdx, setHistIdx] = useState(-1) const [isLocal, setIsLocal] = useState(true) const inputRef = useRef(null) const bottomRef = useRef(null) useEffect(() => { // Check if we're on localhost if (typeof window !== 'undefined') { setIsLocal(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') } fetch('/api/exec').then(r => r.json()).then(d => setIsLocal(d.available)).catch(() => {}) }, []) useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [history]) const run = async (cmd: string) => { if (!cmd.trim()) return const trimmed = cmd.trim() // Built-ins if (BUILT_INS[trimmed]) { const out = BUILT_INS[trimmed]() if (out === '__CLEAR__') { setHistory([]); setInput(''); return } setHistory(h => [...h, { command: trimmed, output: out, exitCode: 0, duration: 0, cwd, ts: new Date().toISOString() }]) setInput('') return } setCmdHistory(h => [trimmed, ...h.slice(0, 49)]) setHistIdx(-1) setRunning(true) try { const r = await fetch('/api/exec', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: trimmed, cwd }), }) const d = await r.json() if (d.error && !d.output) { setHistory(h => [...h, { command: trimmed, output: `⛔ ${d.error}`, exitCode: 1, duration: 0, cwd, ts: new Date().toISOString() }]) } else { setHistory(h => [...h, { command: trimmed, output: d.output || '', exitCode: d.exitCode ?? 0, duration: d.duration || 0, cwd, ts: new Date().toISOString() }]) if (trimmed.startsWith('cd ')) { const newDir = trimmed.slice(3).trim() setCwd(prev => newDir.startsWith('/') ? newDir : `${prev}/${newDir}`) } } } catch { setHistory(h => [...h, { command: trimmed, output: 'Connection error — is the dev server running?', exitCode: 1, duration: 0, cwd, ts: new Date().toISOString() }]) } finally { setRunning(false) setInput('') inputRef.current?.focus() } } const handleKey = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { run(input); return } if (e.key === 'ArrowUp') { const idx = Math.min(histIdx + 1, cmdHistory.length - 1) setHistIdx(idx) setInput(cmdHistory[idx] || '') } if (e.key === 'ArrowDown') { const idx = Math.max(histIdx - 1, -1) setHistIdx(idx) setInput(idx === -1 ? '' : cmdHistory[idx]) } if (e.key === 'l' && e.ctrlKey) { e.preventDefault(); setHistory([]) } } const shortCwd = cwd.replace('/Users/alexa', '~') if (!isLocal) { return (

Terminal

Terminal unavailable on remote deployments
The web terminal only works when running the app locally at localhost:3000.

Use the br CLI for remote operations, or SSH directly to your Pi nodes.
) } return (

Terminal

{shortCwd} · {history.length} commands run

inputRef.current?.focus()} style={{ flex: 1, overflowY: 'auto', background: '#0a0a0a', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12, padding: 16, fontFamily: 'monospace', fontSize: 13, cursor: 'text' }} > {/* Banner */}
{BANNER}
{/* History */} {history.map((entry, i) => (
{entry.cwd.replace('/Users/alexa', '~')} {entry.command} {entry.duration}ms
{entry.output}
))} {/* Current input line */}
{shortCwd} setInput(e.target.value)} onKeyDown={handleKey} disabled={running} autoFocus style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', color: '#fff', fontFamily: 'monospace', fontSize: 13, caretColor: PROMPT_COLOR }} placeholder={running ? 'Running…' : ''} /> {running && }
) }