@vzn/vx 0.0.625 → 0.0.626

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.625",
3
+ "version": "0.0.626",
4
4
  "description": "The fastest task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.625",
25
- "@vzn/vx-linux-arm64": "0.0.625",
26
- "@vzn/vx-darwin-x64": "0.0.625",
27
- "@vzn/vx-darwin-arm64": "0.0.625"
24
+ "@vzn/vx-linux-x64": "0.0.626",
25
+ "@vzn/vx-linux-arm64": "0.0.626",
26
+ "@vzn/vx-darwin-x64": "0.0.626",
27
+ "@vzn/vx-darwin-arm64": "0.0.626"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -59,11 +59,15 @@ const ABORTED = '#f97316' // orange-500 — killed by a shutdown signal
59
59
  const LOCAL = '#38bdf8' // sky-400 — local cache hit
60
60
  const REMOTE = '#2563eb' // blue-600 — remote cache hit
61
61
 
62
+ /** A piece of a task's output, as written: `err` for stderr. */
63
+ export interface OutputChunk {
64
+ text: string
65
+ err: boolean
66
+ }
67
+
62
68
  export interface TaskBlockBody {
63
- /** stdout chunks accumulated during the task. Renders under `├─ stdout`. */
64
- stdout?: string
65
- /** stderr chunks. Renders under `├─ stderr`. */
66
- stderr?: string
69
+ /** What the task wrote, in order. Renders under `├─ OUTPUT`, stderr lines marked. */
70
+ output?: readonly OutputChunk[]
67
71
  /**
68
72
  * Characters the capture dropped from the head of each stream. Non-zero
69
73
  * only for a task whose output is bounded (a persistent one — nothing
@@ -92,8 +96,7 @@ export function formatTaskBlock(
92
96
  // pass already make.
93
97
  if (isGroupTask(node)) return ''
94
98
 
95
- const stdout = body.stdout ?? ''
96
- const stderr = body.stderr ?? ''
99
+ const output = body.output ?? []
97
100
 
98
101
  const idPainted = paintTaskId(node, colors, { bold: true })
99
102
  const corner = (s: string) => paint('', s, colors, { dim: true })
@@ -110,8 +113,7 @@ export function formatTaskBlock(
110
113
  lines.push('', corner(`$ ${shownCommand(node, forwardArgs)}`), '')
111
114
  }
112
115
 
113
- pushStreamSection(lines, stdout, 'STDOUT', SUCCESS, body.droppedStdout ?? 0, colors)
114
- pushStreamSection(lines, stderr, 'STDERR', ERROR, body.droppedStderr ?? 0, colors)
116
+ pushOutputSection(lines, output, (body.droppedStdout ?? 0) + (body.droppedStderr ?? 0), colors)
115
117
 
116
118
  lines.push(...violationSection(outcome, colors))
117
119
 
@@ -141,6 +143,48 @@ function pushStreamSection(
141
143
  lines.push('')
142
144
  }
143
145
 
146
+ /**
147
+ * The one `├─ OUTPUT` section: the task's lines in the order it wrote
148
+ * them, so an error stays beside what it is about (a test runner prints
149
+ * progress to stdout and the failure to stderr). A stderr line is red;
150
+ * without colour it is the task's bytes untouched. A line the task
151
+ * coloured itself keeps its own colours, since a reset inside it would
152
+ * end ours mid-line. A line takes the stream that began it.
153
+ */
154
+ function pushOutputSection(
155
+ lines: string[],
156
+ output: readonly OutputChunk[],
157
+ lost: number,
158
+ colors: ColorSupport,
159
+ ): void {
160
+ if (output.every((c) => c.text.trim().length === 0)) return
161
+ lines.push(sectionLine('OUTPUT', '', colors), '')
162
+ if (lost > 0) {
163
+ lines.push(paint('', `… ${lost} earlier characters dropped`, colors, { dim: true }))
164
+ }
165
+ const body: Array<{ text: string; err: boolean }> = []
166
+ let open: { parts: string[]; err: boolean } | null = null
167
+ for (const c of output) {
168
+ let rest = c.text
169
+ while (rest.length > 0) {
170
+ const nl = rest.indexOf('\n')
171
+ const piece = nl < 0 ? rest : rest.slice(0, nl)
172
+ if (open === null) open = { parts: [], err: c.err }
173
+ open.parts.push(piece)
174
+ if (nl < 0) break
175
+ body.push({ text: open.parts.join(''), err: open.err })
176
+ open = null
177
+ rest = rest.slice(nl + 1)
178
+ }
179
+ }
180
+ if (open !== null) body.push({ text: open.parts.join(''), err: open.err })
181
+ for (const l of body) {
182
+ if (!l.err || !colors.enabled) lines.push(l.text)
183
+ else lines.push(l.text.includes('\x1b') ? l.text : paint(ERROR, l.text, colors))
184
+ }
185
+ lines.push('')
186
+ }
187
+
144
188
  /**
145
189
  * Section labels are bold + state-colored (owner design): stdout green,
146
190
  * stderr red, sandbox yellow, command plain white — only the command
@@ -520,7 +564,9 @@ function isPersistentNode(node: TaskNode): boolean {
520
564
  export function formatPersistentTailBlock(
521
565
  node: TaskNode,
522
566
  outcome: TaskOutcome,
523
- body: TaskBlockBody,
567
+ // Two bounded tails, one per stream: their order is lost, so the
568
+ // streams print apart.
569
+ body: { stdout?: string; stderr?: string },
524
570
  dropped: { stdout?: number; stderr?: number } = {},
525
571
  colors: ColorSupport = NO_COLOR,
526
572
  forwardArgs: readonly string[] = [],
@@ -11,6 +11,7 @@ import {
11
11
  flakyNote,
12
12
  formatTaskHitLine,
13
13
  formatTaskSkippedLine,
14
+ type OutputChunk,
14
15
  } from './framed-output.js'
15
16
  import {
16
17
  createOutputWriter,
@@ -210,8 +211,7 @@ export function defaultLogger(
210
211
  // appending via `+=`; concatenating N small chunks via `+=` is O(N²)
211
212
  // because each `+=` allocates a fresh string of the full accumulated
212
213
  // length. Bun-friendly: join('') is a single contiguous allocation.
213
- const stdoutBuffers = new Map<string, string[]>()
214
- const stderrBuffers = new Map<string, string[]>()
214
+ const outputBuffers = new Map<string, OutputChunk[]>()
215
215
  // Separator bookkeeping: blocks are blank-line-delimited on BOTH
216
216
  // sides — raw (unprefixed) frame content must never collide with a
217
217
  // neighbouring one-liner (owner feedback). A block leaves its own
@@ -222,16 +222,16 @@ export function defaultLogger(
222
222
  // True while the live stream sits mid-line (chunk without trailing
223
223
  // newline) — the frame close must not glue onto partial output.
224
224
  let streamMidLine = false
225
- const pushChunk = (buffers: Map<string, string[]>, id: string, chunk: string): void => {
226
- const arr = buffers.get(id)
227
- if (arr) arr.push(chunk)
228
- else buffers.set(id, [chunk])
225
+ const pushChunk = (id: string, text: string, err: boolean): void => {
226
+ const arr = outputBuffers.get(id)
227
+ if (arr) arr.push({ text, err })
228
+ else outputBuffers.set(id, [{ text, err }])
229
229
  }
230
- const takeChunks = (buffers: Map<string, string[]>, id: string): string => {
231
- const arr = buffers.get(id)
232
- if (!arr) return ''
233
- buffers.delete(id)
234
- return arr.length === 1 ? arr[0]! : arr.join('')
230
+ const takeChunks = (id: string): OutputChunk[] => {
231
+ const arr = outputBuffers.get(id)
232
+ if (!arr) return []
233
+ outputBuffers.delete(id)
234
+ return arr
235
235
  }
236
236
 
237
237
  // `none` is the ONE mode whose contract guarantees the output is
@@ -682,7 +682,7 @@ export function defaultLogger(
682
682
  else appendTail(tail.out, chunk)
683
683
  return
684
684
  }
685
- pushChunk(stdoutBuffers, node.id, chunk)
685
+ pushChunk(node.id, chunk, false)
686
686
  },
687
687
  taskStderr(node, chunk) {
688
688
  if (discardsOutput) return
@@ -698,7 +698,7 @@ export function defaultLogger(
698
698
  else appendTail(tail.err, chunk)
699
699
  return
700
700
  }
701
- pushChunk(stderrBuffers, node.id, chunk)
701
+ pushChunk(node.id, chunk, true)
702
702
  },
703
703
  taskComplete(node, outcome) {
704
704
  if (!statusDead) finished.add(node.id)
@@ -707,8 +707,14 @@ export function defaultLogger(
707
707
  // there — carrying how much the cap dropped, since a truncated log
708
708
  // that reads as complete is worse than one that says what it lost.
709
709
  const preReady = persistentTails.get(node.id)
710
- const stdout = preReady ? tailText(preReady.out) : takeChunks(stdoutBuffers, node.id)
711
- const stderr = preReady ? tailText(preReady.err) : takeChunks(stderrBuffers, node.id)
710
+ // In the order the task wrote it; a bounded tail kept its streams
711
+ // apart, so its stdout reads first.
712
+ const output: OutputChunk[] = preReady
713
+ ? [
714
+ { text: tailText(preReady.out), err: false },
715
+ { text: tailText(preReady.err), err: true },
716
+ ]
717
+ : takeChunks(node.id)
712
718
  const dropped =
713
719
  preReady && (preReady.out.dropped > 0 || preReady.err.dropped > 0)
714
720
  ? { droppedStdout: preReady.out.dropped, droppedStderr: preReady.err.dropped }
@@ -745,16 +751,9 @@ export function defaultLogger(
745
751
  emitFrameClose(formatFrameClose(node, outcome, colors))
746
752
  return
747
753
  }
748
- if (framed && (stdout.length > 0 || stderr.length > 0)) {
754
+ if (framed && output.some((c) => c.text.length > 0)) {
749
755
  emitBlock(
750
- formatTaskBlock(
751
- node,
752
- outcome,
753
- { stdout, stderr, ...dropped },
754
- colors,
755
- true,
756
- forwardArgs,
757
- ),
756
+ formatTaskBlock(node, outcome, { output, ...dropped }, colors, true, forwardArgs),
758
757
  )
759
758
  return
760
759
  }
@@ -847,14 +846,7 @@ export function defaultLogger(
847
846
  if (outcome.status !== 'failed') return
848
847
  emitLine(failureRow(node, outcome))
849
848
  deferredFailures.push(
850
- formatTaskBlock(
851
- node,
852
- outcome,
853
- { stdout, stderr, ...dropped },
854
- colors,
855
- false,
856
- forwardArgs,
857
- ),
849
+ formatTaskBlock(node, outcome, { output, ...dropped }, colors, false, forwardArgs),
858
850
  )
859
851
  return
860
852
  case 'broad':
@@ -866,14 +858,7 @@ export function defaultLogger(
866
858
  // ◼ row now; the full frame replays at runEnd.
867
859
  emitLine(failureRow(node, outcome))
868
860
  deferredFailures.push(
869
- formatTaskBlock(
870
- node,
871
- outcome,
872
- { stdout, stderr, ...dropped },
873
- colors,
874
- false,
875
- forwardArgs,
876
- ),
861
+ formatTaskBlock(node, outcome, { output, ...dropped }, colors, false, forwardArgs),
877
862
  )
878
863
  } else if (outcome.status === 'success') {
879
864
  emitLine(formatTaskExecutedLine(node, outcome, colors))
@@ -910,28 +895,14 @@ export function defaultLogger(
910
895
  if (outcome.status === 'failed') {
911
896
  emitLine(failureRow(node, outcome))
912
897
  deferredFailures.push(
913
- formatTaskBlock(
914
- node,
915
- outcome,
916
- { stdout, stderr, ...dropped },
917
- colors,
918
- false,
919
- forwardArgs,
920
- ),
898
+ formatTaskBlock(node, outcome, { output, ...dropped }, colors, false, forwardArgs),
921
899
  )
922
900
  return
923
901
  }
924
902
  // forceCommand: a requested task's frame shows `$ cmd`
925
903
  // whether it ran or was cached — same frame every run.
926
904
  emitBlock(
927
- formatTaskBlock(
928
- node,
929
- outcome,
930
- { stdout, stderr, ...dropped },
931
- colors,
932
- true,
933
- forwardArgs,
934
- ),
905
+ formatTaskBlock(node, outcome, { output, ...dropped }, colors, true, forwardArgs),
935
906
  )
936
907
  return
937
908
  }
@@ -942,14 +913,7 @@ export function defaultLogger(
942
913
  } else if (outcome.status === 'failed') {
943
914
  emitLine(failureRow(node, outcome))
944
915
  deferredFailures.push(
945
- formatTaskBlock(
946
- node,
947
- outcome,
948
- { stdout, stderr, ...dropped },
949
- colors,
950
- false,
951
- forwardArgs,
952
- ),
916
+ formatTaskBlock(node, outcome, { output, ...dropped }, colors, false, forwardArgs),
953
917
  )
954
918
  }
955
919
  return
@@ -960,7 +924,7 @@ export function defaultLogger(
960
924
  // replayed stdout keep their frame (the output is the
961
925
  // point); misses/failures are always framed. One-liners
962
926
  // stay outside ::group:: — there's nothing to collapse.
963
- if (isHit && stdout.trim().length === 0 && stderr.trim().length === 0) {
927
+ if (isHit && output.every((c) => c.text.trim().length === 0)) {
964
928
  emitLine(formatTaskHitLine(node, outcome, colors))
965
929
  return
966
930
  }
@@ -971,7 +935,7 @@ export function defaultLogger(
971
935
  const block = formatTaskBlock(
972
936
  node,
973
937
  outcome,
974
- { stdout, stderr, ...dropped },
938
+ { output, ...dropped },
975
939
  colors,
976
940
  false,
977
941
  forwardArgs,