@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.
- package/LICENSE +21 -0
- package/README.md +332 -1
- package/launcher.mjs +84 -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 +29 -0
- package/src/cache/inputs.ts +833 -0
- package/src/cache/layered-cache.ts +335 -0
- package/src/cache/remote-cache.ts +251 -0
- package/src/cache/tar.ts +343 -0
- package/src/cli/backend.ts +23 -0
- package/src/cli/cache.ts +101 -0
- package/src/cli/format.ts +15 -0
- package/src/cli/help.ts +123 -0
- package/src/cli/index.ts +85 -0
- package/src/cli/info.ts +83 -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 +661 -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 +421 -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 +69 -0
- package/src/graph/index.ts +18 -0
- package/src/graph/scheduler.ts +482 -0
- package/src/graph/task-graph.ts +325 -0
- package/src/index.ts +197 -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 +883 -0
- package/src/orchestrator/framed-output.ts +461 -0
- package/src/orchestrator/history.ts +148 -0
- package/src/orchestrator/index.ts +163 -0
- package/src/orchestrator/local-shortcircuit.ts +147 -0
- package/src/orchestrator/logger.ts +526 -0
- package/src/orchestrator/metrics.ts +1576 -0
- package/src/orchestrator/options.ts +176 -0
- package/src/orchestrator/plan.ts +129 -0
- package/src/orchestrator/plugin-host.ts +190 -0
- package/src/orchestrator/plugin.ts +283 -0
- package/src/orchestrator/predict.ts +106 -0
- package/src/orchestrator/prepare.ts +297 -0
- package/src/orchestrator/protocol.ts +142 -0
- package/src/orchestrator/remote-cache-setup.ts +60 -0
- package/src/orchestrator/remote-prefetch.ts +81 -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 +842 -0
- package/src/orchestrator/stable-keys.ts +177 -0
- package/src/orchestrator/status-line.ts +287 -0
- package/src/orchestrator/summary.ts +291 -0
- package/src/orchestrator/tally.ts +69 -0
- package/src/orchestrator/task-hash.ts +238 -0
- package/src/orchestrator/telemetry-host.ts +80 -0
- package/src/orchestrator/telemetry.ts +350 -0
- package/src/orchestrator/upstream.ts +93 -0
- package/src/orchestrator/verify.ts +141 -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 +7 -0
- package/src/util/paths.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 +470 -0
- package/src/workspace/workspace.ts +176 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// vx-lock.json — frozen resolved-config lockfile.
|
|
2
|
+
//
|
|
3
|
+
// `vx lock` evaluates every project's vx.config.* in the CURRENT
|
|
4
|
+
// environment and freezes the resolved objects (plus a content hash of
|
|
5
|
+
// each config file) into `vx-lock.json` at the workspace root. While the
|
|
6
|
+
// lock exists, runs load configs from it instead of evaluating —
|
|
7
|
+
// frozen-env semantics. See docs/design/config-lock-2026-06.md.
|
|
8
|
+
//
|
|
9
|
+
// Verification asymmetry (deliberate):
|
|
10
|
+
// - runs TRUST the lock: hash-only staleness check, eval-free.
|
|
11
|
+
// - `vx lock --check` AUDITS it: full re-evaluation + deep equality,
|
|
12
|
+
// which catches eval-time env drift that file hashes cannot see.
|
|
13
|
+
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
import type { ProjectConfig } from '../config.js'
|
|
16
|
+
import { relPosix, UserError } from '../util/index.js'
|
|
17
|
+
import { validateProjectConfig } from './project-loader.js'
|
|
18
|
+
|
|
19
|
+
export const LOCKFILE_NAME = 'vx-lock.json'
|
|
20
|
+
export const LOCKFILE_VERSION = 1
|
|
21
|
+
|
|
22
|
+
export interface LockfileEntry {
|
|
23
|
+
/** Workspace-root-relative POSIX path to the project's config file. */
|
|
24
|
+
configPath: string
|
|
25
|
+
/** xxh3 hex of the config file bytes at lock time. */
|
|
26
|
+
configHash: string
|
|
27
|
+
/** The resolved (post-evaluation) config object, JSON-normalized. */
|
|
28
|
+
config: ProjectConfig
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Lockfile {
|
|
32
|
+
version: number
|
|
33
|
+
projects: Record<string, LockfileEntry>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function lockfilePath(root: string): string {
|
|
37
|
+
return path.join(root, LOCKFILE_NAME)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read and shape-validate `vx-lock.json`. Returns `null` when no lock
|
|
42
|
+
* exists (the common case — the feature is opt-in). The lock is a
|
|
43
|
+
* user-editable file, so this is a system boundary: malformed content
|
|
44
|
+
* throws a `UserError` instead of crashing deeper in the run.
|
|
45
|
+
*/
|
|
46
|
+
export async function readLockfile(root: string): Promise<Lockfile | null> {
|
|
47
|
+
const file = Bun.file(lockfilePath(root))
|
|
48
|
+
if (!(await file.exists())) return null
|
|
49
|
+
let parsed: unknown
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(await file.text())
|
|
52
|
+
} catch {
|
|
53
|
+
throw new UserError(`${LOCKFILE_NAME} is not valid JSON — re-run \`vx lock\` or delete it`)
|
|
54
|
+
}
|
|
55
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
56
|
+
throw new UserError(`${LOCKFILE_NAME} must be a JSON object — re-run \`vx lock\``)
|
|
57
|
+
}
|
|
58
|
+
const lock = parsed as { version?: unknown; projects?: unknown }
|
|
59
|
+
if (lock.version !== LOCKFILE_VERSION) {
|
|
60
|
+
throw new UserError(
|
|
61
|
+
`${LOCKFILE_NAME} has unsupported version ${String(lock.version)} ` +
|
|
62
|
+
`(this vx expects ${LOCKFILE_VERSION}) — re-run \`vx lock\``,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
if (typeof lock.projects !== 'object' || lock.projects === null) {
|
|
66
|
+
throw new UserError(`${LOCKFILE_NAME}: \`projects\` must be an object — re-run \`vx lock\``)
|
|
67
|
+
}
|
|
68
|
+
for (const [name, entry] of Object.entries(lock.projects)) {
|
|
69
|
+
const e = entry as Partial<LockfileEntry> | null
|
|
70
|
+
if (
|
|
71
|
+
!e ||
|
|
72
|
+
typeof e.configPath !== 'string' ||
|
|
73
|
+
typeof e.configHash !== 'string' ||
|
|
74
|
+
typeof e.config !== 'object' ||
|
|
75
|
+
e.config === null
|
|
76
|
+
) {
|
|
77
|
+
throw new UserError(`${LOCKFILE_NAME}: entry for "${name}" is malformed — re-run \`vx lock\``)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return parsed as Lockfile
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function writeLockfile(root: string, lock: Lockfile): Promise<void> {
|
|
84
|
+
await Bun.write(lockfilePath(root), `${JSON.stringify(lock, null, 2)}\n`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Run-time config load from the lock: verify the config file's content
|
|
89
|
+
* hash against the lock entry, then return the FROZEN resolved config.
|
|
90
|
+
* No evaluation happens — this is the fast, eval-free trust path. A
|
|
91
|
+
* changed file or an unlocked project is a hard error: the lock's
|
|
92
|
+
* contract is "what runs is what was locked", and silently falling
|
|
93
|
+
* back to evaluation would break frozen-env semantics.
|
|
94
|
+
*/
|
|
95
|
+
export async function frozenProjectConfig(
|
|
96
|
+
lock: Lockfile,
|
|
97
|
+
meta: { name: string; configPath: string },
|
|
98
|
+
root: string,
|
|
99
|
+
): Promise<ProjectConfig> {
|
|
100
|
+
const rel = relPosix(root, meta.configPath)
|
|
101
|
+
const entry = lock.projects[meta.name]
|
|
102
|
+
if (!entry || entry.configPath !== rel) {
|
|
103
|
+
throw new UserError(
|
|
104
|
+
`${LOCKFILE_NAME} has no entry for "${meta.name}" (${rel}) — ` +
|
|
105
|
+
`run \`vx lock\` to refresh, or delete ${LOCKFILE_NAME}`,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
// No staleness checks here, deliberately: --frozen runs after
|
|
109
|
+
// `vx lock --check` in any sane pipeline, and that audit re-
|
|
110
|
+
// evaluates everything — a per-file byte-hash re-check would be
|
|
111
|
+
// redundant work and a weaker guarantee pretending to add safety
|
|
112
|
+
// (it can't see import closures or env anyway). configHash stays
|
|
113
|
+
// in the file solely for --check's fast file-changed reporting.
|
|
114
|
+
// The lock is hand-editable; the stored config crosses the same
|
|
115
|
+
// boundary a freshly evaluated one does.
|
|
116
|
+
validateProjectConfig(entry.config, `${LOCKFILE_NAME} (${meta.name})`)
|
|
117
|
+
return entry.config
|
|
118
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import type { ProjectEntry } from './workspace.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* For each project, the absolute dirs of other projects that live
|
|
6
|
+
* underneath it. Used to enforce project-boundary isolation: a
|
|
7
|
+
* project's task cannot see files inside another project, even if its
|
|
8
|
+
* globs would otherwise match.
|
|
9
|
+
*
|
|
10
|
+
* Sort once, then scan: nested projects of `p` form a contiguous run
|
|
11
|
+
* immediately after `p` in the sorted order (any non-descendant breaks
|
|
12
|
+
* the prefix-match). O(P log P) instead of the naive O(P²) cross
|
|
13
|
+
* product. Stable for any project set with no dir collisions, which
|
|
14
|
+
* the workspace loader already enforces.
|
|
15
|
+
*/
|
|
16
|
+
export function computeNestedProjectDirs(
|
|
17
|
+
entries: Array<Pick<ProjectEntry, 'name' | 'dir'>>,
|
|
18
|
+
): Map<string, string[]> {
|
|
19
|
+
const result = new Map<string, string[]>()
|
|
20
|
+
if (entries.length === 0) return result
|
|
21
|
+
const sorted = [...entries].sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0))
|
|
22
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
23
|
+
const p = sorted[i]!
|
|
24
|
+
const prefix = p.dir + path.sep
|
|
25
|
+
const nested: string[] = []
|
|
26
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
27
|
+
const other = sorted[j]!
|
|
28
|
+
if (!other.dir.startsWith(prefix)) break
|
|
29
|
+
nested.push(other.dir)
|
|
30
|
+
}
|
|
31
|
+
result.set(p.name, nested)
|
|
32
|
+
}
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ProjectMeta } from './workspace.js'
|
|
2
|
+
|
|
3
|
+
export interface PackageGraph {
|
|
4
|
+
/** Immediate workspace deps (sorted). */
|
|
5
|
+
directDeps: (name: string) => string[]
|
|
6
|
+
/** All transitive workspace deps for each project. */
|
|
7
|
+
transitiveDeps: (name: string) => string[]
|
|
8
|
+
/** All transitive workspace dependents (packages that depend on the named one). */
|
|
9
|
+
transitiveDependents: (name: string) => string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildPackageGraph(projects: ProjectMeta[]): PackageGraph {
|
|
13
|
+
const byName = new Map<string, ProjectMeta>()
|
|
14
|
+
for (const p of projects) byName.set(p.name, p)
|
|
15
|
+
|
|
16
|
+
const directDeps = new Map<string, string[]>()
|
|
17
|
+
for (const p of projects) {
|
|
18
|
+
const seen = new Set<string>()
|
|
19
|
+
for (const field of [
|
|
20
|
+
'dependencies',
|
|
21
|
+
'devDependencies',
|
|
22
|
+
'peerDependencies',
|
|
23
|
+
'optionalDependencies',
|
|
24
|
+
] as const) {
|
|
25
|
+
const obj = p.packageJson[field]
|
|
26
|
+
if (!obj) continue
|
|
27
|
+
for (const name of Object.keys(obj)) {
|
|
28
|
+
if (name === p.name) continue
|
|
29
|
+
if (byName.has(name)) seen.add(name)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
directDeps.set(p.name, [...seen].sort())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Reverse adjacency: who declares X as a workspace dep.
|
|
36
|
+
const directDependents = new Map<string, string[]>()
|
|
37
|
+
for (const [name, deps] of directDeps) {
|
|
38
|
+
for (const d of deps) {
|
|
39
|
+
const arr = directDependents.get(d)
|
|
40
|
+
if (arr) arr.push(name)
|
|
41
|
+
else directDependents.set(d, [name])
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const arr of directDependents.values()) arr.sort()
|
|
45
|
+
|
|
46
|
+
// Set-union DFS closures are O(P²) entries on dense layered graphs
|
|
47
|
+
// (same disease the scheduler's reachOf had — 68 ms at 1090
|
|
48
|
+
// projects). Bitset closures swept in topo order are O(E·P/32),
|
|
49
|
+
// and indexing projects in sorted-name order means materializing a
|
|
50
|
+
// closure is a single ascending bit-scan — already sorted, no
|
|
51
|
+
// per-call sort. Package graphs (unlike task graphs) may legally
|
|
52
|
+
// contain cycles; the bitset sweep requires a DAG, so a cycle
|
|
53
|
+
// (detected by the Kahn pass not draining) falls back wholesale to
|
|
54
|
+
// the legacy DFS — byte-identical behavior for that rare case.
|
|
55
|
+
const names = [...byName.keys()].sort()
|
|
56
|
+
const index = new Map<string, number>()
|
|
57
|
+
for (let i = 0; i < names.length; i++) index.set(names[i]!, i)
|
|
58
|
+
|
|
59
|
+
function bitsetClosures(edges: Map<string, string[]>): Uint32Array | null {
|
|
60
|
+
const n = names.length
|
|
61
|
+
const words = (n + 31) >>> 5
|
|
62
|
+
const adj: number[][] = Array.from({ length: n }, () => [])
|
|
63
|
+
const indegree = new Uint32Array(n)
|
|
64
|
+
for (const [from, tos] of edges) {
|
|
65
|
+
const fi = index.get(from)!
|
|
66
|
+
for (const to of tos) {
|
|
67
|
+
const ti = index.get(to)
|
|
68
|
+
if (ti === undefined) continue
|
|
69
|
+
adj[fi]!.push(ti) // edge from → to; closure(from) ⊇ {to} ∪ closure(to)
|
|
70
|
+
indegree[ti]!++
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const topo = new Int32Array(n)
|
|
74
|
+
let head = 0
|
|
75
|
+
let tail = 0
|
|
76
|
+
for (let i = 0; i < n; i++) if (indegree[i] === 0) topo[tail++] = i
|
|
77
|
+
while (head < tail) {
|
|
78
|
+
const v = topo[head++]!
|
|
79
|
+
for (const t of adj[v]!) if (--indegree[t]! === 0) topo[tail++] = t
|
|
80
|
+
}
|
|
81
|
+
if (tail < n) return null // cycle — caller falls back to DFS
|
|
82
|
+
const closure = new Uint32Array(n * words)
|
|
83
|
+
for (let t = tail - 1; t >= 0; t--) {
|
|
84
|
+
const i = topo[t]!
|
|
85
|
+
const base = i * words
|
|
86
|
+
for (const d of adj[i]!) {
|
|
87
|
+
closure[base + (d >>> 5)]! |= 1 << (d & 31)
|
|
88
|
+
const dbase = d * words
|
|
89
|
+
for (let w = 0; w < words; w++) closure[base + w]! |= closure[dbase + w]!
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return closure
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function legacyTransitive(
|
|
96
|
+
name: string,
|
|
97
|
+
edges: Map<string, string[]>,
|
|
98
|
+
cache: Map<string, string[]>,
|
|
99
|
+
stack: Set<string> = new Set(),
|
|
100
|
+
): string[] {
|
|
101
|
+
const cached = cache.get(name)
|
|
102
|
+
if (cached) return cached
|
|
103
|
+
if (stack.has(name)) return []
|
|
104
|
+
stack.add(name)
|
|
105
|
+
const out = new Set<string>()
|
|
106
|
+
for (const d of edges.get(name) ?? []) {
|
|
107
|
+
out.add(d)
|
|
108
|
+
for (const t of legacyTransitive(d, edges, cache, stack)) out.add(t)
|
|
109
|
+
}
|
|
110
|
+
stack.delete(name)
|
|
111
|
+
const result = [...out].sort()
|
|
112
|
+
cache.set(name, result)
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function makeAccessor(edges: Map<string, string[]>): (name: string) => string[] {
|
|
117
|
+
const closures = bitsetClosures(edges)
|
|
118
|
+
const memo = new Map<string, string[]>()
|
|
119
|
+
if (closures === null) {
|
|
120
|
+
return (name) => legacyTransitive(name, edges, memo)
|
|
121
|
+
}
|
|
122
|
+
const words = (names.length + 31) >>> 5
|
|
123
|
+
return (name) => {
|
|
124
|
+
const cached = memo.get(name)
|
|
125
|
+
if (cached) return cached
|
|
126
|
+
const i = index.get(name)
|
|
127
|
+
if (i === undefined) return []
|
|
128
|
+
const base = i * words
|
|
129
|
+
const out: string[] = []
|
|
130
|
+
for (let w = 0; w < words; w++) {
|
|
131
|
+
let v = closures[base + w]!
|
|
132
|
+
while (v !== 0) {
|
|
133
|
+
const bit = 31 - Math.clz32(v & -v)
|
|
134
|
+
out.push(names[(w << 5) + bit]!)
|
|
135
|
+
v &= v - 1
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
memo.set(name, out)
|
|
139
|
+
return out
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
directDeps: (name) => directDeps.get(name) ?? [],
|
|
145
|
+
transitiveDeps: makeAccessor(directDeps),
|
|
146
|
+
transitiveDependents: makeAccessor(directDependents),
|
|
147
|
+
}
|
|
148
|
+
}
|