1// @effect-diagnostics nodeBuiltinImport:off - Runs before the Effect runtime starts, on raw file descriptors.
2import * as NodeFS from "node:fs";
3import * as NodeTty from "node:tty";
4
5import * as HostProcess from "@t3tools/shared/HostProcess";
6
7/**
8 * Electron's Windows launcher has no stdin; Node substitutes a non-console NUL device.
9 * Preserve pipes, files, console input, and runs without console output.
10 * Explicit `< NUL` is indistinguishable from missing launcher stdin and also reattaches.
11 */
12function needsConsoleStdin(input: {
13 readonly platform: NodeJS.Platform;
14 readonly env: NodeJS.ProcessEnv;
15 readonly stdinMissing: boolean;
16 readonly stdoutIsConsole: boolean;
17}): boolean {
18 return (
19 input.platform === "win32" &&
20 // Set only by the launcher (DesktopCliShim), never for the desktop backend.
21 input.env.T3CODE_DESKTOP_CLI_LAUNCHER === "1" &&
22 input.stdinMissing &&
23 input.stdoutIsConsole
24 );
25}
26
27const isMissingInput = (fd: number) => {
28 try {
29 return NodeFS.fstatSync(fd).isCharacterDevice() && !NodeTty.isatty(fd);
30 } catch {
31 return true;
32 }
33};
34
35/** Call before reading process.stdin. Returns the reopened fd for children, or undefined without changing stdin. */
36export function attachConsoleStdin(): number | undefined {
37 const needed = needsConsoleStdin({
38 platform: HostProcess.Platform.defaultValue(),
39 env: process.env,
40 stdinMissing: isMissingInput(0),
41 stdoutIsConsole: NodeTty.isatty(1),
42 });
43 // The launcher marker must not propagate to child shells or t3 processes.
44 delete process.env.T3CODE_DESKTOP_CLI_LAUNCHER;
45 if (!needed) return undefined;
46 let fd: number;
47 try {
48 fd = NodeFS.openSync("\\\\.\\CONIN$", "r+");
49 } catch {
50 return undefined;
51 }
52 if (!NodeTty.isatty(fd)) {
53 NodeFS.closeSync(fd);
54 return undefined;
55 }
56 const stdin = new NodeTty.ReadStream(fd);
57 // Like Node's built-in stdin, release pending console reads before a child takes over.
58 stdin.on("pause", () => {
59 const internal = stdin as unknown as {
60 _handle?: { reading: boolean; readStop(): void };
61 _readableState: { reading: boolean };
62 };
63 const handle = internal._handle;
64 if (!handle) return;
65 internal._readableState.reading = false;
66 handle.reading = false;
67 handle.readStop();
68 });
69 Object.defineProperty(process, "stdin", { value: stdin, configurable: true, enumerable: true });
70 consoleStdinFd = fd;
71 return fd;
72}
73
74let consoleStdinFd: number | undefined;
75
76export const interactiveStdin = (): number | "inherit" => consoleStdinFd ?? "inherit";