Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions apps/server/src/provider/Drivers/ClaudeExecutable.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
// @effect-diagnostics-next-line nodeBuiltinImport:off -- Mocks the synchronous Windows resolver's filesystem boundary.
import * as NodeFS from "node:fs";

import { describe, expect, it } from "@effect/vitest";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { SpawnExecutableResolution } from "@t3tools/shared/shell";
import * as Effect from "effect/Effect";
import { vi } from "vite-plus/test";

import { ClaudeExecutableFileCheck, resolveClaudeSdkExecutablePath } from "./ClaudeExecutable.ts";

vi.mock("node:fs", { spy: true });

const NPM_DIR = "C:\\Users\\dev\\AppData\\Roaming\\npm";
const NPM_SHIM = `${NPM_DIR}\\claude.cmd`;
const NPM_PACKAGE_EXE = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`;
const NPM_PACKAGE_CLI = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\cli.js`;
const LOCAL_APP_DATA = "C:\\Users\\Developer Name\\AppData\\Local";
const WINGET_SHIM = `${LOCAL_APP_DATA}\\Microsoft\\WinGet\\Links\\claude.cmd`;
const WINGET_PACKAGE = "Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe";
const WINGET_EXE = `${LOCAL_APP_DATA}\\Microsoft\\WinGet\\Packages\\${WINGET_PACKAGE}\\claude.exe`;
const WINGET_LAUNCHER = `@echo off\r\n"%LOCALAPPDATA%\\Microsoft\\WinGet\\Packages\\${WINGET_PACKAGE}\\claude.exe" %*\r\n`;

function withWindowsResolution(input: {
readonly resolvedCommand: string | undefined;
Expand All @@ -24,6 +35,94 @@ function withWindowsResolution(input: {
}

describe("resolveClaudeSdkExecutablePath", () => {
it.effect("follows the WinGet launcher to its native executable", () =>
Effect.gen(function* () {
for (const [launcher, localAppData] of [
[WINGET_LAUNCHER, LOCAL_APP_DATA],
[WINGET_LAUNCHER.toLowerCase().replaceAll("\r\n", "\n"), LOCAL_APP_DATA],
[WINGET_LAUNCHER, "C:\\Users\\literal $& %name%\\AppData\\Local"],
] as const) {
using _read = vi.spyOn(NodeFS, "readFileSync").mockReturnValue(launcher);
const shim = WINGET_SHIM.replace(LOCAL_APP_DATA, () => localAppData);
const executable = WINGET_EXE.replace(LOCAL_APP_DATA, () => localAppData);
for (const environment of [
{ LOCALAPPDATA: localAppData },
{ LocalAppData: localAppData },
]) {
for (const resolvedCommand of [
shim,
"C:\\Users\\DEVELO~1\\AppData\\Local\\Microsoft\\WinGet\\Links\\claude.cmd",
"C:\\winget-links\\claude.cmd",
]) {
using _realpath = vi
.spyOn(NodeFS.realpathSync, "native")
.mockImplementation((filePath) => {
if (filePath === shim || filePath === resolvedCommand) return shim;
throw new Error("unknown launcher");
});
for (const binaryPath of ["claude", resolvedCommand]) {
expect(
yield* resolveClaudeSdkExecutablePath(binaryPath, environment).pipe(
withWindowsResolution({ resolvedCommand, existingFiles: [executable] }),
),
).toBe(executable);
}
}
}
}
}),
);

it.effect("leaves unsupported or missing WinGet targets unresolved", () =>
Effect.gen(function* () {
for (const launcher of [
WINGET_LAUNCHER.replace(WINGET_PACKAGE, "Other.Package"),
WINGET_LAUNCHER.replace("%LOCALAPPDATA%", "%APPDATA%"),
WINGET_LAUNCHER.replace("claude.exe", "..\\other.exe"),
WINGET_LAUNCHER.replace(" %*", " --extra %*"),
`${WINGET_LAUNCHER}echo side effect\r\n`,
WINGET_LAUNCHER.replace(" %*", " %* & echo side effect"),
]) {
using _read = vi.spyOn(NodeFS, "readFileSync").mockReturnValue(launcher);
expect(
yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe(
withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }),
),
).toBe("claude");
}
using _read = vi.spyOn(NodeFS, "readFileSync").mockReturnValue(WINGET_LAUNCHER);
for (const environment of [{}, { LOCALAPPDATA: "relative" }]) {
expect(
yield* resolveClaudeSdkExecutablePath("claude", environment).pipe(
withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }),
),
).toBe("claude");
}
expect(
yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe(
withWindowsResolution({ resolvedCommand: WINGET_SHIM }),
),
).toBe("claude");
using _realpath = vi
.spyOn(NodeFS.realpathSync, "native")
.mockImplementation((filePath) => String(filePath));
for (const shim of [
NPM_SHIM,
WINGET_SHIM.replace(".cmd", ".ps1"),
"C:\\different-winget-links\\claude.cmd",
]) {
expect(
yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe(
withWindowsResolution({
resolvedCommand: shim,
existingFiles: [WINGET_EXE],
}),
),
).toBe("claude");
}
}),
);

it.effect("follows an npm launcher shim to the packaged native binary", () =>
Effect.gen(function* () {
expect(
Expand Down Expand Up @@ -85,6 +184,25 @@ describe("resolveClaudeSdkExecutablePath", () => {
withWindowsResolution({ resolvedCommand: NPM_SHIM }),
),
).toBe("claude");
using _read = vi.spyOn(NodeFS, "readFileSync").mockImplementation(() => {
throw new Error("unreadable launcher");
});
expect(
yield* resolveClaudeSdkExecutablePath(WINGET_SHIM, { LOCALAPPDATA: LOCAL_APP_DATA }).pipe(
withWindowsResolution({ resolvedCommand: WINGET_SHIM, existingFiles: [WINGET_EXE] }),
),
).toBe(WINGET_SHIM);
using _realpath = vi.spyOn(NodeFS.realpathSync, "native").mockImplementation(() => {
throw new Error("unreadable path");
});
expect(
yield* resolveClaudeSdkExecutablePath("claude", { LOCALAPPDATA: LOCAL_APP_DATA }).pipe(
withWindowsResolution({
resolvedCommand: "C:\\unreadable-winget-links\\claude.cmd",
existingFiles: [WINGET_EXE],
}),
),
).toBe("claude");
}),
);
});
43 changes: 42 additions & 1 deletion apps/server/src/provider/Drivers/ClaudeExecutable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,42 @@ const NPM_PACKAGE_ENTRY_CANDIDATES = [
["node_modules", "@anthropic-ai", "claude-code", "cli.js"],
] as const;

