@vzn/vx 0.0.16 → 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 +135 -124
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +11 -7
- package/src/bin.ts +14 -4
- package/src/cache/archive.ts +659 -0
- package/src/cache/cache.ts +769 -1101
- package/src/cache/chained-cache.ts +194 -0
- 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 +24 -5
- package/src/cache/inputs.ts +373 -456
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +96 -43
- 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/tar-stream.ts +358 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +75 -21
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +195 -128
- package/src/cli/index.ts +116 -46
- package/src/cli/info.ts +122 -63
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +172 -0
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +72 -6
- package/src/cli/plugin-commands.ts +103 -0
- package/src/cli/run.ts +245 -271
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +9 -7
- package/src/cli/watch.ts +858 -110
- package/src/cli/why.ts +223 -0
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +187 -150
- package/src/exec/env.ts +18 -1
- package/src/exec/executor.ts +224 -0
- package/src/exec/index.ts +24 -2
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +166 -40
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +642 -260
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +4 -10
- package/src/graph/scheduler.ts +232 -203
- package/src/graph/task-graph.ts +222 -8
- package/src/index.ts +146 -153
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +147 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +72 -8
- package/src/orchestrator/execute-task.ts +374 -549
- package/src/orchestrator/failure-mode.ts +202 -0
- package/src/orchestrator/framed-output.ts +126 -69
- package/src/orchestrator/history.ts +126 -59
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +77 -93
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +208 -35
- package/src/orchestrator/metrics.ts +102 -1388
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +55 -34
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +357 -120
- package/src/orchestrator/plugin.ts +298 -58
- package/src/orchestrator/prepare.ts +202 -106
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/run-artifacts.ts +63 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +74 -47
- package/src/orchestrator/run.ts +534 -417
- 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 +129 -31
- package/src/orchestrator/status-line.ts +49 -9
- package/src/orchestrator/summary.ts +90 -26
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +111 -15
- package/src/orchestrator/task-log-buffer.ts +288 -0
- package/src/orchestrator/telemetry-host.ts +34 -6
- package/src/orchestrator/telemetry.ts +182 -36
- package/src/orchestrator/upstream.ts +72 -31
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +70 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +15 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +64 -0
- package/src/util/settle.ts +67 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +250 -23
- package/src/workspace/config-cache.ts +313 -0
- package/src/workspace/config-eval.ts +217 -0
- package/src/workspace/config-imports.ts +162 -0
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +65 -10
- package/src/workspace/fingerprint.ts +53 -4
- package/src/workspace/index.ts +42 -3
- package/src/workspace/lockfile.ts +25 -9
- package/src/workspace/migrate-scripts.ts +134 -0
- package/src/workspace/migration.ts +314 -0
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +114 -41
- package/src/workspace/project-loader.ts +224 -450
- package/src/workspace/workspace.ts +298 -72
- package/src/cache/cas-backend.ts +0 -97
- package/src/cache/digest.ts +0 -47
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/cli/migrate-nx.ts +0 -450
- package/src/cli/migrate-turbo.ts +0 -364
- package/src/cli/migrate.ts +0 -219
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/resources.ts +0 -59
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/verify.ts +0 -164
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
package/src/cli/index.ts
CHANGED
|
@@ -4,25 +4,36 @@
|
|
|
4
4
|
|
|
5
5
|
import { VERSION } from '../version.js'
|
|
6
6
|
import { runCmd } from './run.js'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
|
|
11
|
+
// Every verb but `run` is imported when invoked. `vx run` is the hot path
|
|
12
|
+
// and nearly every invocation; the other verbs' modules are code that
|
|
13
|
+
// process never calls. Measured 2026-09-03: `--version` is 25 ms either
|
|
14
|
+
// way (module loading is not where start-up goes), so this is hygiene, not
|
|
15
|
+
// a speed-up. The specifiers are string literals, so `bun build --compile`
|
|
16
|
+
// still embeds them.
|
|
16
17
|
|
|
17
18
|
export async function run(argv: readonly string[]): Promise<number> {
|
|
18
19
|
const [command, ...rest] = argv
|
|
19
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
|
+
|
|
20
31
|
switch (command) {
|
|
21
32
|
case undefined:
|
|
22
33
|
case '--help':
|
|
23
34
|
case '-h':
|
|
24
35
|
case 'help':
|
|
25
|
-
printHelp()
|
|
36
|
+
printHelp(await pluginCommandHelp())
|
|
26
37
|
return 0
|
|
27
38
|
case '--version':
|
|
28
39
|
case 'version':
|
|
@@ -31,55 +42,114 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
31
42
|
case 'run':
|
|
32
43
|
return await runCmd(rest)
|
|
33
44
|
case 'watch':
|
|
34
|
-
return await watchCmd(rest)
|
|
45
|
+
return await (await import('./watch.js')).watchCmd(rest)
|
|
35
46
|
case 'cache':
|
|
36
|
-
return await cacheCmd(rest)
|
|
47
|
+
return await (await import('./cache.js')).cacheCmd(rest)
|
|
37
48
|
case 'lock':
|
|
38
|
-
return await lockCmd(rest)
|
|
39
|
-
case '
|
|
40
|
-
return await
|
|
49
|
+
return await (await import('./lock.js')).lockCmd(rest)
|
|
50
|
+
case 'init':
|
|
51
|
+
return await (await import('./init.js')).initCmd(rest)
|
|
41
52
|
case 'upgrade':
|
|
42
|
-
return await upgradeCmd(rest)
|
|
53
|
+
return await (await import('./upgrade.js')).upgradeCmd(rest)
|
|
43
54
|
case 'show':
|
|
44
|
-
return await showCmd(rest)
|
|
55
|
+
return await (await import('./show.js')).showCmd(rest)
|
|
45
56
|
case 'info':
|
|
46
57
|
case 'stats': // deprecated alias — `vx info` absorbed `vx stats`
|
|
47
|
-
return await infoCmd(rest)
|
|
48
|
-
case '
|
|
49
|
-
return await
|
|
50
|
-
case '
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
return await (await import('./info.js')).infoCmd(rest)
|
|
59
|
+
case 'why':
|
|
60
|
+
return await (await import('./why.js')).whyCmd(rest)
|
|
61
|
+
case 'last':
|
|
62
|
+
return await (await import('./last.js')).lastCmd(rest)
|
|
63
|
+
case 'completions':
|
|
64
|
+
return await (await import('./completions.js')).completionsCmd(rest, await pluginVerbs())
|
|
65
|
+
default: {
|
|
66
|
+
// Not a core verb: a plugin declared in the workspace around the cwd
|
|
67
|
+
// may own it (`VxPlugin.commands`). Core verbs were matched above, so
|
|
68
|
+
// nothing here can shadow them.
|
|
69
|
+
const resolved = await resolvePluginCommand(command)
|
|
70
|
+
if (resolved !== null && !('loadError' in resolved)) {
|
|
71
|
+
const code = await resolved.command.run(rest, resolved.ctx)
|
|
72
|
+
// A plugin is a boundary: a JS-authored verb that resolves nothing
|
|
73
|
+
// would reach `process.exit(undefined)` and read as SUCCESS. A verb
|
|
74
|
+
// that cannot say whether it succeeded fails, naming its owner.
|
|
75
|
+
if (!Number.isInteger(code)) {
|
|
76
|
+
throw new UserError(
|
|
77
|
+
`plugin '${resolved.plugin.name}': command '${command}' resolved ${JSON.stringify(code)} instead of an exit code`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
return code
|
|
81
|
+
}
|
|
82
|
+
// A broken workspace file cannot say whether the verb exists. Say
|
|
83
|
+
// both things: the verb is unknown HERE, and why the lookup could not
|
|
84
|
+
// finish — a typo still reads as a typo, and a real plugin verb still
|
|
85
|
+
// points at the file that broke it.
|
|
86
|
+
const loadNote =
|
|
87
|
+
resolved !== null && 'loadError' in resolved
|
|
88
|
+
? `\n (plugin verbs could not be looked up: vx.workspace failed to load: ${resolved.loadError})`
|
|
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
|
+
}
|
|
98
|
+
if (command === 'serve' || command === 'dev') {
|
|
99
|
+
// vx core is only a task runner — it has no service layer of its
|
|
100
|
+
// own. A dashboard, remote cache, distributed execution, etc. are
|
|
101
|
+
// provided by PLUGINS (declared in vx.workspace.ts), never by core.
|
|
102
|
+
// We keep this neutral hint for the common muscle-memory verbs, but
|
|
103
|
+
// core names no specific plugin package: any package can provide
|
|
104
|
+
// these.
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
`vx: '${command}' is not a vx core command.\n` +
|
|
107
|
+
` vx core runs tasks in-process. A dashboard, remote cache, and\n` +
|
|
108
|
+
` distributed execution come from plugins — not core. See the plugin\n` +
|
|
109
|
+
` guide: https://vznjs.github.io/vx/guides/plugins/\n`,
|
|
110
|
+
)
|
|
111
|
+
return 1
|
|
112
|
+
}
|
|
113
|
+
process.stderr.write(`vx: unknown command: ${command}${didYouMeanVerb(command)}${loadNote}\n`)
|
|
66
114
|
printHelp()
|
|
67
115
|
return 1
|
|
116
|
+
}
|
|
68
117
|
}
|
|
69
118
|
}
|
|
70
119
|
|
|
71
120
|
// Re-exports for tests + programmatic embedders.
|
|
72
|
-
export {
|
|
121
|
+
export {
|
|
122
|
+
detectFlow,
|
|
123
|
+
parseConcurrency,
|
|
124
|
+
parseRunArgs,
|
|
125
|
+
resolveRunOptions,
|
|
126
|
+
type RunArgs,
|
|
127
|
+
} from './run.js'
|
|
73
128
|
export { parsePruneArgs, parseDuration, parseSize } from './cache.js'
|
|
74
129
|
export { parseLockArgs, type LockArgs } from './lock.js'
|
|
75
|
-
export {
|
|
130
|
+
export { parseInitArgs, type InitArgs } from './init.js'
|
|
76
131
|
export { parseShowArgs, type ShowArgs } from './show.js'
|
|
77
|
-
export {
|
|
78
|
-
export {
|
|
79
|
-
handleMcpRequest,
|
|
80
|
-
listMcpTools,
|
|
81
|
-
setMcpContext,
|
|
82
|
-
type McpContext,
|
|
83
|
-
type McpToolDef,
|
|
84
|
-
} from './mcp-rpc.js'
|
|
132
|
+
export { parseWhyArgs } from './why.js'
|
|
133
|
+
export { parseLastArgs } from './last.js'
|
|
85
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
|
+
}
|
package/src/cli/info.ts
CHANGED
|
@@ -1,80 +1,139 @@
|
|
|
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 { parseCacheDirFlag, warnToStderr } from './workspace-config.js'
|
|
16
10
|
import { formatBytes } from './format.js'
|
|
17
11
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const metas = await listProjects(await loadWorkspace(root))
|
|
12
|
+
export interface InfoArgs {
|
|
13
|
+
format: 'pretty' | 'json'
|
|
14
|
+
/** `--cache-dir`: report on the cache a run with the same flag uses. */
|
|
15
|
+
cacheDir?: string
|
|
16
|
+
error?: string
|
|
17
|
+
}
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
taskCount += Object.keys(config.tasks ?? {}).length
|
|
35
|
-
} catch {
|
|
36
|
-
// counted as zero
|
|
19
|
+
export function parseInfoArgs(args: readonly string[]): InfoArgs {
|
|
20
|
+
const out: InfoArgs = { format: 'pretty' }
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
const a = args[i]
|
|
23
|
+
if (a === '--format' || a?.startsWith('--format=')) {
|
|
24
|
+
const v = a === '--format' ? args[++i] : a.slice('--format='.length)
|
|
25
|
+
if (v !== 'pretty' && v !== 'json') {
|
|
26
|
+
return { ...out, error: `--format must be pretty or json${seeHelp('info')}` }
|
|
37
27
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
28
|
+
out.format = v
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
const cd = parseCacheDirFlag(args, i)
|
|
32
|
+
if (cd !== null) {
|
|
33
|
+
if ('error' in cd) return { ...out, error: cd.error }
|
|
34
|
+
out.cacheDir = cd.cacheDir
|
|
35
|
+
i = cd.next
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
return { ...out, error: `unknown argument: ${a}${seeHelp('info')}` }
|
|
48
39
|
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
49
42
|
|
|
50
|
-
|
|
43
|
+
export async function infoCmd(args: readonly string[]): Promise<number> {
|
|
44
|
+
const parsed = parseInfoArgs(args)
|
|
45
|
+
if (parsed.error) {
|
|
46
|
+
process.stderr.write(`vx info: ${parsed.error}\n`)
|
|
47
|
+
return 1
|
|
48
|
+
}
|
|
49
|
+
const facts = await collectInfo(process.cwd(), {
|
|
50
|
+
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
51
|
+
warn: warnToStderr,
|
|
52
|
+
})
|
|
53
|
+
process.stdout.write(
|
|
54
|
+
parsed.format === 'json' ? `${JSON.stringify(facts, null, 2)}\n` : `${renderInfo(facts)}\n`,
|
|
55
|
+
)
|
|
56
|
+
return 0
|
|
57
|
+
}
|
|
51
58
|
|
|
59
|
+
export function renderInfo(f: InfoFacts): string {
|
|
52
60
|
const rows: [string, string][] = [
|
|
53
|
-
['vx',
|
|
54
|
-
['bun',
|
|
55
|
-
['git',
|
|
56
|
-
['
|
|
57
|
-
['
|
|
58
|
-
['
|
|
59
|
-
['
|
|
60
|
-
['
|
|
61
|
-
['
|
|
61
|
+
['vx', f.vx],
|
|
62
|
+
['bun', f.bun],
|
|
63
|
+
['git', f.git ?? '(not found)'],
|
|
64
|
+
['git status cache', renderGitStatusCache(f.gitStatusCache)],
|
|
65
|
+
['workspace root', f.workspaceRoot],
|
|
66
|
+
['projects', `${f.projects} (${f.tasks} task${f.tasks === 1 ? '' : 's'})`],
|
|
67
|
+
['plugins', describePlugins(f.plugins)],
|
|
68
|
+
['workers', describeWorkers(f.workers)],
|
|
69
|
+
['memory', describeMemory(f.memory)],
|
|
70
|
+
['cache dir', f.cacheDir],
|
|
71
|
+
['cache versions', `keys ${f.cacheVersion} · index schema ${f.schemaVersion}`],
|
|
72
|
+
['cache entries', `${f.cacheEntries} (${formatBytes(f.cacheBytes)})`],
|
|
73
|
+
// Only when there is something to say.
|
|
74
|
+
...(f.orphans.artifacts > 0
|
|
75
|
+
? ([
|
|
76
|
+
[
|
|
77
|
+
'orphans',
|
|
78
|
+
`${f.orphans.artifacts} artifact${f.orphans.artifacts === 1 ? '' : 's'} (${formatBytes(f.orphans.bytes)}) the index does not know — \`vx cache prune\` reaps them`,
|
|
79
|
+
],
|
|
80
|
+
] as [string, string][])
|
|
81
|
+
: []),
|
|
82
|
+
['runs (24h)', `${f.runs24h} (${f.hits24h} cache hits)`],
|
|
83
|
+
['flaky tasks', describeFlakyTasks(f.flakyTasks)],
|
|
84
|
+
['vx-lock.json', f.lockfile ? 'yes' : 'no'],
|
|
62
85
|
]
|
|
63
86
|
const labelW = Math.max(...rows.map(([label]) => label.length))
|
|
64
|
-
|
|
65
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
66
|
-
return 0
|
|
87
|
+
return rows.map(([label, value]) => `${`${label}:`.padEnd(labelW + 1)} ${value}`).join('\n')
|
|
67
88
|
}
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
/** `4 — the CPU count`, `2 — cgroup CPU quota 2 of 8 cores`, `8 — vx.workspace.ts (4 cores)`. */
|
|
91
|
+
export function describeWorkers(w: InfoFacts['workers']): string {
|
|
92
|
+
const quota = w.cpuQuota === null ? '' : `, cgroup CPU quota ${trimCores(w.cpuQuota)}`
|
|
93
|
+
if (w.source === 'workspace') return `${w.count} — vx.workspace.ts (${w.cores} cores${quota})`
|
|
94
|
+
if (w.source === 'cgroup')
|
|
95
|
+
return `${w.count} — cgroup CPU quota ${trimCores(w.cpuQuota ?? w.count)} of ${w.cores} cores`
|
|
96
|
+
return `${w.count} — the CPU count${quota}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `13 GB usable — cgroup limit; the machine has 16 GB`, or `16 GB`. */
|
|
100
|
+
export function describeMemory(m: InfoFacts['memory']): string {
|
|
101
|
+
if (m.cgroupLimitBytes !== null && m.usableBytes < m.totalBytes) {
|
|
102
|
+
return `${formatBytes(m.usableBytes)} usable — cgroup limit; the machine has ${formatBytes(m.totalBytes)}`
|
|
79
103
|
}
|
|
104
|
+
return formatBytes(m.totalBytes)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function trimCores(n: number): string {
|
|
108
|
+
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/0+$/, '')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `2 — app#test (3 of 7 runs failed on unchanged inputs); api#e2e (1 of 4)`, or `none`. */
|
|
112
|
+
export function describeFlakyTasks(tasks: readonly FlakyTask[]): string {
|
|
113
|
+
if (tasks.length === 0) return 'none'
|
|
114
|
+
const parts = tasks.map(
|
|
115
|
+
(t, i) =>
|
|
116
|
+
`${t.taskId} (${t.failures} of ${t.passes + t.failures} runs failed${i === 0 ? ' on unchanged inputs' : ''})`,
|
|
117
|
+
)
|
|
118
|
+
return `${tasks.length} — ${parts.join('; ')}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function describePlugins(
|
|
122
|
+
plugins: ReadonlyArray<{ name: string; seams: readonly string[] }>,
|
|
123
|
+
): string {
|
|
124
|
+
if (plugins.length === 0) return 'none'
|
|
125
|
+
const parts = plugins.map(
|
|
126
|
+
(p) => `${p.name} (${p.seams.length === 0 ? 'no seams' : p.seams.join(', ')})`,
|
|
127
|
+
)
|
|
128
|
+
return `${plugins.length} — ${parts.join('; ')}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderGitStatusCache(c: InfoFacts['gitStatusCache']): string {
|
|
132
|
+
if (c === null) return '(unknown)'
|
|
133
|
+
if (c.fsmonitor && c.untrackedCache) return 'fsmonitor + untrackedCache on'
|
|
134
|
+
const missing = [
|
|
135
|
+
...(c.fsmonitor ? [] : ['core.fsmonitor']),
|
|
136
|
+
...(c.untrackedCache ? [] : ['core.untrackedCache']),
|
|
137
|
+
]
|
|
138
|
+
return `${missing.join(', ')} off`
|
|
80
139
|
}
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// `vx init [--dry] [--force] [--mjs]` — a workspace from nowhere: one vx.config.ts
|
|
2
|
+
// per package from its package.json scripts, and the workspace file every
|
|
3
|
+
// run needs. Turbo and Nx are not read here; a runner's own config beside
|
|
4
|
+
// the scripts is the richer source, and `@vzn/vx-migrate` maps it.
|
|
5
|
+
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { seeHelp } from './help.js'
|
|
8
|
+
import {
|
|
9
|
+
applyMigration,
|
|
10
|
+
findWorkspaceRoot,
|
|
11
|
+
listProjects,
|
|
12
|
+
loadWorkspace,
|
|
13
|
+
migrateScripts,
|
|
14
|
+
} from '../workspace/index.js'
|
|
15
|
+
|
|
16
|
+
export interface InitArgs {
|
|
17
|
+
dry: boolean
|
|
18
|
+
force: boolean
|
|
19
|
+
/** `vx.config.mjs` instead of `.ts` — see `ApplyMigrationArgs.format`. */
|
|
20
|
+
mjs: boolean
|
|
21
|
+
error?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseInitArgs(args: readonly string[]): InitArgs {
|
|
25
|
+
const out: InitArgs = { dry: false, force: false, mjs: false }
|
|
26
|
+
for (const a of args) {
|
|
27
|
+
if (a === '--dry') out.dry = true
|
|
28
|
+
else if (a === '--force') out.force = true
|
|
29
|
+
else if (a === '--mjs') out.mjs = true
|
|
30
|
+
else if (a.startsWith('-')) return { ...out, error: `unknown flag: ${a}${seeHelp('init')}` }
|
|
31
|
+
else return { ...out, error: `unexpected argument: ${a}` }
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function initCmd(args: readonly string[]): Promise<number> {
|
|
37
|
+
const parsed = parseInitArgs(args)
|
|
38
|
+
if (parsed.error) {
|
|
39
|
+
process.stderr.write(`vx init: ${parsed.error}\n`)
|
|
40
|
+
return 1
|
|
41
|
+
}
|
|
42
|
+
const root = await findWorkspaceRoot(process.cwd())
|
|
43
|
+
const metas = await listProjects(await loadWorkspace(root))
|
|
44
|
+
// `init` reads scripts only; a runner's own config beside them is the
|
|
45
|
+
// richer source (dependsOn, inputs, outputs) and was ignored without a
|
|
46
|
+
// word — the walkthrough on a Turbo repo (2026-09-09) got the scripts'
|
|
47
|
+
// TODOs and none of the edges turbo.json already declared.
|
|
48
|
+
const notes: string[] = []
|
|
49
|
+
if (await Bun.file(path.join(root, 'turbo.json')).exists()) {
|
|
50
|
+
notes.push(
|
|
51
|
+
'turbo.json found and not read — `bunx @vzn/vx-migrate` maps it (dependsOn, inputs, ' +
|
|
52
|
+
'outputs), or `plugins: [turbo()]` from @vzn/vx-migrate runs it with nothing written',
|
|
53
|
+
)
|
|
54
|
+
} else if (
|
|
55
|
+
(await Bun.file(path.join(root, 'nx.json')).exists()) ||
|
|
56
|
+
(await Bun.file(path.join(root, '.nx', 'workspace-data', 'project-graph.json')).exists())
|
|
57
|
+
) {
|
|
58
|
+
notes.push(
|
|
59
|
+
'an Nx workspace found and not read — `bunx @vzn/vx-migrate --from nx` maps its ' +
|
|
60
|
+
'exported project graph',
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
return applyMigration({
|
|
64
|
+
root,
|
|
65
|
+
metas,
|
|
66
|
+
plan: migrateScripts(metas),
|
|
67
|
+
source: 'package.json scripts',
|
|
68
|
+
verb: 'vx init',
|
|
69
|
+
dry: parsed.dry,
|
|
70
|
+
force: parsed.force,
|
|
71
|
+
init: true,
|
|
72
|
+
notes,
|
|
73
|
+
format: parsed.mjs ? 'mjs' : 'ts',
|
|
74
|
+
})
|
|
75
|
+
}
|
package/src/cli/last.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// `vx last [runId]` — replay a recorded run's summary from the terminal,
|
|
2
|
+
// without re-executing anything. Comparison gap #12: with the dashboard's
|
|
3
|
+
// run-detail page gone (the 2026-08-23 cloud removal), the local run
|
|
4
|
+
// history in cache.db is the only replay surface, and this verb reads it.
|
|
5
|
+
// Read-only — no config evaluation, no re-hash, no cache probe.
|
|
6
|
+
|
|
7
|
+
import { Cache, noteSchemaReset } from '../cache/index.js'
|
|
8
|
+
import { formatBytes } from './format.js'
|
|
9
|
+
import { seeHelp } from './help.js'
|
|
10
|
+
import { getInvocation, getRun, listInvocations } from '../orchestrator/index.js'
|
|
11
|
+
import { UserError } from '../util/index.js'
|
|
12
|
+
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
13
|
+
import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
|
|
14
|
+
|
|
15
|
+
interface LastArgs {
|
|
16
|
+
runId?: string
|
|
17
|
+
list?: number
|
|
18
|
+
format: 'pretty' | 'json'
|
|
19
|
+
/** `--cache-dir`: read the history a run with the same flag wrote. */
|
|
20
|
+
cacheDir?: string
|
|
21
|
+
error?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
25
|
+
const out: LastArgs = { format: 'pretty' }
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const a = args[i]!
|
|
28
|
+
if (a === '--list' || a.startsWith('--list=')) {
|
|
29
|
+
const lv = a === '--list' ? '10' : a.slice(7)
|
|
30
|
+
const n = Number(lv)
|
|
31
|
+
if (!Number.isInteger(n) || n < 1 || n > 500) {
|
|
32
|
+
return { ...out, error: `invalid --list: ${lv} (expected 1..500)` }
|
|
33
|
+
}
|
|
34
|
+
out.list = n
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
if (a === '--format' || a.startsWith('--format=')) {
|
|
38
|
+
const fv = a === '--format' ? args[++i] : a.slice(9)
|
|
39
|
+
if (fv !== 'pretty' && fv !== 'json') {
|
|
40
|
+
return { ...out, error: `invalid --format: ${fv ?? ''} (expected pretty | json)` }
|
|
41
|
+
}
|
|
42
|
+
out.format = fv
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
const cd = parseCacheDirFlag(args, i)
|
|
46
|
+
if (cd !== null) {
|
|
47
|
+
if ('error' in cd) return { ...out, error: cd.error }
|
|
48
|
+
out.cacheDir = cd.cacheDir
|
|
49
|
+
i = cd.next
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
if (a.startsWith('-')) return { ...out, error: `unknown flag: ${a}${seeHelp('last')}` }
|
|
53
|
+
if (out.runId !== undefined) return { ...out, error: `unexpected argument: ${a}` }
|
|
54
|
+
out.runId = a
|
|
55
|
+
}
|
|
56
|
+
return out
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fmtWhen = (ms: number): string => new Date(ms).toISOString()
|
|
60
|
+
|
|
61
|
+
function fmtMs(ms: number): string {
|
|
62
|
+
if (ms < 1000) return `${Math.round(ms)}ms`
|
|
63
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(2)}s`
|
|
64
|
+
const m = Math.floor(ms / 60_000)
|
|
65
|
+
return `${m}m ${Math.round((ms - m * 60_000) / 1000)}s`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What an executed task used — its peak RSS and its CPU parallelism (CPU
|
|
70
|
+
* time over wall time) — as the runner recorded them. This is the number
|
|
71
|
+
* `@vzn/vx-schedule-history` reserves from, so the replay shows it where
|
|
72
|
+
* a developer can read it; a hit spent nothing and shows nothing.
|
|
73
|
+
*/
|
|
74
|
+
function fmtUsage(t: {
|
|
75
|
+
cpuMs: number | null
|
|
76
|
+
peakRssBytes: number | null
|
|
77
|
+
durationMs: number
|
|
78
|
+
}): string {
|
|
79
|
+
const parts: string[] = []
|
|
80
|
+
if (t.peakRssBytes !== null) parts.push(formatBytes(t.peakRssBytes))
|
|
81
|
+
if (t.cpuMs !== null && t.durationMs > 0)
|
|
82
|
+
parts.push(`${(t.cpuMs / t.durationMs).toFixed(1)}× cpu`)
|
|
83
|
+
return parts.length > 0 ? ` ${parts.join(' · ')}` : ''
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
87
|
+
const parsed = parseLastArgs(args)
|
|
88
|
+
if (parsed.error !== undefined) throw new UserError(`vx last: ${parsed.error}`)
|
|
89
|
+
|
|
90
|
+
const root = await findWorkspaceRoot(process.cwd())
|
|
91
|
+
const cache = new Cache(await cliCacheDir(root, parsed.cacheDir))
|
|
92
|
+
noteSchemaReset(cache, warnToStderr)
|
|
93
|
+
try {
|
|
94
|
+
const db = cache.dbHandle()
|
|
95
|
+
|
|
96
|
+
if (parsed.list !== undefined) {
|
|
97
|
+
const invocations = listInvocations(db, { limit: parsed.list })
|
|
98
|
+
if (parsed.format === 'json') {
|
|
99
|
+
process.stdout.write(`${JSON.stringify(invocations)}\n`)
|
|
100
|
+
return 0
|
|
101
|
+
}
|
|
102
|
+
if (invocations.length === 0) {
|
|
103
|
+
process.stdout.write('no recorded runs\n')
|
|
104
|
+
return 0
|
|
105
|
+
}
|
|
106
|
+
for (const inv of invocations) {
|
|
107
|
+
const verdict = inv.exitOk ? 'ok ' : 'FAILED'
|
|
108
|
+
process.stdout.write(
|
|
109
|
+
`${verdict} ${fmtWhen(inv.startedAt)} ${inv.runId} ` +
|
|
110
|
+
`${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${inv.hitCount} hit${inv.hitCount === 1 ? '' : 's'}` +
|
|
111
|
+
`${inv.failedCount > 0 ? ` · ${inv.failedCount} failed` : ''} · ${fmtMs(inv.totalDurationMs)} $ ${inv.command}\n`,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const inv =
|
|
118
|
+
parsed.runId !== undefined
|
|
119
|
+
? getInvocation(db, parsed.runId)
|
|
120
|
+
: (listInvocations(db, { limit: 1 })[0] ?? null)
|
|
121
|
+
if (inv === null || inv === undefined) {
|
|
122
|
+
throw new UserError(
|
|
123
|
+
parsed.runId !== undefined
|
|
124
|
+
? `vx last: no recorded run ${parsed.runId} (vx last --list shows recent runs)`
|
|
125
|
+
: 'vx last: no recorded runs yet — run something first',
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
const detail = getRun(db, inv.runId)
|
|
129
|
+
|
|
130
|
+
if (parsed.format === 'json') {
|
|
131
|
+
process.stdout.write(`${JSON.stringify({ invocation: inv, tasks: detail?.tasks ?? [] })}\n`)
|
|
132
|
+
return 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const lines: string[] = []
|
|
136
|
+
lines.push(`run ${inv.runId} — ${inv.exitOk ? 'ok' : 'FAILED'}`)
|
|
137
|
+
lines.push(` $ ${inv.command}`)
|
|
138
|
+
lines.push(
|
|
139
|
+
` ${fmtWhen(inv.startedAt)} · ${fmtMs(inv.totalDurationMs)}` +
|
|
140
|
+
`${inv.branch !== null ? ` · ${inv.branch}` : ''}` +
|
|
141
|
+
`${inv.commitSha !== null ? ` @ ${inv.commitSha.slice(0, 8)}${inv.dirty === true ? '+dirty' : ''}` : ''}` +
|
|
142
|
+
`${inv.ci ? ` · CI${inv.ciProvider !== null ? ` (${inv.ciProvider})` : ''}` : ''}`,
|
|
143
|
+
)
|
|
144
|
+
lines.push(
|
|
145
|
+
` ${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${inv.hitCount} hit${inv.hitCount === 1 ? '' : 's'}` +
|
|
146
|
+
` (${inv.hitLocalCount} local, ${inv.hitRemoteCount} remote)` +
|
|
147
|
+
`${inv.failedCount > 0 ? ` · ${inv.failedCount} failed` : ''}`,
|
|
148
|
+
)
|
|
149
|
+
const tasks = detail?.tasks ?? []
|
|
150
|
+
if (tasks.length > 0) {
|
|
151
|
+
lines.push('')
|
|
152
|
+
const failedFirst = [
|
|
153
|
+
...tasks.filter((t) => t.status === 'failed'),
|
|
154
|
+
...tasks.filter((t) => t.status !== 'failed'),
|
|
155
|
+
]
|
|
156
|
+
const idW = Math.max(...tasks.map((t) => `${t.project}#${t.task}`.length), 4)
|
|
157
|
+
for (const t of failedFirst) {
|
|
158
|
+
const id = `${t.project}#${t.task}`
|
|
159
|
+
// The terminal summary's own word for a task that runs every time
|
|
160
|
+
// by design; a reader must not take its row for a miss.
|
|
161
|
+
lines.push(
|
|
162
|
+
` ${t.status.padEnd(17)} ${id.padEnd(idW)} ${fmtMs(t.durationMs).padStart(8)}` +
|
|
163
|
+
`${t.hash !== '' ? ` ${t.hash}` : ''}${t.cached === false ? ' no-cache' : ''}${fmtUsage(t)}`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
process.stdout.write(`${lines.join('\n')}\n`)
|
|
168
|
+
return 0
|
|
169
|
+
} finally {
|
|
170
|
+
cache.close()
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/cli/lock.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// current environment. Design: docs/design/config-lock-2026-06.md.
|
|
4
4
|
|
|
5
5
|
import type { ProjectConfig } from '../config.js'
|
|
6
|
+
import { seeHelp } from './help.js'
|
|
6
7
|
import { relPosix, xxh3hex } from '../util/index.js'
|
|
7
8
|
import {
|
|
8
9
|
findWorkspaceRoot,
|
|
@@ -27,7 +28,7 @@ export function parseLockArgs(args: readonly string[]): LockArgs {
|
|
|
27
28
|
const out: LockArgs = { check: false }
|
|
28
29
|
for (const a of args) {
|
|
29
30
|
if (a === '--check') out.check = true
|
|
30
|
-
else return { check: false, error: `unknown argument: ${a}` }
|
|
31
|
+
else return { check: false, error: `unknown argument: ${a}${seeHelp('lock')}` }
|
|
31
32
|
}
|
|
32
33
|
return out
|
|
33
34
|
}
|