pi-ast-sgrep 1.4.0 → 2.0.0

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.
@@ -13,6 +13,8 @@ export type StickyWorkerOptions = {
13
13
  signal?: AbortSignal;
14
14
  /** Kill worker when one request exceeds this duration (ms). */
15
15
  timeoutMs?: number;
16
+ /** Maximum bytes accepted in one NDJSON response. */
17
+ maxOutputBytes?: number;
16
18
  };
17
19
  export declare function startStickyWorker(options: StickyWorkerOptions): Promise<StickyWorker>;
18
20
  /** One-shot batch via stdin (avoids tempfile). */
@@ -23,4 +25,5 @@ export declare function runBatchViaStdin(options: {
23
25
  env?: NodeJS.ProcessEnv;
24
26
  signal?: AbortSignal;
25
27
  timeoutMs?: number;
28
+ maxOutputBytes?: number;
26
29
  }): Promise<MachineEnvelope>;
@@ -5,8 +5,8 @@
5
5
  * Amdahl win over per-wave `codemode-batch` spawns.
6
6
  */
7
7
  import { spawn } from "node:child_process";
8
- import { createInterface } from "node:readline";
9
8
  import { asEnvelope } from "./dispatch.js";
9
+ const DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
10
10
  export async function startStickyWorker(options) {
11
11
  if (options.signal?.aborted) {
12
12
  throw new Error("codemode-serve aborted before start");
@@ -20,7 +20,9 @@ export async function startStickyWorker(options) {
20
20
  let nextId = 0;
21
21
  let closed = false;
22
22
  let stderr = "";
23
- const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
23
+ let stdout = Buffer.alloc(0);
24
+ const decoder = new TextDecoder("utf-8", { fatal: true });
25
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
24
26
  const failAll = (err) => {
25
27
  for (const p of pending.values())
26
28
  p.reject(err);
@@ -30,11 +32,13 @@ export async function startStickyWorker(options) {
30
32
  if (closed)
31
33
  return;
32
34
  closed = true;
35
+ options.signal?.removeEventListener("abort", onAbort);
33
36
  killChild(child);
34
37
  failAll(err);
35
- rl.close();
38
+ child.stdout.destroy();
36
39
  };
37
- rl.on("line", (line) => {
40
+ child.stdin.on("error", terminate);
41
+ const handleLine = (line) => {
38
42
  const trimmed = line.trim();
39
43
  if (!trimmed)
40
44
  return;
@@ -43,7 +47,7 @@ export async function startStickyWorker(options) {
43
47
  msg = JSON.parse(trimmed);
44
48
  }
45
49
  catch (cause) {
46
- failAll(new Error(`codemode-serve bad JSON: ${trimmed.slice(0, 200)}`));
50
+ terminate(new Error(`codemode-serve bad JSON: ${trimmed.slice(0, 200)}`));
47
51
  void cause;
48
52
  return;
49
53
  }
@@ -58,6 +62,44 @@ export async function startStickyWorker(options) {
58
62
  return;
59
63
  pending.delete(id);
60
64
  waiter.resolve(msg);
65
+ };
66
+ child.stdout.on("data", (chunk) => {
67
+ if (closed)
68
+ return;
69
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
70
+ let offset = 0;
71
+ let newline;
72
+ while ((newline = bytes.indexOf(0x0a, offset)) >= 0) {
73
+ const segment = bytes.subarray(offset, newline);
74
+ const lineBytes = stdout.length + segment.length + 1;
75
+ if (lineBytes > maxOutputBytes) {
76
+ terminate(new Error(`codemode-serve output exceeded ${maxOutputBytes} bytes`));
77
+ return;
78
+ }
79
+ const line = stdout.length === 0
80
+ ? segment
81
+ : Buffer.concat([stdout, segment], stdout.length + segment.length);
82
+ stdout = Buffer.alloc(0);
83
+ try {
84
+ handleLine(decoder.decode(line));
85
+ }
86
+ catch {
87
+ terminate(new Error("codemode-serve output is not valid UTF-8"));
88
+ return;
89
+ }
90
+ if (closed)
91
+ return;
92
+ offset = newline + 1;
93
+ }
94
+ const tail = bytes.subarray(offset);
95
+ if (stdout.length + tail.length > maxOutputBytes) {
96
+ terminate(new Error(`codemode-serve output exceeded ${maxOutputBytes} bytes`));
97
+ }
98
+ else if (tail.length > 0) {
99
+ stdout = stdout.length === 0
100
+ ? Buffer.from(tail)
101
+ : Buffer.concat([stdout, tail], stdout.length + tail.length);
102
+ }
61
103
  });
62
104
  child.stderr.on("data", (chunk) => {
63
105
  stderr += String(chunk);
@@ -65,11 +107,11 @@ export async function startStickyWorker(options) {
65
107
  stderr = stderr.slice(-8_192);
66
108
  });
67
109
  child.on("error", (err) => {
68
- closed = true;
69
- failAll(err);
110
+ terminate(err);
70
111
  });
71
112
  child.on("close", (code, signal) => {
72
113
  closed = true;
114
+ options.signal?.removeEventListener("abort", onAbort);
73
115
  if (pending.size > 0) {
74
116
  failAll(new Error(`codemode-serve exited code=${code ?? "null"} signal=${signal ?? "null"} stderr=${stderr.slice(0, 512)}`));
75
117
  }
@@ -84,12 +126,15 @@ export async function startStickyWorker(options) {
84
126
  payload.id = id;
85
127
  return new Promise((resolve, reject) => {
86
128
  pending.set(id, { resolve, reject });
87
- child.stdin.write(`${JSON.stringify(payload)}\n`, (err) => {
88
- if (err) {
89
- pending.delete(id);
90
- reject(err);
91
- }
92
- });
129
+ try {
130
+ child.stdin.write(`${JSON.stringify(payload)}\n`, (err) => {
131
+ if (err)
132
+ terminate(err);
133
+ });
134
+ }
135
+ catch (cause) {
136
+ terminate(cause instanceof Error ? cause : new Error(String(cause)));
137
+ }
93
138
  });
94
139
  };
95
140
  const writeWithControls = (payload, label, signal) => {
@@ -151,21 +196,9 @@ export async function startStickyWorker(options) {
151
196
  return out;
152
197
  },
153
198
  async end() {
154
- options.signal?.removeEventListener("abort", onAbort);
155
199
  if (closed)
156
200
  return;
157
- try {
158
- if (child.stdin.writable) {
159
- child.stdin.write(`${JSON.stringify({ type: "end" })}\n`);
160
- child.stdin.end();
161
- }
162
- }
163
- catch {
164
- // ignore
165
- }
166
- killChild(child);
167
- closed = true;
168
- rl.close();
201
+ terminate(new Error("codemode-serve ended"));
169
202
  },
170
203
  };
171
204
  }
@@ -181,48 +214,72 @@ export async function runBatchViaStdin(options) {
181
214
  });
182
215
  let stdout = "";
183
216
  let stderr = "";
217
+ let outputBytes = 0;
218
+ let settled = false;
219
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
220
+ const cleanup = () => {
221
+ if (timer)
222
+ clearTimeout(timer);
223
+ options.signal?.removeEventListener("abort", onAbort);
224
+ };
225
+ const finish = (action) => {
226
+ if (settled)
227
+ return;
228
+ settled = true;
229
+ cleanup();
230
+ action();
231
+ };
232
+ const fail = (error) => finish(() => {
233
+ killChild(child);
234
+ reject(error);
235
+ });
236
+ child.stdin.on("error", fail);
184
237
  const timer = options.timeoutMs && options.timeoutMs > 0
185
238
  ? setTimeout(() => {
186
- killChild(child);
187
- reject(new Error(`codemode-batch timed out after ${options.timeoutMs}ms`));
239
+ fail(new Error(`codemode-batch timed out after ${options.timeoutMs}ms`));
188
240
  }, options.timeoutMs)
189
241
  : undefined;
190
242
  const onAbort = () => {
191
- killChild(child);
192
- reject(new Error("codemode-batch aborted"));
243
+ fail(new Error("codemode-batch aborted"));
193
244
  };
194
245
  options.signal?.addEventListener("abort", onAbort, { once: true });
195
246
  child.stdout.on("data", (c) => {
247
+ outputBytes += Buffer.byteLength(c);
248
+ if (outputBytes > maxOutputBytes) {
249
+ fail(new Error(`codemode-batch output exceeded ${maxOutputBytes} bytes`));
250
+ return;
251
+ }
196
252
  stdout += String(c);
197
253
  });
198
254
  child.stderr.on("data", (c) => {
255
+ outputBytes += Buffer.byteLength(c);
256
+ if (outputBytes > maxOutputBytes) {
257
+ fail(new Error(`codemode-batch output exceeded ${maxOutputBytes} bytes`));
258
+ return;
259
+ }
199
260
  stderr += String(c);
200
261
  });
201
262
  child.on("error", (err) => {
202
- if (timer)
203
- clearTimeout(timer);
204
- options.signal?.removeEventListener("abort", onAbort);
205
- reject(err);
263
+ fail(err);
206
264
  });
207
265
  child.on("close", (code) => {
208
- if (timer)
209
- clearTimeout(timer);
210
- options.signal?.removeEventListener("abort", onAbort);
266
+ if (settled)
267
+ return;
211
268
  if (code !== 0) {
212
- reject(new Error(`codemode-batch exited ${code}: ${stderr.slice(0, 512) || stdout.slice(0, 512)}`));
269
+ fail(new Error(`codemode-batch exited ${code}: ${stderr.slice(0, 512) || stdout.slice(0, 512)}`));
213
270
  return;
214
271
  }
215
272
  try {
216
- resolve(JSON.parse(stdout));
273
+ const value = JSON.parse(stdout);
274
+ finish(() => resolve(value));
217
275
  }
218
276
  catch (cause) {
219
- reject(cause);
277
+ fail(cause instanceof Error ? cause : new Error(String(cause)));
220
278
  }
221
279
  });
222
280
  child.stdin.write(options.body, (err) => {
223
281
  if (err) {
224
- killChild(child);
225
- reject(err);
282
+ fail(err);
226
283
  return;
227
284
  }
228
285
  child.stdin.end();
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ type RuntimeLike = {
15
15
  }): NodeJS.ProcessEnv;
16
16
  config?: {
17
17
  timeoutMs?: number;
18
+ maxOutputBytes?: number;
18
19
  refreshIntervalMs?: number;
19
20
  };
20
21
  inspectIndexCompatibility?(context: {
@@ -23,8 +24,12 @@ type RuntimeLike = {
23
24
  rebuildIncompatibleIndex?(context: {
24
25
  cwd: string;
25
26
  }, options?: RunOptions): Promise<MachineEnvelope>;
27
+ resolveIndexPath?(root: string): string;
28
+ watchExternalChanges?: boolean;
29
+ };
30
+ type FreshnessLike = Pick<FreshnessCoordinator, "ensureFresh" | "markAffectedPath"> & {
31
+ shutdown?(): void;
26
32
  };
27
- type FreshnessLike = Pick<FreshnessCoordinator, "ensureFresh" | "markAffectedPath">;
28
33
  export declare function registerAstSgrepTools(pi: ExtensionAPI, runtime?: RuntimeLike, freshness?: FreshnessLike): void;
29
34
  export declare function registerAstSgrepCommands(pi: ExtensionAPI, runtime?: RuntimeLike): void;
30
35
  export default function astSgrepExtension(pi: ExtensionAPI): void;