@vzn/vx 0.0.473 → 0.0.475
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/package.json +5 -5
- package/src/cli/completions.ts +8 -1
- package/src/cli/plugin-commands.ts +9 -2
- package/src/cli/upgrade.ts +13 -3
- package/src/cli/watch-judge.ts +7 -1
- package/src/exec/executor.ts +17 -0
- package/src/exec/index.ts +2 -0
- package/src/index.ts +3 -0
- package/src/orchestrator/execute-task.ts +20 -1
- package/src/workspace/config-schema.ts +14 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.475",
|
|
4
4
|
"description": "The fastest task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.475",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.475",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.475",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.475"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/completions.ts
CHANGED
|
@@ -70,7 +70,14 @@ export function completionScript(shell: CompletionShell, verbs: readonly string[
|
|
|
70
70
|
...table.map((t) => ` ${t.verb}) compadd -- ${t.words.join(' ')} ;;`),
|
|
71
71
|
' esac',
|
|
72
72
|
'}',
|
|
73
|
-
|
|
73
|
+
// Autoloaded from $fpath (the `#compdef` file), this body runs on
|
|
74
|
+
// the first Tab: it must complete then, not only define `_vx`, or
|
|
75
|
+
// that Tab rings the bell. Sourced, it registers instead.
|
|
76
|
+
'if [[ "${funcstack[1]}" == _vx ]]; then',
|
|
77
|
+
' _vx "$@"',
|
|
78
|
+
'else',
|
|
79
|
+
' compdef _vx vx',
|
|
80
|
+
'fi',
|
|
74
81
|
'',
|
|
75
82
|
].join('\n')
|
|
76
83
|
case 'fish':
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// before this is asked, so a plugin cannot shadow `run`.
|
|
9
9
|
|
|
10
10
|
import type { VxPlugin, PluginCommand, CommandContext } from '../orchestrator/index.js'
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { findWorkspaceRoot, WORKSPACE_CONFIG_FILENAMES } from '../workspace/index.js'
|
|
13
14
|
import { machineParallelism, maskedLine } from '../util/index.js'
|
|
14
15
|
|
|
15
16
|
export interface ResolvedPluginCommand {
|
|
@@ -52,8 +53,14 @@ async function workspacePlugins(
|
|
|
52
53
|
} catch {
|
|
53
54
|
return null
|
|
54
55
|
}
|
|
56
|
+
// Plugins are declared only in a workspace file. Without one, the load
|
|
57
|
+
// below is the orchestrator's import graph (~50 ms) for an empty list,
|
|
58
|
+
// paid by `vx help` and by the `vx completions` a shell sources at start.
|
|
59
|
+
if (!WORKSPACE_CONFIG_FILENAMES.some((f) => existsSync(path.join(workspaceRoot, f))))
|
|
60
|
+
return { workspaceRoot, cacheDir: '', concurrency: 0, plugins: [] }
|
|
55
61
|
let ws
|
|
56
62
|
try {
|
|
63
|
+
const { loadCliWorkspace } = await import('./workspace-config.js')
|
|
57
64
|
ws = await loadCliWorkspace(workspaceRoot)
|
|
58
65
|
} catch (err) {
|
|
59
66
|
return { loadError: err instanceof Error ? err.message : String(err) }
|
package/src/cli/upgrade.ts
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
// Named `upgrade` (not `update`) per CLI convention: bun upgrade,
|
|
13
13
|
// deno upgrade — "update" is what package managers do to indexes.
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { constants } from 'node:fs'
|
|
16
|
+
import { access, chmod, chown, link, rename, rm, stat } from 'node:fs/promises'
|
|
17
|
+
import path from 'node:path'
|
|
16
18
|
import { flagHint, seeHelp } from './help.js'
|
|
17
19
|
import { UserError } from '../util/index.js'
|
|
18
20
|
import { VERSION } from '../version.js'
|
|
@@ -218,6 +220,15 @@ export async function replaceBinary(
|
|
|
218
220
|
sha256: string,
|
|
219
221
|
starts?: (dest: string) => boolean,
|
|
220
222
|
): Promise<void> {
|
|
223
|
+
// The swap writes beside the binary: a directory this user cannot write
|
|
224
|
+
// (a root-owned /usr/local/bin) failed at the rename, the whole release
|
|
225
|
+
// downloaded for nothing, and the hint named npm, which this binary is not.
|
|
226
|
+
const dir = path.dirname(dest)
|
|
227
|
+
await access(dir, constants.W_OK).catch((err: NodeJS.ErrnoException) => {
|
|
228
|
+
throw new UserError(
|
|
229
|
+
`vx upgrade: cannot write to ${dir} (${err.code ?? err.message}), where this vx lives — nothing downloaded; re-run as a user who can (sudo vx upgrade), or install vx somewhere you can write`,
|
|
230
|
+
)
|
|
231
|
+
})
|
|
221
232
|
const res = await fetchOrRefuse(url, { redirect: 'follow' }, 'download the release asset')
|
|
222
233
|
if (!res.ok) {
|
|
223
234
|
throw new UserError(
|
|
@@ -258,8 +269,7 @@ export async function replaceBinary(
|
|
|
258
269
|
if (kept) await rm(old, { force: true })
|
|
259
270
|
const msg = err instanceof Error ? err.message : String(err)
|
|
260
271
|
throw new UserError(
|
|
261
|
-
`vx upgrade: could not replace ${dest} (${msg}) —
|
|
262
|
-
`check permissions, or reinstall with npm install -g @vzn/vx`,
|
|
272
|
+
`vx upgrade: could not replace ${dest} (${msg}) — nothing replaced; check the permissions of ${dest} and its directory`,
|
|
263
273
|
)
|
|
264
274
|
}
|
|
265
275
|
if (starts === undefined || starts(dest)) {
|
package/src/cli/watch-judge.ts
CHANGED
|
@@ -13,13 +13,19 @@ const ABSENT = -1n
|
|
|
13
13
|
function settledState(abs: string): bigint {
|
|
14
14
|
let st: fs.Stats
|
|
15
15
|
try {
|
|
16
|
+
// A link is its target string, as the key folds it: following it saw
|
|
17
|
+
// a retarget to equal bytes as "the same" and ran nothing.
|
|
18
|
+
if (fs.lstatSync(abs).isSymbolicLink()) return xxh3(`link:${fs.readlinkSync(abs)}`)
|
|
16
19
|
st = fs.statSync(abs)
|
|
17
20
|
} catch {
|
|
18
21
|
return ABSENT
|
|
19
22
|
}
|
|
20
23
|
if (!st.isDirectory()) {
|
|
24
|
+
// The mode too: the key reads it (an executable bit can change what a
|
|
25
|
+
// task does), and with the bytes alone a judged file's `chmod -x`
|
|
26
|
+
// settled to "the same" and ran nothing.
|
|
21
27
|
try {
|
|
22
|
-
return xxh3(fs.readFileSync(abs))
|
|
28
|
+
return xxh3(`${xxh3(fs.readFileSync(abs))}:${st.mode & 0o7777}`)
|
|
23
29
|
} catch {
|
|
24
30
|
return ABSENT
|
|
25
31
|
}
|
package/src/exec/executor.ts
CHANGED
|
@@ -223,6 +223,23 @@ export interface TaskExecutor {
|
|
|
223
223
|
execute(req: ExecuteRequest): Promise<ExecuteResult>
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* What a remote executor rejects with when it gives a task back: core runs
|
|
228
|
+
* the same request on the local floor and says `reason` once (a remote that
|
|
229
|
+
* never started it, B-100). A task placed `remote: 'only'` must not run
|
|
230
|
+
* here, so it fails naming `reason` instead. Matched by name, as
|
|
231
|
+
* `isUserError` is: a plugin's `@vzn/vx` can be another copy of this class.
|
|
232
|
+
*/
|
|
233
|
+
export function executorFallback(reason: string): Error {
|
|
234
|
+
const err = new Error(reason)
|
|
235
|
+
err.name = 'ExecutorFallback'
|
|
236
|
+
return err
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function isExecutorFallback(err: unknown): err is Error {
|
|
240
|
+
return err instanceof Error && err.name === 'ExecutorFallback'
|
|
241
|
+
}
|
|
242
|
+
|
|
226
243
|
/**
|
|
227
244
|
* The result an executor resolves with is a plugin's, so its shape is a
|
|
228
245
|
* boundary: a plugin that resolved `{}` met `res.violations` in core and
|
package/src/exec/index.ts
CHANGED
|
@@ -41,6 +41,8 @@ export { isLocalExecutor, localExecutor } from './local-executor.js'
|
|
|
41
41
|
export { holdGroups, killTree, untilGroupsGone } from './kill-tree.js'
|
|
42
42
|
export {
|
|
43
43
|
assertExecuteResult,
|
|
44
|
+
executorFallback,
|
|
45
|
+
isExecutorFallback,
|
|
44
46
|
selectExecutor,
|
|
45
47
|
type ExecuteRequest,
|
|
46
48
|
type ExecuteResult,
|
package/src/index.ts
CHANGED
|
@@ -171,6 +171,9 @@ export type {
|
|
|
171
171
|
// (`runCommand` / `runSandboxed`, the local executor's own primitives, left
|
|
172
172
|
// the façade 2026-09-10: no executor plugin built on them — `@vzn/vx-reapi`
|
|
173
173
|
// speaks a wire — and a seam with no consumer is a special case in waiting.)
|
|
174
|
+
// A remote executor gives a task back to the local floor by rejecting with
|
|
175
|
+
// this (B-100): core runs it here and says why once.
|
|
176
|
+
export { executorFallback } from './exec/index.js'
|
|
174
177
|
export type {
|
|
175
178
|
ExecuteRequest,
|
|
176
179
|
ExecuteResult,
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
wrapSandboxedCommand,
|
|
26
26
|
signalExitCode,
|
|
27
27
|
isLocalExecutor,
|
|
28
|
+
isExecutorFallback,
|
|
29
|
+
localExecutor,
|
|
28
30
|
type CaptureConfig,
|
|
29
31
|
type ExecuteRequest,
|
|
30
32
|
assertExecuteResult,
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
secretMask,
|
|
46
48
|
secretNamed,
|
|
47
49
|
span,
|
|
50
|
+
UserError,
|
|
48
51
|
} from '../util/index.js'
|
|
49
52
|
import { forwardedSignal, SIGNAL_SHUTDOWN_GRACE_MS } from './signals.js'
|
|
50
53
|
import { executorLabel, nameExecutorFailure } from './plugin-host.js'
|
|
@@ -895,7 +898,23 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
895
898
|
assertExecuteResult(args.executor.name, node.id, r)
|
|
896
899
|
return r
|
|
897
900
|
})
|
|
898
|
-
.catch(async (
|
|
901
|
+
.catch(async (thrown: unknown) => {
|
|
902
|
+
let raw = thrown
|
|
903
|
+
// A remote that gives the task back (it never started it): run it on
|
|
904
|
+
// the local floor with a request of its own, so its timeout counts
|
|
905
|
+
// from now. `remote: 'only'` keeps it off this machine: refused.
|
|
906
|
+
if (isExecutorFallback(raw) && args.executor.remote === true) {
|
|
907
|
+
const why = secrets?.mask(raw.message) ?? raw.message
|
|
908
|
+
if (remoteOnly) {
|
|
909
|
+
raw = new UserError(`${why}, and remote: 'only' keeps it off this machine`)
|
|
910
|
+
} else {
|
|
911
|
+
log.status(`[vx] ${node.id}: ${why} — running it here`)
|
|
912
|
+
clearTimeout(timeoutTimer)
|
|
913
|
+
const local = await localExecutor().execute(await buildRequest())
|
|
914
|
+
assertExecuteResult('local', node.id, local)
|
|
915
|
+
return local
|
|
916
|
+
}
|
|
917
|
+
}
|
|
899
918
|
const err = nameExecutorFailure(args.executor, raw)
|
|
900
919
|
// A remote executor's message carries the server's own text, which
|
|
901
920
|
// may echo the env it was sent: masked here, where it is printed,
|
|
@@ -484,9 +484,16 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
484
484
|
assertKnownFields(cache, CACHE_FIELDS, `${where}.cache`)
|
|
485
485
|
const inputs = (cache as { inputs?: unknown }).inputs
|
|
486
486
|
const outputs = (cache as { outputs?: unknown }).outputs
|
|
487
|
-
|
|
487
|
+
// A present field of the wrong shape (`outputs: 'dist'`) was told it
|
|
488
|
+
// was missing; an array is named in `assertKnownFields`.
|
|
489
|
+
if (inputs === undefined || inputs === null) {
|
|
488
490
|
throw new UserError(`${where}.cache.inputs is required when \`cache\` is set`)
|
|
489
491
|
}
|
|
492
|
+
if (typeof inputs !== 'object') {
|
|
493
|
+
throw new UserError(
|
|
494
|
+
`${where}.cache.inputs must be an object — \`inputs: { files: [...] }\``,
|
|
495
|
+
)
|
|
496
|
+
}
|
|
490
497
|
assertKnownFields(inputs, CACHE_INPUT_FIELDS, `${where}.cache.inputs`)
|
|
491
498
|
if (!Array.isArray((inputs as { files?: unknown }).files)) {
|
|
492
499
|
throw new UserError(`${where}.cache.inputs.files must be an array of glob strings`)
|
|
@@ -520,9 +527,14 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
|
|
|
520
527
|
}
|
|
521
528
|
}
|
|
522
529
|
}
|
|
523
|
-
if (
|
|
530
|
+
if (outputs === undefined || outputs === null) {
|
|
524
531
|
throw new UserError(`${where}.cache.outputs is required when \`cache\` is set`)
|
|
525
532
|
}
|
|
533
|
+
if (typeof outputs !== 'object') {
|
|
534
|
+
throw new UserError(
|
|
535
|
+
`${where}.cache.outputs must be an object — \`outputs: { files: [...] }\``,
|
|
536
|
+
)
|
|
537
|
+
}
|
|
526
538
|
assertKnownFields(outputs, CACHE_OUTPUT_FIELDS, `${where}.cache.outputs`)
|
|
527
539
|
const outFiles = (outputs as { files?: unknown }).files
|
|
528
540
|
if (!Array.isArray(outFiles)) {
|