@vzn/vx 0.0.596 → 0.0.598

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.
Files changed (79) hide show
  1. package/launcher.cjs +7 -3
  2. package/package.json +5 -5
  3. package/src/bin.ts +9 -5
  4. package/src/cache/archive.ts +35 -6
  5. package/src/cache/cache.ts +60 -31
  6. package/src/cache/git-inputs.ts +23 -0
  7. package/src/cache/index.ts +2 -0
  8. package/src/cache/inputs.ts +87 -32
  9. package/src/cache/key-fold.ts +1 -1
  10. package/src/cache/layer.ts +2 -1
  11. package/src/cache/layered-cache.ts +14 -9
  12. package/src/cache/output-index.ts +12 -7
  13. package/src/cache/run-history.ts +16 -8
  14. package/src/cli/cache.ts +2 -2
  15. package/src/cli/foreign-flags.ts +56 -2
  16. package/src/cli/info.ts +1 -1
  17. package/src/cli/init.ts +2 -2
  18. package/src/cli/last.ts +6 -3
  19. package/src/cli/lock.ts +5 -3
  20. package/src/cli/plan-format.ts +26 -20
  21. package/src/cli/run.ts +42 -15
  22. package/src/cli/select.ts +19 -5
  23. package/src/cli/show.ts +21 -27
  24. package/src/cli/upgrade.ts +1 -1
  25. package/src/cli/watch-cycle.ts +43 -0
  26. package/src/cli/watch-fs.ts +11 -3
  27. package/src/cli/watch.ts +50 -11
  28. package/src/cli/workspace-config.ts +3 -1
  29. package/src/config.ts +21 -0
  30. package/src/exec/executor.ts +1 -1
  31. package/src/exec/kill-tree.ts +121 -20
  32. package/src/exec/runner.ts +224 -48
  33. package/src/exec/sandbox-paths.ts +11 -0
  34. package/src/exec/sandbox-runtime.ts +187 -54
  35. package/src/exec/sandbox-violations.ts +59 -6
  36. package/src/graph/priorities.ts +1 -2
  37. package/src/graph/scheduler.ts +2 -2
  38. package/src/graph/task-graph.ts +145 -33
  39. package/src/index.ts +3 -1
  40. package/src/orchestrator/affected-tasks.ts +6 -2
  41. package/src/orchestrator/deferred-outputs.ts +3 -2
  42. package/src/orchestrator/doctor.ts +5 -3
  43. package/src/orchestrator/excluded-keys.ts +2 -13
  44. package/src/orchestrator/execute-task.ts +70 -28
  45. package/src/orchestrator/failure-mode.ts +38 -29
  46. package/src/orchestrator/framed-output.ts +5 -5
  47. package/src/orchestrator/hit-restore.ts +1 -1
  48. package/src/orchestrator/index.ts +0 -1
  49. package/src/orchestrator/keyed-projects.ts +6 -3
  50. package/src/orchestrator/lockfile-claim.ts +14 -7
  51. package/src/orchestrator/logger.ts +17 -7
  52. package/src/orchestrator/miss-save.ts +7 -11
  53. package/src/orchestrator/plan.ts +4 -20
  54. package/src/orchestrator/plugin-host.ts +7 -2
  55. package/src/orchestrator/plugin.ts +5 -2
  56. package/src/orchestrator/prepare.ts +4 -2
  57. package/src/orchestrator/projects.ts +1 -1
  58. package/src/orchestrator/run-artifacts.ts +2 -2
  59. package/src/orchestrator/run-context.ts +1 -1
  60. package/src/orchestrator/run-lock.ts +35 -25
  61. package/src/orchestrator/run.ts +2 -1
  62. package/src/orchestrator/sandbox-request.ts +31 -9
  63. package/src/orchestrator/signals.ts +22 -4
  64. package/src/orchestrator/stable-keys.ts +3 -11
  65. package/src/orchestrator/summary.ts +5 -9
  66. package/src/orchestrator/task-hash.ts +13 -3
  67. package/src/orchestrator/upstream.ts +11 -0
  68. package/src/util/hangup.ts +42 -0
  69. package/src/util/index.ts +2 -0
  70. package/src/util/paths.ts +1 -1
  71. package/src/workspace/affected.ts +79 -42
  72. package/src/workspace/config-cache.ts +15 -1
  73. package/src/workspace/config-eval.ts +47 -7
  74. package/src/workspace/config-imports.ts +8 -55
  75. package/src/workspace/config-schema.ts +30 -6
  76. package/src/workspace/lockfile.ts +2 -1
  77. package/src/workspace/migrate-scripts.ts +1 -1
  78. package/src/workspace/project-loader.ts +33 -6
  79. package/src/workspace/workspace.ts +36 -14
