@trim21/personal-pi-extensions 0.1.469 → 0.1.471

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
@@ -365,7 +365,7 @@ read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级
365
365
  "bin": "gopls",
366
366
  "args": [],
367
367
  "cwd": "{root}", // 支持 {root} / {cwd} 模板
368
- "env": { "VIRTUAL_ENV": "{root}/.venv", "PATH_SUFFIX": "${EXTRA_PATH}" }, // 追加到子进程环境变量;值支持 {root} / {cwd} 模板与 ${VAR} 引用
368
+ "env": { "VIRTUAL_ENV": "{root}/.venv", "GITHUB_TOKEN": { "sh": ["gh", "auth", "token"] } }, // 追加到子进程环境变量;string 值支持 {root} / {cwd} 模板与 ${VAR} 引用,{sh} 启动时执行命令取 stdout(失败则服务器启动失败)
369
369
  "languageIdByExtension": { ".go": "go" },
370
370
  "startupTimeoutMs": 45000,
371
371
  "diagnosticsWaitMs": 1500,
@@ -390,9 +390,32 @@ read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级
390
390
  - `bin`:可执行文件——绝对路径、相对调用 cwd 的路径,或名字(先在项目内 `node_modules/.bin`、`.venv/bin`、`venv/bin` 找,再走 PATH)
391
391
  - `languageIdByExtension`:扩展名 → LSP languageId(didOpen 用);缺省回退内置映射表
392
392
  - `startupTimeoutMs` / `diagnosticsWaitMs`:per-server 超时,覆盖全局配置与默认值
393
- - `env`:追加到 LSP 子进程的环境变量(在 `process.env` 之上合并);值支持 `{root}` / `{cwd}` 模板与 `${VAR}` 环境变量引用
393
+ - `env`:追加到 LSP 子进程的环境变量(在 `process.env` 之上合并)。string 值支持 `{root}` / `{cwd}` 模板与 `${VAR}` 环境变量引用;`{ "sh": [...] }` 在服务器启动时执行命令(argv 直接执行、不经 shell,需要 shell 特性时自行包 `["bash", "-c", "..."]`),stdout trim 后作为值,命令失败(非零退出或输出为空)时该服务器启动失败并报错
394
394
  - `initializationOptions` 与 `settings` 按 LSP 语义分离:前者进 initialize 请求,后者进 didChangeConfiguration / workspace/configuration 请求;`initializationOptions` 的字符串值(含嵌套对象/数组)在启动时做 `${VAR}` 插值,`${VAR:-default}` 在变量未定义或为空时用 default,未定义且无 default 替换为空字符串;插值时可引用 `env` 里配置的变量
395
395
 
396
+ `env` 的 `{sh}` 命令与 `initializationOptions` 插值可以组合使用,例如用 `gh auth token` 给服务器的 initialize 请求提供 session token:
397
+
398
+ ```jsonc
399
+ {
400
+ "servers": {
401
+ "github-lsp": {
402
+ "bin": "github-lsp",
403
+ "args": ["--stdio"],
404
+ "env": {
405
+ // 启动时执行 gh auth token,stdout(trim 后)成为环境变量 GITHUB_TOKEN
406
+ "GITHUB_TOKEN": { "sh": ["gh", "auth", "token"] },
407
+ },
408
+ "initializationOptions": {
409
+ // 插值引用上面命令的输出,随 initialize 请求发给服务器
410
+ "sessionToken": "${GITHUB_TOKEN}",
411
+ },
412
+ },
413
+ },
414
+ }
415
+ ```
416
+
417
+ 命令失败(`gh` 未登录 / 不在 PATH)时该服务器启动失败并报错。
418
+
396
419
  没有内置默认服务器:`servers` 的 key 就是服务器 id,全部来自你的配置,未定义 `servers` 时不启动任何语言服务器。executable 的发现逻辑(如 tsserver 路径、venv 里的 python)不内置,需要时用 `bin` / `args` / `settings` 自行表达。
397
420
 
398
421
  启用控制只有顶层两处:`enabled`(白名单)与 `disabled`(排除),按服务器 id 生效。`enabled` 里的 id 必须是已配置服务器,否则视为配置错误;`disabled` 里未注册的 id 直接忽略。全局超时字段(`initializeTimeoutMs` 等)同样配在顶层。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.469",
3
+ "version": "0.1.471",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -12,8 +12,10 @@
12
12
  * 特殊探测逻辑(tsserver 路径、venv python 等)。
13
13
  */
14
14
 
15
+ import { execFile as nodeExecFile } from "node:child_process";
15
16
  import { existsSync } from "node:fs";
16
17
  import { isAbsolute, join, relative, sep } from "node:path";
18
+ import { promisify } from "node:util";
17
19
 
18
20
  import { minimatch } from "minimatch";
19
21
  import { type Static, Type } from "typebox";
