@vzn/vx 0.0.115 → 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.115",
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.115",
21
- "@vzn/vx-linux-arm64": "0.0.115",
22
- "@vzn/vx-darwin-x64": "0.0.115",
23
- "@vzn/vx-darwin-arm64": "0.0.115"
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"
@@ -478,7 +478,7 @@ export class Cache implements CacheLayer {
478
478
  } | null
479
479
  )?.value
480
480
  } finally {
481
- db.close()
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
- this.db.close()
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
- this.db.close()
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.close()
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
@@ -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 { shellArgv, isExecutableMissing, killGraceMs } from '../util/index.js'
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
- return `\n[vx] failed to spawn ${what}: ${message}\n`
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
- if (!res.timedOut) {
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,