@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/ptree.ts ADDED
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Process tree management utilities for Bun subprocesses.
3
+ *
4
+ * - Track managed child processes for cleanup on shutdown (postmortem).
5
+ * - Drain stdout/stderr to avoid subprocess pipe deadlocks.
6
+ * - Cross-platform tree kill for process groups (Windows taskkill, Unix -pid).
7
+ * - Convenience helpers: captureText / execText, AbortSignal, timeouts.
8
+ */
9
+
10
+ import { Process } from "@linxiraos/pi-natives";
11
+ import type { Spawn, Subprocess } from "bun";
12
+
13
+ type InMask = "pipe" | "ignore" | Buffer | Uint8Array | null;
14
+
15
+ /** A Bun subprocess with stdout/stderr always piped (stdin may vary). */
16
+ type PipedSubprocess<In extends InMask = InMask> = Subprocess<In, "pipe", "pipe">;
17
+
18
+ // ── Exceptions ───────────────────────────────────────────────────────────────
19
+
20
+ /**
21
+ * Base for all exceptions representing child process nonzero exit, killed, or
22
+ * cancellation.
23
+ */
24
+ export abstract class Exception extends Error {
25
+ constructor(
26
+ message: string,
27
+ public readonly exitCode: number,
28
+ public readonly stderr: string,
29
+ ) {
30
+ super(message);
31
+ this.name = this.constructor.name;
32
+ }
33
+ abstract readonly aborted: boolean;
34
+ }
35
+
36
+ /** Exception for nonzero exit codes (not cancellation). */
37
+ export class NonZeroExitError extends Exception {
38
+ static readonly MAX_TRACE = 32 * 1024;
39
+
40
+ constructor(exitCode: number, stderr: string) {
41
+ super(`Process exited with code ${exitCode}:\n${stderr}`, exitCode, stderr);
42
+ }
43
+ get aborted() {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /** Exception for explicit process abortion (via signal). */
49
+ export class AbortError extends Exception {
50
+ constructor(
51
+ public readonly reason: unknown,
52
+ stderr: string,
53
+ ) {
54
+ const msg = reason instanceof Error ? reason.message : String(reason ?? "aborted");
55
+ super(`Operation cancelled: ${msg}`, -1, stderr);
56
+ }
57
+ get aborted() {
58
+ return true;
59
+ }
60
+ }
61
+
62
+ /** Exception for process timeout. */
63
+ export class TimeoutError extends AbortError {
64
+ constructor(timeout: number, stderr: string) {
65
+ super(new Error(`Timed out after ${Math.round(timeout / 1000)}s`), stderr);
66
+ }
67
+ }
68
+
69
+ // ── Wait / Exec types ────────────────────────────────────────────────────────
70
+
71
+ /** Options for waiting for process exit and capturing output. */
72
+ export interface WaitOptions {
73
+ allowNonZero?: boolean;
74
+ allowAbort?: boolean;
75
+ /** `full` requires upfront capture; `exec` enables it, while direct `spawn` callers pass `stderr: "full"`. */
76
+ stderr?: "full" | "buffer";
77
+ }
78
+
79
+ /** Result from wait and exec. */
80
+ export interface ExecResult {
81
+ stdout: string;
82
+ stderr: string;
83
+ exitCode: number | null;
84
+ ok: boolean;
85
+ exitError?: Exception;
86
+ }
87
+
88
+ // ── ChildProcess ─────────────────────────────────────────────────────────────
89
+
90
+ /**
91
+ * ChildProcess wraps a managed subprocess, capturing stderr tail, providing
92
+ * cross-platform kill/detach logic plus AbortSignal integration.
93
+ *
94
+ * Stdout is exposed directly from the underlying Bun subprocess; consumers
95
+ * must read it (via text(), wait(), etc.) to prevent pipe deadlock.
96
+ * Stderr is eagerly drained into an internal buffer.
97
+ */
98
+ export class ChildProcess<In extends InMask = InMask> {
99
+ #nothrow = false;
100
+ #stderrTail = "";
101
+ #stderrChunks?: Uint8Array[];
102
+ #exitReason?: Exception;
103
+ #exitReasonPending?: Exception;
104
+ #stderrDone: Promise<void>;
105
+ #exited: Promise<number>;
106
+ #stderrStream?: ReadableStream<Uint8Array>;
107
+
108
+ constructor(
109
+ readonly proc: PipedSubprocess<In>,
110
+ readonly exposeStderr: boolean,
111
+ retainFullStderr = exposeStderr,
112
+ ) {
113
+ if (retainFullStderr) this.#stderrChunks = [];
114
+ // Eagerly drain stderr into a truncated tail, retaining raw chunks only for explicit full capture.
115
+ const dec = new TextDecoder();
116
+ const trim = () => {
117
+ if (this.#stderrTail.length > NonZeroExitError.MAX_TRACE)
118
+ this.#stderrTail = this.#stderrTail.slice(-NonZeroExitError.MAX_TRACE);
119
+ };
120
+ let stderrStream = proc.stderr;
121
+ if (exposeStderr) {
122
+ const [teeStream, drainStream] = stderrStream.tee();
123
+ this.#stderrStream = teeStream;
124
+ stderrStream = drainStream;
125
+ }
126
+ this.#stderrDone = (async () => {
127
+ try {
128
+ for await (const chunk of stderrStream) {
129
+ this.#stderrChunks?.push(chunk);
130
+ this.#stderrTail += dec.decode(chunk, { stream: true });
131
+ trim();
132
+ }
133
+ } catch {}
134
+ this.#stderrTail += dec.decode();
135
+ trim();
136
+ })();
137
+
138
+ // Normalize Bun's exited promise into our exitReason / exitedCleanly model.
139
+ const { promise, resolve, reject } = Promise.withResolvers<number>();
140
+ this.#exited = promise;
141
+
142
+ proc.exited
143
+ .catch(() => null)
144
+ .then(async exitCode => {
145
+ if (this.#exitReasonPending) {
146
+ this.#exitReason = this.#exitReasonPending;
147
+ reject(this.#exitReasonPending);
148
+ return;
149
+ }
150
+ if (exitCode === 0) {
151
+ resolve(0);
152
+ return;
153
+ }
154
+
155
+ await this.#stderrDone;
156
+
157
+ if (exitCode !== null) {
158
+ this.#exitReason = new NonZeroExitError(exitCode, this.#stderrTail);
159
+ resolve(exitCode);
160
+ return;
161
+ }
162
+
163
+ const ex = this.proc.killed
164
+ ? new AbortError(new Error("process killed"), this.#stderrTail)
165
+ : new NonZeroExitError(-1, this.#stderrTail);
166
+ this.#exitReason = ex;
167
+ reject(ex);
168
+ });
169
+ }
170
+
171
+ // ── Properties ───────────────────────────────────────────────────────
172
+
173
+ get pid() {
174
+ return this.proc.pid;
175
+ }
176
+ get exited() {
177
+ return this.#exited;
178
+ }
179
+ get exitCode() {
180
+ return this.proc.exitCode;
181
+ }
182
+ get exitReason() {
183
+ return this.#exitReason;
184
+ }
185
+ get killed() {
186
+ return this.proc.killed;
187
+ }
188
+ get stdin(): Bun.SpawnOptions.WritableToIO<In> {
189
+ return this.proc.stdin;
190
+ }
191
+
192
+ /** Raw stdout stream. Must be consumed to prevent pipe deadlock. */
193
+ get stdout() {
194
+ return this.proc.stdout;
195
+ }
196
+
197
+ /** Optional stderr stream (only when requested in spawn options). */
198
+ get stderr() {
199
+ return this.#stderrStream;
200
+ }
201
+
202
+ get exitedCleanly(): Promise<number> {
203
+ if (this.#nothrow) return this.#exited;
204
+ return this.#exited.then(code => {
205
+ if (code !== 0) throw new NonZeroExitError(code, this.#stderrTail);
206
+ return code;
207
+ });
208
+ }
209
+
210
+ /** Returns the truncated stderr tail (last 32KB). */
211
+ peekStderr() {
212
+ return this.#stderrTail;
213
+ }
214
+
215
+ nothrow(): this {
216
+ this.#nothrow = true;
217
+ return this;
218
+ }
219
+
220
+ kill(reason?: Exception) {
221
+ if (reason && !this.#exitReasonPending) this.#exitReasonPending = reason;
222
+ if (!this.proc.killed)
223
+ void Process.fromPid(this.proc.pid)
224
+ ?.terminate()
225
+ ?.catch(e => void e);
226
+ }
227
+
228
+ // ── Output helpers ───────────────────────────────────────────────────
229
+
230
+ async text(): Promise<string> {
231
+ const p = new Response(this.stdout).text();
232
+ if (this.#nothrow) return p;
233
+ const [text] = await Promise.all([p, this.exitedCleanly]);
234
+ return text;
235
+ }
236
+
237
+ async blob(): Promise<Blob> {
238
+ const p = new Response(this.stdout).blob();
239
+ if (this.#nothrow) return p;
240
+ const [blob] = await Promise.all([p, this.exitedCleanly]);
241
+ return blob;
242
+ }
243
+
244
+ async json(): Promise<unknown> {
245
+ return new Response(this.stdout).json();
246
+ }
247
+
248
+ async arrayBuffer(): Promise<ArrayBuffer> {
249
+ return new Response(this.stdout).arrayBuffer();
250
+ }
251
+
252
+ async bytes(): Promise<Uint8Array> {
253
+ // Bun's `Response(stream).bytes()` returns the raw `ArrayBuffer` once the
254
+ // stream emits more than one chunk (subprocess stdout chunks past ~128 KB).
255
+ // Normalize at the contract boundary so every caller — SSH read,
256
+ // `decodeUtf8Text`, callers slicing with `.subarray` — sees a `Uint8Array`.
257
+ const body = (await new Response(this.stdout).bytes()) as Uint8Array | ArrayBuffer;
258
+ return body instanceof Uint8Array ? body : new Uint8Array(body);
259
+ }
260
+
261
+ // ── Wait ─────────────────────────────────────────────────────────────
262
+
263
+ async wait(opts?: WaitOptions): Promise<ExecResult> {
264
+ const { allowNonZero = false, allowAbort = false, stderr: stderrMode = "buffer" } = opts ?? {};
265
+ const stderrChunks = this.#stderrChunks;
266
+ if (stderrMode === "full" && !stderrChunks) {
267
+ throw new Error('Full stderr capture must be requested when spawning the process (pass stderr: "full")');
268
+ }
269
+
270
+ const stdoutP = new Response(this.stdout).text();
271
+ const stderrP =
272
+ stderrMode === "full" && stderrChunks
273
+ ? this.#stderrDone.then(() => new TextDecoder().decode(Buffer.concat(stderrChunks)))
274
+ : this.#stderrDone.then(() => this.#stderrTail);
275
+
276
+ const [stdout, stderr] = await Promise.all([stdoutP, stderrP]);
277
+
278
+ let exitError: Exception | undefined;
279
+ try {
280
+ await this.#exited;
281
+ } catch (err) {
282
+ if (err instanceof Exception) exitError = err;
283
+ else throw err;
284
+ }
285
+
286
+ if (!exitError) exitError = this.exitReason;
287
+ if (!exitError && this.exitCode !== null && this.exitCode !== 0) {
288
+ exitError = new NonZeroExitError(this.exitCode, this.#stderrTail);
289
+ }
290
+
291
+ const exitCode = this.exitCode ?? (exitError && !exitError.aborted ? exitError.exitCode : null);
292
+ const ok = exitCode === 0;
293
+
294
+ if (exitError) {
295
+ if ((exitError.aborted && !allowAbort) || (!exitError.aborted && !allowNonZero)) throw exitError;
296
+ }
297
+
298
+ return { stdout, stderr, exitCode, ok, exitError };
299
+ }
300
+
301
+ // ── Signal / timeout ─────────────────────────────────────────────────
302
+
303
+ attachSignal(signal: AbortSignal): void {
304
+ const onAbort = () => this.kill(new AbortError(signal.reason, "<cancelled>"));
305
+ if (signal.aborted) return void onAbort();
306
+ signal.addEventListener("abort", onAbort, { once: true });
307
+ this.#exited.catch(() => {}).finally(() => signal.removeEventListener("abort", onAbort));
308
+ }
309
+
310
+ attachTimeout(ms: number): void {
311
+ if (ms <= 0 || this.proc.killed) return;
312
+ this.#exited.catch(() => {});
313
+ Promise.race([
314
+ Bun.sleep(ms).then(() => true),
315
+ this.proc.exited.then(
316
+ () => false,
317
+ () => false,
318
+ ),
319
+ ]).then(timedOut => {
320
+ if (timedOut) this.kill(new TimeoutError(ms, this.#stderrTail));
321
+ });
322
+ }
323
+
324
+ [Symbol.dispose](): void {
325
+ if (this.proc.exitCode !== null) return;
326
+ this.kill(new AbortError("process disposed", this.#stderrTail));
327
+ }
328
+ }
329
+
330
+ // ── Spawn / exec ─────────────────────────────────────────────────────────────
331
+
332
+ /** Options for child spawn. Always pipes stdout/stderr. */
333
+ type ChildSpawnOptions<In extends InMask = InMask> = Omit<
334
+ Spawn.SpawnOptions<In, "pipe", "pipe">,
335
+ "stdout" | "stderr" | "detached"
336
+ > & {
337
+ signal?: AbortSignal;
338
+ detached?: boolean;
339
+ /** Expose and retain complete stderr for a later `wait({ stderr: "full" })`. */
340
+ stderr?: "full" | null;
341
+ };
342
+
343
+ function spawnInternal<In extends InMask = InMask>(
344
+ cmd: string[],
345
+ opts: ChildSpawnOptions<In> | undefined,
346
+ retainFullStderr: boolean,
347
+ ): ChildProcess<In> {
348
+ const { timeout = -1, signal, stderr, ...rest } = opts ?? {};
349
+ const child = Bun.spawn(cmd, {
350
+ stdin: "ignore",
351
+ stdout: "pipe",
352
+ stderr: "pipe",
353
+ windowsHide: true,
354
+ ...rest,
355
+ });
356
+ const cp = new ChildProcess(child, stderr === "full", retainFullStderr);
357
+ if (signal) cp.attachSignal(signal);
358
+ if (timeout > 0) cp.attachTimeout(timeout);
359
+ return cp;
360
+ }
361
+
362
+ /** Spawn a child process with piped stdout/stderr. */
363
+ export function spawn<In extends InMask = InMask>(cmd: string[], opts?: ChildSpawnOptions<In>): ChildProcess<In> {
364
+ return spawnInternal(cmd, opts, opts?.stderr === "full");
365
+ }
366
+
367
+ /** Options for exec. */
368
+ export interface ExecOptions extends Omit<ChildSpawnOptions, "stderr" | "stdin">, WaitOptions {
369
+ input?: string | Buffer | Uint8Array;
370
+ }
371
+
372
+ /** Spawn, wait, and return captured output. */
373
+ export async function exec(cmd: string[], opts?: ExecOptions): Promise<ExecResult> {
374
+ const { input, stderr, allowAbort, allowNonZero, ...spawnOpts } = opts ?? {};
375
+ const stdin = typeof input === "string" ? Buffer.from(input) : input;
376
+ const resolved: ChildSpawnOptions = stdin === undefined ? spawnOpts : { ...spawnOpts, stdin };
377
+ using child = spawnInternal(cmd, resolved, stderr === "full");
378
+ return await child.wait({ stderr, allowAbort, allowNonZero });
379
+ }
380
+
381
+ // ── Signal combinators ───────────────────────────────────────────────────────
382
+
383
+ type SignalValue = AbortSignal | number | null | undefined;
384
+
385
+ /** Combine AbortSignals and timeout values into a single signal. */
386
+ export function combineSignals(...signals: SignalValue[]): AbortSignal | undefined {
387
+ let timeout: number | undefined;
388
+
389
+ let n = 0;
390
+ for (let i = 0; i < signals.length; i++) {
391
+ const s = signals[i];
392
+ if (s instanceof AbortSignal) {
393
+ if (s.aborted) return s;
394
+ if (i !== n) signals[n] = s;
395
+ n++;
396
+ } else if (typeof s === "number" && s > 0) {
397
+ timeout = timeout === undefined ? s : Math.min(timeout, s);
398
+ }
399
+ }
400
+ if (timeout !== undefined) {
401
+ signals[n] = AbortSignal.timeout(timeout);
402
+ n++;
403
+ }
404
+ switch (n) {
405
+ case 0:
406
+ return undefined;
407
+ case 1:
408
+ return signals[0] as AbortSignal;
409
+ default:
410
+ return AbortSignal.any(signals.slice(0, n) as AbortSignal[]);
411
+ }
412
+ }