@trim21/personal-pi-extensions 0.1.572 → 0.1.574

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.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * ripgrep 运行器,供 opencode 风格 grep / glob 复用。
3
+ *
4
+ * 对齐上游 packages/core/src/ripgrep.ts 的 run():以 `rg` 子进程流式读取
5
+ * stdout,按解析结果计数,读满 limit + 1 条即杀掉进程提前结束(宽泛 pattern
6
+ * 不会把整个结果集读进内存)。退出码语义与上游一致:0 成功、1 无匹配、2 部分
7
+ * 文件读失败(仍返回已收集的结果),正则语法错误单独报错。
8
+ */
9
+
10
+ import { spawn } from "node:child_process";
11
+
12
+ /** 单次搜索最多返回的结果数(上游 limit = 100)。 */
13
+ export const RIPGREP_RESULT_LIMIT = 100;
14
+
15
+ /** stderr 最多保留的字节数(上游 ERROR_BYTES)。 */
16
+ const ERROR_BYTES = 8 * 1024;
17
+
18
+ export interface RipgrepRunOptions<R> {
19
+ cwd: string;
20
+ signal?: AbortSignal;
21
+ /** 最多返回的解析结果数;读满 limit + 1 条即终止 rg。 */
22
+ limit: number;
23
+ /** 解析一行 stdout;返回 undefined 表示该行不是结果(如 rg 的 begin/end/summary 记录)。 */
24
+ parse: (line: string) => R | undefined;
25
+ }
26
+
27
+ export interface RipgrepRunResult<R> {
28
+ items: R[];
29
+ /** 结果数超过 limit,已提前终止 rg(可能还有更多结果)。 */
30
+ truncated: boolean;
31
+ }
32
+
33
+ const isInvalidPattern = (stderr: string) =>
34
+ stderr.includes("regex parse error") || stderr.includes("error parsing regex");
35
+
36
+ /**
37
+ * 跑一次 rg 并返回解析后的结果。
38
+ *
39
+ * 路径类参数用 `--` 与 pattern 分隔,pattern 不会被当成选项;调用方负责给出
40
+ * `--no-config`、`--glob` 等选项。
41
+ */
42
+ export function runRipgrep<R>(
43
+ args: string[],
44
+ options: RipgrepRunOptions<R>,
45
+ ): Promise<RipgrepRunResult<R>> {
46
+ return new Promise((resolve, reject) => {
47
+ let child;
48
+ try {
49
+ child = spawn("rg", args, { cwd: options.cwd, stdio: ["ignore", "pipe", "pipe"] });
50
+ } catch (error) {
51
+ reject(error instanceof Error ? error : new Error(String(error)));
52
+ return;
53
+ }
54
+
55
+ const { signal } = options;
56
+ // 事件回调之间共享的状态放进对象:闭包里的 let 会被 TS 控制流判定为「恒假」,
57
+ // 触发 eslint 的 no-unnecessary-condition 误报
58
+ const state = { done: false, settled: false, truncated: false };
59
+ let stderr = "";
60
+ let parseError: Error | undefined;
61
+ let pending = "";
62
+ const items: R[] = [];
63
+
64
+ const stop = () => {
65
+ if (state.done) return;
66
+ state.done = true;
67
+ child.kill("SIGTERM");
68
+ };
69
+
70
+ /** 收尾(resolve/reject 只会发生一次,并摘掉 abort 监听)。 */
71
+ const settle = (finish: () => void) => {
72
+ if (state.settled) return;
73
+ state.settled = true;
74
+ signal?.removeEventListener("abort", onAbort);
75
+ finish();
76
+ };
77
+
78
+ const parseLine = (line: string) => {
79
+ if (state.done || line.length === 0) return;
80
+ let item: R | undefined;
81
+ try {
82
+ item = options.parse(line);
83
+ } catch (error) {
84
+ // 解析失败来自事件回调,必须转成 reject,否则会变成未捕获异常
85
+ parseError = error instanceof Error ? error : new Error(String(error));
86
+ stop();
87
+ return;
88
+ }
89
+ if (item === undefined) return;
90
+ items.push(item);
91
+ if (items.length > options.limit) {
92
+ state.truncated = true;
93
+ stop();
94
+ }
95
+ };
96
+
97
+ child.stderr.setEncoding("utf8");
98
+ child.stderr.on("data", (chunk: string) => {
99
+ if (stderr.length < ERROR_BYTES) stderr += chunk;
100
+ });
101
+
102
+ child.stdout.setEncoding("utf8");
103
+ child.stdout.on("data", (chunk: string) => {
104
+ pending += chunk;
105
+ const lines = pending.split("\n");
106
+ // 末尾没有换行的部分是不完整记录,留到下一块
107
+ pending = lines.pop() ?? "";
108
+ for (const line of lines) parseLine(line);
109
+ });
110
+
111
+ child.on("error", (error) => {
112
+ // spawn 失败(rg 不在 PATH 等)时 close 不保证触发,必须在这里收尾,
113
+ // 否则 promise 永远不落定,工具调用会挂住
114
+ settle(() => {
115
+ reject(
116
+ (error as NodeJS.ErrnoException).code === "ENOENT"
117
+ ? new Error("ripgrep (rg) was not found on PATH; install ripgrep to use this tool.", {
118
+ cause: error,
119
+ })
120
+ : error,
121
+ );
122
+ });
123
+ });
124
+
125
+ const onAbort = () => stop();
126
+ signal?.addEventListener("abort", onAbort, { once: true });
127
+
128
+ // pending 里剩下的内容按定义是不完整记录(rg 的记录一律以 \n 收尾,
129
+ // 被 kill 时可能停在半条),直接丢掉
130
+ child.on("close", (code) => {
131
+ settle(() => {
132
+ // 取消优先于结果与退出码:调用方靠 throwIfAborted 统一处理取消
133
+ if (signal?.aborted) {
134
+ reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
135
+ return;
136
+ }
137
+ if (parseError) {
138
+ reject(parseError);
139
+ return;
140
+ }
141
+ if (state.truncated) {
142
+ resolve({ items: items.slice(0, options.limit), truncated: true });
143
+ return;
144
+ }
145
+ const message = stderr.trim();
146
+ if (code === 2 && isInvalidPattern(message)) {
147
+ reject(new Error(`Invalid pattern: ${message}`));
148
+ return;
149
+ }
150
+ if (code !== 0 && code !== 1 && code !== 2) {
151
+ reject(new Error(message || `ripgrep exited with code ${code}`));
152
+ return;
153
+ }
154
+ // 退出码 1 = 无匹配;退出码 2 = 部分文件读失败,已有结果照常返回
155
+ resolve({ items: code === 1 ? [] : items, truncated: false });
156
+ });
157
+ });
158
+ });
159
+ }