@vzn/vx 0.0.114 → 0.0.116
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.116",
|
|
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.116",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.116",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.116",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.116"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/cache.ts
CHANGED
|
@@ -478,7 +478,7 @@ export class Cache implements CacheLayer {
|
|
|
478
478
|
} | null
|
|
479
479
|
)?.value
|
|
480
480
|
} finally {
|
|
481
|
-
db
|
|
481
|
+
closeDb(db)
|
|
482
482
|
}
|
|
483
483
|
} catch {
|
|
484
484
|
return null
|
|
@@ -545,13 +545,23 @@ export class Cache implements CacheLayer {
|
|
|
545
545
|
return read()
|
|
546
546
|
} catch (err) {
|
|
547
547
|
if (!unreadableIndex(err)) throw err
|
|
548
|
-
|
|
548
|
+
try {
|
|
549
|
+
closeDb(this.db)
|
|
550
|
+
} catch {
|
|
551
|
+
// The refusal below is the error worth reporting.
|
|
552
|
+
}
|
|
549
553
|
throw unreadableIndexError(dbFile, err)
|
|
550
554
|
}
|
|
551
555
|
}
|
|
552
556
|
readable(() => {
|
|
553
557
|
this.db.exec('PRAGMA busy_timeout = 5000')
|
|
554
558
|
this.db.exec('PRAGMA journal_mode = WAL')
|
|
559
|
+
// Keep `-wal` and `-shm` when the last connection closes. A closed
|
|
560
|
+
// index otherwise deletes them, and a cache directory this user may
|
|
561
|
+
// not write (a CI cache restored read-only) cannot make them again:
|
|
562
|
+
// every open there failed `attempt to write a readonly database`
|
|
563
|
+
// before the not-writable refusal could name the directory (O-10).
|
|
564
|
+
if (!absent) this.db.fileControl(SQLITE_FCNTL_PERSIST_WAL, 1)
|
|
555
565
|
this.db.exec('PRAGMA synchronous = NORMAL')
|
|
556
566
|
this.db.exec('PRAGMA foreign_keys = ON')
|
|
557
567
|
this.db.exec(`
|
|
@@ -622,7 +632,11 @@ export class Cache implements CacheLayer {
|
|
|
622
632
|
})
|
|
623
633
|
.immediate()
|
|
624
634
|
} catch (err) {
|
|
625
|
-
|
|
635
|
+
try {
|
|
636
|
+
closeDb(this.db)
|
|
637
|
+
} catch {
|
|
638
|
+
// The migration's own error is the one worth reporting.
|
|
639
|
+
}
|
|
626
640
|
throw err
|
|
627
641
|
}
|
|
628
642
|
}
|
|
@@ -1885,7 +1899,7 @@ export class Cache implements CacheLayer {
|
|
|
1885
1899
|
// `rm -rf .vx/cache`): macOS answers SQLITE_IOERR_VNODE on a
|
|
1886
1900
|
// write to an unlinked file where Linux happily writes on.
|
|
1887
1901
|
}
|
|
1888
|
-
this.db
|
|
1902
|
+
closeDb(this.db)
|
|
1889
1903
|
}
|
|
1890
1904
|
|
|
1891
1905
|
private tarPath(hash: string): string {
|
|
@@ -1893,6 +1907,25 @@ export class Cache implements CacheLayer {
|
|
|
1893
1907
|
}
|
|
1894
1908
|
}
|
|
1895
1909
|
|
|
1910
|
+
/**
|
|
1911
|
+
* SQLite's file-control opcode (sqlite3.h). Spelled here, not imported from
|
|
1912
|
+
* `bun:sqlite`'s `constants`: the playground bundles this file against a
|
|
1913
|
+
* shim that exports only `Database`.
|
|
1914
|
+
*/
|
|
1915
|
+
const SQLITE_FCNTL_PERSIST_WAL = 10
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* Close for real. A plain `close()` leaves the connection open while any
|
|
1919
|
+
* statement from `db.prepare()` lives (bun:sqlite 1.4.2 defers it, as
|
|
1920
|
+
* `sqlite3_close_v2` does), so `cache.db` and its `-wal` and `-shm` stayed
|
|
1921
|
+
* open after `Cache.close()`: a leaked descriptor per run for an embedder,
|
|
1922
|
+
* and on Windows a cache directory nothing could delete (O-10).
|
|
1923
|
+
* `close(true)` finalizes them and closes.
|
|
1924
|
+
*/
|
|
1925
|
+
function closeDb(db: Database): void {
|
|
1926
|
+
db.close(true)
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1896
1929
|
/**
|
|
1897
1930
|
* Refuse an artifact whose file names no restore can materialise as one
|
|
1898
1931
|
* tree, or (given the task's declared outputs) that names a file outside
|
package/src/cli/completions.ts
CHANGED
|
@@ -101,7 +101,8 @@ export async function completionsCmd(
|
|
|
101
101
|
return 1
|
|
102
102
|
}
|
|
103
103
|
const verbs = [...CORE_VERBS.filter((v) => v !== 'help' && v !== 'version'), 'help', 'version']
|
|
104
|
-
|
|
104
|
+
// No overlap to skip: the workspace load refuses a plugin verb named like a core one.
|
|
105
|
+
verbs.push(...pluginVerbs)
|
|
105
106
|
process.stdout.write(completionScript(shell as CompletionShell, verbs))
|
|
106
107
|
return 0
|
|
107
108
|
}
|
package/src/exec/runner.ts
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync } from 'node:fs'
|
|
8
8
|
import { constants as osConstants } from 'node:os'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
shellArgv,
|
|
11
|
+
isExecutableMissing,
|
|
12
|
+
isOutOfFds,
|
|
13
|
+
killGraceMs,
|
|
14
|
+
OUT_OF_FDS_HINT,
|
|
15
|
+
} from '../util/index.js'
|
|
10
16
|
import {
|
|
11
17
|
closeSignalChannel,
|
|
12
18
|
holdGroups,
|
|
@@ -40,6 +46,9 @@ export interface RunResult {
|
|
|
40
46
|
cpuMs?: number
|
|
41
47
|
/** Peak resident set size for the child, in bytes. */
|
|
42
48
|
peakRssBytes?: number
|
|
49
|
+
/** `Bun.spawn` threw: no shell ran, so its 127 is vx's and says nothing
|
|
50
|
+
* about the command (A-41). */
|
|
51
|
+
spawnFailed?: true
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
/**
|
|
@@ -664,7 +673,8 @@ export function spawnFailureText(err: unknown, cwd: string, what = 'task'): stri
|
|
|
664
673
|
return `\n[vx] vx runs each task with sh -c: failed to spawn 'sh' (working dir: ${cwd}). Install a POSIX sh and re-run.\n`
|
|
665
674
|
}
|
|
666
675
|
const message = err instanceof Error ? err.message : String(err)
|
|
667
|
-
|
|
676
|
+
const hint = isOutOfFds(err) ? ` — ${OUT_OF_FDS_HINT}` : ''
|
|
677
|
+
return `\n[vx] failed to spawn ${what}: ${message}${hint}\n`
|
|
668
678
|
}
|
|
669
679
|
|
|
670
680
|
export async function runCommand(opts: RunOptions): Promise<RunResult> {
|
|
@@ -689,7 +699,7 @@ export async function runCommand(opts: RunOptions): Promise<RunResult> {
|
|
|
689
699
|
} catch (err) {
|
|
690
700
|
const stderr = spawnFailureText(err, opts.cwd)
|
|
691
701
|
opts.onStderr?.(stderr)
|
|
692
|
-
return { exitCode: 127, durationMs: Date.now() - start, stdout: '', stderr }
|
|
702
|
+
return { exitCode: 127, durationMs: Date.now() - start, stdout: '', stderr, spawnFailed: true }
|
|
693
703
|
}
|
|
694
704
|
|
|
695
705
|
opts.liveChildren?.add(proc)
|
|
@@ -446,6 +446,28 @@ async function unlinkStaleMuxSockets(): Promise<void> {
|
|
|
446
446
|
* B-19). A tool vx cannot find is left to SRT's own dependency check,
|
|
447
447
|
* whose refusal names it.
|
|
448
448
|
*/
|
|
449
|
+
let javaAgent: { javaAgentJarPath?: string } | undefined
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* The JVM proxy agent SRT ships, named so SRT need not look for it. SRT's
|
|
453
|
+
* search lists `npm root -g` among its candidates before trying any, so it
|
|
454
|
+
* spawned npm on every init (~110 ms, the bulk of `vx info`'s sandbox
|
|
455
|
+
* probe) even though the jar sits beside it. Absent (a compiled vx), SRT
|
|
456
|
+
* searches as before.
|
|
457
|
+
*/
|
|
458
|
+
function bundledJavaAgent(): { javaAgentJarPath?: string } {
|
|
459
|
+
if (javaAgent) return javaAgent
|
|
460
|
+
javaAgent = {}
|
|
461
|
+
try {
|
|
462
|
+
const dist = path.dirname(require.resolve('@anthropic-ai/sandbox-runtime'))
|
|
463
|
+
const jar = path.join(dist, '..', 'vendor', 'java-proxy-agent', 'srt-proxy-agent.jar')
|
|
464
|
+
if (existsSync(jar)) javaAgent = { javaAgentJarPath: jar }
|
|
465
|
+
} catch {
|
|
466
|
+
// unresolvable: SRT searches
|
|
467
|
+
}
|
|
468
|
+
return javaAgent
|
|
469
|
+
}
|
|
470
|
+
|
|
449
471
|
function linuxToolPaths(): { bwrapPath?: string; socatPath?: string } {
|
|
450
472
|
if (process.platform !== 'linux') return {}
|
|
451
473
|
const paths: { bwrapPath?: string; socatPath?: string } = {}
|
|
@@ -526,6 +548,7 @@ export async function initSandbox(opts?: {
|
|
|
526
548
|
filesystem: { denyRead: [], allowWrite: [], denyWrite: [] },
|
|
527
549
|
ignoreViolations: DEFAULT_IGNORE_VIOLATIONS,
|
|
528
550
|
...linuxToolPaths(),
|
|
551
|
+
...bundledJavaAgent(),
|
|
529
552
|
}
|
|
530
553
|
if (!srtUp) await unlinkStaleMuxSockets()
|
|
531
554
|
await SandboxManager.initialize(
|
|
@@ -1341,6 +1364,7 @@ async function runSandboxedOnce(
|
|
|
1341
1364
|
durationMs: Date.now() - start,
|
|
1342
1365
|
stdout: '',
|
|
1343
1366
|
stderr,
|
|
1367
|
+
spawnFailed: true,
|
|
1344
1368
|
violations: [],
|
|
1345
1369
|
tracerFailed: false,
|
|
1346
1370
|
}
|
|
@@ -852,8 +852,10 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
852
852
|
// The shell's 127 and 126 name the word and nothing about why — the
|
|
853
853
|
// PATH vx built, or a `#!` line the file itself carries (items 257,
|
|
854
854
|
// 258) — and an exit above 128 is a signal's number and nothing
|
|
855
|
-
// about what sent it (259). One frame line names the rule.
|
|
856
|
-
|
|
855
|
+
// about what sent it (259). One frame line names the rule. A spawn that
|
|
856
|
+
// threw ran no shell: its 127 is vx's, and the line would send the
|
|
857
|
+
// reader after a command that exists (A-41).
|
|
858
|
+
if (!res.timedOut && res.spawnFailed !== true) {
|
|
857
859
|
const verdict = shellVerdict({
|
|
858
860
|
code,
|
|
859
861
|
command: step.command,
|
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
// hook is usually `rimraf dist`); and a script that is nothing but
|
|
24
24
|
// `<pm> run <other>` becomes a GROUP over `<other>`, so the graph sees the
|
|
25
25
|
// dependency instead of a package-manager subprocess it cannot cache.
|
|
26
|
+
// Yarn 2+ runs no such hooks (probed: `yarn run build` printed BUILD alone),
|
|
27
|
+
// so under it `prebuild` is a script like any other (D-31).
|
|
26
28
|
|
|
29
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
30
|
+
import path from 'node:path'
|
|
27
31
|
import { taskNameProblem } from './config-schema.js'
|
|
28
32
|
import type { ProjectMeta } from './workspace.js'
|
|
29
33
|
import {
|
|
@@ -82,6 +86,40 @@ const OWN_COMMANDS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
|
82
86
|
),
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Whether the package manager that owns `dir` is Yarn 2+: the nearest
|
|
91
|
+
* `packageManager: yarn@<2+>`, or the nearest lockfile being a Berry
|
|
92
|
+
* `yarn.lock` (it opens with `__metadata:`). Another lockfile, or none,
|
|
93
|
+
* is a manager that runs `pre` / `post` hooks.
|
|
94
|
+
*/
|
|
95
|
+
function isYarnBerry(dir: string, memo: Map<string, boolean>): boolean {
|
|
96
|
+
const known = memo.get(dir)
|
|
97
|
+
if (known !== undefined) return known
|
|
98
|
+
let answer: boolean | undefined
|
|
99
|
+
const manifest = path.join(dir, 'package.json')
|
|
100
|
+
try {
|
|
101
|
+
const pm = (JSON.parse(readFileSync(manifest, 'utf8')) as { packageManager?: unknown })
|
|
102
|
+
.packageManager
|
|
103
|
+
if (typeof pm === 'string') answer = /^yarn@([2-9]|\d{2,})/.test(pm)
|
|
104
|
+
} catch {}
|
|
105
|
+
if (answer === undefined) {
|
|
106
|
+
const lock = path.join(dir, 'yarn.lock')
|
|
107
|
+
if (existsSync(lock)) answer = readFileSync(lock, 'utf8').includes('\n__metadata:')
|
|
108
|
+
else if (
|
|
109
|
+
['package-lock.json', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb'].some((f) =>
|
|
110
|
+
existsSync(path.join(dir, f)),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
answer = false
|
|
114
|
+
}
|
|
115
|
+
if (answer === undefined) {
|
|
116
|
+
const up = path.dirname(dir)
|
|
117
|
+
answer = up === dir ? false : isYarnBerry(up, memo)
|
|
118
|
+
}
|
|
119
|
+
memo.set(dir, answer)
|
|
120
|
+
return answer
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
function scriptsOf(meta: ProjectMeta): Record<string, unknown> {
|
|
86
124
|
// package.json is a boundary: `scripts` is whatever the file holds. A
|
|
87
125
|
// string or an array would enumerate its indices as script names.
|
|
@@ -132,8 +170,10 @@ function upstreamBuildOnWorker(tasks: GeneratedTask[]): void {
|
|
|
132
170
|
|
|
133
171
|
export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
|
|
134
172
|
const projects: GeneratedProject[] = []
|
|
173
|
+
const berry = new Map<string, boolean>()
|
|
135
174
|
for (const meta of metas) {
|
|
136
175
|
const scripts = scriptsOf(meta)
|
|
176
|
+
const runsHooks = !isYarnBerry(meta.dir, berry)
|
|
137
177
|
const runnable = Object.keys(scripts).filter(
|
|
138
178
|
(n) => typeof scripts[n] === 'string' && scripts[n] !== '',
|
|
139
179
|
)
|
|
@@ -153,7 +193,7 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
|
|
|
153
193
|
// of a script that exists rides inside that script's command.
|
|
154
194
|
const isTask = (n: string): boolean => {
|
|
155
195
|
if (!has(n) || LIFECYCLE.test(n)) return false
|
|
156
|
-
const hookOf = /^(pre|post)(.+)$/.exec(n)
|
|
196
|
+
const hookOf = runsHooks ? /^(pre|post)(.+)$/.exec(n) : null
|
|
157
197
|
return hookOf === null || !has(hookOf[2]!) || LIFECYCLE.test(hookOf[2]!)
|
|
158
198
|
}
|
|
159
199
|
const tasks: GeneratedTask[] = []
|
|
@@ -165,7 +205,7 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
|
|
|
165
205
|
const delegate = delegatedScript(own)
|
|
166
206
|
// npm lifecycle hooks (`prepack`, `prepublishOnly`, …) belong to the
|
|
167
207
|
// package manager and never ride inside a task — `pack` stays alone.
|
|
168
|
-
const hook = (h: string): boolean => has(h) && !LIFECYCLE.test(h)
|
|
208
|
+
const hook = (h: string): boolean => runsHooks && has(h) && !LIFECYCLE.test(h)
|
|
169
209
|
const hooks = [`pre${name}`, `post${name}`].filter(hook)
|
|
170
210
|
// A group over a script that is no task (`setup: npm run prepare`)
|
|
171
211
|
// named a task nothing defines, and the run `vx init` suggested
|