From 84a341ebf9d08f7e22518321f8f0bc2194006a65 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Thu, 29 Jan 2026 11:33:21 -0800 Subject: [PATCH] log -f --- src/cli/index.ts | 45 +++++++++++++++++++++++++++++++++++++++++--- src/server/index.tsx | 25 ++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/cli/index.ts b/src/cli/index.ts index 949b8c7..9b74099 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -57,7 +57,15 @@ const restartApp = async (app: string) => { await post(`/api/apps/${app}/restart`) } -async function logApp(name: string) { +const printLog = (line: LogLine) => + console.log(`${new Date(line.time).toLocaleTimeString()} ${line.text}`) + +async function logApp(name: string, options: { follow?: boolean }) { + if (options.follow) { + await tailLogs(name) + return + } + const logs: LogLine[] | undefined = await get(`/api/apps/${name}/logs`) if (!logs) { console.error(`App not found: ${name}`) @@ -68,8 +76,37 @@ async function logApp(name: string) { return } for (const line of logs) { - const time = new Date(line.time).toLocaleTimeString() - console.log(`${time} ${line.text}`) + printLog(line) + } +} + +async function tailLogs(name: string) { + const url = join(HOST, `/api/apps/${name}/logs/stream`) + const res = await fetch(url) + if (!res.ok) { + console.error(`App not found: ${name}`) + return + } + if (!res.body) return + + const reader = res.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n\n') + buffer = lines.pop() ?? '' + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = JSON.parse(line.slice(6)) as LogLine + printLog(data) + } + } } } @@ -119,11 +156,13 @@ program .command('logs') .description('Show logs for an app') .argument('', 'app name') + .option('-f, --follow', 'follow log output') .action(logApp) program .command('log', { hidden: true }) .argument('', 'app name') + .option('-f, --follow', 'follow log output') .action(logApp) program diff --git a/src/server/index.tsx b/src/server/index.tsx index 04b3f75..5177ee8 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -80,6 +80,31 @@ app.get('/api/apps/:app/logs', c => { return c.json(app.logs ?? []) }) +app.sse('/api/apps/:app/logs/stream', (send, c) => { + const appName = c.req.param('app') + const targetApp = allApps().find(a => a.name === appName) + if (!targetApp) return + + let lastLogCount = 0 + + const sendNewLogs = () => { + const currentApp = allApps().find(a => a.name === appName) + if (!currentApp) return + + const logs = currentApp.logs ?? [] + const newLogs = logs.slice(lastLogCount) + lastLogCount = logs.length + + for (const line of newLogs) { + send(line) + } + } + + sendNewLogs() + const unsub = onChange(sendNewLogs) + return () => unsub() +}) + app.post('/api/apps/:app/start', c => { const appName = c.req.param('app') if (!appName) return c.json({ error: 'App not found' }, 404)