This commit is contained in:
Chris Wanstrath 2026-01-29 11:33:21 -08:00
parent e1b53fc54d
commit 84a341ebf9
2 changed files with 67 additions and 3 deletions

View File

@ -57,7 +57,15 @@ const restartApp = async (app: string) => {
await post(`/api/apps/${app}/restart`) 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`) const logs: LogLine[] | undefined = await get(`/api/apps/${name}/logs`)
if (!logs) { if (!logs) {
console.error(`App not found: ${name}`) console.error(`App not found: ${name}`)
@ -68,8 +76,37 @@ async function logApp(name: string) {
return return
} }
for (const line of logs) { for (const line of logs) {
const time = new Date(line.time).toLocaleTimeString() printLog(line)
console.log(`${time} ${line.text}`) }
}
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') .command('logs')
.description('Show logs for an app') .description('Show logs for an app')
.argument('<name>', 'app name') .argument('<name>', 'app name')
.option('-f, --follow', 'follow log output')
.action(logApp) .action(logApp)
program program
.command('log', { hidden: true }) .command('log', { hidden: true })
.argument('<name>', 'app name') .argument('<name>', 'app name')
.option('-f, --follow', 'follow log output')
.action(logApp) .action(logApp)
program program

View File

@ -80,6 +80,31 @@ app.get('/api/apps/:app/logs', c => {
return c.json(app.logs ?? []) 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 => { app.post('/api/apps/:app/start', c => {
const appName = c.req.param('app') const appName = c.req.param('app')
if (!appName) return c.json({ error: 'App not found' }, 404) if (!appName) return c.json({ error: 'App not found' }, 404)