@vzn/vx 0.0.18 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -70
  3. package/package.json +10 -20
  4. package/src/bin.ts +64 -5
  5. package/src/cache/archive.ts +279 -28
  6. package/src/cache/cache.ts +952 -1584
  7. package/src/cache/chained-cache.ts +14 -10
  8. package/src/cache/config-evals.ts +112 -0
  9. package/src/cache/file-hashes.ts +290 -0
  10. package/src/cache/git-inputs.ts +966 -0
  11. package/src/cache/index.ts +23 -7
  12. package/src/cache/inputs.ts +553 -739
  13. package/src/cache/key-fold.ts +232 -0
  14. package/src/cache/layer.ts +674 -0
  15. package/src/cache/layered-cache.ts +211 -49
  16. package/src/cache/output-index.ts +323 -0
  17. package/src/cache/policy.ts +84 -0
  18. package/src/cache/run-history.ts +152 -0
  19. package/src/cache/zstd.ts +183 -0
  20. package/src/cli/cache.ts +88 -30
  21. package/src/cli/completions.ts +104 -0
  22. package/src/cli/core-alias.ts +25 -0
  23. package/src/cli/format.ts +4 -15
  24. package/src/cli/help.ts +76 -38
  25. package/src/cli/index.ts +82 -17
  26. package/src/cli/info.ts +162 -91
  27. package/src/cli/init.ts +87 -0
  28. package/src/cli/last.ts +130 -23
  29. package/src/cli/lock.ts +36 -18
  30. package/src/cli/plan-format.ts +18 -7
  31. package/src/cli/plugin-commands.ts +34 -12
  32. package/src/cli/run.ts +138 -313
  33. package/src/cli/select.ts +342 -0
  34. package/src/cli/show.ts +151 -71
  35. package/src/cli/upgrade.ts +136 -15
  36. package/src/cli/watch.ts +1175 -116
  37. package/src/cli/why.ts +51 -29
  38. package/src/cli/workspace-config.ts +138 -0
  39. package/src/config.ts +183 -126
  40. package/src/exec/env.ts +11 -0
  41. package/src/exec/executor.ts +62 -31
  42. package/src/exec/index.ts +16 -2
  43. package/src/exec/kill-tree.ts +281 -0
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +361 -77
  46. package/src/exec/sandbox-binds.ts +211 -0
  47. package/src/exec/sandbox-paths.ts +70 -0
  48. package/src/exec/sandbox-runtime.ts +1152 -415
  49. package/src/exec/sandbox-violations.ts +293 -0
  50. package/src/graph/dependency-spec.ts +14 -14
  51. package/src/graph/index.ts +6 -4
  52. package/src/graph/priorities.ts +130 -0
  53. package/src/graph/scheduler.ts +332 -329
  54. package/src/graph/task-graph.ts +552 -188
  55. package/src/index.ts +111 -102
  56. package/src/orchestrator/admission.ts +161 -0
  57. package/src/orchestrator/colors.ts +8 -3
  58. package/src/orchestrator/deferred-outputs.ts +25 -7
  59. package/src/orchestrator/doctor.ts +363 -0
  60. package/src/orchestrator/events.ts +111 -8
  61. package/src/orchestrator/excluded-keys.ts +155 -0
  62. package/src/orchestrator/execute-task.ts +450 -565
  63. package/src/orchestrator/failure-mode.ts +169 -28
  64. package/src/orchestrator/failure-recap.ts +121 -0
  65. package/src/orchestrator/fingerprint-watch.ts +89 -0
  66. package/src/orchestrator/framed-output.ts +121 -49
  67. package/src/orchestrator/history.ts +123 -104
  68. package/src/orchestrator/hit-restore.ts +285 -0
  69. package/src/orchestrator/index.ts +42 -5
  70. package/src/orchestrator/keyed-projects.ts +89 -0
  71. package/src/orchestrator/local-shortcircuit.ts +83 -23
  72. package/src/orchestrator/lockfile-claim.ts +401 -0
  73. package/src/orchestrator/logger.ts +128 -41
  74. package/src/orchestrator/metrics.ts +201 -72
  75. package/src/orchestrator/miss-save.ts +232 -0
  76. package/src/orchestrator/options.ts +63 -34
  77. package/src/orchestrator/persistent.ts +99 -0
  78. package/src/orchestrator/placement.ts +212 -0
  79. package/src/orchestrator/plan.ts +24 -11
  80. package/src/orchestrator/plugin-host.ts +200 -49
  81. package/src/orchestrator/plugin.ts +190 -14
  82. package/src/orchestrator/prepare.ts +168 -137
  83. package/src/orchestrator/projects.ts +270 -0
  84. package/src/orchestrator/remote-prefetch.ts +22 -2
  85. package/src/orchestrator/run-artifacts.ts +34 -3
  86. package/src/orchestrator/run-context.ts +20 -5
  87. package/src/orchestrator/run-lock.ts +349 -0
  88. package/src/orchestrator/run-records.ts +132 -0
  89. package/src/orchestrator/run-report.ts +6 -4
  90. package/src/orchestrator/run.ts +467 -611
  91. package/src/orchestrator/sandbox-request.ts +569 -0
  92. package/src/orchestrator/save-lane.ts +44 -0
  93. package/src/orchestrator/shell-verdict.ts +117 -0
  94. package/src/orchestrator/signals.ts +167 -0
  95. package/src/orchestrator/stable-keys.ts +259 -10
  96. package/src/orchestrator/status-line.ts +35 -2
  97. package/src/orchestrator/summary.ts +115 -25
  98. package/src/orchestrator/task-hash.ts +103 -21
  99. package/src/orchestrator/task-log-buffer.ts +15 -18
  100. package/src/orchestrator/telemetry-host.ts +27 -8
  101. package/src/orchestrator/telemetry.ts +71 -44
  102. package/src/orchestrator/upstream.ts +91 -45
  103. package/src/util/bun-version.ts +43 -0
  104. package/src/util/cgroup.ts +160 -0
  105. package/src/util/edit-distance.ts +49 -0
  106. package/src/util/errors.ts +86 -0
  107. package/src/util/hash.ts +10 -1
  108. package/src/util/index.ts +42 -5
  109. package/src/util/paths.ts +168 -5
  110. package/src/util/procfs.ts +20 -0
  111. package/src/util/settle.ts +23 -2
  112. package/src/util/size.ts +33 -0
  113. package/src/util/task-id.ts +20 -0
  114. package/src/util/timing.ts +11 -1
  115. package/src/util/verbs.ts +39 -0
  116. package/src/util/which.ts +48 -0
  117. package/src/workspace/affected.ts +368 -60
  118. package/src/workspace/config-cache.ts +271 -32
  119. package/src/workspace/config-eval.ts +60 -16
  120. package/src/workspace/config-imports.ts +131 -9
  121. package/src/workspace/config-schema.ts +1033 -0
  122. package/src/workspace/filter.ts +69 -17
  123. package/src/workspace/fingerprint.ts +59 -7
  124. package/src/workspace/index.ts +37 -3
  125. package/src/workspace/json-data.ts +90 -0
  126. package/src/workspace/load-reads.ts +32 -0
  127. package/src/workspace/lockfile.ts +41 -14
  128. package/src/workspace/migrate-scripts.ts +187 -0
  129. package/src/workspace/migration.ts +353 -0
  130. package/src/workspace/package-graph.ts +239 -31
  131. package/src/workspace/project-loader.ts +295 -815
  132. package/src/workspace/workspace.ts +266 -31
  133. package/plugins/local-cache/index.ts +0 -2
  134. package/plugins/local-executor/index.ts +0 -2
  135. package/plugins/schedule-history/index.ts +0 -2
  136. package/src/cache/cas-backend.ts +0 -115
  137. package/src/cache/digest.ts +0 -32
  138. package/src/cli/migrate-emit.ts +0 -21
  139. package/src/cli/migrate-nx.ts +0 -473
  140. package/src/cli/migrate-scripts.ts +0 -127
  141. package/src/cli/migrate-turbo.ts +0 -401
  142. package/src/cli/migrate.ts +0 -305
  143. package/src/cli/prune.ts +0 -291
  144. package/src/orchestrator/missing-plugin.ts +0 -9
  145. package/src/orchestrator/resources.ts +0 -45
  146. package/src/orchestrator/verify.ts +0 -174
  147. package/src/plugins/local-cache/index.ts +0 -11
  148. package/src/plugins/local-executor/index.ts +0 -45
  149. package/src/plugins/schedule-history/index.ts +0 -109
