@trim21/personal-pi-extensions 0.1.611 → 0.1.613

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 (75) hide show
  1. package/package.json +2 -2
  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 +5 -3
  6. package/src/bwrap/approval-rules.ts +51 -18
  7. package/src/bwrap/approval-suggest.ts +12 -4
  8. package/src/bwrap/core.ts +39 -13
  9. package/src/bwrap/dcg-scan.ts +16 -6
  10. package/src/bwrap/holder.ts +3 -1
  11. package/src/bwrap/mihomo-config.ts +18 -6
  12. package/src/bwrap/network-stack.ts +30 -10
  13. package/src/bwrap/runtime.ts +36 -14
  14. package/src/claude-code/common.ts +6 -2
  15. package/src/claude-code/edit-utils.ts +3 -1
  16. package/src/claude-code/files.ts +40 -14
  17. package/src/claude-code/glob.ts +12 -4
  18. package/src/claude-code/grep.ts +41 -14
  19. package/src/claude-code/session-tools.ts +18 -6
  20. package/src/claude-code/shell.ts +9 -3
  21. package/src/gh/base.ts +72 -24
  22. package/src/gh/tools/download-release-assets.ts +12 -4
  23. package/src/gh/tools/list-releases.ts +3 -1
  24. package/src/gh/tools/list-workflow-runs.ts +9 -3
  25. package/src/gh/tools/read-ci-logs.ts +21 -7
  26. package/src/gh/tools/read-repo.ts +3 -1
  27. package/src/lib/cli-args.ts +6 -2
  28. package/src/lib/cli.ts +55 -22
  29. package/src/lib/file-reads.ts +24 -8
  30. package/src/lib/github.ts +35 -12
  31. package/src/lib/lsp/adapter.ts +6 -2
  32. package/src/lib/lsp/bin.ts +12 -4
  33. package/src/lib/lsp/client.ts +205 -69
  34. package/src/lib/lsp/diagnostic.ts +6 -2
  35. package/src/lib/lsp/inspect-tool.ts +25 -9
  36. package/src/lib/lsp/lsp.ts +154 -58
  37. package/src/lib/lsp/rename-tool.ts +11 -4
  38. package/src/lib/lsp/rename.ts +25 -8
  39. package/src/lib/lsp/server-config.ts +41 -14
  40. package/src/lib/lsp/watcher.ts +51 -17
  41. package/src/lib/path.ts +18 -6
  42. package/src/lib/proc.ts +3 -1
  43. package/src/lib/proxy.ts +9 -3
  44. package/src/lib/request-policy.ts +3 -1
  45. package/src/lib/seq-state.ts +3 -1
  46. package/src/lib/ui.ts +23 -8
  47. package/src/lib/write-guard.ts +21 -7
  48. package/src/openai-cost/config.ts +3 -1
  49. package/src/openai-cost/cost.ts +54 -18
  50. package/src/openai-cost/index.ts +3 -1
  51. package/src/openai-cost/provider.ts +6 -2
  52. package/src/opencode/bash.ts +9 -3
  53. package/src/opencode/edit-engine.ts +63 -41
  54. package/src/opencode/files.ts +30 -10
  55. package/src/opencode/glob.ts +3 -1
  56. package/src/opencode/grep.ts +12 -4
  57. package/src/opencode/question.ts +3 -1
  58. package/src/opencode/ripgrep.ts +23 -9
  59. package/src/opencode/todo.ts +3 -1
  60. package/src/session-name.ts +40 -16
  61. package/src/spawn-agent-agents.ts +18 -6
  62. package/src/spawn-agent.ts +58 -21
  63. package/src/system-prompt/index.ts +9 -3
  64. package/src/talk/core.ts +143 -48
  65. package/src/talk/format.ts +12 -4
  66. package/src/talk/group.ts +9 -3
  67. package/src/talk/index.ts +30 -10
  68. package/src/talk/mailbox.ts +30 -10
  69. package/src/talk/policy.ts +10 -5
  70. package/src/talk/registry.ts +35 -12
  71. package/src/talk/storage.ts +3 -1
  72. package/src/vision-agent.ts +41 -14
  73. package/src/web/config.ts +3 -1
  74. package/src/web/fetch.ts +75 -25
  75. 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.611",
