import { createRequire } 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 { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { PtyAdapter } from "../Services/PTY.ts"; import { PtySpawnError, type PtyAdapterShape, type PtyExitEvent, type PtyProcess, } from "../Services/PTY.ts"; let didEnsureSpawnHelperExecutable = false; const resolveNodePtySpawnHelperPath = Effect.gen(function* () { const requireForNodePty = 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 PtyProcess { private readonly process: import("node-pty").IPty; constructor(process: import("node-pty").IPty) { this.process = process; } 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 { this.process.kill(signal); } onData(callback: (data: string) => void): () => void { const disposable = this.process.onData(callback); return () => { disposable.dispose(); }; } onExit(callback: (event: PtyExitEvent) => void): () => void { const disposable = this.process.onExit((event) => { callback({ exitCode: event.exitCode, signal: event.signal ?? null, }); }); return () => { disposable.dispose(); }; } } export const layer = Layer.effect( PtyAdapter, Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const platform = yield* HostProcessPlatform; const architecture = yield* HostProcessArchitecture; const nodePty = yield* Effect.promise(() => import("node-pty")); 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 { spawn: Effect.fn(function* (input) { yield* ensureNodePtySpawnHelperExecutableCached; const ptyProcess = yield* Effect.try({ try: () => nodePty.spawn(input.shell, input.args ?? [], { cwd: input.cwd, cols: input.cols, rows: input.rows, env: input.env, name: platform === "win32" ? "xterm-color" : "xterm-256color", }), catch: (cause) => new PtySpawnError({ adapter: "node-pty", message: cause instanceof Error ? cause.message : "Failed to spawn PTY process", cause, }), }); return new NodePtyProcess(ptyProcess); }), } satisfies PtyAdapterShape; }), );