@trim21/personal-pi-extensions 0.1.612 → 0.1.619

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.
Files changed (77) hide show
  1. package/package.json +1 -1
  2. package/src/aft/config.ts +9 -3
  3. package/src/aft/index.ts +6 -2
  4. package/src/aft/logger.ts +9 -3
  5. package/src/aft/tools.ts +3 -1
  6. package/src/bwrap/README.md +19 -10
  7. package/src/bwrap/approval-rules.ts +42 -14
  8. package/src/bwrap/approval-suggest.ts +12 -4
  9. package/src/bwrap/core.ts +39 -13
  10. package/src/bwrap/dcg-scan.ts +16 -6
  11. package/src/bwrap/holder.ts +3 -1
  12. package/src/bwrap/mihomo-config.ts +18 -6
  13. package/src/bwrap/network-stack.ts +69 -24
  14. package/src/bwrap/runtime.ts +30 -10
  15. package/src/bwrap/sandbox.ts +1 -1
  16. package/src/claude-code/common.ts +6 -2
  17. package/src/claude-code/edit-utils.ts +3 -1
  18. package/src/claude-code/files.ts +40 -14
  19. package/src/claude-code/glob.ts +12 -4
  20. package/src/claude-code/grep.ts +41 -14
  21. package/src/claude-code/session-tools.ts +18 -6
  22. package/src/claude-code/shell.ts +9 -3
  23. package/src/gh/base.ts +72 -24
  24. package/src/gh/tools/download-release-assets.ts +12 -4
  25. package/src/gh/tools/list-releases.ts +3 -1
  26. package/src/gh/tools/list-workflow-runs.ts +9 -3
  27. package/src/gh/tools/read-ci-logs.ts +21 -7
  28. package/src/gh/tools/read-repo.ts +3 -1
  29. package/src/lib/cli-args.ts +6 -2
  30. package/src/lib/cli.ts +48 -16
  31. package/src/lib/file-reads.ts +24 -8
  32. package/src/lib/github.ts +35 -12
  33. package/src/lib/lsp/adapter.ts +6 -2
  34. package/src/lib/lsp/bin.ts +12 -4
  35. package/src/lib/lsp/client.ts +205 -69
  36. package/src/lib/lsp/diagnostic.ts +6 -2
  37. package/src/lib/lsp/inspect-tool.ts +25 -9
  38. package/src/lib/lsp/lsp.ts +138 -46
  39. package/src/lib/lsp/rename-tool.ts +11 -4
  40. package/src/lib/lsp/rename.ts +25 -8
  41. package/src/lib/lsp/server-config.ts +41 -14
  42. package/src/lib/lsp/watcher.ts +51 -17
  43. package/src/lib/path.ts +18 -6
  44. package/src/lib/proc.ts +3 -1
  45. package/src/lib/proxy.ts +9 -3
  46. package/src/lib/request-policy.ts +3 -1
  47. package/src/lib/seq-state.ts +3 -1
  48. package/src/lib/ui.ts +23 -8
  49. package/src/lib/write-guard.ts +21 -7
  50. package/src/openai-cost/config.ts +3 -1
  51. package/src/openai-cost/cost.ts +54 -18
  52. package/src/openai-cost/index.ts +3 -1
  53. package/src/openai-cost/provider.ts +6 -2
  54. package/src/opencode/bash.ts +9 -3
  55. package/src/opencode/edit-engine.ts +24 -8
  56. package/src/opencode/files.ts +30 -10
  57. package/src/opencode/glob.ts +3 -1
  58. package/src/opencode/grep.ts +12 -4
  59. package/src/opencode/question.ts +3 -1
  60. package/src/opencode/ripgrep.ts +18 -6
  61. package/src/opencode/todo.ts +3 -1
  62. package/src/session-name.ts +39 -13
  63. package/src/spawn-agent-agents.ts +18 -6
  64. package/src/spawn-agent.ts +53 -18
  65. package/src/system-prompt/index.ts +9 -3
  66. package/src/talk/core.ts +143 -48
  67. package/src/talk/format.ts +12 -4
  68. package/src/talk/group.ts +9 -3
  69. package/src/talk/index.ts +30 -10
  70. package/src/talk/mailbox.ts +30 -10
  71. package/src/talk/policy.ts +6 -2
  72. package/src/talk/registry.ts +33 -11
  73. package/src/talk/storage.ts +3 -1
  74. package/src/vision-agent.ts +41 -14
  75. package/src/web/config.ts +3 -1
  76. package/src/web/fetch.ts +75 -25
  77. package/src/web/search.ts +30 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.612",
