@vzn/vx 0.0.432 → 0.0.433
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/select.ts +2 -2
- package/src/exec/sandbox-runtime.ts +47 -4
- package/src/orchestrator/run.ts +21 -3
- package/src/util/edit-distance.ts +10 -0
- package/src/util/index.ts +1 -1
- package/src/workspace/project-loader.ts +12 -2
- package/src/workspace/workspace.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.433",
|
|
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.433",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.433",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.433",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.433"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/select.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import type { ProjectConfig } from '../config.js'
|
|
25
25
|
import type { ProjectEntry } from '../workspace/index.js'
|
|
26
26
|
import { parseDependencySpec } from '../graph/index.js'
|
|
27
|
-
import { maskedLine, nearest, UserError } from '../util/index.js'
|
|
27
|
+
import { listed, maskedLine, nearest, UserError } from '../util/index.js'
|
|
28
28
|
import {
|
|
29
29
|
claimedAffected,
|
|
30
30
|
fingerprintClaims,
|
|
@@ -468,5 +468,5 @@ function didYouMeanProject(
|
|
|
468
468
|
const scoped = bare === undefined ? undefined : byBare.get(bare)
|
|
469
469
|
if (scoped?.length === 1) return `. Did you mean ${scoped[0]}?`
|
|
470
470
|
}
|
|
471
|
-
return ''
|
|
471
|
+
return names.length === 0 ? '' : `. Projects: ${listed(names)}`
|
|
472
472
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
lstatSync,
|
|
33
33
|
existsSync,
|
|
34
34
|
mkdirSync,
|
|
35
|
+
mkdtempSync,
|
|
35
36
|
openSync,
|
|
36
37
|
readFileSync,
|
|
37
38
|
readlinkSync,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
rmSync,
|
|
40
41
|
statSync,
|
|
41
42
|
unlinkSync,
|
|
43
|
+
writeFileSync,
|
|
42
44
|
} from 'node:fs'
|
|
43
45
|
import { mkdir, unlink } from 'node:fs/promises'
|
|
44
46
|
import type { SandboxConfig } from '../config.js'
|
|
@@ -162,7 +164,12 @@ async function probeUncached(weakerNested: boolean): Promise<SandboxAvailability
|
|
|
162
164
|
if (!SandboxManager.isSupportedPlatform()) {
|
|
163
165
|
return { available: false, reason: `platform ${process.platform} not supported` }
|
|
164
166
|
}
|
|
165
|
-
|
|
167
|
+
// SRT checks the scan command its live config names, and once that is
|
|
168
|
+
// vx's no-scan file (B-92) a process `exit` hook may have removed it
|
|
169
|
+
// while the process goes on: name one that is there.
|
|
170
|
+
const deps = SandboxManager.checkDependencies(
|
|
171
|
+
noScan === undefined ? undefined : { command: scanCommandAgain() },
|
|
172
|
+
)
|
|
166
173
|
if (deps.errors.length > 0) return { available: false, reason: dependencyReason(deps.errors) }
|
|
167
174
|
const long = socketPathRefusal()
|
|
168
175
|
if (long !== undefined) return { available: false, reason: long }
|
|
@@ -503,14 +510,50 @@ let scopedDenyScan = false
|
|
|
503
510
|
* 1 the scan finds only the root's entries, which SRT keeps only inside a
|
|
504
511
|
* write grant, where the scoped walk already reaches (the parity rows in
|
|
505
512
|
* `sandbox-deny-scan.unsafe.test.ts`). A no-op in rg's place ends the
|
|
506
|
-
* spawn's 3.8 ms at 1.0; SRT has no way to skip it.
|
|
513
|
+
* spawn's 3.8 ms at 1.0; SRT has no way to skip it. A file that cannot be
|
|
514
|
+
* exec'd ends it sooner still: `posix_spawn` refuses it (ENOEXEC) in
|
|
515
|
+
* 0.32 ms against `true`'s 1.06, SRT reads the refusal as an empty scan,
|
|
516
|
+
* and its dependency check (`Bun.which`) still finds the file (B-92).
|
|
507
517
|
*/
|
|
508
518
|
function scopedScanConfig(): { mandatoryDenySearchDepth: number; ripgrep?: { command: string } } {
|
|
509
519
|
try {
|
|
510
|
-
return { mandatoryDenySearchDepth: 1, ripgrep: { command:
|
|
520
|
+
return { mandatoryDenySearchDepth: 1, ripgrep: { command: noScanCommand() } }
|
|
511
521
|
} catch {
|
|
512
|
-
|
|
522
|
+
try {
|
|
523
|
+
return { mandatoryDenySearchDepth: 1, ripgrep: { command: executablePath('true') } }
|
|
524
|
+
} catch {
|
|
525
|
+
return { mandatoryDenySearchDepth: 1 }
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let noScan: string | undefined
|
|
531
|
+
|
|
532
|
+
/** The no-scan file again (made anew if gone), or `true` where it cannot be. */
|
|
533
|
+
function scanCommandAgain(): string {
|
|
534
|
+
try {
|
|
535
|
+
return noScanCommand()
|
|
536
|
+
} catch {
|
|
537
|
+
return executablePath('true')
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* An empty executable only this process can write: SRT runs it outside the
|
|
542
|
+
* sandbox on every wrap, so it lives in a fresh 0700 directory, never in the
|
|
543
|
+
* shared `/tmp/claude`. Made once a process, again if a temp cleaner took it.
|
|
544
|
+
*/
|
|
545
|
+
function noScanCommand(): string {
|
|
546
|
+
if (noScan !== undefined && existsSync(noScan)) return noScan
|
|
547
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), 'vx-noscan-'))
|
|
548
|
+
const file = path.join(dir, 'rg')
|
|
549
|
+
writeFileSync(file, '', { mode: 0o700 })
|
|
550
|
+
if (noScan === undefined) {
|
|
551
|
+
process.on('exit', () => {
|
|
552
|
+
if (noScan !== undefined) rmSync(path.dirname(noScan), { recursive: true, force: true })
|
|
553
|
+
})
|
|
513
554
|
}
|
|
555
|
+
noScan = file
|
|
556
|
+
return file
|
|
514
557
|
}
|
|
515
558
|
|
|
516
559
|
function linuxToolPaths(): { bwrapPath?: string; socatPath?: string } {
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
parseSize,
|
|
39
39
|
printTimings,
|
|
40
40
|
ulid,
|
|
41
|
+
listed,
|
|
41
42
|
nearest,
|
|
42
43
|
UserError,
|
|
43
44
|
machineParallelism,
|
|
@@ -378,7 +379,7 @@ async function runOnBus(
|
|
|
378
379
|
// case too; the message is identical, so that branch stays below.
|
|
379
380
|
if (prepared.unresolvedTasks.length > 0) {
|
|
380
381
|
log.status(
|
|
381
|
-
`No projects declare task(s): ${prepared.unresolvedTasks.join(', ')}.${elsewhereHint(prepared.declaredElsewhere)}${didYouMean(prepared.unresolvedTasks, prepared.hintProjects)}${await initHint(prepared)}`,
|
|
382
|
+
`No projects declare task(s): ${prepared.unresolvedTasks.join(', ')}.${elsewhereHint(prepared.declaredElsewhere)}${didYouMean(prepared.unresolvedTasks, prepared.hintProjects, prepared.declaredElsewhere)}${await initHint(prepared)}`,
|
|
382
383
|
)
|
|
383
384
|
await teardown()
|
|
384
385
|
prepared.cache.close()
|
|
@@ -1427,7 +1428,7 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1427
1428
|
return {
|
|
1428
1429
|
tasks: [],
|
|
1429
1430
|
unresolvedTasks: prepared.unresolvedTasks,
|
|
1430
|
-
unresolvedHint: `${elsewhereHint(prepared.declaredElsewhere)}${didYouMean(prepared.unresolvedTasks, prepared.hintProjects)}${await initHint(prepared)}`,
|
|
1431
|
+
unresolvedHint: `${elsewhereHint(prepared.declaredElsewhere)}${didYouMean(prepared.unresolvedTasks, prepared.hintProjects, prepared.declaredElsewhere)}${await initHint(prepared)}`,
|
|
1431
1432
|
}
|
|
1432
1433
|
}
|
|
1433
1434
|
if (prepared.empty === 'no-tasks-declared' && prepared.declaredElsewhere.length > 0) {
|
|
@@ -1520,12 +1521,16 @@ async function initHint(prepared: {
|
|
|
1520
1521
|
function didYouMean(
|
|
1521
1522
|
unresolved: readonly string[],
|
|
1522
1523
|
projects: ReadonlyMap<string, ProjectEntry>,
|
|
1524
|
+
elsewhere: readonly string[] = [],
|
|
1523
1525
|
): string {
|
|
1524
1526
|
const tasksOf = (p: ProjectEntry | undefined): string[] => Object.keys(p?.config.tasks ?? {})
|
|
1525
1527
|
const allTasks = new Set<string>()
|
|
1526
1528
|
for (const p of projects.values()) for (const t of tasksOf(p)) allTasks.add(t)
|
|
1527
1529
|
// A Set: two typos of the same task hint it once, not once per typo.
|
|
1528
1530
|
const hints = new Set<string>()
|
|
1531
|
+
// A typo past two edits named nothing to pick from (M-56): with no near
|
|
1532
|
+
// name, say what exists instead.
|
|
1533
|
+
const lists = new Set<string>()
|
|
1529
1534
|
for (const spec of unresolved) {
|
|
1530
1535
|
const at = spec.indexOf('#')
|
|
1531
1536
|
const nx = nxProjectTarget(spec, projects)
|
|
@@ -1536,18 +1541,31 @@ function didYouMean(
|
|
|
1536
1541
|
if (at < 0) {
|
|
1537
1542
|
const t = nearest(spec, allTasks)
|
|
1538
1543
|
if (t !== undefined) hints.add(t)
|
|
1544
|
+
// A name outside the selection is already said, with `--all`.
|
|
1545
|
+
else if (allTasks.size > 0 && !elsewhere.includes(spec)) {
|
|
1546
|
+
lists.add(`Tasks: ${listed(allTasks)}.`)
|
|
1547
|
+
}
|
|
1539
1548
|
continue
|
|
1540
1549
|
}
|
|
1541
1550
|
const [proj, task] = [spec.slice(0, at), spec.slice(at + 1)]
|
|
1542
1551
|
if (!projects.has(proj)) {
|
|
1543
1552
|
const p = projectNamed(proj, projects) ?? nearest(proj, projects.keys())
|
|
1544
1553
|
if (p !== undefined && tasksOf(projects.get(p)).includes(task)) hints.add(`${p}#${task}`)
|
|
1554
|
+
else if (p === undefined && projects.size > 0) {
|
|
1555
|
+
lists.add(`No project is named ${proj}; projects: ${listed(projects.keys())}.`)
|
|
1556
|
+
}
|
|
1545
1557
|
continue
|
|
1546
1558
|
}
|
|
1547
1559
|
const t = nearest(task, tasksOf(projects.get(proj)))
|
|
1548
1560
|
if (t !== undefined) hints.add(`${proj}#${t}`)
|
|
1561
|
+
else if (tasksOf(projects.get(proj)).length > 0) {
|
|
1562
|
+
lists.add(`${proj}'s tasks: ${listed(tasksOf(projects.get(proj)))}.`)
|
|
1563
|
+
}
|
|
1549
1564
|
}
|
|
1550
|
-
return
|
|
1565
|
+
return (
|
|
1566
|
+
(hints.size === 0 ? '' : ` Did you mean ${[...hints].join(', ')}?`) +
|
|
1567
|
+
[...lists].map((l) => ` ${l}`).join('')
|
|
1568
|
+
)
|
|
1551
1569
|
}
|
|
1552
1570
|
|
|
1553
1571
|
/**
|
|
@@ -72,3 +72,13 @@ export function nearMatches(name: string, candidates: Iterable<string>, limit =
|
|
|
72
72
|
}
|
|
73
73
|
return out.slice(0, limit)
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The names to offer when no one name is near: sorted, the first `limit`,
|
|
78
|
+
* then a count. A typo past two edits named nothing to pick from (M-56).
|
|
79
|
+
*/
|
|
80
|
+
export function listed(names: Iterable<string>, limit = 8): string {
|
|
81
|
+
const all = [...new Set(names)].sort()
|
|
82
|
+
const more = all.length - limit
|
|
83
|
+
return all.slice(0, limit).join(', ') + (more > 0 ? `, and ${more} more` : '')
|
|
84
|
+
}
|
package/src/util/index.ts
CHANGED
|
@@ -49,7 +49,7 @@ export {
|
|
|
49
49
|
export { appendTail, createTail, resetTail, tailText, type Tail } from './tail.js'
|
|
50
50
|
export { ulid } from './ulid.js'
|
|
51
51
|
export { splitTaskId } from './task-id.js'
|
|
52
|
-
export { editDistance, nearMatches, nearest } from './edit-distance.js'
|
|
52
|
+
export { editDistance, listed, nearMatches, nearest } from './edit-distance.js'
|
|
53
53
|
export { CORE_VERBS, MOVED_VERBS } from './verbs.js'
|
|
54
54
|
export { isUnsupportedBun, MIN_BUN, unsupportedBunMessage } from './bun-version.js'
|
|
55
55
|
export { executablePath, shellArgv } from './which.js'
|
|
@@ -123,9 +123,15 @@ function servableSource(bytes: Uint8Array, loader: 'ts' | 'js'): string | null {
|
|
|
123
123
|
} catch {
|
|
124
124
|
return null
|
|
125
125
|
}
|
|
126
|
-
return hasEsmExport(source, loader) ? source : null
|
|
126
|
+
return !COMMONJS_HINT.test(source) || hasEsmExport(source, loader) ? source : null
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// What makes Bun run a file as CommonJS is one of these names at the top
|
|
130
|
+
// level (an escaped `\u006dodule` too, so any backslash counts). Source
|
|
131
|
+
// with none of them runs as a module whichever path loads it, so it skips
|
|
132
|
+
// the parse: 16–20 µs a config, 1,000 cold configs (2026-10-03).
|
|
133
|
+
const COMMONJS_HINT = /\b(?:module|exports|require|this|__dirname|__filename)\b|\\/
|
|
134
|
+
|
|
129
135
|
/** vx's module-cache query, which no user wrote: stripped from anything shown to them. */
|
|
130
136
|
const BUST_QUERY = /\?vx-(?:bust|held)=[^'"\s]*/g
|
|
131
137
|
|
|
@@ -303,7 +309,11 @@ export function configLoadError(err: unknown, configPath: string, kind: string):
|
|
|
303
309
|
}
|
|
304
310
|
if (name === 'BuildMessage') {
|
|
305
311
|
const pos = (err as { position?: BuildPosition | null }).position ?? {}
|
|
306
|
-
|
|
312
|
+
// A served config's position names its specifier, query and all.
|
|
313
|
+
const file =
|
|
314
|
+
typeof pos.file === 'string' && pos.file.length > 0
|
|
315
|
+
? pos.file.replace(BUST_QUERY, '')
|
|
316
|
+
: configPath
|
|
307
317
|
const at =
|
|
308
318
|
typeof pos.line === 'number'
|
|
309
319
|
? `:${pos.line}${typeof pos.column === 'number' ? `:${pos.column}` : ''}`
|
|
@@ -118,7 +118,8 @@ export async function findWorkspaceRoot(
|
|
|
118
118
|
if (nearest !== null) return nearest
|
|
119
119
|
throw new UserError(
|
|
120
120
|
`Could not find a workspace root in any parent of ${start} ` +
|
|
121
|
-
`(looked for pnpm-workspace.yaml or package.json)
|
|
121
|
+
`(looked for pnpm-workspace.yaml or package.json): run vx inside a project, ` +
|
|
122
|
+
`or create a package.json (\`bun init\` or \`npm init -y\`) and run \`vx init\``,
|
|
122
123
|
)
|
|
123
124
|
}
|
|
124
125
|
|