@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 +2 -1
- package/package.json +5 -5
- package/src/cli/help.ts +0 -1
- package/src/cli/index.ts +0 -3
- package/src/cli/info.ts +1 -1
- package/src/cli/last.ts +2 -1
- package/src/cli/run-id.ts +27 -0
- package/src/cli/run.ts +3 -1
- package/src/cli/select.ts +7 -2
- package/src/cli/show.ts +6 -1
- package/src/cli/upgrade.ts +10 -4
- package/src/cli/why.ts +5 -1
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/index.ts +0 -1
- package/src/orchestrator/metrics.ts +1 -4
- package/src/util/index.ts +1 -1
- package/src/util/verbs.ts +2 -6
- package/src/workspace/config-schema.ts +2 -2
- package/src/workspace/migration.ts +7 -1
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.
|
|
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.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
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
|
-
//
|
|
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(
|
|
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)
|
|
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(
|
|
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 === '')
|
|
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(
|
package/src/cli/upgrade.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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.
|
|
@@ -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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|