@linxiraos/pi-utils 1.0.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 (244) hide show
  1. package/CHANGELOG.md +449 -0
  2. package/README.md +36 -0
  3. package/dist/types/abortable.d.ts +32 -0
  4. package/dist/types/acp/connection.d.ts +118 -0
  5. package/dist/types/acp/protocol.d.ts +526 -0
  6. package/dist/types/acp/schema.d.ts +41 -0
  7. package/dist/types/acp/stream.d.ts +8 -0
  8. package/dist/types/acp/transport.d.ts +81 -0
  9. package/dist/types/acp.d.ts +6 -0
  10. package/dist/types/async.d.ts +20 -0
  11. package/dist/types/binary.d.ts +17 -0
  12. package/dist/types/browsers.d.ts +68 -0
  13. package/dist/types/chalk.d.ts +125 -0
  14. package/dist/types/cli.d.ts +134 -0
  15. package/dist/types/color.d.ts +102 -0
  16. package/dist/types/dates.d.ts +7 -0
  17. package/dist/types/dirs.d.ts +282 -0
  18. package/dist/types/docx/converter.d.ts +46 -0
  19. package/dist/types/docx/xml.d.ts +26 -0
  20. package/dist/types/docx/zip.d.ts +6 -0
  21. package/dist/types/docx.d.ts +11 -0
  22. package/dist/types/dom/core.d.ts +431 -0
  23. package/dist/types/dom/parser.d.ts +7 -0
  24. package/dist/types/dom/selector.d.ts +5 -0
  25. package/dist/types/dom.d.ts +5 -0
  26. package/dist/types/env.d.ts +124 -0
  27. package/dist/types/fetch-retry.d.ts +96 -0
  28. package/dist/types/file-lock.d.ts +23 -0
  29. package/dist/types/format.d.ts +37 -0
  30. package/dist/types/frontmatter.d.ts +46 -0
  31. package/dist/types/fs-error.d.ts +31 -0
  32. package/dist/types/glob.d.ts +28 -0
  33. package/dist/types/headers.d.ts +34 -0
  34. package/dist/types/index.d.ts +39 -0
  35. package/dist/types/json-parse.d.ts +74 -0
  36. package/dist/types/json.d.ts +14 -0
  37. package/dist/types/logger/rotating-file.d.ts +18 -0
  38. package/dist/types/logger.d.ts +96 -0
  39. package/dist/types/loop-phase.d.ts +10 -0
  40. package/dist/types/lru.d.ts +46 -0
  41. package/dist/types/marked/core.d.ts +445 -0
  42. package/dist/types/marked.d.ts +2 -0
  43. package/dist/types/mermaid-ascii.d.ts +11 -0
  44. package/dist/types/mime.d.ts +29 -0
  45. package/dist/types/module-timer.d.ts +1 -0
  46. package/dist/types/path-tree.d.ts +76 -0
  47. package/dist/types/path.d.ts +2 -0
  48. package/dist/types/peek-file.d.ts +29 -0
  49. package/dist/types/postmortem.d.ts +104 -0
  50. package/dist/types/process-name.d.ts +7 -0
  51. package/dist/types/procmgr.d.ts +74 -0
  52. package/dist/types/prompt.d.ts +18 -0
  53. package/dist/types/ptree.d.ts +110 -0
  54. package/dist/types/readability/readability.d.ts +9 -0
  55. package/dist/types/readability/readerable.d.ts +10 -0
  56. package/dist/types/readability/types.d.ts +70 -0
  57. package/dist/types/readability.d.ts +4 -0
  58. package/dist/types/ring.d.ts +93 -0
  59. package/dist/types/runtime-install.d.ts +78 -0
  60. package/dist/types/sanitize-text.d.ts +29 -0
  61. package/dist/types/snowflake.d.ts +25 -0
  62. package/dist/types/stderr-guard.d.ts +22 -0
  63. package/dist/types/stream.d.ts +67 -0
  64. package/dist/types/tab-spacing.d.ts +24 -0
  65. package/dist/types/temp.d.ts +17 -0
  66. package/dist/types/template.d.ts +62 -0
  67. package/dist/types/timing-buffer.d.ts +22 -0
  68. package/dist/types/tls-fetch.d.ts +37 -0
  69. package/dist/types/turndown/gfm.d.ts +11 -0
  70. package/dist/types/turndown/html.d.ts +5 -0
  71. package/dist/types/turndown/service.d.ts +21 -0
  72. package/dist/types/turndown/types.d.ts +70 -0
  73. package/dist/types/turndown.d.ts +4 -0
  74. package/dist/types/type-guards.d.ts +5 -0
  75. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
  76. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
  77. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
  78. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
  79. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
  80. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
  81. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
  82. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
  83. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
  84. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
  85. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
  86. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
  87. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
  88. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
  89. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
  90. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
  91. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
  92. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
  93. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
  94. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
  95. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
  96. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
  97. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
  98. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
  99. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
  100. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
  101. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
  102. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
  103. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
  104. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
  105. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
  106. package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
  107. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
  108. package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
  109. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
  110. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
  111. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
  112. package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
  113. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
  114. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
  115. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
  116. package/dist/types/version.d.ts +18 -0
  117. package/dist/types/vterm/buffer.d.ts +99 -0
  118. package/dist/types/vterm/terminal.d.ts +44 -0
  119. package/dist/types/vterm.d.ts +8 -0
  120. package/dist/types/which.d.ts +37 -0
  121. package/dist/types/worker-host.d.ts +51 -0
  122. package/dist/types/xml.d.ts +31 -0
  123. package/package.json +59 -0
  124. package/src/abortable.ts +109 -0
  125. package/src/acp/connection.ts +344 -0
  126. package/src/acp/protocol.ts +466 -0
  127. package/src/acp/schema.ts +160 -0
  128. package/src/acp/stream.ts +82 -0
  129. package/src/acp/transport.ts +213 -0
  130. package/src/acp.ts +6 -0
  131. package/src/async.ts +91 -0
  132. package/src/binary.ts +50 -0
  133. package/src/browsers.ts +501 -0
  134. package/src/chalk.ts +312 -0
  135. package/src/cli.ts +529 -0
  136. package/src/color.ts +302 -0
  137. package/src/dates.ts +194 -0
  138. package/src/dirs.ts +1026 -0
  139. package/src/docx/converter.ts +681 -0
  140. package/src/docx/xml.ts +166 -0
  141. package/src/docx/zip.ts +87 -0
  142. package/src/docx.ts +20 -0
  143. package/src/dom/core.ts +1254 -0
  144. package/src/dom/parser.ts +370 -0
  145. package/src/dom/selector.ts +290 -0
  146. package/src/dom.ts +33 -0
  147. package/src/env.ts +400 -0
  148. package/src/fetch-retry.ts +379 -0
  149. package/src/file-lock.ts +69 -0
  150. package/src/format.ts +113 -0
  151. package/src/frontmatter.ts +205 -0
  152. package/src/fs-error.ts +56 -0
  153. package/src/glob.ts +189 -0
  154. package/src/headers.ts +167 -0
  155. package/src/index.ts +59 -0
  156. package/src/json-parse.ts +821 -0
  157. package/src/json.ts +23 -0
  158. package/src/logger/rotating-file.ts +149 -0
  159. package/src/logger.ts +798 -0
  160. package/src/loop-phase.ts +49 -0
  161. package/src/lru.ts +185 -0
  162. package/src/marked/core.ts +1576 -0
  163. package/src/marked.ts +2 -0
  164. package/src/mermaid-ascii.ts +31 -0
  165. package/src/mime.ts +159 -0
  166. package/src/module-timer.ts +148 -0
  167. package/src/path-tree.ts +147 -0
  168. package/src/path.ts +28 -0
  169. package/src/peek-file.ts +188 -0
  170. package/src/postmortem.ts +433 -0
  171. package/src/process-name.ts +57 -0
  172. package/src/procmgr.ts +247 -0
  173. package/src/prompt.ts +545 -0
  174. package/src/ptree.ts +412 -0
  175. package/src/readability/readability.ts +533 -0
  176. package/src/readability/readerable.ts +51 -0
  177. package/src/readability/types.ts +72 -0
  178. package/src/readability.ts +11 -0
  179. package/src/ring.ts +169 -0
  180. package/src/runtime-install.ts +393 -0
  181. package/src/sanitize-text.ts +96 -0
  182. package/src/snowflake.ts +121 -0
  183. package/src/stderr-guard.ts +155 -0
  184. package/src/stream.ts +454 -0
  185. package/src/tab-spacing.ts +336 -0
  186. package/src/temp.ts +134 -0
  187. package/src/template.ts +586 -0
  188. package/src/timing-buffer.ts +47 -0
  189. package/src/tls-fetch.ts +178 -0
  190. package/src/turndown/gfm.ts +106 -0
  191. package/src/turndown/html.ts +257 -0
  192. package/src/turndown/service.ts +334 -0
  193. package/src/turndown/types.ts +81 -0
  194. package/src/turndown.ts +5 -0
  195. package/src/type-guards.ts +17 -0
  196. package/src/vendor/mermaid-ascii/NOTICE +33 -0
  197. package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
  198. package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
  199. package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
  200. package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
  201. package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
  202. package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
  203. package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
  204. package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
  205. package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
  206. package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
  207. package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
  208. package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +277 -0
  209. package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
  210. package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
  211. package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
  212. package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
  213. package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
  214. package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
  215. package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
  216. package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
  217. package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
  218. package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
  219. package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
  220. package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
  221. package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
  222. package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
  223. package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
  224. package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
  225. package/src/vendor/mermaid-ascii/class/types.ts +121 -0
  226. package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
  227. package/src/vendor/mermaid-ascii/er/types.ts +91 -0
  228. package/src/vendor/mermaid-ascii/index.ts +14 -0
  229. package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
  230. package/src/vendor/mermaid-ascii/parser.ts +645 -0
  231. package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
  232. package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
  233. package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
  234. package/src/vendor/mermaid-ascii/types.ts +164 -0
  235. package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
  236. package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
  237. package/src/vendor/mermaid-ascii/xychart/types.ts +150 -0
  238. package/src/version.ts +99 -0
  239. package/src/vterm/buffer.ts +218 -0
  240. package/src/vterm/terminal.ts +773 -0
  241. package/src/vterm.ts +8 -0
  242. package/src/which.ts +234 -0
  243. package/src/worker-host.ts +100 -0
  244. package/src/xml.ts +313 -0
