/**
 * Clean-slate prepare for hermetic generated trees.
 *
 * `src/` is hermetic — every file there is emitted by the compiler and
 * carries the "Generated by quickback.dev" header. The contract is "do
 * not edit directly". So before each compile writes its output, the CLI
 * archives the previous src/ tree (cheap recursive copy) and then wipes
 * it. The fresh write recreates exactly the manifest the compiler
 * produced — no orphan files, no stale folders left by removed features,
 * no `.DS_Store` keeping a deleted feature folder alive, no dead
 * SPA assets with old content hashes.
 *
 * The archive is the safety net: if a generator regression silently
 * stopped emitting a file you depended on, the previous full tree is
 * sitting in `quickback/.archives/src-<ISO>/` ready to grep or copy
 * back. Old archives are pruned, keeping the most recent N.
 *
 * Sanity bound: if the manifest has zero files in the target dir, the
 * wipe is skipped — that's almost always a compile bug upstream and
 * we'd rather not lose the user's last-known-good output.
 */
import { promises as fs } from 'fs';
import { join, sep } from 'path';
const DEFAULT_ARCHIVE_KEEP = 3;
/**
 * Archive then wipe the target dir, in preparation for a fresh write.
 *
 * `manifest` is the full set of paths the compiler intends to write
 * (relative to outputDir). It's used as a sanity check — if the
 * manifest contains *no* files under targetDir we treat that as a
 * compile bug and skip the destructive step.
 *
 * The wipe itself is a single fs.rm({recursive:true}) — fast, total,
 * and matches the documented contract that src/ is compiler-owned.
 */
export async function prepareGeneratedDir(outputDir, targetDir, manifest, options = {}) {
    const fullTarget = join(outputDir, targetDir);
    if (!(await pathExists(fullTarget))) {
        return { wiped: false, skipped: 'no-existing-tree' };
    }
    const targetPrefix = normalizePath(targetDir).replace(/\/+$/, '') + '/';
    let manifestHasTargetFiles = false;
    for (const p of manifest) {
        if (normalizePath(p).startsWith(targetPrefix)) {
            manifestHasTargetFiles = true;
            break;
        }
    }
    if (!manifestHasTargetFiles) {
        return { wiped: false, skipped: 'empty-manifest' };
    }
    let archivePath;
    if (options.archiveDir) {
        archivePath = await archiveDirectory(fullTarget, options.archiveDir, basename(targetDir));
        await pruneArchives(options.archiveDir, basename(targetDir), options.archiveKeep ?? DEFAULT_ARCHIVE_KEEP);
    }
    await fs.rm(fullTarget, { recursive: true, force: true });
    return { wiped: true, archivePath };
}
async function archiveDirectory(srcDir, archiveBase, prefix) {
    const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace(/Z$/, '');
    const dest = join(archiveBase, `${prefix}-${stamp}`);
    await fs.mkdir(archiveBase, { recursive: true });
    // fs.cp with recursive copies the tree, preserves dir structure, no deps.
    await fs.cp(srcDir, dest, { recursive: true, force: true });
    return dest;
}
async function pruneArchives(archiveBase, prefix, keep) {
    if (!(await pathExists(archiveBase)))
        return;
    const entries = await fs.readdir(archiveBase);
    const stamped = entries.filter((e) => e.startsWith(`${prefix}-`)).sort().reverse();
    const stale = stamped.slice(keep);
    for (const old of stale) {
        await fs.rm(join(archiveBase, old), { recursive: true, force: true }).catch(() => { });
    }
}
async function pathExists(p) {
    try {
        await fs.access(p);
        return true;
    }
    catch {
        return false;
    }
}
function normalizePath(p) {
    return sep === '/' ? p : p.split(sep).join('/');
}
function basename(p) {
    const norm = normalizePath(p).replace(/\/+$/, '');
    const idx = norm.lastIndexOf('/');
    return idx >= 0 ? norm.slice(idx + 1) : norm;
}
//# sourceMappingURL=prepare-output.js.map