@vzn/vx 0.0.0 → 0.0.14

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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -1
  3. package/launcher.mjs +92 -0
  4. package/package.json +44 -3
  5. package/src/bin.ts +25 -0
  6. package/src/cache/cache.ts +1832 -0
  7. package/src/cache/cas-backend.ts +97 -0
  8. package/src/cache/digest.ts +47 -0
  9. package/src/cache/index.ts +28 -0
  10. package/src/cache/inputs.ts +839 -0
  11. package/src/cache/layered-cache.ts +355 -0
  12. package/src/cache/tar.ts +343 -0
  13. package/src/cli/backend.ts +23 -0
  14. package/src/cli/cache.ts +96 -0
  15. package/src/cli/format.ts +15 -0
  16. package/src/cli/help.ts +129 -0
  17. package/src/cli/index.ts +85 -0
  18. package/src/cli/info.ts +80 -0
  19. package/src/cli/lock.ts +135 -0
  20. package/src/cli/mcp-rpc.ts +292 -0
  21. package/src/cli/mcp.ts +92 -0
  22. package/src/cli/migrate-nx.ts +450 -0
  23. package/src/cli/migrate-turbo.ts +364 -0
  24. package/src/cli/migrate.ts +219 -0
  25. package/src/cli/plan-format.ts +148 -0
  26. package/src/cli/run.ts +683 -0
  27. package/src/cli/show.ts +201 -0
  28. package/src/cli/upgrade.ts +103 -0
  29. package/src/cli/watch.ts +287 -0
  30. package/src/config.ts +460 -0
  31. package/src/exec/env.ts +74 -0
  32. package/src/exec/index.ts +13 -0
  33. package/src/exec/runner.ts +470 -0
  34. package/src/exec/sandbox-runtime.ts +578 -0
  35. package/src/graph/dependency-spec.ts +85 -0
  36. package/src/graph/index.ts +27 -0
  37. package/src/graph/scheduler.ts +677 -0
  38. package/src/graph/task-graph.ts +377 -0
  39. package/src/index.ts +222 -0
  40. package/src/orchestrator/colors.ts +63 -0
  41. package/src/orchestrator/devframe-surface.ts +74 -0
  42. package/src/orchestrator/events.ts +277 -0
  43. package/src/orchestrator/execute-task.ts +939 -0
  44. package/src/orchestrator/framed-output.ts +461 -0
  45. package/src/orchestrator/history.ts +148 -0
  46. package/src/orchestrator/index.ts +174 -0
  47. package/src/orchestrator/local-shortcircuit.ts +147 -0
  48. package/src/orchestrator/logger.ts +526 -0
  49. package/src/orchestrator/metrics.ts +1927 -0
  50. package/src/orchestrator/options.ts +197 -0
  51. package/src/orchestrator/plan.ts +129 -0
  52. package/src/orchestrator/plugin-host.ts +185 -0
  53. package/src/orchestrator/plugin.ts +283 -0
  54. package/src/orchestrator/predict.ts +106 -0
  55. package/src/orchestrator/prepare.ts +306 -0
  56. package/src/orchestrator/protocol.ts +156 -0
  57. package/src/orchestrator/remote-prefetch.ts +81 -0
  58. package/src/orchestrator/resources.ts +59 -0
  59. package/src/orchestrator/run-artifacts.ts +111 -0
  60. package/src/orchestrator/run-context.ts +205 -0
  61. package/src/orchestrator/run-report.ts +132 -0
  62. package/src/orchestrator/run-state.ts +145 -0
  63. package/src/orchestrator/run.ts +887 -0
  64. package/src/orchestrator/stable-keys.ts +177 -0
  65. package/src/orchestrator/status-line.ts +287 -0
  66. package/src/orchestrator/summary.ts +315 -0
  67. package/src/orchestrator/tally.ts +69 -0
  68. package/src/orchestrator/task-hash.ts +253 -0
  69. package/src/orchestrator/telemetry-host.ts +80 -0
  70. package/src/orchestrator/telemetry.ts +358 -0
  71. package/src/orchestrator/upstream.ts +121 -0
  72. package/src/orchestrator/verify.ts +164 -0
  73. package/src/orchestrator/wire-render.ts +92 -0
  74. package/src/orchestrator/wire.ts +212 -0
  75. package/src/util/errors.ts +11 -0
  76. package/src/util/hash.ts +25 -0
  77. package/src/util/index.ts +8 -0
  78. package/src/util/paths.ts +16 -0
  79. package/src/util/size.ts +16 -0
  80. package/src/util/ulid.ts +15 -0
  81. package/src/version.ts +10 -0
  82. package/src/workspace/affected.ts +118 -0
  83. package/src/workspace/filter.ts +155 -0
  84. package/src/workspace/fingerprint.ts +41 -0
  85. package/src/workspace/index.ts +27 -0
  86. package/src/workspace/lockfile.ts +118 -0
  87. package/src/workspace/nested-dirs.ts +34 -0
  88. package/src/workspace/package-graph.ts +148 -0
  89. package/src/workspace/project-loader.ts +512 -0
  90. package/src/workspace/workspace.ts +176 -0
