// @effect-diagnostics nodeBuiltinImport:off import * as NodeFS from "node:fs"; import * as NodePath from "node:path"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; /** * Windows launcher-script extensions that Node cannot spawn without a shell * (`spawn EINVAL` since Node 20.12) and that the Claude Agent SDK therefore * cannot use as `pathToClaudeCodeExecutable`. */ const WINDOWS_SHIM_EXTENSIONS: ReadonlySet = new Set([".cmd", ".bat", ".ps1"]); /** * Entry points of the npm `@anthropic-ai/claude-code` package relative to the * global `node_modules` directory that sits next to the npm launcher shim. * Newer package versions ship a native `bin/claude.exe`; older versions only * ship `cli.js`, which the SDK runs with a JavaScript runtime. */ const NPM_PACKAGE_ENTRY_CANDIDATES = [ ["node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"], ["node_modules", "@anthropic-ai", "claude-code", "cli.js"], ] as const; export type ExecutableFileCheck = (filePath: string) => boolean; function isExistingFile(filePath: string): boolean { try { return NodeFS.statSync(filePath).isFile(); } catch { return false; } } /** Injectable file-existence check so tests can run against a fake filesystem. */ export const ClaudeExecutableFileCheck = Context.Reference( "server/provider/Drivers/ClaudeExecutableFileCheck", { defaultValue: () => isExistingFile, }, ); /** * Resolves the configured Claude binary path into a value the Claude Agent * SDK can spawn directly via `pathToClaudeCodeExecutable`. * * The SDK spawns the given path without a shell and without Windows PATH / * PATHEXT resolution, so a bare command name like `claude` fails with * "native binary not found" and an npm `claude.cmd` shim fails with * `spawn EINVAL`. CLI probes avoid this via `resolveSpawnCommand`, which can * fall back to `shell: true`; the SDK offers no such escape hatch. * * On Windows this resolves the command against PATH/PATHEXT and, when the * result is an npm launcher shim, follows it to the real package entry * (`bin/claude.exe`, or `cli.js` for older package versions). On other * platforms the configured value is returned unchanged. */ export const resolveClaudeSdkExecutablePath = Effect.fn("resolveClaudeSdkExecutablePath")( function* (binaryPath: string, environment: NodeJS.ProcessEnv): Effect.fn.Return { const platform = yield* HostProcessPlatform; if (platform !== "win32") { return binaryPath; } const resolveExecutable = yield* SpawnExecutableResolution; const isFile = yield* ClaudeExecutableFileCheck; const resolved = resolveExecutable(binaryPath, platform, environment) ?? binaryPath; const extension = NodePath.win32.extname(resolved).toLowerCase(); if (!WINDOWS_SHIM_EXTENSIONS.has(extension)) { return resolved; } const shimDirectory = NodePath.win32.dirname(resolved); for (const entrySegments of NPM_PACKAGE_ENTRY_CANDIDATES) { const candidate = NodePath.win32.join(shimDirectory, ...entrySegments); if (isFile(candidate)) { return candidate; } } yield* Effect.logWarning( "Claude launcher shim resolved but no known package entry was found next to it; the Claude Agent SDK cannot spawn launcher scripts directly.", { binaryPath, resolvedShimPath: resolved }, ); return binaryPath; }, );