@oh-my-pi/pi-utils 18.12.6 → 18.13.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.13.0] - 2026-10-26
6
+
7
+ ### Added
8
+
9
+ - Added `parseHeifImageMimeType()`, which identifies HEIC, HEIF and AVIF images from a file header ([#16612](https://github.com/can1357/oh-my-pi/pull/16612) by [@will-bogusz](https://github.com/will-bogusz))
10
+ - Added `$whichAll()`, which returns every PATH match for a command in PATH order ([#16824](https://github.com/can1357/oh-my-pi/issues/16824))
11
+
12
+ ### Changed
13
+
14
+ - Faster lenient JSONL session parsing when recovering past malformed lines ([#16995](https://github.com/can1357/oh-my-pi/pull/16995) by [@GTanger](https://github.com/GTanger))
15
+
16
+ ### Fixed
17
+
18
+ - `setProcessName()` no longer assigns `process.title` on Windows, where it overwrites the shared console title ([#16842](https://github.com/can1357/oh-my-pi/issues/16842))
19
+ - `setProcessName()` no longer assigns `process.title` on Windows, where it overwrites the shared console title ([#16842](https://github.com/can1357/oh-my-pi/issues/16842))
20
+ - Fixed inline code spans rendering flush against the surrounding text when a backslash preceded the closing backtick ([#16972](https://github.com/can1357/oh-my-pi/pull/16972) by [@akainth015](https://github.com/akainth015))
21
+
5
22
  ## [18.12.6] - 2026-10-21
6
23
 
7
24
  ### Added
@@ -45,15 +45,23 @@ declare class EmphasisDelimiters {
45
45
  }
46
46
  /**
47
47
  * The runs of backticks in the root source, for the code span rule. A span opened by `width` backticks closes at the
48
- * first `width` backticks after its opening run that no backslash escapes: the start of a later run of at least
49
- * `width`, or, in a run whose first backtick a backslash escapes, the backticks from its `width`th on, since the
50
- * search for a closer goes on `width` past an escaped one.
48
+ * next run of exactly `width` backticks: a run of any other length is no closer, and the one that closes is the first
49
+ * run of that width at or after the opener. Backslash escapes do not apply inside a code span, so a backslash just
50
+ * before that closing backtick is content and still closes the span.
51
51
  */
52
52
  declare class CodeRuns {
53
53
  #private;
54
54
  constructor(src: string);
55
55
  /** Where the run of backticks holding the backtick at `at` ends. */
56
56
  endOf(at: number): number;
57
+ /**
58
+ * Whether a backtick string starts at `at` for a scan that walked to `at`: a string is a whole run, so it starts at
59
+ * the run's first backtick and nowhere else. A retry deeper inside a run is not a string, which keeps a run whose
60
+ * opener found no closer from opening a narrower span one backtick later. A position a tokenizer's raw resumed at,
61
+ * or the suffix an escape token left behind, is not a walked position — `codeOpenerWidth` admits those on its own,
62
+ * because it is told the scan resumed there rather than inferring it from the source text.
63
+ */
64
+ startsStringAt(at: number): boolean;
57
65
  /** Where the closer of a code span opened by `width` backticks that end at `from` starts, or -1. */
58
66
  closerAfter(from: number, width: number): number;
59
67
  }
@@ -102,8 +110,13 @@ export declare class InlineClosers {
102
110
  escapeWithin(rest: string, from: number, to: number): boolean;
103
111
  /** Where the emphasis opened by the `width` markers at the start of `rest` closes, or -1. */
104
112
  closeEmphasis(rest: string, marker: string, width: number): number;
105
- /** How many backticks start `rest`, which starts with one. */
106
- codeOpenerWidth(rest: string): number;
113
+ /**
114
+ * How many backticks start `rest`, which starts with one, or 0 where no code span opens because no backtick string
115
+ * starts there: the opener is a whole string, so the rest of a run whose opener found no closer is text, while the
116
+ * backticks left after a backslash escaped the run's first one do open a span. `resumed` says the scan resumed at
117
+ * `rest` after a tokenizer consumed a raw that ended here, where the backticks are a string wherever they sit.
118
+ */
119
+ codeOpenerWidth(rest: string, resumed?: boolean): number;
107
120
  /** Where the code span opened by the `width` backticks at the start of `rest` closes, or -1. */
108
121
  closeCode(rest: string, width: number): number;
109
122
  /**
@@ -26,6 +26,13 @@ export type ImageMetadata = {
26
26
  hasAlpha?: boolean;
27
27
  };
28
28
  export declare function parseImageMetadata(header: Uint8Array): ImageMetadata | null;
29
+ /** Still images in a HEIF (ISO BMFF) container, which the image pipeline cannot decode itself. */
30
+ export type HeifImageMimeType = "image/heic" | "image/heif" | "image/avif";
31
+ /**
32
+ * MIME type of a HEIC, AVIF or other HEIF image from the brands of its leading `ftyp` box:
33
+ * the major brand, then the compatible brands, with the generic `mif1`/`msf1` brands last.
34
+ */
35
+ export declare function parseHeifImageMimeType(header: Uint8Array): HeifImageMimeType | null;
29
36
  /** Why base64 image data is unusable; `undefined` when it is canonical and non-empty. */
30
37
  export type Base64ImageDataFailure = "invalid base64 image data" | "empty image data";
31
38
  /**
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Set both the JS `process.title` and — on Linux — the kernel `comm` name.
2
+ * Set the JS `process.title` (except on Windows, see above) and — on Linux —
3
+ * the kernel `comm` name.
3
4
  *
4
5
  * Never throws: `bun:ffi` unavailability or a failed syscall degrades silently
5
6
  * to the `process.title`-only behavior, so it is safe to call at startup.
@@ -42,4 +42,17 @@ export declare const whichFresh: typeof darwinWhich;
42
42
  * @returns Filesystem path if found, else null
43
43
  */
44
44
  export declare function $which(command: string, options?: WhichOptions): string | null;
45
+ /**
46
+ * Locate every match for `command` on PATH, in PATH order, uncached.
47
+ *
48
+ * Callers that probe candidates (e.g. the Python eval backend) use this so a broken
49
+ * first hit — a version-manager shim with no version selected, a Windows Store alias —
50
+ * does not hide a working binary further down PATH. On macOS the Xcode/CLT toolchain
51
+ * match is appended last, mirroring {@link $which}'s fallback.
52
+ *
53
+ * @param command - Binary name to resolve
54
+ * @param options - `PATH` (defaults to `process.env.PATH`) and `cwd` for relative entries
55
+ * @returns Distinct filesystem paths; empty when nothing matches
56
+ */
57
+ export declare function $whichAll(command: string, options?: Bun.WhichOptions): string[];
45
58
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "18.12.6",
3
+ "version": "18.13.0",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "cli",
@@ -57,7 +57,7 @@
57
57
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
58
58
  },
59
59
  "dependencies": {
60
- "@oh-my-pi/pi-natives": "18.12.6"
60
+ "@oh-my-pi/pi-natives": "18.13.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/bun": "^1.3.14"
@@ -655,39 +655,48 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
655
655
  // The context is shared with the sources nested in this one, which move its end while they are lexed.
656
656
  context.end = closers.end;
657
657
  let rest = lex.rest;
658
+ // A token the lexer consumed ends the walk, so the backticks left where the next token starts are a string of their
659
+ // own even when the run they belong to began earlier: an extension, an override or an escape token may leave the
660
+ // scan inside a run, and a code span opens there. Only the text step's own advance leaves the scan inside a run
661
+ // without consuming a token, and no span opens at such a position.
662
+ let resumed = false;
663
+ /** Pushes `token` and moves past its raw, which is where the next token starts. */
664
+ const consumeToken = (token: Token): void => {
665
+ output.push(token);
666
+ rest = rest.slice(token.raw.length);
667
+ resumed = true;
668
+ };
658
669
  while (rest !== "") {
670
+ const resumedHere = resumed;
671
+ resumed = false;
659
672
  let custom: Tokens.Generic | undefined;
660
673
  for (const extension of lexer.extensions.inline) {
661
674
  custom = extension.tokenizer.call(context, rest, output);
662
675
  if (custom?.raw) break;
663
676
  }
664
677
  if (custom?.raw) {
665
- output.push(custom);
666
- rest = rest.slice(custom.raw.length);
678
+ consumeToken(custom);
667
679
  continue;
668
680
  }
669
681
 
670
682
  const escaped = /^\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/.exec(rest);
671
683
  if (escaped) {
672
- output.push({ type: "escape", raw: escaped[0], text: escaped[1]! });
673
- rest = rest.slice(2);
684
+ consumeToken({ type: "escape", raw: escaped[0], text: escaped[1]! });
674
685
  continue;
675
686
  }
676
687
  const br = lexer.options.breaks ? /^(?: {2,}|\\)?\n/.exec(rest) : /^(?: {2,}|\\)\n/.exec(rest);
677
688
  if (br) {
678
- output.push({ type: "br", raw: br[0] });
679
- rest = rest.slice(br[0].length);
689
+ consumeToken({ type: "br", raw: br[0] });
680
690
  continue;
681
691
  }
682
692
  if (rest[0] === "`") {
683
- const width = closers.codeOpenerWidth(rest);
693
+ const width = closers.codeOpenerWidth(rest, resumedHere);
684
694
  const end = closers.closeCode(rest, width);
685
695
  if (end !== -1) {
686
696
  const raw = rest.slice(0, end + width);
687
697
  let text = rest.slice(width, end).replace(/\n/g, " ");
688
698
  if (/^ .* $/.test(text) && text.trim() !== "") text = text.slice(1, -1);
689
- output.push({ type: "codespan", raw, text });
690
- rest = rest.slice(raw.length);
699
+ consumeToken({ type: "codespan", raw, text });
691
700
  continue;
692
701
  }
693
702
  }
@@ -695,14 +704,12 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
695
704
  if (auto) {
696
705
  const text = auto.slice(1, -1);
697
706
  const href = text.includes("@") && !/^[a-z][a-z+.-]*:\/\//i.test(text) ? `mailto:${text}` : text;
698
- output.push({ type: "link", raw: auto, text, href, tokens: [{ type: "text", raw: text, text }] });
699
- rest = rest.slice(auto.length);
707
+ consumeToken({ type: "link", raw: auto, text, href, tokens: [{ type: "text", raw: text, text }] });
700
708
  continue;
701
709
  }
702
710
  const html = inlineHtmlPrefix(rest, closers);
703
711
  if (html) {
704
- output.push({ type: "html", raw: html, inLink: false, inRawBlock: false, block: false, text: html });
705
- rest = rest.slice(html.length);
712
+ consumeToken({ type: "html", raw: html, inLink: false, inRawBlock: false, block: false, text: html });
706
713
  continue;
707
714
  }
708
715
  const nested = matchLink(rest, lexer, closers) ?? matchEmphasis(rest, output, closers);
@@ -716,8 +723,7 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
716
723
  if (override) del = override.call(lexer.tokenizer, rest, rest);
717
724
  if (del === false || !override) del = lexer.tokenizer.del(rest);
718
725
  if (del) {
719
- output.push(del);
720
- rest = rest.slice(del.raw.length);
726
+ consumeToken(del);
721
727
  continue;
722
728
  }
723
729
  }
@@ -744,8 +750,7 @@ function lexToNested(lex: InlineLex): NestedMatch | undefined {
744
750
  }
745
751
  }
746
752
  if (url) {
747
- output.push(url);
748
- rest = rest.slice(url.raw.length);
753
+ consumeToken(url);
749
754
  continue;
750
755
  }
751
756
 
@@ -170,38 +170,30 @@ class EmphasisDelimiters {
170
170
 
171
171
  /**
172
172
  * The runs of backticks in the root source, for the code span rule. A span opened by `width` backticks closes at the
173
- * first `width` backticks after its opening run that no backslash escapes: the start of a later run of at least
174
- * `width`, or, in a run whose first backtick a backslash escapes, the backticks from its `width`th on, since the
175
- * search for a closer goes on `width` past an escaped one.
173
+ * next run of exactly `width` backticks: a run of any other length is no closer, and the one that closes is the first
174
+ * run of that width at or after the opener. Backslash escapes do not apply inside a code span, so a backslash just
175
+ * before that closing backtick is content and still closes the span.
176
176
  */
177
177
  class CodeRuns {
178
- // Per run, in order: where it starts and ends, and whether a backslash escapes it.
178
+ // Per run, in order: where it starts and ends; the starts are the binary search `endOf` and `startsStringAt` need.
179
179
  readonly #starts: number[] = [];
180
180
  readonly #ends: number[] = [];
181
- readonly #escaped: boolean[] = [];
182
- // A segment tree over the runs of the widest closer each run holds: its length, or half of it when escaped.
183
- readonly #widest: Int32Array;
184
- readonly #leaves: number;
181
+ // Run starts by width, each list ascending: a closer must have the opener's exact width, so finding one is a
182
+ // binary search in the list for that width and no scan of the runs between.
183
+ readonly #byWidth = new Map<number, number[]>();
185
184
 
186
185
  constructor(src: string) {
187
- const widths: number[] = [];
188
186
  for (let at = src.indexOf("`"); at !== -1;) {
189
187
  let end = at + 1;
190
188
  while (src.charCodeAt(end) === 0x60 /* ` */) end++;
191
- const escaped = escapedAt(src, at);
192
189
  this.#starts.push(at);
193
190
  this.#ends.push(end);
194
- this.#escaped.push(escaped);
195
- widths.push(escaped ? (end - at) >> 1 : end - at);
191
+ const width = end - at;
192
+ const starts = this.#byWidth.get(width);
193
+ if (starts === undefined) this.#byWidth.set(width, [at]);
194
+ else starts.push(at);
196
195
  at = src.indexOf("`", end);
197
196
  }
198
- let leaves = 1;
199
- while (leaves < widths.length) leaves <<= 1;
200
- const widest = new Int32Array(2 * leaves);
201
- for (let run = 0; run < widths.length; run++) widest[leaves + run] = widths[run]!;
202
- for (let node = leaves - 1; node >= 1; node--) widest[node] = Math.max(widest[2 * node]!, widest[2 * node + 1]!);
203
- this.#widest = widest;
204
- this.#leaves = leaves;
205
197
  }
206
198
 
207
199
  /** Where the run of backticks holding the backtick at `at` ends. */
@@ -209,28 +201,24 @@ class CodeRuns {
209
201
  return this.#ends[lowerBound(this.#starts, at + 1) - 1]!;
210
202
  }
211
203
 
204
+ /**
205
+ * Whether a backtick string starts at `at` for a scan that walked to `at`: a string is a whole run, so it starts at
206
+ * the run's first backtick and nowhere else. A retry deeper inside a run is not a string, which keeps a run whose
207
+ * opener found no closer from opening a narrower span one backtick later. A position a tokenizer's raw resumed at,
208
+ * or the suffix an escape token left behind, is not a walked position — `codeOpenerWidth` admits those on its own,
209
+ * because it is told the scan resumed there rather than inferring it from the source text.
210
+ */
211
+ startsStringAt(at: number): boolean {
212
+ const run = lowerBound(this.#starts, at + 1) - 1;
213
+ return this.#starts[run] === at;
214
+ }
215
+
212
216
  /** Where the closer of a code span opened by `width` backticks that end at `from` starts, or -1. */
213
217
  closerAfter(from: number, width: number): number {
214
- const run = this.#firstHolding(lowerBound(this.#starts, from), width);
215
- return run === -1 ? -1 : this.#starts[run]! + (this.#escaped[run] ? width : 0);
216
- }
217
-
218
- /** The first run at or after `first` that holds a closer of `width` backticks, or -1. */
219
- #firstHolding(first: number, width: number): number {
220
- if (first >= this.#starts.length) return -1;
221
- const widest = this.#widest;
222
- const leaves = this.#leaves;
223
- let node = leaves + first;
224
- if (widest[node]! >= width) return first;
225
- // Up to the nearest subtree on the right that holds one, then down to its first run that does.
226
- for (;;) {
227
- if (node === 1) return -1;
228
- if ((node & 1) === 0 && widest[node + 1]! >= width) break;
229
- node >>= 1;
230
- }
231
- node++;
232
- while (node < leaves) node = widest[2 * node]! >= width ? 2 * node : 2 * node + 1;
233
- return node - leaves;
218
+ const starts = this.#byWidth.get(width);
219
+ if (starts === undefined) return -1;
220
+ const run = lowerBound(starts, from);
221
+ return run < starts.length ? starts[run]! : -1;
234
222
  }
235
223
  }
236
224
 
@@ -388,10 +376,17 @@ export class InlineClosers {
388
376
  return close === -1 ? -1 : close - pos;
389
377
  }
390
378
 
391
- /** How many backticks start `rest`, which starts with one. */
392
- codeOpenerWidth(rest: string): number {
379
+ /**
380
+ * How many backticks start `rest`, which starts with one, or 0 where no code span opens because no backtick string
381
+ * starts there: the opener is a whole string, so the rest of a run whose opener found no closer is text, while the
382
+ * backticks left after a backslash escaped the run's first one do open a span. `resumed` says the scan resumed at
383
+ * `rest` after a tokenizer consumed a raw that ended here, where the backticks are a string wherever they sit.
384
+ */
385
+ codeOpenerWidth(rest: string, resumed = false): number {
393
386
  const pos = this.end - rest.length;
394
- return Math.min(this.#indexes.codeRuns.endOf(pos), this.end) - pos;
387
+ const runs = this.#indexes.codeRuns;
388
+ if (!resumed && !runs.startsStringAt(pos)) return 0;
389
+ return Math.min(runs.endOf(pos), this.end) - pos;
395
390
  }
396
391
 
397
392
  /** Where the code span opened by the `width` backticks at the start of `rest` closes, or -1. */
package/src/mime.ts CHANGED
@@ -11,6 +11,7 @@ const GIF89A = Buffer.from("GIF89a");
11
11
  const WEBP_VP8X = Buffer.from("VP8X");
12
12
  const WEBP_VP8L = Buffer.from("VP8L");
13
13
  const WEBP_VP8 = Buffer.from("VP8 ");
14
+ const FTYP = Buffer.from("ftyp");
14
15
 
15
16
  export const SUPPORTED_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
16
17
 
@@ -143,6 +144,40 @@ export function parseImageMetadata(header: Uint8Array): ImageMetadata | null {
143
144
  );
144
145
  }
145
146
 
147
+ /** Still images in a HEIF (ISO BMFF) container, which the image pipeline cannot decode itself. */
148
+ export type HeifImageMimeType = "image/heic" | "image/heif" | "image/avif";
149
+
150
+ const HEIF_BRAND_MIME_TYPES: Readonly<Record<string, HeifImageMimeType>> = {
151
+ heic: "image/heic",
152
+ heix: "image/heic",
153
+ heim: "image/heic",
154
+ heis: "image/heic",
155
+ hevc: "image/heic",
156
+ hevx: "image/heic",
157
+ avif: "image/avif",
158
+ avis: "image/avif",
159
+ mif1: "image/heif",
160
+ msf1: "image/heif",
161
+ };
162
+
163
+ /**
164
+ * MIME type of a HEIC, AVIF or other HEIF image from the brands of its leading `ftyp` box:
165
+ * the major brand, then the compatible brands, with the generic `mif1`/`msf1` brands last.
166
+ */
167
+ export function parseHeifImageMimeType(header: Uint8Array): HeifImageMimeType | null {
168
+ if (!magicEquals(header, 4, FTYP)) return null;
169
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
170
+ const boxEnd = Math.min(view.getUint32(0, false), header.length);
171
+ let generic: HeifImageMimeType | null = null;
172
+ // The major brand sits at 8 and the compatible brands start at 16, after the minor version.
173
+ for (let offset = 8; offset + 4 <= boxEnd; offset += offset === 8 ? 8 : 4) {
174
+ const mimeType = HEIF_BRAND_MIME_TYPES[String.fromCharCode(...header.subarray(offset, offset + 4))];
175
+ if (mimeType === "image/heif") generic ??= mimeType;
176
+ else if (mimeType) return mimeType;
177
+ }
178
+ return generic;
179
+ }
180
+
146
181
  /** Why base64 image data is unusable; `undefined` when it is canonical and non-empty. */
147
182
  export type Base64ImageDataFailure = "invalid base64 image data" | "empty image data";
148
183
 
@@ -10,9 +10,11 @@
10
10
  * setter) and additionally drive `prctl` via `bun:ffi` on Linux, mirroring the
11
11
  * libc-FFI pattern in `ttyid.ts` / `stderr-guard.ts`.
12
12
  *
13
- * macOS has no clean userspace equivalent for the shebang-run path, and on
14
- * Windows / compiled binaries the kernel derives the name from the exec'd file,
15
- * so those paths already report correctly; there we only set `process.title`.
13
+ * macOS has no clean userspace equivalent for the shebang-run path, so there
14
+ * we only set `process.title`. Windows derives the name from the exec'd file,
15
+ * and there Bun's `process.title` setter is libuv's `SetConsoleTitleW`: every
16
+ * omp child attached to a pane's console (workers, `browser-relay`, servers)
17
+ * would overwrite that pane's tab title, so Windows skips it entirely.
16
18
  */
17
19
  import { dlopen, FFIType, ptr } from "bun:ffi";
18
20
  import * as os from "node:os";
@@ -21,17 +23,20 @@ import * as os from "node:os";
21
23
  const PR_SET_NAME = 15;
22
24
 
23
25
  /**
24
- * Set both the JS `process.title` and — on Linux — the kernel `comm` name.
26
+ * Set the JS `process.title` (except on Windows, see above) and — on Linux —
27
+ * the kernel `comm` name.
25
28
  *
26
29
  * Never throws: `bun:ffi` unavailability or a failed syscall degrades silently
27
30
  * to the `process.title`-only behavior, so it is safe to call at startup.
28
31
  */
29
32
  export function setProcessName(name: string): void {
33
+ const platform = os.platform();
34
+ if (platform === "win32") return;
30
35
  try {
31
36
  process.title = name;
32
37
  } catch {}
33
38
 
34
- if (os.platform() !== "linux") return;
39
+ if (platform !== "linux") return;
35
40
 
36
41
  // glibc first, then the generic soname for musl-style layouts (see stderr-guard.ts).
37
42
  for (const soname of ["libc.so.6", "libc.so"]) {
package/src/stream.ts CHANGED
@@ -2,6 +2,7 @@ const trailingEvents = new WeakSet<ServerSentEvent>();
2
2
 
3
3
  import { abortableSource } from "./abortable";
4
4
  import { parseStreamingJson } from "./json-parse";
5
+ import { isWhitespace } from "./json-lexer";
5
6
 
6
7
  const LF = 0x0a;
7
8
  const CR = 0x0d;
@@ -613,9 +614,11 @@ export async function* readSseEvents(
613
614
  */
614
615
  export function parseJsonlLenient<T>(buffer: string, options: { onMalformedRecord?: () => void } = {}): T[] {
615
616
  let entries: T[] | undefined;
617
+ let cursor = 0;
618
+ const len = buffer.length;
616
619
 
617
- while (buffer.length > 0) {
618
- const { values, error, read, done } = Bun.JSONL.parseChunk(buffer);
620
+ while (cursor < len) {
621
+ const { values, error, read, done } = Bun.JSONL.parseChunk(buffer, cursor);
619
622
  if (values.length > 0) {
620
623
  const ext = values as T[];
621
624
  if (!entries) {
@@ -625,18 +628,22 @@ export function parseJsonlLenient<T>(buffer: string, options: { onMalformedRecor
625
628
  }
626
629
  }
627
630
  if (error) {
628
- const nextNewline = buffer.indexOf("\n", read);
629
- const malformedEnd = nextNewline === -1 ? buffer.length : nextNewline;
630
- if (buffer.substring(read, malformedEnd).trim().length > 0) options.onMalformedRecord?.();
631
+ let malformedStart = read;
632
+ while (malformedStart < len && isWhitespace(buffer.charCodeAt(malformedStart))) {
633
+ malformedStart++;
634
+ }
635
+ const nextNewline = buffer.indexOf("\n", malformedStart);
636
+ const malformedEnd = nextNewline === -1 ? len : nextNewline;
637
+ if (buffer.substring(malformedStart, malformedEnd).trim().length > 0) options.onMalformedRecord?.();
631
638
  if (nextNewline === -1) break;
632
- buffer = buffer.substring(nextNewline + 1);
639
+ cursor = nextNewline + 1;
633
640
  continue;
634
641
  }
635
- if (read === 0) {
636
- if (buffer.trim().length > 0) options.onMalformedRecord?.();
642
+ if (read === cursor) {
643
+ if (buffer.substring(cursor).trim().length > 0) options.onMalformedRecord?.();
637
644
  break;
638
645
  }
639
- buffer = buffer.substring(read);
646
+ cursor = read;
640
647
  if (done) break;
641
648
  }
642
649
  return entries ?? [];
package/src/which.ts CHANGED
@@ -261,3 +261,30 @@ export function $which(command: string, options?: WhichOptions): string | null {
261
261
  }
262
262
  return result;
263
263
  }
264
+
265
+ /**
266
+ * Locate every match for `command` on PATH, in PATH order, uncached.
267
+ *
268
+ * Callers that probe candidates (e.g. the Python eval backend) use this so a broken
269
+ * first hit — a version-manager shim with no version selected, a Windows Store alias —
270
+ * does not hide a working binary further down PATH. On macOS the Xcode/CLT toolchain
271
+ * match is appended last, mirroring {@link $which}'s fallback.
272
+ *
273
+ * @param command - Binary name to resolve
274
+ * @param options - `PATH` (defaults to `process.env.PATH`) and `cwd` for relative entries
275
+ * @returns Distinct filesystem paths; empty when nothing matches
276
+ */
277
+ export function $whichAll(command: string, options?: Bun.WhichOptions): string[] {
278
+ const searchPath = options?.PATH ?? process.env.PATH ?? "";
279
+ const matches: string[] = [];
280
+ for (const dir of searchPath.split(path.delimiter)) {
281
+ if (!dir) continue;
282
+ const match = Bun.which(command, { cwd: options?.cwd, PATH: dir });
283
+ if (match && !matches.includes(match)) matches.push(match);
284
+ }
285
+ if (os.platform() === "darwin" && isXcodeBin(command)) {
286
+ const toolchainMatch = getMacosToolPaths().get(command);
287
+ if (toolchainMatch && !matches.includes(toolchainMatch)) matches.push(toolchainMatch);
288
+ }
289
+ return matches;
290
+ }