import { execFile } from "node:child_process"; import type { DockerLogsConfig } from "./types"; // Known factory container names const FACTORY_CONTAINERS = [ "ai-factory-orchestrator", "ai-factory-worker-1", "ai-factory-worker-2", "ai-factory-worker-3", "ai-factory-worker-4", "ai-factory-gitea", "ai-factory-postgres", "ai-factory-loki", "ai-factory-grafana", ]; /** * Run `docker logs` for a container and return stdout. */ export function dockerLogs( container: string, tail: number, since?: string, ): Promise<{ ok: boolean; text?: string; error?: string }> { return new Promise((resolve) => { const args = ["logs", "--timestamps"]; if (tail > 0) args.push("--tail", String(tail)); if (since) args.push("--since", since); args.push(container); execFile("docker", args, { timeout: 15000, maxBuffer: 2 * 1024 * 1024 }, (err, stdout, stderr) => { if (err) { resolve({ ok: false, error: stderr || err.message }); return; } resolve({ ok: true, text: stdout }); }); }); } /** * List running Docker containers (factory-related). */ export function listContainers(): Promise<{ ok: boolean; containers?: string[]; error?: string }> { return new Promise((resolve) => { execFile( "docker", ["ps", "--format", "{{.Names}}", "--filter", "name=ai-factory"], { timeout: 5000 }, (err, stdout, stderr) => { if (err) { resolve({ ok: false, error: stderr || err.message }); return; } const containers = stdout .split("\n") .map((c) => c.trim()) .filter(Boolean); resolve({ ok: true, containers }); }, ); }); } /** * Resolve a worker spec to a container name. */ export function resolveWorkerContainer(spec: string): string | null { // Already a full container name if (spec.startsWith("ai-factory-")) return spec; // Number or "worker-N" const num = spec.replace(/^worker-/, ""); return `ai-factory-worker-${num}`; } /** * Search log text for a pattern and return matching lines. */ export function grepLogs(text: string, pattern: string, contextLines: number = 0): string { const lines = text.split("\n"); const matched: string[] = []; for (let i = 0; i < lines.length; i++) { if (lines[i].toLowerCase().includes(pattern.toLowerCase())) { if (contextLines > 0) { const start = Math.max(0, i - contextLines); const end = Math.min(lines.length, i + contextLines + 1); if (matched.length > 0 && start <= i - contextLines + 1) { // Merge overlapping ranges while (matched.length > 0) matched.pop(); } for (let j = start; j < end; j++) { matched.push(lines[j]); } } else { matched.push(lines[i]); } } } return matched.join("\n"); } /** * Truncate log output for display. */ export function truncateOutput(text: string, maxLines: number): { text: string; truncated: boolean; totalLines: number } { const lines = text.split("\n").filter(Boolean); const totalLines = lines.length; if (totalLines <= maxLines) return { text, truncated: false, totalLines }; const head = Math.ceil(maxLines * 0.2); const tail = maxLines - head; const truncated = lines.slice(0, head).join("\n") + `\n\n... [${totalLines - maxLines} lines truncated] ...\n\n` + lines.slice(-tail).join("\n"); return { text: truncated, truncated: true, totalLines }; }