const WINGET_PACKAGE_ENTRY = [
"Microsoft",
"WinGet",
"Packages",
"Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe",
"claude.exe",
] as const;

function resolveWinGetExecutablePath(shimPath: string, environment: NodeJS.ProcessEnv) {
const localAppData = Object.entries(environment).find(
([key]) => key.toUpperCase() === "LOCALAPPDATA",
)?.[1];
if (!localAppData || !NodePath.win32.isAbsolute(localAppData)) return undefined;
const launcherPath = NodePath.win32.join(
localAppData,
"Microsoft",
"WinGet",
"Links",
"claude.cmd",
);
try {
if (
NodePath.win32.normalize(shimPath).toLowerCase() !== launcherPath.toLowerCase() &&
NodeFS.realpathSync.native(shimPath).toLowerCase() !==
NodeFS.realpathSync.native(launcherPath).toLowerCase()
)
return undefined;
const launcher = NodeFS.readFileSync(shimPath, "utf8").replaceAll("\r\n", "\n").trim();
const expectedLauncher = `@echo off\n"%LOCALAPPDATA%\\${WINGET_PACKAGE_ENTRY.join("\\")}" %*`;
if (launcher.toLowerCase() !== expectedLauncher.toLowerCase()) return undefined;
return NodePath.win32.join(localAppData, ...WINGET_PACKAGE_ENTRY);
} catch {
return undefined;
}
}

export type ExecutableFileCheck = (filePath: string) => boolean;

function isExistingFile(filePath: string): boolean {
Expand Down Expand Up @@ -60,8 +96,13 @@ export const resolveClaudeSdkExecutablePath = Effect.fn("resolveClaudeSdkExecuta
}
}

const winGetExecutable = resolveWinGetExecutablePath(resolved, environment);
if (winGetExecutable && isFile(winGetExecutable)) {
return winGetExecutable;
}

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.",
"Claude launcher shim resolved but no known package entry was found; the Claude Agent SDK cannot spawn launcher scripts directly.",
{ binaryPath, resolvedShimPath: resolved },
);
return binaryPath;
Expand Down
Loading