修复 Claude Code TypeScript LSP 在 Windows 上的 ENOENT 问题
问题现象
在 Windows 上使用 Claude Code 时,LSP(Language Server Protocol)工具无法启动 typescript-language-server,报错:
Error performing documentSymbol: ENOENT: no such file or directory, uv_spawn 'typescript-language-server'
根因分析
1. npm 在 Windows 上安装了什么?
当你在 Windows 上安装 typescript-language-server,npm 只会创建以下入口文件:
├── typescript-language-server ← POSIX shell 脚本
├── typescript-language-server.cmd ← Windows 批处理文件
└── typescript-language-server.ps1 ← PowerShell 脚本
注意:npm 不会创建 .exe 文件。
2. Claude Code 只认 .exe
Claude Code 的 LSP 客户端在 Windows 上启动 Language Server 时,默认只查找 .exe 后缀的可执行文件。
它内部调用类似 uv_spawn('typescript-language-server') 的 API。由于目标目录中只有 .cmd 和 .ps1,没有 .exe,进程创建失败,直接抛出:
ENOENT: no such file or directory, uv_spawn 'typescript-language-server'
解决方案:创建 PE .exe 包装器
核心思路:编译一个真正的 Windows PE 可执行文件(.exe),让它作为代理,启动同目录下的 typescript-language-server.cmd。
Claude Code 要 .exe,npm 只给了 .cmd。我们就自己造一个 .exe,内部代理执行 .cmd,让两边都能满意。
步骤一:准备编译环境
你需要一个 Windows C 编译器。以下任选其一:
方案 A:WinLibs(推荐,解压即用)
- 访问 https://winlibs.com
- 下载最新
x86_64-...-posix-seh-ucrt-...版本的.zip或.7z - 解压到
C:\mingw64 - 将
C:\mingw64\bin添加到系统PATH

方案 B:MSYS2(带包管理器)
-
访问 https://www.msys2.org 下载安装
-
打开 MSYS2 UCRT64 终端
-
执行:
bashpacman -Syu pacman -S mingw-w64-ucrt-x86_64-gcc -
将
C:\msys64\ucrt64\bin添加到系统PATH
验证安装:
bash
gcc --version

步骤二:创建包装器源代码
在项目根目录(或任意目录)创建文件 typescript-language-server.c:
c
/**
* TypeScript Language Server Windows Launcher
*
* 解决 Claude Code 在 Windows 上无法启动 typescript-language-server 的问题。
* 原理:编译为 .exe 后,Windows CreateProcess 会优先找到 .exe 文件,
* 本程序再代理执行同目录下的 typescript-language-server.cmd。
*/
#include <windows.h>
#include <string.h>
#define TARGET_CMD "typescript-language-server.cmd"
int main() {
char exeDir[MAX_PATH];
char cmdPath[MAX_PATH];
char commandLine[MAX_PATH * 4];
// 获取当前 .exe 的完整路径
char exePath[MAX_PATH];
DWORD len = GetModuleFileNameA(NULL, exePath, MAX_PATH);
if (len == 0 || len >= MAX_PATH) {
return 1;
}
// 提取 .exe 所在目录
strcpy(exeDir, exePath);
char *lastSlash = strrchr(exeDir, '\\');
if (lastSlash == NULL) {
lastSlash = strrchr(exeDir, '/');
}
if (lastSlash != NULL) {
*(lastSlash + 1) = '\0';
} else {
exeDir[0] = '.';
exeDir[1] = '\\';
exeDir[2] = '\0';
}
// 拼接目标 .cmd 路径
int ret = snprintf(cmdPath, sizeof(cmdPath), "%s%s", exeDir, TARGET_CMD);
if (ret < 0 || ret >= sizeof(cmdPath)) {
return 1;
}
// 检查 .cmd 文件是否存在
DWORD attr = GetFileAttributesA(cmdPath);
if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY)) {
return 1;
}
// 获取原始命令行参数(去掉程序名本身)
LPSTR rawCmdLine = GetCommandLineA();
char *p = rawCmdLine;
// 跳过第一个 token(程序名),处理带引号和不带引号的情况
if (*p == '"') {
p++;
while (*p && *p != '"') p++;
if (*p == '"') p++;
} else {
while (*p && *p != ' ') p++;
}
while (*p == ' ') p++;
// 构建完整的命令行: cmd.exe /c "path.cmd" [args...]
ret = snprintf(commandLine, sizeof(commandLine),
"cmd.exe /c \"%s\" %s", cmdPath, p);
if (ret < 0 || ret >= sizeof(commandLine)) {
return 1;
}
// 启动进程
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
BOOL success = CreateProcessA(
NULL, // 不指定模块名,使用命令行解析
commandLine, // 完整命令行
NULL, NULL, // 默认安全属性
TRUE, // 继承句柄(关键:确保 stdin/stdout/stderr 透传)
0, // 创建标志
NULL, // 使用父进程环境
NULL, // 使用父进程目录
&si,
&pi
);
if (!success) {
return 1;
}
// 等待进程结束并透传退出码
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(pi.hProcess, &exitCode);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return (int)exitCode;
}
步骤三:编译
在 Git Bash 或 CMD 中执行:
bash
gcc -O2 -s -o typescript-language-server.exe typescript-language-server.c
参数说明:
-O2:优化级别 2-s:去除符号表,减小体积-o:指定输出文件名
编译后你会得到一个约 17 KB 的 typescript-language-server.exe 文件。
步骤四:全局安装并定位
1. 全局安装 typescript-language-server
bash
npm install -g typescript-language-server
2. 找到实际安装位置
在 CMD 或 PowerShell 中执行:
cmd
where typescript-language-server
输出示例:

