"use strict";
/**
 * HooksManager — manages ccweb entries in CLI tool settings files
 *
 * install() is idempotent: always calls uninstall() first, then adds fresh hooks.
 * This handles the crash-without-cleanup scenario correctly.
 *
 * Hook commands include "# ccweb-hook" marker at the end so uninstall()
 * can precisely identify and remove them without touching user-defined hooks.
 *
 * Adapter coverage:
 *   - Claude: full hook lifecycle (PreToolUse / PostToolUse / Stop / PermissionRequest)
 *     via `~/.claude/settings.json` + statusLine
 *   - Gemini: partial (PostToolUse + Stop) via its own settings.json
 *   - Codex / OpenCode / Qwen: `getHooksSettingsPath()` returns null, skipping.
 *     Codex uses `~/.codex/config.toml` `notify = [argv...]` which only fires
 *     at turn-complete — semantically incompatible with the pre-tool hook
 *     model ccweb is built around. Not a missed abstraction; deliberate.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.HooksManager = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const adapters_1 = require("./adapters");
const logger_1 = require("./logger");
const log = (0, logger_1.modLogger)('hooks');
const CCWEB_MARKER = '# ccweb-hook';
function readSettings(settingsPath) {
    if (!fs.existsSync(settingsPath))
        return {};
    try {
        const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
        // Refuse non-plain-object roots — a settings.json that parsed to an
        // array or primitive would survive readSettings, get a named property
        // assigned (`settings.hooks = …`), and stringify back into invalid
        // structure that Claude Code can no longer parse.
        if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
            log.warn({ settingsPath }, 'settings file is not a plain object — hook management skipped');
            return null;
        }
        return parsed;
    }
    catch {
        log.warn({ settingsPath }, 'settings file has invalid JSON — hook management skipped');
        return null; // null signals corruption to callers
    }
}
function atomicWrite(settingsPath, data) {
    const dir = path.dirname(settingsPath);
    if (!fs.existsSync(dir))
        fs.mkdirSync(dir, { recursive: true });
    const tmp = settingsPath + `.tmp.${process.pid}`;
    fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
    fs.renameSync(tmp, settingsPath);
}
class HooksManager {
    constructor(portFile) {
        this.portFile = portFile;
    }
    /** Remove all ccweb hook entries (identified by CCWEB_MARKER) for a specific adapter */
    uninstallForAdapter(adapter) {
        const settingsPath = adapter.getHooksSettingsPath();
        if (!settingsPath)
            return;
        const settings = readSettings(settingsPath);
        if (settings === null)
            return; // corrupted — skip to avoid data loss
        const hooks = (settings.hooks ?? {});
        let changed = false;
        for (const event of adapter.getHookEvents()) {
            const list = (hooks[event] ?? []);
            const cleaned = list
                .map((group) => ({
                ...group,
                hooks: (group.hooks ?? []).filter((h) => !h.command?.includes(CCWEB_MARKER)),
            }))
                .filter((group) => (group.hooks?.length ?? 0) > 0);
            if (JSON.stringify(cleaned) !== JSON.stringify(list)) {
                hooks[event] = cleaned;
                changed = true;
            }
        }
        // Clean up statusLine if it was set by ccweb
        if (adapter.tool === 'claude' && settings.statusLine) {
            const sl = settings.statusLine;
            if (sl.command?.includes('/api/hooks/context')) {
                delete settings.statusLine;
                changed = true;
            }
        }
        if (changed) {
            settings.hooks = hooks;
            atomicWrite(settingsPath, settings);
            log.info({ tool: adapter.tool }, 'uninstalled ccweb hooks');
        }
    }
    /** Install hooks for a specific adapter */
    installForAdapter(adapter) {
        this.uninstallForAdapter(adapter); // always clean first — handles crash-without-cleanup
        const settingsPath = adapter.getHooksSettingsPath();
        if (!settingsPath)
            return;
        const settings = readSettings(settingsPath);
        if (settings === null)
            return; // corrupted — skip to avoid data loss
        const hooks = (settings.hooks ?? {});
        for (const event of adapter.getHookEvents()) {
            const command = adapter.buildHookCommand(event, this.portFile);
            if (!command)
                continue;
            const list = (hooks[event] ?? []);
            // PermissionRequest holds the tool until the user decides — allow effectively unbounded wait.
            const hookEntry = { type: 'command', command };
            if (event === 'PermissionRequest')
                hookEntry.timeout = 86400; // 24h, i.e. "no practical limit"
            list.push({ hooks: [hookEntry] });
            hooks[event] = list;
        }
        settings.hooks = hooks;
        // Install status line for context tracking (Claude only)
        if (adapter.tool === 'claude') {
            const statusLineCmd = `jq -r '{dir: .cwd, context_window: .context_window}' | ` +
                `curl -sf -X POST "http://localhost:$(cat ${this.portFile})/api/hooks/context" ` +
                `-H "Content-Type: application/json" -d @- || true`;
            settings.statusLine = { type: 'command', command: statusLineCmd };
        }
        atomicWrite(settingsPath, settings);
        log.info({ tool: adapter.tool }, 'installed ccweb hooks');
    }
    /** Remove all ccweb hook entries from all supported tools */
    uninstall() {
        for (const tool of ['claude', 'opencode', 'codex', 'qwen', 'gemini', 'terminal']) {
            this.uninstallForAdapter((0, adapters_1.getAdapter)(tool));
        }
    }
    /** Idempotent install: remove stale entries first, then add fresh hooks for all supported tools */
    install() {
        for (const tool of ['claude', 'opencode', 'codex', 'qwen', 'gemini', 'terminal']) {
            this.installForAdapter((0, adapters_1.getAdapter)(tool));
        }
    }
    isInstalled() {
        // Check the primary tool (Claude) — if its hooks are installed, consider all installed
        const adapter = (0, adapters_1.getAdapter)('claude');
        const settingsPath = adapter.getHooksSettingsPath();
        if (!settingsPath)
            return false;
        const settings = readSettings(settingsPath);
        if (settings === null)
            return false;
        const hooks = (settings.hooks ?? {});
        const events = adapter.getHookEvents();
        if (events.length === 0)
            return false;
        const list = (hooks[events[0]] ?? []);
        return list.some((g) => g.hooks?.some((h) => h.command?.includes(CCWEB_MARKER)));
    }
}
exports.HooksManager = HooksManager;
//# sourceMappingURL=hooks-manager.js.map