@vzn/vx 0.0.97 → 0.0.99

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/README.md CHANGED
@@ -93,7 +93,8 @@ npx vx run build --all # restored from the cache, not rebuilt
93
93
  Or copy [`examples/basic`](examples/basic), a two-package starter CI
94
94
  runs on every commit. More: [Quickstart](https://vznjs.github.io/vx/quickstart/) ·
95
95
  [Configure](https://vznjs.github.io/vx/guides/configure/) ·
96
- [CLI](packages/vx/docs/cli.md) · [every config field](packages/vx/docs/schema.md).
96
+ [CLI](packages/vx/docs/cli.md) · [every config field](packages/vx/docs/schema.md) ·
97
+ [playground](https://vznjs.github.io/vx/playground/) (the planner, in your browser).
97
98
 
98
99
  ## Already on Turborepo or Nx?
99
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.97",
3
+ "version": "0.0.99",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,10 +17,10 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "optionalDependencies": {
20
- "@vzn/vx-linux-x64": "0.0.97",
21
- "@vzn/vx-linux-arm64": "0.0.97",
22
- "@vzn/vx-darwin-x64": "0.0.97",
23
- "@vzn/vx-darwin-arm64": "0.0.97"
20
+ "@vzn/vx-linux-x64": "0.0.99",
21
+ "@vzn/vx-linux-arm64": "0.0.99",
22
+ "@vzn/vx-darwin-x64": "0.0.99",
23
+ "@vzn/vx-darwin-arm64": "0.0.99"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/help.ts CHANGED
@@ -141,7 +141,6 @@ export function helpText(pluginCommands: readonly string[] = []): string {
141
141
  ' task counts, cache dir/entries/size, recent runs,',
142
142
  ' lock status.',
143
143
  ' --format <fmt> pretty (default) | json.',
144
- ' vx stats Deprecated: alias of vx info.',
145
144
  " vx why <task> Why did this task re-run? Compares the task's latest",
146
145
  ' run (or --run <id>) against its previous run: names the',
147
146
  ' exact changed cache-key components (files / env / runtime',
package/src/cli/index.ts CHANGED
@@ -61,9 +61,6 @@ export async function run(argv: readonly string[]): Promise<number> {
61
61
  return await (await import('./upgrade.js')).upgradeCmd(rest)
62
62
  case 'show':
63
63
  return await (await import('./show.js')).showCmd(rest)
64
- case 'stats': // deprecated alias — `vx info` absorbed `vx stats`
65
- process.stderr.write('vx stats is deprecated; use vx info\n')
66
- return await (await import('./info.js')).infoCmd(rest)
67
64
  case 'info':
68
65
  return await (await import('./info.js')).infoCmd(rest)
69
66
  case 'why':
package/src/cli/info.ts CHANGED
@@ -1,6 +1,6 @@
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). The facts come
3
+ // The facts come
4
4
  // from the orchestrator's `collectInfo` (doctor.ts), which `vx mcp` reads
5
5
  // too; this file parses the flags and renders the rows.
6
6
 
package/src/cli/last.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  import { formatElapsed, UserError } from '../util/index.js'
18
18
  import { findWorkspaceRoot } from '../workspace/index.js'
19
19
  import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
20
+ import { resolveRunId } from './run-id.js'
20
21
 
21
22
  interface LastArgs {
22
23
  runId?: string
@@ -204,7 +205,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
204
205
 
205
206
  const inv =
206
207
  parsed.runId !== undefined
207
- ? getInvocation(db, parsed.runId)
208
+ ? getInvocation(db, resolveRunId(db, parsed.runId, 'vx last') ?? parsed.runId)
208
209
  : (listInvocations(db, { limit: 1 })[0] ?? null)
209
210
  if (inv === null || inv === undefined) {
210
211
  throw new UserError(
@@ -0,0 +1,27 @@
1
+ // A run id as a user types it: the whole id, or a prefix of one, as git
2
+ // takes a short sha. `vx last --list` prints 36-character ids; nobody
3
+ // retypes one whole.
4
+
5
+ import type { Database } from 'bun:sqlite'
6
+ import { UserError } from '../util/index.js'
7
+
8
+ /**
9
+ * The recorded run `raw` names: itself when recorded, else the one run
10
+ * whose id starts with it. Null when none does; a prefix several runs share
11
+ * is refused with those runs, since picking one would replay the wrong run.
12
+ */
13
+ export function resolveRunId(db: Database, raw: string, verb: string): string | null {
14
+ const exact = db.query('SELECT 1 FROM invocations WHERE run_id = ?').get(raw)
15
+ if (exact !== null) return raw
16
+ const rows = db
17
+ .query(
18
+ 'SELECT run_id FROM invocations WHERE substr(run_id, 1, ?) = ? ORDER BY run_id DESC LIMIT 6',
19
+ )
20
+ .all(raw.length, raw) as Array<{ run_id: string }>
21
+ if (rows.length === 0) return null
22
+ if (rows.length === 1) return rows[0]!.run_id
23
+ const shown = rows.slice(0, 5).map((r) => ` ${r.run_id}`)
24
+ throw new UserError(
25
+ `${verb}: run id ${raw} is the start of ${rows.length > 5 ? 'more than 5' : rows.length} runs — type more of it:\n${shown.join('\n')}`,
26
+ )
27
+ }
package/src/cli/run.ts CHANGED
@@ -547,7 +547,9 @@ export async function runCmd(args: readonly string[]): Promise<number> {
547
547
  // explicit anchored positional.
548
548
  if (tasks.length === 0) {
549
549
  if (!process.stdin.isTTY) {
550
- process.stderr.write(`vx run: missing task name (stdin is not a TTY)\n`)
550
+ process.stderr.write(
551
+ `vx run: missing task name (stdin is not a TTY, so no picker; vx run <task>, e.g. vx run build)${seeHelp('run')}\n`,
552
+ )
551
553
  return 1
552
554
  }
553
555
  const picked = await pickTask(
package/src/cli/select.ts CHANGED
@@ -305,7 +305,10 @@ export async function resolveFilters(
305
305
  }
306
306
  // Something matched, so the run proceeds; a pattern that matched nothing
307
307
  // alongside it is still worth a line — it is probably a typo.
308
- for (const f of unmatched) process.stderr.write(`vx: filter "${f}" matched no projects\n`)
308
+ for (const f of unmatched)
309
+ process.stderr.write(
310
+ `vx: filter "${f}" matched no projects${didYouMeanProject([f], projects)}\n`,
311
+ )
309
312
  let staged: Map<string, ProjectEntry> | undefined
310
313
  if (stagedPromise !== undefined) {
311
314
  try {
@@ -398,7 +401,9 @@ export async function pickTask(
398
401
  }
399
402
  const n = Number(answer)
400
403
  if (!Number.isInteger(n) || n < 1 || n > entries.length) {
401
- process.stderr.write(`vx run: invalid selection: ${answer}\n`)
404
+ process.stderr.write(
405
+ `vx run: invalid selection: ${answer} (type a number from 1 to ${entries.length})\n`,
406
+ )
402
407
  return null
403
408
  }
404
409
  return entries[n - 1] ?? null
package/src/cli/show.ts CHANGED
@@ -70,7 +70,12 @@ export async function showCmd(args: readonly string[]): Promise<number> {
70
70
  ? parsed.target
71
71
  : parsed.target.slice(0, hashAt)
72
72
  const taskName = hashAt === -1 ? undefined : parsed.target!.slice(hashAt + 1)
73
- if (taskName === '') throw new UserError(`missing task name after '#' in "${parsed.target}"`)
73
+ if (taskName === '') {
74
+ const list = projectName === '' ? 'vx show' : `vx show ${projectName}`
75
+ throw new UserError(
76
+ `vx show: missing task name after '#' in "${parsed.target}" (${list} lists the tasks)`,
77
+ )
78
+ }
74
79
  if (projectName !== undefined && (taskName !== undefined || byName.has(projectName))) {
75
80
  if (!byName.has(projectName)) {
76
81
  throw new UserError(
@@ -105,11 +105,15 @@ export function releaseAsset(release: unknown, name: string): ReleaseAsset {
105
105
  typeof a === 'object' && a !== null && (a as { name?: unknown }).name === name,
106
106
  )
107
107
  if (asset === undefined) {
108
- throw new UserError(`vx upgrade: release ${tag} has no asset ${name} for this platform`)
108
+ throw new UserError(
109
+ `vx upgrade: release ${tag} has no asset ${name} for this platform — nothing replaced; npm install -g @vzn/vx@latest installs it instead`,
110
+ )
109
111
  }
110
112
  const url = asset.browser_download_url
111
113
  if (typeof url !== 'string' || url.length === 0) {
112
- throw new UserError(`vx upgrade: release ${tag} names no download for ${name}`)
114
+ throw new UserError(
115
+ `vx upgrade: release ${tag} names no download for ${name} — nothing replaced; npm install -g @vzn/vx@latest installs it instead`,
116
+ )
113
117
  }
114
118
  const digest = asset.digest
115
119
  const m = typeof digest === 'string' ? /^sha256:([0-9a-f]{64})$/i.exec(digest) : null
@@ -217,13 +221,15 @@ export async function replaceBinary(
217
221
  ): Promise<void> {
218
222
  const res = await fetchOrRefuse(url, { redirect: 'follow' }, 'download the release asset')
219
223
  if (!res.ok) {
220
- throw new UserError(`vx upgrade: download failed (${res.status}) — ${url}`)
224
+ throw new UserError(
225
+ `vx upgrade: download failed (${res.status}) — ${url} — nothing replaced; re-run`,
226
+ )
221
227
  }
222
228
  const bytes = new Uint8Array(
223
229
  await readOrRefuse(() => res.arrayBuffer(), url, 'download the release asset'),
224
230
  )
225
231
  if (bytes.byteLength === 0) {
226
- throw new UserError(`vx upgrade: empty download — ${url}`)
232
+ throw new UserError(`vx upgrade: empty download — ${url} — nothing replaced; re-run`)
227
233
  }
228
234
  const got = new Bun.CryptoHasher('sha256').update(bytes).digest('hex')
229
235
  if (got !== sha256.toLowerCase()) {
package/src/cli/why.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  import { nearMatches, UserError } from '../util/index.js'
18
18
  import { findWorkspaceRoot } from '../workspace/index.js'
19
19
  import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
20
+ import { resolveRunId } from './run-id.js'
20
21
 
21
22
  interface WhyArgs {
22
23
  target?: string
@@ -166,7 +167,10 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
166
167
  try {
167
168
  const db = cache.dbHandle()
168
169
  const taskId = resolveTarget(cache, parsed.target)
169
- const runId = parsed.runId ?? latestRunId(cache.dbHandle(), taskId)
170
+ const runId =
171
+ parsed.runId !== undefined
172
+ ? (resolveRunId(db, parsed.runId, 'vx why') ?? parsed.runId)
173
+ : latestRunId(db, taskId)
170
174
 
171
175
  if (runId === null) {
172
176
  // Runs exist (resolveTarget passed) but predate run ids — fall back to
@@ -243,7 +243,7 @@ async function sandboxFact(declared: number): Promise<InfoFacts['sandbox']> {
243
243
 
244
244
  /**
245
245
  * The doctor's output is pasted into bug reports and compared between
246
- * invocations (`vx stats` is pinned byte-identical to `vx info`), so a
246
+ * invocations, so a
247
247
  * reason must not carry this process's id. The Linux runtime names its
248
248
  * mux socket after the pid (`srt-mux-<pid>-<n>.sock`), and a listen that
249
249
  * fails — a nested sandbox, a read-only tmpdir — quotes that path.
@@ -154,7 +154,6 @@ export type {
154
154
  CacheKeyExplanation,
155
155
  InputDiffEntry,
156
156
  InvocationDetail,
157
- InvocationRow,
158
157
  ListInvocationsArgs,
159
158
  ListRunsArgs,
160
159
  RunDetail,
@@ -114,7 +114,7 @@ export function listRuns(db: Database, args: ListRunsArgs = {}): RunSummaryRow[]
114
114
  /**
115
115
  * One `vx run` invocation header — every `invocations` column, camelCased,
116
116
  * `tags` parsed to a `Record<string,string>` and `dirty`/`ci`/`exitOk`
117
- * surfaced as booleans. Superset of the old `InvocationRow`: the SPA's
117
+ * surfaced as booleans. The SPA's
118
118
  * existing fields (runId / startedAt / endedAt / taskCount / failedCount /
119
119
  * hitCount / totalDurationMs) are all present, so existing views keep working.
120
120
  */
@@ -147,9 +147,6 @@ export interface InvocationDetail {
147
147
  tags: Record<string, string>
148
148
  }
149
149
 
150
- /** @deprecated kept as an alias of `InvocationDetail` for older callers. */
151
- export type InvocationRow = InvocationDetail
152
-
153
150
  interface InvocationRawRow {
154
151
  runId: string
155
152
  command: string
package/src/util/index.ts CHANGED
@@ -45,7 +45,7 @@ export { appendTail, createTail, resetTail, tailText, type Tail } from './tail.j
45
45
  export { ulid } from './ulid.js'
46
46
  export { splitTaskId } from './task-id.js'
47
47
  export { editDistance, nearMatches, nearest } from './edit-distance.js'
48
- export { CORE_VERBS, DISPATCHED_VERBS, MOVED_VERBS } from './verbs.js'
48
+ export { CORE_VERBS, MOVED_VERBS } from './verbs.js'
49
49
  export { isUnsupportedBun, MIN_BUN, unsupportedBunMessage } from './bun-version.js'
50
50
  export { executablePath } from './which.js'
51
51
  export { procfsIsOwn } from './procfs.js'
package/src/util/verbs.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  /**
2
- * The verbs the CLI dispatcher (`cli/index.ts`) owns; `stats` is a
3
- * deprecated alias of `info` and stays out of hints but is still matched
4
- * first. Lives in util rather than cli because the workspace VALIDATOR
2
+ * The verbs the CLI dispatcher (`cli/index.ts`) owns. Lives in util rather than cli because the workspace VALIDATOR
5
3
  * refuses a plugin verb that names one of these — a verb core matches
6
4
  * first could never run — and the workspace module cannot import cli.
7
5
  */
@@ -21,9 +19,6 @@ export const CORE_VERBS = [
21
19
  'version',
22
20
  ] as const
23
21
 
24
- /** Every word the dispatcher matches before consulting plugins. */
25
- export const DISPATCHED_VERBS: readonly string[] = [...CORE_VERBS, 'stats']
26
-
27
22
  /**
28
23
  * Verbs core owned once and a package owns now. Not core verbs: a plugin
29
24
  * may declare them, and the dispatcher prints the
@@ -36,4 +31,5 @@ export const MOVED_VERBS: Readonly<Record<string, string>> = {
36
31
  prune:
37
32
  'vx prune was removed (2026-09-11): the Docker-subset verb is not shipped; copy the ' +
38
33
  'workspace and `--filter` the build instead',
34
+ stats: 'vx stats was removed (H-19): run `vx info`, which prints the same report',
39
35
  }
@@ -15,7 +15,7 @@ import {
15
15
  type WorkspaceConfig,
16
16
  } from '../config.js'
17
17
  import {
18
- DISPATCHED_VERBS,
18
+ CORE_VERBS,
19
19
  MAX_TIMEOUT_MS,
20
20
  nearest,
21
21
  normalizeGlob,
@@ -174,7 +174,7 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
174
174
  // would run the first declared and hide the second. Here, in the
175
175
  // schema, so every loader of the file — a run, a reading verb, the
176
176
  // plugin-verb lookup — refuses it the same way.
177
- if (DISPATCHED_VERBS.includes(verb)) {
177
+ if ((CORE_VERBS as readonly string[]).includes(verb)) {
178
178
  throw new UserError(
179
179
  `${configPath}: plugin '${plug.name}' declares command '${verb}', a core verb — core verbs cannot be shadowed`,
180
180
  )
@@ -357,7 +357,13 @@ function renderConfigFile(
357
357
  if (p.importLines.length > 0) lines.push(...p.importLines)
358
358
  lines.push('', 'export default {', ' tasks: {')
359
359
  for (const t of p.tasks) {
360
- for (const todo of t.todos) lines.push(` // TODO(vx-migrate): ${todo}`)
360
+ // A reason can quote a manifest's own text (an Nx `env` key), and a
361
+ // line break there ended the comment: the rest was code in the file
362
+ // the next run evaluates (D-21). Each line stays inside the comment.
363
+ for (const todo of t.todos) {
364
+ const [first, ...rest] = todo.split(/\r\n|[\n\r\u2028\u2029]/)
365
+ lines.push(` // TODO(vx-migrate): ${first}`, ...rest.map((l) => ` // ${l}`))
366
+ }
361
367
  if (t.task === null) continue // skipped target — the TODO above explains
362
368
  // `__proto__: {…}` in a literal SETS the prototype, quoted or not, and
363
369
  // the run refused the config (item 1033); a computed key is a property.