@@ -9,6 +9,7 @@ import {
9
9
  CONFIG_EXIT,
10
10
  evalBudgetMs,
11
11
  evaluateConfigFresh,
12
+ thrownValueMessage,
12
13
  WATCHED_BUILTIN_NAMES,
13
14
  } from './config-eval.js'
14
15
  import { hasEsmExport, unprovidedBareImports } from './config-imports.js'
@@ -86,7 +87,12 @@ function assertDefaultObject(mod: unknown, kind: string, configPath: string): vo
86
87
  const heldSources = new Map<string, { source: string; uses: number }>()
87
88
  let serving = false
88
89
 
89
- const HELD_QUERY = /\?vx-held=([0-9a-f]+)$/
90
+ const HELD_QUERY = /\?vx-(?:held|literal)=([0-9a-f]+)$/
91
+
92
+ // Bun's resolver reads `\` as a separator, even in a file: URL, so a
93
+ // config under `a\b` was looked for under `a/b`. Resolving it here keeps
94
+ // the path as written; only the served load can take it.
95
+ const LITERAL_QUERY = /\?vx-literal=[0-9a-f]+$/
90
96
 
91
97
  function serveHeldSources(): void {
92
98
  if (serving) return
@@ -94,6 +100,7 @@ function serveHeldSources(): void {
94
100
  Bun.plugin({
95
101
  name: 'vx-config-bytes',
96
102
  setup(build) {
103
+ build.onResolve({ filter: LITERAL_QUERY }, (args) => ({ path: args.path, namespace: 'file' }))
97
104
  build.onLoad({ filter: HELD_QUERY }, (args) => ({
98
105
  contents: heldSources.get(HELD_QUERY.exec(args.path)![1]!)!.source,
99
106
  loader: /\.[cm]?ts\?/.test(args.path) ? 'ts' : 'js',
@@ -135,7 +142,7 @@ const COMMONJS_HINT =
135
142
  /\b(?:module|exports|require|this|__dirname|__filename)\b|\\|\bexport\s*=(?!=)/
136
143
 
137
144
  /** vx's module-cache query, which no user wrote: stripped from anything shown to them. */
138
- const BUST_QUERY = /\?vx-(?:bust|held)=[^'"\s]*/g
145
+ const BUST_QUERY = /\?vx-(?:bust|held|literal)=[^'"\s]*/g
139
146
 
140
147
  // Bun has native TS / ESM execution — no transpiler dep needed. We fold
141
148
  // a short content hash into the import URL as a cache-bust key so that:
@@ -156,10 +163,18 @@ async function loadDefaultExport(
156
163
  // URL yet, and a repeat load (the one that could replay an evaluation
157
164
  // made under earlier env values) re-evaluates in a worker instead
158
165
  // (`loadedConfigs`, item 678).
166
+ const literal = configPath.includes('\\')
159
167
  const source =
160
- kind === 'Project' ? servableSource(bytes, /\.[cm]?ts$/.test(configPath) ? 'ts' : 'js') : null
168
+ kind === 'Project' || literal
169
+ ? servableSource(bytes, /\.[cm]?ts$/.test(configPath) ? 'ts' : 'js')
170
+ : null
171
+ if (literal && source === null) {
172
+ throw new UserError(
173
+ `${kind} config ${configPath} sits under a path that holds a backslash, which Bun loads only as an ES module in UTF-8`,
174
+ )
175
+ }
161
176
  const hash = xxh3hex(bytes)
162
- const specifier = `${configPath}?vx-${source !== null ? 'held' : 'bust'}=${hash}`
177
+ const specifier = `${configPath}?vx-${literal ? 'literal' : source !== null ? 'held' : 'bust'}=${hash}`
163
178
  if (source !== null) {
164
179
  serveHeldSources()
165
180
  holdSource(hash, source)
@@ -191,7 +206,7 @@ async function loadDefaultExport(
191
206
  if (err instanceof Error && err.stack !== undefined) {
192
207
  err.stack = err.stack.replaceAll(specifier, configPath).replace(BUST_QUERY, '')
193
208
  }
194
- throw configLoadError(err, configPath, kind) ?? err
209
+ throw loadFailure(err, configPath, kind)
195
210
  } finally {
196
211
  clearTimeout(deadline)
197
212
  unguard()
@@ -341,6 +356,16 @@ export function configLoadError(err: unknown, configPath: string, kind: string):
341
356
  return null
342
357
  }
343
358
 
359
+ /** What a failed load throws: Bun's own errors and non-Error throws as user errors, the rest as thrown. */
360
+ function loadFailure(err: unknown, configPath: string, kind: string): unknown {
361
+ const user = configLoadError(err, configPath, kind)
362
+ if (user !== null) return user
363
+ if (err !== null && typeof err === 'object' && typeof (err as Error).message === 'string') {
364
+ return err
365
+ }
366
+ return new UserError(thrownValueMessage(kind, configPath, Bun.inspect(err, { compact: true })))
367
+ }
368
+
344
369
  export interface LoadProjectConfigOptions {
345
370
  /** Observe the CURRENT environment: no eval cache, even when `evalCache` is passed. */
346
371
  fresh?: boolean
@@ -523,7 +548,7 @@ export async function loadProjectConfigs(
523
548
  if (repeat) refuseUnprovidedImports(bytes!, configPath, 'Project')
524
549
  const mod = repeat
525
550
  ? await evaluateConfigFresh(configPath).catch((err: unknown) => {
526
- throw configLoadError(err, configPath, 'Project') ?? err
551
+ throw loadFailure(err, configPath, 'Project')
527
552
  })
528
553
  : await loadDefaultExport(configPath, 'Project', bytes!)
529
554
  // Before anything reads through them: a replaced `Array.prototype.includes`
@@ -812,6 +837,7 @@ const BUN_MEMBERS_VX_READS: readonly PropertyKey[] = [
812
837
  'env',
813
838
  'file',
814
839
  'hash',
840
+ 'inspect',
815
841
  'main',
816
842
  'nanoseconds',
817
843
  'plugin',
@@ -820,6 +846,7 @@ const BUN_MEMBERS_VX_READS: readonly PropertyKey[] = [
820
846
  'semver',
821
847
  'serve',
822
848
  'sleep',
849
+ 'sleepSync',
823
850
  'spawn',
824
851
  'spawnSync',
825
852
  'stderr',
@@ -290,13 +290,17 @@ async function readRootManifests(
290
290
  )
291
291
  }
292
292
  if (ws === undefined || ws === null) return { globs: ['.'], catalogs }
293
+ // bun's `{ catalog }` and yarn's `{ nohoist }` name no members, and both
294
+ // managers run the root alone; vx refused them as no array (D-151).
295
+ if (typeof ws === 'object' && !Array.isArray(ws) && !('packages' in ws)) {
296
+ return { globs: ['.'], catalogs }
297
+ }
293
298
  if (ws && typeof ws === 'object' && !Array.isArray(ws) && 'packages' in ws) {
294
- const globs = assertGlobList(
295
- (ws as { packages?: unknown }).packages ?? [],
296
- pkgPath,
297
- 'workspaces.packages',
298
- )
299
- return { globs, catalogs }
299
+ const packages = (ws as { packages?: unknown }).packages
300
+ // Yarn 1 and 4 run the root alone under `packages: null`; read as an
301
+ // empty list, the root was no project and every verb ran nothing (D-150).
302
+ if (packages === null) return { globs: ['.'], catalogs }
303
+ return { globs: assertGlobList(packages, pkgPath, 'workspaces.packages'), catalogs }
300
304
  }
301
305
  return { globs: assertGlobList(ws, pkgPath, 'workspaces'), catalogs }
302
306
  }
@@ -454,13 +458,27 @@ function extglobRefusal(pattern: string, file: string, field: string): UserError
454
458
  * to the workspace root.
455
459
  */
456
460
  export function resolveCacheDir(root: string, config: WorkspaceConfig | null): string {
457
- const rel = config?.cacheDir ?? (process.env['VX_CACHE_DIR'] || path.join('.vx', 'cache'))
461
+ const env = process.env['VX_CACHE_DIR']
462
+ // `cacheDir: ' '` is refused at load (X-22); the variable made the same
463
+ // directory named in spaces at the root (D-160).
464
+ if (config?.cacheDir === undefined && env !== undefined && env !== '' && env.trim() === '') {
465
+ throw new UserError('VX_CACHE_DIR is only whitespace — name a directory, or unset it')
466
+ }
467
+ const rel = config?.cacheDir ?? (env || path.join('.vx', 'cache'))
468
+ const home = process.env['HOME'] || homedir()
458
469
  // No shell expands `~` in a config string or a quoted variable, and
459
470
  // `'~/.cache/vx'` made a directory named `~` in the workspace.
460
- if (rel.startsWith('~/')) {
461
- return path.join(process.env['HOME'] || homedir(), rel.slice(1))
471
+ const dir =
472
+ rel === '~' || rel.startsWith('~/') ? path.join(home, rel.slice(1)) : path.resolve(root, rel)
473
+ // The cache writes a `*` .gitignore beside its index: a bare `~` made a
474
+ // directory named `~`, and expanded it would land in home itself (D-153).
475
+ if (path.resolve(dir) === path.resolve(home)) {
476
+ const source = config?.cacheDir !== undefined ? 'cacheDir' : 'VX_CACHE_DIR'
477
+ throw new UserError(
478
+ `${source} ${JSON.stringify(rel)} is the home directory itself, where the cache's \`*\` .gitignore and index would land — name a directory under it, like '~/.cache/vx'`,
479
+ )
462
480
  }
463
- return path.resolve(root, rel)
481
+ return dir
464
482
  }
465
483
 
466
484
  /**
@@ -711,12 +729,17 @@ async function readManifest(root: string, dir: string, file: string): Promise<st
711
729
  )
712
730
  if (searchable) unreadable(err, file)
713
731
  process.stderr.write(
714
- `vx: ${relPosix(root, dir)} is not readable by this user — skipped, with any project in it\n`,
732
+ `vx: ${shownDir(root, dir)} is not readable by this user — skipped, with any project in it\n`,
715
733
  )
716
734
  return null
717
735
  }
718
736
  }
719
737
 
738
+ /** `dir` as a discovery line names it: the root's own path is '' (D-127, D-154). */
739
+ function shownDir(root: string, dir: string): string {
740
+ return relPosix(root, dir) || 'the workspace root'
741
+ }
742
+
720
743
  export async function listProjects(workspace: Workspace): Promise<ProjectMeta[]> {
721
744
  return discoverProjects(workspace)
722
745
  }
@@ -769,7 +792,7 @@ export async function discoverProjects(
769
792
  // same flight as the failed read, so naming it costs nothing.
770
793
  if (configPath !== null) {
771
794
  process.stderr.write(
772
- `vx: ${relPosix(workspace.root, dir)} has a vx config but no package.json — skipped: vx names a project by its package.json "name"\n`,
795
+ `vx: ${shownDir(workspace.root, dir)} has a vx config but no package.json — skipped: vx names a project by its package.json "name"\n`,
773
796
  )
774
797
  }
775
798
  return null
@@ -800,9 +823,8 @@ export async function discoverProjects(
800
823
  // fine for a dir that declares no tasks; a dir with a vx config was
801
824
  // meant to run.
802
825
  if (configPath !== null) {
803
- const rel = relPosix(workspace.root, dir)
804
826
  process.stderr.write(
805
- `vx: ${rel === '' ? 'the workspace root' : rel} has a vx config but its package.json has no "name" — skipped\n`,
827
+ `vx: ${shownDir(workspace.root, dir)} has a vx config but its package.json has no "name" — skipped\n`,
806
828
  )
807
829
  }
808
830
  continue