@trim21/personal-pi-extensions 0.1.577 → 0.1.581
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/package.json +1 -1
- package/src/bwrap/runtime.ts +5 -2
- package/src/lib/lsp/server-config.ts +103 -10
- package/src/skills/lsp-config/SKILL.md +74 -26
package/README.md
CHANGED
|
@@ -429,6 +429,7 @@ read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级
|
|
|
429
429
|
- `startupTimeoutMs` / `diagnosticsWaitMs`:per-server 超时,覆盖全局配置与默认值
|
|
430
430
|
- `env`:追加到 LSP 子进程的环境变量(在 `process.env` 之上合并)。string 值支持 `{root}` / `{cwd}` 模板与 `${VAR}` 环境变量引用;`{ "sh": [...] }` 在服务器启动时执行命令(argv 直接执行、不经 shell,需要 shell 特性时自行包 `["bash", "-c", "..."]`),stdout trim 后作为值,命令失败(非零退出或输出为空)时该服务器启动失败并报错
|
|
431
431
|
- `initializationOptions` 与 `settings` 按 LSP 语义分离:前者进 initialize 请求,后者进 didChangeConfiguration / workspace/configuration 请求;`initializationOptions` 的字符串值(含嵌套对象/数组)在启动时做 `${VAR}` 插值,`${VAR:-default}` 在变量未定义或为空时用 default,未定义且无 default 替换为空字符串;插值时可引用 `env` 里配置的变量
|
|
432
|
+
- `initializationOptionsCommand`:启动时执行命令计算 `initializationOptions`(argv 直接执行、不经 shell,需要 shell 特性时自行包 `["bash", "-c", "..."]`;参数项支持 `{root}` / `{cwd}` 模板与 `${VAR}` 插值;cwd 为该服务器的项目根,环境含上面 `env` 解析出的变量)。stdout 必须是 JSON 对象,与静态 `initializationOptions` 深合并(命令输出优先,嵌套对象逐层递归);非零退出、输出为空或不是 JSON 对象时该服务器启动失败并报错
|
|
432
433
|
|
|
433
434
|
`env` 的 `{sh}` 命令与 `initializationOptions` 插值可以组合使用,例如用 `gh auth token` 给服务器的 initialize 请求提供 session token:
|
|
434
435
|
|
|
@@ -453,6 +454,22 @@ read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级
|
|
|
453
454
|
|
|
454
455
|
命令失败(`gh` 未登录 / 不在 PATH)时该服务器启动失败并报错。
|
|
455
456
|
|
|
457
|
+
启动时才能算出的值(例如项目把 `typescript` alias 成 `@typescript/typescript6` 时,要去 pnpm store 里找真实的 `tsserver.js`)用 `initializationOptionsCommand`,避免把绝对路径写死在配置里:
|
|
458
|
+
|
|
459
|
+
```jsonc
|
|
460
|
+
{
|
|
461
|
+
"servers": {
|
|
462
|
+
"typescript": {
|
|
463
|
+
"bin": "typescript-language-server",
|
|
464
|
+
"args": ["--stdio"],
|
|
465
|
+
"initializationOptions": { "tsserver": { "logVerbosity": "verbose" } },
|
|
466
|
+
// 脚本 stdout 的 JSON 对象与上面的静态值深合并(命令优先)
|
|
467
|
+
"initializationOptionsCommand": ["node", "{root}/.pi/lsp/ts-options.mjs"],
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
456
473
|
没有内置默认服务器:`servers` 的 key 就是服务器 id,全部来自你的配置,未定义 `servers` 时不启动任何语言服务器。executable 的发现逻辑(如 tsserver 路径、venv 里的 python)不内置,需要时用 `bin` / `args` / `settings` 自行表达。
|
|
457
474
|
|
|
458
475
|
启用控制只有顶层两处:`enabled`(白名单)与 `disabled`(排除),按服务器 id 生效。`enabled` 里的 id 必须是已配置服务器,否则视为配置错误;`disabled` 里未注册的 id 直接忽略。全局超时字段(`initializeTimeoutMs` 等)同样配在顶层。
|
package/package.json
CHANGED
package/src/bwrap/runtime.ts
CHANGED
|
@@ -292,11 +292,12 @@ const UNSANDBOXED_DENIED = "User denied unsandboxed execution.";
|
|
|
292
292
|
|
|
293
293
|
/** 命令没经沙箱时沿用 prompt 里的说法,给出在沙盒外重跑的手段。 */
|
|
294
294
|
const SANDBOX_ESCAPE_HATCH =
|
|
295
|
-
"
|
|
295
|
+
"If the command needs more than that, use the `dangerouslyDisableSandbox` parameter to request unsandboxed execution; the user must approve this request.";
|
|
296
296
|
|
|
297
297
|
/**
|
|
298
298
|
* 命令失败时作为独立信息块附上的沙箱状态:默认写边界 + 网络层级,以及在沙盒外重跑的手段。
|
|
299
299
|
* 写边界只说沙箱布局(不展开用户配置的可写路径),网络只说层级(不列白名单域名)。
|
|
300
|
+
* 整块包在 `<system-reminder>` 里,与其它系统注入的提示同一形态,模型不会把它当成命令输出。
|
|
300
301
|
* 命令没经沙箱(allow-all、审批通过的全权限、Windows)时返回 undefined:
|
|
301
302
|
* 没有沙箱就没什么可提示的。
|
|
302
303
|
*/
|
|
@@ -304,8 +305,10 @@ export function describeSandbox(resolved: ResolvedBwrap, unsandboxed: boolean):
|
|
|
304
305
|
if (unsandboxed) return undefined;
|
|
305
306
|
const writes = resolved.mode === "readonly" ? "the filesystem is read-only" : SANDBOX_WRITE_RULES;
|
|
306
307
|
return [
|
|
307
|
-
|
|
308
|
+
"<system-reminder>",
|
|
309
|
+
`This command ran in a sandbox: ${writes}; ${describeNetwork(resolved)}.`,
|
|
308
310
|
SANDBOX_ESCAPE_HATCH,
|
|
311
|
+
"</system-reminder>",
|
|
309
312
|
].join("\n");
|
|
310
313
|
}
|
|
311
314
|
|
|
@@ -23,6 +23,7 @@ import { promisify } from "node:util";
|
|
|
23
23
|
|
|
24
24
|
import { minimatch } from "minimatch";
|
|
25
25
|
import { type Static, Type } from "typebox";
|
|
26
|
+
import { Value } from "typebox/value";
|
|
26
27
|
|
|
27
28
|
import { type LspServerAdapter, type LspServerHandle, type ServerKind } from "./adapter.js";
|
|
28
29
|
import { exists, findBinaryInWorkspace, which } from "./bin.js";
|
|
@@ -67,6 +68,13 @@ export const serverConfigSchema = Type.Object({
|
|
|
67
68
|
diagnosticsWaitMs: Type.Optional(Type.Number({ minimum: 1 })),
|
|
68
69
|
/** initialize 请求的 initializationOptions;字符串值支持 ${VAR} / ${VAR:-default} 插值。 */
|
|
69
70
|
initializationOptions: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
71
|
+
/**
|
|
72
|
+
* 启动时执行命令计算 initializationOptions(argv 直接执行、不经 shell,每项支持
|
|
73
|
+
* {root} / {cwd} 模板与 ${VAR} / ${VAR:-default} 插值)。stdout 必须是 JSON 对象,
|
|
74
|
+
* 与静态 initializationOptions 深合并(命令输出优先);失败(spawn 错误 / 非零退出 /
|
|
75
|
+
* 空输出 / 非 JSON 对象)时服务器启动失败并报错。
|
|
76
|
+
*/
|
|
77
|
+
initializationOptionsCommand: Type.Optional(Type.Array(Type.String(), { minItems: 1 })),
|
|
70
78
|
/** didChangeConfiguration / workspace/configuration 请求的 settings。 */
|
|
71
79
|
settings: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
72
80
|
});
|
|
@@ -132,7 +140,7 @@ async function resolveEnv(
|
|
|
132
140
|
if (typeof value === "string") {
|
|
133
141
|
return [key, interpolateEnvVars(resolveTemplate(value, root, cwd), process.env)];
|
|
134
142
|
}
|
|
135
|
-
return [key, await
|
|
143
|
+
return [key, await runConfigCommand(value.sh, { cwd, label: "env command" })];
|
|
136
144
|
}),
|
|
137
145
|
);
|
|
138
146
|
return Object.fromEntries(resolved);
|
|
@@ -140,31 +148,118 @@ async function resolveEnv(
|
|
|
140
148
|
|
|
141
149
|
const execFile = promisify(nodeExecFile);
|
|
142
150
|
|
|
151
|
+
interface ConfigCommandOptions {
|
|
152
|
+
cwd: string;
|
|
153
|
+
/** 子进程环境;缺省继承 process.env。 */
|
|
154
|
+
env?: NodeJS.ProcessEnv;
|
|
155
|
+
/** 报错文案里的命令用途,如 "env command"。 */
|
|
156
|
+
label: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
143
159
|
/**
|
|
144
160
|
* argv 直接执行(不经 shell,避免注入面;需要 shell 特性时配置里自行包
|
|
145
|
-
* ["bash", "-c", "..."]),stdout trim
|
|
161
|
+
* ["bash", "-c", "..."]),stdout trim 后返回。
|
|
146
162
|
* 失败(spawn 错误 / 非零退出 / 空输出)时抛错,由 lsp.ts 捕获后 notify 给用户。
|
|
147
163
|
*/
|
|
148
|
-
async function
|
|
164
|
+
async function runConfigCommand(argv: string[], options: ConfigCommandOptions): Promise<string> {
|
|
165
|
+
const { cwd, env, label } = options;
|
|
149
166
|
let stdout: string;
|
|
150
167
|
try {
|
|
151
|
-
({ stdout } = await execFile(argv[0], argv.slice(1), { cwd }));
|
|
168
|
+
({ stdout } = await execFile(argv[0], argv.slice(1), { cwd, env }));
|
|
152
169
|
} catch (error) {
|
|
153
170
|
const err = error as NodeJS.ErrnoException & { stderr?: string };
|
|
154
171
|
const reason = typeof err.code === "number" ? `exit code ${err.code}` : err.message;
|
|
155
172
|
const stderr = err.stderr?.trim();
|
|
156
173
|
throw new Error(
|
|
157
|
-
|
|
174
|
+
`${label} failed (${argv.join(" ")}): ${reason}${stderr ? `: ${stderr}` : ""}`,
|
|
158
175
|
{ cause: error },
|
|
159
176
|
);
|
|
160
177
|
}
|
|
161
178
|
const output = stdout.trim();
|
|
162
179
|
if (!output) {
|
|
163
|
-
throw new Error(
|
|
180
|
+
throw new Error(`${label} (${argv.join(" ")}) produced empty output`);
|
|
164
181
|
}
|
|
165
182
|
return output;
|
|
166
183
|
}
|
|
167
184
|
|
|
185
|
+
/** 命令 stdout 即 initializationOptions:JSON 对象,经 typebox 校验后作为配置值。 */
|
|
186
|
+
const initializationOptionsOutputSchema = Type.Record(Type.String(), Type.Unknown());
|
|
187
|
+
|
|
188
|
+
/** 纯对象判定(数组与 null 不算),用于深合并与命令输出校验。 */
|
|
189
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
190
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 深合并 initializationOptions:两侧都是纯对象时逐层递归,其余类型整体覆盖(override 优先)。 */
|
|
194
|
+
function mergeInitializationOptions(
|
|
195
|
+
base: Record<string, unknown> | undefined,
|
|
196
|
+
override: Record<string, unknown>,
|
|
197
|
+
): Record<string, unknown> {
|
|
198
|
+
const merged: Record<string, unknown> = { ...base };
|
|
199
|
+
for (const [key, value] of Object.entries(override)) {
|
|
200
|
+
const current = merged[key];
|
|
201
|
+
merged[key] =
|
|
202
|
+
isRecord(current) && isRecord(value) ? mergeInitializationOptions(current, value) : value;
|
|
203
|
+
}
|
|
204
|
+
return merged;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 命令输出 → initializationOptions;JSON 语法错误与非对象输出都带上具体命令,便于定位是哪个脚本挂了。 */
|
|
208
|
+
function parseInitializationOptionsOutput(
|
|
209
|
+
output: string,
|
|
210
|
+
argv: readonly string[],
|
|
211
|
+
): Record<string, unknown> {
|
|
212
|
+
let parsed: unknown;
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(output);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`initializationOptions command (${argv.join(" ")}) produced invalid JSON: ${(error as Error).message}`,
|
|
218
|
+
{ cause: error },
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
return Value.Parse(initializationOptionsOutputSchema, parsed);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`initializationOptions command (${argv.join(" ")}) must print a JSON object, got ${describeValue(parsed)}`,
|
|
226
|
+
{ cause: error },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** 非法输出的类型描述(typebox 的 ParseError 只有 "Parse",自己给出可读文案)。 */
|
|
232
|
+
function describeValue(value: unknown): string {
|
|
233
|
+
if (value === null) return "null";
|
|
234
|
+
if (Array.isArray(value)) return "array";
|
|
235
|
+
return typeof value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 解析 initializationOptions:静态值按环境变量深插值;配置了 initializationOptionsCommand
|
|
240
|
+
* 时执行该命令(cwd 为服务器 root,环境含已解析的 per-server env),stdout 的 JSON 对象
|
|
241
|
+
* 与静态值深合并(命令优先)。
|
|
242
|
+
*/
|
|
243
|
+
async function resolveInitializationOptions(
|
|
244
|
+
config: ServerConfig,
|
|
245
|
+
root: string,
|
|
246
|
+
cwd: string,
|
|
247
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
248
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
249
|
+
const variables = { ...process.env, ...env };
|
|
250
|
+
const staticOptions = interpolateEnvDeep(config.initializationOptions, variables) as
|
|
251
|
+
Record<string, unknown> | undefined;
|
|
252
|
+
const command = config.initializationOptionsCommand;
|
|
253
|
+
if (!command) return staticOptions;
|
|
254
|
+
const argv = command.map((arg) => interpolateEnvVars(resolveTemplate(arg, root, cwd), variables));
|
|
255
|
+
const output = await runConfigCommand(argv, {
|
|
256
|
+
cwd: root,
|
|
257
|
+
env: variables,
|
|
258
|
+
label: "initializationOptions command",
|
|
259
|
+
});
|
|
260
|
+
return mergeInitializationOptions(staticOptions, parseInitializationOptionsOutput(output, argv));
|
|
261
|
+
}
|
|
262
|
+
|
|
168
263
|
/** 解析可执行文件:绝对/相对路径直接用;名字走项目工作区(node_modules/.bin 等)→ PATH。 */
|
|
169
264
|
async function resolveBinary(bin: string, root: string, cwd: string): Promise<string | undefined> {
|
|
170
265
|
if (isAbsolute(bin)) return existsSync(bin) ? bin : undefined;
|
|
@@ -232,15 +327,13 @@ export class ConfigAdapter implements LspServerAdapter {
|
|
|
232
327
|
const resolved = await resolveBinary(bin, root, cwd);
|
|
233
328
|
if (!resolved) return undefined;
|
|
234
329
|
const env = await resolveEnv(this.config.env, root, cwd);
|
|
330
|
+
const initialization = await resolveInitializationOptions(this.config, root, cwd, env);
|
|
235
331
|
return {
|
|
236
332
|
process: spawnProcess(resolved, this.config.args ?? [], {
|
|
237
333
|
cwd: root,
|
|
238
334
|
env,
|
|
239
335
|
}),
|
|
240
|
-
initialization
|
|
241
|
-
...process.env,
|
|
242
|
-
...env,
|
|
243
|
-
}) as Record<string, unknown> | undefined,
|
|
336
|
+
initialization,
|
|
244
337
|
settings: this.config.settings,
|
|
245
338
|
languageIds: this.config.languageIdByExtension,
|
|
246
339
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lsp-config
|
|
3
|
-
description: Use when 编写、检查或排查 LSP 语言服务器配置 —— 项目本地 .pi/lsp.json 或全局 ~/.pi/agent/lsp.json:配置 typescript-language-server / pyright / ruff / gopls / clangd 等服务器的 bin、include、rootMarkers、workingDir、initializationOptions、tsserver.path、watch 与诊断超时;或排查服务器起不来(binary not found / provides no tsserver.js)、typescript alias 依赖下 tsserver 找不到、诊断一直为空、monorepo 子项目 root 不对等问题。
|
|
3
|
+
description: Use when 编写、检查或排查 LSP 语言服务器配置 —— 项目本地 .pi/lsp.json 或全局 ~/.pi/agent/lsp.json:配置 typescript-language-server / pyright / ruff / gopls / clangd 等服务器的 bin、include、rootMarkers、workingDir、initializationOptions、initializationOptionsCommand、tsserver.path、watch 与诊断超时;或排查服务器起不来(binary not found / provides no tsserver.js)、typescript alias 依赖下 tsserver 找不到、诊断一直为空、monorepo 子项目 root 不对等问题。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 配置 LSP 服务器(.pi/lsp.json)
|
|
@@ -39,19 +39,20 @@ diagnosticsFullWaitTimeoutMs / diagnosticsRequestTimeoutMs / initializeTimeoutMs
|
|
|
39
39
|
|
|
40
40
|
## servers.<id> 字段
|
|
41
41
|
|
|
42
|
-
| 字段 | 说明
|
|
43
|
-
| ---------------------------------------- |
|
|
44
|
-
| `include` | 文件 glob(相对项目根或调用 cwd,任一命中即可);缺省匹配所有文件
|
|
45
|
-
| `kind` | `language`(缺省)或 `linter`。rename / inspect 等符号级功能**只面向 `language`**;linter 只参与诊断
|
|
46
|
-
| `rootMarkers` | 项目根标记文件名数组(精确匹配,目录名亦可,如 `pyproject.toml` / `go.mod` / `.git`):从调用 cwd 沿文件路径向下查找,第一个含标记的目录即 LSP root(cwd 自身命中即 cwd),未命中回退 cwd。**与 `workingDir` 互斥**(同现报配置错误)
|
|
47
|
-
| `workingDir` | 服务器工作目录(即 LSP root):绝对路径或相对调用 cwd 的路径;缺省即调用 cwd。文件必须位于该目录内才会由本服务器处理;spawn 工作目录与 rootUri 均用它。**与 `rootMarkers` 互斥**
|
|
48
|
-
| `bin` | 可执行文件:绝对路径、相对调用 cwd 的路径、或仅名字(项目工作区优先,PATH 兜底)
|
|
49
|
-
| `args` | 启动参数数组
|
|
50
|
-
| `env` | 追加的环境变量:string 值支持 `{root}`/`{cwd}` 与 `${VAR}` 插值;`{ "sh": ["cmd","arg"] }` 启动时执行命令取 stdout(非零退出/空输出 → 启动失败并报错)
|
|
51
|
-
| `languageIdByExtension` | 扩展名(含点)→ languageId;缺省回退内置映射(见 `src/lib/lsp/language.ts`,覆盖主流语言)
|
|
52
|
-
| `startupTimeoutMs` / `diagnosticsWaitMs` | 覆盖该服务器的初始化握手 / 写文件后诊断等待(缺省用全局配置与 client 默认)
|
|
53
|
-
| `initializationOptions` | 透传给 initialize 请求;字符串值支持 `${VAR}` / `${VAR:-default}`(读 process env,**不支持 `{root}`/`{cwd}` 模板**)
|
|
54
|
-
| `
|
|
42
|
+
| 字段 | 说明 |
|
|
43
|
+
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `include` | 文件 glob(相对项目根或调用 cwd,任一命中即可);缺省匹配所有文件 |
|
|
45
|
+
| `kind` | `language`(缺省)或 `linter`。rename / inspect 等符号级功能**只面向 `language`**;linter 只参与诊断 |
|
|
46
|
+
| `rootMarkers` | 项目根标记文件名数组(精确匹配,目录名亦可,如 `pyproject.toml` / `go.mod` / `.git`):从调用 cwd 沿文件路径向下查找,第一个含标记的目录即 LSP root(cwd 自身命中即 cwd),未命中回退 cwd。**与 `workingDir` 互斥**(同现报配置错误) |
|
|
47
|
+
| `workingDir` | 服务器工作目录(即 LSP root):绝对路径或相对调用 cwd 的路径;缺省即调用 cwd。文件必须位于该目录内才会由本服务器处理;spawn 工作目录与 rootUri 均用它。**与 `rootMarkers` 互斥** |
|
|
48
|
+
| `bin` | 可执行文件:绝对路径、相对调用 cwd 的路径、或仅名字(项目工作区优先,PATH 兜底) |
|
|
49
|
+
| `args` | 启动参数数组 |
|
|
50
|
+
| `env` | 追加的环境变量:string 值支持 `{root}`/`{cwd}` 与 `${VAR}` 插值;`{ "sh": ["cmd","arg"] }` 启动时执行命令取 stdout(非零退出/空输出 → 启动失败并报错) |
|
|
51
|
+
| `languageIdByExtension` | 扩展名(含点)→ languageId;缺省回退内置映射(见 `src/lib/lsp/language.ts`,覆盖主流语言) |
|
|
52
|
+
| `startupTimeoutMs` / `diagnosticsWaitMs` | 覆盖该服务器的初始化握手 / 写文件后诊断等待(缺省用全局配置与 client 默认) |
|
|
53
|
+
| `initializationOptions` | 透传给 initialize 请求;字符串值支持 `${VAR}` / `${VAR:-default}`(读 process env,**不支持 `{root}`/`{cwd}` 模板**) |
|
|
54
|
+
| `initializationOptionsCommand` | 启动时执行命令计算 `initializationOptions`(argv 直接执行、不经 shell,每项支持 `{root}`/`{cwd}` 模板与 `${VAR}` 插值):命令以服务器 root 为 cwd、环境含解析后的 `env`(可读 `env` 的 `{sh}` 结果);stdout 必须是 JSON 对象,与静态 `initializationOptions` **深合并(命令优先)**,失败(非零退出 / 空输出 / 非 JSON 对象)即启动失败 |
|
|
55
|
+
| `settings` | `workspace/didChangeConfiguration` 与 `workspace/configuration` 请求的负载;缺省回退 `initializationOptions` |
|
|
55
56
|
|
|
56
57
|
## 子目录项目:rootMarkers
|
|
57
58
|
|
|
@@ -164,7 +165,7 @@ typescript-language-server **不内置 TypeScript**(零依赖)。启动时
|
|
|
164
165
|
|
|
165
166
|
**alias 依赖坑**:当项目把 typescript 写成 `npm:@typescript/typescript6`(alias stub)时,`node_modules/typescript/lib/` 下只有转发 stub(`typescript.js`/`tsserverlibrary.js`/`tsc.js`),**没有 `tsserver.js`** → workspace 探测失效;实体 tsserver 在 pnpm 虚拟 store:`node_modules/.pnpm/typescript@<真实版本>/node_modules/typescript/lib/tsserver.js`(目录名无 peer 后缀、相对稳定;**升级 typescript 版本后要同步更新路径**)。依赖为标准 `typescript` 包时 `node_modules/typescript` 直接是完整包,workspace 探测即命中、无需配 path。
|
|
166
167
|
|
|
167
|
-
|
|
168
|
+
这种项目里把路径写死会随 typescript 升级失效,用 `initializationOptionsCommand` 让脚本现算更稳。因为 servers 按 id 整条覆盖,本地 `.pi/lsp.json` 要写完整条目:
|
|
168
169
|
|
|
169
170
|
```json
|
|
170
171
|
{
|
|
@@ -179,16 +180,62 @@ typescript-language-server **不内置 TypeScript**(零依赖)。启动时
|
|
|
179
180
|
".js": "javascript",
|
|
180
181
|
".jsx": "javascriptreact"
|
|
181
182
|
},
|
|
182
|
-
"
|
|
183
|
-
"tsserver": {
|
|
184
|
-
"path": "/path/to/node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/tsserver.js"
|
|
185
|
-
}
|
|
186
|
-
}
|
|
183
|
+
"initializationOptionsCommand": ["node", "{root}/.pi/lsp/ts-options.mjs"]
|
|
187
184
|
}
|
|
188
185
|
}
|
|
189
186
|
}
|
|
190
187
|
```
|
|
191
188
|
|
|
189
|
+
```js
|
|
190
|
+
// .pi/lsp/ts-options.mjs:项目能解析到 typescript/lib/tsserver.js(标准依赖 / hoisted)时输出 {},
|
|
191
|
+
// 让服务器按 workspace 解析、版本随项目走;只有 alias shim(解析不到)才去 pnpm store 里找真实 tsserver。
|
|
192
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
193
|
+
import { createRequire } from "node:module";
|
|
194
|
+
import { join } from "node:path";
|
|
195
|
+
|
|
196
|
+
const requireFromProject = createRequire(join(process.cwd(), "package.json"));
|
|
197
|
+
|
|
198
|
+
// workspace 里可用的 tsserver:解析得到就不干预(hoisted 的依赖也能解析到)
|
|
199
|
+
function workspaceHasTsserver() {
|
|
200
|
+
try {
|
|
201
|
+
requireFromProject.resolve("typescript/lib/tsserver.js");
|
|
202
|
+
return true;
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// store 里带 tsserver.js 的最高版本:typescript@7 是 native 包、没有 tsserver.js,会被筛掉
|
|
209
|
+
function storeTsserver() {
|
|
210
|
+
const store = join(process.cwd(), "node_modules/.pnpm");
|
|
211
|
+
let entries;
|
|
212
|
+
try {
|
|
213
|
+
entries = readdirSync(store);
|
|
214
|
+
} catch {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
return entries
|
|
218
|
+
.map((name) => /^typescript@(\d[^/]*)$/.exec(name)?.[1])
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
|
|
221
|
+
.map((version) =>
|
|
222
|
+
join(store, `typescript@${version}`, "node_modules/typescript/lib/tsserver.js"),
|
|
223
|
+
)
|
|
224
|
+
.find((path) => existsSync(path));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const path = workspaceHasTsserver() ? undefined : storeTsserver();
|
|
228
|
+
console.log(JSON.stringify(path ? { tsserver: { path } } : {}));
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
只在某台机器上临时用、路径不常变时,也可以直接写静态值(同样要整条覆盖):
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
"initializationOptions": {
|
|
235
|
+
"tsserver": { "path": "/abs/path/node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/tsserver.js" }
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
192
239
|
## 服务器启动失败的行为(改了配置后怎么验证)
|
|
193
240
|
|
|
194
241
|
- 启动失败(binary 缺失 / initialize 拒绝)会**主动 notify 报错**(server id、root、原因、提示 `/lsp-reload <id>`),不需要等请求方触发
|
|
@@ -202,9 +249,10 @@ typescript-language-server **不内置 TypeScript**(零依赖)。启动时
|
|
|
202
249
|
|
|
203
250
|
## 常见排查
|
|
204
251
|
|
|
205
|
-
| 现象 | 原因与修法
|
|
206
|
-
| ----------------------------------------------------------------------------- |
|
|
207
|
-
| notify `failed to start …: binary not found` | `bin` 不在 PATH(测试场景二进制缺失时整组跳过)
|
|
208
|
-
| `… provides no tsserver.js. No other valid TypeScript installation was found` | workspace 的 `node_modules/typescript` 是 alias stub
|
|
209
|
-
|
|
|
210
|
-
|
|
|
252
|
+
| 现象 | 原因与修法 |
|
|
253
|
+
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
254
|
+
| notify `failed to start …: binary not found` | `bin` 不在 PATH(测试场景二进制缺失时整组跳过) |
|
|
255
|
+
| `… provides no tsserver.js. No other valid TypeScript installation was found` | workspace 的 `node_modules/typescript` 是 alias stub:用 `initializationOptionsCommand` 算 `.pnpm` 里的实体路径、写静态 `initializationOptions.tsserver.path`,或换标准 typescript 依赖 |
|
|
256
|
+
| notify `failed to start …: initializationOptions command …` | 脚本非零退出 / 空输出 / stdout 不是 JSON 对象(报错里带具体命令、退出码与 stderr) |
|
|
257
|
+
| 诊断一直为空且无任何报错 | 服务器没匹配到文件(`include`/扩展名)、在 broken 冷却中、或文件在调用 cwd 之外(LSP 只在工作目录内启用) |
|
|
258
|
+
| 读取配置直接抛错 | typebox 严格校验拒绝:字段类型不符 / 非法时长格式(未知字段只是 warning,不拒绝) |
|