@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.
- package/CHANGELOG.md +245 -0
- package/README.md +36 -0
- package/dist/types/abortable.d.ts +32 -0
- package/dist/types/async.d.ts +6 -0
- package/dist/types/binary.d.ts +17 -0
- package/dist/types/cli.d.ts +117 -0
- package/dist/types/color.d.ts +102 -0
- package/dist/types/dirs.d.ts +260 -0
- package/dist/types/env.d.ts +74 -0
- package/dist/types/fetch-retry.d.ts +94 -0
- package/dist/types/format.d.ts +37 -0
- package/dist/types/frontmatter.d.ts +25 -0
- package/dist/types/fs-error.d.ts +31 -0
- package/dist/types/glob.d.ts +28 -0
- package/dist/types/index.d.ts +35 -0
- package/dist/types/json-parse.d.ts +48 -0
- package/dist/types/json.d.ts +4 -0
- package/dist/types/logger.d.ts +83 -0
- package/dist/types/loop-phase.d.ts +10 -0
- package/dist/types/mermaid-ascii.d.ts +11 -0
- package/dist/types/mime.d.ts +29 -0
- package/dist/types/module-timer.d.ts +1 -0
- package/dist/types/path-tree.d.ts +76 -0
- package/dist/types/path.d.ts +2 -0
- package/dist/types/peek-file.d.ts +29 -0
- package/dist/types/postmortem.d.ts +37 -0
- package/dist/types/procmgr.d.ts +29 -0
- package/dist/types/prompt.d.ts +18 -0
- package/dist/types/ptree.d.ts +108 -0
- package/dist/types/ring.d.ts +93 -0
- package/dist/types/runtime-install.d.ts +68 -0
- package/dist/types/sanitize-text.d.ts +29 -0
- package/dist/types/snowflake.d.ts +25 -0
- package/dist/types/stream.d.ts +68 -0
- package/dist/types/tab-spacing.d.ts +24 -0
- package/dist/types/temp.d.ts +17 -0
- package/dist/types/timing-buffer.d.ts +22 -0
- package/dist/types/tls-fetch.d.ts +37 -0
- package/dist/types/type-guards.d.ts +3 -0
- package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
- package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
- package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
- package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
- package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
- package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
- package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
- package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
- package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
- package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
- package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
- package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
- package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
- package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
- package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
- package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
- package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
- package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
- package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
- package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
- package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
- package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
- package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
- package/dist/types/which.d.ts +37 -0
- package/dist/types/worker-host.d.ts +47 -0
- package/package.json +62 -0
- package/src/abortable.ts +109 -0
- package/src/async.ts +50 -0
- package/src/binary.ts +50 -0
- package/src/cli.ts +457 -0
- package/src/color.ts +302 -0
- package/src/dirs.ts +925 -0
- package/src/env.ts +226 -0
- package/src/fetch-retry.ts +355 -0
- package/src/format.ts +113 -0
- package/src/frontmatter.ts +162 -0
- package/src/fs-error.ts +56 -0
- package/src/glob.ts +189 -0
- package/src/index.ts +55 -0
- package/src/json-parse.ts +612 -0
- package/src/json.ts +10 -0
- package/src/logger.ts +672 -0
- package/src/loop-phase.ts +49 -0
- package/src/mermaid-ascii.ts +31 -0
- package/src/mime.ts +159 -0
- package/src/module-timer.ts +148 -0
- package/src/path-tree.ts +147 -0
- package/src/path.ts +28 -0
- package/src/peek-file.ts +188 -0
- package/src/postmortem.ts +222 -0
- package/src/procmgr.ts +195 -0
- package/src/prompt.ts +562 -0
- package/src/ptree.ts +395 -0
- package/src/ring.ts +169 -0
- package/src/runtime-install.ts +372 -0
- package/src/sanitize-text.ts +96 -0
- package/src/snowflake.ts +121 -0
- package/src/stream.ts +432 -0
- package/src/tab-spacing.ts +336 -0
- package/src/temp.ts +134 -0
- package/src/timing-buffer.ts +47 -0
- package/src/tls-fetch.ts +178 -0
- package/src/type-guards.ts +11 -0
- package/src/vendor/mermaid-ascii/NOTICE +33 -0
- package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
- package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
- package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
- package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
- package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
- package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
- package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
- package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
- package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
- package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
- package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
- package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +215 -0
- package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
- package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
- package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
- package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
- package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
- package/src/vendor/mermaid-ascii/class/types.ts +121 -0
- package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
- package/src/vendor/mermaid-ascii/er/types.ts +91 -0
- package/src/vendor/mermaid-ascii/index.ts +14 -0
- package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
- package/src/vendor/mermaid-ascii/parser.ts +645 -0
- package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
- package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
- package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
- package/src/vendor/mermaid-ascii/types.ts +164 -0
- package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
- package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
- package/src/vendor/mermaid-ascii/xychart/types.ts +150 -0
- package/src/which.ts +232 -0
- 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
|
+
}
|
package/src/tls-fetch.ts
ADDED
|
@@ -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
|
+
}
|