@@ -39,8 +41,17 @@ export const serverConfigSchema = Type.Object({
39
41
  args: Type.Optional(Type.Array(Type.String())),
40
42
  /** 启动工作目录,支持 {root} / {cwd} 模板;缺省 {root}。 */
41
43
  cwd: Type.Optional(Type.String()),
42
- /** 追加到子进程的环境变量;值支持 {root} / {cwd} 模板与 ${VAR} 环境变量引用。 */
43
- env: Type.Optional(Type.Record(Type.String(), Type.String())),
44
+ /**
45
+ * 追加到子进程的环境变量。string 值支持 {root} / {cwd} 模板与 ${VAR} 引用;
46
+ * {sh: [...]} 在启动时执行命令(argv 直接执行、不经 shell),stdout trim 后作为值,
47
+ * 失败(非零退出 / 空输出)时服务器启动失败并报错。
48
+ */
49
+ env: Type.Optional(
50
+ Type.Record(
51
+ Type.String(),
52
+ Type.Union([Type.String(), Type.Object({ sh: Type.Array(Type.String()) })]),
53
+ ),
54
+ ),
44
55
  /** 文件扩展名(含点)→ LSP languageId,didOpen 用;缺省回退内置映射表。 */
45
56
  languageIdByExtension: Type.Optional(Type.Record(Type.String(), Type.String())),
46
57
  /** initialize 握手超时(ms);缺省用全局配置 / client 默认。 */
@@ -101,20 +112,50 @@ function interpolateEnvDeep(value: unknown, env: NodeJS.ProcessEnv): unknown {
101
112
  return value;
102
113
  }
103
114
 
104
- /** 解析 per-server env:值先做 {root} / {cwd} 模板,再做环境变量引用插值。 */
105
- function resolveEnv(
106
- config: Record<string, string> | undefined,
115
+ /** 解析 per-server env:string 值做 {root} / {cwd} 模板 + 环境变量引用插值,{sh} 执行命令取 stdout。 */
116
+ async function resolveEnv(
117
+ config: Record<string, string | { sh: string[] }> | undefined,
107
118
  root: string,
108
119
  cwd: string,
109
- ): NodeJS.ProcessEnv | undefined {
120
+ ): Promise<NodeJS.ProcessEnv | undefined> {
110
121
  const entries = Object.entries(config ?? {});
111
122
  if (entries.length === 0) return undefined;
112
- return Object.fromEntries(
113
- entries.map(([key, value]) => [
114
- key,
115
- interpolateEnvVars(resolveTemplate(value, root, cwd), process.env),
116
- ]),
123
+ const resolved = await Promise.all(
124
+ entries.map(async ([key, value]): Promise<[string, string]> => {
125
+ if (typeof value === "string") {
126
+ return [key, interpolateEnvVars(resolveTemplate(value, root, cwd), process.env)];
127
+ }
128
+ return [key, await runEnvCommand(value.sh, cwd)];
129
+ }),
117
130
  );
131
+ return Object.fromEntries(resolved);
132
+ }
133
+
134
+ const execFile = promisify(nodeExecFile);
135
+
136
+ /**
137
+ * argv 直接执行(不经 shell,避免注入面;需要 shell 特性时配置里自行包
138
+ * ["bash", "-c", "..."]),stdout trim 后作为 env 值。
139
+ * 失败(spawn 错误 / 非零退出 / 空输出)时抛错,由 lsp.ts 捕获后 notify 给用户。
140
+ */
141
+ async function runEnvCommand(argv: string[], cwd: string): Promise<string> {
142
+ let stdout: string;
143
+ try {
144
+ ({ stdout } = await execFile(argv[0], argv.slice(1), { cwd }));
145
+ } catch (error) {
146
+ const err = error as NodeJS.ErrnoException & { stderr?: string };
147
+ const reason = typeof err.code === "number" ? `exit code ${err.code}` : err.message;
148
+ const stderr = err.stderr?.trim();
149
+ throw new Error(
150
+ `env command failed (${argv.join(" ")}): ${reason}${stderr ? `: ${stderr}` : ""}`,
151
+ { cause: error },
152
+ );
153
+ }
154
+ const output = stdout.trim();
155
+ if (!output) {
156
+ throw new Error(`env command (${argv.join(" ")}) produced empty output`);
157
+ }
158
+ return output;
118
159
  }
119
160
 
120
161
  /** 解析可执行文件:绝对/相对路径直接用;名字走项目工作区(node_modules/.bin 等)→ PATH。 */
@@ -184,10 +225,11 @@ export class ConfigAdapter implements LspServerAdapter {
184
225
  if (!bin) return undefined;
185
226
  const resolved = await resolveBinary(bin, root, cwd);
186
227
  if (!resolved) return undefined;
187
- const env = resolveEnv(this.config.env, root, cwd);
228
+ const spawnCwd = resolveTemplate(this.config.cwd ?? "{root}", root, cwd);
229
+ const env = await resolveEnv(this.config.env, root, cwd);
188
230
  return {
189
231
  process: spawnProcess(resolved, this.config.args ?? [], {
190
- cwd: resolveTemplate(this.config.cwd ?? "{root}", root, cwd),
232
+ cwd: spawnCwd,
191
233
  env,
192
234
  }),
193
235
  initialization: interpolateEnvDeep(this.config.initializationOptions, {
@@ -67,5 +67,4 @@ In code: default to writing no comments. Never write multi-paragraph docstrings
67
67
  {{skills}}
68
68
  {{append}}
69
69
 
70
- Current date: {{date}}
71
70
  Current working directory: {{cwd}}