@vzn/vx 0.0.624 → 0.0.625
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/bin.ts +11 -3
- package/src/util/config-frame.ts +90 -0
- package/src/util/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.625",
|
|
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.625",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.625",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.625",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.625"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/bin.ts
CHANGED
|
@@ -30,8 +30,15 @@ async function main(): Promise<void> {
|
|
|
30
30
|
// is set, and `exitOnceFlushed` exits after both streams have ended.
|
|
31
31
|
process.exitCode = code
|
|
32
32
|
} catch (err) {
|
|
33
|
-
const {
|
|
34
|
-
|
|
33
|
+
const {
|
|
34
|
+
configErrorFrame,
|
|
35
|
+
fsRefusalHint,
|
|
36
|
+
isFsRefusal,
|
|
37
|
+
isOutOfFds,
|
|
38
|
+
isUserError,
|
|
39
|
+
maskedLine,
|
|
40
|
+
OUT_OF_FDS_HINT,
|
|
41
|
+
} = await import('./util/index.js')
|
|
35
42
|
// A plugin's failure quotes what it was told (a remote's reply, a
|
|
36
43
|
// header), and a config's own throw is its author's text: a secret in
|
|
37
44
|
// either reached stderr whole (L-11).
|
|
@@ -44,7 +51,8 @@ async function main(): Promise<void> {
|
|
|
44
51
|
// A message that already names the tool (`vx why: …`, thrown by a verb
|
|
45
52
|
// that wants its own name in the line) is printed as it is; prefixing
|
|
46
53
|
// it produced `vx: vx why: …` (walkthrough, 2026-09-04).
|
|
47
|
-
|
|
54
|
+
// A config refusal names its line and shows it.
|
|
55
|
+
const m = configErrorFrame(err.message, process.cwd()) ?? err.message
|
|
48
56
|
say(m.startsWith('vx ') ? `${m}\n` : `vx: ${m}\n`)
|
|
49
57
|
} else if (isOutOfFds(err)) {
|
|
50
58
|
say(`vx: ${err.message} — ${OUT_OF_FDS_HINT}\n`)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// A config refusal pointed at its line: `vx.config.ts:7:9` (which a
|
|
2
|
+
// terminal opens on click) and the source around it. Read from the
|
|
3
|
+
// message the schema already writes (`<file>: tasks.<name>.<path> …`),
|
|
4
|
+
// so it works for a refusal made in the config worker as for one made
|
|
5
|
+
// here, and a message it cannot place prints as it always did.
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
|
|
10
|
+
// `<file>: tasks.<path>[:] …`, or a top-level `<file> has unknown field …`.
|
|
11
|
+
const HEAD = /^(\/[^\n]*?\/vx\.config\.[cm]?[jt]s)(?:: (tasks\.[^\s:]+)(:?))? /
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The message with its file named at `line:col`, relative to `cwd`, and a
|
|
15
|
+
* frame of the source under it; undefined when the message names no
|
|
16
|
+
* config field or the field is not found in the file.
|
|
17
|
+
*/
|
|
18
|
+
export function configErrorFrame(message: string, cwd: string): string | undefined {
|
|
19
|
+
const head = HEAD.exec(message)
|
|
20
|
+
if (head === null) return undefined
|
|
21
|
+
const [, file, fieldPath, colon] = head as unknown as [string, string, string?, string?]
|
|
22
|
+
const rest = message.slice(head[0].length)
|
|
23
|
+
// The field the refusal is about, past the path that leads to it:
|
|
24
|
+
// `has unknown field "comand"`, or `tasks.build: \`cache\` is not allowed`.
|
|
25
|
+
const named =
|
|
26
|
+
colon === ':' ? /^`([^`]+)`/.exec(rest) : /^has (?:unknown )?field "([^"]+)"/.exec(rest)
|
|
27
|
+
const segments = [...(fieldPath?.split('.') ?? []), ...(named === null ? [] : [named[1]!])]
|
|
28
|
+
if (segments.length === 0) return undefined
|
|
29
|
+
let source: string
|
|
30
|
+
try {
|
|
31
|
+
source = readFileSync(file, 'utf8')
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
const at = locate(source, segments)
|
|
36
|
+
if (at === undefined) return undefined
|
|
37
|
+
const lines = source.split('\n')
|
|
38
|
+
// Relative to where the user stands, unless that means climbing out.
|
|
39
|
+
const rel = path.relative(cwd, file)
|
|
40
|
+
const shown = rel === '' || rel.startsWith('..') ? file : rel
|
|
41
|
+
const first = Math.max(1, at.line - 2)
|
|
42
|
+
const width = String(at.line).length
|
|
43
|
+
const frame: string[] = []
|
|
44
|
+
for (let n = first; n <= at.line; n++) {
|
|
45
|
+
const mark = n === at.line ? '>' : ' '
|
|
46
|
+
frame.push(`${mark} ${String(n).padStart(width)} | ${lines[n - 1]!.replace(/\t/g, ' ')}`)
|
|
47
|
+
}
|
|
48
|
+
const lead = lines[at.line - 1]!.slice(0, at.col - 1)
|
|
49
|
+
.replace(/\t/g, ' ')
|
|
50
|
+
.replace(/\S/g, ' ')
|
|
51
|
+
frame.push(` ${' '.repeat(width)} | ${lead}^`)
|
|
52
|
+
return `${shown}:${at.line}:${at.col}${message.slice(file.length)}\n\n${frame.join('\n')}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Where the last of `segments` is declared as a key, each one searched
|
|
57
|
+
* after the one before. A task name holds dots (`lint.oxfmt`), so the
|
|
58
|
+
* longest run of segments that names a key wins.
|
|
59
|
+
*/
|
|
60
|
+
function locate(
|
|
61
|
+
source: string,
|
|
62
|
+
segments: readonly string[],
|
|
63
|
+
): { line: number; col: number } | undefined {
|
|
64
|
+
let pos = 0
|
|
65
|
+
let found = -1
|
|
66
|
+
let i = 0
|
|
67
|
+
while (i < segments.length) {
|
|
68
|
+
let next = -1
|
|
69
|
+
for (let j = segments.length; j > i && next < 0; j--) {
|
|
70
|
+
const name = segments.slice(i, j).join('.')
|
|
71
|
+
const re = new RegExp(`(?<![\\w$.])(['"\`]?)${escapeRe(name)}\\1\\s*:`, 'g')
|
|
72
|
+
re.lastIndex = pos
|
|
73
|
+
const m = re.exec(source)
|
|
74
|
+
if (m === null) continue
|
|
75
|
+
found = m.index + m[1]!.length
|
|
76
|
+
pos = m.index + m[0].length
|
|
77
|
+
next = j
|
|
78
|
+
}
|
|
79
|
+
if (next < 0) return undefined
|
|
80
|
+
i = next
|
|
81
|
+
}
|
|
82
|
+
if (found < 0) return undefined
|
|
83
|
+
const before = source.slice(0, found)
|
|
84
|
+
const line = before.split('\n').length
|
|
85
|
+
return { line, col: found - before.lastIndexOf('\n') }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function escapeRe(s: string): string {
|
|
89
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
90
|
+
}
|