import fs from 'node:fs';
import path from 'node:path';
import { loadCompiledHooks } from '../hook/loader.js';
import { evaluateHook, formatRejection } from '../hook/runtime.js';
/**
 * The exit code Claude Code's PreToolUse hook convention treats as "block
 * the tool call." Exported so test suites assert against the same constant
 * rather than against a magic number that could drift.
 */
export const EXIT_ALLOW = 0;
export const PRE_TOOL_USE_BLOCK_EXIT_CODE = 2;
export async function hookRunCommand(opts) {
    const { loadConfig, resolveConfigPath } = await import('../utils.js');
    const { sanitize } = await import('@mmnto/totem');
    const cwd = process.cwd();
    const configPath = resolveConfigPath(cwd);
    const config = await loadConfig(configPath);
    const configRoot = path.dirname(configPath);
    const manifestPath = path.join(configRoot, config.totemDir, 'compiled-hooks.json');
    const installedPackVersions = resolveInstalledPackVersions(configRoot);
    const result = executeHookRun({
        manifestPath,
        installedPackVersions,
        payload: { tool: opts.tool, args: opts.args },
    });
    // Sanitize every stderr line at the terminal boundary — the lines flow
    // through `executeHookRun` carrying pack-supplied content (rejection
    // messages, recovery hints, hook ids, loader warnings interpolating pack
    // names). Sanitizing once at the wrapper is simpler than instrumenting
    // every emitter, and the pure-function tests already assert structural
    // shape on the unsanitized form.
    for (const line of result.stderr) {
        console.error(sanitize(line));
    }
    if (result.exitCode !== 0) {
        // Set process.exitCode rather than calling process.exit() so the top-level
        // handler stays in control of shutdown ordering (per the codebase guideline
        // against process.exit() inside command bodies). PreToolUse semantics are
        // preserved — Node exits with the assigned code once the event loop drains.
        process.exitCode = result.exitCode;
        return;
    }
}
/**
 * Pure synchronous core. Given resolved inputs, returns the exit code and
 * stderr lines the wrapper should emit. No process state is touched.
 *
 * Two-phase contract:
 * 1. Load + emit diagnostics (warnings, structured errors) regardless of
 *    whether any hooks loaded successfully.
 * 2. Walk loaded hooks until the first reject; allow when none reject.
 *
 * Order in `stderr` is preserved: load-time diagnostics come before any
 * rejection line, so an operator inspecting stderr sees the staleness or
 * schema-mismatch context that may explain why a rejection fired.
 */
export function executeHookRun(inputs) {
    const { hooks, warnings, errors } = loadCompiledHooks({
        manifestPath: inputs.manifestPath,
        installedPackVersions: inputs.installedPackVersions,
    });
    const stderr = [];
    for (const w of warnings)
        stderr.push(w);
    for (const e of errors)
        stderr.push(`[totem:hook-error] ${e.message}`);
    for (const rule of hooks) {
        const decision = evaluateHook(rule, inputs.payload);
        if (decision.decision === 'reject') {
            stderr.push(formatRejection(decision));
            return { exitCode: PRE_TOOL_USE_BLOCK_EXIT_CODE, stderr };
        }
    }
    return { exitCode: 0, stderr };
}
/**
 * Scan `<projectRoot>/node_modules/@mmnto/pack-*` and read each pack's
 * `package.json` version field. Returns a `packName → version` map for
 * the compiled-hooks loader's staleness check.
 *
 * Bounded cost: one `readdirSync` plus N `readFileSync` calls (N = number
 * of installed `@mmnto/pack-*` packages, typically <5). Expected per-pack
 * read/parse failures (ENOENT/ENOTDIR/EPERM/EACCES on the package.json,
 * SyntaxError on malformed JSON) are skipped so the loader can emit
 * `[totem:hook-stale]` warnings for missing/invalid packs — the correct
 * signal for an operator who has uninstalled a pack but not re-run
 * `totem sync`. Unexpected exceptions (IO timeout, OOM, etc.) are
 * rethrown to surface real faults. The outer `readdirSync` on the
 * `@mmnto` scope dir applies the same discipline: ENOENT returns empty;
 * any other errno surfaces as a thrown error.
 *
 * Workspace setups (pnpm workspace, yarn workspaces) symlink the pack
 * directory into `node_modules/@mmnto/`, so the `readdirSync` + JSON
 * read traverses the symlink transparently and reads the source-of-truth
 * `package.json`. No special case needed.
 */
export function resolveInstalledPackVersions(projectRoot) {
    const result = {};
    const scopeDir = path.join(projectRoot, 'node_modules', '@mmnto');
    let entries;
    try {
        entries = fs.readdirSync(scopeDir, { withFileTypes: true });
        // totem-context: intentional — ENOENT on the scope dir is the valid
        // fresh-repo state (no `@mmnto` packs installed). Any other errno
        // (EACCES, permission denied, etc.) is a real fault that should surface.
    }
    catch (err) {
        if (err.code === 'ENOENT') {
            return result;
        }
        throw err;
    }
    for (const entry of entries) {
        // Don't pre-filter on `entry.isDirectory()`: in pnpm/yarn workspaces a
        // pack is symlinked into `node_modules/@mmnto/`, and a Dirent for a
        // symlink reports `isDirectory()` false even when the target is a
        // directory. The `readFileSync` below transparently traverses the
        // symlink and the surrounding `try/catch` discards any path that does
        // not resolve to a readable package.json.
        if (!entry.name.startsWith('pack-'))
            continue;
        const packName = `@mmnto/${entry.name}`;
        try {
            const pkgRaw = fs.readFileSync(path.join(scopeDir, entry.name, 'package.json'), 'utf8');
            const pkg = JSON.parse(pkgRaw);
            if (typeof pkg.version === 'string') {
                result[packName] = pkg.version;
            }
            // totem-context: intentional — the expected per-pack failure modes
            // (missing/corrupt package.json, permission-quirk on individual pack
            // dirs) are non-fatal; the loader emits a stale warning for any pack
            // referenced in the manifest but missing from this map. Unusual
            // errors (IO timeout, OOM, etc.) are not in the expected set and
            // surface as a real fault so debug consumers can find them.
        }
        catch (err) {
            const code = err.code;
            if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EPERM' || code === 'EACCES') {
                continue;
            }
            if (err instanceof SyntaxError) {
                continue;
            }
            throw err;
        }
    }
    return result;
}
//# sourceMappingURL=hook-run.js.map