3
+ "version": "0.1.613",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -52,7 +52,7 @@
52
52
  "eslint-plugin-import-x": "^4.17.1",
53
53
  "eslint-plugin-promise": "7.3.0",
54
54
  "eslint-plugin-simple-import-sort": "14.0.0",
55
- "eslint-plugin-unicorn": "74.0.0",
55
+ "eslint-plugin-unicorn": "76.0.0",
56
56
  "eslint-plugin-unused-imports": "4.4.1",
57
57
  "husky": "^9.1.7",
58
58
  "jiti": "^2.7.0",
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
@@ -152,7 +152,7 @@ export function registerOutlineTool(pi: ExtensionAPI, ctx: AftToolContext): void
152
152
  }
153
153
  const rawArgs = compactArgs({
154
154
  target: filesMode ? target : resolved,
155
- files: filesMode ? true : undefined,
155
+ files: filesMode || undefined,
156
156
  includeTests: params.includeTests,
157
157
  });
158
158
 
@@ -232,7 +232,7 @@ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
232
232
  1,
233
233
  Number.MAX_SAFE_INTEGER,
234
234
  ),
235
- callgraph: coerceBoolean(params.callgraph) ? true : undefined,
235
+ callgraph: coerceBoolean(params.callgraph) || undefined,
236
236
  });
237
237
 
238
238
  const subtitle = buildZoomSubtitle(extCtx.cwd, params);
@@ -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
 
@@ -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,33 +119,48 @@ 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,`===` 比较失效会无限递归。
141
153
  const nested: BashCommand[] = [];
