@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.624",
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.624",
25
- "@vzn/vx-linux-arm64": "0.0.624",
26
- "@vzn/vx-darwin-x64": "0.0.624",
27
- "@vzn/vx-darwin-arm64": "0.0.624"
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 { fsRefusalHint, isFsRefusal, isOutOfFds, isUserError, maskedLine, OUT_OF_FDS_HINT } =
34
- await import('./util/index.js')
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
- const m = err.message
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
+ }
package/src/util/index.ts CHANGED
@@ -18,6 +18,7 @@ export {
18
18
  notAWorkTree,
19
19
  OUT_OF_FDS_HINT,
20
20
  } from './errors.js'
21
+ export { configErrorFrame } from './config-frame.js'
21
22
  export { xxh3, xxh3hex } from './hash.js'
22
23
  export {
23
24
  beginRun,