@vzn/vx 0.0.18 → 0.0.22
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 +107 -70
- package/package.json +10 -20
- package/src/bin.ts +64 -5
- package/src/cache/archive.ts +279 -28
- package/src/cache/cache.ts +952 -1584
- package/src/cache/chained-cache.ts +14 -10
- package/src/cache/config-evals.ts +112 -0
- package/src/cache/file-hashes.ts +290 -0
- package/src/cache/git-inputs.ts +966 -0
- package/src/cache/index.ts +23 -7
- package/src/cache/inputs.ts +553 -739
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +674 -0
- package/src/cache/layered-cache.ts +211 -49
- package/src/cache/output-index.ts +323 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +152 -0
- package/src/cache/zstd.ts +183 -0
- package/src/cli/cache.ts +88 -30
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +76 -38
- package/src/cli/index.ts +82 -17
- package/src/cli/info.ts +162 -91
- package/src/cli/init.ts +87 -0
- package/src/cli/last.ts +130 -23
- package/src/cli/lock.ts +36 -18
- package/src/cli/plan-format.ts +18 -7
- package/src/cli/plugin-commands.ts +34 -12
- package/src/cli/run.ts +138 -313
- package/src/cli/select.ts +342 -0
- package/src/cli/show.ts +151 -71
- package/src/cli/upgrade.ts +136 -15
- package/src/cli/watch.ts +1175 -116
- package/src/cli/why.ts +51 -29
- package/src/cli/workspace-config.ts +138 -0
- package/src/config.ts +183 -126
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +62 -31
- package/src/exec/index.ts +16 -2
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +361 -77
- package/src/exec/sandbox-binds.ts +211 -0
- package/src/exec/sandbox-paths.ts +70 -0
- package/src/exec/sandbox-runtime.ts +1152 -415
- package/src/exec/sandbox-violations.ts +293 -0
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +6 -4
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +332 -329
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +111 -102
- package/src/orchestrator/admission.ts +161 -0
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +25 -7
- package/src/orchestrator/doctor.ts +363 -0
- package/src/orchestrator/events.ts +111 -8
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +450 -565
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +121 -49
- package/src/orchestrator/history.ts +123 -104
- package/src/orchestrator/hit-restore.ts +285 -0
- package/src/orchestrator/index.ts +42 -5
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +401 -0
- package/src/orchestrator/logger.ts +128 -41
- package/src/orchestrator/metrics.ts +201 -72
- package/src/orchestrator/miss-save.ts +232 -0
- package/src/orchestrator/options.ts +63 -34
- package/src/orchestrator/persistent.ts +99 -0
- package/src/orchestrator/placement.ts +212 -0
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +200 -49
- package/src/orchestrator/plugin.ts +190 -14
- package/src/orchestrator/prepare.ts +168 -137
- package/src/orchestrator/projects.ts +270 -0
- package/src/orchestrator/remote-prefetch.ts +22 -2
- package/src/orchestrator/run-artifacts.ts +34 -3
- package/src/orchestrator/run-context.ts +20 -5
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +132 -0
- package/src/orchestrator/run-report.ts +6 -4
- package/src/orchestrator/run.ts +467 -611
- package/src/orchestrator/sandbox-request.ts +569 -0
- package/src/orchestrator/save-lane.ts +44 -0
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +167 -0
- package/src/orchestrator/stable-keys.ts +259 -10
- package/src/orchestrator/status-line.ts +35 -2
- package/src/orchestrator/summary.ts +115 -25
- package/src/orchestrator/task-hash.ts +103 -21
- package/src/orchestrator/task-log-buffer.ts +15 -18
- package/src/orchestrator/telemetry-host.ts +27 -8
- package/src/orchestrator/telemetry.ts +71 -44
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +42 -5
- package/src/util/paths.ts +168 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +23 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/verbs.ts +39 -0
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +368 -60
- package/src/workspace/config-cache.ts +271 -32
- package/src/workspace/config-eval.ts +60 -16
- package/src/workspace/config-imports.ts +131 -9
- package/src/workspace/config-schema.ts +1033 -0
- package/src/workspace/filter.ts +69 -17
- package/src/workspace/fingerprint.ts +59 -7
- package/src/workspace/index.ts +37 -3
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +41 -14
- package/src/workspace/migrate-scripts.ts +187 -0
- package/src/workspace/migration.ts +353 -0
- package/src/workspace/package-graph.ts +239 -31
- package/src/workspace/project-loader.ts +295 -815
- package/src/workspace/workspace.ts +266 -31
- 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-scripts.ts +0 -127
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/migrate.ts +0 -305
- 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/local-executor/index.ts +0 -45
- package/src/plugins/schedule-history/index.ts +0 -109
package/src/cli/help.ts
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
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
|
+
// The verb goes into a RegExp, so it is escaped: `vx 'r.n' --help` is an
|
|
15
|
+
// unknown verb, not a pattern that matches `vx run` and answers with a
|
|
16
|
+
// help cut for a verb that does not exist.
|
|
17
|
+
const quoted = verb.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
18
|
+
const names = (line: string): boolean =>
|
|
19
|
+
new RegExp(`^\\s*vx ${quoted}( |$)`).test(line) ||
|
|
20
|
+
line.includes(`(for ${verb})`) ||
|
|
21
|
+
line.includes(`(for ${verb} `)
|
|
22
|
+
const kept: string[] = []
|
|
23
|
+
for (const block of blocks.slice(1)) {
|
|
24
|
+
const lines = block.split('\n')
|
|
25
|
+
if (lines[0] === 'Usage:') {
|
|
26
|
+
const own = lines.slice(1).filter(names)
|
|
27
|
+
if (own.length > 0) kept.push(['Usage:', ...own].join('\n'))
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
if (lines.some(names)) kept.push(block)
|
|
31
|
+
}
|
|
32
|
+
if (kept.length === 0) return helpText()
|
|
33
|
+
return [title, ...kept, 'Full reference: vx help', ''].join('\n\n')
|
|
3
34
|
}
|
|
4
35
|
|
|
5
36
|
export function helpText(pluginCommands: readonly string[] = []): string {
|
|
@@ -9,16 +40,15 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
9
40
|
'Usage:',
|
|
10
41
|
' vx run [OPTIONS] [TASK | PKG#TASK] [-- forwarded-args...]',
|
|
11
42
|
' vx watch [OPTIONS] TASK [-- forwarded-args...]',
|
|
12
|
-
' vx cache prune [--older-than <duration>] [--max-size <
|
|
43
|
+
' vx cache prune [--older-than <duration>] [--max-size <size>] [--dry-run] [--cache-dir <path>]',
|
|
13
44
|
' vx lock [--check]',
|
|
14
|
-
' vx init [--dry] [--force]',
|
|
15
|
-
' vx migrate [--from turbo|nx|scripts] [--dry] [--force]',
|
|
45
|
+
' vx init [--dry] [--force] [--mjs]',
|
|
16
46
|
' 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
|
|
47
|
+
' vx show [PROJECT[#TASK] | TASK] [--format pretty|json]',
|
|
48
|
+
' vx info [--format pretty|json] [--cache-dir <path>]',
|
|
49
|
+
' vx why [TASK | PKG#TASK] [--run <runId>] [--format pretty|json] [--cache-dir <path>]',
|
|
50
|
+
' vx last [RUNID] [--list[=N]] [--format pretty|json] [--cache-dir <path>]',
|
|
51
|
+
' vx completions bash|zsh|fish',
|
|
22
52
|
' vx help',
|
|
23
53
|
' vx version',
|
|
24
54
|
'',
|
|
@@ -28,13 +58,13 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
28
58
|
' --filter <pat> pnpm-style filter (repeatable). Examples:',
|
|
29
59
|
' foo, @scope/*, ./packages/foo, foo..., ...foo,',
|
|
30
60
|
' foo^..., !foo, [<since>]',
|
|
31
|
-
' --affected[=<base>] Sugar for --filter "[<base>]". Default base =',
|
|
61
|
+
' --affected[=<base>] Sugar for --filter "...[<base>]". Default base =',
|
|
32
62
|
' origin/HEAD (falls back to HEAD~1).',
|
|
33
63
|
" pkg#task Run a specific project's task directly.",
|
|
34
64
|
'',
|
|
35
65
|
'Execution (for run):',
|
|
36
|
-
' --concurrency <n> Max parallel tasks (default:
|
|
37
|
-
' --
|
|
66
|
+
' --concurrency <n> Max parallel tasks (default: the cores this process may use); `50%` = half of them.',
|
|
67
|
+
' --exclude-dependencies[=names] Skip dependsOn edges. No value = all; comma list = specific names.',
|
|
38
68
|
' --no-cache Disable caching entirely (no reads, no writes, outputs left alone).',
|
|
39
69
|
' --force Re-execute everything (skip reads) but still refresh the cache (writes on).',
|
|
40
70
|
' --cache <spec> Per-layer read/write control. Comma list of <layer>:<flags>',
|
|
@@ -50,23 +80,13 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
50
80
|
' --timeout <ms> Default per-task timeout for tasks without their own exec.timeout',
|
|
51
81
|
' (above VX_TASK_TIMEOUT env + workspace timeout). Kills + fails',
|
|
52
82
|
' a runaway task. Per-task exec.timeout always wins.',
|
|
53
|
-
' --
|
|
54
|
-
'
|
|
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.',
|
|
83
|
+
' --continue[=<mode>] What a failed task takes down: never | deps-ok (default, skips',
|
|
84
|
+
' its dependents) | always (bare form; dependents run, never save).',
|
|
65
85
|
' --frozen Load configs from vx-lock.json (CI; pair with vx lock --check).',
|
|
66
86
|
' --output-logs <mode> full | errors-only | hash-only | none — overrides the flow default',
|
|
67
|
-
' --download <mode> all (default) | toplevel | none — where remote outputs land',
|
|
68
87
|
' (focused without --all/--filter/--affected, broad with, full in CI).',
|
|
69
|
-
' --
|
|
88
|
+
' --download <mode> all (default) | toplevel | none — where remote outputs land.',
|
|
89
|
+
' --verbosity <n> 0 (default) or 1+: print a per-task summary table after the run.',
|
|
70
90
|
'',
|
|
71
91
|
'Planning (for run — skips execution):',
|
|
72
92
|
' --dry[=text|json] Print the task graph + cache hit/miss prediction.',
|
|
@@ -75,6 +95,9 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
75
95
|
'Artifacts (for run):',
|
|
76
96
|
' --summarize[=<path>] Write per-run JSON to <cacheDir>/runs/<run_id>.json.',
|
|
77
97
|
' --profile[=<path>] Write Chrome-trace JSON (default profile.json).',
|
|
98
|
+
' --report[=markdown] Print a markdown run report to stdout after the run.',
|
|
99
|
+
' --report-file <path> Append that report to <path> (for $GITHUB_STEP_SUMMARY).',
|
|
100
|
+
' --tag <k=v> Label this invocation (repeatable); recorded on its history row.',
|
|
78
101
|
'',
|
|
79
102
|
'Extensions (plugins):',
|
|
80
103
|
' vx core runs tasks in-process and nothing else. A dashboard, remote',
|
|
@@ -97,28 +120,29 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
97
120
|
'Cache management:',
|
|
98
121
|
' vx cache prune --older-than 30d Evict entries last accessed > 30 days ago.',
|
|
99
122
|
' vx cache prune --max-size 1G Keep total cache under 1 GB (LRU eviction).',
|
|
123
|
+
' --dry-run Say what the policy would reap; delete nothing.',
|
|
124
|
+
' --cache-dir <path> The cache a run with the same flag uses (why, last, info too).',
|
|
100
125
|
'',
|
|
101
126
|
' Duration units: s, m, h, d. Size units: K, M, G, T (powers of 1024).',
|
|
127
|
+
' Cache statistics — directory, entry count, size — are part of `vx info`.',
|
|
102
128
|
'',
|
|
103
129
|
'Introspection:',
|
|
104
130
|
' vx show List every project: name, dir, declared task count.',
|
|
105
131
|
" vx show <project> Print the project's LIVE resolved config (fresh",
|
|
106
132
|
' evaluation, not the lock), one block per task.',
|
|
107
133
|
" vx show <pkg>#<task> Print a single task's resolved config.",
|
|
134
|
+
' vx show <task> That task in every project declaring it.',
|
|
108
135
|
' --format <fmt> pretty (default) | json.',
|
|
109
136
|
' vx info Workspace doctor: vx/bun/git versions, project +',
|
|
110
137
|
' task counts, cache dir/entries/size, recent runs,',
|
|
111
138
|
' lock status.',
|
|
139
|
+
' --format <fmt> pretty (default) | json.',
|
|
112
140
|
' vx stats Deprecated: alias of vx info.',
|
|
113
141
|
" vx why <task> Why did this task re-run? Compares the task's latest",
|
|
114
142
|
' run (or --run <id>) against its previous run: names the',
|
|
115
143
|
' exact changed cache-key components (files / env / runtime',
|
|
116
144
|
' / upstream) from the persisted input fingerprints.',
|
|
117
145
|
' --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
146
|
" vx last [runId] Replay a recorded run's summary from the local history —",
|
|
123
147
|
' header (command, when, branch, counts) plus the per-task',
|
|
124
148
|
' table, failures first. No re-execution, no cache probe.',
|
|
@@ -127,14 +151,17 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
127
151
|
'',
|
|
128
152
|
'Migration:',
|
|
129
153
|
' 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.',
|
|
154
|
+
' from package.json scripts. Unmappable settings become',
|
|
155
|
+
' TODO(vx-migrate) comments.',
|
|
136
156
|
' --dry Print the generated files instead of writing them.',
|
|
137
|
-
' --force Overwrite existing vx.config.*
|
|
157
|
+
' --force Overwrite existing vx.config.* files.',
|
|
158
|
+
' From turbo.json or an Nx project graph: `bunx @vzn/vx-migrate` (its own package).',
|
|
159
|
+
'',
|
|
160
|
+
'Shell completions:',
|
|
161
|
+
' vx completions <shell> Print a completion script for bash, zsh or fish: the',
|
|
162
|
+
' verbs (plugin verbs of this workspace included) and',
|
|
163
|
+
' every flag of each. `eval "$(vx completions bash)"`,',
|
|
164
|
+
' or write it where the shell loads completions from.',
|
|
138
165
|
'',
|
|
139
166
|
'Config lock:',
|
|
140
167
|
' vx lock Evaluate every vx.config in the current environment and',
|
|
@@ -150,6 +177,15 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
150
177
|
].join('\n')
|
|
151
178
|
}
|
|
152
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Every argument error ends with this. `vx <verb> --help` prints the
|
|
182
|
+
* reference (2026-09-04); before that there was nowhere to send a user who
|
|
183
|
+
* got a flag wrong, so the errors just said no.
|
|
184
|
+
*/
|
|
185
|
+
export function seeHelp(verb: string): string {
|
|
186
|
+
return ` (see \`vx ${verb} --help\`)`
|
|
187
|
+
}
|
|
188
|
+
|
|
153
189
|
/**
|
|
154
190
|
* The documented `--flags` of one verb, read from the help text's
|
|
155
191
|
* sections headed `… (for <verb>):` so there is no second list to drift —
|
|
@@ -166,3 +202,5 @@ export function documentedFlags(verb: string): string[] {
|
|
|
166
202
|
}
|
|
167
203
|
return [...flags]
|
|
168
204
|
}
|
|
205
|
+
|
|
206
|
+
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,14 +60,14 @@ 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
|
|
63
68
|
// nothing here can shadow them.
|
|
64
69
|
const resolved = await resolvePluginCommand(command)
|
|
65
|
-
if (resolved !== null && !('loadError' in resolved)) {
|
|
70
|
+
if (resolved !== null && !('loadError' in resolved) && !('declaredVerbs' in resolved)) {
|
|
66
71
|
const code = await resolved.command.run(rest, resolved.ctx)
|
|
67
72
|
// A plugin is a boundary: a JS-authored verb that resolves nothing
|
|
68
73
|
// would reach `process.exit(undefined)` and read as SUCCESS. A verb
|
|
@@ -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,20 +110,72 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
97
110
|
)
|
|
98
111
|
return 1
|
|
99
112
|
}
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
// One line, as a verb's own unknown flag or subcommand gets; the full
|
|
114
|
+
// help after a typo was a hundred lines past the hint that mattered.
|
|
115
|
+
// The verbs this workspace's plugins declare are verbs HERE, so they
|
|
116
|
+
// join the "did you mean" set, and when nothing is close the second
|
|
117
|
+
// line says where a verb can come from: `vx mpc` in a workspace
|
|
118
|
+
// declaring `mcp` read as a plain unknown command, and `vx mcp`
|
|
119
|
+
// before the plugin was declared said nothing about the file that
|
|
120
|
+
// would declare it (2026-09-20).
|
|
121
|
+
const declaredVerbs =
|
|
122
|
+
resolved !== null && 'declaredVerbs' in resolved ? resolved.declaredVerbs : []
|
|
123
|
+
const guess = didYouMeanVerb(command, declaredVerbs)
|
|
124
|
+
process.stderr.write(
|
|
125
|
+
`vx: unknown command: ${command}${guess}${loadNote} (see \`vx help\`)\n` +
|
|
126
|
+
(guess === '' && loadNote === '' ? verbSourceNote(resolved, declaredVerbs) : ''),
|
|
127
|
+
)
|
|
102
128
|
return 1
|
|
103
129
|
}
|
|
104
130
|
}
|
|
105
131
|
}
|
|
106
132
|
|
|
107
133
|
// Re-exports for tests + programmatic embedders.
|
|
108
|
-
export {
|
|
134
|
+
export {
|
|
135
|
+
detectFlow,
|
|
136
|
+
parseConcurrency,
|
|
137
|
+
parseRunArgs,
|
|
138
|
+
resolveRunOptions,
|
|
139
|
+
type RunArgs,
|
|
140
|
+
} from './run.js'
|
|
109
141
|
export { parsePruneArgs, parseDuration, parseSize } from './cache.js'
|
|
110
142
|
export { parseLockArgs, type LockArgs } from './lock.js'
|
|
111
|
-
export {
|
|
143
|
+
export { parseInitArgs, type InitArgs } from './init.js'
|
|
112
144
|
export { parseShowArgs, type ShowArgs } from './show.js'
|
|
113
145
|
export { parseWhyArgs } from './why.js'
|
|
114
146
|
export { parseLastArgs } from './last.js'
|
|
115
|
-
export { parsePruneWorkspaceArgs } from './prune.js'
|
|
116
147
|
export { formatBytes } from './format.js'
|
|
148
|
+
export { registerCoreAlias } from './core-alias.js'
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Is this invocation asking for help rather than work? See the call site for
|
|
152
|
+
* why the scan stops at `--`, and why plugin verbs are excluded.
|
|
153
|
+
*/
|
|
154
|
+
function wantsHelp(command: string, rest: readonly string[]): boolean {
|
|
155
|
+
if (!(CORE_VERBS as readonly string[]).includes(command)) return false
|
|
156
|
+
const sep = rest.indexOf('--')
|
|
157
|
+
const own = sep === -1 ? rest : rest.slice(0, sep)
|
|
158
|
+
return own.includes('--help') || own.includes('-h')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** ` Did you mean run?` for a verb within two edits of a core one, or of a
|
|
162
|
+
* verb this workspace's plugins declare — the same hint a task or flag typo
|
|
163
|
+
* gets. The plugin verbs come from the lookup that just failed, so they
|
|
164
|
+
* cost no second load. */
|
|
165
|
+
function didYouMeanVerb(verb: string, pluginVerbsHere: readonly string[] = []): string {
|
|
166
|
+
const best = nearest(verb, [...CORE_VERBS, ...pluginVerbsHere])
|
|
167
|
+
return best === undefined ? '' : `. Did you mean ${best}?`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Where a verb that is neither core nor declared COULD come from. Only when
|
|
172
|
+
* nothing is close enough to guess: after a plain typo the guess is the
|
|
173
|
+
* answer, and a second line would bury it.
|
|
174
|
+
*/
|
|
175
|
+
function verbSourceNote(resolved: unknown, declaredVerbs: readonly string[]): string {
|
|
176
|
+
if (declaredVerbs.length > 0) {
|
|
177
|
+
return ` This workspace's plugins declare: ${[...declaredVerbs].sort().join(', ')}.\n`
|
|
178
|
+
}
|
|
179
|
+
const here = resolved === null ? 'there is no workspace here' : 'this workspace declares none'
|
|
180
|
+
return ` A plugin declared in vx.workspace.ts can add verbs; ${here}.\n`
|
|
181
|
+
}
|
package/src/cli/info.ts
CHANGED
|
@@ -1,110 +1,181 @@
|
|
|
1
1
|
// `vx info` — workspace doctor printout. One screen of environment +
|
|
2
2
|
// workspace + cache facts for bug reports and quick sanity checks.
|
|
3
|
-
// `vx stats` is a deprecated alias (info absorbed it).
|
|
3
|
+
// `vx stats` is a deprecated alias (info absorbed it). The facts come
|
|
4
|
+
// from the orchestrator's `collectInfo` (doctor.ts), which `vx mcp` reads
|
|
5
|
+
// too; this file parses the flags and renders the rows.
|
|
4
6
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
findWorkspaceRoot,
|
|
9
|
-
listProjects,
|
|
10
|
-
loadProjectConfig,
|
|
11
|
-
loadWorkspace,
|
|
12
|
-
loadWorkspaceConfig,
|
|
13
|
-
lockfilePath,
|
|
14
|
-
resolveCacheDir,
|
|
15
|
-
} from '../workspace/index.js'
|
|
7
|
+
import { collectInfo, type FlakyTask, type InfoFacts } from '../orchestrator/index.js'
|
|
8
|
+
import { seeHelp } from './help.js'
|
|
9
|
+
import { namedCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
|
|
16
10
|
import { formatBytes } from './format.js'
|
|
11
|
+
import { MIN_BUN } from '../util/index.js'
|
|
17
12
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const metas = await listProjects(await loadWorkspace(root))
|
|
13
|
+
export interface InfoArgs {
|
|
14
|
+
format: 'pretty' | 'json'
|
|
15
|
+
/** `--cache-dir`: report on the cache a run with the same flag uses. */
|
|
16
|
+
cacheDir?: string
|
|
17
|
+
error?: string
|
|
18
|
+
}
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
taskCount += Object.keys(config.tasks ?? {}).length
|
|
35
|
-
} catch {
|
|
36
|
-
// counted as zero
|
|
20
|
+
function parseInfoArgs(args: readonly string[]): InfoArgs {
|
|
21
|
+
const out: InfoArgs = { format: 'pretty' }
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
const a = args[i]
|
|
24
|
+
if (a === '--format' || a?.startsWith('--format=')) {
|
|
25
|
+
const v = a === '--format' ? args[++i] : a.slice('--format='.length)
|
|
26
|
+
if (v !== 'pretty' && v !== 'json') {
|
|
27
|
+
return { ...out, error: `--format must be pretty or json${seeHelp('info')}` }
|
|
37
28
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
29
|
+
out.format = v
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
const cd = parseCacheDirFlag(args, i)
|
|
33
|
+
if (cd !== null) {
|
|
34
|
+
if ('error' in cd) return { ...out, error: cd.error }
|
|
35
|
+
out.cacheDir = cd.cacheDir
|
|
36
|
+
i = cd.next
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
return { ...out, error: `unknown argument: ${a}${seeHelp('info')}` }
|
|
48
40
|
}
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
49
43
|
|
|
50
|
-
|
|
44
|
+
export async function infoCmd(args: readonly string[]): Promise<number> {
|
|
45
|
+
const parsed = parseInfoArgs(args)
|
|
46
|
+
if (parsed.error) {
|
|
47
|
+
process.stderr.write(`vx info: ${parsed.error}\n`)
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
// `collectInfo` takes a resolved directory from `vx mcp` too, where one
|
|
51
|
+
// not made yet is a workspace that never ran; the flag is the user's,
|
|
52
|
+
// and a directory it names that is not there is a typo (item 900).
|
|
53
|
+
if (parsed.cacheDir !== undefined) namedCacheDir(parsed.cacheDir)
|
|
54
|
+
const facts = await collectInfo(process.cwd(), {
|
|
55
|
+
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
56
|
+
warn: warnToStderr,
|
|
57
|
+
})
|
|
58
|
+
process.stdout.write(
|
|
59
|
+
parsed.format === 'json' ? `${JSON.stringify(facts, null, 2)}\n` : `${renderInfo(facts)}\n`,
|
|
60
|
+
)
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
51
63
|
|
|
64
|
+
export function renderInfo(f: InfoFacts): string {
|
|
52
65
|
const rows: [string, string][] = [
|
|
53
|
-
['vx',
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
['
|
|
63
|
-
['cache
|
|
64
|
-
['
|
|
65
|
-
['
|
|
66
|
-
|
|
66
|
+
['vx', f.vx],
|
|
67
|
+
// The prose stays in the rendered row; `f.bun` itself is the bare
|
|
68
|
+
// version a script reads (doctor.ts says why).
|
|
69
|
+
[
|
|
70
|
+
'bun',
|
|
71
|
+
f.bunSupported
|
|
72
|
+
? f.bun
|
|
73
|
+
: `${f.bun} — unsupported, vx needs >= ${MIN_BUN.join('.')}; answers may be wrong`,
|
|
74
|
+
],
|
|
75
|
+
['git', f.git ?? '(not found)'],
|
|
76
|
+
['git status cache', renderGitStatusCache(f.gitStatusCache)],
|
|
77
|
+
['workspace root', f.workspaceRoot],
|
|
78
|
+
['projects', describeProjects(f)],
|
|
79
|
+
// Only when a config did not load: the count above says "zero" for
|
|
80
|
+
// it, and this row says which and why.
|
|
81
|
+
...(f.configErrors.length > 0
|
|
82
|
+
? ([['config errors', f.configErrors.map((e) => `${e.path}: ${e.message}`).join('; ')]] as [
|
|
83
|
+
string,
|
|
84
|
+
string,
|
|
85
|
+
][])
|
|
86
|
+
: []),
|
|
87
|
+
['plugins', describePlugins(f.plugins)],
|
|
88
|
+
['workers', describeWorkers(f.workers)],
|
|
89
|
+
['memory', describeMemory(f.memory)],
|
|
90
|
+
['cache dir', f.cacheDir],
|
|
91
|
+
['cache versions', `keys ${f.cacheVersion} · index schema ${f.schemaVersion}`],
|
|
92
|
+
['cache entries', `${f.cacheEntries} (${formatBytes(f.cacheBytes)})`],
|
|
93
|
+
// Only when there is something to say.
|
|
94
|
+
...(f.orphans.artifacts > 0
|
|
95
|
+
? ([
|
|
96
|
+
[
|
|
97
|
+
'orphans',
|
|
98
|
+
`${f.orphans.artifacts} artifact${f.orphans.artifacts === 1 ? '' : 's'} (${formatBytes(f.orphans.bytes)}) the index does not know — \`vx cache prune\` reaps them`,
|
|
99
|
+
],
|
|
100
|
+
] as [string, string][])
|
|
101
|
+
: []),
|
|
102
|
+
// Task runs, not invocations (which `vx last` calls runs): the count
|
|
103
|
+
// the hits are a share of.
|
|
104
|
+
['task runs (24h)', `${f.runs24h} (${f.hits24h} cache hits)`],
|
|
105
|
+
['flaky tasks', describeFlakyTasks(f.flakyTasks)],
|
|
106
|
+
['sandbox', describeSandbox(f.sandbox)],
|
|
107
|
+
['vx-lock.json', f.lockfile ? 'yes' : 'no'],
|
|
67
108
|
]
|
|
68
109
|
const labelW = Math.max(...rows.map(([label]) => label.length))
|
|
69
|
-
|
|
70
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
71
|
-
return 0
|
|
110
|
+
return rows.map(([label, value]) => `${`${label}:`.padEnd(labelW + 1)} ${value}`).join('\n')
|
|
72
111
|
}
|
|
73
112
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const on = (key: string): boolean =>
|
|
85
|
-
new RegExp(`^core\\.${key} (true|1|yes|on)$`, 'im').test(out)
|
|
86
|
-
const fsmonitor = on('fsmonitor')
|
|
87
|
-
const untracked = on('untrackedcache')
|
|
88
|
-
if (fsmonitor && untracked) return 'fsmonitor + untrackedCache on'
|
|
89
|
-
const missing = [
|
|
90
|
-
...(fsmonitor ? [] : ['core.fsmonitor']),
|
|
91
|
-
...(untracked ? [] : ['core.untrackedCache']),
|
|
92
|
-
]
|
|
93
|
-
return `${missing.join(', ')} off — \`git config ${missing[0]} true\` makes every run's status walk near-free on a large tree`
|
|
94
|
-
} catch {
|
|
95
|
-
return '(unknown)'
|
|
96
|
-
}
|
|
113
|
+
/**
|
|
114
|
+
* `available (3 tasks declare exec.sandbox)`, or `unavailable — <why>; 3
|
|
115
|
+
* tasks declare exec.sandbox and will fail`: a declared sandbox whose
|
|
116
|
+
* runtime cannot start fails the task, so the doctor names it before a run
|
|
117
|
+
* does.
|
|
118
|
+
*/
|
|
119
|
+
function describeSandbox(s: InfoFacts['sandbox']): string {
|
|
120
|
+
const declared = `${s.declared} task${s.declared === 1 ? '' : 's'} declare${s.declared === 1 ? 's' : ''} exec.sandbox`
|
|
121
|
+
if (s.available) return `available (${declared})`
|
|
122
|
+
return `unavailable — ${s.reason}; ${declared}${s.declared > 0 ? ' and will fail' : ''}`
|
|
97
123
|
}
|
|
98
124
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
/** `4 — the CPU count`, `2 — cgroup CPU quota 2 of 8 cores`, `8 — vx.workspace.ts (4 cores)`. */
|
|
126
|
+
export function describeWorkers(w: InfoFacts['workers']): string {
|
|
127
|
+
const quota = w.cpuQuota === null ? '' : `, cgroup CPU quota ${trimCores(w.cpuQuota)}`
|
|
128
|
+
if (w.source === 'workspace') return `${w.count} — vx.workspace.ts (${w.cores} cores${quota})`
|
|
129
|
+
if (w.source === 'cgroup')
|
|
130
|
+
return `${w.count} — cgroup CPU quota ${trimCores(w.cpuQuota ?? w.count)} of ${w.cores} cores`
|
|
131
|
+
return `${w.count} — the CPU count${quota}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `13 GB usable — cgroup limit; the machine has 16 GB`, or `16 GB`. */
|
|
135
|
+
export function describeMemory(m: InfoFacts['memory']): string {
|
|
136
|
+
if (m.cgroupLimitBytes !== null && m.usableBytes < m.totalBytes) {
|
|
137
|
+
return `${formatBytes(m.usableBytes)} usable — cgroup limit; the machine has ${formatBytes(m.totalBytes)}`
|
|
109
138
|
}
|
|
139
|
+
return formatBytes(m.totalBytes)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function trimCores(n: number): string {
|
|
143
|
+
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/0+$/, '')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** `2 — app#test (3 of 7 runs failed on unchanged inputs); api#e2e (1 of 4)`, or `none`. */
|
|
147
|
+
function describeProjects(f: InfoFacts): string {
|
|
148
|
+
const tasks = `${f.tasks} task${f.tasks === 1 ? '' : 's'}`
|
|
149
|
+
const n = f.configErrors.length
|
|
150
|
+
if (n === 0) return `${f.projects} (${tasks})`
|
|
151
|
+
return `${f.projects} (${tasks} · ${n} config${n === 1 ? '' : 's'} did not load)`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function describeFlakyTasks(tasks: readonly FlakyTask[]): string {
|
|
155
|
+
if (tasks.length === 0) return 'none'
|
|
156
|
+
const parts = tasks.map(
|
|
157
|
+
(t, i) =>
|
|
158
|
+
`${t.taskId} (${t.failures} of ${t.passes + t.failures} runs failed${i === 0 ? ' on unchanged inputs' : ''})`,
|
|
159
|
+
)
|
|
160
|
+
return `${tasks.length} — ${parts.join('; ')}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function describePlugins(
|
|
164
|
+
plugins: ReadonlyArray<{ name: string; seams: readonly string[] }>,
|
|
165
|
+
): string {
|
|
166
|
+
if (plugins.length === 0) return 'none'
|
|
167
|
+
const parts = plugins.map(
|
|
168
|
+
(p) => `${p.name} (${p.seams.length === 0 ? 'no seams' : p.seams.join(', ')})`,
|
|
169
|
+
)
|
|
170
|
+
return `${plugins.length} — ${parts.join('; ')}`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderGitStatusCache(c: InfoFacts['gitStatusCache']): string {
|
|
174
|
+
if (c === null) return '(unknown)'
|
|
175
|
+
if (c.fsmonitor && c.untrackedCache) return 'fsmonitor + untrackedCache on'
|
|
176
|
+
const missing = [
|
|
177
|
+
...(c.fsmonitor ? [] : ['core.fsmonitor']),
|
|
178
|
+
...(c.untrackedCache ? [] : ['core.untrackedCache']),
|
|
179
|
+
]
|
|
180
|
+
return `${missing.join(', ')} off`
|
|
110
181
|
}
|