@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/plan-format.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
// Formatters for `--dry-run` (human / JSON) and `--graph` (DOT).
|
|
2
2
|
|
|
3
3
|
import type { CacheStatus, RunPlan } from '../orchestrator/index.js'
|
|
4
|
+
import { formatDuration } from '../orchestrator/index.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Human-readable preview. One line per real task (groups hidden, same
|
|
7
|
-
* as the live runner), tagged with predicted cache outcome.
|
|
8
|
+
* as the live runner), tagged with predicted cache outcome. A would-run
|
|
9
|
+
* task with recorded history also shows its typical duration (~p50).
|
|
8
10
|
*
|
|
9
11
|
* would run:
|
|
10
12
|
* ✓ @vzn/vx#lint cache hit (local) abc12345
|
|
11
13
|
* ↓ @vzn/vx#test cache hit (remote) def67890
|
|
12
|
-
* ▶ @vzn/vx#build cache miss — would exec fedcba98
|
|
14
|
+
* ▶ @vzn/vx#build cache miss — would exec fedcba98 ~1.2s
|
|
13
15
|
*
|
|
14
16
|
* 3 task(s) planned: 2 cache hits (1 local, 1 remote), 1 would run.
|
|
17
|
+
* predicted: ~1.2s wall · ~1.2s total execution
|
|
15
18
|
*/
|
|
16
19
|
export function formatPlanText(plan: RunPlan): string {
|
|
17
20
|
const real = plan.tasks.filter((t) => t.cacheStatus !== 'group')
|
|
@@ -29,7 +32,16 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
29
32
|
const sym = symbolFor(t.cacheStatus)
|
|
30
33
|
const desc = describe(t.cacheStatus)
|
|
31
34
|
const shortHash = t.hash ? t.hash.slice(0, 8) : ''
|
|
32
|
-
|
|
35
|
+
const executes = t.cacheStatus === 'miss' || t.cacheStatus === 'no-cache'
|
|
36
|
+
const eta = executes && t.p50Ms !== undefined ? ` ~${formatDuration(t.p50Ms)}` : ''
|
|
37
|
+
// Placement, by EXECUTOR NAME rather than a local/remote word: the
|
|
38
|
+
// summary line below already spends "local" and "remote" on the cache
|
|
39
|
+
// tier, and a task placed on a named executor is what the reader can
|
|
40
|
+
// act on. Present only when the workspace declared a choice.
|
|
41
|
+
const where = t.executor !== undefined ? ` @${t.executor}` : ''
|
|
42
|
+
lines.push(
|
|
43
|
+
` ${sym} ${t.node.id.padEnd(idWidth)} ${desc.padEnd(tagWidth)} ${shortHash}${eta}${where}`,
|
|
44
|
+
)
|
|
33
45
|
// Optional one-line description from the task config, indented
|
|
34
46
|
// under the id so the eye picks up the task → blurb mapping.
|
|
35
47
|
const taskDesc = t.node.config.description
|
|
@@ -52,6 +64,35 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
52
64
|
if (nocache > 0) summary.push(`${nocache} no-cache`)
|
|
53
65
|
lines.push('')
|
|
54
66
|
lines.push(summary.join(', ') + '.')
|
|
67
|
+
|
|
68
|
+
// `--download` is invisible without this: the eligibility gate silently
|
|
69
|
+
// keeps producers eager, so a user who asked for `none` and got no
|
|
70
|
+
// deferral has nothing to read. Say what WOULD stay remote, and name the
|
|
71
|
+
// refusals when nothing would.
|
|
72
|
+
const deferred = real.filter((t) => t.download === 'deferred')
|
|
73
|
+
const downgrades = plan.downloadDowngrades ?? []
|
|
74
|
+
if (deferred.length > 0 || downgrades.length > 0) {
|
|
75
|
+
lines.push('')
|
|
76
|
+
lines.push(
|
|
77
|
+
`download: ${deferred.length} task(s) would keep outputs remote` +
|
|
78
|
+
(downgrades.length > 0 ? `, ${downgrades.length} kept eager:` : '.'),
|
|
79
|
+
)
|
|
80
|
+
for (const d of downgrades.slice(0, 3)) lines.push(` ${d.taskId} — ${d.reason}`)
|
|
81
|
+
if (downgrades.length > 3) lines.push(` …and ${downgrades.length - 3} more`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Time prediction — only when history gave us something to say: at least
|
|
85
|
+
// one would-run task has a p50 (an all-unknown prediction is pure noise).
|
|
86
|
+
const p = plan.predicted
|
|
87
|
+
const executes = miss + nocache
|
|
88
|
+
if (p !== undefined && executes > 0 && executes > p.unknownCount) {
|
|
89
|
+
const parts = [`predicted: ~${formatDuration(p.wallMs)} wall`]
|
|
90
|
+
parts.push(`~${formatDuration(p.workMs)} total execution`)
|
|
91
|
+
if (p.unknownCount > 0) {
|
|
92
|
+
parts.push(`${p.unknownCount} task${p.unknownCount === 1 ? '' : 's'} without history (+?)`)
|
|
93
|
+
}
|
|
94
|
+
lines.push(parts.join(' · '))
|
|
95
|
+
}
|
|
55
96
|
return lines.join('\n') + '\n'
|
|
56
97
|
}
|
|
57
98
|
|
|
@@ -66,10 +107,24 @@ export function formatPlanJson(plan: RunPlan): string {
|
|
|
66
107
|
hash: t.hash,
|
|
67
108
|
cacheStatus: t.cacheStatus,
|
|
68
109
|
deps: t.deps,
|
|
110
|
+
...(t.p50Ms !== undefined ? { p50Ms: t.p50Ms } : {}),
|
|
111
|
+
...(t.executor !== undefined ? { executor: t.executor } : {}),
|
|
112
|
+
...(t.download !== undefined ? { download: t.download } : {}),
|
|
113
|
+
|
|
69
114
|
...(t.node.config.description !== undefined
|
|
70
115
|
? { description: t.node.config.description }
|
|
71
116
|
: {}),
|
|
72
117
|
})),
|
|
118
|
+
...(plan.predicted !== undefined ? { predicted: plan.predicted } : {}),
|
|
119
|
+
// The gate's refusals belong on the SCRIPTING surface too: a CI job
|
|
120
|
+
// asking "did --download=none actually defer anything, and if not
|
|
121
|
+
// why" reads this, not the human table. (This object enumerates its
|
|
122
|
+
// fields deliberately — the plan's internal shape is not the wire —
|
|
123
|
+
// which is exactly why a new PlannedTask field does not appear here
|
|
124
|
+
// for free, and why `download` above had to be added by hand.)
|
|
125
|
+
...(plan.downloadDowngrades !== undefined
|
|
126
|
+
? { downloadDowngrades: plan.downloadDowngrades }
|
|
127
|
+
: {}),
|
|
73
128
|
},
|
|
74
129
|
null,
|
|
75
130
|
2,
|
|
@@ -89,19 +144,30 @@ export function formatPlanJson(plan: RunPlan): string {
|
|
|
89
144
|
export function formatGraphDot(plan: RunPlan): string {
|
|
90
145
|
const lines: string[] = ['digraph TaskGraph {', ' rankdir=LR;', ' node [shape=box];']
|
|
91
146
|
for (const t of plan.tasks) {
|
|
92
|
-
const label = `${t.node.id}\\n${t.hash.slice(0, 8)}`
|
|
93
147
|
const color = dotColor(t.cacheStatus)
|
|
94
|
-
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
|
+
)
|
|
95
151
|
}
|
|
96
152
|
for (const t of plan.tasks) {
|
|
97
153
|
for (const dep of t.deps) {
|
|
98
|
-
lines.push(`
|
|
154
|
+
lines.push(` ${dotString(dep)} -> ${dotString(t.node.id)};`)
|
|
99
155
|
}
|
|
100
156
|
}
|
|
101
157
|
lines.push('}')
|
|
102
158
|
return lines.join('\n') + '\n'
|
|
103
159
|
}
|
|
104
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
|
+
|
|
105
171
|
function symbolFor(s: CacheStatus): string {
|
|
106
172
|
switch (s) {
|
|
107
173
|
case 'hit-local':
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Plugin-contributed CLI verbs (`VxPlugin.commands`).
|
|
2
|
+
//
|
|
3
|
+
// Consulted only when the dispatcher has no core verb for the word: the
|
|
4
|
+
// workspace config is loaded from the cwd, and the first plugin (in
|
|
5
|
+
// declaration order) declaring the verb runs it. Outside a workspace, or
|
|
6
|
+
// with no plugin declaring it, the answer is null and the dispatcher
|
|
7
|
+
// reports an unknown command as it always did. Core's verbs are matched
|
|
8
|
+
// before this is asked, so a plugin cannot shadow `run`.
|
|
9
|
+
|
|
10
|
+
import type { VxPlugin, PluginCommand, CommandContext } from '../orchestrator/index.js'
|
|
11
|
+
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
12
|
+
import { loadCliWorkspace } from './workspace-config.js'
|
|
13
|
+
|
|
14
|
+
export interface ResolvedPluginCommand {
|
|
15
|
+
plugin: VxPlugin
|
|
16
|
+
command: PluginCommand
|
|
17
|
+
ctx: CommandContext
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Why a verb could not be looked up: the workspace file failed to load.
|
|
22
|
+
* Surfaced next to "unknown command" rather than as the whole answer — a
|
|
23
|
+
* typo'd verb must still read as a typo, and a real plugin verb must still
|
|
24
|
+
* point at the file that broke it.
|
|
25
|
+
*/
|
|
26
|
+
export interface UnresolvedPluginCommand {
|
|
27
|
+
loadError: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function workspacePlugins(
|
|
31
|
+
cwd: string,
|
|
32
|
+
): Promise<
|
|
33
|
+
| { workspaceRoot: string; cacheDir: string; plugins: readonly VxPlugin[] }
|
|
34
|
+
| UnresolvedPluginCommand
|
|
35
|
+
| null
|
|
36
|
+
> {
|
|
37
|
+
let workspaceRoot: string
|
|
38
|
+
try {
|
|
39
|
+
workspaceRoot = await findWorkspaceRoot(cwd)
|
|
40
|
+
} catch {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
let ws
|
|
44
|
+
try {
|
|
45
|
+
ws = await loadCliWorkspace(workspaceRoot)
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { loadError: err instanceof Error ? err.message : String(err) }
|
|
48
|
+
}
|
|
49
|
+
return { workspaceRoot, cacheDir: ws.cacheDir, plugins: ws.plugins }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The plugin verb named `verb` for the workspace around `cwd`; null when
|
|
54
|
+
* there is none; the load error when the workspace file could not say.
|
|
55
|
+
*/
|
|
56
|
+
export async function resolvePluginCommand(
|
|
57
|
+
verb: string,
|
|
58
|
+
cwd = process.cwd(),
|
|
59
|
+
): Promise<ResolvedPluginCommand | UnresolvedPluginCommand | null> {
|
|
60
|
+
const ws = await workspacePlugins(cwd)
|
|
61
|
+
if (ws === null) return null
|
|
62
|
+
if ('loadError' in ws) return ws
|
|
63
|
+
for (const plugin of ws.plugins) {
|
|
64
|
+
const command = plugin.commands?.[verb]
|
|
65
|
+
if (command === undefined) continue
|
|
66
|
+
return {
|
|
67
|
+
plugin,
|
|
68
|
+
command,
|
|
69
|
+
ctx: {
|
|
70
|
+
workspaceRoot: ws.workspaceRoot,
|
|
71
|
+
cacheDir: ws.cacheDir,
|
|
72
|
+
warn: (m) => process.stderr.write(`${m}\n`),
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
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
|
+
|
|
90
|
+
export async function pluginCommandHelp(cwd = process.cwd()): Promise<string[]> {
|
|
91
|
+
const ws = await workspacePlugins(cwd).catch(() => null)
|
|
92
|
+
if (ws === null || 'loadError' in ws) return []
|
|
93
|
+
const lines: string[] = []
|
|
94
|
+
const seen = new Set<string>()
|
|
95
|
+
for (const plugin of ws.plugins) {
|
|
96
|
+
for (const [verb, command] of Object.entries(plugin.commands ?? {})) {
|
|
97
|
+
if (seen.has(verb)) continue
|
|
98
|
+
seen.add(verb)
|
|
99
|
+
lines.push(` vx ${verb.padEnd(17)} ${command.description} (${plugin.name})`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return lines
|
|
103
|
+
}
|