@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/info.ts
CHANGED
|
@@ -1,110 +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
|
-
['
|
|
62
|
-
['
|
|
63
|
-
['cache
|
|
64
|
-
['cache entries', `${
|
|
65
|
-
|
|
66
|
-
|
|
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'],
|
|
67
85
|
]
|
|
68
86
|
const labelW = Math.max(...rows.map(([label]) => label.length))
|
|
69
|
-
|
|
70
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
71
|
-
return 0
|
|
87
|
+
return rows.map(([label, value]) => `${`${label}:`.padEnd(labelW + 1)} ${value}`).join('\n')
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
/**
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
stderr: 'pipe',
|
|
82
|
-
})
|
|
83
|
-
const out = p.exitCode === 0 ? new TextDecoder().decode(p.stdout) : ''
|
|
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
|
-
}
|
|
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
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return new TextDecoder()
|
|
104
|
-
.decode(p.stdout)
|
|
105
|
-
.trim()
|
|
106
|
-
.replace(/^git version /, '')
|
|
107
|
-
} catch {
|
|
108
|
-
return '(not found)'
|
|
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)}`
|
|
109
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`
|
|
110
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
CHANGED
|
@@ -4,15 +4,20 @@
|
|
|
4
4
|
// history in cache.db is the only replay surface, and this verb reads it.
|
|
5
5
|
// Read-only — no config evaluation, no re-hash, no cache probe.
|
|
6
6
|
|
|
7
|
-
import { Cache } from '../cache/index.js'
|
|
7
|
+
import { Cache, noteSchemaReset } from '../cache/index.js'
|
|
8
|
+
import { formatBytes } from './format.js'
|
|
9
|
+
import { seeHelp } from './help.js'
|
|
8
10
|
import { getInvocation, getRun, listInvocations } from '../orchestrator/index.js'
|
|
9
11
|
import { UserError } from '../util/index.js'
|
|
10
|
-
import { findWorkspaceRoot
|
|
12
|
+
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
13
|
+
import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
|
|
11
14
|
|
|
12
15
|
interface LastArgs {
|
|
13
16
|
runId?: string
|
|
14
17
|
list?: number
|
|
15
18
|
format: 'pretty' | 'json'
|
|
19
|
+
/** `--cache-dir`: read the history a run with the same flag wrote. */
|
|
20
|
+
cacheDir?: string
|
|
16
21
|
error?: string
|
|
17
22
|
}
|
|
18
23
|
|
|
@@ -37,7 +42,14 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
37
42
|
out.format = fv
|
|
38
43
|
continue
|
|
39
44
|
}
|
|
40
|
-
|
|
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')}` }
|
|
41
53
|
if (out.runId !== undefined) return { ...out, error: `unexpected argument: ${a}` }
|
|
42
54
|
out.runId = a
|
|
43
55
|
}
|
|
@@ -53,13 +65,31 @@ function fmtMs(ms: number): string {
|
|
|
53
65
|
return `${m}m ${Math.round((ms - m * 60_000) / 1000)}s`
|
|
54
66
|
}
|
|
55
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
|
+
|
|
56
86
|
export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
57
87
|
const parsed = parseLastArgs(args)
|
|
58
88
|
if (parsed.error !== undefined) throw new UserError(`vx last: ${parsed.error}`)
|
|
59
89
|
|
|
60
90
|
const root = await findWorkspaceRoot(process.cwd())
|
|
61
|
-
const
|
|
62
|
-
|
|
91
|
+
const cache = new Cache(await cliCacheDir(root, parsed.cacheDir))
|
|
92
|
+
noteSchemaReset(cache, warnToStderr)
|
|
63
93
|
try {
|
|
64
94
|
const db = cache.dbHandle()
|
|
65
95
|
|
|
@@ -126,9 +156,11 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
126
156
|
const idW = Math.max(...tasks.map((t) => `${t.project}#${t.task}`.length), 4)
|
|
127
157
|
for (const t of failedFirst) {
|
|
128
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.
|
|
129
161
|
lines.push(
|
|
130
162
|
` ${t.status.padEnd(17)} ${id.padEnd(idW)} ${fmtMs(t.durationMs).padStart(8)}` +
|
|
131
|
-
`${t.hash !== '' ? ` ${t.hash}` : ''}`,
|
|
163
|
+
`${t.hash !== '' ? ` ${t.hash}` : ''}${t.cached === false ? ' no-cache' : ''}${fmtUsage(t)}`,
|
|
132
164
|
)
|
|
133
165
|
}
|
|
134
166
|
}
|
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
|
}
|
package/src/cli/plan-format.ts
CHANGED
|
@@ -144,19 +144,30 @@ export function formatPlanJson(plan: RunPlan): string {
|
|
|
144
144
|
export function formatGraphDot(plan: RunPlan): string {
|
|
145
145
|
const lines: string[] = ['digraph TaskGraph {', ' rankdir=LR;', ' node [shape=box];']
|
|
146
146
|
for (const t of plan.tasks) {
|
|
147
|
-
const label = `${t.node.id}\\n${t.hash.slice(0, 8)}`
|
|
148
147
|
const color = dotColor(t.cacheStatus)
|
|
149
|
-
lines.push(
|
|
148
|
+
lines.push(
|
|
149
|
+
` ${dotString(t.node.id)} [label=${dotString(`${t.node.id}\n${t.hash.slice(0, 8)}`)}, style="filled", fillcolor="${color}"];`,
|
|
150
|
+
)
|
|
150
151
|
}
|
|
151
152
|
for (const t of plan.tasks) {
|
|
152
153
|
for (const dep of t.deps) {
|
|
153
|
-
lines.push(`
|
|
154
|
+
lines.push(` ${dotString(dep)} -> ${dotString(t.node.id)};`)
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
lines.push('}')
|
|
157
158
|
return lines.join('\n') + '\n'
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* A DOT double-quoted ID. A task name is any object key in a config, so
|
|
163
|
+
* `"`, `\` and a newline are all reachable; unescaped they end the
|
|
164
|
+
* string early and the document stops parsing. `\n` is DOT's own line
|
|
165
|
+
* break inside a label.
|
|
166
|
+
*/
|
|
167
|
+
function dotString(s: string): string {
|
|
168
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, '\\n')}"`
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
function symbolFor(s: CacheStatus): string {
|
|
161
172
|
switch (s) {
|
|
162
173
|
case 'hit-local':
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// before this is asked, so a plugin cannot shadow `run`.
|
|
9
9
|
|
|
10
10
|
import type { VxPlugin, PluginCommand, CommandContext } from '../orchestrator/index.js'
|
|
11
|
-
import { findWorkspaceRoot
|
|
11
|
+
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
12
|
+
import { loadCliWorkspace } from './workspace-config.js'
|
|
12
13
|
|
|
13
14
|
export interface ResolvedPluginCommand {
|
|
14
15
|
plugin: VxPlugin
|
|
@@ -39,17 +40,13 @@ async function workspacePlugins(
|
|
|
39
40
|
} catch {
|
|
40
41
|
return null
|
|
41
42
|
}
|
|
42
|
-
let
|
|
43
|
+
let ws
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
+
ws = await loadCliWorkspace(workspaceRoot)
|
|
45
46
|
} catch (err) {
|
|
46
47
|
return { loadError: err instanceof Error ? err.message : String(err) }
|
|
47
48
|
}
|
|
48
|
-
return {
|
|
49
|
-
workspaceRoot,
|
|
50
|
-
cacheDir: resolveCacheDir(workspaceRoot, config),
|
|
51
|
-
plugins: (config?.plugins ?? []) as readonly VxPlugin[],
|
|
52
|
-
}
|
|
49
|
+
return { workspaceRoot, cacheDir: ws.cacheDir, plugins: ws.plugins }
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
/**
|
|
@@ -80,6 +77,16 @@ export async function resolvePluginCommand(
|
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
/** `vx help` lines for every plugin verb in the workspace around `cwd`. */
|
|
80
|
+
/** The plugin verbs the workspace around `cwd` declares, in declaration order; none when it does not load. */
|
|
81
|
+
export async function pluginVerbs(cwd = process.cwd()): Promise<string[]> {
|
|
82
|
+
const ws = await workspacePlugins(cwd).catch(() => null)
|
|
83
|
+
if (ws === null || 'loadError' in ws) return []
|
|
84
|
+
const verbs = new Set<string>()
|
|
85
|
+
for (const plugin of ws.plugins)
|
|
86
|
+
for (const verb of Object.keys(plugin.commands ?? {})) verbs.add(verb)
|
|
87
|
+
return [...verbs]
|
|
88
|
+
}
|
|
89
|
+
|
|
83
90
|
export async function pluginCommandHelp(cwd = process.cwd()): Promise<string[]> {
|
|
84
91
|
const ws = await workspacePlugins(cwd).catch(() => null)
|
|
85
92
|
if (ws === null || 'loadError' in ws) return []
|