142
154
  for (const descendant of all) {
143
- if (descendant === node) continue;
144
- if (descendant.startIndex >= node.startIndex && descendant.endIndex <= node.endIndex) {
145
- const inner = collectCommand(descendant, all);
146
- if (inner) nested.push(inner);
155
+ if (descendant === node) {
156
+ continue;
157
+ }
158
+ if (descendant.startIndex < node.startIndex || descendant.endIndex > node.endIndex) {
159
+ continue;
160
+ }
161
+ const inner = collectCommand(descendant, all);
162
+ if (inner) {
163
+ nested.push(inner);
147
164
  }
148
165
  }
149
166
  return { name, args, raw: node.text, nested };
@@ -161,7 +178,9 @@ export async function parseBashCommands(command: string): Promise<ParsedBash> {
161
178
  const commands: BashCommand[] = [];
162
179
  for (const node of all) {
163
180
  const parsed = collectCommand(node, all);
164
- if (parsed) commands.push(parsed);
181
+ if (parsed) {
182
+ commands.push(parsed);
183
+ }
165
184
  }
166
185
  return { commands, hasFileOutputRedirect: treeHasFileOutputRedirect(tree.rootNode) };
167
186
  } catch (error) {
@@ -184,7 +203,9 @@ export function matchRule(input: string, pattern: string): boolean {
184
203
  .replaceAll(/[.+^${}()|[\]\\]/g, String.raw`\$&`)
185
204
  .replaceAll("*", ".*")
186
205
  .replaceAll("?", ".");
187
- if (escaped.endsWith(" .*")) escaped = escaped.slice(0, -3) + "( .*)?";
206
+ if (escaped.endsWith(" .*")) {
207
+ escaped = escaped.slice(0, -3) + "( .*)?";
208
+ }
188
209
  return new RegExp(`^${escaped}$`, "s").test(input);
189
210
  }
190
211
 
@@ -209,16 +230,28 @@ export async function evaluateBashApproval(
209
230
  const raws: string[] = [];
210
231
  const visit = (cmd: BashCommand) => {
211
232
  raws.push(cmd.raw);
212
- for (const nested of cmd.nested) visit(nested);
233
+ for (const nested of cmd.nested) {
234
+ visit(nested);
235
+ }
213
236
  };
214
- for (const cmd of parsed.commands) visit(cmd);
215
- if (raws.length === 0) return;
237
+ for (const cmd of parsed.commands) {
238
+ visit(cmd);
239
+ }
240
+ if (raws.length === 0) {
241
+ return;
242
+ }
216
243
  let allowed = 0;
217
244
  for (const raw of raws) {
218
245
  const rule = rules.findLast((r) => matchRule(raw, r.pattern));
219
- if (rule?.action === "deny") return "deny";
220
- 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;
221
255
  }
222
- if (parsed.hasFileOutputRedirect) return;
223
256
  return allowed === raws.length ? "allow" : undefined;
224
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
  }
@@ -84,7 +84,9 @@ const HOLDER_PATH = fileURLToPath(new URL("holder.js", import.meta.url));
84
84
  const SBIN_PATH_SUFFIX = "/usr/local/sbin:/usr/sbin:/sbin";
85
85
 
86
86
  function killProcess(pid: number | undefined, signal: NodeJS.Signals = "SIGTERM"): void {
87
- if (!pid) return;
87
+ if (!pid) {
88
+ return;
89
+ }
88
90
  try {
89
91
  process.kill(pid, signal);
90
92
  } catch {
@@ -136,7 +138,9 @@ async function waitForNewUserns(pid: number, timeoutMs = 5000): Promise<void> {
136
138
  const deadline = Date.now() + timeoutMs;
137
139
  while (Date.now() < deadline) {
138
140
  try {
139
- if ((await readlink(`/proc/${pid}/ns/user`)) !== self) return;
141
+ if ((await readlink(`/proc/${pid}/ns/user`)) !== self) {
142
+ return;
143
+ }
140
144
  } catch {
141
145
  // /proc/<pid> 尚未就绪,重试
142
146
  }
@@ -153,12 +157,16 @@ function waitForMihomoStarted(holder: ChildProcess, timeoutMs = 20000): Promise<
153
157
  return new Promise((resolve, reject) => {
154
158
  let settled = false;
155
159
  const timer = setTimeout(() => {
156
- if (settled) return;
160
+ if (settled) {
161
+ return;
162
+ }
157
163
  settled = true;
158
164
  reject(new Error("Timed out waiting for mihomo to start"));
159
165
  }, timeoutMs);
160
166
  const onReady = (): void => {
161
- if (settled) return;
167
+ if (settled) {
168
+ return;
169
+ }
162
170
  settled = true;
163
171
  clearTimeout(timer);
164
172
  resolve();
@@ -168,7 +176,9 @@ function waitForMihomoStarted(holder: ChildProcess, timeoutMs = 20000): Promise<
168
176
  if (stream) {
169
177
  // 不提前停止:holder 生命周期内持续消费输出,避免流无消费者触发背压
170
178
  void forEachLine(stream, (line) => {
171
- if (line.includes("Tun adapter listening")) onReady();
179
+ if (line.includes("Tun adapter listening")) {
180
+ onReady();
181
+ }
172
182
  }).catch(() => {
173
183
  // 就绪判定由超时与 holder exit 兜底,流的 error 忽略
174
184
  });
@@ -178,7 +188,9 @@ function waitForMihomoStarted(holder: ChildProcess, timeoutMs = 20000): Promise<
178
188
  }
179
189
 
180
190
  function killChild(pid: number | undefined): void {
181
- if (!pid) return;
191
+ if (!pid) {
192
+ return;
193
+ }
182
194
  try {
183
195
  process.kill(-pid, "SIGKILL");
184
196
  } catch {
@@ -404,14 +416,20 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
404
416
  execOptions.signal?.addEventListener("abort", onAbort, { once: true });
405
417
 
406
418
  child.once("error", (error) => {
407
- if (settled) return;
419
+ if (settled) {
420
+ return;
421
+ }
408
422
  settled = true;
409
423
  reject(error);
410
424
  });
411
425
  child.once("close", (exitCode) => {
412
- if (settled) return;
426
+ if (settled) {
427
+ return;
428
+ }
413
429
  settled = true;
414
- if (timeoutHandle) clearTimeout(timeoutHandle);
430
+ if (timeoutHandle) {
431
+ clearTimeout(timeoutHandle);
432
+ }
415
433
  execOptions.signal?.removeEventListener("abort", onAbort);
416
434
  // 中断:reject signal.reason(默认是 name=AbortError 的 DOMException)
417
435
  if (execOptions.signal?.aborted) {
@@ -423,7 +441,9 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
423
441
  } else if (timedOut) {
424
442
  // 超时:name=TimeoutError(对齐标准错误分类)
425
443
  reject(new TimeoutError(execOptions.timeout));
426
- } else resolve({ exitCode });
444
+ } else {
445
+ resolve({ exitCode });
446
+ }
427
447
  });
428
448
  });
429
449
  },