@@ -0,0 +1,470 @@
1
+ // Child-process invocation + resource accounting.
2
+ //
3
+ // Uses Bun.spawn so we get .resourceUsage() (cpuTime, maxRSS) after the
4
+ // process exits. cpuMs / peakRssBytes are then surfaced on RunResult and
5
+ // folded into the v11 `runs` table by the orchestrator.
6
+
7
+ import { constants as osConstants } from 'node:os'
8
+
9
+ export interface RunResult {
10
+ exitCode: number
11
+ durationMs: number
12
+ stdout: string
13
+ stderr: string
14
+ /** The signal that killed the child, if any (Bun's `signalCode`).
15
+ * SIGINT/SIGTERM here means a Ctrl-C / shutdown teardown — the
16
+ * orchestrator reverts such a task to aborted, not failed. */
17
+ signal?: string
18
+ /** True when vx's own `timeout` timer fired and SIGTERMed the child.
19
+ * Distinguishes a timeout (a real `failed`) from a Ctrl-C shutdown
20
+ * SIGTERM (which the orchestrator reverts to `aborted`). */
21
+ timedOut?: boolean
22
+ /** Total user+system CPU time for the child, in milliseconds. */
23
+ cpuMs?: number
24
+ /** Peak resident set size for the child, in bytes. */
25
+ peakRssBytes?: number
26
+ }
27
+
28
+ export interface RunOptions {
29
+ command: string
30
+ cwd: string
31
+ env: NodeJS.ProcessEnv
32
+ /** Extra args appended to the command (shell-quoted). For arg forwarding. */
33
+ forwardArgs?: readonly string[] | undefined
34
+ /** Called for each chunk of stdout/stderr as it arrives, for live output. */
35
+ onStdout?: (chunk: string) => void
36
+ onStderr?: (chunk: string) => void
37
+ /**
38
+ * Upper bound (ms) on the child's run time. When it elapses the
39
+ * child is SIGTERMed and the result is flagged `timedOut`. Undefined
40
+ * → no limit.
41
+ */
42
+ timeoutMs?: number
43
+ /**
44
+ * Run-scoped registry of in-flight subprocesses. The child is added
45
+ * on spawn and removed once it exits, so the orchestrator's signal
46
+ * handler can SIGTERM everything still alive mid-run.
47
+ */
48
+ liveChildren?: Set<ReturnType<typeof Bun.spawn>>
49
+ }
50
+
51
+ export function shellQuote(arg: string): string {
52
+ if (arg === '') return `''`
53
+ if (/^[A-Za-z0-9_\-.,/=:@%+]+$/.test(arg)) return arg
54
+ return `'${arg.replace(/'/g, `'\\''`)}'`
55
+ }
56
+
57
+ // Any shell control/expansion character means the wrapping `sh` has real
58
+ // work to do (chaining, pipes, redirects, globbing, variables, subshells,
59
+ // backgrounding) and must stay resident.
60
+ const SHELL_CONTROL = /[&|;<>(){}$`\n\\!*?~]/
61
+
62
+ // Shell builtins run INSIDE sh — `exec <builtin>` fails (there's no
63
+ // external `exit`/`true`/`echo`). They also spawn no grandchild, so
64
+ // there's nothing to gain by exec-wrapping them.
65
+ const SHELL_BUILTINS = new Set([
66
+ 'exit',
67
+ 'true',
68
+ 'false',
69
+ ':',
70
+ 'echo',
71
+ 'cd',
72
+ 'export',
73
+ 'set',
74
+ 'unset',
75
+ 'read',
76
+ 'test',
77
+ '[',
78
+ 'printf',
79
+ 'pwd',
80
+ 'umask',
81
+ 'wait',
82
+ 'trap',
83
+ 'eval',
84
+ 'exec',
85
+ 'source',
86
+ '.',
87
+ 'alias',
88
+ 'unalias',
89
+ 'type',
90
+ 'hash',
91
+ 'jobs',
92
+ 'kill',
93
+ 'shift',
94
+ 'return',
95
+ 'break',
96
+ 'continue',
97
+ 'local',
98
+ 'readonly',
99
+ 'times',
100
+ 'ulimit',
101
+ 'command',
102
+ 'builtin',
103
+ 'let',
104
+ 'declare',
105
+ 'typeset',
106
+ ])
107
+
108
+ /**
109
+ * Prepend `exec ` to a command that is a single EXTERNAL program (no
110
+ * shell control characters, not a builtin, no leading env-assignment).
111
+ * `exec` REPLACES the wrapping `sh` with the program, so on a teardown
112
+ * SIGTERM there is no intermediate shell whose death would orphan the
113
+ * real process — the documented grandchild-orphan limitation, resolved
114
+ * for the common single-command case (dev servers: `astro dev`, `vite`,
115
+ * `next dev`; one-shot compilers). It also makes `resourceUsage` measure
116
+ * the program itself rather than the shell. Compound commands, builtins,
117
+ * and `FOO=bar cmd` forms keep the shell (compound-command grandchildren
118
+ * still orphan on a hard programmatic kill — the residual limit every
119
+ * non-cgroup runner shares).
120
+ */
121
+ export function execWrap(command: string): string {
122
+ if (SHELL_CONTROL.test(command)) return command
123
+ const first = command.trimStart().split(/\s+/)[0] ?? ''
124
+ if (first === '' || first.includes('=') || SHELL_BUILTINS.has(first)) return command
125
+ return `exec ${command}`
126
+ }
127
+
128
+ /**
129
+ * POSIX shells report signal death as 128 + signal number (SIGTERM →
130
+ * 143, SIGKILL → 137). Bun gives us the signal NAME; `os.constants`
131
+ * maps it to the platform-correct number. Unknown names fall back to
132
+ * 130 (128 + SIGINT).
133
+ */
134
+ export function signalExitCode(signal: string): number {
135
+ const num = (osConstants.signals as Partial<Record<string, number>>)[signal]
136
+ return num === undefined ? 130 : 128 + num
137
+ }
138
+
139
+ /**
140
+ * Arm a SIGTERM timeout on a spawned child. Returns a handle whose
141
+ * `timedOut()` reports whether the timer fired — so the caller can
142
+ * classify the resulting SIGTERM as a real failure rather than a
143
+ * Ctrl-C abort — and `clear()` cancels the timer once the child exits
144
+ * on its own. A no-op (never fires, nothing to clear) when `timeoutMs`
145
+ * is undefined.
146
+ */
147
+ export function armTimeout(
148
+ proc: ReturnType<typeof Bun.spawn>,
149
+ timeoutMs: number | undefined,
150
+ ): { timedOut: () => boolean; clear: () => void } {
151
+ if (timeoutMs === undefined) return { timedOut: () => false, clear: () => {} }
152
+ let fired = false
153
+ const timer = setTimeout(() => {
154
+ fired = true
155
+ proc.kill('SIGTERM')
156
+ }, timeoutMs)
157
+ return { timedOut: () => fired, clear: () => clearTimeout(timer) }
158
+ }
159
+
160
+ export interface PersistentSpawn {
161
+ /** Underlying Bun subprocess so the orchestrator can SIGTERM it later. */
162
+ child: ReturnType<typeof Bun.spawn>
163
+ /**
164
+ * Resolves once the task is considered "ready":
165
+ * - immediately on successful spawn when no `readyWhen` is given,
166
+ * - on the first stdout/stderr line that matches `readyWhen`.
167
+ * Rejects with the spawn error if the child fails to start.
168
+ */
169
+ ready: Promise<void>
170
+ /** Captured stdout/stderr up to the moment ready resolved. */
171
+ bufferedStdout: () => string
172
+ bufferedStderr: () => string
173
+ /** ms elapsed from spawn to ready (or to current time if not yet ready). */
174
+ readyMs: () => number
175
+ }
176
+
177
+ export interface PersistentOptions extends Omit<RunOptions, 'forwardArgs'> {
178
+ /**
179
+ * String regex. The first stdout/stderr line that matches signals
180
+ * "ready". Undefined → ready immediately on spawn.
181
+ */
182
+ readyWhen?: string
183
+ /**
184
+ * Bound the readiness wait: if `readyWhen` hasn't matched within
185
+ * this window, the child is SIGTERMed and `ready` rejects. Only
186
+ * meaningful together with `readyWhen` (a ready-on-spawn task
187
+ * resolves before the timer can fire).
188
+ */
189
+ timeoutMs?: number
190
+ }
191
+
192
+ /**
193
+ * Spawn a long-running task. Unlike `runCommand`, this returns once
194
+ * the task is *ready* (per `readyWhen`) — not when it exits. The
195
+ * caller owns the returned `child` and must SIGTERM it during
196
+ * cleanup. Stdout/stderr keep streaming into the live `onStdout` /
197
+ * `onStderr` callbacks for the whole lifetime of the child.
198
+ */
199
+ export function runPersistent(opts: PersistentOptions): PersistentSpawn {
200
+ const start = Date.now()
201
+ let bufferedStdout = ''
202
+ let bufferedStderr = ''
203
+ let readyAt: number | undefined
204
+
205
+ // Pattern compiled once; thrown errors surface synchronously so the
206
+ // caller can wrap with a user-facing message.
207
+ const readyRe = opts.readyWhen !== undefined ? new RegExp(opts.readyWhen) : undefined
208
+
209
+ let child: ReturnType<typeof Bun.spawn>
210
+ try {
211
+ child = Bun.spawn(['sh', '-c', execWrap(opts.command)], {
212
+ cwd: opts.cwd,
213
+ env: opts.env as Record<string, string>,
214
+ stdin: 'ignore',
215
+ stdout: 'pipe',
216
+ stderr: 'pipe',
217
+ })
218
+ } catch (err) {
219
+ const message = err instanceof Error ? err.message : String(err)
220
+ return {
221
+ child: undefined as unknown as ReturnType<typeof Bun.spawn>,
222
+ ready: Promise.reject(new Error(`failed to spawn persistent task: ${message}`)),
223
+ bufferedStdout: () => '',
224
+ bufferedStderr: () => `\n[vx] failed to spawn: ${message}\n`,
225
+ readyMs: () => Date.now() - start,
226
+ }
227
+ }
228
+
229
+ let resolveReady!: () => void
230
+ let rejectReady!: (err: Error) => void
231
+ const ready = new Promise<void>((resolve, reject) => {
232
+ resolveReady = resolve
233
+ rejectReady = reject
234
+ })
235
+
236
+ const markReady = (): void => {
237
+ if (readyAt === undefined) {
238
+ readyAt = Date.now()
239
+ if (readyTimer !== undefined) clearTimeout(readyTimer)
240
+ resolveReady()
241
+ }
242
+ }
243
+
244
+ // Stream readers. Each stream owns a pending fragment so a regex
245
+ // match isn't missed across chunk boundaries. The match runs over
246
+ // the WHOLE fragment, including a trailing partial line — prompt-
247
+ // style markers ("Listening on :3000" with no newline) would never
248
+ // resolve ready under line-by-line-only matching. Complete lines
249
+ // that didn't match are discarded after each test to bound memory.
250
+ const consumeChunks = async (
251
+ stream: ReadableStream<Uint8Array> | number | undefined,
252
+ isStderr: boolean,
253
+ ): Promise<void> => {
254
+ if (!stream || typeof stream === 'number') return
255
+ let fragment = ''
256
+ const handleChunk = (chunk: string): void => {
257
+ if (chunk.length === 0) return
258
+ // Buffers capture "up to the moment ready resolved" (their
259
+ // documented contract) — a dev server kept alive for hours must
260
+ // not accrete its whole log history into vx's heap.
261
+ if (isStderr) {
262
+ if (readyAt === undefined) bufferedStderr += chunk
263
+ opts.onStderr?.(chunk)
264
+ } else {
265
+ if (readyAt === undefined) bufferedStdout += chunk
266
+ opts.onStdout?.(chunk)
267
+ }
268
+ if (readyRe && readyAt === undefined) {
269
+ fragment += chunk
270
+ if (readyRe.test(fragment)) {
271
+ markReady()
272
+ fragment = ''
273
+ } else {
274
+ const lastNl = fragment.lastIndexOf('\n')
275
+ if (lastNl >= 0) fragment = fragment.slice(lastNl + 1)
276
+ }
277
+ }
278
+ }
279
+ const reader = stream.getReader()
280
+ const decoder = new TextDecoder()
281
+ try {
282
+ while (true) {
283
+ const { value, done } = await reader.read()
284
+ if (done) break
285
+ handleChunk(decoder.decode(value, { stream: true }))
286
+ }
287
+ // Flush any undecoded multi-byte tail when the stream closes so
288
+ // the buffered text (and a final-fragment match) stay complete.
289
+ handleChunk(decoder.decode())
290
+ } finally {
291
+ reader.releaseLock()
292
+ }
293
+ }
294
+
295
+ // Wire up readers. We deliberately don't await them — they run for
296
+ // the child's lifetime. The `ready` promise resolves out-of-band.
297
+ void consumeChunks(child.stdout, false)
298
+ void consumeChunks(child.stderr, true)
299
+
300
+ opts.liveChildren?.add(child)
301
+
302
+ // Readiness deadline. Reject FIRST so the failure reads as a
303
+ // timeout, then SIGTERM — the exit handler's later reject is a
304
+ // no-op on the settled promise. Cleared the moment ready fires so
305
+ // a healthy server is never killed by a stale timer.
306
+ let readyTimer: ReturnType<typeof setTimeout> | undefined
307
+ if (readyRe && opts.timeoutMs !== undefined) {
308
+ readyTimer = setTimeout(() => {
309
+ if (readyAt === undefined) {
310
+ rejectReady(
311
+ new Error(
312
+ `persistent task not ready within ${opts.timeoutMs}ms — ` +
313
+ `readyWhen pattern never matched; child killed`,
314
+ ),
315
+ )
316
+ child.kill('SIGTERM')
317
+ }
318
+ }, opts.timeoutMs)
319
+ }
320
+
321
+ // If the child exits BEFORE ready fires, that's a failure to start
322
+ // — reject the ready promise so the caller can surface it.
323
+ void child.exited.then((code) => {
324
+ opts.liveChildren?.delete(child)
325
+ if (readyTimer !== undefined) clearTimeout(readyTimer)
326
+ if (readyAt === undefined) {
327
+ rejectReady(
328
+ new Error(
329
+ `persistent task exited before becoming ready (exit ${code ?? '?'})` +
330
+ (readyRe ? ` — readyWhen pattern never matched` : ''),
331
+ ),
332
+ )
333
+ }
334
+ })
335
+
336
+ // No readyWhen → ready immediately. We still wire the readers above
337
+ // so output streams during the task's lifetime.
338
+ if (!readyRe) markReady()
339
+
340
+ return {
341
+ child,
342
+ ready,
343
+ bufferedStdout: () => bufferedStdout,
344
+ bufferedStderr: () => bufferedStderr,
345
+ readyMs: () => (readyAt ?? Date.now()) - start,
346
+ }
347
+ }
348
+
349
+ export async function runCommand(opts: RunOptions): Promise<RunResult> {
350
+ const start = Date.now()
351
+ const fullCommand =
352
+ opts.forwardArgs && opts.forwardArgs.length > 0
353
+ ? opts.command + ' ' + opts.forwardArgs.map(shellQuote).join(' ')
354
+ : opts.command
355
+
356
+ let proc: ReturnType<typeof Bun.spawn>
357
+ try {
358
+ proc = Bun.spawn(['sh', '-c', execWrap(fullCommand)], {
359
+ cwd: opts.cwd,
360
+ env: opts.env as Record<string, string>,
361
+ stdin: 'ignore',
362
+ stdout: 'pipe',
363
+ stderr: 'pipe',
364
+ })
365
+ } catch (err) {
366
+ const message = err instanceof Error ? err.message : String(err)
367
+ return {
368
+ exitCode: 127,
369
+ durationMs: Date.now() - start,
370
+ stdout: '',
371
+ stderr: `\n[vx] failed to spawn: ${message}\n`,
372
+ }
373
+ }
374
+
375
+ opts.liveChildren?.add(proc)
376
+ const timeout = armTimeout(proc, opts.timeoutMs)
377
+ const ac = new AbortController()
378
+ const streams = Promise.all([
379
+ streamToString(proc.stdout, opts.onStdout, ac.signal),
380
+ streamToString(proc.stderr, opts.onStderr, ac.signal),
381
+ ])
382
+ // Gate on the child's own exit, not on stream EOF: a timeout SIGTERM
383
+ // kills `sh` but an orphaned grandchild can keep the pipe open, so we
384
+ // abort the readers once the child is gone rather than wait forever.
385
+ await proc.exited
386
+ timeout.clear()
387
+ if (timeout.timedOut()) ac.abort()
388
+ const [stdout, stderr] = await streams
389
+ opts.liveChildren?.delete(proc)
390
+ const exitCode = proc.exitCode ?? (proc.signalCode ? signalExitCode(proc.signalCode) : 1)
391
+ return {
392
+ exitCode,
393
+ durationMs: Date.now() - start,
394
+ stdout,
395
+ stderr,
396
+ ...(proc.signalCode ? { signal: proc.signalCode } : {}),
397
+ ...(timeout.timedOut() ? { timedOut: true } : {}),
398
+ ...resourceUsageToCpuRss(proc.resourceUsage()),
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Drain a `Bun.spawn` stdout/stderr stream while invoking the live
404
+ * callback per UTF-8 chunk. Returns the full accumulated string.
405
+ */
406
+ export async function streamToString(
407
+ stream: ReadableStream<Uint8Array> | number | undefined,
408
+ onChunk?: (s: string) => void,
409
+ signal?: AbortSignal,
410
+ ): Promise<string> {
411
+ // Bun.spawn types stdout/stderr as `ReadableStream | number | undefined`
412
+ // — the `number` is for inheritance modes, only present when the caller
413
+ // chose `'inherit'` instead of `'pipe'`. We only call this with `'pipe'`,
414
+ // so the runtime value is always a ReadableStream; the `number` branch
415
+ // is unreachable but typed.
416
+ if (!stream || typeof stream === 'number') return ''
417
+ let full = ''
418
+ const reader = stream.getReader()
419
+ const decoder = new TextDecoder()
420
+ // On abort, cancel the read so a pending `reader.read()` resolves
421
+ // `done` and we return whatever we captured. Needed for the timeout
422
+ // path: SIGTERMing `sh` doesn't close the pipe if an orphaned
423
+ // grandchild still holds the write end, so EOF never arrives — the
424
+ // abort breaks us out instead of hanging the run.
425
+ const onAbort = (): void => void reader.cancel().catch(() => {})
426
+ if (signal?.aborted) onAbort()
427
+ else signal?.addEventListener('abort', onAbort, { once: true })
428
+ try {
429
+ while (true) {
430
+ const { value, done } = await reader.read()
431
+ if (done) break
432
+ const chunk = decoder.decode(value, { stream: true })
433
+ full += chunk
434
+ onChunk?.(chunk)
435
+ }
436
+ const tail = decoder.decode()
437
+ if (tail.length > 0) {
438
+ full += tail
439
+ onChunk?.(tail)
440
+ }
441
+ } finally {
442
+ signal?.removeEventListener('abort', onAbort)
443
+ reader.releaseLock()
444
+ }
445
+ return full
446
+ }
447
+
448
+ /**
449
+ * Pull CPU + RSS out of Bun's resourceUsage() shape into our schema's
450
+ * shape (ms + bytes). Returns an empty object when usage isn't available
451
+ * (e.g., the platform didn't expose rusage), so the orchestrator records
452
+ * NULLs in the runs table for those tasks.
453
+ */
454
+ export function resourceUsageToCpuRss(
455
+ usage: ReturnType<ReturnType<typeof Bun.spawn>['resourceUsage']>,
456
+ platform: NodeJS.Platform = process.platform,
457
+ ): { cpuMs?: number; peakRssBytes?: number } {
458
+ if (!usage) return {}
459
+ // cpuTime.total is microseconds as a bigint → ms.
460
+ const cpuMs = Number(usage.cpuTime.total) / 1000
461
+ // maxRSS is the raw kernel `ru_maxrss`, whose UNIT IS PLATFORM-SPECIFIC and
462
+ // Bun does NOT normalize it:
463
+ // • Linux → kilobytes (multiply by 1024 for bytes)
464
+ // • macOS / BSD → bytes (already bytes; do NOT multiply)
465
+ // • Windows → bytes (PeakWorkingSetSize)
466
+ // Treating macOS's byte value as KB inflates peak RSS by 1024× — e.g. a
467
+ // real 460 MB showed up as 460 GB. Convert per platform.
468
+ const peakRssBytes = platform === 'linux' ? usage.maxRSS * 1024 : usage.maxRSS
469
+ return { cpuMs, peakRssBytes }
470
+ }