@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.
- package/LICENSE +21 -0
- package/README.md +349 -1
- package/launcher.mjs +92 -0
- package/package.json +44 -3
- package/src/bin.ts +25 -0
- package/src/cache/cache.ts +1832 -0
- package/src/cache/cas-backend.ts +97 -0
- package/src/cache/digest.ts +47 -0
- package/src/cache/index.ts +28 -0
- package/src/cache/inputs.ts +839 -0
- package/src/cache/layered-cache.ts +355 -0
- package/src/cache/tar.ts +343 -0
- package/src/cli/backend.ts +23 -0
- package/src/cli/cache.ts +96 -0
- package/src/cli/format.ts +15 -0
- package/src/cli/help.ts +129 -0
- package/src/cli/index.ts +85 -0
- package/src/cli/info.ts +80 -0
- package/src/cli/lock.ts +135 -0
- package/src/cli/mcp-rpc.ts +292 -0
- package/src/cli/mcp.ts +92 -0
- package/src/cli/migrate-nx.ts +450 -0
- package/src/cli/migrate-turbo.ts +364 -0
- package/src/cli/migrate.ts +219 -0
- package/src/cli/plan-format.ts +148 -0
- package/src/cli/run.ts +683 -0
- package/src/cli/show.ts +201 -0
- package/src/cli/upgrade.ts +103 -0
- package/src/cli/watch.ts +287 -0
- package/src/config.ts +460 -0
- package/src/exec/env.ts +74 -0
- package/src/exec/index.ts +13 -0
- package/src/exec/runner.ts +470 -0
- package/src/exec/sandbox-runtime.ts +578 -0
- package/src/graph/dependency-spec.ts +85 -0
- package/src/graph/index.ts +27 -0
- package/src/graph/scheduler.ts +677 -0
- package/src/graph/task-graph.ts +377 -0
- package/src/index.ts +222 -0
- package/src/orchestrator/colors.ts +63 -0
- package/src/orchestrator/devframe-surface.ts +74 -0
- package/src/orchestrator/events.ts +277 -0
- package/src/orchestrator/execute-task.ts +939 -0
- package/src/orchestrator/framed-output.ts +461 -0
- package/src/orchestrator/history.ts +148 -0
- package/src/orchestrator/index.ts +174 -0
- package/src/orchestrator/local-shortcircuit.ts +147 -0
- package/src/orchestrator/logger.ts +526 -0
- package/src/orchestrator/metrics.ts +1927 -0
- package/src/orchestrator/options.ts +197 -0
- package/src/orchestrator/plan.ts +129 -0
- package/src/orchestrator/plugin-host.ts +185 -0
- package/src/orchestrator/plugin.ts +283 -0
- package/src/orchestrator/predict.ts +106 -0
- package/src/orchestrator/prepare.ts +306 -0
- package/src/orchestrator/protocol.ts +156 -0
- package/src/orchestrator/remote-prefetch.ts +81 -0
- package/src/orchestrator/resources.ts +59 -0
- package/src/orchestrator/run-artifacts.ts +111 -0
- package/src/orchestrator/run-context.ts +205 -0
- package/src/orchestrator/run-report.ts +132 -0
- package/src/orchestrator/run-state.ts +145 -0
- package/src/orchestrator/run.ts +887 -0
- package/src/orchestrator/stable-keys.ts +177 -0
- package/src/orchestrator/status-line.ts +287 -0
- package/src/orchestrator/summary.ts +315 -0
- package/src/orchestrator/tally.ts +69 -0
- package/src/orchestrator/task-hash.ts +253 -0
- package/src/orchestrator/telemetry-host.ts +80 -0
- package/src/orchestrator/telemetry.ts +358 -0
- package/src/orchestrator/upstream.ts +121 -0
- package/src/orchestrator/verify.ts +164 -0
- package/src/orchestrator/wire-render.ts +92 -0
- package/src/orchestrator/wire.ts +212 -0
- package/src/util/errors.ts +11 -0
- package/src/util/hash.ts +25 -0
- package/src/util/index.ts +8 -0
- package/src/util/paths.ts +16 -0
- package/src/util/size.ts +16 -0
- package/src/util/ulid.ts +15 -0
- package/src/version.ts +10 -0
- package/src/workspace/affected.ts +118 -0
- package/src/workspace/filter.ts +155 -0
- package/src/workspace/fingerprint.ts +41 -0
- package/src/workspace/index.ts +27 -0
- package/src/workspace/lockfile.ts +118 -0
- package/src/workspace/nested-dirs.ts +34 -0
- package/src/workspace/package-graph.ts +148 -0
- package/src/workspace/project-loader.ts +512 -0
- package/src/workspace/workspace.ts +176 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import type { TaskConfig } from '../config.js'
|
|
2
|
+
import { UserError } from '../util/index.js'
|
|
3
|
+
import type { PackageGraph, ProjectEntry } from '../workspace/index.js'
|
|
4
|
+
import {
|
|
5
|
+
DependencySpecError,
|
|
6
|
+
compileTaskPattern,
|
|
7
|
+
isTaskPattern,
|
|
8
|
+
parseDependencySpec,
|
|
9
|
+
type DependencySpec,
|
|
10
|
+
} from './dependency-spec.js'
|
|
11
|
+
|
|
12
|
+
// Re-exported so existing importers keep working while the type's home
|
|
13
|
+
// moves to workspace (it's the joint product of discovery + loading).
|
|
14
|
+
export type { ProjectEntry } from '../workspace/index.js'
|
|
15
|
+
|
|
16
|
+
export interface TaskNode {
|
|
17
|
+
/** Stable id: `${projectName}#${taskName}`. */
|
|
18
|
+
id: string
|
|
19
|
+
projectName: string
|
|
20
|
+
projectDir: string
|
|
21
|
+
taskName: string
|
|
22
|
+
config: TaskConfig
|
|
23
|
+
/** Ids of tasks that must complete before this one runs. */
|
|
24
|
+
deps: string[]
|
|
25
|
+
/**
|
|
26
|
+
* True for the tasks the user actually asked for (via cwd, `-r`, `-F`,
|
|
27
|
+
* or `pkg#task`). False for dependencies pulled in by `dependsOn`
|
|
28
|
+
* expansion. Used by the orchestrator to scope `forwardArgs` so trailing
|
|
29
|
+
* CLI args don't leak into upstream tasks the user didn't address.
|
|
30
|
+
*/
|
|
31
|
+
requested: boolean
|
|
32
|
+
/**
|
|
33
|
+
* Display-only: a same-project, non-group, direct `dependsOn` task of
|
|
34
|
+
* a REQUESTED GROUP. A group produces no output of its own, so in
|
|
35
|
+
* focused flow we surface the work it stands for one level down (no
|
|
36
|
+
* recursion, no `^`/cross-project deps — see `markSurfacedDeps`). The
|
|
37
|
+
* focused logger shows these like requested tasks; it does NOT make
|
|
38
|
+
* them `requested`, so `forwardArgs` scoping is unaffected.
|
|
39
|
+
*/
|
|
40
|
+
surfaced?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function taskId(project: string, task: string): string {
|
|
44
|
+
return `${project}#${task}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A task is a "group" if it has no `exec` — it exists only to chain
|
|
49
|
+
* `dependsOn` (an umbrella for `vx run ci`). Group tasks:
|
|
50
|
+
* - never spawn a process,
|
|
51
|
+
* - never read/write the cache (it's a config error to declare one),
|
|
52
|
+
* - never appear in the run summary or `runs` analytics table,
|
|
53
|
+
* - render no framed block in the live output.
|
|
54
|
+
* Six call sites used to repeat `node.config.exec === undefined`;
|
|
55
|
+
* centralising the predicate prevents that check from drifting.
|
|
56
|
+
*/
|
|
57
|
+
export function isGroupTask(node: TaskNode): boolean {
|
|
58
|
+
return node.config.exec === undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Mark, for focused-flow display, the real tasks a requested GROUP
|
|
63
|
+
* stands for. A group has no output of its own, so `vx run build`
|
|
64
|
+
* (where `build` is a group) would otherwise print nothing.
|
|
65
|
+
*
|
|
66
|
+
* Walk `dependsOn` from the requested group, DESCENDING THROUGH nested
|
|
67
|
+
* same-project groups (e.g. `build` → `build.bun` → `build.bun.*`), and
|
|
68
|
+
* surface the first non-group task on each path. Two hard limits:
|
|
69
|
+
* - never leave the requested project — `^`/cross-project deps are
|
|
70
|
+
* not entered (the user asked to run THIS project's group),
|
|
71
|
+
* - never descend past a real task — its own `dependsOn` is its
|
|
72
|
+
* implementation detail, not work the group "is".
|
|
73
|
+
*
|
|
74
|
+
* Display-only: it never flips `requested`, so `forwardArgs` scoping
|
|
75
|
+
* stays put. Returns the count of newly surfaced nodes.
|
|
76
|
+
*/
|
|
77
|
+
export function markSurfacedDeps(nodes: Map<string, TaskNode>): number {
|
|
78
|
+
let surfaced = 0
|
|
79
|
+
for (const node of nodes.values()) {
|
|
80
|
+
if (!node.requested || !isGroupTask(node)) continue
|
|
81
|
+
const project = node.projectName
|
|
82
|
+
const stack = [...node.deps]
|
|
83
|
+
const visited = new Set<string>()
|
|
84
|
+
while (stack.length > 0) {
|
|
85
|
+
const depId = stack.pop()!
|
|
86
|
+
if (visited.has(depId)) continue
|
|
87
|
+
visited.add(depId)
|
|
88
|
+
const dep = nodes.get(depId)
|
|
89
|
+
// Stay inside the requested project; `^`/cross-project deps are
|
|
90
|
+
// neither surfaced nor traversed.
|
|
91
|
+
if (!dep || dep.projectName !== project) continue
|
|
92
|
+
if (isGroupTask(dep)) {
|
|
93
|
+
// A nested group: keep descending, don't surface the group.
|
|
94
|
+
for (const next of dep.deps) stack.push(next)
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
if (dep.surfaced !== true) {
|
|
98
|
+
dep.surfaced = true
|
|
99
|
+
surfaced++
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return surfaced
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Expand the user-requested task list into concrete `{project, task}`
|
|
108
|
+
* pairs the graph builder consumes.
|
|
109
|
+
*
|
|
110
|
+
* - Bare task names (`'build'`) → one entry per project in
|
|
111
|
+
* `candidates` that declares the task. Missing in a given project
|
|
112
|
+
* is silent (sparse tasks are normal across a workspace).
|
|
113
|
+
* - Anchored entries (`'pkg#task'`) → one entry exactly, ignoring
|
|
114
|
+
* `candidates`. Silently dropped if pkg/task doesn't exist (the
|
|
115
|
+
* CLI's pre-validation catches malformed strings).
|
|
116
|
+
*
|
|
117
|
+
* Duplicates are deduped (a user might pass `vx run build pkg#build`).
|
|
118
|
+
*/
|
|
119
|
+
export function expandRequested(
|
|
120
|
+
tasks: readonly string[],
|
|
121
|
+
candidates: readonly string[],
|
|
122
|
+
projects: Map<string, ProjectEntry>,
|
|
123
|
+
): Array<{ project: string; task: string }> {
|
|
124
|
+
const seen = new Set<string>()
|
|
125
|
+
const out: Array<{ project: string; task: string }> = []
|
|
126
|
+
const push = (project: string, task: string): void => {
|
|
127
|
+
const key = `${project}#${task}`
|
|
128
|
+
if (seen.has(key)) return
|
|
129
|
+
seen.add(key)
|
|
130
|
+
out.push({ project, task })
|
|
131
|
+
}
|
|
132
|
+
for (const spec of tasks) {
|
|
133
|
+
const idx = spec.indexOf('#')
|
|
134
|
+
if (idx >= 0) {
|
|
135
|
+
const project = spec.slice(0, idx)
|
|
136
|
+
const task = spec.slice(idx + 1)
|
|
137
|
+
if (projects.get(project)?.config.tasks?.[task]) push(project, task)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
for (const name of candidates) {
|
|
141
|
+
if (projects.get(name)?.config.tasks?.[spec]) push(name, spec)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface BuildGraphOptions {
|
|
148
|
+
projects: Map<string, ProjectEntry>
|
|
149
|
+
packageGraph: PackageGraph
|
|
150
|
+
/** Initial set: `{ project, task }` pairs the user asked to run. */
|
|
151
|
+
requested: Array<{ project: string; task: string }>
|
|
152
|
+
/**
|
|
153
|
+
* Filter `dependsOn` expansion.
|
|
154
|
+
* - `undefined` → every dependsOn entry is followed (default).
|
|
155
|
+
* - `'all'` → no expansion; only the requested nodes are added.
|
|
156
|
+
* - `string[]` → expand normally, but drop edges whose target
|
|
157
|
+
* task name is in the list. `dependsOn.self` and
|
|
158
|
+
* `dependsOn.dependencies` are both filtered.
|
|
159
|
+
*/
|
|
160
|
+
excludeDependencies?: 'all' | readonly string[]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode> {
|
|
164
|
+
const { projects, packageGraph, requested, excludeDependencies } = options
|
|
165
|
+
const skipAll = excludeDependencies === 'all'
|
|
166
|
+
const skipNames =
|
|
167
|
+
Array.isArray(excludeDependencies) && excludeDependencies.length > 0
|
|
168
|
+
? new Set(excludeDependencies)
|
|
169
|
+
: null
|
|
170
|
+
const nodes = new Map<string, TaskNode>()
|
|
171
|
+
|
|
172
|
+
function addNode(projectName: string, taskName: string, requested: boolean): TaskNode | null {
|
|
173
|
+
const id = taskId(projectName, taskName)
|
|
174
|
+
const existing = nodes.get(id)
|
|
175
|
+
if (existing) {
|
|
176
|
+
// Promote an already-added node to requested if any caller asked
|
|
177
|
+
// for it directly. Once requested, never demoted.
|
|
178
|
+
if (requested) existing.requested = true
|
|
179
|
+
return existing
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const project = projects.get(projectName)
|
|
183
|
+
if (!project) return null
|
|
184
|
+
const taskConfig = project.config.tasks?.[taskName]
|
|
185
|
+
if (!taskConfig) return null
|
|
186
|
+
|
|
187
|
+
const node: TaskNode = {
|
|
188
|
+
id,
|
|
189
|
+
projectName,
|
|
190
|
+
projectDir: project.dir,
|
|
191
|
+
taskName,
|
|
192
|
+
config: taskConfig,
|
|
193
|
+
deps: [],
|
|
194
|
+
requested,
|
|
195
|
+
}
|
|
196
|
+
nodes.set(id, node)
|
|
197
|
+
|
|
198
|
+
if (skipAll) return node
|
|
199
|
+
|
|
200
|
+
const rawSpecs = taskConfig.dependsOn ?? []
|
|
201
|
+
for (const raw of rawSpecs) {
|
|
202
|
+
let spec: DependencySpec
|
|
203
|
+
try {
|
|
204
|
+
spec = parseDependencySpec(raw)
|
|
205
|
+
} catch (err) {
|
|
206
|
+
if (err instanceof DependencySpecError) {
|
|
207
|
+
throw new UserError(`Task ${id}: ${err.message}`)
|
|
208
|
+
}
|
|
209
|
+
throw err
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// dependsOn is about which tasks to ADD to the graph, not which
|
|
213
|
+
// to filter. BARE wildcards ("*"/"^*" = "everything upstream") and
|
|
214
|
+
// negation aren't meaningful here — they're cache.inputs.tasks
|
|
215
|
+
// operations. PARTIAL patterns (`build.*`, `^build.*`) are legal:
|
|
216
|
+
// they name a namespace of tasks to add (Nx 19.5 parity).
|
|
217
|
+
if (spec.kind === 'wildcardSelf' || spec.kind === 'wildcardDeps') {
|
|
218
|
+
throw new UserError(`Task ${id}: dependsOn does not accept bare wildcards (got "${raw}")`)
|
|
219
|
+
}
|
|
220
|
+
if (spec.negated) {
|
|
221
|
+
throw new UserError(`Task ${id}: dependsOn does not accept negation (got "${raw}")`)
|
|
222
|
+
}
|
|
223
|
+
if (spec.kind === 'cross' && (isTaskPattern(spec.task) || isTaskPattern(spec.project))) {
|
|
224
|
+
throw new UserError(
|
|
225
|
+
`Task ${id}: dependsOn patterns are not supported in the "pkg#task" form (got "${raw}")`,
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
// CLI `--excludeDependencies=name1,name2` drops edges whose target
|
|
229
|
+
// task name matches, regardless of bucket (self / deps / cross).
|
|
230
|
+
// Pattern specs re-apply the filter per EXPANDED name below.
|
|
231
|
+
if (skipNames?.has(spec.task)) continue
|
|
232
|
+
|
|
233
|
+
if (spec.kind === 'self') {
|
|
234
|
+
if (isTaskPattern(spec.task)) {
|
|
235
|
+
// `build.*` — every OTHER same-project task matching the
|
|
236
|
+
// pattern (the declaring task never matches itself — that
|
|
237
|
+
// would be an instant self-cycle). Zero matches is legal: a
|
|
238
|
+
// preset-spread pattern needn't match in every project.
|
|
239
|
+
const re = compileTaskPattern(spec.task)
|
|
240
|
+
for (const name of Object.keys(project.config.tasks ?? {})) {
|
|
241
|
+
if (name === taskName || !re.test(name)) continue
|
|
242
|
+
if (skipNames?.has(name)) continue
|
|
243
|
+
const child = addNode(projectName, name, false)
|
|
244
|
+
if (child) node.deps.push(child.id)
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// Missing target is a hard error — the user typed a name that
|
|
248
|
+
// doesn't resolve in this project.
|
|
249
|
+
const child = addNode(projectName, spec.task, false)
|
|
250
|
+
if (!child) {
|
|
251
|
+
throw new UserError(
|
|
252
|
+
`Task ${id} depends on ${taskId(projectName, spec.task)} but no such task is declared`,
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
node.deps.push(child.id)
|
|
256
|
+
}
|
|
257
|
+
} else if (spec.kind === 'deps') {
|
|
258
|
+
// Nearest-holder frontier (Turbo/Nx direct-deps parity +
|
|
259
|
+
// sparse bridging): walk the package dep graph from this
|
|
260
|
+
// project's direct deps; each path stops at the FIRST package
|
|
261
|
+
// declaring the task — a holder's own dependsOn is responsible
|
|
262
|
+
// for anything deeper. Packages without the task are passed
|
|
263
|
+
// through so a sparse dep doesn't break ordering to deeper
|
|
264
|
+
// holders. The visited set both dedupes shared subtrees and
|
|
265
|
+
// terminates on package-graph cycles (legal in PMs).
|
|
266
|
+
//
|
|
267
|
+
// With a pattern (`^build.*`), a holder is a package declaring
|
|
268
|
+
// AT LEAST ONE matching task and it receives edges to ALL its
|
|
269
|
+
// matches — holder-ness is about declaration, so a holder still
|
|
270
|
+
// stops the walk even when every match is --excludeDependencies'd.
|
|
271
|
+
const re = isTaskPattern(spec.task) ? compileTaskPattern(spec.task) : null
|
|
272
|
+
const visited = new Set<string>()
|
|
273
|
+
const frontier = [...packageGraph.directDeps(projectName)]
|
|
274
|
+
while (frontier.length > 0) {
|
|
275
|
+
const target = frontier.pop()!
|
|
276
|
+
if (visited.has(target)) continue
|
|
277
|
+
visited.add(target)
|
|
278
|
+
if (re === null) {
|
|
279
|
+
const child = addNode(target, spec.task, false)
|
|
280
|
+
if (child) node.deps.push(child.id)
|
|
281
|
+
else frontier.push(...packageGraph.directDeps(target))
|
|
282
|
+
} else {
|
|
283
|
+
const names = Object.keys(projects.get(target)?.config.tasks ?? {}).filter((n) =>
|
|
284
|
+
re.test(n),
|
|
285
|
+
)
|
|
286
|
+
if (names.length > 0) {
|
|
287
|
+
for (const name of names) {
|
|
288
|
+
if (skipNames?.has(name)) continue
|
|
289
|
+
const child = addNode(target, name, false)
|
|
290
|
+
if (child) node.deps.push(child.id)
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
frontier.push(...packageGraph.directDeps(target))
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
// Cross-project edge: pkg#task. Missing target is a hard error
|
|
299
|
+
// because the user named the package + task explicitly.
|
|
300
|
+
const child = addNode(spec.project, spec.task, false)
|
|
301
|
+
if (!child) {
|
|
302
|
+
throw new UserError(
|
|
303
|
+
`Task ${id} depends on ${taskId(spec.project, spec.task)} but no such project or task is declared`,
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
node.deps.push(child.id)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Stable ordering for deterministic scheduling and cache keys — deduped:
|
|
311
|
+
// a target named twice (an exact entry + a pattern matching it, or a
|
|
312
|
+
// literal duplicate) must contribute ONE edge, not a double-folded
|
|
313
|
+
// upstream hash and a doubled DOT edge.
|
|
314
|
+
node.deps = [...new Set(node.deps)].sort()
|
|
315
|
+
return node
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const { project, task } of requested) {
|
|
319
|
+
addNode(project, task, true)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
detectCycle(nodes)
|
|
323
|
+
return nodes
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function detectCycle(nodes: Map<string, TaskNode>): void {
|
|
327
|
+
// Iterative DFS over dense-indexed colors. Recursion + `Map<string,
|
|
328
|
+
// number>` worked, but deep workspaces (long chains of `dependsOn`)
|
|
329
|
+
// can blow V8's frame budget, and per-node Map lookups dominate the
|
|
330
|
+
// pass. A Uint8Array indexed by node-position is allocation-free
|
|
331
|
+
// and ~2× faster on cycle detection itself.
|
|
332
|
+
// 0 = WHITE (unvisited) — the typed array's zero-init default.
|
|
333
|
+
const GRAY = 1
|
|
334
|
+
const BLACK = 2
|
|
335
|
+
const idsArr = [...nodes.keys()]
|
|
336
|
+
const idIdx = new Map<string, number>()
|
|
337
|
+
for (let i = 0; i < idsArr.length; i++) idIdx.set(idsArr[i]!, i)
|
|
338
|
+
const color = new Uint8Array(idsArr.length)
|
|
339
|
+
|
|
340
|
+
// Each stack frame is `[idIndex, nextChildIdx]`. We mutate the
|
|
341
|
+
// child index in place as children are visited so we know which one
|
|
342
|
+
// to resume on when the leaf returns control here.
|
|
343
|
+
const stack: number[] = []
|
|
344
|
+
for (let start = 0; start < idsArr.length; start++) {
|
|
345
|
+
if (color[start] === BLACK) continue
|
|
346
|
+
stack.push(start, 0)
|
|
347
|
+
color[start] = GRAY
|
|
348
|
+
while (stack.length > 0) {
|
|
349
|
+
const childIdx = stack[stack.length - 1]!
|
|
350
|
+
const idx = stack[stack.length - 2]!
|
|
351
|
+
const node = nodes.get(idsArr[idx]!)
|
|
352
|
+
const deps = node ? node.deps : []
|
|
353
|
+
if (childIdx < deps.length) {
|
|
354
|
+
stack[stack.length - 1] = childIdx + 1
|
|
355
|
+
const depIdx = idIdx.get(deps[childIdx]!)
|
|
356
|
+
if (depIdx === undefined) continue
|
|
357
|
+
const c = color[depIdx]!
|
|
358
|
+
if (c === BLACK) continue
|
|
359
|
+
if (c === GRAY) {
|
|
360
|
+
// Reconstruct the cycle by walking the open stack until we
|
|
361
|
+
// hit the id that triggered the GRAY-on-GRAY hit.
|
|
362
|
+
const path: string[] = []
|
|
363
|
+
for (let i = 0; i < stack.length; i += 2) path.push(idsArr[stack[i]!]!)
|
|
364
|
+
const startInPath = path.indexOf(idsArr[depIdx]!)
|
|
365
|
+
const cycle = [...path.slice(startInPath), idsArr[depIdx]!].join(' -> ')
|
|
366
|
+
throw new UserError(`Cycle detected in task graph: ${cycle}`)
|
|
367
|
+
}
|
|
368
|
+
color[depIdx] = GRAY
|
|
369
|
+
stack.push(depIdx, 0)
|
|
370
|
+
} else {
|
|
371
|
+
color[idx] = BLACK
|
|
372
|
+
stack.pop()
|
|
373
|
+
stack.pop()
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Public API for @vzn/vx.
|
|
2
|
+
//
|
|
3
|
+
// This is the stable cross-package contract — any plugin or integration
|
|
4
|
+
// package imports everything it needs from here via the bare
|
|
5
|
+
// `'@vzn/vx'` specifier (never a deep `src/...` path). The surface is pinned
|
|
6
|
+
// by tests/package-boundaries.test.ts; a widening updates that snapshot
|
|
7
|
+
// deliberately. See docs/design/core-cloud-split-2026-06.md §3.5.
|
|
8
|
+
|
|
9
|
+
export { VERSION } from './version.js'
|
|
10
|
+
|
|
11
|
+
// Clean error type — user-input failures print a message without a stack.
|
|
12
|
+
export { UserError } from './util/index.js'
|
|
13
|
+
|
|
14
|
+
// Schema types and helpers (used by user vx.config files and presets).
|
|
15
|
+
export type {
|
|
16
|
+
WorkspaceConfig,
|
|
17
|
+
ProjectConfig,
|
|
18
|
+
TaskConfig,
|
|
19
|
+
ExecConfig,
|
|
20
|
+
ExecEnv,
|
|
21
|
+
CacheConfig,
|
|
22
|
+
CacheInputs,
|
|
23
|
+
CacheOutputs,
|
|
24
|
+
ResourcesConfig,
|
|
25
|
+
SandboxConfig,
|
|
26
|
+
SandboxNetworkConfig,
|
|
27
|
+
} from './config.js'
|
|
28
|
+
export { defineProject, defineWorkspace } from './config.js'
|
|
29
|
+
|
|
30
|
+
// Programmatic engine API (run / plan / prepare) + the graph primitives a
|
|
31
|
+
// distribution submitter/agent reasons over + the cache-key hashing seam.
|
|
32
|
+
// `deriveStableKeys` is THE shared stable-key derivation (remote-prefetch,
|
|
33
|
+
// the local short-circuit, and the distributed submitter must never drift
|
|
34
|
+
// on the stability gate); `captureGitContext`/`captureWorkspaceIdentity`
|
|
35
|
+
// give agents + the submitter identity before/without a telemetry run.
|
|
36
|
+
export { run, planRun, prepareRun } from './orchestrator/index.js'
|
|
37
|
+
export type { PreparedRun } from './orchestrator/index.js'
|
|
38
|
+
export { computeTaskHash, createHashCache, deriveStableKeys } from './orchestrator/index.js'
|
|
39
|
+
export type { DeriveStableKeysArgs, HashCache, StableKey } from './orchestrator/index.js'
|
|
40
|
+
export {
|
|
41
|
+
captureGitContext,
|
|
42
|
+
captureWorkspaceIdentity,
|
|
43
|
+
resolveCacheScope,
|
|
44
|
+
} from './orchestrator/index.js'
|
|
45
|
+
export type { GitContext, WorkspaceIdentity } from './orchestrator/index.js'
|
|
46
|
+
export { FULL_CACHE_POLICY, parseCachePolicy } from './orchestrator/index.js'
|
|
47
|
+
export type {
|
|
48
|
+
CachePolicy,
|
|
49
|
+
Logger,
|
|
50
|
+
OutputView,
|
|
51
|
+
RunOptions,
|
|
52
|
+
RunSummary,
|
|
53
|
+
} from './orchestrator/index.js'
|
|
54
|
+
export { defaultLogger, resolveOutputView } from './orchestrator/index.js'
|
|
55
|
+
export { buildTaskGraph, expandRequested, isGroupTask, markSurfacedDeps } from './graph/index.js'
|
|
56
|
+
export type {
|
|
57
|
+
OutputFingerprint,
|
|
58
|
+
TaskNode,
|
|
59
|
+
TaskOutcome,
|
|
60
|
+
TaskStatus,
|
|
61
|
+
VerifyVerdict,
|
|
62
|
+
} from './graph/index.js'
|
|
63
|
+
// The one output-tree diff implementation (verify verdicts + a serve's
|
|
64
|
+
// cross-machine fingerprint diff both name rels through it).
|
|
65
|
+
export { diffOutputTrees } from './orchestrator/index.js'
|
|
66
|
+
|
|
67
|
+
// Cache classes + the layer interface (the `cache` capability's currency)
|
|
68
|
+
// and input-output resolution. The blob-CAS/digest substrate
|
|
69
|
+
// (cas-backend.ts / digest.ts) stays module-internal until it has a
|
|
70
|
+
// consumer — no speculative public API. `cleanOutputs` is public for the
|
|
71
|
+
// distributed submitter's targeted output materialization (wipe declared
|
|
72
|
+
// outputs, then `restoreOutputs` the artifact — never a naive re-run).
|
|
73
|
+
export {
|
|
74
|
+
Cache,
|
|
75
|
+
LayeredCache,
|
|
76
|
+
GitFilesCache,
|
|
77
|
+
cleanOutputs,
|
|
78
|
+
resolveInputs,
|
|
79
|
+
resolveOutputs,
|
|
80
|
+
} from './cache/index.js'
|
|
81
|
+
export type { CacheLayer, RemoteCacheLayer, RunRecord, InvocationRecord } from './cache/index.js'
|
|
82
|
+
|
|
83
|
+
// Workspace discovery + the project/config catalog surface — an
|
|
84
|
+
// out-of-process service/CLI needs these. `readLockfile` is THE one reader
|
|
85
|
+
// of vx-lock.json (the format carries its own version sentinel; a second
|
|
86
|
+
// parser in a sibling package would drift), and the loader chain
|
|
87
|
+
// (`loadWorkspace` → `listProjectMetas` → `loadProjectConfig`) is the same
|
|
88
|
+
// one `vx show` uses. Workspace's `listProjects` re-exports as
|
|
89
|
+
// `listProjectMetas` — the bare name belongs to the metrics query below.
|
|
90
|
+
export { findWorkspaceRoot, loadWorkspaceConfig, resolveCacheDir } from './workspace/index.js'
|
|
91
|
+
export { readLockfile, LOCKFILE_NAME } from './workspace/index.js'
|
|
92
|
+
export type { Lockfile, LockfileEntry } from './workspace/index.js'
|
|
93
|
+
export {
|
|
94
|
+
loadWorkspace,
|
|
95
|
+
loadProjectConfig,
|
|
96
|
+
listProjects as listProjectMetas,
|
|
97
|
+
} from './workspace/index.js'
|
|
98
|
+
export type { ProjectMeta } from './workspace/index.js'
|
|
99
|
+
|
|
100
|
+
// Plugin API — the run-level extension points. Behavior capabilities
|
|
101
|
+
// (backend / cache) change WHAT/HOW work runs; the observe-only `telemetry`
|
|
102
|
+
// capability is the canonical data-export path and cannot change behavior.
|
|
103
|
+
// A plugin is declared in vx.workspace.ts via defineWorkspace({ plugins }).
|
|
104
|
+
// See docs/design/observability-architecture-2026-06.md.
|
|
105
|
+
export type {
|
|
106
|
+
VxPlugin,
|
|
107
|
+
EventSink,
|
|
108
|
+
BackendContext,
|
|
109
|
+
CacheContext,
|
|
110
|
+
EventSinkContext,
|
|
111
|
+
PluginSetupContext,
|
|
112
|
+
} from './orchestrator/index.js'
|
|
113
|
+
|
|
114
|
+
// Telemetry — THE canonical, versioned data-export contract every exporter
|
|
115
|
+
// (an OTel exporter, an HTTP sink, or any third-party consumer) reads. A sink implements TelemetrySink and is
|
|
116
|
+
// returned from VxPlugin.telemetry(); it receives immutable records and holds
|
|
117
|
+
// no run handle (observe-only by construction).
|
|
118
|
+
export { TELEMETRY_SCHEMA_VERSION, deriveCacheSource } from './orchestrator/index.js'
|
|
119
|
+
export type {
|
|
120
|
+
CacheSource,
|
|
121
|
+
RunContextRecord,
|
|
122
|
+
RunSummaryRecord,
|
|
123
|
+
TaskTelemetry,
|
|
124
|
+
TelemetryContext,
|
|
125
|
+
TelemetryRecord,
|
|
126
|
+
TelemetrySink,
|
|
127
|
+
} from './orchestrator/index.js'
|
|
128
|
+
|
|
129
|
+
// The submitter wire contract + backend interface (the `backend`
|
|
130
|
+
// capability's currency) and the serializable event projection.
|
|
131
|
+
export {
|
|
132
|
+
optionsToRequest,
|
|
133
|
+
requestToOptions,
|
|
134
|
+
projectNode,
|
|
135
|
+
projectOutcome,
|
|
136
|
+
createWireRenderer,
|
|
137
|
+
} from './orchestrator/index.js'
|
|
138
|
+
export type {
|
|
139
|
+
RunBackend,
|
|
140
|
+
RunRequest,
|
|
141
|
+
RunResult,
|
|
142
|
+
ClientMessage,
|
|
143
|
+
ServerMessage,
|
|
144
|
+
} from './orchestrator/index.js'
|
|
145
|
+
|
|
146
|
+
// Event bus + wire form — adapters (otel-bridge, custom subscribers) ride this.
|
|
147
|
+
export {
|
|
148
|
+
createEventBus,
|
|
149
|
+
wireForwarder,
|
|
150
|
+
toWireEvent,
|
|
151
|
+
createVxSurface,
|
|
152
|
+
} from './orchestrator/index.js'
|
|
153
|
+
export type {
|
|
154
|
+
EventBus,
|
|
155
|
+
RunEvent,
|
|
156
|
+
RunEventSubscriber,
|
|
157
|
+
WireEvent,
|
|
158
|
+
TaskView,
|
|
159
|
+
OutcomeView,
|
|
160
|
+
} from './orchestrator/index.js'
|
|
161
|
+
|
|
162
|
+
// The JSON-RPC 2.0 wire envelope — an out-of-process serve/agent speaks this framing.
|
|
163
|
+
export {
|
|
164
|
+
clientMessageToEnvelope,
|
|
165
|
+
decodeEnvelope,
|
|
166
|
+
encodeForNDJSON,
|
|
167
|
+
encodeForSSE,
|
|
168
|
+
encodeForWS,
|
|
169
|
+
ENVELOPE_ERRORS,
|
|
170
|
+
envelopeToClientMessage,
|
|
171
|
+
envelopeToServerMessage,
|
|
172
|
+
isEnvelope,
|
|
173
|
+
isNotification,
|
|
174
|
+
isRequest,
|
|
175
|
+
makeError,
|
|
176
|
+
makeNotification,
|
|
177
|
+
makeRequest,
|
|
178
|
+
makeResponse,
|
|
179
|
+
serverMessageToEnvelope,
|
|
180
|
+
WIRE_CHANNELS,
|
|
181
|
+
WIRE_PROTOCOL_VERSION,
|
|
182
|
+
} from './orchestrator/index.js'
|
|
183
|
+
export type {
|
|
184
|
+
Envelope,
|
|
185
|
+
ErrorResponse,
|
|
186
|
+
Notification,
|
|
187
|
+
WireRequest,
|
|
188
|
+
WireResponse,
|
|
189
|
+
WireChannel,
|
|
190
|
+
} from './orchestrator/index.js'
|
|
191
|
+
|
|
192
|
+
// Metrics / analytics query layer over cache.db — a service plugin's HTTP
|
|
193
|
+
// surface reads from these (the queries stay in core; a serve exposes them over HTTP).
|
|
194
|
+
export {
|
|
195
|
+
cacheKeyDiff,
|
|
196
|
+
compareRuns,
|
|
197
|
+
explainCacheKeyQuery,
|
|
198
|
+
getBottlenecks,
|
|
199
|
+
getCacheBreakdown,
|
|
200
|
+
getCacheSavings,
|
|
201
|
+
getCacheStatsSql,
|
|
202
|
+
getFlakiestTasks,
|
|
203
|
+
getHistory,
|
|
204
|
+
getHitRateSplit,
|
|
205
|
+
getInvocation,
|
|
206
|
+
getParallelismHistory,
|
|
207
|
+
getPeriodComparison,
|
|
208
|
+
getPrunableEntries,
|
|
209
|
+
getRecentFailures,
|
|
210
|
+
getRegressions,
|
|
211
|
+
getRun,
|
|
212
|
+
getRunHeatmap,
|
|
213
|
+
getRunTrends,
|
|
214
|
+
getStorageGrowth,
|
|
215
|
+
getTaskDetail,
|
|
216
|
+
getTopTimeBurners,
|
|
217
|
+
listCacheEntries,
|
|
218
|
+
listInvocations,
|
|
219
|
+
listProjects,
|
|
220
|
+
listRuns,
|
|
221
|
+
whyDidThisRerunQuery,
|
|
222
|
+
} from './orchestrator/index.js'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Tiny color helper — gates Bun.color through a `ColorSupport` token
|
|
2
|
+
// so the formatters stay pure. The token is computed once at logger
|
|
3
|
+
// construction; tests pass `{ enabled: false }` so assertions match
|
|
4
|
+
// plain strings.
|
|
5
|
+
//
|
|
6
|
+
// Bun.color('red', 'ansi') is broken in current Bun versions (emits
|
|
7
|
+
// the color number as a raw byte instead of decimal text), so we use
|
|
8
|
+
// 'ansi-16m' which emits 24-bit truecolor sequences. Every modern
|
|
9
|
+
// terminal supports them, and the `tput colors`-style downgrade isn't
|
|
10
|
+
// worth the complexity for our handful of UI accents.
|
|
11
|
+
|
|
12
|
+
const RESET = '\x1b[0m'
|
|
13
|
+
const BOLD = '\x1b[1m'
|
|
14
|
+
const DIM = '\x1b[2m'
|
|
15
|
+
|
|
16
|
+
export interface ColorSupport {
|
|
17
|
+
enabled: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PaintOptions {
|
|
21
|
+
bold?: boolean
|
|
22
|
+
dim?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Standard env precedence:
|
|
27
|
+
* NO_COLOR=anything → off (overrides FORCE_COLOR)
|
|
28
|
+
* FORCE_COLOR=anything → on
|
|
29
|
+
* else: on iff stdout is a TTY.
|
|
30
|
+
*/
|
|
31
|
+
export function detectColors(stream: NodeJS.WriteStream = process.stdout): ColorSupport {
|
|
32
|
+
if (process.env['NO_COLOR']) return { enabled: false }
|
|
33
|
+
if (process.env['FORCE_COLOR']) return { enabled: true }
|
|
34
|
+
return { enabled: stream.isTTY === true }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Bun.color() is pure for a given (hex, mode) pair but does FFI work
|
|
38
|
+
// on each call. The framed-output renderer paints the same handful of
|
|
39
|
+
// accent colors thousands of times per run; memoizing here turns those
|
|
40
|
+
// into hash-map hits.
|
|
41
|
+
const ansiCache = new Map<string, string>()
|
|
42
|
+
function ansiFor(color: string): string {
|
|
43
|
+
const cached = ansiCache.get(color)
|
|
44
|
+
if (cached !== undefined) return cached
|
|
45
|
+
const v = Bun.color(color, 'ansi-16m') ?? ''
|
|
46
|
+
ansiCache.set(color, v)
|
|
47
|
+
return v
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function paint(
|
|
51
|
+
color: string,
|
|
52
|
+
text: string,
|
|
53
|
+
colors: ColorSupport,
|
|
54
|
+
opts: PaintOptions = {},
|
|
55
|
+
): string {
|
|
56
|
+
if (!colors.enabled) return text
|
|
57
|
+
let prefix = ''
|
|
58
|
+
if (opts.bold) prefix += BOLD
|
|
59
|
+
if (opts.dim) prefix += DIM
|
|
60
|
+
if (color) prefix += ansiFor(color)
|
|
61
|
+
if (prefix.length === 0) return text
|
|
62
|
+
return `${prefix}${text}${RESET}`
|
|
63
|
+
}
|