package/src/cli/why.ts CHANGED
@@ -5,20 +5,25 @@
5
5
  // names the exact cache-key components that differ. Read-only over cache.db —
6
6
  // no config evaluation, no re-hash.
7
7
 
8
- import { Cache } from '../cache/index.js'
8
+ import { Cache, noteSchemaReset } from '../cache/index.js'
9
+ import { seeHelp } from './help.js'
9
10
  import { splitTaskId } from '../graph/index.js'
10
11
  import {
11
12
  cacheKeyDiff,
12
13
  explainCacheKeyQuery as explainCacheKey,
14
+ latestRunId,
13
15
  whyDidThisRerunQuery as whyDidThisRerun,
14
16
  } from '../orchestrator/index.js'
15
- import { UserError } from '../util/index.js'
16
- import { findWorkspaceRoot, loadWorkspaceConfig, resolveCacheDir } from '../workspace/index.js'
17
+ import { nearMatches, UserError } from '../util/index.js'
18
+ import { findWorkspaceRoot } from '../workspace/index.js'
19
+ import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
17
20
 
18
21
  interface WhyArgs {
19
22
  target?: string
20
23
  runId?: string
21
24
  format: 'pretty' | 'json'
25
+ /** `--cache-dir`: read the history a run with the same flag wrote. */
26
+ cacheDir?: string
22
27
  error?: string
23
28
  }
