@aliou/pi-processes 0.9.4 → 0.10.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.
Files changed (171) hide show
  1. package/CONTRIBUTING.md +40 -42
  2. package/README.md +54 -84
  3. package/extensions/process-tabs.ts +26 -0
  4. package/extensions/processes/client.ts +194 -0
  5. package/extensions/processes/commands/clear.ts +29 -0
  6. package/extensions/processes/commands/kill.ts +152 -0
  7. package/extensions/processes/commands/overview.ts +102 -0
  8. package/extensions/processes/components/overview-component.ts +729 -0
  9. package/extensions/processes/components/overview-panel.ts +177 -0
  10. package/extensions/processes/config/defaults.ts +28 -0
  11. package/extensions/processes/config/index.ts +13 -0
  12. package/extensions/processes/config/loader.ts +62 -0
  13. package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
  14. package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
  15. package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
  16. package/extensions/processes/config/migrations/index.ts +25 -0
  17. package/extensions/processes/config/schema.ts +9 -0
  18. package/extensions/processes/config/types.ts +72 -0
  19. package/extensions/processes/constants.ts +1 -0
  20. package/extensions/processes/handlers/commands.ts +113 -0
  21. package/extensions/processes/handlers/kill-process.ts +31 -0
  22. package/extensions/processes/handlers/notifications.ts +147 -0
  23. package/extensions/processes/handlers/requests.ts +114 -0
  24. package/extensions/processes/handlers/subscriptions.ts +144 -0
  25. package/extensions/processes/hooks/background-blocker.ts +201 -0
  26. package/extensions/processes/hooks/cleanup.ts +43 -0
  27. package/extensions/processes/hooks/event-bridge.ts +32 -0
  28. package/extensions/processes/i18n/index.ts +3 -0
  29. package/extensions/processes/i18n/messages.ts +27 -0
  30. package/extensions/processes/i18n/translator.ts +49 -0
  31. package/extensions/processes/index.ts +109 -0
  32. package/extensions/processes/message-renderer.ts +117 -0
  33. package/extensions/processes/notification-sender.ts +43 -0
  34. package/extensions/processes/notifications/classify.ts +27 -0
  35. package/extensions/processes/notifications/log-matchers.ts +146 -0
  36. package/extensions/processes/notifications/registry.ts +225 -0
  37. package/extensions/processes/notifications/render-content.ts +122 -0
  38. package/extensions/processes/notifications/service.ts +336 -0
  39. package/extensions/processes/notifications/types.ts +11 -0
  40. package/extensions/processes/settings/apply-setting-change.ts +30 -0
  41. package/extensions/processes/settings/build-sections.ts +420 -0
  42. package/extensions/processes/settings/index.ts +42 -0
  43. package/extensions/processes/tools/clear/index.ts +21 -0
  44. package/extensions/processes/tools/clear/render.ts +40 -0
  45. package/extensions/processes/tools/components/index.ts +4 -0
  46. package/extensions/processes/tools/components/process-action-header.ts +31 -0
  47. package/extensions/processes/tools/components/process-action-title.ts +17 -0
  48. package/extensions/processes/tools/components/tool-layout.ts +70 -0
  49. package/extensions/processes/tools/components/watch.ts +53 -0
  50. package/extensions/processes/tools/index.ts +265 -0
  51. package/extensions/processes/tools/list/index.ts +157 -0
  52. package/extensions/processes/tools/list/render.ts +211 -0
  53. package/extensions/processes/tools/notify.ts +191 -0
  54. package/extensions/processes/tools/output/index.ts +307 -0
  55. package/extensions/processes/tools/output/render.ts +229 -0
  56. package/extensions/processes/tools/schema.ts +263 -0
  57. package/extensions/processes/tools/start/index.ts +64 -0
  58. package/extensions/processes/tools/start/render.ts +67 -0
  59. package/extensions/processes/tools/stop/index.ts +35 -0
  60. package/extensions/processes/tools/stop/render.ts +43 -0
  61. package/extensions/processes/tools/update/index.ts +320 -0
  62. package/extensions/processes/tools/update/render.ts +251 -0
  63. package/extensions/processes/tools/utils.ts +112 -0
  64. package/extensions/processes/tools/watch-format.ts +14 -0
  65. package/extensions/processes/tools/write/index.ts +75 -0
  66. package/extensions/processes/tools/write/render.ts +68 -0
  67. package/extensions/processes/utils/truncate.ts +285 -0
  68. package/extensions/processes-dock/client.ts +60 -0
  69. package/extensions/processes-dock/commands/dock.ts +70 -0
  70. package/extensions/processes-dock/commands/pin.ts +166 -0
  71. package/extensions/processes-dock/components/log-dock-component.ts +375 -0
  72. package/extensions/processes-dock/dock-state.ts +33 -0
  73. package/extensions/processes-dock/index.ts +28 -0
  74. package/extensions/processes-dock/logs-client.ts +5 -0
  75. package/extensions/processes-dock/widget/setup.ts +520 -0
  76. package/extensions/processes-dock/widget/status.ts +149 -0
  77. package/extensions/processes-dock/widget/types.ts +16 -0
  78. package/extensions/processes-logs/client.ts +51 -0
  79. package/extensions/processes-logs/commands/logs.ts +108 -0
  80. package/extensions/processes-logs/completions.ts +31 -0
  81. package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
  82. package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
  83. package/extensions/processes-logs/index.ts +27 -0
  84. package/extensions/processes-logs/logs-client.ts +101 -0
  85. package/extensions/shared/line-buffer.ts +35 -0
  86. package/extensions/shared/output-payload.ts +28 -0
  87. package/extensions/shared/ui.ts +146 -0
  88. package/package.json +47 -20
  89. package/schema.json +134 -0
  90. package/skills/pi-processes/SKILL.md +496 -25
  91. package/src/get-manager.ts +15 -0
  92. package/src/manager/index.ts +182 -0
  93. package/src/manager/internal-types.ts +130 -0
  94. package/src/manager/limits.ts +15 -0
  95. package/src/manager/process-log-store.ts +422 -0
  96. package/src/manager/process-output.ts +345 -0
  97. package/src/manager/process-registry.ts +78 -0
  98. package/src/manager/process-runtime-controller.ts +499 -0
  99. package/src/protocol/broadcasts.ts +14 -0
  100. package/src/protocol/channels.ts +30 -0
  101. package/src/protocol/commands.ts +24 -0
  102. package/src/protocol/index.ts +34 -0
  103. package/src/protocol/logs.ts +23 -0
  104. package/src/protocol/notifications.ts +48 -0
  105. package/src/protocol/requests.ts +76 -0
  106. package/src/types.ts +66 -0
  107. package/src/utils/ansi.ts +2 -4
  108. package/src/utils/format.ts +24 -32
  109. package/src/utils/index.ts +5 -1
  110. package/src/utils/is-record.ts +3 -0
  111. package/src/utils/match-line.ts +32 -0
  112. package/src/utils/platform.ts +13 -0
  113. package/src/utils/shorten-path.ts +11 -0
  114. package/src/utils/signals.ts +22 -0
  115. package/src/commands/clear/command.ts +0 -14
  116. package/src/commands/clear/index.ts +0 -1
  117. package/src/commands/completions.ts +0 -38
  118. package/src/commands/dock/command.ts +0 -29
  119. package/src/commands/dock/index.ts +0 -1
  120. package/src/commands/index.ts +0 -33
  121. package/src/commands/kill/command.ts +0 -69
  122. package/src/commands/kill/index.ts +0 -1
  123. package/src/commands/logs/command.ts +0 -47
  124. package/src/commands/logs/index.ts +0 -1
  125. package/src/commands/pick-process.ts +0 -34
  126. package/src/commands/pin/command.ts +0 -34
  127. package/src/commands/pin/index.ts +0 -1
  128. package/src/commands/processes/command.ts +0 -39
  129. package/src/commands/processes/index.ts +0 -1
  130. package/src/commands/settings/apply-setting-change.ts +0 -72
  131. package/src/commands/settings/build-sections.ts +0 -160
  132. package/src/commands/settings/command.ts +0 -20
  133. package/src/commands/settings/index.ts +0 -1
  134. package/src/components/log-dock-component.test.ts +0 -29
  135. package/src/components/log-dock-component.ts +0 -236
  136. package/src/components/log-file-viewer.ts +0 -317
  137. package/src/components/log-overlay-component.ts +0 -546
  138. package/src/components/panel-helpers.ts +0 -37
  139. package/src/components/process-picker-component.ts +0 -166
  140. package/src/components/processes-component.ts +0 -485
  141. package/src/components/status-format.ts +0 -38
  142. package/src/config.ts +0 -107
  143. package/src/constants/index.ts +0 -16
  144. package/src/constants/types.ts +0 -106
  145. package/src/hooks/background-blocker.ts +0 -66
  146. package/src/hooks/cleanup.ts +0 -10
  147. package/src/hooks/index.ts +0 -32
  148. package/src/hooks/message-renderer.ts +0 -115
  149. package/src/hooks/process-end.ts +0 -69
  150. package/src/hooks/process-watch.ts +0 -85
  151. package/src/hooks/utils.ts +0 -23
  152. package/src/hooks/widget/index.ts +0 -2
  153. package/src/hooks/widget/setup.ts +0 -167
  154. package/src/hooks/widget/status-widget.ts +0 -105
  155. package/src/hooks/widget/types.ts +0 -20
  156. package/src/index.ts +0 -35
  157. package/src/manager.test.ts +0 -331
  158. package/src/manager.ts +0 -758
  159. package/src/tools/actions/clear.ts +0 -20
  160. package/src/tools/actions/debug.ts +0 -148
  161. package/src/tools/actions/index.ts +0 -171
  162. package/src/tools/actions/kill.ts +0 -89
  163. package/src/tools/actions/list.ts +0 -188
  164. package/src/tools/actions/logs.ts +0 -117
  165. package/src/tools/actions/output.ts +0 -269
  166. package/src/tools/actions/start.ts +0 -245
  167. package/src/tools/actions/write.ts +0 -113
  168. package/src/tools/index.ts +0 -207
  169. package/src/utils/ansi.test.ts +0 -22
  170. package/src/utils/command-executor.test.ts +0 -48
  171. package/src/utils/keybindings.ts +0 -71
