import { execFile, execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
import { platform } from "node:os";
import path from "node:path";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
function resolveExecutableFromWhichOutput(name, output, source) {
    const lines = output
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter((line) => line.length > 0);
    const candidate = lines.at(-1);
    if (!candidate) {
        return null;
    }
    if (!path.isAbsolute(candidate)) {
        console.warn(`[findExecutable] Ignoring non-absolute ${source} output for '${name}': ${JSON.stringify(candidate)}`);
        return null;
    }
    return candidate;
}
// `where.exe codex` lists every match across PATH in PATH order, including
// bare-name files (no extension). npm's old global install layout leaves a
// POSIX-style sh shim named just `codex` next to `codex.cmd`; Windows can
// nominally exec the bare file but it doesn't honor `--version` cleanly,
// surfacing as `Current version: unknown` + spurious CODEX_UPGRADE_REQUIRED.
// See #179.
//
// We must NOT globally prefer `.cmd`/`.exe` across the whole result — that
// would let a stale install in a later PATH directory override a fresh one
// earlier. Instead, walk results in PATH order and only skip a bare-name hit
// when the SAME directory also contains a PATHEXT sibling (i.e. the bare file
// is shadowed in place). All other bare hits stay eligible.
const WINDOWS_PATHEXT_PREFERENCE = [".cmd", ".bat", ".exe", ".com"];
function hasPathextSuffix(line) {
    const lower = line.toLowerCase();
    return WINDOWS_PATHEXT_PREFERENCE.some((ext) => lower.endsWith(ext));
}
function pickWindowsWhereLine(output) {
    const lines = output
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter((line) => line.length > 0);
    if (lines.length === 0)
        return null;
    // Group hits by the directory they live in so we can answer "does this
    // bare-name hit have a PATHEXT sibling next to it?" without losing PATH
    // order across groups. We use path.win32.dirname here because pickWindowsWhereLine
    // always processes Windows-shaped paths from where.exe, regardless of which
    // platform this code is being invoked on (e.g. unit tests on macOS).
    const siblingExtsByDir = new Map();
    for (const line of lines) {
        const dir = path.win32.dirname(line).toLowerCase();
        const lower = line.toLowerCase();
        const ext = WINDOWS_PATHEXT_PREFERENCE.find((e) => lower.endsWith(e));
        if (ext) {
            const set = siblingExtsByDir.get(dir) ?? new Set();
            set.add(ext);
            siblingExtsByDir.set(dir, set);
        }
    }
    // First pass: pick the first hit that is either a real Windows executable
    // or a bare-name file with no PATHEXT sibling in its directory.
    for (const line of lines) {
        if (hasPathextSuffix(line))
            return line;
        const dir = path.win32.dirname(line).toLowerCase();
        const siblings = siblingExtsByDir.get(dir);
        if (!siblings || siblings.size === 0)
            return line;
    }
    // All remaining hits are bare-name files shadowed by a PATHEXT sibling in
    // the same directory; fall through to the first line as a last resort.
    return lines[0] ?? null;
}
/**
 * On Unix we use `which`. On Windows we use `where.exe`.
 *
 * Both rely on the inherited process.env.PATH — on macOS/Linux, Electron
 * enriches it at startup via inheritLoginShellEnv(); on Windows, Electron
 * inherits the full user environment from Explorer.
 */
export function findExecutableSync(name, dependencies) {
    const trimmed = name.trim();
    if (!trimmed) {
        return null;
    }
    const deps = {
        execFileSync,
        existsSync,
        platform,
        ...dependencies,
    };
    if (trimmed.includes("/") || trimmed.includes("\\")) {
        return deps.existsSync(trimmed) ? trimmed : null;
    }
    if (deps.platform() === "win32") {
        try {
            const out = deps
                .execFileSync("where.exe", [trimmed], {
                encoding: "utf8",
                windowsHide: true,
            })
                .trim();
            return pickWindowsWhereLine(out);
        }
        catch {
            return null;
        }
    }
    try {
        return resolveExecutableFromWhichOutput(trimmed, deps.execFileSync("which", [trimmed], { encoding: "utf8" }).trim(), "which");
    }
    catch {
        return null;
    }
}
export function isCommandAvailableSync(command) {
    return findExecutableSync(command) !== null;
}
export async function findExecutable(name) {
    const trimmed = name.trim();
    if (!trimmed) {
        return null;
    }
    if (trimmed.includes("/") || trimmed.includes("\\")) {
        return existsSync(trimmed) ? trimmed : null;
    }
    if (platform() === "win32") {
        try {
            const { stdout } = await execFileAsync("where.exe", [trimmed], {
                encoding: "utf8",
                windowsHide: true,
            });
            return pickWindowsWhereLine(stdout.trim());
        }
        catch {
            return null;
        }
    }
    try {
        const { stdout } = await execFileAsync("which", [trimmed], { encoding: "utf8" });
        return resolveExecutableFromWhichOutput(trimmed, stdout.trim(), "which");
    }
    catch {
        return null;
    }
}
export async function isCommandAvailable(command) {
    return (await findExecutable(command)) !== null;
}
/**
 * When spawning with `shell: true` on Windows, the command is passed to
 * `cmd.exe /d /s /c "command args"`. The `/s` strips outer quotes, so a
 * command path with spaces (e.g. `C:\Program Files\...`) is split at the
 * space. Wrapping it in quotes produces the correct `"C:\Program Files\..." args`.
 */
export function quoteWindowsCommand(command) {
    if (process.platform !== "win32")
        return command;
    if (!command.includes(" "))
        return command;
    if (command.startsWith('"') && command.endsWith('"'))
        return command;
    return `"${command}"`;
}
/**
 * `spawn(..., { shell: true })` on Windows also passes argv through `cmd.exe`.
 * Any argument containing spaces must be quoted or it will be split before the
 * child process sees it.
 */
export function quoteWindowsArgument(argument) {
    if (process.platform !== "win32")
        return argument;
    if (!argument.includes(" "))
        return argument;
    if (argument.startsWith('"') && argument.endsWith('"'))
        return argument;
    return `"${argument}"`;
}
//# sourceMappingURL=executable.js.map