"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrototypeBuildService = exports.PrototypeBuildError = void 0;
const child_process_1 = require("child_process");
const fs_1 = __importDefault(require("fs"));
const promises_1 = __importDefault(require("fs/promises"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const logger_1 = require("../utils/logger");
const log = (0, logger_1.createLogger)('PrototypeBuildService');
/** Dirs never copied into the build scratch — they're either huge, regenerated,
 *  or history/VCS noise. Mirrors VariantHistoryService.COPY_EXCLUDE. */
const COPY_EXCLUDE = new Set([
    'node_modules',
    '.git',
    '.vite',
    '.rivet',
    'dist',
    'build',
    '.next',
    '.cache',
]);
class PrototypeBuildError extends Error {
    detail;
    constructor(message, 
    /** Tail of the failing step's stderr, for surfacing in the UI toast. */
    detail) {
        super(message);
        this.detail = detail;
        this.name = 'PrototypeBuildError';
    }
}
exports.PrototypeBuildError = PrototypeBuildError;
const INSTALL_TIMEOUT_MS = 180_000;
const BUILD_TIMEOUT_MS = 120_000;
/**
 * Builds a fresh-project (Vite) variant into a static bundle ready to upload to
 * the Prototype Preview Host.
 *
 * The variant snapshot under `.rivet/variants/.../files/` is treated as
 * immutable, so we copy it to an OS temp scratch dir and build there. We run
 * `vite build --base=./` directly (not `npm run build`) so a `tsc` type error
 * in agent-generated code doesn't block a purely visual deploy, and so asset
 * URLs are relative (the host serves under `/p/<deployId>/` and injects a
 * matching `<base href>`).
 */
class PrototypeBuildService {
    /** Build `filesDir` (the variant's `files/`) and return the packed bundle. */
    async build(filesDir) {
        const pkgJson = path_1.default.join(filesDir, 'package.json');
        if (!fs_1.default.existsSync(pkgJson)) {
            throw new PrototypeBuildError('This prototype has no package.json — only fresh Vite app prototypes can be deployed.');
        }
        const scratch = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'rivet-deploy-'));
        try {
            const projectDir = path_1.default.join(scratch, 'app');
            await promises_1.default.cp(filesDir, projectDir, {
                recursive: true,
                filter: (src) => !COPY_EXCLUDE.has(path_1.default.basename(src)),
            });
            const hasLockfile = fs_1.default.existsSync(path_1.default.join(projectDir, 'package-lock.json'));
            await this.runStep('npm', hasLockfile ? ['ci'] : ['install', '--no-audit', '--no-fund'], projectDir, INSTALL_TIMEOUT_MS, 'install dependencies');
            await this.runStep('npx', ['vite', 'build', '--base=./'], projectDir, BUILD_TIMEOUT_MS, 'build the prototype');
            const distDir = path_1.default.join(projectDir, 'dist');
            if (!fs_1.default.existsSync(path_1.default.join(distDir, 'index.html'))) {
                throw new PrototypeBuildError('Build completed but produced no dist/index.html — unable to deploy.');
            }
            return await this.packDir(distDir);
        }
        finally {
            await promises_1.default.rm(scratch, { recursive: true, force: true });
        }
    }
    /**
     * Pack an already-built static bundle (a variant's `files/` dir) for upload
     * without a build step. Used for history static-preview variants, which are
     * already servable static sites — there's nothing to compile.
     */
    async packStatic(filesDir) {
        if (!fs_1.default.existsSync(path_1.default.join(filesDir, 'index.html'))) {
            throw new PrototypeBuildError('This prototype has no index.html to deploy.');
        }
        return this.packDir(filesDir);
    }
    /** Walk a directory and return every file base64-encoded. */
    async packDir(distDir) {
        const files = [];
        let totalBytes = 0;
        const walk = async (dir, prefix) => {
            const entries = await promises_1.default.readdir(dir, { withFileTypes: true });
            for (const entry of entries) {
                if (COPY_EXCLUDE.has(entry.name))
                    continue;
                const abs = path_1.default.join(dir, entry.name);
                const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
                if (entry.isDirectory()) {
                    await walk(abs, rel);
                }
                else if (entry.isFile()) {
                    const body = await promises_1.default.readFile(abs);
                    totalBytes += body.length;
                    files.push({ path: rel, content: body.toString('base64') });
                }
            }
        };
        await walk(distDir, '');
        return { files, totalBytes };
    }
    runStep(cmd, args, cwd, timeoutMs, label) {
        return new Promise((resolve, reject) => {
            const proc = (0, child_process_1.spawn)(cmd, args, {
                cwd,
                env: { ...process.env },
                stdio: ['ignore', 'pipe', 'pipe'],
                detached: false,
            });
            // Keep only the tail of stderr — enough to explain a failure without
            // holding megabytes of npm/vite chatter in memory.
            let stderrTail = '';
            const appendTail = (data) => {
                stderrTail = (stderrTail + data.toString()).slice(-4_000);
            };
            // Drain stdout too: with `stdio: 'pipe'` and no consumer the OS pipe
            // buffer fills and the child blocks (same hazard as the dev-server spawn).
            proc.stdout?.on('data', (d) => {
                log.debug(`[deploy-build] ${d.toString().trim()}`);
            });
            proc.stderr?.on('data', (d) => {
                appendTail(d);
                log.debug(`[deploy-build] ${d.toString().trim()}`);
            });
            const timer = setTimeout(() => {
                try {
                    proc.kill('SIGTERM');
                }
                catch { /* ignore */ }
                setTimeout(() => {
                    try {
                        proc.kill('SIGKILL');
                    }
                    catch { /* ignore */ }
                }, 5_000).unref();
                reject(new PrototypeBuildError(`Timed out trying to ${label} (after ${Math.round(timeoutMs / 1000)}s).`, stderrTail.trim()));
            }, timeoutMs);
            proc.on('error', (err) => {
                clearTimeout(timer);
                reject(new PrototypeBuildError(`Failed to ${label}: ${err.message}`, stderrTail.trim()));
            });
            proc.on('close', (code) => {
                clearTimeout(timer);
                if (code === 0) {
                    resolve();
                }
                else {
                    reject(new PrototypeBuildError(`Failed to ${label} (exit ${code}).`, stderrTail.trim()));
                }
            });
        });
    }
}
exports.PrototypeBuildService = PrototypeBuildService;
//# sourceMappingURL=PrototypeBuildService.js.map