24
29
 
@@ -51,20 +56,37 @@ export function parseWhyArgs(args: readonly string[]): WhyArgs {
51
56
  out.format = fv
52
57
  continue
53
58
  }
54
- if (a.startsWith('-')) return { ...out, error: `unknown flag: ${a}` }
59
+ const cd = parseCacheDirFlag(args, i)
60
+ if (cd !== null) {
61
+ if ('error' in cd) return { ...out, error: cd.error }
62
+ out.cacheDir = cd.cacheDir
63
+ i = cd.next
64
+ continue
65
+ }
66
+ if (a.startsWith('-')) return { ...out, error: `unknown flag: ${a}${seeHelp('why')}` }
55
67
  if (out.target !== undefined) return { ...out, error: `unexpected argument: ${a}` }
56
68
  out.target = a
57
69
  }
58
70
  return out
59
71
  }
60
72
 
61
- /** Simple includes-match in both directions, case-insensitive. */
62
- function suggest(query: string, candidates: readonly string[]): string {
63
- const q = query.toLowerCase()
64
- const hits = candidates.filter((c) => {
65
- const n = c.toLowerCase()
66
- return n.includes(q) || q.includes(n)
67
- })
73
+ /**
74
+ * A bare query is a TASK name, so it is matched against the task half of
75
+ * every recorded id (the same rule `vx run` hints with) and the hint is
76
+ * the runnable `project#task`; an anchored query is matched whole.
77
+ */
78
+ function suggest(query: string, ids: readonly string[]): string {
79
+ let hits: string[]
80
+ if (query.includes('#')) {
81
+ hits = nearMatches(query, ids)
82
+ } else {
83
+ const byTask = new Map<string, string[]>()
84
+ for (const id of ids) {
85
+ const task = splitTaskId(id)[1]
86
+ byTask.set(task, [...(byTask.get(task) ?? []), id])
87
+ }
88
+ hits = nearMatches(query, byTask.keys()).flatMap((t) => byTask.get(t) ?? [])
89
+ }
68
90
  return hits.length > 0 ? ` — did you mean ${hits.slice(0, 3).join(', ')}?` : ''
69
91
  }
70
92
 
@@ -75,12 +97,21 @@ function suggest(query: string, candidates: readonly string[]): string {
75
97
  */
76
98
  function resolveTarget(cache: Cache, target: string): string {
77
99
  const db = cache.dbHandle()
100
+ if (target.includes('#')) {
101
+ // An exact id needs one row, and its newest is where a scan from the
102
+ // top of the table finds it first — the full DISTINCT walk below is for
103
+ // the error's suggestions and the bare-name form only.
104
+ const [project, task] = splitTaskId(target)
105
+ const hit = db
106
+ .query('SELECT 1 FROM runs WHERE project = ? AND task = ? ORDER BY id DESC LIMIT 1')
107
+ .get(project, task)
108
+ if (hit !== null) return target
109
+ }
78
110
  const pairs = db
79
111
  .query('SELECT DISTINCT project, task FROM runs ORDER BY project, task')
80
112
  .all() as Array<{ project: string; task: string }>
81
113
  const ids = pairs.map((p) => `${p.project}#${p.task}`)
82
114
  if (target.includes('#')) {
83
- if (ids.includes(target)) return target
84
115
  throw new UserError(`vx why: no recorded runs for "${target}"${suggest(target, ids)}`)
85
116
  }
86
117
  const matches = ids.filter((id) => id.endsWith(`#${target}`))
@@ -94,19 +125,6 @@ function resolveTarget(cache: Cache, target: string): string {
94
125
  throw new UserError(`vx why: no recorded runs for task "${target}"${suggest(target, ids)}`)
95
126
  }
96
127
 
97
- /** The latest recorded run of a task (run_id may be NULL on very old rows). */
98
- function latestRunId(cache: Cache, taskId: string): string | null {
99
- const [project, task] = splitTaskId(taskId)
100
- const row = cache
101
- .dbHandle()
102
- .query(
103
- `SELECT run_id AS runId FROM runs WHERE project = ? AND task = ?
104
- ORDER BY started_at DESC LIMIT 1`,
105
- )
106
- .get(project, task) as { runId: string | null } | undefined
107
- return row?.runId ?? null
108
- }
109
-
110
128
  const fmtWhen = (ms: number): string => new Date(ms).toISOString()
111
129
 
112
130
  export async function whyCmd(args: readonly string[]): Promise<number> {
@@ -117,12 +135,12 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
117
135
  }
118
136
 
119
137
  const root = await findWorkspaceRoot(process.cwd())
120
- const cacheDir = resolveCacheDir(root, await loadWorkspaceConfig(root))
121
- const cache = new Cache(cacheDir)
138
+ const cache = Cache.inspect(await cliCacheDir(root, parsed.cacheDir))
139
+ noteSchemaReset(cache, warnToStderr)
122
140
  try {
123
141
  const db = cache.dbHandle()
124
142
  const taskId = resolveTarget(cache, parsed.target)
125
- const runId = parsed.runId ?? latestRunId(cache, taskId)
143
+ const runId = parsed.runId ?? latestRunId(cache.dbHandle(), taskId)
126
144
 
127
145
  if (runId === null) {
128
146
  // Runs exist (resolveTarget passed) but predate run ids — fall back to
@@ -155,9 +173,13 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
155
173
  const lines: string[] = []
156
174
  const t = why.thisRun!
157
175
  lines.push(`${taskId} — run ${runId}`)
176
+ // A hit's status (`cache-hit`, `cache-hit-remote`) already names it and
177
+ // its tier; only an executed run needs the word. A skipped task never
178
+ // ran and has no key: it read `skipped · executed · key ` (item 898).
179
+ const ran = t.cacheHit === false && t.status !== 'skipped'
158
180
  lines.push(
159
181
  ` this run ${fmtWhen(t.startedAt)} · ${t.status}` +
160
- `${t.cacheHit === true ? ' · cache hit' : t.cacheHit === false ? ' · executed' : ''} · key ${t.hash}`,
182
+ `${ran ? ' · executed' : ''}${t.hash === '' ? ' · no key' : ` · key ${t.hash}`}`,
161
183
  )
162
184
  if (why.previousRun == null) {
163
185
  lines.push(' previous (none — first recorded run of this task)')
@@ -0,0 +1,138 @@
1
+ // The workspace as every verb must see it: the config with the plugin
2
+ // `config` stage applied, and the cache dir derived from THAT. The stage
3
+ // is documented to shape `cacheDir`; a verb that read the file raw opened
4
+ // a directory the run never used — `vx last` found no runs, `vx cache
5
+ // prune` pruned nothing, `vx watch` ignored the wrong path.
6
+
7
+ import { existsSync } from 'node:fs'
8
+ import path from 'node:path'
9
+ import type { WorkspaceConfig } from '../config.js'
10
+ import { UserError } from '../util/index.js'
11
+ import { Cache, noteSchemaReset } from '../cache/index.js'
12
+ import { loadProjects, loadWorkspacePlugins } from '../orchestrator/index.js'
13
+ import type { VxPlugin } from '../orchestrator/index.js'
14
+ import {
15
+ buildPackageGraph,
16
+ computeWorkspaceFingerprint,
17
+ FROZEN_WITHOUT_LOCK,
18
+ type ProjectEntry,
19
+ type ProjectMeta,
20
+ readLockfile,
21
+ resolveCacheDir,
22
+ } from '../workspace/index.js'
23
+
24
+ export interface CliWorkspace {
25
+ workspaceConfig: WorkspaceConfig | null
26
+ plugins: readonly VxPlugin[]
27
+ cacheDir: string
28
+ }
29
+
30
+ export const warnToStderr = (message: string): void => {
31
+ process.stderr.write(`${message}\n`)
32
+ }
33
+
34
+ export async function loadCliWorkspace(workspaceRoot: string): Promise<CliWorkspace> {
35
+ const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, warnToStderr)
36
+ return { workspaceConfig, plugins, cacheDir: resolveCacheDir(workspaceRoot, workspaceConfig) }
37
+ }
38
+
39
+ /**
40
+ * `--cache-dir <path>` / `--cache-dir=<path>` for a verb that reads the
41
+ * cache a run wrote — `why`, `last`, `info`, `cache prune`. Same rules as
42
+ * `vx run`'s: a value is required, and the space form refuses a value
43
+ * that looks like a flag (an unquoted empty shell variable would otherwise
44
+ * swallow the next flag as the path). Returns null when `args[i]` is not
45
+ * this flag; `next` is the index of the last argv slot consumed.
46
+ */
47
+ export function parseCacheDirFlag(
48
+ args: readonly string[],
49
+ i: number,
50
+ ): { cacheDir: string; next: number } | { error: string } | null {
51
+ const a = args[i]!
52
+ if (a !== '--cache-dir' && !a.startsWith('--cache-dir=')) return null
53
+ const v = a === '--cache-dir' ? args[i + 1] : a.slice('--cache-dir='.length)
54
+ if (v === undefined || v === '') return { error: '--cache-dir requires a path' }
55
+ if (a === '--cache-dir' && v.startsWith('-')) {
56
+ return { error: `--cache-dir requires a path, got flag: ${v}` }
57
+ }
58
+ return { cacheDir: v, next: a === '--cache-dir' ? i + 1 : i }
59
+ }
60
+
61
+ /**
62
+ * A reading verb's `--cache-dir`, resolved against the working directory.
63
+ * Named explicitly, a directory that is not there is a typo to report, not
64
+ * an empty cache to read (item 900).
65
+ */
66
+ export function namedCacheDir(override: string): string {
67
+ const dir = path.resolve(process.cwd(), override)
68
+ if (!existsSync(dir)) throw new UserError(`--cache-dir ${override}: no such directory (${dir})`)
69
+ return dir
70
+ }
71
+
72
+ /**
73
+ * The cache directory a reading verb opens: `--cache-dir` resolved against
74
+ * cwd exactly as `vx run` resolves it (prepare.ts), else the workspace's —
75
+ * so `vx run --cache-dir X` and `vx last --cache-dir X` read one history.
76
+ */
77
+ export async function cliCacheDir(
78
+ workspaceRoot: string,
79
+ override: string | undefined,
80
+ ): Promise<string> {
81
+ if (override !== undefined) return namedCacheDir(override)
82
+ return (await loadCliWorkspace(workspaceRoot)).cacheDir
83
+ }
84
+
85
+ /** The run flags a verb's staged load must honour to see what the run sees. */
86
+ export interface CliLoadOptions {
87
+ /** `--cache-dir`: where the cached evaluations live. */
88
+ cacheDir?: string
89
+ /** `--frozen`: configs come from `vx-lock.json`, not evaluation. */
90
+ frozen?: boolean
91
+ }
92
+
93
+ /**
94
+ * The run path's project-config load (`loadProjects`) for a verb that only
95
+ * reads: the plugin `project` stage applies, and the local cache opens only
96
+ * to serve cached evaluations — a pure config costs a stat, not an
97
+ * evaluation. `scope` is every project or a list of names; no closure, no
98
+ * lock (a verb reads live, as a default run does).
99
+ */
100
+ export async function loadCliProjects(
101
+ workspaceRoot: string,
102
+ metas: readonly ProjectMeta[],
103
+ scope: 'all' | readonly string[] = 'all',
104
+ opts: CliLoadOptions = {},
105
+ ): Promise<Map<string, ProjectEntry>> {
106
+ const ws = await loadCliWorkspace(workspaceRoot)
107
+ const { plugins } = ws
108
+ // The cached evaluations live where the run's do: a verb given
109
+ // `--cache-dir` must not open (and create) the workspace's default one.
110
+ const cacheDir = opts.cacheDir ?? ws.cacheDir
111
+ // A frozen run reads its configs from the lock; what it SELECTS must be
112
+ // decided from the same configs, or an env-dependent `workspaceFiles`
113
+ // glob could select live what the run then treats otherwise.
114
+ const lock = opts.frozen === true ? await readLockfile(workspaceRoot) : null
115
+ if (opts.frozen === true && lock === null) throw new UserError(FROZEN_WITHOUT_LOCK)
116
+ const cache = new Cache(cacheDir, { read: true, write: true }, workspaceRoot)
117
+ noteSchemaReset(cache, warnToStderr)
118
+ try {
119
+ const loaded = await loadProjects({
120
+ workspaceRoot,
121
+ cacheDir,
122
+ plugins,
123
+ projectMetas: metas,
124
+ packageGraph: buildPackageGraph([...metas]),
125
+ seeds: scope,
126
+ closure: false,
127
+ lock,
128
+ evalCache: {
129
+ store: cache,
130
+ workspaceFingerprint: await computeWorkspaceFingerprint(workspaceRoot),
131
+ },
132
+ warn: warnToStderr,
133
+ })
134
+ return loaded.projects
135
+ } finally {
136
+ cache.close()
137
+ }
138
+ }