@vzn/vx 0.0.18 → 0.0.21
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 +1 -1
- package/README.md +99 -65
- package/package.json +10 -20
- package/src/bin.ts +12 -2
- package/src/cache/archive.ts +120 -18
- package/src/cache/cache.ts +313 -1324
- package/src/cache/chained-cache.ts +0 -8
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +16 -7
- package/src/cache/inputs.ts +246 -733
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +12 -17
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +39 -11
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +64 -36
- package/src/cli/index.ts +54 -15
- package/src/cli/info.ts +120 -91
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +38 -6
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +14 -3
- package/src/cli/plugin-commands.ts +15 -8
- package/src/cli/run.ts +84 -302
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +2 -1
- package/src/cli/watch.ts +647 -97
- package/src/cli/why.ts +44 -14
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +156 -136
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +11 -26
- package/src/exec/index.ts +6 -2
- package/src/{plugins/local-executor/index.ts → exec/local-executor.ts} +12 -16
- package/src/exec/runner.ts +57 -18
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +582 -370
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +1 -9
- package/src/graph/scheduler.ts +158 -203
- package/src/graph/task-graph.ts +2 -2
- package/src/index.ts +97 -102
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +3 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/events.ts +42 -8
- package/src/orchestrator/execute-task.ts +145 -554
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/framed-output.ts +41 -36
- package/src/orchestrator/history.ts +118 -98
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +35 -4
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +13 -22
- package/src/orchestrator/metrics.ts +53 -20
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +34 -31
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plugin-host.ts +195 -38
- package/src/orchestrator/plugin.ts +162 -9
- package/src/orchestrator/prepare.ts +77 -132
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-artifacts.ts +25 -3
- package/src/orchestrator/run-context.ts +1 -1
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +2 -2
- package/src/orchestrator/run.ts +246 -597
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +60 -3
- package/src/orchestrator/status-line.ts +2 -1
- package/src/orchestrator/summary.ts +68 -25
- package/src/orchestrator/task-hash.ts +20 -18
- package/src/orchestrator/task-log-buffer.ts +8 -11
- package/src/orchestrator/telemetry-host.ts +11 -1
- package/src/orchestrator/telemetry.ts +3 -17
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/index.ts +11 -3
- package/src/util/paths.ts +23 -2
- package/src/util/settle.ts +19 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +111 -11
- package/src/workspace/config-cache.ts +11 -2
- package/src/workspace/config-eval.ts +11 -4
- package/src/workspace/config-imports.ts +5 -4
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +43 -6
- package/src/workspace/fingerprint.ts +37 -3
- package/src/workspace/index.ts +30 -3
- package/src/workspace/lockfile.ts +12 -7
- package/src/{cli → workspace}/migrate-scripts.ts +26 -19
- package/src/{cli/migrate.ts → workspace/migration.ts} +133 -124
- package/src/workspace/package-graph.ts +98 -28
- package/src/workspace/project-loader.ts +76 -746
- package/src/workspace/workspace.ts +98 -13
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/schedule-history/index.ts +0 -109
package/src/workspace/filter.ts
CHANGED
|
@@ -34,6 +34,10 @@ export interface ParsedFilter {
|
|
|
34
34
|
* to a concrete set of project names before calling applyFilters.
|
|
35
35
|
*/
|
|
36
36
|
gitSince?: string
|
|
37
|
+
/** A path form carrying a glob (`./packages/*`): matched over the root-relative project dir. */
|
|
38
|
+
pathGlob?: Bun.Glob
|
|
39
|
+
/** The workspace root `pathGlob` is relative to. */
|
|
40
|
+
pathRoot?: string
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export function parseFilter(raw: string, workspaceRoot: string): ParsedFilter {
|
|
@@ -71,15 +75,35 @@ export function parseFilter(raw: string, workspaceRoot: string): ParsedFilter {
|
|
|
71
75
|
|
|
72
76
|
let isPath = false
|
|
73
77
|
let matcher = s
|
|
74
|
-
|
|
78
|
+
let pathGlob: Bun.Glob | undefined
|
|
79
|
+
const pathForm =
|
|
80
|
+
s.startsWith('./') || s === '.'
|
|
81
|
+
? s
|
|
82
|
+
: s.startsWith('{') && s.endsWith('}')
|
|
83
|
+
? s.slice(1, -1)
|
|
84
|
+
: undefined
|
|
85
|
+
if (pathForm !== undefined) {
|
|
75
86
|
isPath = true
|
|
76
|
-
matcher = path.resolve(workspaceRoot,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
matcher = path.resolve(workspaceRoot, pathForm)
|
|
88
|
+
// `./packages/*` — pnpm's and Turbo's spelling for "every package under
|
|
89
|
+
// packages": a glob over the root-relative project dir. Resolved
|
|
90
|
+
// against the workspace root like the literal form.
|
|
91
|
+
if (/[*?[\]{}]/.test(pathForm)) {
|
|
92
|
+
const rel = path.relative(workspaceRoot, matcher).split(path.sep).join('/')
|
|
93
|
+
pathGlob = new Bun.Glob(rel.replace(/\/+$/, ''))
|
|
94
|
+
}
|
|
80
95
|
}
|
|
81
96
|
|
|
82
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
raw,
|
|
99
|
+
negate,
|
|
100
|
+
withDeps,
|
|
101
|
+
withDependents,
|
|
102
|
+
onlyDeps,
|
|
103
|
+
isPath,
|
|
104
|
+
matcher,
|
|
105
|
+
...(pathGlob !== undefined ? { pathGlob, pathRoot: workspaceRoot } : {}),
|
|
106
|
+
}
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
function matchProjects(
|
|
@@ -95,6 +119,19 @@ function matchProjects(
|
|
|
95
119
|
}
|
|
96
120
|
const out: string[] = []
|
|
97
121
|
if (filter.isPath) {
|
|
122
|
+
if (filter.pathGlob !== undefined) {
|
|
123
|
+
// The glob is matched against the project's own dir, as pnpm and
|
|
124
|
+
// Turbo do: `./packages/*` is the packages directly under `packages`,
|
|
125
|
+
// `./packages/**` reaches the nested ones too.
|
|
126
|
+
for (const p of projects) {
|
|
127
|
+
const rel = path
|
|
128
|
+
.relative(filter.pathRoot ?? '', p.dir)
|
|
129
|
+
.split(path.sep)
|
|
130
|
+
.join('/')
|
|
131
|
+
if (filter.pathGlob.match(rel)) out.push(p.name)
|
|
132
|
+
}
|
|
133
|
+
return out
|
|
134
|
+
}
|
|
98
135
|
const prefix = filter.matcher + path.sep
|
|
99
136
|
for (const p of projects) {
|
|
100
137
|
if (p.dir === filter.matcher || p.dir.startsWith(prefix)) out.push(p.name)
|
|
@@ -44,13 +44,47 @@ export const WORKSPACE_FINGERPRINT_FILES = [
|
|
|
44
44
|
* invalidates every cached entry. Coarse but correct.
|
|
45
45
|
*/
|
|
46
46
|
export async function computeWorkspaceFingerprint(workspaceRoot: string): Promise<string> {
|
|
47
|
-
|
|
47
|
+
return (await computeWorkspaceFingerprints(workspaceRoot, NONE)).all
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const NONE: ReadonlySet<string> = new Set()
|
|
51
|
+
|
|
52
|
+
export interface WorkspaceFingerprints {
|
|
53
|
+
/** Every file folded — what the config-evaluation cache keys on. */
|
|
54
|
+
readonly all: string
|
|
55
|
+
/**
|
|
56
|
+
* The same fold minus the files a plugin claims (`VxPlugin.fingerprint`)
|
|
57
|
+
* — what every task key folds. Identical to `all` with nothing claimed.
|
|
58
|
+
*/
|
|
59
|
+
readonly unclaimed: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Both digests from one read of each file. A claimed file leaves the key
|
|
64
|
+
* digest entirely (its name too): the claimant folds what it means per
|
|
65
|
+
* project, and a fold of "present" would still re-key the workspace when
|
|
66
|
+
* the file appeared.
|
|
67
|
+
*/
|
|
68
|
+
export async function computeWorkspaceFingerprints(
|
|
69
|
+
workspaceRoot: string,
|
|
70
|
+
claimed: ReadonlySet<string>,
|
|
71
|
+
): Promise<WorkspaceFingerprints> {
|
|
72
|
+
let all = 0n
|
|
73
|
+
let unclaimed = 0n
|
|
48
74
|
for (const f of WORKSPACE_FINGERPRINT_FILES) {
|
|
49
75
|
const full = path.join(workspaceRoot, f)
|
|
50
76
|
const file = Bun.file(full)
|
|
51
77
|
if (!(await file.exists())) continue
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
const bytes = await file.bytes()
|
|
79
|
+
all = xxh3(`${f}\0`, all)
|
|
80
|
+
all = xxh3(bytes, all)
|
|
81
|
+
if (claimed.has(f)) continue
|
|
82
|
+
unclaimed = xxh3(`${f}\0`, unclaimed)
|
|
83
|
+
unclaimed = xxh3(bytes, unclaimed)
|
|
54
84
|
}
|
|
85
|
+
return { all: hex(all), unclaimed: hex(unclaimed) }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hex(h: bigint): string {
|
|
55
89
|
return h.toString(16).padStart(16, '0')
|
|
56
90
|
}
|
package/src/workspace/index.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
// Module contract. Cross-module imports must come through here; see
|
|
2
2
|
// docs/design/module-isolation-2026-06.md and tests/module-boundaries.test.ts.
|
|
3
3
|
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
affectedProjects,
|
|
6
|
+
defaultAffectedBase,
|
|
7
|
+
type FingerprintClaims,
|
|
8
|
+
workspaceGlobsMatch,
|
|
9
|
+
} from './affected.js'
|
|
5
10
|
export { applyFilters, parseFilter } from './filter.js'
|
|
6
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
computeWorkspaceFingerprint,
|
|
13
|
+
computeWorkspaceFingerprints,
|
|
14
|
+
WORKSPACE_FINGERPRINT_FILES,
|
|
15
|
+
type WorkspaceFingerprints,
|
|
16
|
+
} from './fingerprint.js'
|
|
7
17
|
export { computeNestedProjectDirs } from './nested-dirs.js'
|
|
8
18
|
export { buildPackageGraph, type PackageGraph } from './package-graph.js'
|
|
9
19
|
export {
|
|
@@ -11,6 +21,7 @@ export {
|
|
|
11
21
|
LOCKFILE_NAME,
|
|
12
22
|
LOCKFILE_VERSION,
|
|
13
23
|
lockfilePath,
|
|
24
|
+
FROZEN_WITHOUT_LOCK,
|
|
14
25
|
readLockfile,
|
|
15
26
|
writeLockfile,
|
|
16
27
|
type Lockfile,
|
|
@@ -20,8 +31,9 @@ export {
|
|
|
20
31
|
loadProjectConfig,
|
|
21
32
|
loadProjectConfigs,
|
|
22
33
|
loadWorkspaceConfig,
|
|
23
|
-
|
|
34
|
+
WORKSPACE_CONFIG_FILENAMES,
|
|
24
35
|
} from './project-loader.js'
|
|
36
|
+
export { validateProjectConfig } from './config-schema.js'
|
|
25
37
|
export type { LoadProjectConfigOptions } from './project-loader.js'
|
|
26
38
|
export {
|
|
27
39
|
blobOidOf,
|
|
@@ -33,7 +45,22 @@ export {
|
|
|
33
45
|
findWorkspaceRoot,
|
|
34
46
|
listProjects,
|
|
35
47
|
loadWorkspace,
|
|
48
|
+
memberBaseDirs,
|
|
36
49
|
resolveCacheDir,
|
|
37
50
|
type ProjectEntry,
|
|
38
51
|
type ProjectMeta,
|
|
39
52
|
} from './workspace.js'
|
|
53
|
+
// The migration seam (see migration.ts) and core's own mapper, the scripts one.
|
|
54
|
+
export {
|
|
55
|
+
applyMigration,
|
|
56
|
+
PERSISTENT_TASK_NAMES,
|
|
57
|
+
PERSISTENT_TODO,
|
|
58
|
+
quoteTsLiteral,
|
|
59
|
+
type ApplyMigrationArgs,
|
|
60
|
+
type MigrationFormat,
|
|
61
|
+
type GeneratedProject,
|
|
62
|
+
type GeneratedTask,
|
|
63
|
+
type MigrationPlan,
|
|
64
|
+
type RawExpr,
|
|
65
|
+
} from './migration.js'
|
|
66
|
+
export { delegatedScript, migrateScripts } from './migrate-scripts.js'
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import path from 'node:path'
|
|
15
15
|
import type { ProjectConfig } from '../config.js'
|
|
16
16
|
import { relPosix, UserError } from '../util/index.js'
|
|
17
|
-
import { validateProjectConfig } from './
|
|
17
|
+
import { validateProjectConfig } from './config-schema.js'
|
|
18
18
|
|
|
19
19
|
export const LOCKFILE_NAME = 'vx-lock.json'
|
|
20
20
|
export const LOCKFILE_VERSION = 1
|
|
@@ -43,6 +43,10 @@ export function lockfilePath(root: string): string {
|
|
|
43
43
|
* user-editable file, so this is a system boundary: malformed content
|
|
44
44
|
* throws a `UserError` instead of crashing deeper in the run.
|
|
45
45
|
*/
|
|
46
|
+
/** What a `--frozen` verb says when there is no lock to read. */
|
|
47
|
+
export const FROZEN_WITHOUT_LOCK =
|
|
48
|
+
"--frozen requires vx-lock.json at the workspace root — run 'vx lock' and commit it"
|
|
49
|
+
|
|
46
50
|
export async function readLockfile(root: string): Promise<Lockfile | null> {
|
|
47
51
|
const file = Bun.file(lockfilePath(root))
|
|
48
52
|
if (!(await file.exists())) return null
|
|
@@ -96,12 +100,13 @@ export async function writeLockfile(root: string, lock: Lockfile): Promise<void>
|
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
/**
|
|
99
|
-
* Run-time config load from the lock:
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
103
|
+
* Run-time config load from the lock: return the FROZEN resolved config
|
|
104
|
+
* for a project the lock has an entry for. No evaluation happens — this
|
|
105
|
+
* is the fast, eval-free trust path — and no staleness check either (see
|
|
106
|
+
* below): the lock's contract is "what runs is what was locked", so a
|
|
107
|
+
* config file edited since `vx lock` still runs as locked until
|
|
108
|
+
* `vx lock --check` says otherwise. An unlocked project is a hard error;
|
|
109
|
+
* silently falling back to evaluation would break frozen-env semantics.
|
|
105
110
|
*/
|
|
106
111
|
export async function frozenProjectConfig(
|
|
107
112
|
lock: Lockfile,
|
|
@@ -3,15 +3,18 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Scripts carry a command and nothing else, so the mapping is honest about
|
|
5
5
|
// what it cannot know: every task gets `exec.command` verbatim; `build`
|
|
6
|
-
// gets the conventional `dependsOn: ['^build']` and `test` / `
|
|
7
|
-
//
|
|
6
|
+
// gets the conventional `dependsOn: ['^build']` and `test` / `typecheck`
|
|
7
|
+
// wait for `build` when the package has one (`lint` does not); a dev-server
|
|
8
8
|
// shaped script becomes persistent. Caching is opt-in and needs declared
|
|
9
|
-
// inputs AND outputs, which a script cannot tell us — so
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// that
|
|
13
|
-
//
|
|
14
|
-
//
|
|
9
|
+
// inputs AND outputs, which a script cannot tell us — so NO task gets a
|
|
10
|
+
// cache block; `build` carries a TODO showing the block to add. Until
|
|
11
|
+
// 2026-09-04 `build` was emitted with whole-project inputs and EMPTY
|
|
12
|
+
// outputs "to fill in": that is not an uncached task but a no-output one —
|
|
13
|
+
// it hits on unchanged inputs and skips the build with nothing to restore,
|
|
14
|
+
// so a deleted `dist` stayed deleted under a green `up-to-date` run
|
|
15
|
+
// (reproduced on the init walkthrough). A block that guessed `dist/**`
|
|
16
|
+
// would restore the wrong tree for every package that writes somewhere
|
|
17
|
+
// else, and a wrong restore is the worst failure vx has.
|
|
15
18
|
//
|
|
16
19
|
// Two npm conventions are mapped rather than copied, because copying them
|
|
17
20
|
// loses behaviour: `pre<x>` / `post<x>` hooks, which npm runs around `x`
|
|
@@ -21,11 +24,18 @@
|
|
|
21
24
|
// `<pm> run <other>` becomes a GROUP over `<other>`, so the graph sees the
|
|
22
25
|
// dependency instead of a package-manager subprocess it cannot cache.
|
|
23
26
|
|
|
24
|
-
import type { ProjectMeta } from '
|
|
25
|
-
import
|
|
27
|
+
import type { ProjectMeta } from './workspace.js'
|
|
28
|
+
import {
|
|
29
|
+
type GeneratedProject,
|
|
30
|
+
type GeneratedTask,
|
|
31
|
+
type MigrationPlan,
|
|
32
|
+
PERSISTENT_TASK_NAMES,
|
|
33
|
+
PERSISTENT_TODO,
|
|
34
|
+
} from './migration.js'
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
// `lint` is not here: a linter reads sources, and an edge to `build`
|
|
37
|
+
// serialises the two for nothing (the init walkthrough, 2026-09-04).
|
|
38
|
+
const AFTER_BUILD = new Set(['test', 'typecheck', 'check', 'e2e'])
|
|
29
39
|
const LIFECYCLE = /^(pre|post)(install|publish|pack|version)$|^(prepare|prepublishOnly|install)$/
|
|
30
40
|
|
|
31
41
|
/**
|
|
@@ -97,17 +107,14 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
|
|
|
97
107
|
)
|
|
98
108
|
}
|
|
99
109
|
const task: Record<string, unknown> = { exec: { command } }
|
|
100
|
-
if (
|
|
110
|
+
if (PERSISTENT_TASK_NAMES.has(name)) {
|
|
101
111
|
task['exec'] = { command, persistent: {} }
|
|
102
|
-
todos.push(
|
|
103
|
-
'persistent: add `readyWhen: "<line the server prints when ready>"` so dependents wait for it',
|
|
104
|
-
)
|
|
112
|
+
todos.push(PERSISTENT_TODO)
|
|
105
113
|
}
|
|
106
114
|
if (name === 'build') {
|
|
107
115
|
task['dependsOn'] = ['^build']
|
|
108
|
-
task['cache'] = { inputs: { files: ['**/*'] }, outputs: { files: [] } }
|
|
109
116
|
todos.push(
|
|
110
|
-
"cache:
|
|
117
|
+
"cache: add `cache: { inputs: { files: ['src/**'] }, outputs: { files: ['dist/**'] } }` with this package's real inputs and outputs — without it the task always runs; a block with EMPTY outputs would be a cached no-op, not an uncached task",
|
|
111
118
|
)
|
|
112
119
|
} else if (AFTER_BUILD.has(name) && hasBuild) {
|
|
113
120
|
task['dependsOn'] = ['build']
|
|
@@ -118,7 +125,7 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
|
|
|
118
125
|
}
|
|
119
126
|
return {
|
|
120
127
|
headerNotes: [
|
|
121
|
-
'each script became a task with its command verbatim; caching needs declared inputs and outputs, so
|
|
128
|
+
'each script became a task with its command verbatim; caching needs declared inputs and outputs, so no task got a cache block — `build` carries a TODO showing the one to add',
|
|
122
129
|
],
|
|
123
130
|
projects,
|
|
124
131
|
extraFiles: [],
|