@trim21/personal-pi-extensions 0.0.317 → 0.0.318

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 CHANGED
@@ -348,7 +348,7 @@ pi -e ./src/talk/index.ts
348
348
 
349
349
  read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级诊断)。LSP protocol 是统一的,因此服务器不需要为每个语言写 adapter:用一份 JSON 配置声明如何启动即可。
350
350
 
351
- 配置文件(项目优先于全局,逐字段覆盖):
351
+ 配置文件(项目优先于全局):顶层字段(`enabled`/`disabled`、超时等)本地覆盖全局;`servers` 按服务器 id 合并——同名 id 本地整体覆盖、新增 id,全局其余服务器保留。
352
352
 
353
353
  - `~/.pi/agent/lsp.json`(全局)
354
354
  - `.pi/lsp.json`(项目)
@@ -382,7 +382,7 @@ read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级
382
382
  - `startupTimeoutMs` / `diagnosticsWaitMs`:per-server 超时,覆盖全局配置与默认值
383
383
  - `initializationOptions` 与 `settings` 按 LSP 语义分离:前者进 initialize 请求,后者进 didChangeConfiguration / workspace/configuration 请求
384
384
 
385
- 内置默认服务器(typescript / pyright / ruff / clangd)始终存在;`servers` 以 key 为服务器 id 与默认合并——同 key 覆盖、新 key 新增、`"enabled": false` 移除(如 `"clangd": { "enabled": false }`)。executable 的发现逻辑(如 tsserver 路径、venv 里的 python)不内置,需要时用 `bin` / `args` / `settings` 自行表达。
385
+ 内置默认服务器(typescript / pyright / ruff / clangd)始终存在;`servers` 以 key 为服务器 id 与默认合并——同 key 整体覆盖(整个配置替换默认)、新 key 新增、`"enabled": false` 移除(如 `"clangd": { "enabled": false }`)。executable 的发现逻辑(如 tsserver 路径、venv 里的 python)不内置,需要时用 `bin` / `args` / `settings` 自行表达。
386
386
 
387
387
  旧的 `enabled`(白名单)/ `disabled`(排除)与全局超时字段(`initializeTimeoutMs` 等)继续可用。
388
388
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.317",
3
+ "version": "0.0.318",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -4,14 +4,17 @@
4
4
  * - state(client 缓存、broken 集合、spawning 去重)是 service 工厂的
5
5
  * 闭包变量,不做成模块级全局;
6
6
  * - 配置来源:全局 `~/.pi/agent/lsp.json` + 本地 `<cwd>/.pi/lsp.json`
7
- * (本地逐字段覆盖全局):`servers` 数组配置驱动地定义语言服务器
8
- * (按 id 与内置默认服务器合并),`enabled`/`disabled` 白名单与各超时
9
- * 参数继续生效;配置在每个工具的调用 cwd 下惰性读取;
7
+ * (本地覆盖全局):`servers` 按 id 合并(同名 id 整体覆盖、新增 id,全局
8
+ * 其余服务器保留),`enabled`/`disabled` 白名单与各超时参数继续生效;
9
+ * 配置在每个工具的调用 cwd 下惰性读取;enabled/disabled 引用不存在的
10
+ * 服务器 id 是配置错误:全局配置在扩展加载(createLspService)时抛错,
11
+ * 本地配置在 session 开始预加载时通知,工具调用时校验抛错兜底;
10
12
  * - client 按 (root, serverID) 缓存,并发 spawn 去重,启动失败记入 broken
11
13
  * 集合(服务实例生命周期内不再重试);
12
14
  * - 工具只与 touchFile / diagnostics / lspDiagnosticsForFile 三个方法打交道;通知回调按请求传入。
13
15
  */
14
16
 
17
+ import { readFileSync } from "node:fs";
15
18
  import { readFile } from "node:fs/promises";
16
19
  import { homedir } from "node:os";
