( paths: readonly string[], maxLines = MAX_LINES, )
| 71 | * The MAX_LINES tail cap applies across the combined set. |
| 72 | */ |
| 73 | export async function readLogs( |
| 74 | paths: readonly string[], |
| 75 | maxLines = MAX_LINES, |
| 76 | ): Promise<LogReadResult | null> { |
| 77 | const allLines: string[] = []; |
| 78 | let read = 0; |
| 79 | for (const path of paths) { |
| 80 | let raw: string; |
| 81 | try { |
| 82 | raw = await readFile(path, 'utf8'); |
| 83 | } catch { |
| 84 | continue; |
| 85 | } |
| 86 | read += 1; |
| 87 | const lines = raw.split(/\r?\n/); |
| 88 | // Drop a single trailing empty line from each file's final newline. |
| 89 | if (lines.length > 0 && lines.at(-1) === '') lines.pop(); |
| 90 | for (const line of lines) allLines.push(line); |
| 91 | } |
| 92 | if (read === 0) return null; |
| 93 | |
| 94 | const truncated = allLines.length > maxLines; |
| 95 | const startLineNo = truncated ? allLines.length - maxLines : 0; |
| 96 | const slice = truncated ? allLines.slice(startLineNo) : allLines; |
| 97 | |
| 98 | const lines: LogLine[] = slice.map((text, i) => parseLogLine(text, startLineNo + i + 1)); |
| 99 | return { lines, truncated }; |
| 100 | } |
| 101 | |
| 102 | export function parseLogLine(raw: string, lineNo: number): LogLine { |
| 103 | const m = LINE_RE.exec(raw); |
no test coverage detected