@linxiraos/pi-utils 1.1.15 → 1.1.16
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 +2 -2
- package/THIRD-PARTY-NOTICES.txt +54 -25
- package/dist/types/async.d.ts +18 -0
- package/dist/types/browsers.d.ts +2 -30
- package/dist/types/color.d.ts +2 -0
- package/dist/types/dirs.d.ts +18 -1
- package/dist/types/env.d.ts +12 -2
- package/dist/types/executable.d.ts +4 -0
- package/dist/types/fetch-retry.d.ts +8 -2
- package/dist/types/file-lock.d.ts +6 -0
- package/dist/types/format.d.ts +7 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/mime.d.ts +1 -0
- package/dist/types/path.d.ts +6 -0
- package/dist/types/peek-file.d.ts +2 -3
- 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 +76 -1
- package/dist/types/which.d.ts +8 -2
- package/dist/types/yaml-config.d.ts +2 -0
- package/package.json +2 -2
- package/src/acp/transport.ts +37 -3
- package/src/async.ts +31 -0
- package/src/browsers.ts +10 -169
- package/src/color.ts +1 -1
- package/src/dirs.ts +28 -6
- package/src/env.ts +81 -34
- package/src/executable.ts +17 -0
- package/src/fetch-retry.ts +72 -13
- package/src/file-lock.ts +28 -0
- package/src/format.ts +16 -0
- package/src/index.ts +2 -1
- package/src/json.ts +12 -1
- package/src/logger/rotating-file.ts +40 -3
- package/src/logger.ts +16 -4
- package/src/mime.ts +3 -7
- package/src/path.ts +14 -0
- package/src/peek-file.ts +17 -67
- package/src/postmortem.ts +95 -47
- 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 +167 -35
- package/src/which.ts +38 -14
- package/src/xml.ts +16 -0
- package/src/yaml-config.ts +8 -0
- package/dist/types/glob.d.ts +0 -28
- package/src/glob.ts +0 -189
package/src/stream.ts
CHANGED
|
@@ -4,7 +4,14 @@ import { abortableSource } from "./abortable";
|
|
|
4
4
|
import { parseStreamingJson } from "./json-parse";
|
|
5
5
|
|
|
6
6
|
const LF = 0x0a;
|
|
7
|
+
const CR = 0x0d;
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Split a byte stream on LF boundaries.
|
|
11
|
+
*
|
|
12
|
+
* Every yielded line owns its bytes and remains unchanged after the generator
|
|
13
|
+
* advances or drains. Line terminators are excluded.
|
|
14
|
+
*/
|
|
8
15
|
export async function* readLines(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<Uint8Array> {
|
|
9
16
|
const buffer = new ConcatSink();
|
|
10
17
|
const source = abortableSource(stream, signal);
|
|
@@ -56,13 +63,19 @@ export async function* readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?:
|
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Amortized byte accumulator for chunked stream readers.
|
|
68
|
+
*
|
|
69
|
+
* Holds the unconsumed tail of a stream in a single growing `Buffer` so that
|
|
70
|
+
* appending N chunks costs O(total bytes) instead of re-copying the whole
|
|
71
|
+
* prefix per chunk. Backs {@link readLines}, {@link readJsonl} and
|
|
72
|
+
* {@link readSseEvents}; also usable directly when a reader needs its own
|
|
73
|
+
* framing loop (see `consume` and `flush`).
|
|
74
|
+
*/
|
|
75
|
+
export class ConcatSink {
|
|
64
76
|
#space?: Buffer;
|
|
65
77
|
#length = 0;
|
|
78
|
+
#skipLeadingLf = false;
|
|
66
79
|
|
|
67
80
|
#ensureCapacity(size: number): Buffer {
|
|
68
81
|
const space = this.#space;
|
|
@@ -100,16 +113,37 @@ class ConcatSink {
|
|
|
100
113
|
return this.#length === 0;
|
|
101
114
|
}
|
|
102
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The buffered bytes as a live view — invalidated by the next `append`,
|
|
118
|
+
* `reset` or `consume`.
|
|
119
|
+
*/
|
|
103
120
|
flush(): Uint8Array | undefined {
|
|
104
121
|
if (!this.#length) return undefined;
|
|
105
122
|
return this.#space!.subarray(0, this.#length);
|
|
106
123
|
}
|
|
107
124
|
|
|
125
|
+
/** Drop the first `count` buffered bytes, keeping the remainder. */
|
|
126
|
+
consume(count: number) {
|
|
127
|
+
if (count <= 0) return;
|
|
128
|
+
if (count >= this.#length) {
|
|
129
|
+
this.#length = 0;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.#space!.copyWithin(0, count, this.#length);
|
|
133
|
+
this.#length -= count;
|
|
134
|
+
}
|
|
135
|
+
|
|
108
136
|
clear() {
|
|
109
137
|
this.#length = 0;
|
|
110
138
|
}
|
|
111
139
|
|
|
112
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Append a chunk and yield each complete LF-delimited line.
|
|
142
|
+
*
|
|
143
|
+
* Yielded lines are owned snapshots. Unlike {@link flush}, they remain
|
|
144
|
+
* valid after this sink or the input chunk is mutated.
|
|
145
|
+
*/
|
|
146
|
+
*appendAndFlushLines(chunk: Uint8Array): Generator<Uint8Array> {
|
|
113
147
|
let pos = 0;
|
|
114
148
|
while (pos < chunk.length) {
|
|
115
149
|
const nl = chunk.indexOf(LF, pos);
|
|
@@ -120,12 +154,12 @@ class ConcatSink {
|
|
|
120
154
|
const suffix = chunk.subarray(pos, nl);
|
|
121
155
|
pos = nl + 1;
|
|
122
156
|
if (this.isEmpty) {
|
|
123
|
-
yield suffix;
|
|
157
|
+
yield new Uint8Array(suffix);
|
|
124
158
|
} else {
|
|
125
159
|
this.append(suffix);
|
|
126
160
|
const payload = this.flush();
|
|
127
161
|
if (payload) {
|
|
128
|
-
yield payload;
|
|
162
|
+
yield new Uint8Array(payload);
|
|
129
163
|
this.clear();
|
|
130
164
|
}
|
|
131
165
|
}
|
|
@@ -133,19 +167,27 @@ class ConcatSink {
|
|
|
133
167
|
}
|
|
134
168
|
|
|
135
169
|
appendAndFlushText(chunk: Uint8Array, decoder: TextDecoder): string | undefined {
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
170
|
+
let start = 0;
|
|
171
|
+
if (this.#skipLeadingLf) {
|
|
172
|
+
if (chunk.length === 0) return undefined;
|
|
173
|
+
this.#skipLeadingLf = false;
|
|
174
|
+
if (chunk[0] === LF) start = 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const lastLineEnd = Math.max(chunk.lastIndexOf(LF), chunk.lastIndexOf(CR));
|
|
178
|
+
if (lastLineEnd < start) {
|
|
179
|
+
if (start < chunk.length) this.append(chunk.subarray(start));
|
|
139
180
|
return undefined;
|
|
140
181
|
}
|
|
141
182
|
|
|
142
|
-
const completeEnd =
|
|
183
|
+
const completeEnd = lastLineEnd + 1;
|
|
184
|
+
this.#skipLeadingLf = chunk[lastLineEnd] === CR && completeEnd === chunk.length;
|
|
143
185
|
let text: string;
|
|
144
186
|
if (this.isEmpty) {
|
|
145
|
-
const complete = completeEnd === chunk.length ? chunk : chunk.subarray(
|
|
187
|
+
const complete = start === 0 && completeEnd === chunk.length ? chunk : chunk.subarray(start, completeEnd);
|
|
146
188
|
text = decoder.decode(complete);
|
|
147
189
|
} else {
|
|
148
|
-
this.append(
|
|
190
|
+
this.append(chunk.subarray(start, completeEnd));
|
|
149
191
|
text = decoder.decode(this.flush());
|
|
150
192
|
this.clear();
|
|
151
193
|
}
|
|
@@ -197,6 +239,10 @@ class ConcatSink {
|
|
|
197
239
|
}
|
|
198
240
|
}
|
|
199
241
|
|
|
242
|
+
// =============================================================================
|
|
243
|
+
// SSE (Server-Sent Events)
|
|
244
|
+
// =============================================================================
|
|
245
|
+
|
|
200
246
|
/**
|
|
201
247
|
* Stream parsed JSON objects from SSE `data:` lines.
|
|
202
248
|
*
|
|
@@ -236,12 +282,27 @@ function isRecoverableTrailingJson(data: string): boolean {
|
|
|
236
282
|
return typeof recovered === "object" && recovered !== null;
|
|
237
283
|
}
|
|
238
284
|
|
|
239
|
-
|
|
285
|
+
/**
|
|
286
|
+
* One dispatched `data:` frame from {@link readSseFrames}: either the parsed JSON
|
|
287
|
+
* value, or the text of a frame `JSON.parse` rejected together with the
|
|
288
|
+
* `SyntaxError` it raised (so the strict reader can rethrow it unchanged).
|
|
289
|
+
*/
|
|
290
|
+
type SseFrame<T> = { ok: true; value: T } | { ok: false; raw: string; error: SyntaxError };
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Shared `data:`-line framing for {@link readSseJson} and
|
|
294
|
+
* {@link readSseJsonOrText}: skips empty events, stops at the OpenAI `[DONE]`
|
|
295
|
+
* sentinel, notifies the diagnostic observer, and treats a container-shaped
|
|
296
|
+
* stream tail as a clean end of iteration.
|
|
297
|
+
*/
|
|
298
|
+
async function* readSseFrames<T>(
|
|
240
299
|
stream: ReadableStream<Uint8Array>,
|
|
241
300
|
signal?: AbortSignal,
|
|
242
301
|
onEvent?: SseEventObserver,
|
|
243
|
-
): AsyncGenerator<T
|
|
244
|
-
|
|
302
|
+
): AsyncGenerator<SseFrame<T>> {
|
|
303
|
+
// The diagnostic observer is the only reader of `raw`; capture it exactly
|
|
304
|
+
// when one is attached so the hot path stays allocation-free.
|
|
305
|
+
for await (const sse of readSseEvents(stream, signal, onEvent ? { captureRaw: true } : undefined)) {
|
|
245
306
|
const isTrailing = trailingEvents.has(sse);
|
|
246
307
|
notifySseEventObserver(onEvent, sse);
|
|
247
308
|
const data = sse.data;
|
|
@@ -250,16 +311,60 @@ export async function* readSseJson<T>(
|
|
|
250
311
|
continue;
|
|
251
312
|
}
|
|
252
313
|
try {
|
|
253
|
-
yield JSON.parse(data) as T;
|
|
314
|
+
yield { ok: true, value: JSON.parse(data) as T };
|
|
254
315
|
} catch (err) {
|
|
255
316
|
if (err instanceof SyntaxError && isTrailing && isRecoverableTrailingJson(data)) {
|
|
256
317
|
return;
|
|
257
318
|
}
|
|
319
|
+
if (err instanceof SyntaxError) {
|
|
320
|
+
yield { ok: false, raw: data, error: err };
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
258
323
|
throw err;
|
|
259
324
|
}
|
|
260
325
|
}
|
|
261
326
|
}
|
|
262
327
|
|
|
328
|
+
export async function* readSseJson<T>(
|
|
329
|
+
stream: ReadableStream<Uint8Array>,
|
|
330
|
+
signal?: AbortSignal,
|
|
331
|
+
onEvent?: SseEventObserver,
|
|
332
|
+
): AsyncGenerator<T> {
|
|
333
|
+
for await (const frame of readSseFrames<T>(stream, signal, onEvent)) {
|
|
334
|
+
if (!frame.ok) throw frame.error;
|
|
335
|
+
yield frame.value;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Like {@link readSseJson}, but a `data:` frame that is not valid JSON is yielded
|
|
341
|
+
* as its raw text instead of raising a `SyntaxError`. Cut-off container-shaped
|
|
342
|
+
* stream tails stay recoverable, exactly as they are in {@link readSseJson}.
|
|
343
|
+
*
|
|
344
|
+
* Consumers that only understand objects must treat a `string` yield as a
|
|
345
|
+
* transport-level failure (for example a `429 Too Many Requests` or an HTML
|
|
346
|
+
* throttle page from a reverse proxy that already committed to the stream). This
|
|
347
|
+
* exists because `readSseJson`'s baseline consumers span unrelated transports
|
|
348
|
+
* whose error handling a text yield would subtly change; new call sites opt in.
|
|
349
|
+
*
|
|
350
|
+
* Note that the text lane is only the frames `JSON.parse` *rejected*: a frame
|
|
351
|
+
* carrying a JSON-encoded string (`data: "429 Too Many Requests"`) parses, so it
|
|
352
|
+
* is yielded as that string and is indistinguishable from a rejected frame by
|
|
353
|
+
* type alone. Consumers branching on `typeof === "string"` therefore see both,
|
|
354
|
+
* which is the safe direction — each is classified as text rather than trusted as
|
|
355
|
+
* an event object.
|
|
356
|
+
*/
|
|
357
|
+
export async function* readSseJsonOrText<T>(
|
|
358
|
+
stream: ReadableStream<Uint8Array>,
|
|
359
|
+
signal?: AbortSignal,
|
|
360
|
+
onEvent?: SseEventObserver,
|
|
361
|
+
): AsyncGenerator<T | string> {
|
|
362
|
+
for await (const frame of readSseFrames<T>(stream, signal, onEvent)) {
|
|
363
|
+
if (!frame.ok) yield frame.raw;
|
|
364
|
+
else yield frame.value;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
263
368
|
/**
|
|
264
369
|
* A single Server-Sent Event dispatched on a blank-line boundary.
|
|
265
370
|
*
|
|
@@ -276,6 +381,15 @@ export async function* readSseJson<T>(
|
|
|
276
381
|
export interface ServerSentEvent {
|
|
277
382
|
event: string | null;
|
|
278
383
|
data: string;
|
|
384
|
+
/**
|
|
385
|
+
* Decoded wire lines for this event (`event:`/`data:`/etc.), for the
|
|
386
|
+
* diagnostic pipeline. Populated only when the reader opts in via
|
|
387
|
+
* {@link ReadSseEventsOptions.captureRaw} (or attaches an `onSseEvent`
|
|
388
|
+
* observer to the JSON readers, which opt in automatically); otherwise
|
|
389
|
+
* `[]`. Direct `readSseEvents` callers that need wire text must pass
|
|
390
|
+
* `{ captureRaw: true }` — the field is allocation-free by default so
|
|
391
|
+
* the token path pays no per-frame array/slice cost.
|
|
392
|
+
*/
|
|
279
393
|
raw: string[];
|
|
280
394
|
id?: string;
|
|
281
395
|
retry?: number;
|
|
@@ -288,50 +402,48 @@ interface SseEventState {
|
|
|
288
402
|
// of buffering an array and joining at flush. `null` means "no data: field
|
|
289
403
|
// seen yet" (distinct from a `data:` field with an empty value).
|
|
290
404
|
data: string | null;
|
|
291
|
-
|
|
405
|
+
// Diagnostic wire lines, captured only when a reader asked for them (see
|
|
406
|
+
// `readSseEventsOptions.captureRaw`): per-frame array+slice allocation on
|
|
407
|
+
// the token path otherwise. `null` means capture is off.
|
|
408
|
+
raw: string[] | null;
|
|
292
409
|
id?: string;
|
|
293
410
|
retry?: number;
|
|
294
411
|
}
|
|
295
412
|
|
|
296
413
|
// Complete lines are decoded in one batch per source chunk. Each batch ends on
|
|
297
|
-
//
|
|
414
|
+
// an ASCII line-ending byte, which cannot split a multi-byte UTF-8 sequence.
|
|
298
415
|
const SSE_DECODER = new TextDecoder("utf-8");
|
|
299
416
|
|
|
300
417
|
function flushSseEvent(state: SseEventState): ServerSentEvent | null {
|
|
301
418
|
if (state.event === null && state.data === null && state.id === undefined && state.retry === undefined) {
|
|
302
|
-
state.raw = [];
|
|
419
|
+
if (state.raw !== null) state.raw = [];
|
|
303
420
|
return null;
|
|
304
421
|
}
|
|
305
422
|
const event: ServerSentEvent = {
|
|
306
423
|
event: state.event,
|
|
307
424
|
data: state.data ?? "",
|
|
308
|
-
raw: state.raw,
|
|
425
|
+
raw: state.raw ?? [],
|
|
309
426
|
};
|
|
310
427
|
if (state.id !== undefined) event.id = state.id;
|
|
311
428
|
if (state.retry !== undefined) event.retry = state.retry;
|
|
312
429
|
state.event = null;
|
|
313
430
|
state.data = null;
|
|
314
|
-
state.raw = [];
|
|
431
|
+
if (state.raw !== null) state.raw = [];
|
|
315
432
|
state.id = undefined;
|
|
316
433
|
state.retry = undefined;
|
|
317
434
|
return event;
|
|
318
435
|
}
|
|
319
436
|
|
|
320
437
|
function pushSseLine(line: string, state: SseEventState): ServerSentEvent | null {
|
|
321
|
-
// Complete-line batches split on LF only; strip a trailing CR so CRLF sources
|
|
322
|
-
// don't leak `\r` into field values.
|
|
323
|
-
if (line.charCodeAt(line.length - 1) === 0x0d /* '\r' */) {
|
|
324
|
-
line = line.slice(0, -1);
|
|
325
|
-
}
|
|
326
438
|
if (line.length === 0) return flushSseEvent(state);
|
|
327
439
|
|
|
328
440
|
// Comment line: keep in `raw` for diagnostic context, skip parsing.
|
|
329
441
|
if (line.charCodeAt(0) === 0x3a /* ':' */) {
|
|
330
|
-
state.raw
|
|
442
|
+
state.raw?.push(line);
|
|
331
443
|
return null;
|
|
332
444
|
}
|
|
333
445
|
|
|
334
|
-
state.raw
|
|
446
|
+
state.raw?.push(line);
|
|
335
447
|
|
|
336
448
|
const colon = line.indexOf(":");
|
|
337
449
|
const fieldName = colon === -1 ? line : line.slice(0, colon);
|
|
@@ -385,12 +497,24 @@ function pushSseLine(line: string, state: SseEventState): ServerSentEvent | null
|
|
|
385
497
|
* }
|
|
386
498
|
* ```
|
|
387
499
|
*/
|
|
500
|
+
export interface ReadSseEventsOptions {
|
|
501
|
+
/**
|
|
502
|
+
* Capture per-line wire text into `event.raw` for the diagnostic
|
|
503
|
+
* pipeline (`onSseEvent` observers, raw-SSE viewer). Off by default:
|
|
504
|
+
* every frame otherwise pays an array allocation plus one string slice
|
|
505
|
+
* per line on the token path.
|
|
506
|
+
*/
|
|
507
|
+
captureRaw?: boolean;
|
|
508
|
+
}
|
|
509
|
+
|
|
388
510
|
export async function* readSseEvents(
|
|
389
511
|
stream: ReadableStream<Uint8Array>,
|
|
390
512
|
signal?: AbortSignal,
|
|
513
|
+
options?: ReadSseEventsOptions,
|
|
391
514
|
): AsyncGenerator<ServerSentEvent> {
|
|
392
515
|
const lineBuffer = new ConcatSink();
|
|
393
|
-
const
|
|
516
|
+
const captureRaw = options?.captureRaw === true;
|
|
517
|
+
const state: SseEventState = { event: null, data: null, raw: captureRaw ? [] : null };
|
|
394
518
|
const source = abortableSource(stream, signal);
|
|
395
519
|
try {
|
|
396
520
|
for await (const chunk of source) {
|
|
@@ -398,13 +522,21 @@ export async function* readSseEvents(
|
|
|
398
522
|
if (text === undefined) continue;
|
|
399
523
|
let start = 0;
|
|
400
524
|
while (start < text.length) {
|
|
401
|
-
|
|
402
|
-
|
|
525
|
+
let lineEnd = start;
|
|
526
|
+
while (lineEnd < text.length) {
|
|
527
|
+
const code = text.charCodeAt(lineEnd);
|
|
528
|
+
if (code === LF || code === CR) break;
|
|
529
|
+
lineEnd++;
|
|
530
|
+
}
|
|
531
|
+
const event = pushSseLine(text.slice(start, lineEnd), state);
|
|
403
532
|
if (event) yield event;
|
|
404
|
-
|
|
533
|
+
if (text.charCodeAt(lineEnd) === CR && text.charCodeAt(lineEnd + 1) === LF) {
|
|
534
|
+
lineEnd++;
|
|
535
|
+
}
|
|
536
|
+
start = lineEnd + 1;
|
|
405
537
|
}
|
|
406
538
|
}
|
|
407
|
-
// Treat any trailing partial line (no terminating
|
|
539
|
+
// Treat any trailing partial line (no terminating line ending) as complete.
|
|
408
540
|
if (!lineBuffer.isEmpty) {
|
|
409
541
|
const tail = lineBuffer.flush();
|
|
410
542
|
if (tail) {
|
package/src/which.ts
CHANGED
|
@@ -11,8 +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
|
-
|
|
15
|
-
type CacheKey = string | bigint | number;
|
|
14
|
+
import { isFullyQualifiedPath } from "./path";
|
|
16
15
|
|
|
17
16
|
// Tools shipped by Xcode / Command Line Tools that callers actually look up.
|
|
18
17
|
// Keeps the set small so darwinWhich can fast-reject non-Xcode commands without
|
|
@@ -148,12 +147,12 @@ function getMacosToolPaths(): Map<string, string> {
|
|
|
148
147
|
}
|
|
149
148
|
|
|
150
149
|
// Map: cache key -> resolved binary path or null (not found)
|
|
151
|
-
const toolCache = new Map<
|
|
150
|
+
const toolCache = new Map<string, string | null>();
|
|
152
151
|
|
|
153
152
|
/**
|
|
154
153
|
* Cache policy for which lookups.
|
|
155
154
|
*/
|
|
156
|
-
export
|
|
155
|
+
export enum WhichCachePolicy {
|
|
157
156
|
/**
|
|
158
157
|
* Use cached result if available.
|
|
159
158
|
*/
|
|
@@ -176,9 +175,15 @@ export const enum WhichCachePolicy {
|
|
|
176
175
|
export interface WhichOptions extends Bun.WhichOptions {
|
|
177
176
|
/**
|
|
178
177
|
* Cache policy for the lookup.
|
|
179
|
-
* Defaults to `WhichCachePolicy.
|
|
178
|
+
* Defaults to `WhichCachePolicy.Cached`.
|
|
180
179
|
*/
|
|
181
180
|
cache?: WhichCachePolicy;
|
|
181
|
+
/**
|
|
182
|
+
* Only search absolute directory entries in PATH, ignoring relative entries
|
|
183
|
+
* (e.g. `.` or `./bin`) and empty components to prevent resolving against
|
|
184
|
+
* an untrusted working directory.
|
|
185
|
+
*/
|
|
186
|
+
requireAbsolutePaths?: boolean;
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
// Darwin-specific "which" shim: consult Xcode/CLT toolchain directories after $PATH.
|
|
@@ -192,6 +197,15 @@ function darwinWhich(command: string, options?: Bun.WhichOptions): string | null
|
|
|
192
197
|
return null;
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
function filterAbsoluteSearchPath(rawPath: string | undefined): string | null {
|
|
201
|
+
if (!rawPath) return null;
|
|
202
|
+
const safePath = rawPath
|
|
203
|
+
.split(path.delimiter)
|
|
204
|
+
.filter(dir => dir.length > 0 && isFullyQualifiedPath(dir))
|
|
205
|
+
.join(path.delimiter);
|
|
206
|
+
return safePath || null;
|
|
207
|
+
}
|
|
208
|
+
|
|
195
209
|
// Which function that incorporates Darwin Xcode logic if platform reports as 'darwin'.
|
|
196
210
|
// Look `Bun.which` up per call rather than capturing it at import, so a `Bun.which`
|
|
197
211
|
// stub installed later (the per-test seam) is honoured on every platform.
|
|
@@ -200,14 +214,14 @@ export const whichFresh =
|
|
|
200
214
|
? darwinWhich
|
|
201
215
|
: (command: string, options?: Bun.WhichOptions): string | null => Bun.which(command, options);
|
|
202
216
|
|
|
203
|
-
//
|
|
204
|
-
|
|
217
|
+
// Length-prefixed (command, cwd, PATH) tuple: exact, unlike the 64-bit hash
|
|
218
|
+
// chain it replaced, and the length prefixes keep `("ab", "c")` and
|
|
219
|
+
// `("a", "bc")` distinct without a separator that cwd/PATH could contain.
|
|
220
|
+
function cacheKey(command: string, options?: Bun.WhichOptions): string {
|
|
205
221
|
if (!options) return command;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (options.PATH) h = Bun.hash(options.PATH, h);
|
|
210
|
-
return h;
|
|
222
|
+
const cwd = options?.cwd ?? "";
|
|
223
|
+
const binPath = options?.PATH ?? "";
|
|
224
|
+
return `${command.length}:${command}${cwd.length}:${cwd}${binPath.length}:${binPath}`;
|
|
211
225
|
}
|
|
212
226
|
|
|
213
227
|
/**
|
|
@@ -219,9 +233,16 @@ function cacheKey(command: string, options?: Bun.WhichOptions): CacheKey {
|
|
|
219
233
|
*/
|
|
220
234
|
export function $which(command: string, options?: WhichOptions): string | null {
|
|
221
235
|
const cachePolicy = options?.cache ?? WhichCachePolicy.Cached;
|
|
222
|
-
|
|
236
|
+
let lookupOptions =
|
|
223
237
|
options?.PATH !== undefined || process.env.PATH === undefined ? options : { ...options, PATH: process.env.PATH };
|
|
224
|
-
|
|
238
|
+
|
|
239
|
+
if (options?.requireAbsolutePaths) {
|
|
240
|
+
const safePath = filterAbsoluteSearchPath(lookupOptions?.PATH);
|
|
241
|
+
if (!safePath) return null;
|
|
242
|
+
lookupOptions = { ...lookupOptions, PATH: safePath };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let key: string | undefined;
|
|
225
246
|
|
|
226
247
|
if (cachePolicy !== WhichCachePolicy.Bypass) {
|
|
227
248
|
key = cacheKey(command, lookupOptions);
|
|
@@ -232,6 +253,9 @@ export function $which(command: string, options?: WhichOptions): string | null {
|
|
|
232
253
|
}
|
|
233
254
|
|
|
234
255
|
const result = whichFresh(command, lookupOptions);
|
|
256
|
+
if (result && options?.requireAbsolutePaths && !isFullyQualifiedPath(result)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
235
259
|
if (key != null && cachePolicy !== WhichCachePolicy.ReadOnly) {
|
|
236
260
|
toolCache.set(key, result);
|
|
237
261
|
}
|
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);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { YAML } from "bun";
|
|
2
|
+
|
|
3
|
+
const YAML_MAPPING_HEADER_TRAILING_SPACE = /: +$/gm;
|
|
4
|
+
|
|
5
|
+
/** Serialize config YAML without Bun's trailing space on block mapping headers. */
|
|
6
|
+
export function stringifyYamlConfig(value: unknown): string {
|
|
7
|
+
return YAML.stringify(value, null, 2).replace(YAML_MAPPING_HEADER_TRAILING_SPACE, ":");
|
|
8
|
+
}
|
package/dist/types/glob.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export interface GlobPathsOptions {
|
|
2
|
-
/** Base directory for glob patterns. Defaults to getProjectDir(). */
|
|
3
|
-
cwd?: string;
|
|
4
|
-
/** Glob exclusion patterns. */
|
|
5
|
-
exclude?: string[];
|
|
6
|
-
/** Abort signal to cancel the glob. */
|
|
7
|
-
signal?: AbortSignal;
|
|
8
|
-
/** Timeout in milliseconds for the glob operation. */
|
|
9
|
-
timeoutMs?: number;
|
|
10
|
-
/** Include dotfiles when true. */
|
|
11
|
-
dot?: boolean;
|
|
12
|
-
/** Only return files (skip directories). Default: true. */
|
|
13
|
-
onlyFiles?: boolean;
|
|
14
|
-
/** Respect .gitignore files when true. Walks up directory tree to find all applicable .gitignore files. */
|
|
15
|
-
gitignore?: boolean;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Load .gitignore patterns from a directory and its parents.
|
|
19
|
-
* Walks up the directory tree to find all applicable .gitignore files.
|
|
20
|
-
* Returns glob-compatible exclude patterns.
|
|
21
|
-
*/
|
|
22
|
-
export declare function loadGitignorePatterns(baseDir: string): Promise<string[]>;
|
|
23
|
-
/**
|
|
24
|
-
* Resolve filesystem paths matching glob patterns with optional exclude filters.
|
|
25
|
-
* Returns paths relative to the provided cwd (or getProjectDir()).
|
|
26
|
-
* Errors and abort/timeouts are surfaced to the caller.
|
|
27
|
-
*/
|
|
28
|
-
export declare function globPaths(patterns: string | string[], options?: GlobPathsOptions): Promise<string[]>;
|