import { execAgentFile } from './invocation.js';
import { AGENT_DEFS } from './registry.js';
import { DEFAULT_MODEL_OPTION, rememberLiveModels } from './models.js';
import { applyAgentLaunchEnv, resolveAgentLaunch } from './launch.js';
import { spawnEnvForAgent } from './env.js';
import { probeAgentAuthStatus } from './auth.js';
import { agentCapabilities } from './capabilities.js';
import { installMetaForAgent } from './metadata.js';
async function fetchModels(def, resolvedBin, env) {
    if (typeof def.fetchModels === 'function') {
        try {
            const parsed = await def.fetchModels(resolvedBin, env);
            if (!parsed || parsed.length === 0)
                return def.fallbackModels;
            return parsed;
        }
        catch {
            return def.fallbackModels;
        }
    }
    if (!def.listModels)
        return def.fallbackModels;
    try {
        const { stdout } = await execAgentFile(resolvedBin, def.listModels.args, {
            env,
            timeout: def.listModels.timeoutMs ?? 5000,
            // Models lists from popular CLIs (e.g. opencode) easily exceed the
            // default 1MB buffer once you include every openrouter model. Bump
            // it so we don't truncate the listing.
            maxBuffer: 8 * 1024 * 1024,
        });
        const parsed = def.listModels.parse(String(stdout));
        // Empty / null parse result means the CLI didn't actually return a
        // usable list (e.g. cursor-agent's "No models available"); fall back
        // to the static hint so the picker isn't stuck on Default-only.
        if (!parsed || parsed.length === 0)
            return def.fallbackModels;
        return parsed;
    }
    catch {
        return def.fallbackModels;
    }
}
/**
 * Run the agent's `--version` probe and classify the result. The probe
 * has two distinct failure modes the catch arm has to discriminate:
 *
 *   - **Not invocable.** The OS rejected the spawn outright (ENOENT
 *     for a vanished target, EACCES for a stripped-x bit, ENOTDIR
 *     for a broken parent), OR the wrapper script spawned but its
 *     underlying interpreter / target is missing and the shim exits
 *     with code 127 ("command not found") / 126 ("not executable").
 *     127 is the canonical POSIX shell signal for "I ran but the
 *     thing I delegate to is gone"; 126 is the perm/not-a-binary
 *     sibling. Both shapes are reproducible by leftover npm bin
 *     shims, mise/nvm/fnm pointer files, and Windows `.CMD` shims
 *     whose target was uninstalled. We mark the agent unavailable
 *     so Settings does not advertise a ghost entry (issue #658,
 *     lefarcen review P2 on PR #1301).
 *
 *   - **Spawned but `--version` was unhappy.** The binary itself ran
 *     (any other rejection: timeout, generic non-zero exit, stderr
 *     noise) so the CLI is invocable; we just can't read a version
 *     string. Adapters whose `--version` flag is unsupported land
 *     here and must keep working with `version: null`.
 *
 * `child_process.execFile` reports OS-level rejections with a string
 * `err.code` (`'ENOENT'`, `'EACCES'`, `'ENOTDIR'`) and non-zero exit
 * codes with a *numeric* `err.code` equal to the exit status, so the
 * two arms below are unambiguous.
 */
async function probeVersionAtPath(def, resolved, env) {
    try {
        const { stdout } = await execAgentFile(resolved, def.versionArgs, {
            env,
            timeout: 3000,
        });
        const version = String(stdout).trim().split('\n')[0] ?? null;
        return { kind: 'spawned', version };
    }
    catch (err) {
        const code = err?.code;
        if (typeof code === 'string') {
            if (code === 'ENOENT' || code === 'EACCES' || code === 'ENOTDIR') {
                return { kind: 'not-invocable' };
            }
        }
        else if (typeof code === 'number' && (code === 126 || code === 127)) {
            return { kind: 'not-invocable' };
        }
        return { kind: 'spawned', version: null };
    }
}
function unavailableAgent(def) {
    return {
        ...stripFns(def),
        models: def.fallbackModels ?? [DEFAULT_MODEL_OPTION],
        available: false,
        ...installMetaForAgent(def.id),
    };
}
async function probe(def, configuredEnv = {}) {
    // Detection must probe the exact path the runtime will spawn, not just the
    // PATH-visible shim. This is load-bearing for Codex under nvm/fnm/mise:
    // the discovered `codex` entry is often a `#!/usr/bin/env node` wrapper
    // that is not invocable from a GUI-launched app's stripped PATH, while the
    // launch resolver can still upgrade it to the packaged native Codex binary.
    // If detection probes the shim but chat/run spawns the native binary, the
    // UI incorrectly reports "not installed" until the user pins CODEX_BIN by
    // hand even though the real launch path is healthy.
    const launch = resolveAgentLaunch(def, configuredEnv);
    if (!launch.selectedPath || !launch.launchPath) {
        return unavailableAgent(def);
    }
    const probeEnv = applyAgentLaunchEnv(spawnEnvForAgent(def.id, {
        ...process.env,
        ...(def.env || {}),
    }, configuredEnv), launch);
    const outcome = await probeVersionAtPath(def, launch.launchPath, probeEnv);
    if (outcome.kind === 'not-invocable') {
        return unavailableAgent(def);
    }
    // Probe `--help` once per agent and record which flags the installed CLI
    // advertises. Cached on `agentCapabilities` for buildArgs to consult.
    if (def.helpArgs && def.capabilityFlags) {
        const caps = {};
        try {
            const { stdout } = await execAgentFile(launch.launchPath, def.helpArgs, {
                env: probeEnv,
                timeout: 5000,
                maxBuffer: 4 * 1024 * 1024,
            });
            for (const [flag, key] of Object.entries(def.capabilityFlags)) {
                caps[key] = String(stdout).includes(flag);
            }
        }
        catch {
            // If --help fails, leave caps empty so buildArgs falls back to the safe
            // baseline (no optional flags).
        }
        agentCapabilities.set(def.id, caps);
    }
    const models = await fetchModels(def, launch.launchPath, probeEnv);
    const auth = await probeAgentAuthStatus(def.id, launch.launchPath, probeEnv);
    return {
        ...stripFns(def),
        models,
        available: true,
        path: launch.selectedPath,
        version: outcome.version,
        ...(auth
            ? {
                authStatus: auth.status,
                ...(auth.message ? { authMessage: auth.message } : {}),
            }
            : {}),
        ...installMetaForAgent(def.id),
    };
}
function stripFns(def) {
    // Drop the buildArgs / listModels closures but keep declarative metadata
    // (reasoningOptions, streamFormat, name, bin, etc.). `models` is
    // populated separately by `fetchModels`, so we strip the static
    // `fallbackModels` slot here too. `helpArgs` / `capabilityFlags` /
    // `fallbackBins` / `maxPromptArgBytes` / `env` are probe-or-spawn-only
    // metadata and shouldn't bleed into the API response either.
    const { buildArgs, listModels, fetchModels, fallbackModels, helpArgs, capabilityFlags, fallbackBins, maxPromptArgBytes, env, ...rest } = def;
    return rest;
}
export async function detectAgents(configuredEnvByAgent = {}) {
    const results = await Promise.all(AGENT_DEFS.map((def) => probe(def, configuredEnvByAgent?.[def.id] ?? {})));
    // Refresh the validation cache from whatever we just surfaced to the UI
    // so /api/chat can accept any model the user could have just picked,
    // including ones that only showed up after a CLI re-auth.
    for (const agent of results) {
        rememberLiveModels(agent.id, agent.models);
    }
    return results;
}
//# sourceMappingURL=detection.js.map