import { chmod, mkdir, open, readFile, writeFile, lstat, type FileHandle } from "node:fs/promises"; import { constants } from "node:fs"; import { tmpdir } from "node:os"; import { basename, join, relative, resolve } from "node:path"; import { randomUUID } from "node:crypto"; export type ResultStream = "stdout" | "stderr" | "formatted"; export interface ResultReference { id: string; command: string; format: "json" | "text"; stdoutPath: string; stderrPath: string; formattedPath: string; bytes: { stdout: number; stderr: number; formatted: number }; captureComplete: boolean; } export interface Page { text: string; offset: number; bytes: number; totalBytes: number; nextOffset: number | null; eof: boolean; } function lineLimitEnd(text: string, maxLines: number): number | undefined { const newlinePositions: number[] = []; for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) newlinePositions.push(i); const lineCount = text.endsWith("\n") ? newlinePositions.length : newlinePositions.length + 1; return lineCount > maxLines ? newlinePositions[maxLines - 1] + 1 : undefined; } export function readPage(data: Buffer, options: { offset?: number; limit?: number; maxLines?: number } = {}): Page { const offset = options.offset ?? 0; const limit = options.limit ?? 8192; const maxLines = options.maxLines ?? 200; if (!Number.isSafeInteger(offset) || offset < 0 || offset > data.length) throw new Error("invalid byte offset"); if (!Number.isSafeInteger(limit) || limit < 4 || limit > 32 * 1024) throw new Error("limit must be between 4 and 32768 bytes"); if (offset < data.length && (data[offset] & 0xc0) === 0x80) throw new Error("offset is inside a UTF-8 character"); let end = Math.min(data.length, offset + limit); const decoder = new TextDecoder("utf-8", { fatal: true }); while (end > offset) { try { const text = decoder.decode(data.subarray(offset, end)); const chars = lineLimitEnd(text, maxLines); if (chars !== undefined) { end = offset + Buffer.byteLength(text.slice(0, chars)); continue; } const bytes = end - offset; return { text, offset, bytes, totalBytes: data.length, nextOffset: end < data.length ? end : null, eof: end >= data.length }; } catch { end--; } } if (offset < data.length) throw new Error("artifact contains invalid UTF-8"); return { text: "", offset, bytes: 0, totalBytes: data.length, nextOffset: null, eof: true }; } function safePath(path: string, root: string, expectedName: string) { const temp = resolve(tmpdir()); const directory = resolve(root); const resolved = resolve(path); const relativeTemp = relative(temp, directory); if (!relativeTemp || relativeTemp.startsWith("..") || !basename(directory).startsWith("axi-result-") || resolved !== join(directory, expectedName)) throw new Error("result expired or missing"); return resolved; } async function writePrivate(path: string, content: string) { await writeFile(path, content, { encoding: "utf8", mode: 0o600, flag: "wx" }); await chmod(path, 0o600); } export async function saveResult(input: { command: string; format: "json" | "text"; stdout: string; stderr: string; formatted: string; captureComplete: boolean; append: (reference: ResultReference) => void; }): Promise { const sizes = [input.stdout, input.stderr, input.formatted].map((value) => Buffer.byteLength(value)); if (sizes.some((size) => size > 64 * 1024 * 1024)) throw new Error("result exceeds 64 MiB"); const id = randomUUID(); const directory = join(tmpdir(), `axi-result-${id}`); await mkdir(directory, { mode: 0o700 }); await chmod(directory, 0o700); const reference: ResultReference = { id, command: input.command, format: input.format, stdoutPath: join(directory, "stdout"), stderrPath: join(directory, "stderr"), formattedPath: join(directory, "formatted"), bytes: { stdout: Buffer.byteLength(input.stdout), stderr: Buffer.byteLength(input.stderr), formatted: Buffer.byteLength(input.formatted) }, captureComplete: input.captureComplete, }; try { await Promise.all([ writePrivate(reference.stdoutPath, input.stdout), writePrivate(reference.stderrPath, input.stderr), writePrivate(reference.formattedPath, input.formatted), writePrivate(join(directory, "reference.json"), JSON.stringify(reference)), ]); input.append(reference); return reference; } catch (error) { throw new Error(`could not save AXI result: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } } function isReference(value: unknown): value is ResultReference { if (!value || typeof value !== "object") return false; const reference = value as Partial; const bytes = reference.bytes; return typeof reference.id === "string" && /^[-0-9a-f]{36}$/.test(reference.id) && typeof reference.command === "string" && (reference.format === "json" || reference.format === "text") && typeof reference.stdoutPath === "string" && typeof reference.stderrPath === "string" && typeof reference.formattedPath === "string" && !!bytes && ["stdout", "stderr", "formatted"].every((key) => Number.isSafeInteger(bytes[key as keyof typeof bytes]) && bytes[key as keyof typeof bytes] >= 0) && typeof reference.captureComplete === "boolean"; } export interface AxiResultStats { results: number; complete: number; incomplete: number; bytes: { stdout: number; stderr: number; formatted: number; total: number }; } export function summarizeResults(entries: unknown[]): AxiResultStats { const references = new Map(); for (const entry of entries) { if (!entry || typeof entry !== "object") continue; const item = entry as { type?: string; customType?: string; data?: unknown }; if (item.type === "custom" && item.customType === "axi-result" && isReference(item.data)) references.set(item.data.id, item.data); } const bytes = { stdout: 0, stderr: 0, formatted: 0, total: 0 }; let complete = 0; for (const reference of references.values()) { bytes.stdout += reference.bytes.stdout; bytes.stderr += reference.bytes.stderr; bytes.formatted += reference.bytes.formatted; if (reference.captureComplete) complete++; } bytes.total = bytes.stdout + bytes.stderr + bytes.formatted; return { results: references.size, complete, incomplete: references.size - complete, bytes }; } export function findResult(entries: unknown[], id: string): ResultReference | undefined { // ponytail: linear session-entry scan; add an index only if lookup cost is measurable. for (const entry of entries) { if (!entry || typeof entry !== "object") continue; const item = entry as { type?: string; customType?: string; data?: unknown }; if (item.type !== "custom" || item.customType !== "axi-result") continue; if (isReference(item.data) && item.data.id === id) return item.data; } return undefined; } export async function findSharedResult(entries: unknown[], id: string): Promise { const local = findResult(entries, id); if (local || !/^[-0-9a-f]{36}$/.test(id)) return local; const directory = join(tmpdir(), `axi-result-${id}`); const path = join(directory, "reference.json"); try { const directoryStat = await lstat(directory); const stat = await lstat(path); if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink() || !stat.isFile() || stat.isSymbolicLink()) return undefined; const reference = JSON.parse(await readFile(path, "utf8")) as unknown; return isReference(reference) && reference.id === id ? reference : undefined; } catch { return undefined; } } async function readHandlePage(handle: FileHandle, reference: ResultReference, stream: ResultStream, options: { offset?: number; limit?: number }): Promise { const initial = await handle.stat(); const totalBytes = initial.size; const expectedBytes = reference.bytes[stream]; if (!initial.isFile() || totalBytes !== expectedBytes || totalBytes > 64 * 1024 * 1024) throw new Error("result expired or missing"); const offset = options.offset ?? 0; if (!Number.isSafeInteger(offset) || offset < 0 || offset > totalBytes) throw new Error("invalid byte offset"); const length = Math.min(options.limit ?? 8192, totalBytes - offset); const data = Buffer.alloc(length); let position = 0; while (position < length) { const read = await handle.read(data, position, length - position, offset + position); if (read.bytesRead === 0) throw new Error("result expired or missing"); position += read.bytesRead; } const final = await handle.stat(); if (final.size !== totalBytes) throw new Error("result expired or missing"); if (length > 0 && (data[0] & 0xc0) === 0x80) throw new Error("offset is inside a UTF-8 character"); const page = readPage(data, { ...options, offset: 0 }); return { ...page, offset, totalBytes, nextOffset: offset + page.bytes < totalBytes ? offset + page.bytes : null, eof: offset + page.bytes >= totalBytes }; } export async function readResultPage(reference: ResultReference, stream: ResultStream, options: { offset?: number; limit?: number } = {}): Promise { const paths = { stdout: reference.stdoutPath, stderr: reference.stderrPath, formatted: reference.formattedPath }; const path = paths[stream]; if (!reference.id || !/^[-0-9a-f]{36}$/.test(reference.id) || !Object.values(paths).every((item) => typeof item === "string")) throw new Error("result expired or missing"); const directories = Object.values(paths).map((item) => resolve(item, "..")); if (new Set(directories).size !== 1) throw new Error("result expired or missing"); const safe = safePath(path, directories[0], stream); const directoryStat = await lstat(directories[0]).catch(() => undefined); const stat = await lstat(safe).catch(() => undefined); if (!directoryStat || !directoryStat.isDirectory() || directoryStat.isSymbolicLink() || !stat || !stat.isFile() || stat.isSymbolicLink()) throw new Error("result expired or missing"); const handle = await open(safe, constants.O_RDONLY | constants.O_NOFOLLOW).catch(() => undefined); if (!handle) throw new Error("result expired or missing"); try { return await readHandlePage(handle, reference, stream, options); } finally { await handle.close(); } }