90 lines · 3.5 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:offimport * 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<string> = 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<ExecutableFileCheck>(  "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<string> {    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;  },);