@oh-my-pi/pi-utils 18.2.4 → 18.2.6
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 +13 -0
- package/dist/types/color.d.ts +2 -0
- package/dist/types/env.d.ts +2 -0
- package/dist/types/format.d.ts +7 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/stream.d.ts +19 -1
- package/dist/types/which.d.ts +2 -2
- package/dist/types/yaml-config.d.ts +2 -0
- package/package.json +2 -2
- package/src/color.ts +1 -1
- package/src/env.ts +31 -3
- package/src/format.ts +16 -0
- package/src/index.ts +1 -0
- package/src/json.ts +12 -1
- package/src/logger/rotating-file.ts +40 -3
- package/src/stream.ts +34 -8
- package/src/which.ts +11 -13
- package/src/yaml-config.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.2.5] - 2026-09-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added utilities for reading dotenv-sourced environment values, customizing filtered child-shell environment values, converting color palettes to RGB, cleaning trailing spaces from YAML block headers, and counting newlines in text.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Improved rotating file logging performance by reusing an append file descriptor for each active log file.
|
|
14
|
+
- Improved JSON serialization performance by avoiding unnecessary bigint handling when serializing values without bigints.
|
|
15
|
+
- Fixed `$which` cache collisions for lookups using different PATH or working-directory options.
|
|
16
|
+
- SSE token reads now expose raw wire-line data only when explicitly requested; the default token path no longer includes per-line raw slices.
|
|
17
|
+
|
|
5
18
|
## [18.2.3] - 2026-09-17
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/dist/types/color.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export declare function adjustHsv(hex: string, adj: HSVAdjustment): string;
|
|
|
82
82
|
* Convert HSL (h: 0-360, s: 0-1, l: 0-1) to a CSS hex string.
|
|
83
83
|
*/
|
|
84
84
|
export declare function hslToHex(h: number, s: number, l: number): string;
|
|
85
|
+
/** Parse a 256-color palette index (0–255) to RGB (0..255). */
|
|
86
|
+
export declare function paletteToRgb(index: number): RGB | undefined;
|
|
85
87
|
export interface OKLCH {
|
|
86
88
|
/** Perceptual lightness (0-1) */
|
|
87
89
|
l: number;
|
package/dist/types/env.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export declare function filterProcessEnv(env: Record<string, string | undefined>
|
|
|
36
36
|
export declare function stripGitRepoLocationEnv(env: Record<string, string>, platform?: NodeJS.Platform): void;
|
|
37
37
|
/** Filters process env for child shells without launch-cwd dotenv values. */
|
|
38
38
|
export declare function filterChildShellEnv(env: Record<string, string | undefined>, cwd?: string): Record<string, string>;
|
|
39
|
+
/** Return every value defined by `cwd`'s dotenv files, plus environment values that came from them. */
|
|
40
|
+
export declare function getDotenvEnvValues(cwd?: string, env?: Record<string, string | undefined>): string[];
|
|
39
41
|
/**
|
|
40
42
|
* Parses a complete .env file with the runtime's dotenv grammar, then retains
|
|
41
43
|
* only shell-identifier names and spawn-safe values before mirroring valid
|
package/dist/types/format.d.ts
CHANGED
|
@@ -14,6 +14,13 @@ export declare function formatNumber(n: number): string;
|
|
|
14
14
|
* Examples: "512B", "1.5KB", "2.3MB", "1.2GB"
|
|
15
15
|
*/
|
|
16
16
|
export declare function formatBytes(bytes: number): string;
|
|
17
|
+
/**
|
|
18
|
+
* Count `\n` code units via native `indexOf` — no split array, roughly an
|
|
19
|
+
* order of magnitude cheaper than a per-code-unit loop on multi-MiB text.
|
|
20
|
+
* Line-count semantics are the caller's (empty text is 0 or 1 lines
|
|
21
|
+
* depending on the contract).
|
|
22
|
+
*/
|
|
23
|
+
export declare function countNewlines(text: string): number;
|
|
17
24
|
/**
|
|
18
25
|
* Truncate a string to maxLen characters, appending an ellipsis if truncated.
|
|
19
26
|
* For display-width-aware truncation (terminals), use truncateToWidth from @oh-my-pi/pi-tui.
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/stream.d.ts
CHANGED
|
@@ -91,6 +91,15 @@ export declare function readSseJsonOrText<T>(stream: ReadableStream<Uint8Array>,
|
|
|
91
91
|
export interface ServerSentEvent {
|
|
92
92
|
event: string | null;
|
|
93
93
|
data: string;
|
|
94
|
+
/**
|
|
95
|
+
* Decoded wire lines for this event (`event:`/`data:`/etc.), for the
|
|
96
|
+
* diagnostic pipeline. Populated only when the reader opts in via
|
|
97
|
+
* {@link ReadSseEventsOptions.captureRaw} (or attaches an `onSseEvent`
|
|
98
|
+
* observer to the JSON readers, which opt in automatically); otherwise
|
|
99
|
+
* `[]`. Direct `readSseEvents` callers that need wire text must pass
|
|
100
|
+
* `{ captureRaw: true }` — the field is allocation-free by default so
|
|
101
|
+
* the token path pays no per-frame array/slice cost.
|
|
102
|
+
*/
|
|
94
103
|
raw: string[];
|
|
95
104
|
id?: string;
|
|
96
105
|
retry?: number;
|
|
@@ -114,7 +123,16 @@ export interface ServerSentEvent {
|
|
|
114
123
|
* }
|
|
115
124
|
* ```
|
|
116
125
|
*/
|
|
117
|
-
export
|
|
126
|
+
export interface ReadSseEventsOptions {
|
|
127
|
+
/**
|
|
128
|
+
* Capture per-line wire text into `event.raw` for the diagnostic
|
|
129
|
+
* pipeline (`onSseEvent` observers, raw-SSE viewer). Off by default:
|
|
130
|
+
* every frame otherwise pays an array allocation plus one string slice
|
|
131
|
+
* per line on the token path.
|
|
132
|
+
*/
|
|
133
|
+
captureRaw?: boolean;
|
|
134
|
+
}
|
|
135
|
+
export declare function readSseEvents(stream: ReadableStream<Uint8Array>, signal?: AbortSignal, options?: ReadSseEventsOptions): AsyncGenerator<ServerSentEvent>;
|
|
118
136
|
/**
|
|
119
137
|
* Parse a complete JSONL string, skipping malformed lines instead of throwing.
|
|
120
138
|
*
|
package/dist/types/which.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cache policy for which lookups.
|
|
3
3
|
*/
|
|
4
|
-
export declare
|
|
4
|
+
export declare enum WhichCachePolicy {
|
|
5
5
|
/**
|
|
6
6
|
* Use cached result if available.
|
|
7
7
|
*/
|
|
@@ -22,7 +22,7 @@ export declare const enum WhichCachePolicy {
|
|
|
22
22
|
export interface WhichOptions extends Bun.WhichOptions {
|
|
23
23
|
/**
|
|
24
24
|
* Cache policy for the lookup.
|
|
25
|
-
* Defaults to `WhichCachePolicy.
|
|
25
|
+
* Defaults to `WhichCachePolicy.Cached`.
|
|
26
26
|
*/
|
|
27
27
|
cache?: WhichCachePolicy;
|
|
28
28
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.2.
|
|
3
|
+
"version": "18.2.6",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@oh-my-pi/pi-natives": "18.2.
|
|
57
|
+
"@oh-my-pi/pi-natives": "18.2.6"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/bun": "^1.3.14"
|
package/src/color.ts
CHANGED
|
@@ -239,7 +239,7 @@ const ANSI_16: readonly (readonly [number, number, number])[] = [
|
|
|
239
239
|
const CUBE_STEPS = [0, 95, 135, 175, 215, 255] as const;
|
|
240
240
|
|
|
241
241
|
/** Parse a 256-color palette index (0–255) to RGB (0..255). */
|
|
242
|
-
function paletteToRgb(index: number): RGB | undefined {
|
|
242
|
+
export function paletteToRgb(index: number): RGB | undefined {
|
|
243
243
|
if (!Number.isInteger(index) || index < 0 || index > 255) return undefined;
|
|
244
244
|
if (index < 16) {
|
|
245
245
|
const rgb = ANSI_16[index];
|
package/src/env.ts
CHANGED
|
@@ -150,10 +150,10 @@ function expandDotenvValues(values: Record<string, string>, env: Record<string,
|
|
|
150
150
|
return expanded;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
export function filterChildShellEnv(
|
|
153
|
+
function filterChildShellEnvInternal(
|
|
155
154
|
env: Record<string, string | undefined>,
|
|
156
|
-
cwd: string
|
|
155
|
+
cwd: string,
|
|
156
|
+
onDotenvValue?: (value: string) => void,
|
|
157
157
|
): Record<string, string> {
|
|
158
158
|
const runtimeLaunchEnvValues = env === Bun.env || env === process.env ? launchEnvValues : undefined;
|
|
159
159
|
const result = filterProcessEnv(env);
|
|
@@ -190,6 +190,12 @@ export function filterChildShellEnv(
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
const allLaunchEnv = fallbackLaunchEnv ? { ...launchEnv, ...fallbackLaunchEnv } : launchEnv;
|
|
193
|
+
if (onDotenvValue) {
|
|
194
|
+
// Every value the project's dotenv files define is dotenv-sourced, whether
|
|
195
|
+
// or not this process loaded it (a `--cwd` launch never did).
|
|
196
|
+
for (const key in allLaunchEnv) onDotenvValue(allLaunchEnv[key]!);
|
|
197
|
+
for (const key in expandedLaunchEnv) onDotenvValue(expandedLaunchEnv[key]!);
|
|
198
|
+
}
|
|
193
199
|
for (const key in allLaunchEnv) {
|
|
194
200
|
const launchValue = runtimeLaunchEnvValues?.get(key);
|
|
195
201
|
if (launchValue !== undefined) {
|
|
@@ -211,6 +217,8 @@ export function filterChildShellEnv(
|
|
|
211
217
|
// Strong provenance: the launch environment is known and this name is
|
|
212
218
|
// absent from it, or OMP itself injected the value — either way it came
|
|
213
219
|
// from a project dotenv file, not the parent shell.
|
|
220
|
+
const value = result[key];
|
|
221
|
+
if (value !== undefined) onDotenvValue?.(value);
|
|
214
222
|
delete result[key];
|
|
215
223
|
} else if (
|
|
216
224
|
result[key] === launchEnv[key] ||
|
|
@@ -220,6 +228,8 @@ export function filterChildShellEnv(
|
|
|
220
228
|
) {
|
|
221
229
|
// No launch-env snapshot (dotenv autoloaded without procfs): best-effort
|
|
222
230
|
// value match against the Bun-parsed dotenv.
|
|
231
|
+
const value = result[key];
|
|
232
|
+
if (value !== undefined) onDotenvValue?.(value);
|
|
223
233
|
delete result[key];
|
|
224
234
|
}
|
|
225
235
|
}
|
|
@@ -229,6 +239,24 @@ export function filterChildShellEnv(
|
|
|
229
239
|
return result;
|
|
230
240
|
}
|
|
231
241
|
|
|
242
|
+
/** Filters process env for child shells without launch-cwd dotenv values. */
|
|
243
|
+
export function filterChildShellEnv(
|
|
244
|
+
env: Record<string, string | undefined>,
|
|
245
|
+
cwd: string = getProjectDir(),
|
|
246
|
+
): Record<string, string> {
|
|
247
|
+
return filterChildShellEnvInternal(env, cwd);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Return every value defined by `cwd`'s dotenv files, plus environment values that came from them. */
|
|
251
|
+
export function getDotenvEnvValues(
|
|
252
|
+
cwd: string = getProjectDir(),
|
|
253
|
+
env: Record<string, string | undefined> = process.env,
|
|
254
|
+
): string[] {
|
|
255
|
+
const values = new Set<string>();
|
|
256
|
+
filterChildShellEnvInternal(env, cwd, value => values.add(value));
|
|
257
|
+
return [...values];
|
|
258
|
+
}
|
|
259
|
+
|
|
232
260
|
/**
|
|
233
261
|
* Parses a complete .env file with the runtime's dotenv grammar, then retains
|
|
234
262
|
* only shell-identifier names and spawn-safe values before mirroring valid
|
package/src/format.ts
CHANGED
|
@@ -58,6 +58,22 @@ export function formatBytes(bytes: number): string {
|
|
|
58
58
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Count `\n` code units via native `indexOf` — no split array, roughly an
|
|
63
|
+
* order of magnitude cheaper than a per-code-unit loop on multi-MiB text.
|
|
64
|
+
* Line-count semantics are the caller's (empty text is 0 or 1 lines
|
|
65
|
+
* depending on the contract).
|
|
66
|
+
*/
|
|
67
|
+
export function countNewlines(text: string): number {
|
|
68
|
+
let count = 0;
|
|
69
|
+
let pos = text.indexOf("\n");
|
|
70
|
+
while (pos !== -1) {
|
|
71
|
+
count++;
|
|
72
|
+
pos = text.indexOf("\n", pos + 1);
|
|
73
|
+
}
|
|
74
|
+
return count;
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
/**
|
|
62
78
|
* Truncate a string to maxLen characters, appending an ellipsis if truncated.
|
|
63
79
|
* For display-width-aware truncation (terminals), use truncateToWidth from @oh-my-pi/pi-tui.
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export * from "./tls-fetch";
|
|
|
40
40
|
export * from "./type-guards";
|
|
41
41
|
export * from "./version";
|
|
42
42
|
export * from "./which";
|
|
43
|
+
export * from "./yaml-config";
|
|
43
44
|
|
|
44
45
|
function isPlainObject(val: object): val is Record<string, unknown> {
|
|
45
46
|
return Object.getPrototypeOf(val) === Object.prototype || Array.isArray(val);
|
package/src/json.ts
CHANGED
|
@@ -19,7 +19,18 @@ export function tryParseJson<T = unknown>(content: string): T | null {
|
|
|
19
19
|
* only lossless JSON representation.
|
|
20
20
|
*/
|
|
21
21
|
export function stringifyJson(value: unknown, space?: string | number): string | undefined {
|
|
22
|
-
|
|
22
|
+
// Fast path: a replacer forces the slow stringify on every call, but
|
|
23
|
+
// bigint payloads are vanishingly rare. Try plain stringify first and
|
|
24
|
+
// retry with the coercing replacer only on TypeError (the only error a
|
|
25
|
+
// bigint raises). A TypeError from elsewhere (circular value, throwing
|
|
26
|
+
// toJSON) recurs on the retry and surfaces from that second walk, so
|
|
27
|
+
// stateful serializers ahead of a bigint run twice on that path.
|
|
28
|
+
try {
|
|
29
|
+
return JSON.stringify(value, undefined, space);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (!(error instanceof TypeError)) throw error;
|
|
32
|
+
return JSON.stringify(value, (_key, item) => (typeof item === "bigint" ? item.toString() : item), space);
|
|
33
|
+
}
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
function stableJsonClone(value: unknown): unknown {
|
|
@@ -47,6 +47,12 @@ export class RotatingFileSink {
|
|
|
47
47
|
#activePath: string | undefined;
|
|
48
48
|
#activeBytes = 0;
|
|
49
49
|
#closed = false;
|
|
50
|
+
// Held append fd: the old code did open+write+close per line
|
|
51
|
+
// (appendFileSync) plus a throwaway open in the constructor. A single fd
|
|
52
|
+
// per active file removes two syscalls per log line; it is reopened on
|
|
53
|
+
// rotation and closed on close()/rotation (required for Windows
|
|
54
|
+
// delete-on-prune semantics).
|
|
55
|
+
#fd: number | undefined;
|
|
50
56
|
|
|
51
57
|
constructor(options: RotatingFileOptions) {
|
|
52
58
|
this.#directory = options.directory;
|
|
@@ -61,26 +67,57 @@ export class RotatingFileSink {
|
|
|
61
67
|
const activePath = this.#activePath;
|
|
62
68
|
if (activePath) {
|
|
63
69
|
this.#registerFile(activePath, now.getTime());
|
|
64
|
-
|
|
70
|
+
this.#openFd(activePath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#openFd(filePath: string): void {
|
|
75
|
+
this.#closeFd();
|
|
76
|
+
this.#fd = fs.openSync(filePath, "a");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#closeFd(): void {
|
|
80
|
+
if (this.#fd !== undefined) {
|
|
81
|
+
try {
|
|
82
|
+
fs.closeSync(this.#fd);
|
|
83
|
+
} catch {
|
|
84
|
+
// Best-effort: the fd may already be invalid after rotation.
|
|
85
|
+
}
|
|
86
|
+
this.#fd = undefined;
|
|
65
87
|
}
|
|
66
88
|
}
|
|
67
89
|
|
|
68
90
|
/** Append one already-formatted log record. */
|
|
69
91
|
write(line: string): void {
|
|
70
92
|
if (this.#closed) return;
|
|
93
|
+
const prevPath = this.#activePath;
|
|
71
94
|
const now = new Date();
|
|
72
95
|
this.#selectFile(this.#localDay(now));
|
|
73
96
|
const activePath = this.#activePath;
|
|
74
97
|
if (!activePath) return;
|
|
98
|
+
// Rotation moved the active path: close the old descriptor BEFORE
|
|
99
|
+
// registering the new path, because registration prunes beyond
|
|
100
|
+
// maxFiles — on Windows the pruned predecessor cannot be deleted
|
|
101
|
+
// while still open, which would leak it (audit entry removed, file
|
|
102
|
+
// left on disk, retention unbounded).
|
|
103
|
+
if (activePath !== prevPath) this.#closeFd();
|
|
75
104
|
this.#registerFile(activePath, now.getTime());
|
|
105
|
+
if (this.#fd === undefined) this.#openFd(activePath);
|
|
76
106
|
const record = `${line}${os.EOL}`;
|
|
77
|
-
|
|
78
|
-
|
|
107
|
+
const buf = Buffer.from(record, "utf8");
|
|
108
|
+
let off = 0;
|
|
109
|
+
while (off < buf.length) {
|
|
110
|
+
const written = fs.writeSync(this.#fd!, buf, off);
|
|
111
|
+
if (written <= 0) break;
|
|
112
|
+
off += written;
|
|
113
|
+
}
|
|
114
|
+
this.#activeBytes += buf.length;
|
|
79
115
|
}
|
|
80
116
|
|
|
81
117
|
/** Stop accepting records. Synchronous writes require no drain phase. */
|
|
82
118
|
close(): void {
|
|
83
119
|
this.#closed = true;
|
|
120
|
+
this.#closeFd();
|
|
84
121
|
}
|
|
85
122
|
|
|
86
123
|
#localDay(date: Date): string {
|
package/src/stream.ts
CHANGED
|
@@ -300,7 +300,9 @@ async function* readSseFrames<T>(
|
|
|
300
300
|
signal?: AbortSignal,
|
|
301
301
|
onEvent?: SseEventObserver,
|
|
302
302
|
): AsyncGenerator<SseFrame<T>> {
|
|
303
|
-
|
|
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)) {
|
|
304
306
|
const isTrailing = trailingEvents.has(sse);
|
|
305
307
|
notifySseEventObserver(onEvent, sse);
|
|
306
308
|
const data = sse.data;
|
|
@@ -379,6 +381,15 @@ export async function* readSseJsonOrText<T>(
|
|
|
379
381
|
export interface ServerSentEvent {
|
|
380
382
|
event: string | null;
|
|
381
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
|
+
*/
|
|
382
393
|
raw: string[];
|
|
383
394
|
id?: string;
|
|
384
395
|
retry?: number;
|
|
@@ -391,7 +402,10 @@ interface SseEventState {
|
|
|
391
402
|
// of buffering an array and joining at flush. `null` means "no data: field
|
|
392
403
|
// seen yet" (distinct from a `data:` field with an empty value).
|
|
393
404
|
data: string | null;
|
|
394
|
-
|
|
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;
|
|
395
409
|
id?: string;
|
|
396
410
|
retry?: number;
|
|
397
411
|
}
|
|
@@ -402,19 +416,19 @@ const SSE_DECODER = new TextDecoder("utf-8");
|
|
|
402
416
|
|
|
403
417
|
function flushSseEvent(state: SseEventState): ServerSentEvent | null {
|
|
404
418
|
if (state.event === null && state.data === null && state.id === undefined && state.retry === undefined) {
|
|
405
|
-
state.raw = [];
|
|
419
|
+
if (state.raw !== null) state.raw = [];
|
|
406
420
|
return null;
|
|
407
421
|
}
|
|
408
422
|
const event: ServerSentEvent = {
|
|
409
423
|
event: state.event,
|
|
410
424
|
data: state.data ?? "",
|
|
411
|
-
raw: state.raw,
|
|
425
|
+
raw: state.raw ?? [],
|
|
412
426
|
};
|
|
413
427
|
if (state.id !== undefined) event.id = state.id;
|
|
414
428
|
if (state.retry !== undefined) event.retry = state.retry;
|
|
415
429
|
state.event = null;
|
|
416
430
|
state.data = null;
|
|
417
|
-
state.raw = [];
|
|
431
|
+
if (state.raw !== null) state.raw = [];
|
|
418
432
|
state.id = undefined;
|
|
419
433
|
state.retry = undefined;
|
|
420
434
|
return event;
|
|
@@ -425,11 +439,11 @@ function pushSseLine(line: string, state: SseEventState): ServerSentEvent | null
|
|
|
425
439
|
|
|
426
440
|
// Comment line: keep in `raw` for diagnostic context, skip parsing.
|
|
427
441
|
if (line.charCodeAt(0) === 0x3a /* ':' */) {
|
|
428
|
-
state.raw
|
|
442
|
+
state.raw?.push(line);
|
|
429
443
|
return null;
|
|
430
444
|
}
|
|
431
445
|
|
|
432
|
-
state.raw
|
|
446
|
+
state.raw?.push(line);
|
|
433
447
|
|
|
434
448
|
const colon = line.indexOf(":");
|
|
435
449
|
const fieldName = colon === -1 ? line : line.slice(0, colon);
|
|
@@ -483,12 +497,24 @@ function pushSseLine(line: string, state: SseEventState): ServerSentEvent | null
|
|
|
483
497
|
* }
|
|
484
498
|
* ```
|
|
485
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
|
+
|
|
486
510
|
export async function* readSseEvents(
|
|
487
511
|
stream: ReadableStream<Uint8Array>,
|
|
488
512
|
signal?: AbortSignal,
|
|
513
|
+
options?: ReadSseEventsOptions,
|
|
489
514
|
): AsyncGenerator<ServerSentEvent> {
|
|
490
515
|
const lineBuffer = new ConcatSink();
|
|
491
|
-
const
|
|
516
|
+
const captureRaw = options?.captureRaw === true;
|
|
517
|
+
const state: SseEventState = { event: null, data: null, raw: captureRaw ? [] : null };
|
|
492
518
|
const source = abortableSource(stream, signal);
|
|
493
519
|
try {
|
|
494
520
|
for await (const chunk of source) {
|
package/src/which.ts
CHANGED
|
@@ -13,8 +13,6 @@ import * as os from "node:os";
|
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { isFullyQualifiedPath } from "./path";
|
|
15
15
|
|
|
16
|
-
type CacheKey = string | bigint | number;
|
|
17
|
-
|
|
18
16
|
// Tools shipped by Xcode / Command Line Tools that callers actually look up.
|
|
19
17
|
// Keeps the set small so darwinWhich can fast-reject non-Xcode commands without
|
|
20
18
|
// touching the filesystem. Only needs entries for binaries that live *exclusively*
|
|
@@ -149,12 +147,12 @@ function getMacosToolPaths(): Map<string, string> {
|
|
|
149
147
|
}
|
|
150
148
|
|
|
151
149
|
// Map: cache key -> resolved binary path or null (not found)
|
|
152
|
-
const toolCache = new Map<
|
|
150
|
+
const toolCache = new Map<string, string | null>();
|
|
153
151
|
|
|
154
152
|
/**
|
|
155
153
|
* Cache policy for which lookups.
|
|
156
154
|
*/
|
|
157
|
-
export
|
|
155
|
+
export enum WhichCachePolicy {
|
|
158
156
|
/**
|
|
159
157
|
* Use cached result if available.
|
|
160
158
|
*/
|
|
@@ -177,7 +175,7 @@ export const enum WhichCachePolicy {
|
|
|
177
175
|
export interface WhichOptions extends Bun.WhichOptions {
|
|
178
176
|
/**
|
|
179
177
|
* Cache policy for the lookup.
|
|
180
|
-
* Defaults to `WhichCachePolicy.
|
|
178
|
+
* Defaults to `WhichCachePolicy.Cached`.
|
|
181
179
|
*/
|
|
182
180
|
cache?: WhichCachePolicy;
|
|
183
181
|
/**
|
|
@@ -216,14 +214,14 @@ export const whichFresh =
|
|
|
216
214
|
? darwinWhich
|
|
217
215
|
: (command: string, options?: Bun.WhichOptions): string | null => Bun.which(command, options);
|
|
218
216
|
|
|
219
|
-
//
|
|
220
|
-
|
|
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 {
|
|
221
221
|
if (!options) return command;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (options.PATH) h = Bun.hash(options.PATH, h);
|
|
226
|
-
return h;
|
|
222
|
+
const cwd = options?.cwd ?? "";
|
|
223
|
+
const binPath = options?.PATH ?? "";
|
|
224
|
+
return `${command.length}:${command}${cwd.length}:${cwd}${binPath.length}:${binPath}`;
|
|
227
225
|
}
|
|
228
226
|
|
|
229
227
|
/**
|
|
@@ -244,7 +242,7 @@ export function $which(command: string, options?: WhichOptions): string | null {
|
|
|
244
242
|
lookupOptions = { ...lookupOptions, PATH: safePath };
|
|
245
243
|
}
|
|
246
244
|
|
|
247
|
-
let key:
|
|
245
|
+
let key: string | undefined;
|
|
248
246
|
|
|
249
247
|
if (cachePolicy !== WhichCachePolicy.Bypass) {
|
|
250
248
|
key = cacheKey(command, lookupOptions);
|
|
@@ -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
|
+
}
|