3
+ "version": "0.1.619",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
package/src/aft/config.ts CHANGED
@@ -86,8 +86,12 @@ function remoteOf(semantic: RawConfig["semantic"]): SemanticRemote | undefined {
86
86
  const backend = trimmedOrUndefined(semantic?.backend);
87
87
  // 去掉尾斜杠:aft 侧按 base_url + /embeddings 拼请求。
88
88
  const baseUrl = trimmedOrUndefined(semantic?.base_url)?.replace(/\/+$/, "");
89
- if (backend === undefined || baseUrl === undefined) return undefined;
90
- if (!isRemoteBackend(backend)) return undefined;
89
+ if (backend === undefined || baseUrl === undefined) {
90
+ return undefined;
91
+ }
92
+ if (!isRemoteBackend(backend)) {
93
+ return undefined;
94
+ }
91
95
  return {
92
96
  backend,
93
97
  baseUrl,
@@ -110,7 +114,9 @@ function toReadConfig(raw: RawConfig): AftReadConfig {
110
114
  */
111
115
  export function loadAftConfig(userConfigPath: string): AftReadConfig {
112
116
  try {
113
- if (!existsSync(userConfigPath)) return DEFAULTS;
117
+ if (!existsSync(userConfigPath)) {
118
+ return DEFAULTS;
119
+ }
114
120
  const parsed: unknown = parse(readFileSync(userConfigPath, "utf8"));
115
121
  return toReadConfig(parseWithSchema(rawConfigSchema, parsed ?? {}));
116
122
  } catch {
package/src/aft/index.ts CHANGED
@@ -34,7 +34,9 @@ import {
34
34
  export default function aftReadTools(pi: ExtensionAPI): void {
35
35
  const cwd = process.cwd();
36
36
  const cfg = loadAftConfig(resolveCortexKitConfigPaths(cwd).userConfigPath);
37
- if (!cfg.enabled) return;
37
+ if (!cfg.enabled) {
38
+ return;
39
+ }
38
40
 
39
41
  // bridge 状态跟 session 生命周期走,作用域就是本工厂闭包,不落到模块级。
40
42
  let state: Awaited<ReturnType<typeof createAftState>> | null = null;
@@ -88,7 +90,9 @@ export default function aftReadTools(pi: ExtensionAPI): void {
88
90
  const shutdown = async (): Promise<void> => {
89
91
  const current = state;
90
92
  state = null;
91
- if (!current) return;
93
+ if (!current) {
94
+ return;
95
+ }
92
96
  try {
93
97
  await shutdownAftPool(current.pool);
94
98
  await current.logger.drain();
package/src/aft/logger.ts CHANGED
@@ -43,7 +43,9 @@ export function createAftLogger(sessionId?: string): AftLogger {
43
43
  }
44
44
 
45
45
  function flush(): void {
46
- if (buffer.length === 0) return;
46
+ if (buffer.length === 0) {
47
+ return;
48
+ }
47
49
  const data = buffer.join("");
48
50
  buffer = [];
49
51
  try {
@@ -54,7 +56,9 @@ export function createAftLogger(sessionId?: string): AftLogger {
54
56
  }
55
57
 
56
58
  function scheduleFlush(): void {
57
- if (flushTimer) return;
59
+ if (flushTimer) {
60
+ return;
61
+ }
58
62
  flushTimer = setTimeout(() => {
59
63
  flushTimer = null;
60
64
  flush();
@@ -92,7 +96,9 @@ export function createAftLogger(sessionId?: string): AftLogger {
92
96
  getLogFilePath: logFilePath,
93
97
  drain: async (): Promise<void> => {
94
98
  flush();
95
- if (sink) await sink.drain();
99
+ if (sink) {
100
+ await sink.drain();
101
+ }
96
102
  },
97
103
  };
98
104
  }
package/src/aft/tools.ts CHANGED
@@ -444,7 +444,9 @@ export function subscribeBridgeStatus(
444
444
  bridge: AftProjectTransport,
445
445
  listener: (snapshot: StatusSnapshot) => void,
446
446
  ): (() => void) | undefined {
447
- if (!isStatusSubscribable(bridge)) return undefined;
447
+ if (!isStatusSubscribable(bridge)) {
448
+ return undefined;
449
+ }
448
450
  return bridge.subscribeStatus(listener);
449
451
  }
450
452
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 进程模型
8
8
 
9
- `network: limited` 模式下,一个沙箱 session 的常驻进程树(宿主侧视角,共 4 个):
9
+ `network: limited` 模式下,每条命令的网络栈进程树(宿主侧视角,共 4 个):
10
10
 
11
11
  ```
12
12
  pi 进程(network-stack.ts)
@@ -21,7 +21,7 @@ pi 进程(network-stack.ts)
21
21
  必须在宿主 netns 启动(原因见「设计约束」);持 exit-fd 读端 + tapfd。
22
22
  ```
23
23
 
24
- 每条命令的短命子树(命令结束即退,与常驻栈无关):
24
+ 每条命令的短命子树(命令结束即退):
25
25
 
26
26
  ```
27
27
  nsenter -U -n --preserve-credentials -t <①的pid> \
@@ -29,7 +29,8 @@ nsenter -U -n --preserve-credentials -t <①的pid> \
29
29
  ```
30
30
 
31
31
  nsenter 进入 holder 的 userns/netns,bwrap 在里面再嵌套创建自己的 user/pid
32
- ns 跑命令。一个 session 内 N 条命令复用同一套常驻栈。
32
+ ns 跑命令。网络栈是每条命令现建现停(起栈 ~40ms、停栈 ~100ms),不跨命令复用:
33
+ allowlist 变更因此即时生效,代价是每条命令重新启动一次 mihomo。
33
34
 
34
35
  ## 网络路径
35
36
 
@@ -54,14 +55,16 @@ exit-fd(socketpair)是 slirp4netns 与 holder 之间唯一的生命周期绑
54
55
  访问器(`stdio[3].fd` 恒为 undefined),只能经 `_handle.fd` 取原始 fd 再
55
56
  dup 给 slirp4netns,且仅在子进程存活期间有效。
56
57
 
57
- | 触发 | 清理链路 |
58
- | ----------------- | ----------------------------------------------------------------------------------------------------- |
59
- | 正常 `stop()` | SIGTERM ④(先杀,它 pin 着 netns)→ SIGTERM ① → `--kill-child` 转发给 ② → ② 杀 ③ 退出 → 内核清 pid ns |
60
- | pi 进程被 SIGKILL | stdin 写端关闭 → ② EOF 自杀 → pid ns 清理 → exit-fd 写端关闭 → ④ HUP 自杀 → tapfd 释放 → netns 销毁 |
61
- | 单独 kill ① | PDEATHSIG → ② SIGTERM → 同上;① wait 结束退出 → 写端全关 → ④ 退 |
62
- | 单独 kill ② | pid-ns init 死 → 内核清 ③;① 退出 → 写端全关 → ④ 退 |
58
+ | 触发 | 清理链路 |
59
+ | ---------------------- | ---------------------------------------------------------------------------------------------------- |
60
+ | 正常 `stop()` | SIGTERM ④(先杀,它 pin 着 netns)→ SIGKILL ① → PDEATHSIG 给 ② SIGTERM → ② 杀 ③ 退出 → 内核清 pid ns |
61
+ | pi 进程被 SIGKILL | stdin 写端关闭 → ② EOF 自杀 → pid ns 清理 → exit-fd 写端关闭 → ④ HUP 自杀 → tapfd 释放 → netns 销毁 |
62
+ | 单独 kill ①(SIGKILL) | PDEATHSIG → ② SIGTERM → 同上;① wait 结束退出 → 写端全关 → ④ 退 |
63
+ | 单独 kill ② | pid-ns init 死 → 内核清 ③;① 退出 → 写端全关 → ④ 退 |
63
64
 
64
- 四条路径下常驻进程全部收敛、netns 引用归零。
65
+ 四条路径下网络栈全部进程收敛、netns 引用归零。注意 `--kill-child` 不是信号转发:
66
+ 它的实现是 unshare fork 出的子进程给自己设 PDEATHSIG(unshare 死亡时收到 SIGTERM),
67
+ 所以终止 ① 只能靠 SIGKILL(见「设计约束」第 5 条)。
65
68
 
66
69
  ## 设计约束与教训
67
70
 
@@ -81,6 +84,12 @@ dup 给 slirp4netns,且仅在子进程存活期间有效。
81
84
  日志;`nsenter -U -n --preserve-credentials -t <holderPid>` 可手动进入
82
85
  netns 用 AF_PACKET 抓 tap0 / 检查 `ip rule`(注意:沙盒里看不到宿主机
83
86
  进程,宿主机诊断必须在沙盒外做)。
87
+ 5. **终止 unshare 只能用 SIGKILL**。util-linux 的 unshare 在 fork 前
88
+ `sigprocmask(SIG_BLOCK, {SIGINT, SIGTERM})`,且只在子进程里恢复掩码:
89
+ 父进程永久阻塞这两个信号且不装 handler,发给它的 SIGTERM 只会 pending
90
+ 永不投递(实测 3s 后仍存活)。曾因此在 `stop()` 里白等 `waitForExit`
91
+ 的 2000ms 默认超时,把每条命令的沙箱开销从 ~180ms 抬到 ~2.09s。SIGKILL
92
+ 立即生效,PDEATHSIG 再把 SIGTERM 交给 ② 走优雅退出。
84
93
 
85
94
  ## 调试入口
86
95
 
@@ -106,7 +106,9 @@ function extractParts(node: Node): { name: string; args: string[] } {
106
106
  };
107
107
  for (let i = 0; i < node.childCount; i++) {
108
108
  const child = node.child(i);
109
- if (child) visit(child);
109
+ if (child) {
110
+ visit(child);
111
+ }
110
112
  }
111
113
  return { name: name.join(" "), args };
112
114
  }
@@ -117,24 +119,34 @@ const FILE_OUTPUT_REDIRECT_OPS = new Set([">", ">>", ">|", "&>", "&>>"]);
117
119
  function fileRedirectWritesToFile(node: Node): boolean {
118
120
  for (let i = 0; i < node.childCount; i++) {
119
121
  const child = node.child(i);
120
- if (!child) continue;
121
- if (FILE_OUTPUT_REDIRECT_OPS.has(child.type)) return true;
122
+ if (!child) {
123
+ continue;
124
+ }
125
+ if (FILE_OUTPUT_REDIRECT_OPS.has(child.type)) {
126
+ return true;
127
+ }
122
128
  // `<>` 被解析成 `<` + 含 `>` 的 ERROR
123
- if (child.type === "ERROR" && child.text.includes(">")) return true;
129
+ if (child.type === "ERROR" && child.text.includes(">")) {
130
+ return true;
131
+ }
124
132
  }
125
133
  return false;
126
134
  }
127
135
 
128
136
  function treeHasFileOutputRedirect(root: Node): boolean {
129
137
  for (const node of root.descendantsOfType("file_redirect")) {
130
- if (fileRedirectWritesToFile(node)) return true;
138
+ if (fileRedirectWritesToFile(node)) {
139
+ return true;
140
+ }
131
141
  }
132
142
  return false;
133
143
  }
134
144
 
135
145
  function collectCommand(node: Node, all: Node[]): BashCommand | undefined {
136
146
  const { name, args } = extractParts(node);
137
- if (!name) return undefined;
147
+ if (!name) {
148
+ return undefined;
149
+ }
138
150
  // 嵌套命令 = 完全落在本命令范围内的其他 command 节点(含 `$(...)` 内的)。
139
151
  // 用位置判断而非 descendantsOfType 递归:0.26 的 descendantsOfType 会包含
140
152
  // 自身且每次返回新 wrapper,`===` 比较失效会无限递归。
@@ -166,7 +178,9 @@ export async function parseBashCommands(command: string): Promise<ParsedBash> {
166
178
  const commands: BashCommand[] = [];
167
179
  for (const node of all) {
168
180
  const parsed = collectCommand(node, all);
169
- if (parsed) commands.push(parsed);
181
+ if (parsed) {
182
+ commands.push(parsed);
183
+ }
170
184
  }
171
185
  return { commands, hasFileOutputRedirect: treeHasFileOutputRedirect(tree.rootNode) };
172
186
  } catch (error) {
@@ -189,7 +203,9 @@ export function matchRule(input: string, pattern: string): boolean {
189
203
  .replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`)
190
204
  .replaceAll("*", ".*")
191
205
  .replaceAll("?", ".");
192
- if (escaped.endsWith(" .*")) escaped = escaped.slice(0, -3) + "( .*)?";
206
+ if (escaped.endsWith(" .*")) {
207
+ escaped = escaped.slice(0, -3) + "( .*)?";
208
+ }
193
209
  return new RegExp(`^${escaped}$`, "s").test(input);
194
210
  }
195
211
 
@@ -214,16 +230,28 @@ export async function evaluateBashApproval(
214
230
  const raws: string[] = [];
215
231
  const visit = (cmd: BashCommand) => {
216
232
  raws.push(cmd.raw);
217
- for (const nested of cmd.nested) visit(nested);
233
+ for (const nested of cmd.nested) {
234
+ visit(nested);
235
+ }
218
236
  };
219
- for (const cmd of parsed.commands) visit(cmd);
220
- if (raws.length === 0) return;
237
+ for (const cmd of parsed.commands) {
238
+ visit(cmd);
239
+ }
240
+ if (raws.length === 0) {
241
+ return;
242
+ }
221
243
  let allowed = 0;
222
244
  for (const raw of raws) {
223
245
  const rule = rules.findLast((r) => matchRule(raw, r.pattern));
224
- if (rule?.action === "deny") return "deny";
225
- if (rule?.action === "allow") allowed++;
246
+ if (rule?.action === "deny") {
247
+ return "deny";
248
+ }
249
+ if (rule?.action === "allow") {
250
+ allowed++;
251
+ }
252
+ }
253
+ if (parsed.hasFileOutputRedirect) {
254
+ return;
226
255
  }
227
- if (parsed.hasFileOutputRedirect) return;
228
256
  return allowed === raws.length ? "allow" : undefined;
229
257
  }
@@ -30,9 +30,13 @@ export function commandPattern(command: BashCommand): string {
30
30
  for (let len = tokens.length; len > 0; len--) {
31
31
  const prefix = tokens.slice(0, len).join(" ");
32
32
  const arity = ARITY[prefix];
33
- if (arity !== undefined) return [...tokens.slice(0, arity), "*"].join(" ");
33
+ if (arity !== undefined) {
34
+ return [...tokens.slice(0, arity), "*"].join(" ");
35
+ }
36
+ }
37
+ if (tokens.length === 0) {
38
+ return "*";
34
39
  }
35
- if (tokens.length === 0) return "*";
36
40
  return [tokens[0], "*"].join(" ");
37
41
  }
38
42
 
@@ -41,9 +45,13 @@ function patternsFromCommands(commands: BashCommand[]): string[] {
41
45
  const flat: string[] = [];
42
46
  const visit = (cmd: BashCommand) => {
43
47
  flat.push(commandPattern(cmd));
44
- for (const nested of cmd.nested) visit(nested);
48
+ for (const nested of cmd.nested) {
49
+ visit(nested);
50
+ }
45
51
  };
46
- for (const cmd of commands) visit(cmd);
52
+ for (const cmd of commands) {
53
+ visit(cmd);
54
+ }
47
55
  return flat;
48
56
  }
49
57
 
package/src/bwrap/core.ts CHANGED
@@ -226,7 +226,9 @@ export function loadBwrapConfig(cwd: string, paths = getBwrapConfigPaths(cwd)):
226
226
  // 去重:调用方用同一路径表达「只读这一个文件」时不做二次合并
227
227
  // (否则 extraWritablePaths / approvalRules 会被重复拼接)
228
228
  for (const path of new Set([paths.global, paths.project])) {
229
- if (!existsSync(path)) continue;
229
+ if (!existsSync(path)) {
230
+ continue;
231
+ }
230
232
  config = deepMerge(config, parseBwrapConfigFile(path));
231
233
  }
232
234
  return Value.Parse(bwrapConfigSchema, config);
@@ -241,14 +243,18 @@ function findDefaultBwrap(): string {
241
243
  const pathEnv = process.env.PATH ?? "";
242
244
  for (const directory of pathEnv.split(delimiter)) {
243
245
  const candidate = join(directory, "bwrap");
244
- if (existsSync(candidate)) return candidate;
246
+ if (existsSync(candidate)) {
247
+ return candidate;
248
+ }
245
249
  }
246
250
  for (const candidate of [
247
251
  "/usr/bin/bwrap",
248
252
  "/usr/local/bin/bwrap",
249
253
  "/run/current-system/sw/bin/bwrap",
250
254
  ]) {
251
- if (existsSync(candidate)) return candidate;
255
+ if (existsSync(candidate)) {
256
+ return candidate;
257
+ }
252
258
  }
253
259
  throw new Error(
254
260
  "bwrap (bubblewrap) not found in PATH. Install it:\n" +
@@ -272,7 +278,9 @@ function findCommandInPath(name: string, hint: string): string {
272
278
  const pathEnv = process.env.PATH ?? "";
273
279
  for (const directory of pathEnv.split(delimiter)) {
274
280
  const candidate = join(directory, name);
275
- if (existsSync(candidate)) return candidate;
281
+ if (existsSync(candidate)) {
282
+ return candidate;
283
+ }
276
284
  }
277
285
  throw new Error(hint);
278
286
  }
@@ -335,7 +343,9 @@ class ScanLimitError extends Error {}
335
343
  export async function findGitDirs(root: string): Promise<string[]> {
336
344
  const found: string[] = [];
337
345
  const walk = async (dir: string, depth: number): Promise<void> => {
338
- if (depth > MAX_GIT_SCAN_DEPTH) return;
346
+ if (depth > MAX_GIT_SCAN_DEPTH) {
347
+ return;
348
+ }
339
349
  let entries: Dirent[];
340
350
  try {
341
351
  entries = await readdir(dir, { withFileTypes: true });
@@ -343,19 +353,25 @@ export async function findGitDirs(root: string): Promise<string[]> {
343
353
  return;
344
354
  }
345
355
  for (const entry of entries) {
346
- if (found.length >= MAX_GIT_DIRS) throw new ScanLimitError();
356
+ if (found.length >= MAX_GIT_DIRS) {
357
+ throw new ScanLimitError();
358
+ }
347
359
  if (entry.name === ".git" && entry.isDirectory()) {
348
360
  found.push(join(dir, ".git"));
349
361
  continue;
350
362
  }
351
- if (!entry.isDirectory() || GIT_DIR_SCAN_SKIP.has(entry.name)) continue;
363
+ if (!entry.isDirectory() || GIT_DIR_SCAN_SKIP.has(entry.name)) {
364
+ continue;
365
+ }
352
366
  await walk(join(dir, entry.name), depth + 1);
353
367
  }
354
368
  };
355
369
  try {
356
370
  await walk(root, 0);
357
371
  } catch (error) {
358
- if (!(error instanceof ScanLimitError)) throw error;
372
+ if (!(error instanceof ScanLimitError)) {
373
+ throw error;
374
+ }
359
375
  }
360
376
  return found;
361
377
  }
@@ -403,7 +419,9 @@ export async function buildBwrapArgs(resolved: ResolvedBwrap, cwd: string): Prom
403
419
  args.push("--ro-bind-try", "/dev/null", target);
404
420
  }
405
421
  }
406
- if (resolved.network === "block") args.push("--unshare-net");
422
+ if (resolved.network === "block") {
423
+ args.push("--unshare-net");
424
+ }
407
425
  // fs allow-all:整棵根可写,不做保护绑定(.pi/.agent/.git 的只读覆盖与「完整可写」矛盾)。
408
426
  if (resolved.fs !== "allow-all") {
409
427
  // --ro-bind-try:目录不存在(或已被删除)时自动忽略
@@ -432,7 +450,9 @@ export async function buildBwrapArgs(resolved: ResolvedBwrap, cwd: string): Prom
432
450
  }
433
451
 
434
452
  function killChild(child: ChildProcess): void {
435
- if (!child.pid) return;
453
+ if (!child.pid) {
454
+ return;
455
+ }
436
456
  try {
437
457
  process.kill(-child.pid, "SIGKILL");
438
458
  } catch {
@@ -450,7 +470,9 @@ export async function createNetworkStack(
450
470
  resolved: ResolvedBwrap,
451
471
  log?: NetworkStackLog,
452
472
  ): Promise<NetworkStack | undefined> {
453
- if (resolved.network !== "limited") return undefined;
473
+ if (resolved.network !== "limited") {
474
+ return undefined;
475
+ }
454
476
  return startNetworkStack({
455
477
  allowlist: resolved.networkAllowlist,
456
478
  dnsServers: await resolveDnsServers(),
@@ -584,7 +606,9 @@ export function createBwrapBashOperations(
584
606
  signal?.addEventListener("abort", onAbort, { once: true });
585
607
  child.once("error", reject);
586
608
  child.once("close", (exitCode) => {
587
- if (timeoutHandle) clearTimeout(timeoutHandle);
609
+ if (timeoutHandle) {
610
+ clearTimeout(timeoutHandle);
611
+ }
588
612
  signal?.removeEventListener("abort", onAbort);
589
613
  // 中断:reject signal.reason(默认是 name=AbortError 的 DOMException)
590
614
  if (signal?.aborted) {
@@ -596,7 +620,9 @@ export function createBwrapBashOperations(
596
620
  } else if (timedOut) {
597
621
  // 超时:name=TimeoutError(对齐标准错误分类)
598
622
  reject(new TimeoutError(timeout));
599
- } else resolve({ exitCode });
623
+ } else {
624
+ resolve({ exitCode });
625
+ }
600
626
  });
601
627
  });
602
628
  },
@@ -50,7 +50,9 @@ export async function dcgSuggestion(command: string): Promise<DcgScanOutcome> {
50
50
  try {
51
51
  stdout = await runDcgScan(command);
52
52
  } catch (error) {
53
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return { kind: "not-installed" };
53
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
54
+ return { kind: "not-installed" };
55
+ }
54
56
  return { kind: "failed", detail: error instanceof Error ? error.message : String(error) };
55
57
  }
56
58
 
@@ -102,11 +104,16 @@ function runDcgScan(command: string): Promise<string> {
102
104
  settle(new Error("dcg scan timed out"));
103
105
  }, DCG_SCAN_TIMEOUT_MS);
104
106
  const settle = (error?: Error) => {
105
- if (settled) return;
107
+ if (settled) {
108
+ return;
109
+ }
106
110
  settled = true;
107
111
  clearTimeout(timeoutId);
108
- if (error) reject(error);
109
- else resolve(stdout);
112
+ if (error) {
113
+ reject(error);
114
+ } else {
115
+ resolve(stdout);
116
+ }
110
117
  };
111
118
  proc.stdout.setEncoding("utf8");
112
119
  proc.stdout.on("data", (chunk: string) => {
@@ -119,8 +126,11 @@ function runDcgScan(command: string): Promise<string> {
119
126
  proc.on("close", (code) => {
120
127
  // dcg 的退出码是决策结果(deny 时非 0),不是失败标志:只要 stdout
121
128
  // 有内容就交给上层解析;真正出错时(参数错误等)stdout 为空。
122
- if (code === 0 || stdout.length > 0) settle();
123
- else settle(new Error(`dcg exited with code ${String(code)}`));
129
+ if (code === 0 || stdout.length > 0) {
130
+ settle();
131
+ } else {
132
+ settle(new Error(`dcg exited with code ${String(code)}`));
133
+ }
124
134
  });
125
135
  proc.stdin.end(command);
126
136
  });
@@ -34,7 +34,9 @@ async function waitFor(
34
34
  predicate: () => Promise<boolean>,
35
35
  ): Promise<boolean> {
36
36
  for (let i = 0; i < attempts; i++) {
37
- if (await predicate()) return true;
37
+ if (await predicate()) {
38
+ return true;
39
+ }
38
40
  await sleep(delayMs);
39
41
  }
40
42
  return false;
@@ -58,7 +58,9 @@ function isIp(host: string): boolean {
58
58
 
59
59
  /** 单 IP 补掩码为 CIDR(IPv4 /32、IPv6 /128),带掩码原样返回。 */
60
60
  function toCidr(host: string): string {
61
- if (host.includes("/")) return host;
61
+ if (host.includes("/")) {
62
+ return host;
63
+ }
62
64
  return host.includes(":") ? `${host}/128` : `${host}/32`;
63
65
  }
64
66
 
@@ -79,7 +81,9 @@ function parseAllowlistEntry(entry: string): AllowlistEntry {
79
81
  let port: number | undefined;
80
82
  if (entry.startsWith("[")) {
81
83
  const match = /^\[(.+)\](?::(\d+))?$/.exec(entry);
82
- if (match?.[1] === undefined) throw new Error(`Invalid allowlist entry "${entry}"`);
84
+ if (match?.[1] === undefined) {
85
+ throw new Error(`Invalid allowlist entry "${entry}"`);
86
+ }
83
87
  host = match[1];
84
88
  // 端口组 (?::(\d+))? 可选:无端口时 at(2) 是 undefined
85
89
  const portPart = match.at(2);
@@ -87,13 +91,19 @@ function parseAllowlistEntry(entry: string): AllowlistEntry {
87
91
  } else {
88
92
  const colon = entry.lastIndexOf(":");
89
93
  if (colon === -1) {
90
- if (entry.length === 0) throw new Error(`Invalid allowlist entry ""`);
94
+ if (entry.length === 0) {
95
+ throw new Error(`Invalid allowlist entry ""`);
96
+ }
91
97
  host = entry;
92
98
  } else {
93
99
  const portPart = entry.slice(colon + 1);
94
- if (!/^\d+$/.test(portPart)) throw new Error(`Invalid allowlist entry "${entry}"`);
100
+ if (!/^\d+$/.test(portPart)) {
101
+ throw new Error(`Invalid allowlist entry "${entry}"`);
102
+ }
95
103
  host = entry.slice(0, colon);
96
- if (host.length === 0) throw new Error(`Invalid allowlist entry "${entry}"`);
104
+ if (host.length === 0) {
105
+ throw new Error(`Invalid allowlist entry "${entry}"`);
106
+ }
97
107
  if (host.includes(":")) {
98
108
  throw new Error(`IPv6 addresses must be wrapped in brackets, e.g. "[${host}]:${portPart}"`);
99
109
  }
@@ -101,7 +111,9 @@ function parseAllowlistEntry(entry: string): AllowlistEntry {
101
111
  }
102
112
  }
103
113
  if (isIp(host)) {
104
- if (!IP_CHARS_PATTERN.test(host)) throw new Error(`Invalid allowlist entry "${entry}"`);
114
+ if (!IP_CHARS_PATTERN.test(host)) {
115
+ throw new Error(`Invalid allowlist entry "${entry}"`);
116
+ }
105
117
  } else if (!DOMAIN_PATTERN.test(host)) {
106
118
  throw new Error(`Invalid allowlist entry "${entry}"`);
107
119
  }