@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/env.ts ADDED
@@ -0,0 +1,400 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { getAgentDir, getConfigRootDir, refreshDirsFromEnv } from "./dirs";
5
+
6
+ export * from "./worker-host";
7
+
8
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
9
+
10
+ /**
11
+ * Strict shell-identifier shape. Used for dotenv keys we accept into
12
+ * `Bun.env` — those should be referenceable as `$NAME` from POSIX shells,
13
+ * so we reject anything outside `[A-Za-z_][A-Za-z0-9_]*`.
14
+ */
15
+ export function isValidEnvName(name: string): boolean {
16
+ return ENV_NAME_RE.test(name);
17
+ }
18
+
19
+ /**
20
+ * The only names that are genuinely unsafe to forward to a native `execve`
21
+ * spawn: empty, containing `=` (would corrupt the `KEY=VALUE` framing) or
22
+ * NUL (terminates the C string mid-entry). Windows ships standard variables
23
+ * whose names contain parentheses (e.g. `ProgramFiles(x86)`, `CommonProgramFiles(x86)`)
24
+ * — those MUST survive the scrub so downstream resolvers (Git Bash discovery
25
+ * in `procmgr.ts`, etc.) can still read them.
26
+ */
27
+ export function isSafeEnvName(name: string): boolean {
28
+ return name.length > 0 && !name.includes("=") && !name.includes("\0");
29
+ }
30
+
31
+ export function isSafeEnvValue(value: string): boolean {
32
+ return !value.includes("\0");
33
+ }
34
+
35
+ export function isMacosMallocStackLoggingEnvName(name: string): boolean {
36
+ return name === "MallocStackLogging" || name === "MallocStackLoggingNoCompact";
37
+ }
38
+
39
+ export function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string> {
40
+ const result: Record<string, string> = {};
41
+ for (const key in env) {
42
+ const value = env[key];
43
+ if (
44
+ !isSafeEnvName(key) ||
45
+ isMacosMallocStackLoggingEnvName(key) ||
46
+ value === undefined ||
47
+ !isSafeEnvValue(value)
48
+ ) {
49
+ continue;
50
+ }
51
+ result[key] = value;
52
+ }
53
+ return result;
54
+ }
55
+ // Bun autoloads the project's dotenv files into `process.env` before user code
56
+ // runs — including inside `bun build --compile` binaries — so a snapshot of
57
+ // `Bun.env` is only pre-dotenv when autoloading was explicitly disabled. Linux
58
+ // keeps the original exec environment in procfs, which is authoritative.
59
+ function readLaunchEnv(): ReadonlyMap<string, string> | undefined {
60
+ if (process.platform === "linux") {
61
+ try {
62
+ const values = new Map<string, string>();
63
+ for (const entry of fs.readFileSync("/proc/self/environ", "utf8").split("\0")) {
64
+ const separator = entry.indexOf("=");
65
+ if (separator > 0) values.set(entry.slice(0, separator), entry.slice(separator + 1));
66
+ }
67
+ return values;
68
+ } catch {}
69
+ }
70
+ if (!process.execArgv.includes("--no-env-file")) return undefined;
71
+ const values = new Map<string, string>();
72
+ for (const key in Bun.env) {
73
+ const value = Bun.env[key];
74
+ if (value !== undefined) values.set(key, value);
75
+ }
76
+ return values;
77
+ }
78
+
79
+ const launchEnvValues = readLaunchEnv();
80
+ const projectEnvNamesLoadedByOmp = new Set<string>();
81
+
82
+ function expandDotenvValues(values: Record<string, string>, env: Record<string, string>): Record<string, string> {
83
+ const expanded: Record<string, string> = {};
84
+ for (const key in values) {
85
+ expanded[key] = values[key].replace(
86
+ /(\\)?\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g,
87
+ (match, escaped: string | undefined, braced: string | undefined, bare: string | undefined) => {
88
+ if (escaped) return match.slice(1);
89
+ const name = braced ?? bare;
90
+ if (!name) return match;
91
+ return env[name] ?? expanded[name] ?? "";
92
+ },
93
+ );
94
+ }
95
+ return expanded;
96
+ }
97
+
98
+ /** Filters process env for child shells without launch-cwd dotenv values. */
99
+ export function filterChildShellEnv(
100
+ env: Record<string, string | undefined>,
101
+ cwd: string = process.cwd(),
102
+ ): Record<string, string> {
103
+ const result = filterProcessEnv(env);
104
+ const projectEnv = parseEnvFile(path.join(cwd, ".env"));
105
+ const nodeEnvName = `.env.${env.NODE_ENV || "development"}`;
106
+ const modeEnv = parseEnvFile(path.join(cwd, nodeEnvName));
107
+ const localEnv = parseEnvFile(path.join(cwd, ".env.local"));
108
+ const launchEnv = { ...projectEnv, ...modeEnv, ...localEnv };
109
+ const expandedLaunchEnv = {
110
+ ...expandDotenvValues(projectEnv, result),
111
+ ...expandDotenvValues(modeEnv, result),
112
+ ...expandDotenvValues(localEnv, result),
113
+ };
114
+ for (const key in launchEnv) {
115
+ const launchValue = launchEnvValues?.get(key);
116
+ if (launchValue !== undefined) {
117
+ // Launcher-owned name: it keeps the launcher's own value. Bun overwrites
118
+ // an empty launcher value with the dotenv one, so restore the launcher
119
+ // value whenever what survived is exactly what the dotenv file defines.
120
+ if (
121
+ result[key] !== launchValue &&
122
+ (result[key] === launchEnv[key] || result[key] === expandedLaunchEnv[key])
123
+ ) {
124
+ result[key] = launchValue;
125
+ }
126
+ continue;
127
+ }
128
+ if (launchEnvValues || projectEnvNamesLoadedByOmp.has(key)) {
129
+ // Strong provenance: the launch environment is known and this name is
130
+ // absent from it, or OMP itself injected the value — either way it came
131
+ // from a project dotenv file, not the parent shell.
132
+ delete result[key];
133
+ } else if (result[key] === launchEnv[key] || result[key] === expandedLaunchEnv[key]) {
134
+ // No launch-env snapshot (dotenv autoloaded without procfs): best-effort
135
+ // value match against the Bun-parsed dotenv.
136
+ delete result[key];
137
+ }
138
+ }
139
+ return result;
140
+ }
141
+
142
+ /**
143
+ * Parse one dotenv line with Bun-compatible semantics: an optional `export`
144
+ * prefix, full-line `#` comments, inline `#` comments after whitespace on
145
+ * unquoted values, and single/double/backtick quoting (a `#` inside quotes
146
+ * stays literal). Returns undefined for blank lines, comments, and malformed
147
+ * names.
148
+ */
149
+ function parseEnvLine(line: string): { key: string; value: string } | undefined {
150
+ const trimmed = line.trim();
151
+ if (!trimmed || trimmed.startsWith("#")) return undefined;
152
+ const eqIndex = trimmed.indexOf("=");
153
+ if (eqIndex === -1) return undefined;
154
+ let key = trimmed.slice(0, eqIndex).trim();
155
+ const exported = key.match(/^export[ \t]+(.*)$/);
156
+ if (exported) key = exported[1].trim();
157
+ if (!isValidEnvName(key)) return undefined;
158
+ const raw = trimmed.slice(eqIndex + 1).replace(/^[ \t]+/, "");
159
+ const quote = raw[0];
160
+ if (quote === '"' || quote === "'" || quote === "`") {
161
+ let close = raw.indexOf(quote, 1);
162
+ while (close !== -1 && raw[close - 1] === "\\") close = raw.indexOf(quote, close + 1);
163
+ return { key, value: close === -1 ? raw.slice(1) : raw.slice(1, close) };
164
+ }
165
+ const commentIndex = raw.search(/[ \t]#/);
166
+ return { key, value: (commentIndex === -1 ? raw : raw.slice(0, commentIndex)).trimEnd() };
167
+ }
168
+
169
+ /**
170
+ * Parses a .env file synchronously into key-value string pairs using
171
+ * {@link parseEnvLine} for Bun-compatible line semantics, then mirrors valid
172
+ * `OMP_` variables to their `PI_` aliases.
173
+ */
174
+ export function parseEnvFile(filePath: string): Record<string, string> {
175
+ const result: Record<string, string> = {};
176
+ try {
177
+ const content = fs.readFileSync(filePath, "utf-8");
178
+ for (const line of content.split("\n")) {
179
+ const parsed = parseEnvLine(line);
180
+ if (parsed && isSafeEnvValue(parsed.value)) result[parsed.key] = parsed.value;
181
+ }
182
+ } catch {
183
+ // File doesn't exist or can't be read - return empty result
184
+ }
185
+
186
+ // OMP_ overrides PI_
187
+ for (const k in result) {
188
+ if (k.startsWith("OMP_")) {
189
+ result[`PI_${k.slice(4)}`] = result[k];
190
+ }
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ // Eagerly parse the user's $HOME/.env and the current project's .env (from cwd)
197
+ const homeEnv = parseEnvFile(path.join(os.homedir(), ".env"));
198
+ const piEnv = parseEnvFile(path.join(getConfigRootDir(), ".env"));
199
+ const agentEnv = parseEnvFile(path.join(getAgentDir(), ".env"));
200
+ const projectEnv = parseEnvFile(path.join(process.cwd(), ".env"));
201
+
202
+ for (const key of Object.keys(Bun.env)) {
203
+ const value = Bun.env[key];
204
+ if (!isSafeEnvName(key) || isMacosMallocStackLoggingEnvName(key) || value === undefined || !isSafeEnvValue(value)) {
205
+ delete Bun.env[key];
206
+ }
207
+ }
208
+
209
+ for (const file of [projectEnv, agentEnv, piEnv, homeEnv]) {
210
+ for (const key in file) {
211
+ if (!isMacosMallocStackLoggingEnvName(key) && !Bun.env[key]) {
212
+ Bun.env[key] = file[key];
213
+ if (file === projectEnv) projectEnvNamesLoadedByOmp.add(key);
214
+ }
215
+ }
216
+ }
217
+
218
+ // Directory-affecting keys (XDG_*_HOME, and in default mode PI_CODING_AGENT_DIR)
219
+ // may have just arrived from the profile/agent `.env` applied above. The dirs
220
+ // resolver cached its paths at module load — before this file ran — so rebuild
221
+ // it now from the updated env. `getAgentDir()` already located the `.env` from
222
+ // the profile name + home, so this re-reads only the directory vars.
223
+ refreshDirsFromEnv();
224
+
225
+ /**
226
+ * Intentional re-export of Bun.env.
227
+ *
228
+ * All users should import this env module (import { $env } from "@linxiraos/pi-utils")
229
+ * before using environment variables. This ensures that .env files have been loaded and
230
+ * overrides (project, home) have been applied, so $env always reflects the correct values.
231
+ */
232
+ export const $env: Record<string, string> = Bun.env as Record<string, string>;
233
+
234
+ /**
235
+ * Resolve the first environment variable value from the given keys.
236
+ * @param keys - The keys to resolve.
237
+ * @returns The first environment variable value, or undefined if no value is found.
238
+ */
239
+ export function $pickenv(...keys: string[]): string | undefined {
240
+ for (const key of keys) {
241
+ const value = Bun.env[key]?.trim();
242
+ if (value) {
243
+ return value;
244
+ }
245
+ }
246
+ return undefined;
247
+ }
248
+
249
+ /**
250
+ * Read an environment variable by its EXACT, case-sensitive name.
251
+ *
252
+ * `process.env` / `Bun.env` lookups are case-insensitive on Windows (Node backs
253
+ * them with `uv_os_getenv`, Bun with a `CaseInsensitiveASCIIStringArrayHashMap`),
254
+ * so a lowercase literal like `public` silently resolves to a differently-cased
255
+ * system variable — Windows ships `PUBLIC=C:\Users\Public`. Enumerated keys are
256
+ * the only signal that preserves the real casing, so this trusts the lookup only
257
+ * when a key with identical casing is actually present. On POSIX (case-sensitive
258
+ * env) it is equivalent to a direct lookup.
259
+ *
260
+ * Use this instead of `process.env[name] ?? literal` wherever `name` may be a
261
+ * user-supplied literal (e.g. a stored API key) rather than a genuine env-var
262
+ * reference — otherwise the literal gets hijacked by a same-named system var.
263
+ *
264
+ * @param name - Environment variable name to look up.
265
+ * @param env - Environment source; defaults to `process.env`.
266
+ */
267
+ export function $envExact(name: string, env: Record<string, string | undefined> = process.env): string | undefined {
268
+ const value = env[name];
269
+ if (value === undefined) return undefined;
270
+ // Enumeration preserves real key casing on Windows, unlike the getter; the
271
+ // value is trusted only when an exact-case entry actually exists.
272
+ for (const key in env) {
273
+ if (key === name) return value;
274
+ }
275
+ return undefined;
276
+ }
277
+
278
+ /**
279
+ * Parses a positive decimal integer from `$env[name]`.
280
+ * Empty, invalid, NaN, zero, or negative values return `defaultValue`.
281
+ */
282
+ export function $envpos(name: string, defaultValue: number): number {
283
+ const raw = $env[name];
284
+ if (!raw) return defaultValue;
285
+ const parsed = Number.parseInt(raw, 10);
286
+ if (Number.isNaN(parsed) || parsed <= 0) return defaultValue;
287
+ return parsed;
288
+ }
289
+
290
+ const BUN_TEST_ENTRY_PATTERN = /[._](?:test|spec)\.[cm]?[jt]sx?$/;
291
+
292
+ /** True when the process is an explicitly marked test child or Bun is running a test entrypoint. */
293
+ export function isBunTestRuntime(): boolean {
294
+ if (Bun.env.PI_TEST_RUNTIME === "1") return true;
295
+ const hasTestEnvironment = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
296
+ return hasTestEnvironment && BUN_TEST_ENTRY_PATTERN.test(Bun.main);
297
+ }
298
+
299
+ let terminalHeadless = isBunTestRuntime();
300
+
301
+ /**
302
+ * True when real-terminal side effects must be suppressed: stdout escape/frame
303
+ * writes, stdin raw-mode + resume, CSI/OSC capability probes, SIGWINCH, window
304
+ * title changes, and emergency restore. Defaults to {@link isBunTestRuntime} so
305
+ * `bun test` launched inside a real TTY never paints the TUI, leaks probe
306
+ * queries, or hijacks the developer's stdin; production runtimes stay
307
+ * interactive.
308
+ *
309
+ * Terminal-contract tests that must exercise the real I/O path opt out with
310
+ * `setTerminalHeadless(false)` and restore it afterwards.
311
+ */
312
+ export function isTerminalHeadless(): boolean {
313
+ return terminalHeadless;
314
+ }
315
+
316
+ /**
317
+ * Override the {@link isTerminalHeadless} default and return the previous value
318
+ * so callers can restore exact prior state (`const prev = setTerminalHeadless(false); … setTerminalHeadless(prev);`).
319
+ */
320
+ export function setTerminalHeadless(headless: boolean): boolean {
321
+ const previous = terminalHeadless;
322
+ terminalHeadless = headless;
323
+ return previous;
324
+ }
325
+
326
+ let interactiveHost = false;
327
+
328
+ /**
329
+ * True when this process runs an interactive coding-agent host — the only
330
+ * context where the operator can browse the Agent Hub and focus a live
331
+ * subagent's session (`SessionFocusController`), so a subagent's session title
332
+ * can become operator-visible. Off by default (print/RPC/ACP/eval/SDK/`bun
333
+ * test` never render a focusable session tree); the interactive entrypoint
334
+ * flips it on with {@link setInteractiveHost}.
335
+ */
336
+ export function isInteractiveHost(): boolean {
337
+ return interactiveHost;
338
+ }
339
+
340
+ /**
341
+ * Set the interactive-host flag and return the previous value so callers can
342
+ * restore exact prior state. See {@link isInteractiveHost}.
343
+ */
344
+ export function setInteractiveHost(interactive: boolean): boolean {
345
+ const previous = interactiveHost;
346
+ interactiveHost = interactive;
347
+ return previous;
348
+ }
349
+
350
+ /**
351
+ * SQLite `busy_timeout` for the session-critical databases (agent.db,
352
+ * history.db, stats.db).
353
+ *
354
+ * Interactive hosts tolerate a longer synchronous wait on lock contention
355
+ * (SQLITE_BUSY during WAL recovery/checkpoint — see oh-my-pi#2421): the
356
+ * operator sees a brief freeze and the statement eventually completes.
357
+ * Headless hosts (print/RPC/ACP/eval/SDK) run a protocol on the same thread —
358
+ * a multi-second synchronous busy-wait freezes their event loop and stalls
359
+ * every in-flight frame with no liveness signal, so they use a short timeout
360
+ * and rely on the existing asynchronous open/retry paths to recover from
361
+ * contention instead of blocking.
362
+ */
363
+ export function getDbBusyTimeoutMs(): number {
364
+ return isInteractiveHost() ? 5000 : 1000;
365
+ }
366
+
367
+ /**
368
+ * True when this code is running inside a `bun build --compile` standalone
369
+ * binary. Detects via the embedded virtual-filesystem path markers
370
+ * (`$bunfs`, `~BUN`, or its URL-encoded form `%7EBUN`) in `import.meta.url`,
371
+ * which Bun rewrites for every module bundled into the executable. The
372
+ * `PI_COMPILED` env var (set by the build script's `--define`) is checked
373
+ * first for cheap fast-path detection.
374
+ */
375
+ export function isCompiledBinary(): boolean {
376
+ if (process.env.PI_COMPILED || Bun.env.PI_COMPILED) return true;
377
+ const url = import.meta.url;
378
+ return url.includes("$bunfs") || url.includes("~BUN") || url.includes("%7EBUN");
379
+ }
380
+
381
+ const TRUTHY: Dict<boolean> = {
382
+ "1": true,
383
+ Y: true,
384
+ y: true,
385
+ TRUE: true,
386
+ true: true,
387
+ YES: true,
388
+ yes: true,
389
+ ON: true,
390
+ on: true,
391
+ };
392
+ /** Parse a boolean-ish env value ("1", "yes", "on", …); `def` when unset/empty. */
393
+ export function parseFlag(value: string | undefined, def = false): boolean {
394
+ if (!value) return def;
395
+ return TRUTHY[value] === true;
396
+ }
397
+
398
+ export function $flag(name: string, def: boolean = false): boolean {
399
+ return parseFlag($env[name], def);
400
+ }