@oh-my-pi/pi-utils 18.2.0 → 18.2.2
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.
- package/CHANGELOG.md +22 -0
- package/dist/types/dirs.d.ts +7 -0
- package/dist/types/env.d.ts +8 -0
- package/dist/types/executable.d.ts +4 -0
- package/dist/types/fetch-retry.d.ts +8 -3
- package/dist/types/file-lock.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mime.d.ts +1 -0
- package/dist/types/path.d.ts +6 -0
- package/dist/types/postmortem.d.ts +26 -5
- package/dist/types/procmgr.d.ts +2 -4
- package/dist/types/snowflake.d.ts +1 -0
- package/dist/types/sqlite.d.ts +27 -7
- package/dist/types/stream.d.ts +45 -0
- package/dist/types/which.d.ts +6 -0
- package/package.json +2 -2
- package/src/acp/transport.ts +37 -3
- package/src/dirs.ts +10 -0
- package/src/env.ts +45 -0
- package/src/executable.ts +17 -0
- package/src/fetch-retry.ts +26 -17
- package/src/file-lock.ts +23 -0
- package/src/index.ts +1 -0
- package/src/logger.ts +2 -3
- package/src/mime.ts +3 -7
- package/src/path.ts +14 -0
- package/src/postmortem.ts +89 -41
- package/src/procmgr.ts +2 -12
- package/src/ptree.ts +36 -5
- package/src/snowflake.ts +12 -1
- package/src/sqlite.ts +242 -7
- package/src/stream.ts +89 -8
- package/src/which.ts +27 -1
- package/src/xml.ts +16 -0
package/src/stream.ts
CHANGED
|
@@ -57,11 +57,16 @@ export async function* readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?:
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Amortized byte accumulator for chunked stream readers.
|
|
62
|
+
*
|
|
63
|
+
* Holds the unconsumed tail of a stream in a single growing `Buffer` so that
|
|
64
|
+
* appending N chunks costs O(total bytes) instead of re-copying the whole
|
|
65
|
+
* prefix per chunk. Backs {@link readLines}, {@link readJsonl} and
|
|
66
|
+
* {@link readSseEvents}; also usable directly when a reader needs its own
|
|
67
|
+
* framing loop (see `consume` and `flush`).
|
|
68
|
+
*/
|
|
69
|
+
export class ConcatSink {
|
|
65
70
|
#space?: Buffer;
|
|
66
71
|
#length = 0;
|
|
67
72
|
#skipLeadingLf = false;
|
|
@@ -102,11 +107,26 @@ class ConcatSink {
|
|
|
102
107
|
return this.#length === 0;
|
|
103
108
|
}
|
|
104
109
|
|
|
110
|
+
/**
|
|
111
|
+
* The buffered bytes as a live view — invalidated by the next `append`,
|
|
112
|
+
* `reset` or `consume`.
|
|
113
|
+
*/
|
|
105
114
|
flush(): Uint8Array | undefined {
|
|
106
115
|
if (!this.#length) return undefined;
|
|
107
116
|
return this.#space!.subarray(0, this.#length);
|
|
108
117
|
}
|
|
109
118
|
|
|
119
|
+
/** Drop the first `count` buffered bytes, keeping the remainder. */
|
|
120
|
+
consume(count: number) {
|
|
121
|
+
if (count <= 0) return;
|
|
122
|
+
if (count >= this.#length) {
|
|
123
|
+
this.#length = 0;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.#space!.copyWithin(0, count, this.#length);
|
|
127
|
+
this.#length -= count;
|
|
128
|
+
}
|
|
129
|
+
|
|
110
130
|
clear() {
|
|
111
131
|
this.#length = 0;
|
|
112
132
|
}
|
|
@@ -207,6 +227,10 @@ class ConcatSink {
|
|
|
207
227
|
}
|
|
208
228
|
}
|
|
209
229
|
|
|
230
|
+
// =============================================================================
|
|
231
|
+
// SSE (Server-Sent Events)
|
|
232
|
+
// =============================================================================
|
|
233
|
+
|
|
210
234
|
/**
|
|
211
235
|
* Stream parsed JSON objects from SSE `data:` lines.
|
|
212
236
|
*
|
|
@@ -246,11 +270,24 @@ function isRecoverableTrailingJson(data: string): boolean {
|
|
|
246
270
|
return typeof recovered === "object" && recovered !== null;
|
|
247
271
|
}
|
|
248
272
|
|
|
249
|
-
|
|
273
|
+
/**
|
|
274
|
+
* One dispatched `data:` frame from {@link readSseFrames}: either the parsed JSON
|
|
275
|
+
* value, or the text of a frame `JSON.parse` rejected together with the
|
|
276
|
+
* `SyntaxError` it raised (so the strict reader can rethrow it unchanged).
|
|
277
|
+
*/
|
|
278
|
+
type SseFrame<T> = { ok: true; value: T } | { ok: false; raw: string; error: SyntaxError };
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Shared `data:`-line framing for {@link readSseJson} and
|
|
282
|
+
* {@link readSseJsonOrText}: skips empty events, stops at the OpenAI `[DONE]`
|
|
283
|
+
* sentinel, notifies the diagnostic observer, and treats a container-shaped
|
|
284
|
+
* stream tail as a clean end of iteration.
|
|
285
|
+
*/
|
|
286
|
+
async function* readSseFrames<T>(
|
|
250
287
|
stream: ReadableStream<Uint8Array>,
|
|
251
288
|
signal?: AbortSignal,
|
|
252
289
|
onEvent?: SseEventObserver,
|
|
253
|
-
): AsyncGenerator<T
|
|
290
|
+
): AsyncGenerator<SseFrame<T>> {
|
|
254
291
|
for await (const sse of readSseEvents(stream, signal)) {
|
|
255
292
|
const isTrailing = trailingEvents.has(sse);
|
|
256
293
|
notifySseEventObserver(onEvent, sse);
|
|
@@ -260,16 +297,60 @@ export async function* readSseJson<T>(
|
|
|
260
297
|
continue;
|
|
261
298
|
}
|
|
262
299
|
try {
|
|
263
|
-
yield JSON.parse(data) as T;
|
|
300
|
+
yield { ok: true, value: JSON.parse(data) as T };
|
|
264
301
|
} catch (err) {
|
|
265
302
|
if (err instanceof SyntaxError && isTrailing && isRecoverableTrailingJson(data)) {
|
|
266
303
|
return;
|
|
267
304
|
}
|
|
305
|
+
if (err instanceof SyntaxError) {
|
|
306
|
+
yield { ok: false, raw: data, error: err };
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
268
309
|
throw err;
|
|
269
310
|
}
|
|
270
311
|
}
|
|
271
312
|
}
|
|
272
313
|
|
|
314
|
+
export async function* readSseJson<T>(
|
|
315
|
+
stream: ReadableStream<Uint8Array>,
|
|
316
|
+
signal?: AbortSignal,
|
|
317
|
+
onEvent?: SseEventObserver,
|
|
318
|
+
): AsyncGenerator<T> {
|
|
319
|
+
for await (const frame of readSseFrames<T>(stream, signal, onEvent)) {
|
|
320
|
+
if (!frame.ok) throw frame.error;
|
|
321
|
+
yield frame.value;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Like {@link readSseJson}, but a `data:` frame that is not valid JSON is yielded
|
|
327
|
+
* as its raw text instead of raising a `SyntaxError`. Cut-off container-shaped
|
|
328
|
+
* stream tails stay recoverable, exactly as they are in {@link readSseJson}.
|
|
329
|
+
*
|
|
330
|
+
* Consumers that only understand objects must treat a `string` yield as a
|
|
331
|
+
* transport-level failure (for example a `429 Too Many Requests` or an HTML
|
|
332
|
+
* throttle page from a reverse proxy that already committed to the stream). This
|
|
333
|
+
* exists because `readSseJson`'s baseline consumers span unrelated transports
|
|
334
|
+
* whose error handling a text yield would subtly change; new call sites opt in.
|
|
335
|
+
*
|
|
336
|
+
* Note that the text lane is only the frames `JSON.parse` *rejected*: a frame
|
|
337
|
+
* carrying a JSON-encoded string (`data: "429 Too Many Requests"`) parses, so it
|
|
338
|
+
* is yielded as that string and is indistinguishable from a rejected frame by
|
|
339
|
+
* type alone. Consumers branching on `typeof === "string"` therefore see both,
|
|
340
|
+
* which is the safe direction — each is classified as text rather than trusted as
|
|
341
|
+
* an event object.
|
|
342
|
+
*/
|
|
343
|
+
export async function* readSseJsonOrText<T>(
|
|
344
|
+
stream: ReadableStream<Uint8Array>,
|
|
345
|
+
signal?: AbortSignal,
|
|
346
|
+
onEvent?: SseEventObserver,
|
|
347
|
+
): AsyncGenerator<T | string> {
|
|
348
|
+
for await (const frame of readSseFrames<T>(stream, signal, onEvent)) {
|
|
349
|
+
if (!frame.ok) yield frame.raw;
|
|
350
|
+
else yield frame.value;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
273
354
|
/**
|
|
274
355
|
* A single Server-Sent Event dispatched on a blank-line boundary.
|
|
275
356
|
*
|
package/src/which.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as os from "node:os";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
+
import { isFullyQualifiedPath } from "./path";
|
|
14
15
|
|
|
15
16
|
type CacheKey = string | bigint | number;
|
|
16
17
|
|
|
@@ -179,6 +180,12 @@ export interface WhichOptions extends Bun.WhichOptions {
|
|
|
179
180
|
* Defaults to `WhichCachePolicy.Fresh`.
|
|
180
181
|
*/
|
|
181
182
|
cache?: WhichCachePolicy;
|
|
183
|
+
/**
|
|
184
|
+
* Only search absolute directory entries in PATH, ignoring relative entries
|
|
185
|
+
* (e.g. `.` or `./bin`) and empty components to prevent resolving against
|
|
186
|
+
* an untrusted working directory.
|
|
187
|
+
*/
|
|
188
|
+
requireAbsolutePaths?: boolean;
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
// Darwin-specific "which" shim: consult Xcode/CLT toolchain directories after $PATH.
|
|
@@ -192,6 +199,15 @@ function darwinWhich(command: string, options?: Bun.WhichOptions): string | null
|
|
|
192
199
|
return null;
|
|
193
200
|
}
|
|
194
201
|
|
|
202
|
+
function filterAbsoluteSearchPath(rawPath: string | undefined): string | null {
|
|
203
|
+
if (!rawPath) return null;
|
|
204
|
+
const safePath = rawPath
|
|
205
|
+
.split(path.delimiter)
|
|
206
|
+
.filter(dir => dir.length > 0 && isFullyQualifiedPath(dir))
|
|
207
|
+
.join(path.delimiter);
|
|
208
|
+
return safePath || null;
|
|
209
|
+
}
|
|
210
|
+
|
|
195
211
|
// Which function that incorporates Darwin Xcode logic if platform reports as 'darwin'.
|
|
196
212
|
// Look `Bun.which` up per call rather than capturing it at import, so a `Bun.which`
|
|
197
213
|
// stub installed later (the per-test seam) is honoured on every platform.
|
|
@@ -219,8 +235,15 @@ function cacheKey(command: string, options?: Bun.WhichOptions): CacheKey {
|
|
|
219
235
|
*/
|
|
220
236
|
export function $which(command: string, options?: WhichOptions): string | null {
|
|
221
237
|
const cachePolicy = options?.cache ?? WhichCachePolicy.Cached;
|
|
222
|
-
|
|
238
|
+
let lookupOptions =
|
|
223
239
|
options?.PATH !== undefined || process.env.PATH === undefined ? options : { ...options, PATH: process.env.PATH };
|
|
240
|
+
|
|
241
|
+
if (options?.requireAbsolutePaths) {
|
|
242
|
+
const safePath = filterAbsoluteSearchPath(lookupOptions?.PATH);
|
|
243
|
+
if (!safePath) return null;
|
|
244
|
+
lookupOptions = { ...lookupOptions, PATH: safePath };
|
|
245
|
+
}
|
|
246
|
+
|
|
224
247
|
let key: CacheKey | undefined;
|
|
225
248
|
|
|
226
249
|
if (cachePolicy !== WhichCachePolicy.Bypass) {
|
|
@@ -232,6 +255,9 @@ export function $which(command: string, options?: WhichOptions): string | null {
|
|
|
232
255
|
}
|
|
233
256
|
|
|
234
257
|
const result = whichFresh(command, lookupOptions);
|
|
258
|
+
if (result && options?.requireAbsolutePaths && !isFullyQualifiedPath(result)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
235
261
|
if (key != null && cachePolicy !== WhichCachePolicy.ReadOnly) {
|
|
236
262
|
toolCache.set(key, result);
|
|
237
263
|
}
|
package/src/xml.ts
CHANGED
|
@@ -104,6 +104,12 @@ class XmlReader {
|
|
|
104
104
|
this.#readProcessingInstruction(document);
|
|
105
105
|
continue;
|
|
106
106
|
}
|
|
107
|
+
if (this.#xml.startsWith("</", this.#position)) {
|
|
108
|
+
// Stray end tag with no element open: skip it instead of parsing it as a new element,
|
|
109
|
+
// whose empty name would leave #readElement stuck on the `/`.
|
|
110
|
+
this.#skipThrough(">");
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
107
113
|
if (this.#xml[this.#position] === "<") {
|
|
108
114
|
const element = this.#readElement("");
|
|
109
115
|
this.#addValue(document, element.name, element.name, element.value, element.leaf, null);
|
|
@@ -138,6 +144,11 @@ class XmlReader {
|
|
|
138
144
|
this.#position++;
|
|
139
145
|
this.#skipWhitespace();
|
|
140
146
|
value = this.#readAttributeValue();
|
|
147
|
+
} else if (attributeName === "") {
|
|
148
|
+
// Markup #readName cannot consume (the stray `/` in `<a / >`): skip the character
|
|
149
|
+
// so the loop always makes progress.
|
|
150
|
+
this.#position++;
|
|
151
|
+
continue;
|
|
141
152
|
}
|
|
142
153
|
attributes.push([attributeName, value]);
|
|
143
154
|
}
|
|
@@ -153,6 +164,11 @@ class XmlReader {
|
|
|
153
164
|
this.#position = close < 0 ? this.#xml.length : close + 1;
|
|
154
165
|
break;
|
|
155
166
|
}
|
|
167
|
+
if (this.#xml.startsWith("</", this.#position)) {
|
|
168
|
+
// End tag for an ancestor (an unclosed `<br>` inside `<p>`, or plain mismatched
|
|
169
|
+
// markup): close this element implicitly and leave the tag to its owner.
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
156
172
|
if (this.#xml.startsWith("<![CDATA[", this.#position)) {
|
|
157
173
|
const end = this.#xml.indexOf("]]>", this.#position + 9);
|
|
158
174
|
const raw = this.#xml.slice(this.#position + 9, end < 0 ? this.#xml.length : end);
|