@@ -0,0 +1,422 @@
1
+ import {
2
+ appendFileSync,
3
+ closeSync,
4
+ fstatSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ openSync,
8
+ readFileSync,
9
+ readSync,
10
+ rmSync,
11
+ statSync,
12
+ truncateSync,
13
+ } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ import type { ProcessLogPaths } from "./internal-types";
18
+ import { MAX_LOG_FILE_BYTES, MAX_TAIL_READ_BYTES } from "./limits";
19
+
20
+ const TAIL_CHUNK_BYTES = 64 * 1024;
21
+ const MAX_FULL_FILE_BYTES = MAX_TAIL_READ_BYTES * 8;
22
+ const TAIL_TRUNCATION_PREFIX = "[… truncated] ";
23
+ const STDOUT_TRUNCATION_MARKER =
24
+ "[log truncated at 64 MB — earlier output discarded]\n";
25
+ const STDERR_TRUNCATION_MARKER =
26
+ "[log truncated at 64 MB — earlier output discarded]\n";
27
+ const COMBINED_TRUNCATION_MARKER =
28
+ "2:[log truncated at 64 MB — earlier output discarded]\n";
29
+
30
+ /**
31
+ * Stores process log files on disk and enforces a per-file size cap.
32
+ *
33
+ * The log directory is created lazily: constructing a store without an
34
+ * explicit `logDir` does not touch the filesystem, so a session that never
35
+ * starts a process leaves no temp directory behind. An explicit `logDir` is
36
+ * also no longer created eagerly at construction; it is created on first use.
37
+ *
38
+ * Each file holds a single window of history. When a file exceeds
39
+ * {@link MAX_LOG_FILE_BYTES} it is truncated to zero and writing restarts
40
+ * from a marker line, so `stdout.log`, `stderr.log`, and `combined.log` can
41
+ * hold different windows of history after a breach. `combined.log` is what
42
+ * the UI reads; the per-stream files are what the agent greps.
43
+ */
44
+ export class ProcessLogStore {
45
+ private logDir: string | null = null;
46
+ private readonly explicitLogDir: string | undefined;
47
+ private readonly maxFileBytes: number;
48
+ private readonly fileBytes = new Map<string, number>();
49
+
50
+ constructor(logDir?: string, options?: { maxFileBytes?: number }) {
51
+ this.explicitLogDir = logDir;
52
+ this.maxFileBytes = options?.maxFileBytes ?? MAX_LOG_FILE_BYTES;
53
+ }
54
+
55
+ private ensureLogDir(): string {
56
+ if (this.logDir) return this.logDir;
57
+ if (this.explicitLogDir) {
58
+ mkdirSync(this.explicitLogDir, { recursive: true });
59
+ this.logDir = this.explicitLogDir;
60
+ return this.logDir;
61
+ }
62
+ const tempParent = tmpdir();
63
+ mkdirSync(tempParent, { recursive: true });
64
+ this.logDir = mkdtempSync(join(tempParent, "pi-processes-"));
65
+ return this.logDir;
66
+ }
67
+
68
+ getLogDir(): string {
69
+ return this.ensureLogDir();
70
+ }
71
+
72
+ createLogs(processId: string): ProcessLogPaths {
73
+ const dir = this.ensureLogDir();
74
+ const stdoutFile = join(dir, `${processId}-stdout.log`);
75
+ const stderrFile = join(dir, `${processId}-stderr.log`);
76
+ const combinedFile = join(dir, `${processId}-combined.log`);
77
+
78
+ // `appendFileSync("", ...)` does not truncate, so a reused explicit logDir
79
+ // can hold pre-existing content. Seed the counters from the actual file
80
+ // sizes so the cap accounts for those bytes.
81
+ appendFileSync(stdoutFile, "");
82
+ appendFileSync(stderrFile, "");
83
+ appendFileSync(combinedFile, "");
84
+ this.fileBytes.set(stdoutFile, this.sizeOf(stdoutFile));
85
+ this.fileBytes.set(stderrFile, this.sizeOf(stderrFile));
86
+ this.fileBytes.set(combinedFile, this.sizeOf(combinedFile));
87
+
88
+ return { stdoutFile, stderrFile, combinedFile };
89
+ }
90
+
91
+ appendStdout(file: string, data: Buffer): void {
92
+ this.appendCapped(file, data, STDOUT_TRUNCATION_MARKER);
93
+ }
94
+
95
+ appendStderr(file: string, data: Buffer): void {
96
+ this.appendCapped(file, data, STDERR_TRUNCATION_MARKER);
97
+ }
98
+
99
+ appendCombinedLine(
100
+ file: string,
101
+ source: "stdout" | "stderr",
102
+ line: string,
103
+ ): void {
104
+ const tag = source === "stdout" ? "1" : "2";
105
+ const data = `${tag}:${line}\n`;
106
+ // The marker must carry a stream tag, or getCombinedOutput will parse it
107
+ // as untagged stdout. Use the stderr tag so it renders with the warning
108
+ // tone in the dock and overlay.
109
+ this.appendCapped(file, data, COMBINED_TRUNCATION_MARKER);
110
+ }
111
+
112
+ appendErrorLine(file: string, message: string): void {
113
+ this.appendCapped(file, `${message}\n`, STDERR_TRUNCATION_MARKER);
114
+ }
115
+
116
+ readTailLines(filePath: string, lines: number): string[] {
117
+ if (lines <= 0) return [];
118
+
119
+ let fd: number | undefined;
120
+ try {
121
+ fd = openSync(filePath, "r");
122
+ const size = fstatSync(fd).size;
123
+ if (size === 0) return [];
124
+
125
+ const buffer = Buffer.allocUnsafe(Math.min(size, MAX_TAIL_READ_BYTES));
126
+ let bufferStart = buffer.length;
127
+ let position = size;
128
+ let bytesRead = 0;
129
+ let newlineCount = 0;
130
+
131
+ while (
132
+ position > 0 &&
133
+ bytesRead < MAX_TAIL_READ_BYTES &&
134
+ newlineCount <= lines
135
+ ) {
136
+ const length = Math.min(
137
+ TAIL_CHUNK_BYTES,
138
+ position,
139
+ MAX_TAIL_READ_BYTES - bytesRead,
140
+ );
141
+ position -= length;
142
+ bufferStart -= length;
143
+ readExact(fd, buffer, bufferStart, length, position);
144
+ bytesRead += length;
145
+ newlineCount += countNewlines(
146
+ buffer.subarray(bufferStart, bufferStart + length),
147
+ );
148
+ }
149
+
150
+ let populated: Buffer<ArrayBufferLike> = buffer.subarray(bufferStart);
151
+ const startedMidFile = position > 0;
152
+ let partialFirstLine: Buffer | undefined;
153
+ if (startedMidFile) {
154
+ const firstNewline = populated.indexOf(0x0a);
155
+ if (firstNewline === -1) {
156
+ populated = alignUtf8Start(populated);
157
+ const suffix = decodeUtf8Bounded(populated, MAX_TAIL_READ_BYTES);
158
+ return [`${TAIL_TRUNCATION_PREFIX}${suffix}`];
159
+ }
160
+ // The read began in the middle of a logical line. Dropping through
161
+ // its newline also removes any partial UTF-8 code point at the start.
162
+ const partial = alignUtf8Start(populated.subarray(0, firstNewline));
163
+ if (partial.length > 0) {
164
+ partialFirstLine = partial;
165
+ }
166
+ populated = populated.subarray(firstNewline + 1);
167
+ }
168
+
169
+ const rawLines = tailLineBuffersNewestFirst(populated, lines);
170
+ if (partialFirstLine && rawLines.length < lines) {
171
+ rawLines.push(partialFirstLine);
172
+ }
173
+
174
+ const decodedNewestFirst: string[] = [];
175
+ let remainingBytes = MAX_TAIL_READ_BYTES;
176
+ for (
177
+ let index = 0;
178
+ index < rawLines.length && remainingBytes > 0;
179
+ index++
180
+ ) {
181
+ const rawLine = rawLines[index];
182
+ const decoded = decodeUtf8Bounded(rawLine, remainingBytes);
183
+ const marked =
184
+ partialFirstLine && rawLine === partialFirstLine
185
+ ? `${TAIL_TRUNCATION_PREFIX}${decoded}`
186
+ : decoded;
187
+ decodedNewestFirst.push(marked);
188
+ remainingBytes -= Buffer.byteLength(decoded);
189
+ }
190
+ return decodedNewestFirst.reverse();
191
+ } catch (_error) {
192
+ return [];
193
+ } finally {
194
+ if (fd !== undefined) closeSync(fd);
195
+ }
196
+ }
197
+
198
+ readFullFile(filePath: string): string {
199
+ try {
200
+ const size = statSync(filePath).size;
201
+ if (size > MAX_FULL_FILE_BYTES) {
202
+ const marker = `[… truncated, see ${filePath}]\n`;
203
+ const markerBytes = Buffer.byteLength(marker);
204
+ const suffix = this.readFileSuffix(
205
+ filePath,
206
+ Math.max(0, MAX_FULL_FILE_BYTES - markerBytes),
207
+ );
208
+ return marker + suffix;
209
+ }
210
+ return readFileSync(filePath, "utf-8");
211
+ } catch (_error) {
212
+ return "";
213
+ }
214
+ }
215
+
216
+ getCombinedOutput(
217
+ combinedFile: string,
218
+ tailLines: number,
219
+ ): Array<{ type: "stdout" | "stderr"; text: string }> {
220
+ const rawLines = this.readTailLines(combinedFile, tailLines);
221
+ return rawLines.map((line) => {
222
+ if (line.startsWith("2:")) {
223
+ return { type: "stderr" as const, text: line.slice(2) };
224
+ }
225
+ return {
226
+ type: "stdout" as const,
227
+ text: line.startsWith("1:") ? line.slice(2) : line,
228
+ };
229
+ });
230
+ }
231
+
232
+ getFileSize(paths: ProcessLogPaths): { stdout: number; stderr: number } {
233
+ try {
234
+ return {
235
+ stdout: statSync(paths.stdoutFile).size,
236
+ stderr: statSync(paths.stderrFile).size,
237
+ };
238
+ } catch (_error) {
239
+ return { stdout: 0, stderr: 0 };
240
+ }
241
+ }
242
+
243
+ removeLogs(paths: ProcessLogPaths): void {
244
+ try {
245
+ rmSync(paths.stdoutFile, { force: true });
246
+ rmSync(paths.stderrFile, { force: true });
247
+ rmSync(paths.combinedFile, { force: true });
248
+ } catch (_error) {
249
+ void _error; // Intentionally ignored
250
+ }
251
+ }
252
+
253
+ cleanup(): void {
254
+ if (!this.logDir) return;
255
+ try {
256
+ rmSync(this.logDir, { recursive: true, force: true });
257
+ } catch (_error) {
258
+ void _error; // Intentionally ignored
259
+ }
260
+ this.logDir = null;
261
+ this.fileBytes.clear();
262
+ }
263
+
264
+ [Symbol.dispose](): void {
265
+ this.cleanup();
266
+ }
267
+
268
+ /**
269
+ * Append `data` to `file`, enforcing the per-file size cap. When the cap
270
+ * would be exceeded, the file is truncated to zero and restarted with
271
+ * `marker`, then `data` is written. Returns silently on any fs error,
272
+ * matching the existing best-effort append behavior. Each file is capped
273
+ * independently, so the per-stream and combined files can hold different
274
+ * windows of history after a breach.
275
+ */
276
+ private appendCapped(
277
+ file: string,
278
+ data: string | Buffer,
279
+ marker: string,
280
+ ): void {
281
+ try {
282
+ const size = Buffer.byteLength(data as never);
283
+ const current = this.fileBytes.get(file) ?? 0;
284
+ if (current + size > this.maxFileBytes) {
285
+ truncateSync(file, 0);
286
+ appendFileSync(file, marker);
287
+ this.fileBytes.set(file, Buffer.byteLength(marker));
288
+ }
289
+ appendFileSync(file, data);
290
+ this.fileBytes.set(file, (this.fileBytes.get(file) ?? 0) + size);
291
+ } catch (_error) {
292
+ void _error; // Intentionally ignored
293
+ }
294
+ }
295
+
296
+ private sizeOf(file: string): number {
297
+ try {
298
+ return statSync(file).size;
299
+ } catch (_error) {
300
+ return 0;
301
+ }
302
+ }
303
+
304
+ private readFileSuffix(filePath: string, maxBytes: number): string {
305
+ let fd: number | undefined;
306
+ try {
307
+ fd = openSync(filePath, "r");
308
+ const size = fstatSync(fd).size;
309
+ const length = Math.min(size, maxBytes);
310
+ const buffer = Buffer.allocUnsafe(length);
311
+ readExact(fd, buffer, 0, length, size - length);
312
+ return decodeUtf8Bounded(alignUtf8Start(buffer), maxBytes);
313
+ } catch (_error) {
314
+ return "";
315
+ } finally {
316
+ if (fd !== undefined) closeSync(fd);
317
+ }
318
+ }
319
+ }
320
+
321
+ function countNewlines(buffer: Buffer): number {
322
+ let count = 0;
323
+ for (const byte of buffer) {
324
+ if (byte === 0x0a) count++;
325
+ }
326
+ return count;
327
+ }
328
+
329
+ function alignUtf8Start(buffer: Buffer): Buffer {
330
+ let start = 0;
331
+ while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start++;
332
+ return buffer.subarray(start);
333
+ }
334
+
335
+ function readExact(
336
+ fd: number,
337
+ buffer: Buffer,
338
+ offset: number,
339
+ length: number,
340
+ position: number,
341
+ ): void {
342
+ let total = 0;
343
+ while (total < length) {
344
+ const count = readSync(
345
+ fd,
346
+ buffer,
347
+ offset + total,
348
+ length - total,
349
+ position + total,
350
+ );
351
+ if (count === 0) throw new Error("Unexpected end of log file");
352
+ total += count;
353
+ }
354
+ }
355
+
356
+ function tailLineBuffersNewestFirst(buffer: Buffer, count: number): Buffer[] {
357
+ if (buffer.length === 0 || count <= 0) return [];
358
+
359
+ const result: Buffer[] = [];
360
+ let end = buffer.length;
361
+ if (buffer[end - 1] === 0x0a) end--;
362
+
363
+ while (result.length < count && end >= 0) {
364
+ const newline = end > 0 ? buffer.lastIndexOf(0x0a, end - 1) : -1;
365
+ const start = newline + 1;
366
+ let lineEnd = end;
367
+ if (lineEnd > start && buffer[lineEnd - 1] === 0x0d) lineEnd--;
368
+ result.push(buffer.subarray(start, lineEnd));
369
+ if (newline === -1) break;
370
+ end = newline;
371
+ }
372
+
373
+ return result;
374
+ }
375
+
376
+ function decodeUtf8Bounded(buffer: Buffer, maxOutputBytes: number): string {
377
+ if (buffer.length === 0 || maxOutputBytes <= 0) return "";
378
+
379
+ const decoder = new TextDecoder("utf-8");
380
+ const parts: string[] = [];
381
+ let remaining = maxOutputBytes;
382
+
383
+ for (let offset = 0; offset < buffer.length && remaining > 0; ) {
384
+ const end = Math.min(buffer.length, offset + TAIL_CHUNK_BYTES);
385
+ const text = decoder.decode(buffer.subarray(offset, end), {
386
+ stream: end < buffer.length,
387
+ });
388
+ const textBytes = Buffer.byteLength(text);
389
+ if (textBytes <= remaining) {
390
+ parts.push(text);
391
+ remaining -= textBytes;
392
+ } else {
393
+ const prefix = trimIncompleteUtf8Suffix(
394
+ Buffer.from(text).subarray(0, remaining),
395
+ );
396
+ parts.push(prefix.toString("utf-8"));
397
+ remaining = 0;
398
+ }
399
+ offset = end;
400
+ }
401
+
402
+ return parts.join("");
403
+ }
404
+
405
+ function trimIncompleteUtf8Suffix(buffer: Buffer): Buffer {
406
+ if (buffer.length === 0) return buffer;
407
+ let lead = buffer.length - 1;
408
+ while (lead >= 0 && (buffer[lead] & 0xc0) === 0x80) lead--;
409
+ if (lead < 0) return Buffer.alloc(0);
410
+ const byte = buffer[lead];
411
+ const expected =
412
+ byte < 0x80
413
+ ? 1
414
+ : (byte & 0xe0) === 0xc0
415
+ ? 2
416
+ : (byte & 0xf0) === 0xe0
417
+ ? 3
418
+ : (byte & 0xf8) === 0xf0
419
+ ? 4
420
+ : 1;
421
+ return buffer.length - lead < expected ? buffer.subarray(0, lead) : buffer;
422
+ }