@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.
Files changed (138) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +135 -124
  3. package/index.ts +10 -0
  4. package/launcher.mjs +10 -11
  5. package/package.json +11 -7
  6. package/src/bin.ts +14 -4
  7. package/src/cache/archive.ts +659 -0
  8. package/src/cache/cache.ts +769 -1101
  9. package/src/cache/chained-cache.ts +194 -0
  10. package/src/cache/config-evals.ts +89 -0
  11. package/src/cache/file-hashes.ts +262 -0
  12. package/src/cache/git-inputs.ts +637 -0
  13. package/src/cache/index.ts +24 -5
  14. package/src/cache/inputs.ts +373 -456
  15. package/src/cache/layer.ts +604 -0
  16. package/src/cache/layered-cache.ts +96 -43
  17. package/src/cache/output-index.ts +224 -0
  18. package/src/cache/policy.ts +84 -0
  19. package/src/cache/run-history.ts +147 -0
  20. package/src/cache/tar-stream.ts +358 -0
  21. package/src/cache/zstd.ts +160 -0
  22. package/src/cli/cache.ts +75 -21
  23. package/src/cli/completions.ts +104 -0
  24. package/src/cli/core-alias.ts +25 -0
  25. package/src/cli/help.ts +195 -128
  26. package/src/cli/index.ts +116 -46
  27. package/src/cli/info.ts +122 -63
  28. package/src/cli/init.ts +75 -0
  29. package/src/cli/last.ts +172 -0
  30. package/src/cli/lock.ts +2 -1
  31. package/src/cli/plan-format.ts +72 -6
  32. package/src/cli/plugin-commands.ts +103 -0
  33. package/src/cli/run.ts +245 -271
  34. package/src/cli/select.ts +332 -0
  35. package/src/cli/show.ts +144 -69
  36. package/src/cli/upgrade.ts +9 -7
  37. package/src/cli/watch.ts +858 -110
  38. package/src/cli/why.ts +223 -0
  39. package/src/cli/workspace-config.ts +126 -0
  40. package/src/config.ts +187 -150
  41. package/src/exec/env.ts +18 -1
  42. package/src/exec/executor.ts +224 -0
  43. package/src/exec/index.ts +24 -2
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +166 -40
  46. package/src/exec/sandbox-binds.ts +182 -0
  47. package/src/exec/sandbox-paths.ts +50 -0
  48. package/src/exec/sandbox-runtime.ts +642 -260
  49. package/src/exec/sandbox-violations.ts +280 -0
  50. package/src/graph/index.ts +4 -10
  51. package/src/graph/scheduler.ts +232 -203
  52. package/src/graph/task-graph.ts +222 -8
  53. package/src/index.ts +146 -153
  54. package/src/orchestrator/admission.ts +137 -0
  55. package/src/orchestrator/deferred-outputs.ts +147 -0
  56. package/src/orchestrator/doctor.ts +255 -0
  57. package/src/orchestrator/download-policy.ts +171 -0
  58. package/src/orchestrator/events.ts +72 -8
  59. package/src/orchestrator/execute-task.ts +374 -549
  60. package/src/orchestrator/failure-mode.ts +202 -0
  61. package/src/orchestrator/framed-output.ts +126 -69
  62. package/src/orchestrator/history.ts +126 -59
  63. package/src/orchestrator/hit-restore.ts +221 -0
  64. package/src/orchestrator/index.ts +77 -93
  65. package/src/orchestrator/local-shortcircuit.ts +22 -0
  66. package/src/orchestrator/lockfile-claim.ts +281 -0
  67. package/src/orchestrator/logger.ts +208 -35
  68. package/src/orchestrator/metrics.ts +102 -1388
  69. package/src/orchestrator/miss-save.ts +169 -0
  70. package/src/orchestrator/options.ts +55 -34
  71. package/src/orchestrator/persistent.ts +79 -0
  72. package/src/orchestrator/placement.ts +182 -0
  73. package/src/orchestrator/plan.ts +144 -1
  74. package/src/orchestrator/plugin-host.ts +357 -120
  75. package/src/orchestrator/plugin.ts +298 -58
  76. package/src/orchestrator/prepare.ts +202 -106
  77. package/src/orchestrator/projects.ts +259 -0
  78. package/src/orchestrator/remote-prefetch.ts +12 -8
  79. package/src/orchestrator/run-artifacts.ts +63 -16
  80. package/src/orchestrator/run-context.ts +167 -29
  81. package/src/orchestrator/run-records.ts +149 -0
  82. package/src/orchestrator/run-report.ts +74 -47
  83. package/src/orchestrator/run.ts +534 -417
  84. package/src/orchestrator/sandbox-request.ts +243 -0
  85. package/src/orchestrator/save-lane.ts +47 -0
  86. package/src/orchestrator/signals.ts +111 -0
  87. package/src/orchestrator/stable-keys.ts +129 -31
  88. package/src/orchestrator/status-line.ts +49 -9
  89. package/src/orchestrator/summary.ts +90 -26
  90. package/src/orchestrator/tally.ts +78 -35
  91. package/src/orchestrator/task-hash.ts +111 -15
  92. package/src/orchestrator/task-log-buffer.ts +288 -0
  93. package/src/orchestrator/telemetry-host.ts +34 -6
  94. package/src/orchestrator/telemetry.ts +182 -36
  95. package/src/orchestrator/upstream.ts +72 -31
  96. package/src/util/cgroup.ts +160 -0
  97. package/src/util/edit-distance.ts +70 -0
  98. package/src/util/errors.ts +15 -1
  99. package/src/util/index.ts +15 -2
  100. package/src/util/num.ts +37 -0
  101. package/src/util/paths.ts +64 -0
  102. package/src/util/settle.ts +67 -0
  103. package/src/util/size.ts +5 -1
  104. package/src/util/tail.ts +66 -0
  105. package/src/util/timing.ts +63 -0
  106. package/src/util/verbs.ts +39 -0
  107. package/src/workspace/affected.ts +250 -23
  108. package/src/workspace/config-cache.ts +313 -0
  109. package/src/workspace/config-eval.ts +217 -0
  110. package/src/workspace/config-imports.ts +162 -0
  111. package/src/workspace/config-schema.ts +837 -0
  112. package/src/workspace/filter.ts +65 -10
  113. package/src/workspace/fingerprint.ts +53 -4
  114. package/src/workspace/index.ts +42 -3
  115. package/src/workspace/lockfile.ts +25 -9
  116. package/src/workspace/migrate-scripts.ts +134 -0
  117. package/src/workspace/migration.ts +314 -0
  118. package/src/workspace/nested-dirs.ts +21 -7
  119. package/src/workspace/package-graph.ts +114 -41
  120. package/src/workspace/project-loader.ts +224 -450
  121. package/src/workspace/workspace.ts +298 -72
  122. package/src/cache/cas-backend.ts +0 -97
  123. package/src/cache/digest.ts +0 -47
  124. package/src/cache/tar.ts +0 -343
  125. package/src/cli/backend.ts +0 -23
  126. package/src/cli/mcp-rpc.ts +0 -292
  127. package/src/cli/mcp.ts +0 -92
  128. package/src/cli/migrate-nx.ts +0 -450
  129. package/src/cli/migrate-turbo.ts +0 -364
  130. package/src/cli/migrate.ts +0 -219
  131. package/src/orchestrator/devframe-surface.ts +0 -74
  132. package/src/orchestrator/predict.ts +0 -106
  133. package/src/orchestrator/protocol.ts +0 -156
  134. package/src/orchestrator/resources.ts +0 -59
  135. package/src/orchestrator/run-state.ts +0 -145
  136. package/src/orchestrator/verify.ts +0 -164
  137. package/src/orchestrator/wire-render.ts +0 -92
  138. package/src/orchestrator/wire.ts +0 -212
@@ -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
- lines.push(` ${sym} ${t.node.id.padEnd(idWidth)} ${desc.padEnd(tagWidth)} ${shortHash}`)
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(` "${t.node.id}" [label="${label}", style="filled", fillcolor="${color}"];`)
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(` "${dep}" -> "${t.node.id}";`)
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
+ }