@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/cli/cache.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { Cache } from '../cache/index.js'
|
|
1
|
+
import { Cache, noteSchemaReset } from '../cache/index.js'
|
|
2
|
+
import { seeHelp } from './help.js'
|
|
2
3
|
import { parseDecimalInt, parseSize } from '../util/index.js'
|
|
3
|
-
import { findWorkspaceRoot
|
|
4
|
+
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
5
|
+
import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
|
|
4
6
|
import { formatBytes } from './format.js'
|
|
5
7
|
|
|
6
8
|
// parseSize moved to `util` (the orchestrator's resource resolver needs it
|
|
@@ -16,7 +18,7 @@ export async function cacheCmd(args: readonly string[]): Promise<number> {
|
|
|
16
18
|
process.stderr.write('vx cache: missing subcommand. Try `vx cache prune`.\n')
|
|
17
19
|
return 1
|
|
18
20
|
default:
|
|
19
|
-
process.stderr.write(`vx cache: unknown subcommand: ${sub}\n`)
|
|
21
|
+
process.stderr.write(`vx cache: unknown subcommand: ${sub}${seeHelp('cache')}\n`)
|
|
20
22
|
return 1
|
|
21
23
|
}
|
|
22
24
|
}
|
|
@@ -24,6 +26,9 @@ export async function cacheCmd(args: readonly string[]): Promise<number> {
|
|
|
24
26
|
interface PruneArgs {
|
|
25
27
|
olderThanMs?: number
|
|
26
28
|
maxBytes?: number
|
|
29
|
+
dryRun?: boolean
|
|
30
|
+
/** `--cache-dir`: prune the cache a run with the same flag uses. */
|
|
31
|
+
cacheDir?: string
|
|
27
32
|
error?: string
|
|
28
33
|
}
|
|
29
34
|
|
|
@@ -60,9 +65,25 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
|
|
|
60
65
|
error: `--max-size 0 would evict every entry — delete the cache directory instead`,
|
|
61
66
|
}
|
|
62
67
|
}
|
|
68
|
+
// A bare number is bytes to `parseSize` (a computed size needs that),
|
|
69
|
+
// but nobody caps a cache at 10 bytes: `--max-size 10`
|
|
70
|
+
// meant 10G or 10M and would evict everything, the same class of
|
|
71
|
+
// destructive typo the zero bound above refuses. An explicit `10B`
|
|
72
|
+
// still passes.
|
|
73
|
+
if (/^\d+$/.test(v)) {
|
|
74
|
+
return {
|
|
75
|
+
error: `--max-size ${v} would read as ${v} bytes and evict nearly everything — give a unit (e.g. ${v}M, ${v}G)`,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
63
78
|
out.maxBytes = bytes
|
|
79
|
+
} else if (a === '--dry-run') {
|
|
80
|
+
out.dryRun = true
|
|
64
81
|
} else {
|
|
65
|
-
|
|
82
|
+
const cd = parseCacheDirFlag(args, i)
|
|
83
|
+
if (cd === null) return { error: `unknown argument: ${a}${seeHelp('cache')}` }
|
|
84
|
+
if ('error' in cd) return { error: cd.error }
|
|
85
|
+
out.cacheDir = cd.cacheDir
|
|
86
|
+
i = cd.next
|
|
66
87
|
}
|
|
67
88
|
}
|
|
68
89
|
if (out.olderThanMs === undefined && out.maxBytes === undefined) {
|
|
@@ -85,18 +106,25 @@ async function pruneCmd(args: readonly string[]): Promise<number> {
|
|
|
85
106
|
process.stderr.write(`vx cache prune: ${(err as Error).message}\n`)
|
|
86
107
|
return 1
|
|
87
108
|
}
|
|
88
|
-
// Honor `defineWorkspace({ cacheDir: '...' })`
|
|
89
|
-
// `
|
|
90
|
-
// silently no-ops against the
|
|
91
|
-
|
|
92
|
-
const cache = new Cache(
|
|
109
|
+
// Honor `--cache-dir`, `defineWorkspace({ cacheDir: '...' })` and a
|
|
110
|
+
// `config` plugin's edit of it — `vx run` and `vx cache prune` must
|
|
111
|
+
// operate on the same directory or prune silently no-ops against the
|
|
112
|
+
// wrong path.
|
|
113
|
+
const cache = new Cache(await cliCacheDir(root, parsed.cacheDir))
|
|
114
|
+
noteSchemaReset(cache, warnToStderr)
|
|
93
115
|
try {
|
|
94
|
-
const opts: { olderThanMs?: number; maxBytes?: number } = {}
|
|
116
|
+
const opts: { olderThanMs?: number; maxBytes?: number; dryRun?: boolean } = {}
|
|
95
117
|
if (parsed.olderThanMs !== undefined) opts.olderThanMs = parsed.olderThanMs
|
|
96
118
|
if (parsed.maxBytes !== undefined) opts.maxBytes = parsed.maxBytes
|
|
119
|
+
if (parsed.dryRun) opts.dryRun = true
|
|
97
120
|
const result = await cache.prune(opts)
|
|
121
|
+
const dry = parsed.dryRun === true
|
|
122
|
+
const orphans =
|
|
123
|
+
result.orphans > 0
|
|
124
|
+
? `, ${dry ? 'would reap' : 'reaped'} ${result.orphans} orphaned artifact${result.orphans === 1 ? '' : 's'} (${formatBytes(result.orphanBytes)})`
|
|
125
|
+
: ''
|
|
98
126
|
process.stdout.write(
|
|
99
|
-
|
|
127
|
+
`${dry ? 'Would prune' : 'Pruned'} ${result.evicted} entr${result.evicted === 1 ? 'y' : 'ies'} (${formatBytes(result.bytesFreed)}${dry ? '' : ' freed'})${orphans}\n`,
|
|
100
128
|
)
|
|
101
129
|
} finally {
|
|
102
130
|
cache.close()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// `vx completions <shell>` — a bash / zsh / fish script over the verb table
|
|
2
|
+
// and each verb's help cut, so the verbs and every flag of each complete
|
|
3
|
+
// from the one text `vx <verb> --help` prints. Plugin verbs of the
|
|
4
|
+
// workspace around the cwd are included at generation time; task and
|
|
5
|
+
// project names are not (a completion that evaluates configs on every
|
|
6
|
+
// Tab is the wrong price).
|
|
7
|
+
|
|
8
|
+
import { CORE_VERBS, seeHelp, verbHelpText } from './help.js'
|
|
9
|
+
|
|
10
|
+
export type CompletionShell = 'bash' | 'zsh' | 'fish'
|
|
11
|
+
const SHELLS: readonly CompletionShell[] = ['bash', 'zsh', 'fish']
|
|
12
|
+
|
|
13
|
+
/** Every `--flag` the verb's help cut names, plus `--help`; deduped, in order of appearance. */
|
|
14
|
+
export function verbFlags(verb: string): string[] {
|
|
15
|
+
const flags = new Set<string>(['--help'])
|
|
16
|
+
for (const f of verbHelpText(verb).match(/--[a-zA-Z][a-zA-Z-]*/g) ?? []) flags.add(f)
|
|
17
|
+
return [...flags]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The words that follow `vx <verb>` besides flags: subcommands and fixed choices. */
|
|
21
|
+
const VERB_WORDS: Readonly<Record<string, readonly string[]>> = {
|
|
22
|
+
cache: ['prune'],
|
|
23
|
+
completions: [...SHELLS],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function completionScript(shell: CompletionShell, verbs: readonly string[]): string {
|
|
27
|
+
const table = verbs.map((v) => ({
|
|
28
|
+
verb: v,
|
|
29
|
+
// A plugin verb owns its own arguments; only `--help` is known here.
|
|
30
|
+
words: [
|
|
31
|
+
...(VERB_WORDS[v] ?? []),
|
|
32
|
+
...((CORE_VERBS as readonly string[]).includes(v) ? verbFlags(v) : ['--help']),
|
|
33
|
+
],
|
|
34
|
+
}))
|
|
35
|
+
switch (shell) {
|
|
36
|
+
case 'bash':
|
|
37
|
+
return [
|
|
38
|
+
'# vx completions — generated by `vx completions bash`',
|
|
39
|
+
'_vx() {',
|
|
40
|
+
' local cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
41
|
+
' local verb="${COMP_WORDS[1]}"',
|
|
42
|
+
' if [ "$COMP_CWORD" -eq 1 ]; then',
|
|
43
|
+
` COMPREPLY=($(compgen -W "${verbs.join(' ')}" -- "$cur"))`,
|
|
44
|
+
' return',
|
|
45
|
+
' fi',
|
|
46
|
+
' case "$verb" in',
|
|
47
|
+
...table.map(
|
|
48
|
+
(t) => ` ${t.verb}) COMPREPLY=($(compgen -W "${t.words.join(' ')}" -- "$cur")) ;;`,
|
|
49
|
+
),
|
|
50
|
+
' *) COMPREPLY=() ;;',
|
|
51
|
+
' esac',
|
|
52
|
+
'}',
|
|
53
|
+
'complete -F _vx vx',
|
|
54
|
+
'',
|
|
55
|
+
].join('\n')
|
|
56
|
+
case 'zsh':
|
|
57
|
+
return [
|
|
58
|
+
'#compdef vx',
|
|
59
|
+
'# vx completions — generated by `vx completions zsh`',
|
|
60
|
+
'_vx() {',
|
|
61
|
+
' if (( CURRENT == 2 )); then',
|
|
62
|
+
` compadd -- ${verbs.join(' ')}`,
|
|
63
|
+
' return',
|
|
64
|
+
' fi',
|
|
65
|
+
' case "${words[2]}" in',
|
|
66
|
+
...table.map((t) => ` ${t.verb}) compadd -- ${t.words.join(' ')} ;;`),
|
|
67
|
+
' esac',
|
|
68
|
+
'}',
|
|
69
|
+
'compdef _vx vx',
|
|
70
|
+
'',
|
|
71
|
+
].join('\n')
|
|
72
|
+
case 'fish':
|
|
73
|
+
return [
|
|
74
|
+
'# vx completions — generated by `vx completions fish`',
|
|
75
|
+
'complete -c vx -f',
|
|
76
|
+
...verbs.map((v) => `complete -c vx -n __fish_use_subcommand -a ${v}`),
|
|
77
|
+
...table.flatMap((t) =>
|
|
78
|
+
t.words.map((w) =>
|
|
79
|
+
w.startsWith('--')
|
|
80
|
+
? `complete -c vx -n "__fish_seen_subcommand_from ${t.verb}" -l ${w.slice(2)}`
|
|
81
|
+
: `complete -c vx -n "__fish_seen_subcommand_from ${t.verb}" -a ${w}`,
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
'',
|
|
85
|
+
].join('\n')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function completionsCmd(
|
|
90
|
+
args: readonly string[],
|
|
91
|
+
pluginVerbs: readonly string[],
|
|
92
|
+
): Promise<number> {
|
|
93
|
+
const shell = args[0]
|
|
94
|
+
if (args.length !== 1 || !(SHELLS as readonly string[]).includes(shell ?? '')) {
|
|
95
|
+
process.stderr.write(
|
|
96
|
+
`vx completions: expected one shell — bash, zsh or fish${shell !== undefined && args.length === 1 ? ` (got ${shell})` : ''}${seeHelp('completions')}\n`,
|
|
97
|
+
)
|
|
98
|
+
return 1
|
|
99
|
+
}
|
|
100
|
+
const verbs = [...CORE_VERBS.filter((v) => v !== 'help' && v !== 'version'), 'help', 'version']
|
|
101
|
+
for (const v of pluginVerbs) if (!verbs.includes(v)) verbs.push(v)
|
|
102
|
+
process.stdout.write(completionScript(shell as CompletionShell, verbs))
|
|
103
|
+
return 0
|
|
104
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One core per process. A plugin package, a `vx.workspace.ts`, a
|
|
3
|
+
* `vx.config.ts` all `import … from '@vzn/vx'`, and that specifier
|
|
4
|
+
* resolves through node_modules — to a SECOND copy of core when the host
|
|
5
|
+
* is the compiled binary (whose own copy lives under the bunfs root) or
|
|
6
|
+
* an install with its own nested `@vzn/vx`. The second copy is core's
|
|
7
|
+
* whole source transpiled again, per process: ~20 ms per plugin package
|
|
8
|
+
* in the binary (measured 2026-09-10, `@vzn/vx-otel`: 20–25 ms → 2–3 ms),
|
|
9
|
+
* and two module states — two `definePlugin` memos, two of everything
|
|
10
|
+
* keyed by identity rather than by registry symbol.
|
|
11
|
+
*
|
|
12
|
+
* The alias is a Bun virtual module for the exact specifier `@vzn/vx`,
|
|
13
|
+
* served from the host's own façade, loaded lazily on the first import
|
|
14
|
+
* so a verb that never loads a plugin never loads the façade. The
|
|
15
|
+
* source form (`bun src/bin.ts`) resolves the specifier to the same file
|
|
16
|
+
* anyway; the alias makes that a guarantee rather than a layout fact.
|
|
17
|
+
*/
|
|
18
|
+
export function registerCoreAlias(load: () => Promise<Record<string, unknown>>): void {
|
|
19
|
+
Bun.plugin({
|
|
20
|
+
name: 'vx-core-alias',
|
|
21
|
+
setup(build) {
|
|
22
|
+
build.module('@vzn/vx', async () => ({ exports: await load(), loader: 'object' }))
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
}
|
package/src/cli/help.ts
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
|
-
export function printHelp(pluginCommands: readonly string[] = []): void {
|
|
2
|
-
process.stdout.write(helpText(pluginCommands))
|
|
1
|
+
export function printHelp(pluginCommands: readonly string[] = [], verb?: string): void {
|
|
2
|
+
process.stdout.write(verb === undefined ? helpText(pluginCommands) : verbHelpText(verb))
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `vx <verb> --help`: the reference cut to one verb — the title, the Usage
|
|
7
|
+
* lines that name it, and every section that is `(for <verb>)` or lists a
|
|
8
|
+
* `vx <verb>` form — read from the same text, so nothing can drift. A verb
|
|
9
|
+
* the reference does not know gets the whole thing.
|
|
10
|
+
*/
|
|
11
|
+
export function verbHelpText(verb: string): string {
|
|
12
|
+
const blocks = helpText().split('\n\n')
|
|
13
|
+
const title = blocks[0]!
|
|
14
|
+
const names = (line: string): boolean =>
|
|
15
|
+
new RegExp(`^\\s*vx ${verb}( |$)`).test(line) ||
|
|
16
|
+
line.includes(`(for ${verb})`) ||
|
|
17
|
+
line.includes(`(for ${verb} `)
|
|
18
|
+
const kept: string[] = []
|
|
19
|
+
for (const block of blocks.slice(1)) {
|
|
20
|
+
const lines = block.split('\n')
|
|
21
|
+
if (lines[0] === 'Usage:') {
|
|
22
|
+
const own = lines.slice(1).filter(names)
|
|
23
|
+
if (own.length > 0) kept.push(['Usage:', ...own].join('\n'))
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
if (lines.some(names)) kept.push(block)
|
|
27
|
+
}
|
|
28
|
+
if (kept.length === 0) return helpText()
|
|
29
|
+
return [title, ...kept, 'Full reference: vx help', ''].join('\n\n')
|
|
3
30
|
}
|
|
4
31
|
|
|
5
32
|
export function helpText(pluginCommands: readonly string[] = []): string {
|
|
@@ -9,16 +36,15 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
9
36
|
'Usage:',
|
|
10
37
|
' vx run [OPTIONS] [TASK | PKG#TASK] [-- forwarded-args...]',
|
|
11
38
|
' vx watch [OPTIONS] TASK [-- forwarded-args...]',
|
|
12
|
-
' vx cache prune [--older-than <duration>] [--max-size <
|
|
39
|
+
' vx cache prune [--older-than <duration>] [--max-size <size>] [--dry-run] [--cache-dir <path>]',
|
|
13
40
|
' vx lock [--check]',
|
|
14
|
-
' vx init [--dry] [--force]',
|
|
15
|
-
' vx migrate [--from turbo|nx|scripts] [--dry] [--force]',
|
|
41
|
+
' vx init [--dry] [--force] [--mjs]',
|
|
16
42
|
' vx upgrade [tag]',
|
|
17
|
-
' vx show [PROJECT[#TASK]] [--format pretty|json]',
|
|
18
|
-
' vx info',
|
|
19
|
-
' vx why [TASK | PKG#TASK] [--run <runId>] [--format pretty|json]',
|
|
20
|
-
' vx last [RUNID] [--list[=N]] [--format pretty|json]',
|
|
21
|
-
' vx
|
|
43
|
+
' vx show [PROJECT[#TASK] | TASK] [--format pretty|json]',
|
|
44
|
+
' vx info [--format pretty|json] [--cache-dir <path>]',
|
|
45
|
+
' vx why [TASK | PKG#TASK] [--run <runId>] [--format pretty|json] [--cache-dir <path>]',
|
|
46
|
+
' vx last [RUNID] [--list[=N]] [--format pretty|json] [--cache-dir <path>]',
|
|
47
|
+
' vx completions bash|zsh|fish',
|
|
22
48
|
' vx help',
|
|
23
49
|
' vx version',
|
|
24
50
|
'',
|
|
@@ -33,8 +59,8 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
33
59
|
" pkg#task Run a specific project's task directly.",
|
|
34
60
|
'',
|
|
35
61
|
'Execution (for run):',
|
|
36
|
-
' --concurrency <n> Max parallel tasks (default:
|
|
37
|
-
' --
|
|
62
|
+
' --concurrency <n> Max parallel tasks (default: the cores this process may use); `50%` = half of them.',
|
|
63
|
+
' --exclude-dependencies[=names] Skip dependsOn edges. No value = all; comma list = specific names.',
|
|
38
64
|
' --no-cache Disable caching entirely (no reads, no writes, outputs left alone).',
|
|
39
65
|
' --force Re-execute everything (skip reads) but still refresh the cache (writes on).',
|
|
40
66
|
' --cache <spec> Per-layer read/write control. Comma list of <layer>:<flags>',
|
|
@@ -50,23 +76,11 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
50
76
|
' --timeout <ms> Default per-task timeout for tasks without their own exec.timeout',
|
|
51
77
|
' (above VX_TASK_TIMEOUT env + workspace timeout). Kills + fails',
|
|
52
78
|
' a runaway task. Per-task exec.timeout always wins.',
|
|
53
|
-
' --memory <size> Memory budget (e.g. 8GB) that exec.resources.memory reservations',
|
|
54
|
-
' pack against. Default: total system RAM — pass this in',
|
|
55
|
-
' cgroup-limited containers (the default reads the HOST RAM).',
|
|
56
|
-
' Reservations are per-task config (exec.resources), not flags.',
|
|
57
|
-
' --verify[=<what>] Prove cache correctness. determinism (default): re-run each',
|
|
58
|
-
' executed cacheable task + content-compare outputs. inputs: sandbox',
|
|
59
|
-
' with the declared-input baseline + flag reads of undeclared inputs.',
|
|
60
|
-
' fingerprint: ship output-tree fingerprints for the cross-machine',
|
|
61
|
-
' diff on a connected serve (~1× exec — no re-run). all: everything.',
|
|
62
|
-
' An unsafe task fails the run with the exact paths.',
|
|
63
|
-
' ~2× exec (determinism); CI-shaped. inputs/all need the OS sandbox.',
|
|
64
|
-
' --verify-allow <pkg#task,…> Exempt known-nondeterministic tasks from failing --verify.',
|
|
65
79
|
' --frozen Load configs from vx-lock.json (CI; pair with vx lock --check).',
|
|
66
80
|
' --output-logs <mode> full | errors-only | hash-only | none — overrides the flow default',
|
|
67
81
|
' --download <mode> all (default) | toplevel | none — where remote outputs land',
|
|
68
82
|
' (focused without --all/--filter/--affected, broad with, full in CI).',
|
|
69
|
-
' --verbosity <n> 0
|
|
83
|
+
' --verbosity <n> 0 (default) or 1+: print a per-task summary table after the run.',
|
|
70
84
|
'',
|
|
71
85
|
'Planning (for run — skips execution):',
|
|
72
86
|
' --dry[=text|json] Print the task graph + cache hit/miss prediction.',
|
|
@@ -97,6 +111,8 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
97
111
|
'Cache management:',
|
|
98
112
|
' vx cache prune --older-than 30d Evict entries last accessed > 30 days ago.',
|
|
99
113
|
' vx cache prune --max-size 1G Keep total cache under 1 GB (LRU eviction).',
|
|
114
|
+
' --dry-run Say what the policy would reap; delete nothing.',
|
|
115
|
+
' --cache-dir <path> The cache a run with the same flag uses (why, last, info too).',
|
|
100
116
|
'',
|
|
101
117
|
' Duration units: s, m, h, d. Size units: K, M, G, T (powers of 1024).',
|
|
102
118
|
'',
|
|
@@ -105,20 +121,18 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
105
121
|
" vx show <project> Print the project's LIVE resolved config (fresh",
|
|
106
122
|
' evaluation, not the lock), one block per task.',
|
|
107
123
|
" vx show <pkg>#<task> Print a single task's resolved config.",
|
|
124
|
+
' vx show <task> That task in every project declaring it.',
|
|
108
125
|
' --format <fmt> pretty (default) | json.',
|
|
109
126
|
' vx info Workspace doctor: vx/bun/git versions, project +',
|
|
110
127
|
' task counts, cache dir/entries/size, recent runs,',
|
|
111
128
|
' lock status.',
|
|
129
|
+
' --format <fmt> pretty (default) | json.',
|
|
112
130
|
' vx stats Deprecated: alias of vx info.',
|
|
113
131
|
" vx why <task> Why did this task re-run? Compares the task's latest",
|
|
114
132
|
' run (or --run <id>) against its previous run: names the',
|
|
115
133
|
' exact changed cache-key components (files / env / runtime',
|
|
116
134
|
' / upstream) from the persisted input fingerprints.',
|
|
117
135
|
' --format <fmt> pretty (default) | json.',
|
|
118
|
-
' vx prune <project> Emit a self-contained workspace SUBSET — the project +',
|
|
119
|
-
' its transitive workspace deps, root manifests, and the',
|
|
120
|
-
' (unpruned) lockfile — for Docker builds. --docker splits',
|
|
121
|
-
' json/ (manifests, the cacheable install layer) from full/.',
|
|
122
136
|
" vx last [runId] Replay a recorded run's summary from the local history —",
|
|
123
137
|
' header (command, when, branch, counts) plus the per-task',
|
|
124
138
|
' table, failures first. No re-execution, no cache probe.',
|
|
@@ -127,14 +141,17 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
127
141
|
'',
|
|
128
142
|
'Migration:',
|
|
129
143
|
' vx init Generate vx.workspace.ts + one vx.config.ts per package',
|
|
130
|
-
' from package.json scripts
|
|
131
|
-
'
|
|
132
|
-
' Nx project graph (.nx/workspace-data/project-graph.json);',
|
|
133
|
-
' with neither present, from package.json scripts.',
|
|
134
|
-
' Unmappable settings become TODO(vx-migrate) comments;',
|
|
135
|
-
' turbo global fields become a shared vx-preset.ts.',
|
|
144
|
+
' from package.json scripts. Unmappable settings become',
|
|
145
|
+
' TODO(vx-migrate) comments.',
|
|
136
146
|
' --dry Print the generated files instead of writing them.',
|
|
137
|
-
' --force Overwrite existing vx.config.*
|
|
147
|
+
' --force Overwrite existing vx.config.* files.',
|
|
148
|
+
' From turbo.json or an Nx project graph: `bunx @vzn/vx-migrate` (its own package).',
|
|
149
|
+
'',
|
|
150
|
+
'Shell completions:',
|
|
151
|
+
' vx completions <shell> Print a completion script for bash, zsh or fish: the',
|
|
152
|
+
' verbs (plugin verbs of this workspace included) and',
|
|
153
|
+
' every flag of each. `eval "$(vx completions bash)"`,',
|
|
154
|
+
' or write it where the shell loads completions from.',
|
|
138
155
|
'',
|
|
139
156
|
'Config lock:',
|
|
140
157
|
' vx lock Evaluate every vx.config in the current environment and',
|
|
@@ -150,6 +167,15 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
150
167
|
].join('\n')
|
|
151
168
|
}
|
|
152
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Every argument error ends with this. `vx <verb> --help` prints the
|
|
172
|
+
* reference (2026-09-04); before that there was nowhere to send a user who
|
|
173
|
+
* got a flag wrong, so the errors just said no.
|
|
174
|
+
*/
|
|
175
|
+
export function seeHelp(verb: string): string {
|
|
176
|
+
return ` (see \`vx ${verb} --help\`)`
|
|
177
|
+
}
|
|
178
|
+
|
|
153
179
|
/**
|
|
154
180
|
* The documented `--flags` of one verb, read from the help text's
|
|
155
181
|
* sections headed `… (for <verb>):` so there is no second list to drift —
|
|
@@ -166,3 +192,5 @@ export function documentedFlags(verb: string): string[] {
|
|
|
166
192
|
}
|
|
167
193
|
return [...flags]
|
|
168
194
|
}
|
|
195
|
+
|
|
196
|
+
export { CORE_VERBS } from '../util/index.js'
|
package/src/cli/index.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { VERSION } from '../version.js'
|
|
6
6
|
import { runCmd } from './run.js'
|
|
7
|
-
import { printHelp } from './help.js'
|
|
8
|
-
import { pluginCommandHelp, resolvePluginCommand } from './plugin-commands.js'
|
|
9
|
-
import { UserError } from '../util/index.js'
|
|
7
|
+
import { CORE_VERBS, printHelp } from './help.js'
|
|
8
|
+
import { pluginCommandHelp, resolvePluginCommand, pluginVerbs } from './plugin-commands.js'
|
|
9
|
+
import { MOVED_VERBS, nearest, UserError } from '../util/index.js'
|
|
10
10
|
|
|
11
11
|
// Every verb but `run` is imported when invoked. `vx run` is the hot path
|
|
12
12
|
// and nearly every invocation; the other verbs' modules are code that
|
|
@@ -18,6 +18,16 @@ import { UserError } from '../util/index.js'
|
|
|
18
18
|
export async function run(argv: readonly string[]): Promise<number> {
|
|
19
19
|
const [command, ...rest] = argv
|
|
20
20
|
|
|
21
|
+
// `vx <verb> --help` is the universal reflex, and every verb answered
|
|
22
|
+
// `unknown flag: --help` and exited 1 (walkthrough, 2026-09-04). Only args
|
|
23
|
+
// BEFORE a `--` count: `vx run build -- --help` forwards it to the task,
|
|
24
|
+
// which is the one place `--help` is not being asked of vx. Core verbs
|
|
25
|
+
// only — a plugin verb owns its own arguments, `--help` included.
|
|
26
|
+
if (command !== undefined && wantsHelp(command, rest)) {
|
|
27
|
+
printHelp(await pluginCommandHelp(), command)
|
|
28
|
+
return 0
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
switch (command) {
|
|
22
32
|
case undefined:
|
|
23
33
|
case '--help':
|
|
@@ -37,13 +47,8 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
37
47
|
return await (await import('./cache.js')).cacheCmd(rest)
|
|
38
48
|
case 'lock':
|
|
39
49
|
return await (await import('./lock.js')).lockCmd(rest)
|
|
40
|
-
case 'migrate':
|
|
41
|
-
return await (await import('./migrate.js')).migrateCmd(rest)
|
|
42
50
|
case 'init':
|
|
43
|
-
|
|
44
|
-
return await (
|
|
45
|
-
await import('./migrate.js')
|
|
46
|
-
).migrateCmd(['--from', 'scripts', ...rest], { init: true })
|
|
51
|
+
return await (await import('./init.js')).initCmd(rest)
|
|
47
52
|
case 'upgrade':
|
|
48
53
|
return await (await import('./upgrade.js')).upgradeCmd(rest)
|
|
49
54
|
case 'show':
|
|
@@ -55,8 +60,8 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
55
60
|
return await (await import('./why.js')).whyCmd(rest)
|
|
56
61
|
case 'last':
|
|
57
62
|
return await (await import('./last.js')).lastCmd(rest)
|
|
58
|
-
case '
|
|
59
|
-
return await (await import('./
|
|
63
|
+
case 'completions':
|
|
64
|
+
return await (await import('./completions.js')).completionsCmd(rest, await pluginVerbs())
|
|
60
65
|
default: {
|
|
61
66
|
// Not a core verb: a plugin declared in the workspace around the cwd
|
|
62
67
|
// may own it (`VxPlugin.commands`). Core verbs were matched above, so
|
|
@@ -82,6 +87,14 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
82
87
|
resolved !== null && 'loadError' in resolved
|
|
83
88
|
? `\n (plugin verbs could not be looked up: vx.workspace failed to load: ${resolved.loadError})`
|
|
84
89
|
: ''
|
|
90
|
+
// A verb core owned once (`migrate`, `prune`) and a package owns now:
|
|
91
|
+
// the pointer, but only after the plugins had their chance — a
|
|
92
|
+
// workspace that declares a plugin verb of that name keeps it.
|
|
93
|
+
const moved = MOVED_VERBS[command]
|
|
94
|
+
if (moved !== undefined) {
|
|
95
|
+
process.stderr.write(`${moved}${loadNote}\n`)
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
85
98
|
if (command === 'serve' || command === 'dev') {
|
|
86
99
|
// vx core is only a task runner — it has no service layer of its
|
|
87
100
|
// own. A dashboard, remote cache, distributed execution, etc. are
|
|
@@ -97,7 +110,7 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
97
110
|
)
|
|
98
111
|
return 1
|
|
99
112
|
}
|
|
100
|
-
process.stderr.write(`vx: unknown command: ${command}${loadNote}\n`)
|
|
113
|
+
process.stderr.write(`vx: unknown command: ${command}${didYouMeanVerb(command)}${loadNote}\n`)
|
|
101
114
|
printHelp()
|
|
102
115
|
return 1
|
|
103
116
|
}
|
|
@@ -105,12 +118,38 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
// Re-exports for tests + programmatic embedders.
|
|
108
|
-
export {
|
|
121
|
+
export {
|
|
122
|
+
detectFlow,
|
|
123
|
+
parseConcurrency,
|
|
124
|
+
parseRunArgs,
|
|
125
|
+
resolveRunOptions,
|
|
126
|
+
type RunArgs,
|
|
127
|
+
} from './run.js'
|
|
109
128
|
export { parsePruneArgs, parseDuration, parseSize } from './cache.js'
|
|
110
129
|
export { parseLockArgs, type LockArgs } from './lock.js'
|
|
111
|
-
export {
|
|
130
|
+
export { parseInitArgs, type InitArgs } from './init.js'
|
|
112
131
|
export { parseShowArgs, type ShowArgs } from './show.js'
|
|
113
132
|
export { parseWhyArgs } from './why.js'
|
|
114
133
|
export { parseLastArgs } from './last.js'
|
|
115
|
-
export { parsePruneWorkspaceArgs } from './prune.js'
|
|
116
134
|
export { formatBytes } from './format.js'
|
|
135
|
+
export { registerCoreAlias } from './core-alias.js'
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Is this invocation asking for help rather than work? See the call site for
|
|
139
|
+
* why the scan stops at `--`, and why plugin verbs are excluded.
|
|
140
|
+
*/
|
|
141
|
+
function wantsHelp(command: string, rest: readonly string[]): boolean {
|
|
142
|
+
if (!(CORE_VERBS as readonly string[]).includes(command)) return false
|
|
143
|
+
const sep = rest.indexOf('--')
|
|
144
|
+
const own = sep === -1 ? rest : rest.slice(0, sep)
|
|
145
|
+
return own.includes('--help') || own.includes('-h')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** ` Did you mean run?` for a verb within two edits of a core one — the same
|
|
149
|
+
* hint a task or flag typo gets. Plugin verbs are not listed: resolving
|
|
150
|
+
* them loads the workspace, and this path is reached only when that
|
|
151
|
+
* lookup found nothing. */
|
|
152
|
+
function didYouMeanVerb(verb: string): string {
|
|
153
|
+
const best = nearest(verb, CORE_VERBS)
|
|
154
|
+
return best === undefined ? '' : `. Did you mean ${best}?`
|
|
155
|
+
}
|