@oh-my-pi-zen/pi-utils 16.3.6-zen.1

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 (164) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +36 -0
  3. package/dist/types/abortable.d.ts +32 -0
  4. package/dist/types/async.d.ts +6 -0
  5. package/dist/types/binary.d.ts +17 -0
  6. package/dist/types/cli.d.ts +117 -0
  7. package/dist/types/color.d.ts +102 -0
  8. package/dist/types/dirs.d.ts +260 -0
  9. package/dist/types/env.d.ts +74 -0
  10. package/dist/types/fetch-retry.d.ts +94 -0
  11. package/dist/types/format.d.ts +37 -0
  12. package/dist/types/frontmatter.d.ts +25 -0
  13. package/dist/types/fs-error.d.ts +31 -0
  14. package/dist/types/glob.d.ts +28 -0
  15. package/dist/types/index.d.ts +35 -0
  16. package/dist/types/json-parse.d.ts +48 -0
  17. package/dist/types/json.d.ts +4 -0
  18. package/dist/types/logger.d.ts +83 -0
  19. package/dist/types/loop-phase.d.ts +10 -0
  20. package/dist/types/mermaid-ascii.d.ts +11 -0
  21. package/dist/types/mime.d.ts +29 -0
  22. package/dist/types/module-timer.d.ts +1 -0
  23. package/dist/types/path-tree.d.ts +76 -0
  24. package/dist/types/path.d.ts +2 -0
  25. package/dist/types/peek-file.d.ts +29 -0
  26. package/dist/types/postmortem.d.ts +37 -0
  27. package/dist/types/procmgr.d.ts +29 -0
  28. package/dist/types/prompt.d.ts +18 -0
  29. package/dist/types/ptree.d.ts +108 -0
  30. package/dist/types/ring.d.ts +93 -0
  31. package/dist/types/runtime-install.d.ts +68 -0
  32. package/dist/types/sanitize-text.d.ts +29 -0
  33. package/dist/types/snowflake.d.ts +25 -0
  34. package/dist/types/stream.d.ts +68 -0
  35. package/dist/types/tab-spacing.d.ts +24 -0
  36. package/dist/types/temp.d.ts +17 -0
  37. package/dist/types/timing-buffer.d.ts +22 -0
  38. package/dist/types/tls-fetch.d.ts +37 -0
  39. package/dist/types/type-guards.d.ts +3 -0
  40. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
  41. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
  42. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
  43. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
  44. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
  45. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
  46. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
  47. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
  48. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
  49. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
  50. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
  51. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
  52. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
  53. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
  54. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
  55. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
  56. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
  57. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
  58. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
  59. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
  60. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
  61. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
  62. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
  63. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
  64. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
  65. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
  66. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
  67. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
  68. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
  69. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
  70. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
  71. package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
  72. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
  73. package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
  74. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
  75. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
  76. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
  77. package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
  78. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
  79. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
  80. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
  81. package/dist/types/which.d.ts +37 -0
  82. package/dist/types/worker-host.d.ts +47 -0
  83. package/package.json +62 -0
  84. package/src/abortable.ts +109 -0
  85. package/src/async.ts +50 -0
  86. package/src/binary.ts +50 -0
  87. package/src/cli.ts +457 -0
  88. package/src/color.ts +302 -0
  89. package/src/dirs.ts +925 -0
  90. package/src/env.ts +226 -0
  91. package/src/fetch-retry.ts +355 -0
  92. package/src/format.ts +113 -0
  93. package/src/frontmatter.ts +162 -0
  94. package/src/fs-error.ts +56 -0
  95. package/src/glob.ts +189 -0
  96. package/src/index.ts +55 -0
  97. package/src/json-parse.ts +612 -0
  98. package/src/json.ts +10 -0
  99. package/src/logger.ts +672 -0
  100. package/src/loop-phase.ts +49 -0
  101. package/src/mermaid-ascii.ts +31 -0
  102. package/src/mime.ts +159 -0
  103. package/src/module-timer.ts +148 -0
  104. package/src/path-tree.ts +147 -0
  105. package/src/path.ts +28 -0
  106. package/src/peek-file.ts +188 -0
  107. package/src/postmortem.ts +222 -0
  108. package/src/procmgr.ts +195 -0
  109. package/src/prompt.ts +562 -0
  110. package/src/ptree.ts +395 -0
  111. package/src/ring.ts +169 -0
  112. package/src/runtime-install.ts +372 -0
  113. package/src/sanitize-text.ts +96 -0
  114. package/src/snowflake.ts +121 -0
  115. package/src/stream.ts +432 -0
  116. package/src/tab-spacing.ts +336 -0
  117. package/src/temp.ts +134 -0
  118. package/src/timing-buffer.ts +47 -0
  119. package/src/tls-fetch.ts +178 -0
  120. package/src/type-guards.ts +11 -0
  121. package/src/vendor/mermaid-ascii/NOTICE +33 -0
  122. package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
  123. package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
  124. package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
  125. package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
  126. package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
  127. package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
  128. package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
  129. package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
  130. package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
  131. package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
  132. package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
  133. package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +215 -0
  134. package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
  135. package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
  136. package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
  137. package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
  138. package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
  139. package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
  140. package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
  141. package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
  142. package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
  143. package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
  144. package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
  145. package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
  146. package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
  147. package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
  148. package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
  149. package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
  150. package/src/vendor/mermaid-ascii/class/types.ts +121 -0
  151. package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
  152. package/src/vendor/mermaid-ascii/er/types.ts +91 -0
  153. package/src/vendor/mermaid-ascii/index.ts +14 -0
  154. package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
  155. package/src/vendor/mermaid-ascii/parser.ts +645 -0
  156. package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
  157. package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
  158. package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
  159. package/src/vendor/mermaid-ascii/types.ts +164 -0
  160. package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
  161. package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
  162. package/src/vendor/mermaid-ascii/xychart/types.ts +150 -0
  163. package/src/which.ts +232 -0
  164. package/src/worker-host.ts +92 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Default tab width (display / tab expansion) and per-file width from `.editorconfig`.
