@vzn/vx 0.0.50 → 0.0.51
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 +38 -31
- package/package.json +5 -5
- package/src/bin.ts +1 -1
- package/src/cache/archive.ts +18 -6
- package/src/cache/cache.ts +150 -67
- package/src/cache/chained-cache.ts +9 -2
- package/src/cache/file-hashes.ts +32 -3
- package/src/cache/git-inputs.ts +96 -11
- package/src/cache/index.ts +1 -0
- package/src/cache/layer.ts +28 -3
- package/src/cache/layered-cache.ts +7 -2
- package/src/cache/output-index.ts +16 -7
- package/src/cache/run-history.ts +5 -1
- package/src/cache/zstd.ts +40 -8
- package/src/cli/cache.ts +35 -9
- package/src/cli/completions.ts +10 -7
- package/src/cli/help.ts +48 -4
- package/src/cli/index.ts +5 -3
- package/src/cli/info.ts +1 -1
- package/src/cli/plugin-commands.ts +10 -2
- package/src/cli/run.ts +1 -0
- package/src/cli/select.ts +29 -2
- package/src/cli/upgrade.ts +110 -22
- package/src/cli/watch.ts +35 -18
- package/src/config.ts +18 -5
- package/src/exec/executor.ts +3 -2
- package/src/exec/index.ts +1 -1
- package/src/exec/runner.ts +102 -24
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-runtime.ts +149 -40
- package/src/exec/sandbox-violations.ts +42 -10
- package/src/graph/scheduler.ts +11 -5
- package/src/graph/task-graph.ts +96 -12
- package/src/orchestrator/admission.ts +72 -17
- package/src/orchestrator/execute-task.ts +39 -25
- package/src/orchestrator/hit-restore.ts +7 -1
- package/src/orchestrator/miss-save.ts +29 -1
- package/src/orchestrator/placement.ts +23 -3
- package/src/orchestrator/plugin-host.ts +1 -1
- package/src/orchestrator/plugin.ts +9 -1
- package/src/orchestrator/prepare.ts +1 -0
- package/src/orchestrator/run.ts +75 -12
- package/src/orchestrator/sandbox-request.ts +19 -10
- package/src/orchestrator/summary.ts +26 -15
- package/src/orchestrator/task-hash.ts +7 -2
- package/src/orchestrator/telemetry.ts +14 -4
- package/src/util/paths.ts +16 -9
- package/src/workspace/affected.ts +201 -38
- package/src/workspace/config-eval.ts +4 -1
- package/src/workspace/config-schema.ts +70 -10
- package/src/workspace/fingerprint.ts +6 -0
- package/src/workspace/migration.ts +1 -1
- package/src/workspace/project-loader.ts +53 -1
- package/src/workspace/workspace.ts +54 -14
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { realpathSync, statSync } from 'node:fs'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
import type { ProjectConfig, WorkspaceConfig } from '../config.js'
|
|
3
4
|
import { UserError, xxh3hex } from '../util/index.js'
|
|
4
5
|
import { validateProjectConfig, validateWorkspace } from './config-schema.js'
|
|
5
6
|
import { beginEvalRound, evaluateConfigFresh } from './config-eval.js'
|
|
6
7
|
import { hasEsmExport, unprovidedBareImports } from './config-imports.js'
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
configEvalKey,
|
|
10
|
+
configEvalKeyFromClosure,
|
|
11
|
+
configImports,
|
|
12
|
+
type ConfigEvalStore,
|
|
13
|
+
} from './config-cache.js'
|
|
8
14
|
import { readOnce } from './load-reads.js'
|
|
9
15
|
|
|
10
16
|
// The validator lives in config-schema.ts; re-exported so a reader that
|
|
@@ -420,6 +426,41 @@ export async function loadProjectConfig(
|
|
|
420
426
|
return config!
|
|
421
427
|
}
|
|
422
428
|
|
|
429
|
+
/** When this process first began loading each workspace config, in ms. */
|
|
430
|
+
const workspaceLoadedAt = new Map<string, number>()
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* A repeat load in one process (`vx mcp`'s every call, `vx watch`'s every
|
|
434
|
+
* cycle) busts the workspace config's own URL, but Bun answers what it
|
|
435
|
+
* imports from the module registry: an edited local plugin kept its first
|
|
436
|
+
* version and `listTasks` served its old tasks with no word (item 1046).
|
|
437
|
+
* Bun cannot evaluate an imported module again, so the load is refused,
|
|
438
|
+
* naming the file, as `vx watch` already says when it sees the edit. Only
|
|
439
|
+
* a repeat load pays for the walk; a CLI verb loads once.
|
|
440
|
+
*/
|
|
441
|
+
async function refuseStaleWorkspaceImports(configPath: string, since: number): Promise<void> {
|
|
442
|
+
for (const file of await configImports(configPath)) {
|
|
443
|
+
let mtime: number
|
|
444
|
+
try {
|
|
445
|
+
mtime = statSync(file).mtimeMs
|
|
446
|
+
} catch {
|
|
447
|
+
continue
|
|
448
|
+
}
|
|
449
|
+
// `since` is whole milliseconds and an mtime is not: a file written in
|
|
450
|
+
// the millisecond the first load began (1000.4 against 1000) read as
|
|
451
|
+
// changed, and the control reload was refused one run in ten. An edit
|
|
452
|
+
// inside that millisecond is one the load may have read anyway.
|
|
453
|
+
if (Math.floor(mtime) > since) {
|
|
454
|
+
// The walk names real paths: a root reached through a symlink (macOS's
|
|
455
|
+
// temp dir) named `../target/helper.mjs` from the link.
|
|
456
|
+
const from = path.dirname(realpathSync(configPath))
|
|
457
|
+
throw new UserError(
|
|
458
|
+
`${path.basename(configPath)} imports ${path.relative(from, file)}, which changed after this process loaded it; a running process cannot evaluate an imported module again — restart it to apply the edit`,
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
423
464
|
/**
|
|
424
465
|
* Find and load `vx.workspace.{ts,mts,js,mjs}` from the workspace
|
|
425
466
|
* root. Returns `null` if no such file exists (the common case;
|
|
@@ -430,8 +471,19 @@ export async function loadWorkspaceConfig(root: string): Promise<WorkspaceConfig
|
|
|
430
471
|
for (const configPath of WORKSPACE_CONFIG_FILENAMES.map((f) => path.join(root, f))) {
|
|
431
472
|
const bytes = await readOnce(undefined, configPath)
|
|
432
473
|
if (bytes === null) continue
|
|
474
|
+
const since = workspaceLoadedAt.get(configPath)
|
|
475
|
+
if (since !== undefined) await refuseStaleWorkspaceImports(configPath, since)
|
|
476
|
+
const startedAt = Date.now()
|
|
433
477
|
const mod = (await loadDefaultExport(configPath, 'Workspace', bytes)) as WorkspaceConfig
|
|
478
|
+
// Checked again once awaited, as a project config is: a Promise default
|
|
479
|
+
// passed the first check, and `Promise.resolve(null)` crashed the
|
|
480
|
+
// validator with a stack while `Promise.resolve(42)` loaded as no
|
|
481
|
+
// config at all (D-6).
|
|
482
|
+
assertDefaultObject(mod, 'Workspace', configPath)
|
|
434
483
|
validateWorkspace(mod, configPath)
|
|
484
|
+
// Set by a load that succeeded: a failed one may have left nothing in
|
|
485
|
+
// the registry, and its fix must not be refused.
|
|
486
|
+
if (since === undefined) workspaceLoadedAt.set(configPath, startedAt)
|
|
435
487
|
return mod
|
|
436
488
|
}
|
|
437
489
|
return null
|
|
@@ -416,25 +416,65 @@ async function memberDirs(root: string, pattern: string): Promise<string[]> {
|
|
|
416
416
|
}
|
|
417
417
|
return dirs
|
|
418
418
|
}
|
|
419
|
-
const globPattern = normalized === '' ? 'package.json' : `${normalized}/package.json`
|
|
420
|
-
const glob = new Bun.Glob(globPattern)
|
|
421
419
|
const dirs: string[] = []
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
420
|
+
// `Bun.Glob`'s scan finds nothing for a brace whose alternatives hold a
|
|
421
|
+
// slash, though its `match` reads one: `packages/{a,nested/b}` listed
|
|
422
|
+
// neither package, and both ran under no verb while the root still
|
|
423
|
+
// claimed them (D-2). Such a pattern is scanned as its expansions.
|
|
424
|
+
for (const expanded of slashBraceExpansions(normalized)) {
|
|
425
|
+
const globPattern = expanded === '' ? 'package.json' : `${expanded}/package.json`
|
|
426
|
+
const glob = new Bun.Glob(globPattern)
|
|
427
|
+
// A linked member is a member, as the readdir path above has it:
|
|
428
|
+
// `packages/*` found `packages/b -> ../ext/b` while `packages/{a,b}` and
|
|
429
|
+
// `pack*/*` did not (item 987). Followed only where the depth is bounded:
|
|
430
|
+
// under `**` the scan would walk every pnpm `node_modules` link.
|
|
431
|
+
const followSymlinks = !expanded.includes('**')
|
|
432
|
+
for await (const rel of glob.scan({ cwd: root, onlyFiles: true, dot: false, followSymlinks })) {
|
|
433
|
+
// Skip nested node_modules — workspace package globs shouldn't
|
|
434
|
+
// ever reach into them, but a pathological pattern like `**`
|
|
435
|
+
// would. Avoid splitting the path on the hot loop.
|
|
436
|
+
if (rel.includes(`${path.sep}node_modules${path.sep}`)) continue
|
|
437
|
+
if (rel.startsWith(`node_modules${path.sep}`)) continue
|
|
438
|
+
dirs.push(path.dirname(path.resolve(root, rel)))
|
|
439
|
+
}
|
|
434
440
|
}
|
|
435
441
|
return dirs
|
|
436
442
|
}
|
|
437
443
|
|
|
444
|
+
/**
|
|
445
|
+
* `pattern` with its first brace group expanded, recursively, while that
|
|
446
|
+
* group holds a `/`; any other pattern as itself, since `Bun.Glob`
|
|
447
|
+
* expands a slash-free brace on its own. An unbalanced or escaped brace
|
|
448
|
+
* is left to the glob.
|
|
449
|
+
*/
|
|
450
|
+
function slashBraceExpansions(pattern: string): string[] {
|
|
451
|
+
if (pattern.includes('\\')) return [pattern]
|
|
452
|
+
for (let open = pattern.indexOf('{'); open !== -1; open = pattern.indexOf('{', open + 1)) {
|
|
453
|
+
let depth = 0
|
|
454
|
+
const cuts: number[] = []
|
|
455
|
+
let close = -1
|
|
456
|
+
for (let i = open; i < pattern.length && close === -1; i++) {
|
|
457
|
+
const c = pattern[i]
|
|
458
|
+
if (c === '{') depth++
|
|
459
|
+
else if (c === '}' && --depth === 0) close = i
|
|
460
|
+
else if (c === ',' && depth === 1) cuts.push(i)
|
|
461
|
+
}
|
|
462
|
+
if (close === -1) return [pattern]
|
|
463
|
+
const body = pattern.slice(open + 1, close)
|
|
464
|
+
if (cuts.length === 0 || !body.includes('/')) continue
|
|
465
|
+
const head = pattern.slice(0, open)
|
|
466
|
+
const tail = pattern.slice(close + 1)
|
|
467
|
+
const bounds = [open, ...cuts, close]
|
|
468
|
+
const out: string[] = []
|
|
469
|
+
for (let k = 0; k + 1 < bounds.length; k++) {
|
|
470
|
+
const alt = pattern.slice(bounds[k]! + 1, bounds[k + 1])
|
|
471
|
+
out.push(...slashBraceExpansions(head + alt + tail))
|
|
472
|
+
}
|
|
473
|
+
return [...new Set(out)]
|
|
474
|
+
}
|
|
475
|
+
return [pattern]
|
|
476
|
+
}
|
|
477
|
+
|
|
438
478
|
/**
|
|
439
479
|
* The project's config file, by `PROJECT_CONFIG_FILENAMES` precedence, or null.
|
|
440
480
|
*
|