import { execFile, spawn, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { open, readFile, stat } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describeCodexCli, findExecutableOnPath, resolveCodexCli } from "../agents/codex/codex-cli.js";
import { findLatestDatabase } from "../agents/codex/codex-state.js";
import { describeClaudeCodeCli, resolveClaudeCodeCli } from "../agents/claude-code/claude-code-cli.js";
import { describeHermesCli, resolveHermesCli } from "../agents/hermes/hermes-cli.js";
import { describeOpenClawCli, resolveOpenClawCli } from "../agents/openclaw/openclaw-cli.js";
import { describePiCli, resolvePiCli } from "../agents/pi/pi-cli.js";
import { normalizeCursorLimit } from "../core/pagination.js";
const APP_NAME = "nordrelay";
const PACKAGE_NAME = "@nordbyte/nordrelay";
const CODEX_PACKAGE_NAME = "@openai/codex";
const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
const LEGACY_PI_PACKAGE_NAME = "@mariozechner/pi-coding-agent";
const HERMES_PACKAGE_NAME = "hermes-agent";
const OPENCLAW_PACKAGE_NAME = "openclaw";
const CLAUDE_CODE_PACKAGE_NAME = "@anthropic-ai/claude-code";
const CLAUDE_CODE_SDK_PACKAGE_NAME = "@anthropic-ai/claude-agent-sdk";
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
const SECRET_RE = /(bot|token|api[_-]?key|authorization|bearer|password|secret)(["'=: ]+)([^\s"',]+)/gi;
const DEFAULT_VERSION_CACHE_TTL_MS = 60 * 60 * 1000;
const DEFAULT_CLI_VERSION_CACHE_TTL_MS = 60 * 1000;
const cliVersionCache = new Map();
export function clearAgentCliVersionCache(agentId, options = {}) {
    const env = process.env;
    const paths = new Set();
    const add = (filePath) => {
        if (filePath)
            paths.add(filePath);
    };
    if (!agentId || agentId === "codex")
        add(resolveCodexCli(env).path);
    if (!agentId || agentId === "pi")
        add(resolvePiCli(env, options.piCliPath).path);
    if (!agentId || agentId === "hermes")
        add(resolveHermesCli(env, options.hermesCliPath).path);
    if (!agentId || agentId === "openclaw")
        add(resolveOpenClawCli(env, options.openClawCliPath).path);
    if (!agentId || agentId === "claudeCode" || agentId === "claude-code")
        add(resolveClaudeCodeCli(env, options.claudeCodeCliPath).path);
    let cleared = 0;
    for (const filePath of paths) {
        if (cliVersionCache.delete(filePath))
            cleared += 1;
    }
    return cleared;
}
export function getConnectorHome() {
    return process.env.NORDRELAY_HOME || DEFAULT_HOME;
}
export function getConnectorStatePath() {
    return process.env.NORDRELAY_STATE_FILE || path.join(getConnectorHome(), "state.json");
}
export function getConnectorLogPath() {
    return path.join(getConnectorHome(), "nordrelay.log");
}
export function getUpdateLogPath() {
    return path.join(getConnectorHome(), "update.log");
}
export function getAgentUpdateLogPath(home = getConnectorHome()) {
    return path.join(home, "agent-updates.log");
}
export async function readConnectorState() {
    try {
        return JSON.parse(await readFile(getConnectorStatePath(), "utf8"));
    }
    catch {
        return {};
    }
}
export async function readLogTail(lines = 80, filePath = getConnectorLogPath()) {
    const boundedLines = Math.min(Math.max(lines, 1), 300);
    try {
        const contents = await readFile(filePath, "utf8");
        return redactSecrets(contents.split(/\r?\n/).slice(-boundedLines).join("\n").trim());
    }
    catch (error) {
        return `Cannot read log: ${error instanceof Error ? error.message : String(error)}`;
    }
}
export async function readFormattedLogTail(linesOrOptions = 80, filePath = getConnectorLogPath()) {
    const options = typeof linesOrOptions === "number" ? { lines: linesOrOptions, limit: linesOrOptions } : linesOrOptions;
    const boundedLines = normalizeCursorLimit(options.limit ?? options.lines, 80, 300);
    const maxScanLines = normalizeCursorLimit(options.maxScanLines, 20_000, 100_000);
    const cursorOffset = parseLogCursor(options.cursor);
    try {
        const { lines: rawLines, stats, truncated } = await readRecentNonEmptyLines(filePath, maxScanLines);
        const filtered = parseFormattedLogEntries(rawLines.map(formatLogLine)).filter((entry) => logEntryMatches(entry, options));
        const pageEnd = Math.max(0, filtered.length - cursorOffset);
        const pageStart = Math.max(0, pageEnd - boundedLines);
        const page = filtered.slice(pageStart, pageEnd);
        const pageLines = page.map((entry) => redactSecrets(entry.line));
        const hasNext = pageStart > 0;
        const nextOffset = cursorOffset + page.length;
        return {
            filePath,
            requestedLines: boundedLines,
            lineCount: page.length,
            scannedLineCount: rawLines.length,
            totalLineCount: filtered.length,
            truncated,
            updatedAt: stats?.mtime ?? null,
            entries: page.map((entry, index) => ({ ...entry, line: pageLines[index] ?? "" })),
            pagination: {
                limit: boundedLines,
                nextCursor: hasNext ? String(nextOffset) : null,
                hasNext,
                total: filtered.length,
            },
            plain: pageLines.join("\n"),
        };
    }
    catch (error) {
        const message = `Cannot read log: ${error instanceof Error ? error.message : String(error)}`;
        return {
            filePath,
            requestedLines: boundedLines,
            lineCount: 1,
            scannedLineCount: 0,
            totalLineCount: 0,
            truncated: false,
            updatedAt: null,
            entries: [{ line: message, level: "ERROR", time: 0 }],
            pagination: {
                limit: boundedLines,
                nextCursor: null,
                hasNext: false,
                total: 0,
            },
            plain: message,
        };
    }
}
async function readRecentNonEmptyLines(filePath, maxLines) {
    const stats = await stat(filePath);
    if (stats.size === 0) {
        return { lines: [], stats, truncated: false };
    }
    const file = await open(filePath, "r");
    const collected = [];
    const chunkSize = 64 * 1024;
    let position = stats.size;
    let carry = "";
    let truncated = false;
    try {
        while (position > 0 && collected.length <= maxLines) {
            const readSize = Math.min(chunkSize, position);
            position -= readSize;
            const buffer = Buffer.allocUnsafe(readSize);
            await file.read(buffer, 0, readSize, position);
            const text = buffer.toString("utf8") + carry;
            const parts = text.split(/\r?\n/);
            carry = parts.shift() ?? "";
            for (let index = parts.length - 1; index >= 0; index -= 1) {
                const line = parts[index] ?? "";
                if (line.trim().length > 0) {
                    collected.push(line);
                }
                if (collected.length > maxLines) {
                    break;
                }
            }
        }
        if (position === 0 && carry.trim().length > 0) {
            collected.push(carry);
        }
        truncated = position > 0 || collected.length > maxLines;
        return { lines: collected.slice(0, maxLines).reverse(), stats, truncated };
    }
    finally {
        await file.close();
    }
}
export function clearLogFile(filePath = getConnectorLogPath()) {
    mkdirSync(path.dirname(filePath), { recursive: true });
    writeFileSync(filePath, "", "utf8");
    return {
        filePath,
        clearedAt: new Date(),
    };
}
export async function getPackageVersion() {
    try {
        const pkg = JSON.parse(await readFile(path.join(getSourceRoot(), "package.json"), "utf8"));
        return typeof pkg.version === "string" ? pkg.version : "unknown";
    }
    catch {
        return "unknown";
    }
}
export async function getVersionChecks(options = {}) {
    const [nordrelayVersion, cliVersions] = await Promise.all([
        getPackageVersion(),
        resolveAgentCliVersions(options),
    ]);
    const [nordrelay, codex, pi, hermes, openclaw, claudeCode,] = await Promise.all([
        buildVersionCheck({
            label: "NordRelay",
            packageName: PACKAGE_NAME,
            installedLabel: nordrelayVersion,
            installedVersion: extractVersion(nordrelayVersion),
            forceRefresh: options.forceRefresh,
        }),
        buildVersionCheck({
            label: "Codex",
            packageName: CODEX_PACKAGE_NAME,
            installedLabel: cliVersions.codexVersionLabel,
            installedVersion: extractVersion(cliVersions.codexVersionLabel),
            notInstalled: cliVersions.codexVersionLabel === "not installed",
            forceRefresh: options.forceRefresh,
        }),
        buildVersionCheck({
            label: "Pi",
            packageName: PI_PACKAGE_NAME,
            installedLabel: cliVersions.piVersionLabel,
            installedVersion: extractVersion(cliVersions.piVersionLabel),
            notInstalled: cliVersions.piVersionLabel === "not installed",
            detail: cliVersions.legacyPiPackageVersion ? `Legacy package ${LEGACY_PI_PACKAGE_NAME} is present; current package is ${PI_PACKAGE_NAME}.` : undefined,
            forceRefresh: options.forceRefresh,
        }),
        buildHermesVersionCheck(cliVersions.hermesVersionLabel, { forceRefresh: options.forceRefresh }),
        buildVersionCheck({
            label: "OpenClaw",
            packageName: OPENCLAW_PACKAGE_NAME,
            installedLabel: cliVersions.openClawVersionLabel,
            installedVersion: extractVersion(cliVersions.openClawVersionLabel),
            notInstalled: cliVersions.openClawVersionLabel === "not installed",
            forceRefresh: options.forceRefresh,
        }),
        buildVersionCheck({
            label: "Claude Code",
            packageName: cliVersions.claudeCodePackageName,
            installedLabel: cliVersions.claudeCodeVersionLabel,
            installedVersion: extractVersion(cliVersions.claudeCodeVersionLabel),
            notInstalled: cliVersions.claudeCodeVersionLabel === "not installed",
            forceRefresh: options.forceRefresh,
        }),
    ]);
    return {
        nordrelay,
        codex,
        pi,
        hermes,
        openclaw,
        claudeCode,
    };
}
export async function getConnectorHealth(options = {}) {
    const [rawState, version, cliVersions] = await Promise.all([
        readConnectorState(),
        getPackageVersion(),
        resolveAgentCliVersions(options),
    ]);
    const pidRunning = isProcessRunning(rawState.pid);
    const appPidRunning = isProcessRunning(rawState.appPid);
    const state = normalizeConnectorState(rawState, pidRunning, appPidRunning);
    return {
        version,
        state,
        pidRunning,
        appPidRunning,
        codexCli: describeCodexCli(cliVersions.codexCli),
        codexCliPath: cliVersions.codexCli.path ?? null,
        codexCliVersion: cliVersions.codexVersionLabel,
        piCli: describePiCli(cliVersions.piCli),
        piCliPath: cliVersions.piCli.path ?? null,
        piCliVersion: cliVersions.piVersionLabel,
        hermesCli: describeHermesCli(cliVersions.hermesCli),
        hermesCliPath: cliVersions.hermesCli.path ?? null,
        hermesCliVersion: cliVersions.hermesVersionLabel,
        openClawCli: describeOpenClawCli(cliVersions.openClawCli),
        openClawCliPath: cliVersions.openClawCli.path ?? null,
        openClawCliVersion: cliVersions.openClawVersionLabel,
        claudeCodeCli: describeClaudeCodeCli(cliVersions.claudeCodeCli),
        claudeCodeCliPath: cliVersions.claudeCodeCli.path ?? null,
        claudeCodeCliVersion: cliVersions.claudeCodeVersionLabel,
        stateFile: getConnectorStatePath(),
        logFile: getConnectorLogPath(),
        databasePath: findLatestDatabase(),
        uptimeSeconds: Math.max(0, Math.round(process.uptime())),
    };
}
export function spawnConnectorRestart() {
    const script = getWrapperScriptPath();
    const child = spawn(process.execPath, [script, "restart", "--keep-pending-updates"], {
        cwd: getSourceRoot(),
        detached: true,
        env: buildConnectorChildEnv(),
        stdio: "ignore",
    });
    child.unref();
}
export function spawnSelfUpdate() {
    const sourceRoot = getSourceRoot();
    const script = getWrapperScriptPath();
    const updateLog = getUpdateLogPath();
    const method = detectSelfUpdateMethod(sourceRoot);
    mkdirSync(path.dirname(updateLog), { recursive: true });
    const child = spawn(process.execPath, [
        script,
        "update",
        "--method",
        method,
        "--home",
        getConnectorHome(),
        "--keep-pending-updates",
    ], {
        cwd: sourceRoot,
        detached: true,
        env: buildConnectorChildEnv(),
        stdio: "ignore",
    });
    child.unref();
    return {
        logPath: updateLog,
        method,
        sourceRoot,
        summary: method === "npm"
            ? `Install latest ${PACKAGE_NAME} with npm, verify the CLI, and restart.`
            : "Pull origin/main, install dependencies, run check, tests, build, and restart.",
    };
}
export function buildConnectorChildEnv(extra = {}, base = process.env) {
    const env = { ...base, ...extra };
    env.PATH = prependUniquePathDirs(env.PATH, [
        path.dirname(process.execPath),
        resolveNpmGlobalBinDir(env),
        ...commonNpmGlobalBinDirs(env),
    ]);
    return env;
}
export function getSourceRoot() {
    return process.env.NORDRELAY_SOURCE_ROOT || process.cwd();
}
export function detectSelfUpdateMethod(sourceRoot = getSourceRoot()) {
    const override = process.env.NORDRELAY_UPDATE_METHOD?.trim().toLowerCase();
    if (override === "npm" || override === "git") {
        return override;
    }
    return existsSync(path.join(sourceRoot, ".git")) ? "git" : "npm";
}
function getWrapperScriptPath() {
    const sourceRoot = getSourceRoot();
    const script = path.join(sourceRoot, "plugins", APP_NAME, "scripts", `${APP_NAME}.mjs`);
    if (existsSync(script)) {
        return script;
    }
    return path.join(process.cwd(), "plugins", APP_NAME, "scripts", `${APP_NAME}.mjs`);
}
function isProcessRunning(pid) {
    if (!Number.isInteger(pid) || pid <= 0) {
        return false;
    }
    try {
        process.kill(pid, 0);
        return true;
    }
    catch {
        return false;
    }
}
function normalizeConnectorState(state, pidRunning, appPidRunning) {
    const stoppedSignal = state.signal === "SIGTERM" || state.signal === "SIGINT";
    if (state.status === "error" && stoppedSignal && !state.error && !pidRunning && !appPidRunning) {
        return { ...state, status: "stopped" };
    }
    return state;
}
function redactSecrets(text) {
    return text.replace(SECRET_RE, "$1$2[redacted]");
}
async function resolveAgentCliVersions(options = {}) {
    const codexCli = resolveCodexCli();
    const piCli = resolvePiCli(process.env, options.piCliPath);
    const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
    const openClawCli = resolveOpenClawCli(process.env, options.openClawCliPath);
    const claudeCodeCli = resolveClaudeCodeCli(process.env, options.claudeCodeCliPath);
    const legacyPiPackageVersion = readInstalledPackageVersion(LEGACY_PI_PACKAGE_NAME);
    const [codexVersionLabel, piVersionLabel, hermesVersionLabel, openClawVersionLabel, claudeCodeVersionLabel,] = await Promise.all([
        codexCli.path ? detectCliVersion(codexCli.path, { forceRefresh: options.forceRefresh }) : Promise.resolve(readInstalledPackageVersion(CODEX_PACKAGE_NAME) ?? "not installed"),
        piCli.path ? detectCliVersion(piCli.path, { forceRefresh: options.forceRefresh }) : Promise.resolve(readInstalledPackageVersion(PI_PACKAGE_NAME) ?? legacyPiPackageVersion ?? "not installed"),
        hermesCli.path ? detectCliVersion(hermesCli.path, { forceRefresh: options.forceRefresh }) : Promise.resolve("not installed"),
        openClawCli.path ? detectCliVersion(openClawCli.path, { forceRefresh: options.forceRefresh }) : Promise.resolve("not installed"),
        claudeCodeCli.path ? detectCliVersion(claudeCodeCli.path, { forceRefresh: options.forceRefresh }) : Promise.resolve(readInstalledPackageVersion(CLAUDE_CODE_SDK_PACKAGE_NAME) ?? "bundled"),
    ]);
    return {
        codexCli,
        piCli,
        hermesCli,
        openClawCli,
        claudeCodeCli,
        codexVersionLabel,
        piVersionLabel,
        hermesVersionLabel,
        openClawVersionLabel,
        claudeCodeVersionLabel,
        claudeCodePackageName: claudeCodeCli.path ? CLAUDE_CODE_PACKAGE_NAME : CLAUDE_CODE_SDK_PACKAGE_NAME,
        legacyPiPackageVersion,
    };
}
async function detectCliVersion(commandPath, options = {}) {
    if (!commandPath) {
        return "not installed";
    }
    const ttlMs = parseCliVersionCacheTtlMs();
    if (ttlMs > 0 && !options.forceRefresh) {
        const cached = cliVersionCache.get(commandPath);
        if (cached && Date.now() < cached.expiresAt) {
            if (cached.value !== undefined) {
                return cached.value;
            }
            if (cached.promise) {
                return cached.promise;
            }
        }
        const promise = detectCliVersionUncached(commandPath);
        cliVersionCache.set(commandPath, { promise, expiresAt: Date.now() + ttlMs });
        const value = await promise;
        cliVersionCache.set(commandPath, { value, expiresAt: Date.now() + ttlMs });
        return value;
    }
    const value = await detectCliVersionUncached(commandPath);
    if (ttlMs > 0) {
        cliVersionCache.set(commandPath, { value, expiresAt: Date.now() + ttlMs });
    }
    return value;
}
async function detectCliVersionUncached(commandPath) {
    const result = await runCommand(commandPath, ["--version"], {
        shell: isWindowsShellScript(commandPath),
        timeout: 3000,
    });
    const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
    if (result.error) {
        return `unavailable (${result.error.message})`;
    }
    if (result.status !== 0) {
        return output ? `unavailable (${output})` : `unavailable (exit ${result.status ?? "unknown"})`;
    }
    return output || "unknown";
}
async function buildHermesVersionCheck(installedLabel, options = {}) {
    if (installedLabel === "not installed") {
        const latest = await detectLatestNpmVersion(HERMES_PACKAGE_NAME, { forceRefresh: options.forceRefresh });
        return {
            label: "Hermes",
            packageName: HERMES_PACKAGE_NAME,
            installedLabel: "not installed",
            installedVersion: null,
            latestVersion: latest.version,
            status: "not-installed",
            detail: latest.error,
        };
    }
    const lines = installedLabel.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
    const versionLine = lines[0] ?? installedLabel;
    const updateLine = lines.find((line) => /^Update available:/i.test(line));
    const installedVersion = extractVersion(versionLine);
    return {
        label: "Hermes",
        packageName: HERMES_PACKAGE_NAME,
        installedLabel: versionLine,
        installedVersion,
        latestVersion: updateLine?.replace(/^Update available:\s*/i, "") ?? null,
        status: updateLine ? "outdated" : installedVersion ? "current" : "unknown",
        detail: updateLine ?? (installedVersion ? undefined : "Could not parse Hermes version or update status"),
    };
}
async function buildVersionCheck(options) {
    if (options.notInstalled) {
        const latest = options.skipLatest ? { version: null, error: undefined } : await detectLatestNpmVersion(options.packageName, { forceRefresh: options.forceRefresh });
        return {
            label: options.label,
            packageName: options.packageName,
            installedLabel: "not installed",
            installedVersion: null,
            latestVersion: latest.version,
            status: "not-installed",
            detail: [options.detail, latest.error].filter(Boolean).join(" ") || undefined,
        };
    }
    if (options.skipLatest) {
        return {
            label: options.label,
            packageName: options.packageName,
            installedLabel: options.installedLabel,
            installedVersion: options.installedVersion,
            latestVersion: null,
            status: options.installedVersion ? "unknown" : "unknown",
            detail: options.detail ?? "Latest-version lookup is not available for this package source",
        };
    }
    const latest = await detectLatestNpmVersion(options.packageName, { forceRefresh: options.forceRefresh });
    if (!options.installedVersion || !latest.version) {
        return {
            label: options.label,
            packageName: options.packageName,
            installedLabel: options.installedLabel,
            installedVersion: options.installedVersion,
            latestVersion: latest.version,
            status: "unknown",
            detail: [options.detail, latest.error ?? "Could not parse installed version"].filter(Boolean).join(" "),
        };
    }
    return {
        label: options.label,
        packageName: options.packageName,
        installedLabel: options.installedLabel,
        installedVersion: options.installedVersion,
        latestVersion: latest.version,
        status: compareVersions(options.installedVersion, latest.version) < 0 ? "outdated" : "current",
        detail: [options.detail, latest.error].filter(Boolean).join(" ") || undefined,
    };
}
export async function detectLatestNpmVersion(packageName, options = {}) {
    const cached = options.forceRefresh ? null : readVersionCache(packageName);
    if (cached) {
        return cached;
    }
    const npm = resolveNpmSpawnCommand();
    if (!npm) {
        return { version: null, error: "npm was not found on PATH; latest-version lookup is unavailable" };
    }
    const result = await runCommand(npm.command, [...npm.argsPrefix, "view", packageName, "version", "--registry=https://registry.npmjs.org"], {
        shell: npm.shell,
        timeout: 5000,
    });
    const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
    if (result.error) {
        return { version: null, error: `${npm.display}: ${result.error.message}` };
    }
    if (result.status !== 0) {
        return { version: null, error: output || `npm exited ${result.status ?? "unknown"}` };
    }
    const resolved = { version: output.split(/\r?\n/).at(-1)?.trim() || null };
    writeVersionCache(packageName, resolved.version);
    return resolved;
}
function runCommand(command, args, options) {
    return new Promise((resolve) => {
        const useShell = Boolean(options.shell);
        execFile(useShell ? formatShellCommand(command, args) : command, useShell ? [] : args, {
            encoding: "utf8",
            shell: useShell,
            timeout: options.timeout,
            windowsHide: true,
            env: process.env,
            maxBuffer: 1024 * 1024,
        }, (error, stdout, stderr) => {
            const enriched = error;
            resolve({
                stdout: typeof stdout === "string" ? stdout : "",
                stderr: typeof stderr === "string" ? stderr : "",
                status: typeof enriched?.code === "number" ? enriched.code : error ? 1 : 0,
                signal: enriched?.signal,
                error: enriched ?? undefined,
            });
        });
    });
}
export function formatShellCommand(command, args) {
    return [command, ...args].map(quoteShellArg).join(" ");
}
function quoteShellArg(value) {
    if (process.platform === "win32") {
        return quoteWindowsCmdArg(value);
    }
    return `'${value.replace(/'/g, `'\\''`)}'`;
}
function quoteWindowsCmdArg(value) {
    if (value.length === 0) {
        return "\"\"";
    }
    if (!/[\s"&|<>()^%]/.test(value)) {
        return value;
    }
    return `"${value
        .replace(/%/g, "%%")
        .replace(/(\\*)"/g, '$1$1\\"')
        .replace(/(\\+)$/g, "$1$1")}"`;
}
function prependUniquePathDirs(pathValue, directories) {
    const existing = (pathValue || "")
        .split(path.delimiter)
        .map((item) => item.trim())
        .filter(Boolean);
    const normalized = new Set(existing.map((item) => normalizePathKey(item)));
    const prefix = [];
    for (const directory of directories) {
        if (!directory)
            continue;
        const resolved = path.resolve(directory);
        const key = normalizePathKey(resolved);
        if (normalized.has(key))
            continue;
        normalized.add(key);
        prefix.push(resolved);
    }
    return [...prefix, ...existing].join(path.delimiter) || undefined;
}
function normalizePathKey(value) {
    const resolved = path.resolve(value);
    return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}
function resolveNpmGlobalBinDir(env = process.env) {
    const prefix = resolveNpmGlobalPrefix(env);
    if (!prefix)
        return null;
    return process.platform === "win32" ? prefix : path.join(prefix, "bin");
}
function resolveNpmGlobalPrefix(env = process.env) {
    if (env.npm_config_prefix?.trim()) {
        return path.resolve(env.npm_config_prefix.trim());
    }
    const npm = resolveNpmSpawnCommand(env);
    if (!npm)
        return null;
    const command = npm.shell
        ? formatShellCommand(npm.command, [...npm.argsPrefix, "prefix", "-g"])
        : npm.command;
    const args = npm.shell ? [] : [...npm.argsPrefix, "prefix", "-g"];
    const result = spawnSync(command, args, {
        cwd: os.homedir(),
        env,
        shell: npm.shell,
        encoding: "utf8",
        windowsHide: true,
        timeout: 5000,
    });
    if (result.status !== 0)
        return null;
    const prefix = String(result.stdout || "").trim().split(/\r?\n/).at(-1)?.trim();
    return prefix ? path.resolve(prefix) : null;
}
function commonNpmGlobalBinDirs(env = process.env) {
    if (process.platform === "win32") {
        return [
            env.APPDATA ? path.join(env.APPDATA, "npm") : null,
            path.dirname(process.execPath),
        ].filter((value) => Boolean(value));
    }
    const home = os.homedir();
    return [
        path.join(home, ".npm-global", "bin"),
        path.join(home, ".npm-packages", "bin"),
        path.join(home, ".local", "bin"),
        "/opt/homebrew/bin",
        "/usr/local/bin",
        path.dirname(process.execPath),
    ];
}
export function resolveNpmSpawnCommand(env = process.env) {
    const npmExecPath = env.npm_execpath?.trim();
    if (npmExecPath && existsSync(npmExecPath)) {
        return {
            command: process.execPath,
            argsPrefix: [npmExecPath],
            display: `${process.execPath} ${npmExecPath}`,
            shell: false,
        };
    }
    const pathMatch = findExecutableOnPath("npm", env.PATH, { pathext: env.PATHEXT });
    if (pathMatch) {
        return {
            command: pathMatch,
            argsPrefix: [],
            display: pathMatch,
            shell: isWindowsShellScript(pathMatch),
        };
    }
    for (const candidate of commonNpmCandidates(env)) {
        if (!existsSync(candidate)) {
            continue;
        }
        return {
            command: candidate,
            argsPrefix: [],
            display: candidate,
            shell: isWindowsShellScript(candidate),
        };
    }
    return null;
}
function commonNpmCandidates(env) {
    const names = process.platform === "win32" ? ["npm.cmd", "npm.bat", "npm"] : ["npm"];
    const directories = [
        path.dirname(process.execPath),
        env.APPDATA ? path.join(env.APPDATA, "npm") : undefined,
        env.ProgramFiles ? path.join(env.ProgramFiles, "nodejs") : undefined,
        env["ProgramFiles(x86)"] ? path.join(env["ProgramFiles(x86)"], "nodejs") : undefined,
    ].filter((value) => Boolean(value));
    return directories.flatMap((directory) => names.map((name) => path.join(directory, name)));
}
function isWindowsShellScript(filePath) {
    return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(filePath);
}
function readVersionCache(packageName) {
    const ttlMs = parseVersionCacheTtlMs();
    if (ttlMs <= 0) {
        return null;
    }
    try {
        const payload = JSON.parse(readFileSync(getVersionCachePath(), "utf8"));
        const entry = payload.packages?.[packageName];
        if (!entry || typeof entry.version !== "string" || typeof entry.checkedAt !== "number") {
            return null;
        }
        if (Date.now() - entry.checkedAt > ttlMs) {
            return null;
        }
        return { version: entry.version };
    }
    catch {
        return null;
    }
}
function writeVersionCache(packageName, version) {
    if (!version || parseVersionCacheTtlMs() <= 0) {
        return;
    }
    const filePath = getVersionCachePath();
    try {
        const existing = existsSync(filePath)
            ? JSON.parse(readFileSync(filePath, "utf8"))
            : {};
        const packages = existing.packages ?? {};
        packages[packageName] = { version, checkedAt: Date.now() };
        mkdirSync(path.dirname(filePath), { recursive: true });
        writeFileSync(filePath, `${JSON.stringify({ packages }, null, 2)}\n`, "utf8");
    }
    catch {
        // Best-effort cache only.
    }
}
function getVersionCachePath() {
    return path.join(getConnectorHome(), "version-cache.json");
}
function parseVersionCacheTtlMs() {
    const raw = process.env.NORDRELAY_VERSION_CACHE_TTL_MS;
    if (!raw) {
        return DEFAULT_VERSION_CACHE_TTL_MS;
    }
    const parsed = Number(raw);
    return Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : DEFAULT_VERSION_CACHE_TTL_MS;
}
function parseCliVersionCacheTtlMs() {
    const raw = process.env.NORDRELAY_CLI_VERSION_CACHE_TTL_MS;
    if (!raw) {
        return DEFAULT_CLI_VERSION_CACHE_TTL_MS;
    }
    const parsed = Number(raw);
    return Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : DEFAULT_CLI_VERSION_CACHE_TTL_MS;
}
function readInstalledPackageVersion(packageName) {
    try {
        const packagePath = path.join(getSourceRoot(), "node_modules", ...packageName.split("/"), "package.json");
        const pkg = JSON.parse(readFileSyncUtf8(packagePath));
        return typeof pkg.version === "string" ? pkg.version : null;
    }
    catch {
        return null;
    }
}
function readFileSyncUtf8(filePath) {
    return readFileSync(filePath, "utf8");
}
function extractVersion(value) {
    const match = value.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
    return match?.[0] ?? null;
}
function compareVersions(left, right) {
    const leftParts = parseVersionParts(left);
    const rightParts = parseVersionParts(right);
    for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
        const diff = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
        if (diff !== 0) {
            return diff;
        }
    }
    return 0;
}
function parseVersionParts(value) {
    return value.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
}
function formatLogLine(line) {
    const trimmed = line.trim();
    if (!trimmed) {
        return "";
    }
    const parsedJson = parseJsonLogLine(trimmed);
    if (parsedJson) {
        return parsedJson;
    }
    const textRecord = trimmed.match(/^\[(?<timestamp>[^\]]+)\]\s+(?<level>INFO|WARN|ERROR)\s+(?<message>.*)$/i);
    if (textRecord?.groups) {
        return [
            formatLogTimestamp(textRecord.groups.timestamp),
            textRecord.groups.level.toUpperCase().padEnd(5),
            textRecord.groups.message,
        ].join(" ");
    }
    const timestampedShellLine = trimmed.match(/^\[(?<timestamp>[^\]]+)\]\s+(?<message>.*)$/);
    if (timestampedShellLine?.groups) {
        return [
            formatLogTimestamp(timestampedShellLine.groups.timestamp),
            "INFO ".padEnd(5),
            timestampedShellLine.groups.message,
        ].join(" ");
    }
    return trimmed;
}
function parseLogCursor(cursor) {
    const parsed = Number.parseInt(String(cursor ?? ""), 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
function parseFormattedLogEntries(lines) {
    let currentLevel = "INFO";
    let currentTime = 0;
    return lines.filter((line) => line.length > 0).map((line) => {
        const explicitLevel = explicitFormattedLogLevel(line);
        if (explicitLevel) {
            currentLevel = explicitLevel;
        }
        const parsedTime = formattedLogTime(line);
        if (parsedTime) {
            currentTime = parsedTime;
        }
        return {
            line,
            level: currentLevel,
            time: currentTime,
        };
    });
}
function explicitFormattedLogLevel(line) {
    const match = String(line || "").match(/^\s*(?:\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}|unknown time)?\s*(ERROR|WARN|INFO)\b/i);
    return match?.[1]?.toUpperCase() ?? "";
}
function formattedLogTime(line) {
    const match = String(line || "").match(/^\s*(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2})/);
    if (!match?.[1]) {
        return 0;
    }
    const parsed = new Date(match[1].replace(" ", "T")).getTime();
    return Number.isFinite(parsed) ? parsed : 0;
}
function logEntryMatches(entry, options) {
    const level = String(options.level || "all").toUpperCase();
    if (level !== "ALL" && entry.level !== level) {
        return false;
    }
    const search = String(options.search || "").trim().toLowerCase();
    if (search && !entry.line.toLowerCase().includes(search)) {
        return false;
    }
    const since = logSinceTime(options.since);
    if (since && entry.time && entry.time < since) {
        return false;
    }
    return true;
}
function logSinceTime(value) {
    if (!value) {
        return 0;
    }
    const parsed = new Date(value).getTime();
    return Number.isFinite(parsed) ? parsed : 0;
}
function parseJsonLogLine(line) {
    if (!line.startsWith("{")) {
        return null;
    }
    try {
        const parsed = JSON.parse(line);
        const timestamp = typeof parsed.ts === "string"
            ? parsed.ts
            : typeof parsed.timestamp === "string"
                ? parsed.timestamp
                : null;
        const level = typeof parsed.level === "string" ? parsed.level.toUpperCase() : "INFO";
        const message = typeof parsed.message === "string" ? parsed.message : JSON.stringify(parsed);
        const event = typeof parsed.event === "string" && parsed.event !== "console" ? ` ${parsed.event}` : "";
        return [
            formatLogTimestamp(timestamp),
            `${level}${event}`.slice(0, 12).padEnd(12),
            message,
        ].join(" ");
    }
    catch {
        return null;
    }
}
function formatLogTimestamp(value) {
    if (!value) {
        return "unknown time".padEnd(25);
    }
    const parsed = new Date(value);
    if (Number.isNaN(parsed.getTime())) {
        return value.padEnd(25).slice(0, 25);
    }
    return formatLocalTimestamp(parsed);
}
function formatLocalTimestamp(date) {
    return [
        `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
        `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`,
    ].join(" ");
}
function pad(value) {
    return String(value).padStart(2, "0");
}
