import type { LogLine } from '@types' import { get, handleError, makeUrl } from '../http' import { resolveAppName } from '../name' interface LogOptions { date?: string follow?: boolean grep?: string since?: string } const formatDate = (date: Date) => date.toISOString().slice(0, 10) const matchesGrep = (text: string, pattern: string) => text.toLowerCase().includes(pattern.toLowerCase()) const parseDuration = (duration: string): number | null => { const match = duration.match(/^(\d+)([hdwm])$/) if (!match) return null const value = parseInt(match[1]!, 10) const unit = match[2]! const ms = { h: 60 * 60 * 1000, d: 24 * 60 * 60 * 1000, w: 7 * 24 * 60 * 60 * 1000, m: 30 * 24 * 60 * 60 * 1000, } return value * ms[unit as keyof typeof ms] } const printDiskLog = (line: string, grep?: string) => { if (grep && !matchesGrep(line, grep)) return console.log(line) } export const printLog = (line: LogLine, grep?: string) => { if (grep && !matchesGrep(line.text, grep)) return console.log(`${new Date(line.time).toLocaleTimeString()} ${line.text}`) } export async function logApp(arg: string | undefined, options: LogOptions) { const name = resolveAppName(arg) if (!name) return if (options.follow) { await tailLogs(name, options.grep) return } // Handle --since option if (options.since) { const ms = parseDuration(options.since) if (!ms) { console.error('Invalid duration. Use format like: 1h, 2d, 1w, 1m') return } const dates = await get(`/api/apps/${name}/logs/dates`) if (!dates) { console.error(`App not found: ${name}`) return } const cutoff = new Date(Date.now() - ms) const cutoffDate = formatDate(cutoff) // Filter dates that are >= cutoff const relevantDates = dates.filter(d => d >= cutoffDate).reverse() if (relevantDates.length === 0) { console.log('No logs in the specified time range') return } for (const date of relevantDates) { const lines = await get(`/api/apps/${name}/logs?date=${date}`) if (!lines) continue for (const line of lines) { printDiskLog(line, options.grep) } } return } // Handle --date option if (options.date) { const lines = await get(`/api/apps/${name}/logs?date=${options.date}`) if (!lines) { console.error(`App not found: ${name}`) return } if (lines.length === 0) { console.log('No logs for this date') return } for (const line of lines) { printDiskLog(line, options.grep) } return } // Default: show today's in-memory logs const logs: LogLine[] | undefined = await get(`/api/apps/${name}/logs`) if (!logs) { console.error(`App not found: ${name}`) return } if (logs.length === 0) { console.log('No logs yet') return } for (const line of logs) { printLog(line, options.grep) } } export async function tailLogs(name: string, grep?: string) { try { const url = makeUrl(`/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, grep) } } } } catch (error) { handleError(error) } }