3
+ * Mirrors former `pi-natives` `indent` + `text` default-tab-width behavior (no N-API).
4
+ */
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import { isFsError } from "./fs-error";
8
+
9
+ export const MIN_TAB_WIDTH = 1;
10
+ export const MAX_TAB_WIDTH = 16;
11
+ export const DEFAULT_TAB_WIDTH = 3;
12
+
13
+ /**
14
+ * Per-component path length cap on common filesystems (`NAME_MAX = 255` on
15
+ * Linux ext4 / macOS APFS / Windows NTFS). Paths with components longer than
16
+ * this cannot be opened at all, so editorconfig discovery short-circuits to
17
+ * the default instead of running into `ENAMETOOLONG` from `readFileSync`.
18
+ */
19
+ const NAME_MAX_BYTES = 255;
20
+
21
+ const EDITORCONFIG_NAME = ".editorconfig";
22
+
23
+ const editorConfigCache = new Map<string, ParsedEditorConfig | null>();
24
+ const editorConfigChainCache = new Map<string, ChainEntry[]>();
25
+
26
+ interface EditorConfigSection {
27
+ pattern: string;
28
+ properties: Map<string, string>;
29
+ }
30
+
31
+ interface ParsedEditorConfig {
32
+ root: boolean;
33
+ sections: EditorConfigSection[];
34
+ }
35
+
36
+ interface ChainEntry {
37
+ dir: string;
38
+ parsed: ParsedEditorConfig;
39
+ }
40
+
41
+ const enum IndentStyle {
42
+ Space,
43
+ Tab,
44
+ }
45
+
46
+ type IndentSize = { kind: "spaces"; n: number } | { kind: "tab" };
47
+
48
+ interface EditorConfigMatch {
49
+ indentStyle?: IndentStyle;
50
+ indentSize?: IndentSize;
51
+ tabWidth?: number;
52
+ }
53
+
54
+ function clampTabWidth(value: number): number {
55
+ return Math.min(MAX_TAB_WIDTH, Math.max(MIN_TAB_WIDTH, Math.trunc(value)));
56
+ }
57
+
58
+ function parsePositiveInteger(raw: string | undefined): number | undefined {
59
+ if (raw === undefined) return undefined;
60
+ if (!/^\d+$/.test(raw)) return undefined;
61
+ const parsed = Number.parseInt(raw, 10);
62
+ if (parsed === 0) return undefined;
63
+ return clampTabWidth(parsed);
64
+ }
65
+
66
+ function fixUnclosedBraces(pattern: string): string {
67
+ const opens = [...pattern].filter(c => c === "{").length;
68
+ const closes = [...pattern].filter(c => c === "}").length;
69
+ if (opens > closes) {
70
+ return pattern + "}".repeat(opens - closes);
71
+ }
72
+ return pattern;
73
+ }
74
+
75
+ /** Match `crates/pi-natives/src/glob_util.rs` `build_glob_pattern`. */
76
+ function buildGlobPattern(globStr: string, recursive: boolean): string {
77
+ const normalized = globStr.replace(/\\/g, "/");
78
+ const pattern =
79
+ !recursive || normalized.includes("/") || normalized.startsWith("**") ? normalized : `**/${normalized}`;
80
+ return fixUnclosedBraces(pattern);
81
+ }
82
+
83
+ function globMatches(pattern: string, relativePath: string): boolean {
84
+ try {
85
+ const g = new Bun.Glob(pattern);
86
+ return g.match(relativePath);
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ function matchesEditorConfigPattern(pattern: string, relativePath: string): boolean {
93
+ const normalized = pattern.replace(/^\/+/, "");
94
+ if (!normalized) {
95
+ return false;
96
+ }
97
+
98
+ const candidates = normalized.includes("/")
99
+ ? [buildGlobPattern(normalized, false)]
100
+ : [buildGlobPattern(normalized, false), buildGlobPattern(normalized, true)];
101
+
102
+ for (const p of candidates) {
103
+ if (globMatches(p, relativePath)) {
104
+ return true;
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+
110
+ function parseEditorConfigFile(content: string): ParsedEditorConfig {
111
+ const parsed: ParsedEditorConfig = { root: false, sections: [] };
112
+ let currentSectionIdx: number | undefined;
113
+
114
+ for (const rawLine of content.split(/\n/)) {
115
+ const line = rawLine.trim();
116
+ if (line === "") continue;
117
+ if (line.startsWith("#") || line.startsWith(";")) continue;
118
+
119
+ if (line.startsWith("[") && line.endsWith("]") && line.length >= 2) {
120
+ const secPattern = line.slice(1, -1).trim();
121
+ if (secPattern === "") {
122
+ currentSectionIdx = undefined;
123
+ continue;
124
+ }
125
+ parsed.sections.push({ pattern: secPattern, properties: new Map() });
126
+ currentSectionIdx = parsed.sections.length - 1;
127
+ continue;
128
+ }
129
+
130
+ const eq = line.indexOf("=");
131
+ if (eq === -1) continue;
132
+ const key = line.slice(0, eq).trim().toLowerCase();
133
+ const value = line
134
+ .slice(eq + 1)
135
+ .trim()
136
+ .toLowerCase();
137
+ if (key === "") continue;
138
+
139
+ if (currentSectionIdx !== undefined) {
140
+ parsed.sections[currentSectionIdx]!.properties.set(key, value);
141
+ } else if (key === "root") {
142
+ parsed.root = value === "true";
143
+ }
144
+ }
145
+
146
+ return parsed;
147
+ }
148
+
149
+ function parseCachedEditorConfig(configPath: string): ParsedEditorConfig | undefined {
150
+ const key = path.resolve(configPath);
151
+ const hit = editorConfigCache.get(key);
152
+ if (hit !== undefined) {
153
+ return hit ?? undefined;
154
+ }
155
+
156
+ let content: string;
157
+ try {
158
+ content = fs.readFileSync(key, "utf8");
159
+ } catch (err) {
160
+ // editorconfig discovery is best-effort. Any filesystem error
161
+ // (`ENOENT`, `ENAMETOOLONG`, `ENOTDIR`, `EACCES`, `ELOOP`, `EINVAL`,
162
+ // …) means "no usable config at this path" — never a fatal condition
163
+ // for callers like the edit renderer that hand us arbitrary strings.
164
+ if (isFsError(err)) return undefined;
165
+ throw err;
166
+ }
167
+ const parsed = parseEditorConfigFile(content);
168
+ editorConfigCache.set(key, parsed);
169
+ return parsed;
170
+ }
171
+
172
+ function resolveFilePath(projectDir: string, file: string): string {
173
+ if (path.isAbsolute(file)) {
174
+ return path.normalize(path.resolve(file));
175
+ }
176
+ return path.normalize(path.resolve(projectDir, file));
177
+ }
178
+
179
+ /** Like `pathdiff::diff_paths` + forward slashes (see `indent.rs`). */
180
+ function relativePathUnified(baseDir: string, absoluteFile: string): string {
181
+ const base = path.resolve(baseDir);
182
+ const file = path.resolve(absoluteFile);
183
+ const rel = path.relative(base, file);
184
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
185
+ return ".";
186
+ }
187
+ return rel.replace(/\\/g, "/");
188
+ }
189
+
190
+ function collectEditorConfigChain(startDir: string): ChainEntry[] {
191
+ const key = path.resolve(startDir);
192
+ const cached = editorConfigChainCache.get(key);
193
+ if (cached !== undefined) {
194
+ return cached;
195
+ }
196
+
197
+ const chain: ChainEntry[] = [];
198
+ let cursor = key;
199
+ for (;;) {
200
+ const configPath = path.join(cursor, EDITORCONFIG_NAME);
201
+ const parsed = parseCachedEditorConfig(configPath);
202
+ if (parsed !== undefined) {
203
+ chain.push({ dir: cursor, parsed });
204
+ if (parsed.root) {
205
+ break;
206
+ }
207
+ }
208
+
209
+ const parent = path.dirname(cursor);
210
+ if (parent === cursor) {
211
+ break;
212
+ }
213
+ cursor = parent;
214
+ }
215
+
216
+ chain.reverse();
217
+ editorConfigChainCache.set(key, chain);
218
+ return chain;
219
+ }
220
+
221
+ function resolveEditorConfigMatch(absoluteFile: string): EditorConfigMatch | undefined {
222
+ const fileDir = path.dirname(absoluteFile);
223
+ const chain = collectEditorConfigChain(fileDir);
224
+ if (chain.length === 0) {
225
+ return undefined;
226
+ }
227
+
228
+ const match: EditorConfigMatch = {};
229
+ for (const { dir, parsed } of chain) {
230
+ const relativePath = relativePathUnified(dir, absoluteFile);
231
+ for (const section of parsed.sections) {
232
+ if (!matchesEditorConfigPattern(section.pattern, relativePath)) {
233
+ continue;
234
+ }
235
+
236
+ const style = section.properties.get("indent_style");
237
+ if (style === "space") {
238
+ match.indentStyle = IndentStyle.Space;
239
+ } else if (style === "tab") {
240
+ match.indentStyle = IndentStyle.Tab;
241
+ }
242
+
243
+ const rawSize = section.properties.get("indent_size");
244
+ if (rawSize === "tab") {
245
+ match.indentSize = { kind: "tab" };
246
+ } else if (rawSize !== undefined) {
247
+ const n = parsePositiveInteger(rawSize);
248
+ if (n !== undefined) {
249
+ match.indentSize = { kind: "spaces", n };
250
+ }
251
+ }
252
+
253
+ const tw = parsePositiveInteger(section.properties.get("tab_width"));
254
+ if (tw !== undefined) {
255
+ match.tabWidth = tw;
256
+ }
257
+ }
258
+ }
259
+
260
+ if (match.indentStyle === undefined && match.indentSize === undefined && match.tabWidth === undefined) {
261
+ return undefined;
262
+ }
263
+ return match;
264
+ }
265
+
266
+ function hasOverlongPathComponent(filePath: string): boolean {
267
+ for (const part of filePath.split(/[\\/]/)) {
268
+ if (part.length > 0 && Buffer.byteLength(part) > NAME_MAX_BYTES) {
269
+ return true;
270
+ }
271
+ }
272
+ return false;
273
+ }
274
+
275
+ /**
276
+ * `.editorconfig`-derived formatting options for an LSP `textDocument/formatting` request.
277
+ *
278
+ * Both fields are absent when the resolved `.editorconfig` chain does not pin them, so callers
279
+ * can layer their own fallbacks (content sniffing, project defaults) underneath. Returned values
280
+ * are clamped to {@link MIN_TAB_WIDTH}..{@link MAX_TAB_WIDTH}.
281
+ */
282
+ export interface EditorConfigFormatting {
283
+ /** Effective indent width in columns, from `indent_size` or `tab_width`. */
284
+ tabSize?: number;
285
+ /** `true` for `indent_style = space`, `false` for `indent_style = tab` (or `indent_size = tab`). */
286
+ insertSpaces?: boolean;
287
+ }
288
+
289
+ /**
290
+ * Resolve `.editorconfig` formatting hints for `file` without falling back to any default.
291
+ *
292
+ * Used by the LSP format-on-write path so a missing `.editorconfig` declaration falls through
293
+ * to caller-provided defaults instead of clobbering the file with the renderer's
294
+ * display tab width (issue #2329).
295
+ */
296
+ export function getEditorConfigFormatting(file?: string | null, projectDir?: string | null): EditorConfigFormatting {
297
+ if (file === undefined || file === null || file === "") {
298
+ return {};
299
+ }
300
+
301
+ const cwd = projectDir ?? process.cwd();
302
+ const absoluteFile = resolveFilePath(cwd, file);
303
+
304
+ // NAME_MAX guard: editorconfig discovery is
305
+ // best-effort and must never escape as `ENAMETOOLONG` from a renderer's
306
+ // stray gibberish path.
307
+ if (hasOverlongPathComponent(absoluteFile)) {
308
+ return {};
309
+ }
310
+
311
+ const match = resolveEditorConfigMatch(absoluteFile);
312
+ if (match === undefined) {
313
+ return {};
314
+ }
315
+
316
+ const result: EditorConfigFormatting = {};
317
+
318
+ if (match.indentSize?.kind === "spaces") {
319
+ result.tabSize = clampTabWidth(match.indentSize.n);
320
+ } else if (match.tabWidth !== undefined) {
321
+ result.tabSize = clampTabWidth(match.tabWidth);
322
+ }
323
+
324
+ if (match.indentStyle === IndentStyle.Space) {
325
+ result.insertSpaces = true;
326
+ } else if (match.indentStyle === IndentStyle.Tab || match.indentSize?.kind === "tab") {
327
+ result.insertSpaces = false;
328
+ } else if (match.indentSize?.kind === "spaces") {
329
+ // `indent_size = <n>` without an explicit `indent_style` is universally
330
+ // read as "indent with N spaces" — both VSCode and Sublime infer
331
+ // `indent_style = space` in that case.
332
+ result.insertSpaces = true;
333
+ }
334
+
335
+ return result;
336
+ }
package/src/temp.ts ADDED
@@ -0,0 +1,134 @@
1
+ import * as fs from "node:fs";
2
+ import * as fsPromises from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+
6
+ export class TempDir {
7
+ #path: string;
8
+ private constructor(path: string) {
9
+ this.#path = path;
10
+ }
11
+
12
+ static createSync(prefix?: string): TempDir {
13
+ return new TempDir(fs.mkdtempSync(normalizePrefix(prefix)));
14
+ }
15
+
16
+ static async create(prefix?: string): Promise<TempDir> {
17
+ return new TempDir(await fsPromises.mkdtemp(normalizePrefix(prefix)));
18
+ }
19
+
20
+ #removePromise: Promise<void> | null = null;
21
+
22
+ path(): string {
23
+ return this.#path;
24
+ }
25
+
26
+ absolute(): string {
27
+ return path.resolve(this.#path);
28
+ }
29
+
30
+ remove(): Promise<void> {
31
+ if (this.#removePromise) {
32
+ return this.#removePromise;
33
+ }
34
+ const removePromise = removeWithRetries(this.#path);
35
+ this.#removePromise = removePromise;
36
+ return removePromise;
37
+ }
38
+
39
+ removeSync(): void {
40
+ removeSyncWithRetries(this.#path);
41
+ this.#removePromise = Promise.resolve();
42
+ }
43
+
44
+ toString(): string {
45
+ return this.#path;
46
+ }
47
+
48
+ join(...paths: string[]): string {
49
+ return path.join(this.#path, ...paths);
50
+ }
51
+
52
+ async [Symbol.asyncDispose](): Promise<void> {
53
+ try {
54
+ await this.remove();
55
+ } catch {
56
+ // Ignore cleanup errors
57
+ }
58
+ }
59
+
60
+ [Symbol.dispose](): void {
61
+ try {
62
+ this.removeSync();
63
+ } catch {
64
+ // Ignore cleanup errors
65
+ }
66
+ }
67
+ }
68
+
69
+ const kTempDir = os.tmpdir();
70
+
71
+ function normalizePrefix(prefix?: string): string {
72
+ if (!prefix) {
73
+ return `${kTempDir}${path.sep}pi-temp-`;
74
+ } else if (prefix.startsWith("@")) {
75
+ return path.join(kTempDir, prefix.slice(1));
76
+ }
77
+ return prefix;
78
+ }
79
+
80
+ const kRemoveOptions = { recursive: true, force: true } as const;
81
+ const kRemoveRetries = 40;
82
+ // 50ms × 40 retries = 2s total retry window. Windows holds file locks on
83
+ // SQLite DBs for up to ~1.5s after close(); the previous 25ms (1s total)
84
+ // was too short for some test cleanup scenarios.
85
+ const kRemoveRetryDelayMs = 50;
86
+ const kRetryableRemoveErrorCodes = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]);
87
+ const kSleepBuffer = new Int32Array(new SharedArrayBuffer(4));
88
+
89
+ /** Removes a path recursively, retrying transient Windows deletion failures. */
90
+ export async function removeWithRetries(target: string): Promise<void> {
91
+ for (let attempt = 0; ; attempt++) {
92
+ try {
93
+ await fsPromises.rm(target, kRemoveOptions);
94
+ return;
95
+ } catch (err) {
96
+ if (!shouldRetryRemove(err, attempt)) throw err;
97
+ await Bun.sleep(kRemoveRetryDelayMs);
98
+ }
99
+ }
100
+ }
101
+
102
+ export function removeSyncWithRetries(target: string): void {
103
+ for (let attempt = 0; ; attempt++) {
104
+ try {
105
+ fs.rmSync(target, kRemoveOptions);
106
+ return;
107
+ } catch (err) {
108
+ if (!shouldRetryRemove(err, attempt)) throw err;
109
+ sleepSync(kRemoveRetryDelayMs);
110
+ }
111
+ }
112
+ }
113
+
114
+ function shouldRetryRemove(err: unknown, attempt: number): boolean {
115
+ return attempt < kRemoveRetries && process.platform === "win32" && isRetryableRemoveError(err);
116
+ }
117
+
118
+ function isRetryableRemoveError(err: unknown): boolean {
119
+ return (
120
+ typeof err === "object" &&
121
+ err !== null &&
122
+ "code" in err &&
123
+ typeof err.code === "string" &&
124
+ kRetryableRemoveErrorCodes.has(err.code)
125
+ );
126
+ }
127
+
128
+ function sleepSync(ms: number): void {
129
+ if ("sleepSync" in Bun && typeof Bun.sleepSync === "function") {
130
+ Bun.sleepSync(ms);
131
+ return;
132
+ }
133
+ Atomics.wait(kSleepBuffer, 0, 0, ms);
134
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared contract between the {@link module-timer} preload and {@link logger}'s
3
+ * timing tree. Kept in its own dependency-free module so the preload can import
4
+ * it without pulling in winston (via logger) and the logger can drain the buffer
5
+ * without importing the Bun-plugin preload.
6
+ */
7
+
8
+ export interface ModuleLoadEvent {
9
+ /** Absolute or Bun-resolved module path. */
10
+ path: string;
11
+ /** `performance.now()` timestamp captured at Bun `onLoad` entry. */
12
+ start: number;
13
+ /** Inclusive module window: `onLoad` entry → appended final marker. */
14
+ durationMs: number;
15
+ /** Own top-level body / TLA time: prepended body marker → appended final marker. */
16
+ bodyMs?: number;
17
+ /** Resolved static children imported by this module. */
18
+ imports: string[];
19
+ }
20
+
21
+ /**
22
+ * Registry-global key under which the preload accumulates module-load events.
23
+ * `Symbol.for` so both modules resolve the same symbol independently.
24
+ */
25
+ const KEY: symbol = Symbol.for("omp.moduleLoadBuffer");
26
+
27
+ type Store = Record<symbol, ModuleLoadEvent[] | undefined>;
28
+
29
+ /** The append-only buffer the preload pushes into (created on first access). */
30
+ export function moduleLoadBuffer(): ModuleLoadEvent[] {
31
+ const store = globalThis as unknown as Store;
32
+ let buffer = store[KEY];
33
+ if (!buffer) {
34
+ buffer = [];
35
+ store[KEY] = buffer;
36
+ }
37
+ return buffer;
38
+ }
39
+
40
+ /** Drain and return all buffered events, leaving the buffer empty. */
41
+ export function drainModuleLoadEvents(): ModuleLoadEvent[] {
42
+ const store = globalThis as unknown as Store;
43
+ const buffer = store[KEY];
44
+ if (!buffer || buffer.length === 0) return [];
45
+ store[KEY] = [];
46
+ return buffer;
47
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * `NODE_EXTRA_CA_CERTS` shim for Bun's `fetch`.
3
+ *
4
+ * Node's TLS layer honours `NODE_EXTRA_CA_CERTS` natively, but Bun's
5
+ * `fetch` does not, and both the provider streams (`openai-responses`,
6
+ * `openai-completions`, `openai-codex-responses`, `ollama-chat`, ...) and
7
+ * catalog model discovery (`/models` probes) route every request through
8
+ * Bun's runtime. Without this wrapper, corporate relays and private
9
+ * gateways behind a custom CA bundle fail with
10
+ * `unknown certificate verification error` even when the env var is set.
11
+ *
12
+ * The wrapper merges the resolved CA bundle into Bun's `RequestInit.tls.ca`.
13
+ * Bun's `tls.ca` REPLACES the default trust store when set, so the wrapper
14
+ * always seeds {@link tls.rootCertificates} when the caller has not already
15
+ * curated their own CA list.
16
+ */
17
+ import * as fs from "node:fs";
18
+ import * as tls from "node:tls";
19
+ import { $env } from "./env";
20
+ import { isEnoent } from "./fs-error";
21
+
22
+ /**
23
+ * `fetch`-compatible function. Accepts any callable matching the standard
24
+ * fetch signature; `preconnect` is optional because non-Bun runtimes
25
+ * (browsers, test mocks) won't expose it.
26
+ */
27
+ export type FetchImpl = ((input: string | URL | Request, init?: RequestInit) => Promise<Response>) & {
28
+ preconnect?: typeof globalThis.fetch.preconnect;
29
+ };
30
+
31
+ /**
32
+ * `NODE_EXTRA_CA_CERTS` was set but unusable (path does not exist). This is
33
+ * a config/contract error, not a transient transport fault — it is never
34
+ * retried.
35
+ */
36
+ export class ExtraCaError extends Error {
37
+ constructor(message: string, options?: { cause?: unknown }) {
38
+ super(message, options?.cause === undefined ? undefined : { cause: options.cause });
39
+ this.name = "ExtraCaError";
40
+ }
41
+ }
42
+
43
+ /** Bun extension to `RequestInit` for the TLS options we touch. */
44
+ type BunTlsOptions = {
45
+ ca?: string | string[];
46
+ cert?: string;
47
+ key?: string;
48
+ rejectUnauthorized?: boolean;
49
+ serverName?: string;
50
+ ciphers?: string;
51
+ };
52
+
53
+ type BunTlsRequestInit = RequestInit & { tls?: BunTlsOptions };
54
+
55
+ const EXTRA_CA_FETCH_MARKER = Symbol("omp.extraCaFetch");
56
+ type ExtraCaFetch = FetchImpl & { [EXTRA_CA_FETCH_MARKER]?: true };
57
+
58
+ /**
59
+ * Cached resolution of `NODE_EXTRA_CA_CERTS`. Keyed on the env value plus
60
+ * the file mtime for path values so on-disk cert rotation (short-lived
61
+ * corporate bundles) invalidates the cache instead of pinning the first
62
+ * read forever.
63
+ */
64
+ let cacheKey: string | undefined;
65
+ let cacheValue: string | undefined;
66
+
67
+ /**
68
+ * Returns the PEM bytes referenced by `NODE_EXTRA_CA_CERTS`, or `undefined`
69
+ * when the env var is unset/empty.
70
+ *
71
+ * Accepts the same shapes Node accepts plus an inline-PEM escape hatch:
72
+ * - Inline PEM (`-----BEGIN CERTIFICATE-----...`). Literal `\n` escapes in
73
+ * the env value are expanded so callers can ship single-line PEMs through
74
+ * shell exports.
75
+ * - File path. Anything that does not contain a PEM header is treated as a
76
+ * path, matching Node's "extensionless filename is still a path" contract.
77
+ * `ENOENT` becomes {@link ExtraCaError}; other I/O errors bubble.
78
+ */
79
+ function resolveExtraCa(): string | undefined {
80
+ const raw = $env.NODE_EXTRA_CA_CERTS?.trim();
81
+ if (!raw) return undefined;
82
+
83
+ let key: string;
84
+ if (raw.includes("-----BEGIN")) {
85
+ key = raw;
86
+ } else {
87
+ try {
88
+ key = `${raw}@${fs.statSync(raw).mtimeMs}`;
89
+ } catch {
90
+ key = raw;
91
+ }
92
+ }
93
+ if (key === cacheKey) return cacheValue;
94
+
95
+ if (raw.includes("-----BEGIN")) {
96
+ cacheValue = raw.replace(/\\n/g, "\n");
97
+ } else {
98
+ try {
99
+ cacheValue = fs.readFileSync(raw, "utf8");
100
+ } catch (error) {
101
+ if (isEnoent(error)) {
102
+ throw new ExtraCaError(`NODE_EXTRA_CA_CERTS path does not exist: ${raw}`);
103
+ }
104
+ throw error;
105
+ }
106
+ }
107
+ cacheKey = key;
108
+ return cacheValue;
109
+ }
110
+
111
+ /** Test seam: drop the cached PEM so a follow-up call re-reads the env. */
112
+ export function __resetExtraCaCache(): void {
113
+ cacheKey = undefined;
114
+ cacheValue = undefined;
115
+ }
116
+
117
+ /**
118
+ * Merge `extraCa` into `init.tls.ca`. When the caller has not supplied a CA
119
+ * list, the system root store is included alongside the extra bundle —
120
+ * Bun's `tls.ca` replaces the default trust store, so omitting roots would
121
+ * break every public host. When the caller already curated a list (e.g.
122
+ * Anthropic Foundry's mTLS options, which already seed
123
+ * `tls.rootCertificates`), only the extra CA is appended.
124
+ */
125
+ function withExtraCaInit(init: RequestInit | undefined, extraCa: string): RequestInit {
126
+ const existingTls = (init as BunTlsRequestInit | undefined)?.tls;
127
+ const existingCa = existingTls?.ca;
128
+ let mergedCa: string[];
129
+ if (existingCa === undefined) {
130
+ mergedCa = [...tls.rootCertificates, extraCa];
131
+ } else if (Array.isArray(existingCa)) {
132
+ mergedCa = [...existingCa, extraCa];
133
+ } else {
134
+ mergedCa = [existingCa, extraCa];
135
+ }
136
+ return { ...init, tls: { ...existingTls, ca: mergedCa } } as RequestInit;
137
+ }
138
+
139
+ /**
140
+ * Wrap `fetchImpl` so every call honours `NODE_EXTRA_CA_CERTS`. Idempotent:
141
+ * a fetch already wrapped is returned unchanged so repeated composition
142
+ * (Anthropic auth-retry replays, request-debug fan-out) never stacks
143
+ * wrappers. When the env var is unset the original fetch is returned, so
144
+ * default deployments pay nothing.
145
+ */
146
+ export function wrapFetchForExtraCa(fetchImpl: FetchImpl): FetchImpl {
147
+ const maybeWrapped = fetchImpl as ExtraCaFetch;
148
+ if (maybeWrapped[EXTRA_CA_FETCH_MARKER]) return fetchImpl;
149
+ // Peek once at construction — if the env var is unset, skip the wrapper
150
+ // entirely so the hot fetch path stays a single function call. The env
151
+ // is evaluated again per request below to catch in-process updates from
152
+ // tests (`__resetExtraCaCache` + env mutation).
153
+ if (!$env.NODE_EXTRA_CA_CERTS?.trim()) return fetchImpl;
154
+
155
+ const wrapped = Object.assign(
156
+ async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
157
+ const extraCa = resolveExtraCa();
158
+ return extraCa ? fetchImpl(input, withExtraCaInit(init, extraCa)) : fetchImpl(input, init);
159
+ },
160
+ fetchImpl.preconnect ? { preconnect: fetchImpl.preconnect } : {},
161
+ { [EXTRA_CA_FETCH_MARKER]: true as const },
162
+ );
163
+ return wrapped;
164
+ }
165
+
166
+ /**
167
+ * Convenience for options-bag composition (e.g. the stream-entry path in
168
+ * `@oh-my-pi-zen/pi-ai`'s `stream.ts`, which mirrors `withRequestDebugFetch` so
169
+ * the proxy/debug/extra-CA wrappers compose uniformly). No-op when the env
170
+ * var is unset.
171
+ */
172
+ export function withExtraCaFetch<T extends { fetch?: FetchImpl } | undefined>(options: T): T {
173
+ if (!$env.NODE_EXTRA_CA_CERTS?.trim()) return options;
174
+ const fetchImpl = options?.fetch ?? (globalThis.fetch as FetchImpl);
175
+ const wrapped = wrapFetchForExtraCa(fetchImpl);
176
+ if (wrapped === fetchImpl && options?.fetch !== undefined) return options;
177
+ return { ...(options ?? {}), fetch: wrapped } as T;
178
+ }
@@ -0,0 +1,11 @@
1
+ export function isRecord(value: unknown): value is Record<string, unknown> {
2
+ return !!value && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+
5
+ export function asRecord(value: unknown): Record<string, unknown> | null {
6
+ return isRecord(value) ? value : null;
7
+ }
8
+
9
+ export function toError(value: unknown): Error {
10
+ return value instanceof Error ? value : new Error(String(value));
11
+ }