@vzn/vx 0.0.0 → 0.0.12

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 +332 -1
  3. package/launcher.mjs +84 -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 +29 -0
  10. package/src/cache/inputs.ts +833 -0
  11. package/src/cache/layered-cache.ts +335 -0
  12. package/src/cache/remote-cache.ts +251 -0
  13. package/src/cache/tar.ts +343 -0
  14. package/src/cli/backend.ts +23 -0
  15. package/src/cli/cache.ts +101 -0
  16. package/src/cli/format.ts +15 -0
  17. package/src/cli/help.ts +123 -0
  18. package/src/cli/index.ts +85 -0
  19. package/src/cli/info.ts +83 -0
  20. package/src/cli/lock.ts +135 -0
  21. package/src/cli/mcp-rpc.ts +292 -0
  22. package/src/cli/mcp.ts +92 -0
  23. package/src/cli/migrate-nx.ts +450 -0
  24. package/src/cli/migrate-turbo.ts +364 -0
  25. package/src/cli/migrate.ts +219 -0
  26. package/src/cli/plan-format.ts +148 -0
  27. package/src/cli/run.ts +661 -0
  28. package/src/cli/show.ts +201 -0
  29. package/src/cli/upgrade.ts +103 -0
  30. package/src/cli/watch.ts +287 -0
  31. package/src/config.ts +421 -0
  32. package/src/exec/env.ts +74 -0
  33. package/src/exec/index.ts +13 -0
  34. package/src/exec/runner.ts +470 -0
  35. package/src/exec/sandbox-runtime.ts +578 -0
  36. package/src/graph/dependency-spec.ts +69 -0
  37. package/src/graph/index.ts +18 -0
  38. package/src/graph/scheduler.ts +482 -0
  39. package/src/graph/task-graph.ts +325 -0
  40. package/src/index.ts +197 -0
  41. package/src/orchestrator/colors.ts +63 -0
  42. package/src/orchestrator/devframe-surface.ts +74 -0
  43. package/src/orchestrator/events.ts +277 -0
  44. package/src/orchestrator/execute-task.ts +883 -0
  45. package/src/orchestrator/framed-output.ts +461 -0
  46. package/src/orchestrator/history.ts +148 -0
  47. package/src/orchestrator/index.ts +163 -0
  48. package/src/orchestrator/local-shortcircuit.ts +147 -0
  49. package/src/orchestrator/logger.ts +526 -0
  50. package/src/orchestrator/metrics.ts +1576 -0
  51. package/src/orchestrator/options.ts +176 -0
  52. package/src/orchestrator/plan.ts +129 -0
  53. package/src/orchestrator/plugin-host.ts +190 -0
  54. package/src/orchestrator/plugin.ts +283 -0
  55. package/src/orchestrator/predict.ts +106 -0
  56. package/src/orchestrator/prepare.ts +297 -0
  57. package/src/orchestrator/protocol.ts +142 -0
  58. package/src/orchestrator/remote-cache-setup.ts +60 -0
  59. package/src/orchestrator/remote-prefetch.ts +81 -0
  60. package/src/orchestrator/run-artifacts.ts +111 -0
  61. package/src/orchestrator/run-context.ts +205 -0
  62. package/src/orchestrator/run-report.ts +132 -0
  63. package/src/orchestrator/run-state.ts +145 -0
  64. package/src/orchestrator/run.ts +842 -0
  65. package/src/orchestrator/stable-keys.ts +177 -0
  66. package/src/orchestrator/status-line.ts +287 -0
  67. package/src/orchestrator/summary.ts +291 -0
  68. package/src/orchestrator/tally.ts +69 -0
  69. package/src/orchestrator/task-hash.ts +238 -0
  70. package/src/orchestrator/telemetry-host.ts +80 -0
  71. package/src/orchestrator/telemetry.ts +350 -0
  72. package/src/orchestrator/upstream.ts +93 -0
  73. package/src/orchestrator/verify.ts +141 -0
  74. package/src/orchestrator/wire-render.ts +92 -0
  75. package/src/orchestrator/wire.ts +212 -0
  76. package/src/util/errors.ts +11 -0
  77. package/src/util/hash.ts +25 -0
  78. package/src/util/index.ts +7 -0
  79. package/src/util/paths.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 +470 -0
  90. package/src/workspace/workspace.ts +176 -0
