import { readdir, readFile, stat } from "node:fs/promises";
import { basename, join, normalize, relative, resolve, sep } from "node:path";
export class ArtifactService {
    cwd;
    artifactRoot;
    constructor(cwd) {
        this.cwd = cwd;
        this.artifactRoot = resolve(cwd, ".threadwell", "artifacts");
    }
    async listArtifacts() {
        const metadataPaths = await this.listMetadataFiles(this.artifactRoot);
        const artifacts = [];
        for (const metadataPath of metadataPaths) {
            const metadata = await this.readMetadataFile(metadataPath);
            if (metadata)
                artifacts.push(metadata);
        }
        return artifacts.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
    }
    async getArtifact(id) {
        const identifier = normalizeArtifactIdentifier(id);
        const artifacts = await this.listArtifacts();
        return artifacts.find((artifact) => {
            const outputPath = artifact.outputPath.replaceAll("\\", "/");
            const metadataPath = artifact.metadataPath.replaceAll("\\", "/");
            return (artifact.id === identifier ||
                outputPath === identifier ||
                metadataPath === identifier ||
                basename(outputPath) === identifier ||
                basename(metadataPath) === identifier ||
                basename(outputPath, ".txt") === identifier ||
                basename(metadataPath, ".json") === identifier);
        });
    }
    async readArtifact(id, options = {}) {
        const artifact = await this.requireArtifact(id);
        const content = await readFile(this.resolveProjectPath(artifact.outputPath), "utf-8");
        const lines = content.split("\n");
        const startLine = Math.max(1, Math.floor(options.startLine ?? 1));
        const maxLines = Math.max(1, Math.floor(options.maxLines ?? 200));
        const startIndex = Math.min(lines.length, startLine - 1);
        const selected = lines.slice(startIndex, startIndex + maxLines);
        const endLine = selected.length === 0 ? startLine : startLine + selected.length - 1;
        return {
            artifact,
            content: selected.join("\n"),
            startLine,
            endLine,
            totalLines: lines.length,
            truncated: startIndex + selected.length < lines.length,
        };
    }
    async readArtifactTail(id, maxLines = 200) {
        const artifact = await this.requireArtifact(id);
        const content = await readFile(this.resolveProjectPath(artifact.outputPath), "utf-8");
        const lines = content.split("\n");
        const boundedMaxLines = Math.max(1, Math.floor(maxLines));
        const startIndex = Math.max(0, lines.length - boundedMaxLines);
        const selected = lines.slice(startIndex);
        return {
            artifact,
            content: selected.join("\n"),
            startLine: startIndex + 1,
            endLine: lines.length,
            totalLines: lines.length,
            truncated: startIndex > 0,
        };
    }
    async searchArtifact(id, query, options = {}) {
        const artifact = await this.requireArtifact(id);
        const content = await readFile(this.resolveProjectPath(artifact.outputPath), "utf-8");
        const lines = content.split("\n");
        const maxMatches = Math.max(1, Math.floor(options.maxMatches ?? 100));
        const normalizedQuery = options.caseSensitive ? query : query.toLowerCase();
        const matches = [];
        let truncated = false;
        for (let i = 0; i < lines.length; i++) {
            const haystack = options.caseSensitive ? lines[i] : lines[i].toLowerCase();
            if (!haystack.includes(normalizedQuery))
                continue;
            if (matches.length >= maxMatches) {
                truncated = true;
                break;
            }
            matches.push({ line: i + 1, text: lines[i] });
        }
        return { artifact, query, matches, truncated };
    }
    async requireArtifact(id) {
        const artifact = await this.getArtifact(id);
        if (!artifact)
            throw new Error(`Artifact not found: ${id}`);
        return artifact;
    }
    async listMetadataFiles(dir) {
        let entries;
        try {
            entries = await readdir(dir, { withFileTypes: true });
        }
        catch {
            return [];
        }
        const paths = [];
        for (const entry of entries) {
            const path = join(dir, entry.name);
            if (entry.isDirectory()) {
                paths.push(...(await this.listMetadataFiles(path)));
            }
            else if (entry.isFile() && entry.name.endsWith(".json")) {
                paths.push(path);
            }
        }
        return paths;
    }
    async readMetadataFile(path) {
        try {
            const raw = JSON.parse(await readFile(path, "utf-8"));
            if (!isArtifactMetadata(raw))
                return undefined;
            const outputPath = this.resolveProjectPath(raw.outputPath);
            const metadataPath = this.resolveProjectPath(raw.metadataPath);
            if (!this.isInsideProject(outputPath) || !this.isInsideProject(metadataPath))
                return undefined;
            await stat(outputPath);
            return raw;
        }
        catch {
            return undefined;
        }
    }
    resolveProjectPath(path) {
        const resolved = resolve(this.cwd, path);
        if (!this.isInsideProject(resolved)) {
            throw new Error(`Artifact path escapes project: ${path}`);
        }
        return resolved;
    }
    isInsideProject(path) {
        const rel = relative(this.cwd, normalize(path));
        return rel === "" || (!rel.startsWith("..") && !rel.includes(`..${sep}`));
    }
}
function normalizeArtifactIdentifier(identifier) {
    return identifier.replaceAll("\\", "/").replace(/^\.\//, "");
}
function isArtifactMetadata(value) {
    if (!value || typeof value !== "object")
        return false;
    const record = value;
    return (typeof record.id === "string" &&
        record.kind === "tool_output" &&
        record.tool === "bash" &&
        typeof record.cwd === "string" &&
        typeof record.createdAt === "string" &&
        typeof record.outputPath === "string" &&
        typeof record.metadataPath === "string" &&
        typeof record.bytes === "number" &&
        typeof record.lines === "number" &&
        record.previewPolicy === "tail" &&
        typeof record.previewLines === "number");
}
//# sourceMappingURL=artifact-service.js.map