package/src/marked.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Behavior-compatible reimplementation of marked's used surface. */
2
+ export * from "./marked/core";
@@ -0,0 +1,31 @@
1
+ import { type AsciiRenderOptions, renderMermaidASCII } from "./vendor/mermaid-ascii";
2
+
3
+ export type { AsciiRenderOptions as MermaidAsciiRenderOptions };
4
+
5
+ export function renderMermaidAscii(source: string, options?: AsciiRenderOptions): string {
6
+ return renderMermaidASCII(source, options);
7
+ }
8
+
9
+ export function renderMermaidAsciiSafe(source: string, options?: AsciiRenderOptions): string | null {
10
+ try {
11
+ return renderMermaidASCII(source, options);
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Extract mermaid code blocks from markdown text.
19
+ */
20
+ export function extractMermaidBlocks(markdown: string): { source: string; hash: bigint | number }[] {
21
+ const blocks: { source: string; hash: bigint | number }[] = [];
22
+ const regex = /```mermaid\s*\n([\s\S]*?)```/g;
23
+
24
+ for (let match = regex.exec(markdown); match !== null; match = regex.exec(markdown)) {
25
+ const source = match[1].trim();
26
+ const hash = Bun.hash(source);
27
+ blocks.push({ source, hash });
28
+ }
29
+
30
+ return blocks;
31
+ }
package/src/mime.ts ADDED
@@ -0,0 +1,159 @@
1
+ import { peekFile, peekFileSync } from "./peek-file";
2
+
3
+ const DEFAULT_IMAGE_METADATA_HEADER_BYTES = 256 * 1024;
4
+
5
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
6
+ const JPEG_MAGIC = Buffer.from([0xff, 0xd8, 0xff]);
7
+ const WEBP_RIFF_MAGIC = Buffer.from([0x52, 0x49, 0x46, 0x46]);
8
+ const WEBP_MAGIC = Buffer.from([0x57, 0x45, 0x42, 0x50]);
9
+ const PNG_IHDR = Buffer.from("IHDR");
10
+ const GIF87A = Buffer.from("GIF87a");
11
+ const GIF89A = Buffer.from("GIF89a");
12
+ const WEBP_VP8X = Buffer.from("VP8X");
13
+ const WEBP_VP8L = Buffer.from("VP8L");
14
+ const WEBP_VP8 = Buffer.from("VP8 ");
15
+
16
+ export const SUPPORTED_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
17
+
18
+ export type ImageMetadata =
19
+ | { mimeType: "image/png"; width?: number; height?: number; channels?: number; hasAlpha?: boolean }
20
+ | { mimeType: "image/jpeg"; width?: number; height?: number; channels?: number; hasAlpha?: false }
21
+ | { mimeType: "image/gif"; width?: number; height?: number; channels?: 3; hasAlpha?: never }
22
+ | { mimeType: "image/webp"; width?: number; height?: number; channels?: number; hasAlpha?: boolean };
23
+
24
+ function magicEquals(header: Uint8Array, offset: number, magic: Buffer): boolean {
25
+ if (header.length < offset + magic.length) {
26
+ return false;
27
+ }
28
+ return magic.equals(header.subarray(offset, offset + magic.length));
29
+ }
30
+
31
+ function parsePngMetadata(header: Uint8Array): ImageMetadata | null {
32
+ if (!magicEquals(header, 0, PNG_MAGIC)) return null;
33
+ if (!magicEquals(header, 12, PNG_IHDR)) return { mimeType: "image/png" };
34
+ if (header.length < 26) return { mimeType: "image/png" };
35
+
36
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
37
+ const width = view.getUint32(16, false);
38
+ const height = view.getUint32(20, false);
39
+ const colorType = view.getUint8(25);
40
+ if (colorType === 0) return { mimeType: "image/png", width, height, channels: 1, hasAlpha: false };
41
+ if (colorType === 2) return { mimeType: "image/png", width, height, channels: 3, hasAlpha: false };
42
+ if (colorType === 3) return { mimeType: "image/png", width, height, channels: 3 };
43
+ if (colorType === 4) return { mimeType: "image/png", width, height, channels: 2, hasAlpha: true };
44
+ if (colorType === 6) return { mimeType: "image/png", width, height, channels: 4, hasAlpha: true };
45
+ return { mimeType: "image/png", width, height };
46
+ }
47
+
48
+ function parseJpegMetadata(header: Uint8Array): ImageMetadata | null {
49
+ if (!magicEquals(header, 0, JPEG_MAGIC)) return null;
50
+ if (header.length < 4) return { mimeType: "image/jpeg" };
51
+
52
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
53
+ let offset = 2;
54
+ while (offset + 9 < header.length) {
55
+ if (header[offset] !== 0xff) {
56
+ offset += 1;
57
+ continue;
58
+ }
59
+
60
+ let markerOffset = offset + 1;
61
+ while (markerOffset < header.length && header[markerOffset] === 0xff) {
62
+ markerOffset += 1;
63
+ }
64
+ if (markerOffset >= header.length) break;
65
+
66
+ const marker = header[markerOffset];
67
+ const segmentOffset = markerOffset + 1;
68
+ if (marker === 0xd8 || marker === 0xd9 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
69
+ offset = segmentOffset;
70
+ continue;
71
+ }
72
+ if (segmentOffset + 1 >= header.length) break;
73
+
74
+ const segmentLength = view.getUint16(segmentOffset, false);
75
+ if (segmentLength < 2) break;
76
+
77
+ const isStartOfFrame = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
78
+ if (isStartOfFrame) {
79
+ if (segmentOffset + 7 >= header.length) break;
80
+ const height = view.getUint16(segmentOffset + 3, false);
81
+ const width = view.getUint16(segmentOffset + 5, false);
82
+ const channels = header[segmentOffset + 7];
83
+ return {
84
+ mimeType: "image/jpeg",
85
+ width,
86
+ height,
87
+ channels: Number.isFinite(channels) ? channels : undefined,
88
+ hasAlpha: false,
89
+ };
90
+ }
91
+
92
+ offset = segmentOffset + segmentLength;
93
+ }
94
+
95
+ return { mimeType: "image/jpeg" };
96
+ }
97
+
98
+ function parseGifMetadata(header: Uint8Array): ImageMetadata | null {
99
+ if (!magicEquals(header, 0, GIF87A) && !magicEquals(header, 0, GIF89A)) return null;
100
+ if (header.length < 10) return { mimeType: "image/gif" };
101
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
102
+ return {
103
+ mimeType: "image/gif",
104
+ width: view.getUint16(6, true),
105
+ height: view.getUint16(8, true),
106
+ channels: 3,
107
+ };
108
+ }
109
+
110
+ function parseWebpMetadata(header: Uint8Array): ImageMetadata | null {
111
+ if (!magicEquals(header, 0, WEBP_RIFF_MAGIC)) return null;
112
+ if (!magicEquals(header, 8, WEBP_MAGIC)) return null;
113
+ if (header.length < 30) return { mimeType: "image/webp" };
114
+
115
+ if (magicEquals(header, 12, WEBP_VP8X)) {
116
+ const hasAlpha = (header[20] & 0x10) !== 0;
117
+ const width = (header[24] | (header[25] << 8) | (header[26] << 16)) + 1;
118
+ const height = (header[27] | (header[28] << 8) | (header[29] << 16)) + 1;
119
+ return { mimeType: "image/webp", width, height, channels: hasAlpha ? 4 : 3, hasAlpha };
120
+ }
121
+
122
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
123
+ if (magicEquals(header, 12, WEBP_VP8L)) {
124
+ if (header.length < 25) return { mimeType: "image/webp" };
125
+ const bits = view.getUint32(21, true);
126
+ const width = (bits & 0x3fff) + 1;
127
+ const height = ((bits >> 14) & 0x3fff) + 1;
128
+ const hasAlpha = ((bits >> 28) & 0x1) === 1;
129
+ return { mimeType: "image/webp", width, height, channels: hasAlpha ? 4 : 3, hasAlpha };
130
+ }
131
+
132
+ if (magicEquals(header, 12, WEBP_VP8)) {
133
+ const width = view.getUint16(26, true) & 0x3fff;
134
+ const height = view.getUint16(28, true) & 0x3fff;
135
+ return { mimeType: "image/webp", width, height, channels: 3, hasAlpha: false };
136
+ }
137
+
138
+ return { mimeType: "image/webp" };
139
+ }
140
+
141
+ export function parseImageMetadata(header: Uint8Array): ImageMetadata | null {
142
+ return (
143
+ parsePngMetadata(header) ?? parseJpegMetadata(header) ?? parseGifMetadata(header) ?? parseWebpMetadata(header)
144
+ );
145
+ }
146
+
147
+ export function readImageMetadataSync(
148
+ filePath: string,
149
+ maxBytes = DEFAULT_IMAGE_METADATA_HEADER_BYTES,
150
+ ): ImageMetadata | null {
151
+ return peekFileSync(filePath, maxBytes, parseImageMetadata);
152
+ }
153
+
154
+ export function readImageMetadata(
155
+ filePath: string,
156
+ maxBytes = DEFAULT_IMAGE_METADATA_HEADER_BYTES,
157
+ ): Promise<ImageMetadata | null> {
158
+ return peekFile(filePath, maxBytes, parseImageMetadata);
159
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Module-load timing preload.
3
+ *
4
+ * `bun --preload .../module-timer.ts <entry>` installs Bun plugin hooks (only
5
+ * when `PI_TIMING` is set) that record an inclusive module window plus resolved
6
+ * static child edges:
7
+ *
8
+ * onLoad start → appended end marker after the module's top-level body
9
+ *
10
+ * Events are pushed into a process-global buffer that {@link logger.printTimings}
11
+ * drains and renders as a module DAG/tree. Each module row can therefore show
12
+ * both total time and `self` time after subtracting child module intervals.
13
+ *
14
+ * Why a preload (and not a normal import): Bun reads the *entire* statically
15
+ * reachable graph before evaluating any module, so hooks installed from inside
16
+ * that graph cannot observe its own loading — they only catch later dynamically
17
+ * loaded modules. A preload runs first, so it sees the static-import phase that
18
+ * dominates startup.
19
+ *
20
+ * Kept dependency-free on purpose: the sole import is Bun's `plugin`, so this is
21
+ * cheap to preload before pi-utils (and winston) exist. The buffer is shared with
22
+ * the logger via a registry Symbol so neither side needs to import the other.
23
+ *
24
+ * **What is measured:** an inclusive per-module window. `onLoad` stamps the
25
+ * start before reading source; the returned source has a tiny marker appended at
26
+ * the end of the module. That marker runs after Bun parses/transpiles the module
27
+ * and after any top-level await in that module completes, so the duration
28
+ * includes read + parse/transpile + dependency wait + top-level execution/TLA.
29
+ * If a module throws before its final statement, no end marker is recorded.
30
+ *
31
+ * **Tree shape:** `onResolve` observes importer → specifier edges and resolves
32
+ * them with `Bun.resolveSync` without taking over Bun's real resolution. The
33
+ * logger renders these edges as a DAG/tree and computes module `self` time by
34
+ * subtracting the union of child intervals, avoiding misleading flat inclusive
35
+ * totals.
36
+ *
37
+ * **Coverage limits:**
38
+ * - TS/TSX only — intercepting `node_modules` CJS `.js`/`.cjs` and forcing ESM
39
+ * breaks their default-export detection, so they are left to Bun's default path.
40
+ * - **Dev runs only.** In the compiled `omp` binary every module is pre-bundled
41
+ * into bunfs, so `onLoad` never fires; profile with a `bun --preload` dev run.
42
+ */
43
+ import { plugin } from "bun";
44
+ import { moduleLoadBuffer } from "./timing-buffer";
45
+
46
+ // Restrict to TS/TSX only. node_modules ships CommonJS `.js`/`.cjs` that Bun
47
+ // auto-detects when loaded via its default path; if we intercept and return
48
+ // `{ contents, loader: "js" }`, Bun forces ESM and CJS modules fail to load
49
+ // (e.g. `Missing 'default' export`). Our own source tree (where the interesting
50
+ // timing lives) is uniformly TypeScript, so a TS-only filter is both safe and
51
+ // sufficient.
52
+ const MODULE_LOADER_FILTER = /\.[mc]?tsx?$/;
53
+ const MODULE_COMPLETE_KEY: symbol = Symbol.for("omp.moduleLoadComplete");
54
+ const MODULE_BODY_START_KEY: symbol = Symbol.for("omp.moduleBodyStart");
55
+ const STATIC_IMPORT_PATTERN =
56
+ /\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
57
+
58
+ type CompleteStore = Record<symbol, ((path: string) => void) | undefined>;
59
+
60
+ function bodyStartMarker(path: string): string {
61
+ return `;globalThis[Symbol.for("omp.moduleBodyStart")]?.(${JSON.stringify(path)});\n`;
62
+ }
63
+
64
+ function completionMarker(path: string): string {
65
+ return `\n;globalThis[Symbol.for("omp.moduleLoadComplete")]?.(${JSON.stringify(path)});\n`;
66
+ }
67
+
68
+ function instrumentContents(path: string, contents: string): string {
69
+ const start = bodyStartMarker(path);
70
+ const end = completionMarker(path);
71
+ if (!contents.startsWith("#!")) return `${start}${contents}${end}`;
72
+ const newline = contents.indexOf("\n");
73
+ if (newline === -1) return `${contents}\n${start}${end}`;
74
+ return `${contents.slice(0, newline + 1)}${start}${contents.slice(newline + 1)}${end}`;
75
+ }
76
+ function importerDir(importer: string): string {
77
+ const slash = importer.lastIndexOf("/");
78
+ if (slash === -1) return ".";
79
+ return importer.slice(0, slash);
80
+ }
81
+
82
+ function childSetFor(importsByPath: Map<string, Set<string>>, path: string): Set<string> {
83
+ let children = importsByPath.get(path);
84
+ if (!children) {
85
+ children = new Set<string>();
86
+ importsByPath.set(path, children);
87
+ }
88
+ return children;
89
+ }
90
+
91
+ function addImportEdges(importsByPath: Map<string, Set<string>>, importer: string, contents: string): void {
92
+ STATIC_IMPORT_PATTERN.lastIndex = 0;
93
+ for (const match of contents.matchAll(STATIC_IMPORT_PATTERN)) {
94
+ const specifier = match[1] ?? match[2];
95
+ if (!specifier) continue;
96
+ try {
97
+ const resolved = Bun.resolveSync(specifier, importerDir(importer));
98
+ if (MODULE_LOADER_FILTER.test(resolved) && resolved !== importer) {
99
+ childSetFor(importsByPath, importer).add(resolved);
100
+ }
101
+ } catch {
102
+ // Leave Bun's real resolver/runtime to surface any error. This scanner is only an observer.
103
+ }
104
+ }
105
+ }
106
+
107
+ if (process.env.PI_TIMING) {
108
+ const buffer = moduleLoadBuffer();
109
+ const starts = new Map<string, number>();
110
+ const bodyStarts = new Map<string, number>();
111
+ const importsByPath = new Map<string, Set<string>>();
112
+ const store = globalThis as unknown as CompleteStore;
113
+ store[MODULE_BODY_START_KEY] = (path: string): void => {
114
+ bodyStarts.set(path, performance.now());
115
+ };
116
+ store[MODULE_COMPLETE_KEY] = (path: string): void => {
117
+ const start = starts.get(path);
118
+ if (start === undefined) return;
119
+ starts.delete(path);
120
+ const end = performance.now();
121
+ const bodyStart = bodyStarts.get(path);
122
+ bodyStarts.delete(path);
123
+ const imports = importsByPath.get(path);
124
+ buffer.push({
125
+ path,
126
+ start,
127
+ durationMs: end - start,
128
+ bodyMs: bodyStart === undefined ? undefined : end - bodyStart,
129
+ imports: imports ? [...imports] : [],
130
+ });
131
+ };
132
+
133
+ plugin({
134
+ name: "pi-module-load-timer",
135
+ setup(build) {
136
+ build.onLoad({ filter: MODULE_LOADER_FILTER }, async args => {
137
+ starts.set(args.path, performance.now());
138
+ childSetFor(importsByPath, args.path);
139
+ const contents = await Bun.file(args.path).text();
140
+ addImportEdges(importsByPath, args.path, contents);
141
+ return {
142
+ contents: instrumentContents(args.path, contents),
143
+ loader: args.path.endsWith(".tsx") ? "tsx" : "ts",
144
+ };
145
+ });
146
+ },
147
+ });
148
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Multi-level path tree shared by grouped file listings (find / grep / ast tools)
3
+ * and compaction file-operation lists.
4
+ *
5
+ * Flat path lists used to group by the *immediate* parent directory and print the
6
+ * full directory path in every header. For results spread across a deep tree — or
7
+ * rooted outside cwd, where paths stay absolute — that repeated the shared prefix
8
+ * on every line. The tree below folds single-child directory chains (so the common
9
+ * prefix collapses into one header) and nests the rest, charging the model one
10
+ * token per path segment instead of one per file.
11
+ */
12
+
13
+ const URL_LIKE_PATH_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
14
+
15
+ /** True for `scheme://…` entries that have no meaningful directory structure. */
16
+ export function isUrlLikePath(filePath: string): boolean {
17
+ return URL_LIKE_PATH_RE.test(filePath);
18
+ }
19
+
20
+ export interface PathTreeNode {
21
+ /** Direct file leaves, in first-seen order. */
22
+ files: Array<{ name: string; key: string }>;
23
+ /** Dedup set for `files` (a glob can surface the same path twice on retry). */
24
+ fileNames: Set<string>;
25
+ /** Child directories, in first-seen order. */
26
+ subdirs: Array<{ name: string; node: PathTreeNode }>;
27
+ /** Dedup index for `subdirs`. */
28
+ dirIndex: Map<string, PathTreeNode>;
29
+ }
30
+
31
+ export interface PathTreeInput {
32
+ /** Path string; absolute, cwd-relative, or url-like. Backslashes are normalized. */
33
+ path: string;
34
+ /** Whether the leaf itself is a directory (trailing-slash match from find). */
35
+ isDir: boolean;
36
+ /** Opaque key carried onto file events for section lookup. Defaults to `path`. */
37
+ key?: string;
38
+ }
39
+
40
+ /** One node emitted while walking the tree: a folded directory or a file leaf. */
41
+ export interface GroupedTreeEvent {
42
+ kind: "dir" | "file";
43
+ /** 0-based nesting depth (root children are depth 0). */
44
+ depth: number;
45
+ /** Folded chain for dirs (e.g. `a/b/c`, no trailing slash); basename for files. */
46
+ name: string;
47
+ /** File key for `kind === "file"`; empty string for directories. */
48
+ key: string;
49
+ }
50
+
51
+ function createNode(): PathTreeNode {
52
+ return { files: [], fileNames: new Set(), subdirs: [], dirIndex: new Map() };
53
+ }
54
+
55
+ function addFile(node: PathTreeNode, name: string, key: string): void {
56
+ if (node.fileNames.has(name)) return;
57
+ node.fileNames.add(name);
58
+ node.files.push({ name, key });
59
+ }
60
+
61
+ /**
62
+ * Build a directory tree from a flat list of paths. URL-like entries are kept
63
+ * whole as root-level file leaves (they have no meaningful directory structure).
64
+ * Absolute paths carry a leading empty segment so they share a common `/` root
65
+ * and fold like any other prefix.
66
+ */
67
+ export function buildPathTree(entries: Iterable<PathTreeInput>): PathTreeNode {
68
+ const root = createNode();
69
+ for (const { path: rawPath, isDir, key } of entries) {
70
+ const normalized = rawPath.replace(/\\/g, "/");
71
+ const fileKey = key ?? rawPath;
72
+ if (isUrlLikePath(normalized)) {
73
+ addFile(root, normalized, fileKey);
74
+ continue;
75
+ }
76
+ const trimmed = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
77
+ if (trimmed.length === 0) continue;
78
+ const segments = trimmed.split("/");
79
+ const dirCount = isDir ? segments.length : segments.length - 1;
80
+ let node = root;
81
+ for (let i = 0; i < dirCount; i++) {
82
+ const segment = segments[i]!;
83
+ let child = node.dirIndex.get(segment);
84
+ if (!child) {
85
+ child = createNode();
86
+ node.dirIndex.set(segment, child);
87
+ node.subdirs.push({ name: segment, node: child });
88
+ }
89
+ node = child;
90
+ }
91
+ if (!isDir) {
92
+ addFile(node, segments[segments.length - 1]!, fileKey);
93
+ }
94
+ }
95
+ return root;
96
+ }
97
+
98
+ /**
99
+ * Depth-first walk yielding directory and file events. Directories collapse their
100
+ * single-child chains (`a` → `a/b` → `a/b/c`) so a shared prefix becomes one
101
+ * header. Each node's direct files are emitted before its subdirectories, keeping
102
+ * a file unambiguously attached to the header above it.
103
+ */
104
+ export function* walkPathTree(node: PathTreeNode, depth = 0): Generator<GroupedTreeEvent> {
105
+ for (const file of node.files) {
106
+ yield { kind: "file", depth, name: file.name, key: file.key };
107
+ }
108
+ for (const subdir of node.subdirs) {
109
+ let dirNode = subdir.node;
110
+ const parts = [subdir.name];
111
+ while (dirNode.files.length === 0 && dirNode.subdirs.length === 1) {
112
+ const only = dirNode.subdirs[0]!;
113
+ parts.push(only.name);
114
+ dirNode = only.node;
115
+ }
116
+ yield { kind: "dir", depth, name: parts.join("/"), key: "" };
117
+ yield* walkPathTree(dirNode, depth + 1);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Render a flat path list as a grouped, prefix-folded directory tree without
123
+ * per-file bodies (find-tool output shape, also used by compaction `<files>`
124
+ * lists). Single-child directory chains fold into one header (`# a/b/c/`),
125
+ * each level adds one `#`, and files are listed bare under the deepest
126
+ * directory header that owns them. Trailing-slash entries are directory
127
+ * leaves and keep their slash in the header.
128
+ *
129
+ * `annotate` receives each file's full original path and its return value is
130
+ * appended verbatim to the file line (e.g. ` (RW)`).
131
+ *
132
+ * Order follows the input: a directory appears when its first member is
133
+ * emitted, and a node's own files precede its subdirectories.
134
+ */
135
+ export function formatGroupedPaths(paths: readonly string[], annotate?: (path: string) => string): string {
136
+ if (paths.length === 0) return "";
137
+ const tree = buildPathTree(paths.map(entry => ({ path: entry, isDir: entry.endsWith("/") })));
138
+ const lines: string[] = [];
139
+ for (const event of walkPathTree(tree)) {
140
+ if (event.kind === "dir") {
141
+ lines.push(`${"#".repeat(event.depth + 1)} ${event.name}/`);
142
+ } else {
143
+ lines.push(annotate ? `${event.name}${annotate(event.key)}` : event.name);
144
+ }
145
+ }
146
+ return lines.join("\n");
147
+ }
package/src/path.ts ADDED
@@ -0,0 +1,28 @@
1
+ const WINDOWS_DRIVE_EXTENDED_PREFIX = /^\\\\[?]\\([A-Za-z]:[\\/].*)$/;
2
+ const WINDOWS_UNC_EXTENDED_PREFIX = /^\\\\[?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
3
+ const WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/([A-Za-z]:\/.*)$/;
4
+ const WINDOWS_UNC_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/UNC\/([^/]+)\/(.+)$/i;
5
+ const WINDOWS_DRIVE_NT_PREFIX = /^\\\\[?][?]\\([A-Za-z]:[\\/].*)$/;
6
+ const WINDOWS_UNC_NT_PREFIX = /^\\\\[?][?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
7
+
8
+ /** Removes Win32 extended-length prefixes before passing paths to Bun APIs. */
9
+ export function stripWindowsExtendedLengthPathPrefix(
10
+ filePath: string,
11
+ platform: NodeJS.Platform = process.platform,
12
+ ): string {
13
+ if (platform !== "win32") return filePath;
14
+
15
+ const uncMatch = WINDOWS_UNC_EXTENDED_PREFIX.exec(filePath) ?? WINDOWS_UNC_NT_PREFIX.exec(filePath);
16
+ if (uncMatch) return `\\\\${uncMatch[1]}\\${uncMatch[2]}`;
17
+
18
+ const driveMatch = WINDOWS_DRIVE_EXTENDED_PREFIX.exec(filePath) ?? WINDOWS_DRIVE_NT_PREFIX.exec(filePath);
19
+ if (driveMatch) return driveMatch[1];
20
+
21
+ const forwardUncMatch = WINDOWS_UNC_EXTENDED_FORWARD_PREFIX.exec(filePath);
22
+ if (forwardUncMatch) return `//${forwardUncMatch[1]}/${forwardUncMatch[2]}`;
23
+
24
+ const forwardDriveMatch = WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX.exec(filePath);
25
+ if (forwardDriveMatch) return forwardDriveMatch[1];
26
+
27
+ return filePath;
28
+ }