import * as NodeModule from "node:module"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as PtyAdapter from "./PtyAdapter.ts"; export class NodePtyModuleLoadError extends Schema.TaggedError()( "NodePtyModuleLoadError", { platform: Schema.String, architecture: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to load node-pty for ${this.platform}-${this.architecture}.`; } } type NodePtyModuleLoader = () => Promise; let didEnsureSpawnHelperExecutable = false; const resolveNodePtySpawnHelperPath = Effect.gen(function* () { const requireForNodePty = NodeModule.createRequire(import.meta.url); const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; const platform = yield* HostProcessPlatform; const architecture = yield* HostProcessArchitecture; const packageJsonPath = requireForNodePty.resolve("node-pty/package.json"); const packageDir = path.dirname(packageJsonPath); const candidates = [ path.join(packageDir, "build", "Release", "spawn-helper"), path.join(packageDir, "build", "Debug", "spawn-helper"), path.join(packageDir, "prebuilds", `${platform}-${architecture}`, "spawn-helper"), ]; for (const candidate of candidates) { if (yield* fs.exists(candidate)) { return candidate; } } return null; }).pipe(Effect.orElseSucceed(() => null)); const ensureNodePtySpawnHelperExecutable = Effect.fn(function* () { const fs = yield* FileSystem.FileSystem; const platform = yield* HostProcessPlatform; if (platform === "win32") return; if (didEnsureSpawnHelperExecutable) return; const helperPath = yield* resolveNodePtySpawnHelperPath; if (!helperPath) return; didEnsureSpawnHelperExecutable = true; if (!(yield* fs.exists(helperPath))) { return; } // Best-effort: avoid FileSystem.stat in packaged mode where some fs metadata can be missing. yield* fs.chmod(helperPath, 0o755).pipe(Effect.orElseSucceed(() => undefined)); }); class NodePtyProcess implements PtyAdapter.PtyProcess { private readonly process: import("node-pty").IPty; private readonly platform: NodeJS.Platform; constructor(process: import("node-pty").IPty, platform: NodeJS.Platform) { this.process = process; this.platform = platform; } get pid(): number { return this.process.pid; } write(data: string): void { this.process.write(data); } resize(cols: number, rows: number): void { this.process.resize(cols, rows); } kill(signal?: string): void { // node-pty terminates the Windows process tree without a POSIX signal. this.process.kill(this.platform === "win32" ? undefined : signal); } onData(callback: (data: string) => void): () => void { const disposable = this.process.onData(callback); return () => { disposable.dispose(); }; } onExit(callback: (event: PtyAdapter.PtyExitEvent) => void): () => void { const disposable = this.process.onExit((event) => { callback({ exitCode: event.exitCode, signal: event.signal ?? null, }); }); return () => { disposable.dispose(); }; } } export const make = Effect.fn("NodePtyAdapter.make")(function* ( loadNodePtyModule: NodePtyModuleLoader = () => import("node-pty"), ) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const platform = yield* HostProcessPlatform; const architecture = yield* HostProcessArchitecture; const nodePty = yield* Effect.tryPromise({ try: loadNodePtyModule, catch: (cause) => new NodePtyModuleLoadError({ platform, architecture, cause, }), }).pipe(Effect.orDie); const ensureNodePtySpawnHelperExecutableCached = yield* Effect.cached( ensureNodePtySpawnHelperExecutable().pipe( Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), Effect.provideService(HostProcessPlatform, platform), Effect.provideService(HostProcessArchitecture, architecture), Effect.orElseSucceed(() => undefined), ), ); return PtyAdapter.PtyAdapter.of({ spawn: Effect.fn("NodePtyAdapter.spawn")(function* (input) { yield* ensureNodePtySpawnHelperExecutableCached; // node-pty only writes `name` into the child's TERM on the Unix path; // the ConPTY path leaves the environment untouched, so Windows children // inherit a missing or 16-color TERM unless it is set here. const env = platform === "win32" && input.env["TERM"] === undefined ? { ...input.env, TERM: "xterm-256color" } : input.env; const ptyProcess = yield* Effect.try({ try: () => nodePty.spawn(input.shell, input.args ?? [], { cwd: input.cwd, cols: input.cols, rows: input.rows, env, name: "xterm-256color", }), catch: (cause) => new PtyAdapter.PtySpawnError({ adapter: "node-pty", shell: input.shell, cause, }), }); return new NodePtyProcess(ptyProcess, platform); }), }); }); export const layer = Layer.effect(PtyAdapter.PtyAdapter, make());