/**
 * Tracking manifest for Lisa-managed OpenCode artifacts.
 *
 * When Lisa emits files into a host project's `.opencode/` directory, it needs
 * a way to identify those files on the next run so:
 *   1. Stale files (skills Lisa stopped shipping) can be cleaned up.
 *   2. Host-authored files (custom skills/agents the user added) are never
 *      accidentally overwritten or deleted.
 *
 * The manifest lives at `.opencode/.lisa-managed.json` in the destination
 * project. It is checked into the host repo so the cleanup behavior is
 * deterministic across machines. This mirrors `src/codex/manifest.ts`; the only
 * difference is the config directory (`.opencode/` vs `.codex/`).
 * @module opencode/manifest
 */
import * as fse from "fs-extra";
import { readFile, writeFile } from "node:fs/promises";
import * as path from "node:path";
/** Config directory OpenCode reads project-scope artifacts from */
export const OPENCODE_CONFIG_DIR = ".opencode";
/** Filename of the Lisa-managed manifest, relative to `.opencode/` */
export const LISA_MANAGED_MANIFEST_FILENAME = ".lisa-managed.json";
/**
 * Read the Lisa-managed manifest from `<destDir>/.opencode/.lisa-managed.json`.
 * Returns an empty manifest if the file doesn't exist.
 * @param destDir - Absolute path to the destination project root.
 * @returns Parsed manifest (with empty file list if absent).
 */
export async function readManagedManifest(destDir) {
    const manifestPath = path.join(destDir, OPENCODE_CONFIG_DIR, LISA_MANAGED_MANIFEST_FILENAME);
    if (!(await fse.pathExists(manifestPath))) {
        return { files: [] };
    }
    const raw = await readFile(manifestPath, "utf8");
    const parsed = JSON.parse(raw);
    return validateManifest(parsed, manifestPath);
}
/**
 * Write the Lisa-managed manifest to disk, replacing any existing content.
 * @param destDir - Absolute path to the destination project root.
 * @param files - List of relative-to-`.opencode/` file paths Lisa shipped.
 */
export async function writeManagedManifest(destDir, files) {
    const configDir = path.join(destDir, OPENCODE_CONFIG_DIR);
    await fse.ensureDir(configDir);
    const manifestPath = path.join(configDir, LISA_MANAGED_MANIFEST_FILENAME);
    const manifest = {
        files: [...files].sort((a, b) => a.localeCompare(b)),
    };
    await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
}
/**
 * Type-guard validator. Throws on shape errors so a corrupted manifest is
 * surfaced rather than silently producing data loss.
 * @param parsed - Untrusted JSON value.
 * @param manifestPath - Path used in error messages.
 * @returns Validated manifest.
 */
function validateManifest(parsed, manifestPath) {
    if (parsed === null || typeof parsed !== "object") {
        throw new Error(`Invalid Lisa-managed manifest at ${manifestPath}: expected JSON object`);
    }
    const obj = parsed;
    if (!Array.isArray(obj.files)) {
        throw new Error(`Invalid Lisa-managed manifest at ${manifestPath}: expected "files" array`);
    }
    const files = obj.files.map((file, index) => validateManifestPath(file, index, manifestPath));
    return { files: Object.freeze(files) };
}
/**
 * Validate one manifest entry is a normalized, in-tree relative path.
 *
 * The manifest drives stale-file deletion, so a traversal or absolute entry
 * (`../../etc`, `/abs`, mixed `\` separators) could expand the delete scope
 * beyond the Lisa skill boundary. Reject those here so no consumer ever acts on
 * an unsafe path.
 * @param file - Untrusted entry from the parsed `files` array.
 * @param index - Position in the array (for error messages).
 * @param manifestPath - Path used in error messages.
 * @returns The validated, forward-slash-normalized relative path.
 */
function validateManifestPath(file, index, manifestPath) {
    if (typeof file !== "string") {
        throw new Error(`Invalid Lisa-managed manifest at ${manifestPath}: "files[${index}]" must be a string`);
    }
    const normalized = file.replaceAll("\\", "/");
    const segments = normalized.split("/");
    if (path.posix.isAbsolute(normalized) ||
        segments.some(seg => seg === "" || seg === "." || seg === "..")) {
        throw new Error(`Invalid Lisa-managed manifest at ${manifestPath}: "files[${index}]" must be a normalized relative path`);
    }
    return normalized;
}
//# sourceMappingURL=manifest.js.map