17
20
  import { extname, join, normalize, sep } from "node:path";
@@ -23,7 +26,13 @@ import { Value } from "typebox/value";
23
26
  import { type LspServerAdapter } from "./adapter.js";
24
27
  import { create, type CreateInput, type Diagnostic, type Info as LspClient } from "./client.js";
25
28
  import { report } from "./diagnostic.js";
26
- import { createAdapters, serverConfigSchema } from "./server-config.js";
29
+ import {
30
+ createAdapters,
31
+ defaultServers,
32
+ mergeServerConfigs,
33
+ mergeServerRecords,
34
+ serverConfigSchema,
35
+ } from "./server-config.js";
27
36
 
28
37
  /** 超时值:number(毫秒,>=1)或字符串("500"、"5s"、"1m"),Parse 后由 toMs 统一换算。 */
29
38
  const timeoutValue = Type.Union([Type.Number({ minimum: 1 }), Type.String()]);
@@ -99,9 +108,42 @@ async function readConfigFile(filePath: string): Promise<LspConfig> {
99
108
  }
100
109
  }
101
110
 
111
+ /** 同步版 readConfigFile(扩展加载时校验全局配置用)。 */
112
+ function readConfigFileSync(filePath: string): LspConfig {
113
+ try {
114
+ const raw = readFileSync(filePath, "utf8");
115
+ return Value.Parse(lspConfigSchema, JSON.parse(raw) as unknown);
116
+ } catch {
117
+ return {};
118
+ }
119
+ }
120
+
121
+ /**
122
+ * enabled/disabled 引用的 id 必须存在于实际生效的服务器集合(注入的 adapters
123
+ * 或默认 + 配置 servers),否则抛配置错误(避免静默失效)。
124
+ */
125
+ function validateConfig(config: LspConfig, adapters?: LspServerAdapter[]): void {
126
+ const available = new Set(
127
+ adapters
128
+ ? adapters.map((adapter) => adapter.id)
129
+ : Object.keys(mergeServerConfigs(defaultServers, config.servers)),
130
+ );
131
+ const unknown = [...(config.enabled ?? []), ...(config.disabled ?? [])].filter(
132
+ (id) => !available.has(id),
133
+ );
134
+ if (unknown.length > 0) {
135
+ const list = [...available].toSorted().join(", ") || "none";
136
+ throw new Error(
137
+ `lsp.json: unknown server id in enabled/disabled: ${unknown.join(", ")} (available: ${list})`,
138
+ );
139
+ }
140
+ }
141
+
102
142
  /**
103
143
  * 合并后的生效配置:全局 `~/.pi/agent/lsp.json` 为基底,本地
104
- * `<cwd>/.pi/lsp.json` 逐字段覆盖。
144
+ * `<cwd>/.pi/lsp.json` 覆盖——顶层标量字段(enabled/disabled、超时等)本地
145
+ * 直接替换;`servers` 按 id 合并(同名 id 整体覆盖、新增 id,全局其余服务器
146
+ * 保留)。
105
147
  */
106
148
  export async function loadLspConfig(
107
149
  cwd: string,
@@ -111,14 +153,23 @@ export async function loadLspConfig(
111
153
  readConfigFile(globalConfigPath),
112
154
  readConfigFile(join(cwd, ".pi", "lsp.json")),
113
155
  ]);
114
- return { ...globalConfig, ...localConfig };
156
+ const servers = mergeServerRecords(globalConfig.servers, localConfig.servers);
157
+ return {
158
+ ...globalConfig,
159
+ ...localConfig,
160
+ ...(servers && { servers }),
161
+ };
115
162
  }
116
163
 