第一个是无扩展名的 POSIX 脚本,第二个是 .cmd 批处理文件。记下这个目录路径(比如 C:\Users\xxx\AppData\Roaming\npm)。
3. 复制 .exe 包装器到该目录
部署后该目录结构:
%APPDATA%\npm\
├── typescript-language-server ← POSIX shell 脚本(npm 原文件)
├── typescript-language-server.cmd ← Windows 批处理(npm 原文件)
├── typescript-language-server.exe ← ← 我们的包装器(新增)
└── typescript-language-server.ps1 ← PowerShell 脚本(npm 原文件)
为什么推荐全局安装? 全局安装一次,所有项目都能用,不需要每个项目重复复制。而且
typescript-language-server作为 TypeScript 的语言服务,本来就是全局工具,不依赖特定项目的node_modules。
步骤五:验证
1. 基础启动测试
cmd
typescript-language-server.exe --version
# 预期输出: 5.3.0(或你安装的版本号)
2. LSP JSON-RPC 协议测试
使用 Node.js 发送标准的 LSP 初始化请求:
bash
node -e "
const msg = JSON.stringify({jsonrpc:'2.0',id:1,method:'initialize',params:{processId:null,rootUri:'file:///E:/your-project',capabilities:{}}});
process.stdout.write('Content-Length: ' + Buffer.byteLength(msg) + '\r\n\r\n' + msg);
setTimeout(() => {
const s = JSON.stringify({jsonrpc:'2.0',id:2,method:'shutdown'});
const e = JSON.stringify({jsonrpc:'2.0',method:'exit'});
process.stdout.write('Content-Length: ' + Buffer.byteLength(s) + '\r\n\r\n' + s);
process.stdout.write('Content-Length: ' + Buffer.byteLength(e) + '\r\n\r\n' + e);
}, 2000);
" | typescript-language-server.exe --stdio
预期能看到类似以下的 JSON-RPC 响应:
json
Content-Length: 206
{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":3,"message":"Using Typescript version (workspace) 6.0.3 from path \"...\""}}
Content-Length: 1593
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"textDocumentSync":2,"completionProvider":...,"definitionProvider":true,...}}}
Content-Length: 38
{"jsonrpc":"2.0","id":2,"result":null}
3. 在 Claude Code 中测试
重启 Claude Code,尝试使用 TypeScript LSP(如代码跳转、类型提示、符号搜索等),不再报 ENOENT 即说明修复成功。
原理总结
| 层级 | 行为 |
|---|---|
| npm | 在 Windows 上只创建 .cmd / .ps1,不创建 .exe |
| Claude Code LSP | 在 Windows 上只识别 .exe 后缀的可执行文件 |
| 修复前 | 两边不匹配:Claude Code 找 .exe,npm 只给了 .cmd → ENOENT |
| 修复后 | 自己提供 .exe:Claude Code 找到 .exe → .exe 代理执行 .cmd → LSP 正常工作 |
常见问题
Q: 为什么不做成通用的 LSP 启动器?
A: 硬编码 typescript-language-server.cmd 更可靠。如果做成根据 .exe 自身文件名推导 .cmd 的通用版本,当文件名不规则时可能出错。每个 LSP 单独编译一个 .exe 虽然多几个文件,但逻辑最简单、最稳定。
Q: 可以用符号链接代替 .exe 吗?
A: 不能 。Windows CreateProcess 在解析 .exe 符号链接时,会检查目标文件是否为有效的 PE 格式。如果目标是 .cmd 批处理文件,CreateProcess 会拒绝执行。
Q: 这个方案对其他 LSP(如 Vue、Rust、Go)也有效吗?
A: 有效 。只要 Claude Code 在 Windows 上对该 LSP 也只认 .exe,而 npm 只提供了 .cmd,就可以用同样的方法解决。只需把 TARGET_CMD 宏改成对应的 .cmd 文件名,重新编译即可。
Q: 需要管理员权限吗?
A: 不需要 。编译、复制到 node_modules/.bin/ 都是普通用户权限即可完成的操作。