@vzn/vx 0.0.487 → 0.0.489

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.487",
3
+ "version": "0.0.489",
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.487",
25
- "@vzn/vx-linux-arm64": "0.0.487",
26
- "@vzn/vx-darwin-x64": "0.0.487",
27
- "@vzn/vx-darwin-arm64": "0.0.487"
24
+ "@vzn/vx-linux-x64": "0.0.489",
25
+ "@vzn/vx-linux-arm64": "0.0.489",
26
+ "@vzn/vx-darwin-x64": "0.0.489",
27
+ "@vzn/vx-darwin-arm64": "0.0.489"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/exec/env.ts CHANGED
@@ -74,6 +74,11 @@ export function buildIsolatedEnv(opts: BuildEnvOptions): NodeJS.ProcessEnv {
74
74
  for (const [name, value] of Object.entries(opts.define)) {
75
75
  out[name] = value
76
76
  }
77
+ // A task's output is a pipe, so a tool asks no TTY and prints plain. vx
78
+ // forces colour, as Nx and Turbo do, and strips it where its own output
79
+ // is plain (`plainOutput`), so the cache holds one form for every
80
+ // machine. Any FORCE_COLOR the task already sees, or a NO_COLOR, wins.
81
+ if (out['FORCE_COLOR'] === undefined && !out['NO_COLOR']) out['FORCE_COLOR'] = '1'
77
82
 
78
83
  // A directory holding the delimiter cannot be named in PATH: split, it
79
84
  // became two entries naming nothing, the second RELATIVE, so resolved
@@ -0,0 +1,57 @@
1
+ import type { TaskNode } from '../graph/index.js'
2
+ import type { Logger } from './logger.js'
3
+
4
+ // A prefix that may yet complete as a CSI or OSC sequence. An escape cut
5
+ // at a chunk's end is held for the next chunk: stripped alone, its
6
+ // remainder (`1m`) reached the log as text.
7
+ // eslint-disable-next-line no-control-regex -- ESC is the point
8
+ const PARTIAL_ESCAPE = /^\x1b(?:\[[0-?]*[ -/]*|\][^\x07\x1b]*\x1b?)?$/
9
+
10
+ /**
11
+ * `sink` with terminal escapes stripped from task output. Tasks run with
12
+ * `FORCE_COLOR` (exec/env.ts), so their bytes carry colour wherever vx's
13
+ * own output is plain: a pipe, CI, `NO_COLOR`.
14
+ */
15
+ export function plainOutput(sink: Logger): Logger {
16
+ const held = new Map<string, string>()
17
+ const strip = (key: string, chunk: string): string => {
18
+ const prior = held.get(key)
19
+ const text = prior === undefined ? chunk : prior + chunk
20
+ if (prior !== undefined) held.delete(key)
21
+ const esc = text.lastIndexOf('\x1b')
22
+ if (esc === -1) return text
23
+ if (PARTIAL_ESCAPE.test(text.slice(esc))) {
24
+ held.set(key, text.slice(esc))
25
+ return Bun.stripANSI(text.slice(0, esc))
26
+ }
27
+ return Bun.stripANSI(text)
28
+ }
29
+ const flush = (node: TaskNode, key: string, write: (n: TaskNode, c: string) => void): void => {
30
+ const rest = held.get(key)
31
+ if (rest === undefined) return
32
+ held.delete(key)
33
+ const text = Bun.stripANSI(rest)
34
+ if (text !== '') write(node, text)
35
+ }
36
+ // Delegated one by one, never spread: an embedder's logger may be a class
37
+ // whose methods live on its prototype and read `this`.
38
+ return {
39
+ status: (line) => sink.status(line),
40
+ ...(sink.runStart && { runStart: (info) => sink.runStart!(info) }),
41
+ ...(sink.taskStart && { taskStart: (node) => sink.taskStart!(node) }),
42
+ ...(sink.runEnd && { runEnd: () => sink.runEnd!() }),
43
+ taskStdout(node, chunk) {
44
+ const text = strip(`${node.id}\0out`, chunk)
45
+ if (text !== '' || chunk === '') sink.taskStdout(node, text)
46
+ },
47
+ taskStderr(node, chunk) {
48
+ const text = strip(`${node.id}\0err`, chunk)
49
+ if (text !== '' || chunk === '') sink.taskStderr(node, text)
50
+ },
51
+ taskComplete(node, outcome) {
52
+ flush(node, `${node.id}\0out`, (n, c) => sink.taskStdout(n, c))
53
+ flush(node, `${node.id}\0err`, (n, c) => sink.taskStderr(n, c))
54
+ sink.taskComplete(node, outcome)
55
+ },
56
+ }
57
+ }
@@ -59,6 +59,7 @@ import { assembleRunSummary, isPassStatus } from './telemetry.js'
59
59
  import type { RunContextRecord } from './telemetry.js'
60
60
  import { defaultLogger, resolveOutputView, type Logger } from './logger.js'
61
61
  import { detectColors, type ColorSupport } from './colors.js'
62
+ import { plainOutput } from './plain-output.js'
62
63
  import { formatPersistentList } from './framed-output.js'
63
64
  import { LocalHistoryProvider } from './history.js'
64
65
  import { plan, type RunPlan } from './plan.js'
@@ -307,7 +308,12 @@ export async function run(options: RunOptions): Promise<RunSummary> {
307
308
  // outlives the run, and a renderer left behind reported the next run on
308
309
  // it twice (item 635). Otherwise a fresh internal bus.
309
310
  const bus = options.bus ?? createEventBus()
310
- const unsubscribeTerminal = bus.subscribe(terminalSubscriber(sink))
311
+ // Tasks run with colour forced (exec/env.ts); where the terminal prints
312
+ // plain, their escapes are stripped. A custom logger gets the task's
313
+ // bytes as written, as the cache holds them: it formats its own output.
314
+ const unsubscribeTerminal = bus.subscribe(
315
+ terminalSubscriber(terminal === null || colors.enabled ? sink : plainOutput(sink)),
316
+ )
311
317
  const detach = (): void => {
312
318
  unsubscribeTerminal()
313
319
  terminal?.settle()
@@ -733,6 +733,7 @@ const BUN_MEMBERS_VX_READS: readonly PropertyKey[] = [
733
733
  'spawnSync',
734
734
  'stdout',
735
735
  'stringWidth',
736
+ 'stripANSI',
736
737
  'version',
737
738
  'which',
738
739
  'write',