/**
 * Detection helpers for `neotoma doctor` and `neotoma hooks`.
 *
 * Returns a consolidated `HooksReport` describing, per hook-capable harness,
 * whether Neotoma's lifecycle hooks are already installed and whether any
 * non-Neotoma hook plugin is registered in the same file. This is the state
 * the activation flow in install.md reads before offering `neotoma hooks
 * install`.
 *
 * The module re-implements the small amount of "is this entry Neotoma's?"
 * logic that the per-package installers already own, intentionally duplicated
 * (rather than imported) so the CLI does not pay the cost of loading
 * `@neotoma/client` and its transitive deps just to read a JSON/TOML file.
 */
import fs from "node:fs/promises";
import { existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
export const HOOK_HARNESSES = [
    "cursor",
    "claude-code",
    "codex",
    "opencode",
    "claude-agent-sdk",
];
/** Neotoma's Cursor hook scripts, emitted by packages/cursor-hooks. */
const CURSOR_HOOK_SCRIPTS = [
    "session_start.js",
    "before_submit_prompt.js",
    "after_tool_use.js",
    "post_tool_use_failure.js",
    "stop.js",
];
function isNeotomaCursorEntry(entry) {
    if (!entry || typeof entry !== "object")
        return false;
    const obj = entry;
    const parts = [];
    if (typeof obj.command === "string")
        parts.push(obj.command);
    if (Array.isArray(obj.args)) {
        for (const arg of obj.args) {
            if (typeof arg === "string")
                parts.push(arg);
        }
    }
    return parts.some((part) => {
        if (part.includes("@neotoma/cursor-hooks"))
            return true;
        if (!part.includes("cursor-hooks"))
            return false;
        return CURSOR_HOOK_SCRIPTS.some((name) => part.endsWith(name) || part.includes(`dist/${name}`));
    });
}
function entryLabel(entry) {
    if (!entry || typeof entry !== "object")
        return "unknown";
    const obj = entry;
    if (typeof obj.name === "string" && obj.name.trim())
        return obj.name;
    if (typeof obj.command === "string" && obj.command.trim())
        return obj.command;
    if (Array.isArray(obj.args) && obj.args.length > 0) {
        const first = obj.args.find((a) => typeof a === "string" && a.trim());
        if (typeof first === "string") {
            const base = path.basename(first);
            return base || first;
        }
    }
    return "unknown";
}
async function readJson(filePath) {
    try {
        const raw = await fs.readFile(filePath, "utf8");
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
async function detectCursor(cwd) {
    const file = path.join(cwd, ".cursor", "hooks.json");
    const parsed = await readJson(file);
    if (!parsed || !existsSync(file)) {
        return { present: false, path: file, other_hook_plugins: [] };
    }
    let present = false;
    const others = new Set();
    const hooks = parsed.hooks ?? {};
    for (const entries of Object.values(hooks)) {
        if (!Array.isArray(entries))
            continue;
        for (const entry of entries) {
            if (isNeotomaCursorEntry(entry)) {
                present = true;
            }
            else {
                others.add(entryLabel(entry));
            }
        }
    }
    return { present, path: file, other_hook_plugins: [...others] };
}
async function detectClaudeCode(cwd) {
    // Claude Code plugins can live at project scope (.claude/plugins.json) or
    // user scope (~/.claude/plugins.json). We check both; `present` is true iff
    // either references the neotoma plugin id.
    const candidates = [
        path.join(cwd, ".claude", "plugins.json"),
        path.join(os.homedir(), ".claude", "plugins.json"),
    ];
    const others = new Set();
    let present = false;
    let pathUsed = null;
    for (const candidate of candidates) {
        if (!existsSync(candidate))
            continue;
        pathUsed = pathUsed ?? candidate;
        const parsed = await readJson(candidate);
        if (!parsed)
            continue;
        const list = [...(parsed.plugins ?? []), ...(parsed.installed ?? [])];
        for (const plugin of list) {
            const name = (plugin.name ?? plugin.id ?? "").toString();
            if (!name)
                continue;
            if (name === "neotoma" || name.startsWith("neotoma/") || name.includes("neotoma-")) {
                present = true;
            }
            else {
                others.add(name);
            }
        }
    }
    return { present, path: pathUsed, other_hook_plugins: [...others] };
}
async function detectCodex() {
    const file = path.join(os.homedir(), ".codex", "config.toml");
    let raw;
    try {
        raw = await fs.readFile(file, "utf8");
    }
    catch {
        return { present: false, path: file, other_hook_plugins: [] };
    }
    const present = raw.includes("# BEGIN neotoma-codex-hooks");
    // Best-effort: identify non-Neotoma hook entries outside the Neotoma-marked
    // block. Codex uses root-level `notify = […]` (not `[notify]`); legacy tables
    // and stray `[history]` still appear in some configs.
    const others = new Set();
    const withoutBlock = raw.replace(/# BEGIN neotoma-codex-hooks[\s\S]*?# END neotoma-codex-hooks/, "");
    const sectionRe = /^\s*\[(notify|history)\]/gm;
    let match;
    while ((match = sectionRe.exec(withoutBlock)) !== null) {
        others.add(`[${match[1]}] (external)`);
    }
    if (/^\s*notify\s*=/m.test(withoutBlock)) {
        others.add("notify (root argv, external)");
    }
    return { present, path: file, other_hook_plugins: [...others] };
}
async function detectOpenCode() {
    // OpenCode plugins are user-code imports, not a config file, so the best
    // we can do is look for a plugin module with "neotoma" in its name under
    // the standard plugin directories.
    const candidates = [
        path.join(os.homedir(), ".config", "opencode", "plugins"),
        path.join(os.homedir(), ".opencode", "plugins"),
    ];
    let present = false;
    let pathUsed = null;
    for (const dir of candidates) {
        if (!existsSync(dir))
            continue;
        pathUsed = pathUsed ?? dir;
        try {
            const entries = await fs.readdir(dir);
            if (entries.some((e) => /neotoma/i.test(e))) {
                present = true;
                break;
            }
        }
        catch {
            // ignore
        }
    }
    return { present, path: pathUsed, other_hook_plugins: [] };
}
async function detectClaudeAgentSdk() {
    // The SDK adapter is always a user-code import; there is no installable
    // state file to inspect. We surface `present: false` and `path: null` so
    // activation does not offer auto-install for this surface.
    return { present: false, path: null, other_hook_plugins: [] };
}
/** Collect per-harness installation state for activation / `neotoma hooks`. */
export async function detectHooks(opts = {}) {
    const cwd = opts.cwd ?? process.cwd();
    const [cursor, claudeCode, codex, opencode, claudeAgentSdk] = await Promise.all([
        detectCursor(cwd),
        detectClaudeCode(cwd),
        detectCodex(),
        detectOpenCode(),
        detectClaudeAgentSdk(),
    ]);
    const installed = {
        cursor,
        "claude-code": claudeCode,
        codex,
        opencode,
        "claude-agent-sdk": claudeAgentSdk,
    };
    const currentTool = opts.currentTool ?? null;
    const supported_by_tool = Boolean(currentTool && HOOK_HARNESSES.includes(currentTool));
    const currentInstalled = currentTool && installed[currentTool] ? installed[currentTool].present : false;
    const eligible_for_offer = supported_by_tool && !currentInstalled && Boolean(opts.mcpConfigured);
    return { supported_by_tool, installed, eligible_for_offer };
}
/** Narrow an arbitrary string to a `HookHarnessId` or `null`. */
export function toHookHarness(value) {
    if (typeof value !== "string")
        return null;
    return HOOK_HARNESSES.includes(value) ? value : null;
}
//# sourceMappingURL=hooks_detect.js.map