/**
 * install/install-kernel.mjs — locally-installed, self-contained kernel.
 *
 * Why this exists: skills ship verbatim into external projects. Every
 * SKILL.md that invokes the kernel uses `node .aperant/deps/node_modules/
 * @aperant/framework/bin/apt-tools.mjs` after the transform pipeline
 * substitution. This helper is what puts the package at that path.
 *
 * We run `npm install --prefix .aperant/deps @aperant/framework@<version>`
 * so the user gets a durable, version-pinned copy of the framework (plus
 * its deps) inside their project. No global mutation, no PATH assumption,
 * no per-call `npx` overhead once installed.
 *
 * Design notes:
 *   - `--prefix` routes the install to the isolated deps/ directory even
 *     though the user's project doesn't have a package.json for Aperant.
 *   - `--no-save` / `--no-package-lock` keep our install out of any user
 *     package.json + lockfile they might add later.
 *   - `--no-audit` / `--no-fund` silence the chatty-by-default footer so
 *     clack's frame stays clean.
 *   - Respects the user's `.npmrc` by default (so `@aperant:registry=...`
 *     set for Verdaccio testing Just Works).
 *   - Timeout + JSON-stable error envelope so init can decide whether to
 *     hard-fail or surface a graceful skip hint.
 */
import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
/**
 * Path from the project root to the installed kernel binary. SKILL.md /
 * agent transform substitution relies on this string — keep it in sync
 * with KERNEL_PATH_REPLACEMENT in transforms/_shared.mjs.
 */
export const KERNEL_BIN_REL_PATH = '.aperant/deps/node_modules/@aperant/framework/bin/apt-tools.mjs';
/**
 * Absolute path to the npm-like binary we should spawn. Prefers `npm`
 * because `--prefix` is universally supported there; other package
 * managers have subtly different semantics.
 *
 * @returns {string}
 */
function resolveNpmBin() {
    return process.env.APT_NPM_BIN || 'npm';
}
/**
 * Walk up from `startDir` looking for an `.npmrc`, parse it, and return
 * the `@aperant:registry=...` value if set. Mirrors npm's own config
 * lookup path but just for the one setting we care about.
 *
 * We need this because `npm install --prefix <dir>` re-anchors config
 * resolution to `<dir>` and ignores the user's project `.npmrc` at the
 * invocation cwd (observed empirically — the scoped registry set in
 * `/target/.npmrc` does not win). Passing `--@aperant:registry=<url>`
 * explicitly is the only reliable way to honor it.
 *
 * @param {string} startDir
 * @returns {string|null}
 */
function findAperantScopedRegistry(startDir) {
    const visited = new Set();
    /** @type {string[]} */
    const candidates = [];
    let dir = resolve(startDir);
    while (!visited.has(dir)) {
        visited.add(dir);
        candidates.push(join(dir, '.npmrc'));
        const parent = dirname(dir);
        if (parent === dir)
            break;
        dir = parent;
    }
    candidates.push(join(homedir(), '.npmrc')); // per-user fallback
    const re = /^\s*@aperant:registry\s*=\s*(\S+)\s*$/m;
    for (const path of candidates) {
        if (!existsSync(path))
            continue;
        try {
            const body = readFileSync(path, 'utf-8');
            const m = body.match(re);
            if (m)
                return m[1];
        }
        catch {
            // unreadable .npmrc — skip, continue upward
        }
    }
    return null;
}
/**
 * @typedef {Object} InstallKernelResult
 * @property {'installed' | 'skipped' | 'failed'} status
 * @property {string} kernelPath                Absolute path to apt-tools.mjs after install
 * @property {string} [reason]                   Human-readable skip/fail reason
 * @property {number} [durationMs]
 */
/**
 * Install `@aperant/framework@<version>` into `<targetDir>/.aperant/deps/`.
 * Safe to call multiple times — npm will reconcile against the existing
 * tree. Does not touch any files outside `.aperant/deps/`.
 *
 * @param {string} targetDir  project root
 * @param {string} version    exact semver to install (typically the version
 *                            of the framework currently running the init)
 * @param {Object} [opts]
 * @param {number} [opts.timeoutMs=180000]  hard ceiling on the npm call
 * @param {boolean} [opts.silent=true]      pass --silent to npm (false for --verbose debug runs)
 * @returns {InstallKernelResult}
 */
export function installKernel(targetDir, version, opts = {}) {
    const timeoutMs = opts.timeoutMs ?? 180_000;
    const silent = opts.silent !== false;
    const aperantDir = join(targetDir, '.aperant');
    const depsDir = join(aperantDir, 'deps');
    mkdirSync(depsDir, { recursive: true });
    const kernelPath = join(targetDir, KERNEL_BIN_REL_PATH);
    const npmBin = resolveNpmBin();
    // Bundle B — idempotency short-circuit. When the kernel package.json
    // already reports the requested version AND the kernel binary exists,
    // skip the npm spawn entirely. Returns the same `status: 'installed'`
    // envelope shape that downstream consumers branch on (init.mjs:1441,
    // init.mjs:1735, apt-update/SKILL.md Scenario D) — no new enum value
    // per D-02.
    const installedPkgPath = join(depsDir, 'node_modules', '@aperant', 'framework', 'package.json');
    if (existsSync(installedPkgPath) && existsSync(kernelPath)) {
        try {
            const installedPkg = JSON.parse(readFileSync(installedPkgPath, 'utf-8'));
            if (installedPkg?.version === version) {
                return {
                    status: 'installed',
                    kernelPath,
                    reason: 'already at target version',
                    durationMs: 0,
                };
            }
        }
        catch {
            // Malformed package.json — fall through to the npm install path
            // (npm will reconcile / repair).
        }
    }
    const t0 = Date.now();
    const args = [
        'install',
        `@aperant/framework@${version}`,
        '--prefix',
        depsDir,
        '--no-save',
        '--no-package-lock',
        '--no-audit',
        '--no-fund',
    ];
    if (silent)
        args.push('--silent');
    // Propagate the user's @aperant-scoped registry override if any — covers
    // Verdaccio testing, internal registries, GitHub Packages, etc. `--prefix`
    // would otherwise drop the project .npmrc on the floor.
    const scopedRegistry = findAperantScopedRegistry(targetDir);
    if (scopedRegistry) {
        args.push(`--@aperant:registry=${scopedRegistry}`);
    }
    const result = spawnSync(npmBin, args, {
        cwd: targetDir, // cwd so any project-local .npmrc is honored
        stdio: silent ? ['ignore', 'pipe', 'pipe'] : 'inherit',
        timeout: timeoutMs,
        encoding: 'utf-8',
    });
    const durationMs = Date.now() - t0;
    if (result.error) {
        return {
            status: 'failed',
            kernelPath,
            reason: `npm spawn failed: ${result.error.message}`,
            durationMs,
        };
    }
    if (result.status !== 0) {
        const stderr = (result.stderr || '').trim().slice(-600); // last 600 chars
        return {
            status: 'failed',
            kernelPath,
            reason: `npm install --prefix exited with ${result.status}: ${stderr || 'no stderr'}`,
            durationMs,
        };
    }
    if (!existsSync(kernelPath)) {
        return {
            status: 'failed',
            kernelPath,
            reason: `npm install reported success but kernel not found at ${kernelPath}`,
            durationMs,
        };
    }
    return { status: 'installed', kernelPath, durationMs };
}
//# sourceMappingURL=install-kernel.mjs.map