@@ -0,0 +1,277 @@
1
+ // The run event stream — the substrate every output surface subscribes
2
+ // to. The orchestrator no longer calls a Logger directly; it emits
3
+ // RunEvents through a bus, and the terminal renderer (or a custom
4
+ // embedder logger) is just the default, always-on, in-process
5
+ // subscriber. Off-thread surfaces (web devtool, TUI, MCP) attach as
6
+ // additional subscribers later, so a slow/wedged renderer can never
7
+ // stall task exec. See docs/design/event-stream-2026-06.md.
8
+
9
+ import type { TaskNode, TaskOutcome } from '../graph/index.js'
10
+ import { isGroupTask } from '../graph/index.js'
11
+ import type { Logger } from './logger.js'
12
+ import type { RunContext } from './summary.js'
13
+
14
+ /** Payload of the `run:start` event — mirrors the Logger.runStart hook. */
15
+ export interface RunStartInfo {
16
+ total: number
17
+ concurrency?: number
18
+ requestedCount?: number
19
+ /** Run banner context so a live footer can match the final summary. */
20
+ context?: RunContext
21
+ }
22
+
23
+ /**
24
+ * The in-process event vocabulary. Carries LIVE `TaskNode` / `TaskOutcome`
25
+ * refs — zero-copy, because the default terminal subscriber runs on the
26
+ * same thread and reads those objects directly. Crossing a thread or RPC
27
+ * needs the serializable WireEvent form (see `projectNode` / `toWireEvent`
28
+ * below); that mapping is the boundary's job, not the producer's.
29
+ */
30
+ export type RunEvent =
31
+ | { kind: 'run:start'; info: RunStartInfo }
32
+ | { kind: 'task:start'; node: TaskNode }
33
+ | { kind: 'task:stdout'; node: TaskNode; chunk: string }
34
+ | { kind: 'task:stderr'; node: TaskNode; chunk: string }
35
+ | { kind: 'task:complete'; node: TaskNode; outcome: TaskOutcome }
36
+ | { kind: 'run:status'; line: string }
37
+ | { kind: 'run:end' }
38
+
39
+ export type RunEventSubscriber = (event: RunEvent) => void
40
+
41
+ export interface EventBus {
42
+ emit(event: RunEvent): void
43
+ /** Register a subscriber; returns a disposer that removes it. */
44
+ subscribe(subscriber: RunEventSubscriber): () => void
45
+ }
46
+
47
+ export function createEventBus(): EventBus {
48
+ const subscribers: RunEventSubscriber[] = []
49
+ return {
50
+ emit(event) {
51
+ // Synchronous, in-subscription-order fan-out. Ordering is part of
52
+ // the terminal renderer's contract (a stdout chunk must reach it
53
+ // before the task's completion block; block-separator bookkeeping
54
+ // depends on it), so emit must never reorder or defer.
55
+ //
56
+ // A throwing subscriber must NEVER break the run — the whole point
57
+ // of the bus is that a surface (the devframe dev server, a future
58
+ // TUI) is isolated from execution. We swallow per-subscriber so one
59
+ // bad surface can't crash the user's build (mirrors the deleted
60
+ // Observer's makeSafeObserver contract). The terminal renderer is
61
+ // trusted and output-tested, so a real renderer bug still surfaces
62
+ // as wrong output, not a silent hang.
63
+ for (const subscriber of subscribers) {
64
+ try {
65
+ subscriber(event)
66
+ } catch {
67
+ // isolate: a surface fault can't propagate into the orchestrator
68
+ }
69
+ }
70
+ },
71
+ subscribe(subscriber) {
72
+ subscribers.push(subscriber)
73
+ return () => {
74
+ const i = subscribers.indexOf(subscriber)
75
+ if (i >= 0) subscribers.splice(i, 1)
76
+ }
77
+ },
78
+ }
79
+ }
80
+
81
+ /**
82
+ * A `Logger`-shaped facade that turns every call into a RunEvent. The
83
+ * orchestrator holds one of these as its `log`, so every existing
84
+ * `log.X(...)` call site emits through the bus unchanged — the concrete
85
+ * renderer subscribes via `terminalSubscriber`.
86
+ */
87
+ export function busLogger(bus: EventBus): Logger {
88
+ return {
89
+ status: (line) => bus.emit({ kind: 'run:status', line }),
90
+ taskStdout: (node, chunk) => bus.emit({ kind: 'task:stdout', node, chunk }),
91
+ taskStderr: (node, chunk) => bus.emit({ kind: 'task:stderr', node, chunk }),
92
+ taskComplete: (node, outcome) => bus.emit({ kind: 'task:complete', node, outcome }),
93
+ runStart: (info) => bus.emit({ kind: 'run:start', info }),
94
+ taskStart: (node) => bus.emit({ kind: 'task:start', node }),
95
+ runEnd: () => bus.emit({ kind: 'run:end' }),
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Translate events back into `Logger` calls on a concrete renderer (the
101
+ * default terminal logger, or a custom embedder logger). This IS the
102
+ * in-process terminal surface: synchronous, full-fidelity, always-on.
103
+ * Output is byte-identical to calling the renderer directly because the
104
+ * bus fan-out is synchronous and order-preserving.
105
+ */
106
+ export function terminalSubscriber(sink: Logger): RunEventSubscriber {
107
+ return (event) => {
108
+ switch (event.kind) {
109
+ case 'run:start':
110
+ sink.runStart?.(event.info)
111
+ return
112
+ case 'task:start':
113
+ sink.taskStart?.(event.node)
114
+ return
115
+ case 'task:stdout':
116
+ sink.taskStdout(event.node, event.chunk)
117
+ return
118
+ case 'task:stderr':
119
+ sink.taskStderr(event.node, event.chunk)
120
+ return
121
+ case 'task:complete':
122
+ sink.taskComplete(event.node, event.outcome)
123
+ return
124
+ case 'run:status':
125
+ sink.status(event.line)
126
+ return
127
+ case 'run:end':
128
+ sink.runEnd?.()
129
+ return
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * A bus subscriber that projects each event to its `WireEvent` and hands
136
+ * it to `send` — the shared run→consumer forwarding path (the `vx dev`
137
+ * hub frames each as NDJSON; the `vx serve` protocol wraps each in an
138
+ * envelope). Group-task start/complete events are dropped (no command,
139
+ * pure scheduling noise). `run()` emits run:end twice (normal + finally)
140
+ * plus trailing summary status lines; we forward the run once and then go
141
+ * quiet, so a consumer sees exactly one terminal frame per run. `send`
142
+ * must be fire-and-forget and tolerant of its own failures (a dead
143
+ * consumer can never break the run); the bus already isolates throws.
144
+ */
145
+ export function wireForwarder(send: (event: WireEvent) => void): RunEventSubscriber {
146
+ // run() emits run:end TWICE (normal + finally), with the summary footer
147
+ // (run:status lines) emitted in BETWEEN. Drop the duplicate run:end, but
148
+ // keep forwarding everything else — including those post-run:end status
149
+ // lines, so a consumer that renders them (the serve client) gets the
150
+ // footer. A consumer that ignores run:status (the dev hub) is unaffected.
151
+ let endForwarded = false
152
+ return (event) => {
153
+ if (event.kind === 'run:end') {
154
+ if (endForwarded) return
155
+ endForwarded = true
156
+ } else if (
157
+ (event.kind === 'task:start' || event.kind === 'task:complete') &&
158
+ isGroupTask(event.node)
159
+ ) {
160
+ return
161
+ }
162
+ send(toWireEvent(event))
163
+ }
164
+ }
165
+
166
+ // --- Serializable projections (the off-thread / devframe wire form) ----
167
+ //
168
+ // The in-process bus carries live refs; crossing a worker postMessage or
169
+ // an RPC needs a JSON/structured-clone-safe form. Two concrete blockers
170
+ // make the raw objects un-sendable:
171
+ // 1. `TaskOutcome.wallclockStartNs/EndNs` are bigint — JSON.stringify
172
+ // THROWS on a bigint.
173
+ // 2. `TaskOutcome.node` back-references the whole TaskNode graph (its
174
+ // config + dep nodes), so every event would drag the graph across
175
+ // the boundary.
176
+ // The projection fixes both: ids instead of the node graph, decimal
177
+ // strings instead of bigint. Consumers on the far side rebuild their
178
+ // view-model from the one-time task table in `run:start`.
179
+
180
+ /** Display projection of a TaskNode — exactly the fields renderers read. */
181
+ export interface TaskView {
182
+ id: string
183
+ project: string
184
+ task: string
185
+ isGroup: boolean
186
+ requested: boolean
187
+ surfaced: boolean
188
+ persistent: boolean
189
+ command?: string
190
+ }
191
+
192
+ /** Serializable projection of a TaskOutcome (no node ref, ns as strings). */
193
+ export interface OutcomeView {
194
+ taskId: string
195
+ status: TaskOutcome['status']
196
+ exitCode: number
197
+ durationMs: number
198
+ hash?: string
199
+ cpuMs?: number
200
+ peakRssBytes?: number
201
+ restored?: boolean
202
+ sandboxViolations?: number
203
+ sandboxViolationLines?: string[]
204
+ /** bigint hrtime ns encoded as a decimal string (JSON/RPC-safe). */
205
+ wallclockStartNs?: string
206
+ wallclockEndNs?: string
207
+ }
208
+
209
+ export function projectNode(node: TaskNode): TaskView {
210
+ const view: TaskView = {
211
+ id: node.id,
212
+ project: node.projectName,
213
+ task: node.taskName,
214
+ isGroup: isGroupTask(node),
215
+ requested: node.requested,
216
+ surfaced: node.surfaced === true,
217
+ persistent: node.config.exec?.persistent !== undefined,
218
+ }
219
+ if (node.config.exec?.command !== undefined) view.command = node.config.exec.command
220
+ return view
221
+ }
222
+
223
+ export function projectOutcome(outcome: TaskOutcome): OutcomeView {
224
+ const view: OutcomeView = {
225
+ taskId: outcome.node.id,
226
+ status: outcome.status,
227
+ exitCode: outcome.exitCode,
228
+ durationMs: outcome.durationMs,
229
+ }
230
+ if (outcome.hash !== undefined) view.hash = outcome.hash
231
+ if (outcome.cpuMs !== undefined) view.cpuMs = outcome.cpuMs
232
+ if (outcome.peakRssBytes !== undefined) view.peakRssBytes = outcome.peakRssBytes
233
+ if (outcome.restored !== undefined) view.restored = outcome.restored
234
+ if (outcome.sandboxViolations !== undefined) view.sandboxViolations = outcome.sandboxViolations
235
+ if (outcome.sandboxViolationLines !== undefined)
236
+ view.sandboxViolationLines = outcome.sandboxViolationLines
237
+ if (outcome.wallclockStartNs !== undefined)
238
+ view.wallclockStartNs = outcome.wallclockStartNs.toString()
239
+ if (outcome.wallclockEndNs !== undefined) view.wallclockEndNs = outcome.wallclockEndNs.toString()
240
+ return view
241
+ }
242
+
243
+ /**
244
+ * The serializable form of a RunEvent — JSON / structured-clone safe, the
245
+ * wire contract a non-local consumer (devframe, `vx serve` client) reads.
246
+ * `task:start` carries the full `TaskView` so a consumer can rebuild a
247
+ * node-shaped object incrementally (no upfront table needed); later
248
+ * task events reference the id, which the consumer already holds.
249
+ */
250
+ export type WireEvent =
251
+ | { kind: 'run:start'; info: RunStartInfo }
252
+ | { kind: 'task:start'; task: TaskView }
253
+ | { kind: 'task:stdout'; taskId: string; chunk: string }
254
+ | { kind: 'task:stderr'; taskId: string; chunk: string }
255
+ | { kind: 'task:complete'; outcome: OutcomeView }
256
+ | { kind: 'run:status'; line: string }
257
+ | { kind: 'run:end' }
258
+
259
+ /** Map an in-process event to its serializable wire form. */
260
+ export function toWireEvent(event: RunEvent): WireEvent {
261
+ switch (event.kind) {
262
+ case 'run:start':
263
+ return { kind: 'run:start', info: event.info }
264
+ case 'task:start':
265
+ return { kind: 'task:start', task: projectNode(event.node) }
266
+ case 'task:stdout':
267
+ return { kind: 'task:stdout', taskId: event.node.id, chunk: event.chunk }
268
+ case 'task:stderr':
269
+ return { kind: 'task:stderr', taskId: event.node.id, chunk: event.chunk }
270
+ case 'task:complete':
271
+ return { kind: 'task:complete', outcome: projectOutcome(event.outcome) }
272
+ case 'run:status':
273
+ return { kind: 'run:status', line: event.line }
274
+ case 'run:end':
275
+ return { kind: 'run:end' }
276
+ }
277
+ }