import { spawn as nodeSpawn } from "node:child_process";
import process from "node:process";
import { PassThrough } from "node:stream";
import { getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
import { ReadBuffer, serializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
import { markDetachedChildProcess, signalChildProcessTree } from "../processes/child-process-tree.js";
const CLOSE_GRACE_MS = 2_000;
function delay(ms) {
    return new Promise((resolve) => {
        const timer = setTimeout(resolve, ms);
        timer.unref?.();
    });
}
/**
 * MCP's SDK stdio transport cannot request `detached: true`, so closing it only
 * kills the direct child. Sources like `lark-cli event` may leave grandchildren
 * alive; this transport owns an isolated process group and tears it down on close.
 */
export class DetachedStdioClientTransport {
    readBuffer = new ReadBuffer();
    serverParams;
    stderrStream;
    childProcess;
    onclose;
    onerror;
    onmessage;
    constructor(server) {
        this.serverParams = server;
        this.stderrStream = server.stderr === "pipe" || server.stderr === "overlapped" ? new PassThrough() : null;
    }
    async start() {
        if (this.childProcess) {
            throw new Error("DetachedStdioClientTransport already started.");
        }
        await new Promise((resolve, reject) => {
            const detached = process.platform !== "win32";
            const stdio = ["pipe", "pipe", this.serverParams.stderr ?? "inherit"];
            const child = nodeSpawn(this.serverParams.command, this.serverParams.args ?? [], {
                cwd: this.serverParams.cwd,
                detached,
                env: {
                    ...getDefaultEnvironment(),
                    ...this.serverParams.env,
                },
                stdio,
                shell: false,
                windowsHide: true,
            });
            this.childProcess = detached ? markDetachedChildProcess(child) : child;
            child.once("error", (error) => {
                reject(error);
                this.onerror?.(error);
            });
            child.once("spawn", () => {
                resolve();
            });
            child.once("close", () => {
                this.childProcess = undefined;
                this.onclose?.();
            });
            child.stdin?.on("error", (error) => {
                this.onerror?.(error);
            });
            child.stdout?.on("data", (chunk) => {
                this.readBuffer.append(chunk);
                this.processReadBuffer();
            });
            child.stdout?.on("error", (error) => {
                this.onerror?.(error);
            });
            if (this.stderrStream && child.stderr) {
                child.stderr.pipe(this.stderrStream);
            }
        });
    }
    get stderr() {
        if (this.stderrStream) {
            return this.stderrStream;
        }
        return this.childProcess?.stderr ?? null;
    }
    get pid() {
        return this.childProcess?.pid ?? null;
    }
    async close() {
        const child = this.childProcess;
        if (!child) {
            this.readBuffer.clear();
            return;
        }
        this.childProcess = undefined;
        const closePromise = new Promise((resolve) => {
            child.once("close", () => {
                resolve();
            });
        });
        try {
            child.stdin?.end();
        }
        catch {
            // ignore
        }
        await Promise.race([closePromise, delay(CLOSE_GRACE_MS)]);
        signalChildProcessTree(child, "SIGTERM");
        await Promise.race([closePromise, delay(CLOSE_GRACE_MS)]);
        signalChildProcessTree(child, "SIGKILL");
        this.readBuffer.clear();
    }
    async send(message, _options) {
        const stdin = this.childProcess?.stdin;
        if (!stdin) {
            throw new Error("Not connected");
        }
        const json = serializeMessage(message);
        if (stdin.write(json)) {
            return;
        }
        await new Promise((resolve) => {
            stdin.once("drain", resolve);
        });
    }
    processReadBuffer() {
        for (;;) {
            try {
                const message = this.readBuffer.readMessage();
                if (message === null) {
                    break;
                }
                this.onmessage?.(message);
            }
            catch (error) {
                this.onerror?.(error instanceof Error ? error : new Error(String(error)));
            }
        }
    }
}
//# sourceMappingURL=detached-stdio-client-transport.js.map