117
- /** 按配置过滤 adapter 列表。 */
164
+ /**
165
+ * 按配置过滤 adapter 列表。enabled/disabled 引用了实际生效集合中不存在的
166
+ * 服务器 id 时抛错(配置错误,避免静默失效)。
167
+ */
118
168
  export function filterAdapters(
119
169
  adapters: LspServerAdapter[],
120
170
  config: LspConfig,
121
171
  ): LspServerAdapter[] {
172
+ validateConfig(config, adapters);
122
173
  return adapters.filter((adapter) => {
123
174
  if (config.enabled && !config.enabled.includes(adapter.id)) return false;
124
175
  if (config.disabled?.includes(adapter.id)) return false;
@@ -166,6 +217,11 @@ export function createLspService(
166
217
  adapters?: LspServerAdapter[],
167
218
  globalConfigPath?: string,
168
219
  ): LspService {
220
+ // 扩展加载时校验全局配置(本地配置在 session_start 预加载时校验)
221
+ validateConfig(
222
+ readConfigFileSync(globalConfigPath ?? join(homedir(), ".pi", "agent", "lsp.json")),
223
+ adapters,
224
+ );
169
225
  const state: LspState = {
170
226
  clients: [],
171
227
  broken: new Set(),
@@ -333,5 +389,13 @@ export interface LspServiceOptions {
333
389
  export function registerLsp(pi: ExtensionAPI, options?: LspServiceOptions): LspService {
334
390
  const service = createLspService(options?.adapters, options?.globalConfigPath);
335
391
  pi.on?.("session_shutdown", () => service.shutdownAll());
392
+ // session 开始是最早能拿到本地配置 cwd 的时机:预加载并校验,配置错误立即通知
393
+ pi.on?.("session_start", (_event, ctx) => {
394
+ void loadLspConfig(ctx.cwd, options?.globalConfigPath)
395
+ .then((config) => validateConfig(config, options?.adapters))
396
+ .catch((error: unknown) => {
397
+ if (error instanceof Error) ctx.ui.notify?.(error.message, "error");
398
+ });
399
+ });
336
400
  return service;
337
401
  }
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * 配置文件沿用 lsp.json(全局 ~/.pi/agent/lsp.json + 本地 <cwd>/.pi/lsp.json):
6
6
  * 顶层 `servers` 是 id → 配置的 record,按 id 与内置默认服务器合并
7
- * (覆盖同名、新增 id、enabled:false 禁用),之后仍受现有 enabled/disabled
8
- * 白名单过滤。
7
+ * (同名 id 整体覆盖、新增 id、enabled:false 禁用),之后仍受现有
8
+ * enabled/disabled 白名单过滤。
9
9
  *
10
10
  * executable 发现统一由用户配置:bin 支持绝对路径 / 项目工作区
11
11
  * (node_modules/.bin、.venv/bin、venv/bin)/ PATH,不再内置各语言的
@@ -111,7 +111,19 @@ export const defaultServers: Record<string, ServerConfig> = {
111
111
  },
112
112
  };
113
113
 
114
- /** 用户 servers(id 配置)与默认配置合并:覆盖同名、新增 id、enabled:false 移除。 */
114
+ /** id 合并 servers record:同名 id 整体覆盖(不做逐字段 merge),其余保留;返回 undefined 表示没有任何 servers 定义。 */
115
+ export function mergeServerRecords(
116
+ ...records: (Readonly<Record<string, ServerConfig>> | undefined)[]
117
+ ): Record<string, ServerConfig> | undefined {
118
+ const merged: Record<string, ServerConfig> = {};
119
+ for (const record of records) {
120
+ if (!record) continue;
121
+ Object.assign(merged, record);
122
+ }
123
+ return Object.keys(merged).length > 0 ? merged : undefined;
124
+ }
125
+
126
+ /** 用户 servers(id → 配置)与默认配置合并:同名 id 整体覆盖、新增 id、enabled:false 移除。 */
115
127
  export function mergeServerConfigs(
116
128
  defaults: Readonly<Record<string, ServerConfig>>,
117
129
  user: Readonly<Record<string, ServerConfig>> | undefined,