@vzn/vx 0.0.650 → 0.0.652
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 +7 -23
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/schemas/show.json +14 -20
- package/src/bin.ts +21 -31
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +221 -30
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +77 -14
- package/src/cache/layer.ts +17 -6
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +5 -3
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +56 -17
- package/src/cache/schema.ts +15 -2
- package/src/cli/cache.ts +28 -18
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +14 -25
- package/src/cli/index.ts +28 -9
- package/src/cli/info.ts +16 -8
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +246 -204
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +8 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +45 -52
- package/src/cli/task-verb.ts +22 -0
- package/src/cli/upgrade.ts +3 -1
- package/src/cli/watch-fs.ts +6 -1
- package/src/cli/watch-judge.ts +3 -2
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +94 -52
- package/src/cli/why.ts +123 -89
- package/src/cli/workspace-config.ts +2 -1
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +42 -2
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +58 -5
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/pipes.ts +152 -0
- package/src/exec/runner.ts +199 -182
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +317 -23
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/definition.ts +16 -3
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +17 -11
- package/src/graph/task-graph.ts +51 -24
- package/src/handoff.ts +10 -7
- package/src/history/failure-mode.ts +24 -1
- package/src/history/history.ts +5 -0
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +14 -6
- package/src/index.ts +7 -3
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +67 -20
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/live-usage.ts +16 -11
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +108 -29
- package/src/orchestrator/miss-save.ts +58 -11
- package/src/orchestrator/persistent-task.ts +5 -1
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +7 -3
- package/src/orchestrator/plugin-host.ts +67 -2
- package/src/orchestrator/plugin.ts +53 -2
- package/src/orchestrator/prepare.ts +16 -1
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +62 -13
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +88 -59
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +145 -41
- package/src/orchestrator/sandbox-request.ts +52 -9
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +21 -7
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +6 -14
- package/src/report/forecast.ts +10 -32
- package/src/report/framed-output.ts +283 -175
- package/src/report/index.ts +8 -3
- package/src/report/logger.ts +74 -39
- package/src/report/plain-output.ts +3 -1
- package/src/report/status-line.ts +49 -357
- package/src/report/summary.ts +105 -136
- package/src/settings.ts +8 -8
- package/src/ui/ansi.ts +48 -0
- package/src/ui/components.ts +506 -0
- package/src/ui/index.ts +79 -0
- package/src/ui/kit.ts +37 -0
- package/src/ui/live-region.ts +363 -0
- package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
- package/src/ui/terminal.ts +51 -0
- package/src/ui/theme.ts +224 -0
- package/src/ui/version-line.ts +28 -0
- package/src/util/config-frame.ts +191 -14
- package/src/util/edit-distance.ts +6 -1
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +21 -3
- package/src/util/secret-mask.ts +39 -3
- package/src/util/settle.ts +33 -1
- package/src/util/tail.ts +4 -1
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +70 -63
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +36 -1
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +16 -9
- package/src/workspace/workspace.ts +3 -0
- package/src/util/colors.ts +0 -133
- package/src/util/report.ts +0 -44
- package/src/util/ui.ts +0 -258
- /package/src/{report/path-links.ts → ui/links.ts} +0 -0
- /package/src/{report → ui}/terminal-signals.ts +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// `vx --version`'s one line, imported alone: the answer that loads nothing
|
|
2
|
+
// else (the ui barrel costs ~20 ms, `node:tty` 17 ms). Painted as the
|
|
3
|
+
// `status` component's verb and the info hue; piped, the same words a
|
|
4
|
+
// script reads.
|
|
5
|
+
|
|
6
|
+
import { fstatSync } from 'node:fs'
|
|
7
|
+
|
|
8
|
+
function versionLine(version: string, where: string, on: boolean): string {
|
|
9
|
+
const sgr = (code: string, text: string): string => (on ? `\x1b[${code}m${text}\x1b[0m` : text)
|
|
10
|
+
return `${sgr('1', 'vx')} ${sgr('38;2;6;182;212', version)}${where === '' ? '' : sgr('2', where)}\n`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Write the line to stdout. Colour is decided here, not by `detectColors`:
|
|
15
|
+
* fd 1 as a character device is the TTY answer, and a CI's log is not
|
|
16
|
+
* painted for this one line. `Bun.stdout`, not `process.stdout`: the Node
|
|
17
|
+
* stream's first touch is ~7 ms, half of this verb (2026-10-02). A reader
|
|
18
|
+
* gone (EPIPE) is no failure of the line.
|
|
19
|
+
*/
|
|
20
|
+
export async function printVersion(version: string, where: string): Promise<void> {
|
|
21
|
+
const env = process.env
|
|
22
|
+
const on = env['NO_COLOR']
|
|
23
|
+
? false
|
|
24
|
+
: env['FORCE_COLOR'] !== undefined
|
|
25
|
+
? env['FORCE_COLOR'] !== '0' && env['FORCE_COLOR'] !== 'false'
|
|
26
|
+
: env['TERM'] !== 'dumb' && fstatSync(1).isCharacterDevice()
|
|
27
|
+
await Bun.write(Bun.stdout, versionLine(version, where, on)).catch(() => {})
|
|
28
|
+
}
|
package/src/util/config-frame.ts
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
import { readFileSync } from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
|
|
10
|
-
// `<file>: tasks.<path>[:] …`,
|
|
11
|
-
//
|
|
12
|
-
|
|
10
|
+
// `<file>: tasks.<path>[:] …`, `<file>: \`<setting>\` …`, `<file>: task name "<name>" …`,
|
|
11
|
+
// or a top-level `<file> has unknown field …`, from a project's vx.config
|
|
12
|
+
// or the workspace's .vx/workspace.ts.
|
|
13
|
+
const HEAD =
|
|
14
|
+
/^(\/[^\n]*?\/(?:vx\.config|\.vx\/workspace)\.[cm]?[jt]s)(?:: (?:(tasks\.[^\s:]+)(:?)|`([^`\n]+)`|task name ("(?:[^"\\\n]|\\.)*")))? /
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* The message with its file named at `line:col`, relative to `cwd`, and a
|
|
@@ -19,13 +21,36 @@ const HEAD = /^(\/[^\n]*?\/(?:vx\.config|\.vx\/workspace)\.[cm]?[jt]s)(?:: (task
|
|
|
19
21
|
export function configErrorFrame(message: string, cwd: string): string | undefined {
|
|
20
22
|
const head = HEAD.exec(message)
|
|
21
23
|
if (head === null) return positionFrame(message, cwd)
|
|
22
|
-
const [, file,
|
|
24
|
+
const [, file, tasksPath, colon, setting, taskName] = head as unknown as [
|
|
25
|
+
string,
|
|
26
|
+
string,
|
|
27
|
+
string?,
|
|
28
|
+
string?,
|
|
29
|
+
string?,
|
|
30
|
+
string?,
|
|
31
|
+
]
|
|
23
32
|
const rest = message.slice(head[0].length)
|
|
24
33
|
// The field the refusal is about, past the path that leads to it:
|
|
25
34
|
// `has unknown field "comand"`, or `tasks.build: \`cache\` is not allowed`.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
// A refused key is quoted: `tasks.b.exec.env.define: "A=B" is not …`.
|
|
36
|
+
const said =
|
|
37
|
+
colon === ':'
|
|
38
|
+
? /^(?:`([^`]+)`|("(?:[^"\\\n]|\\.)*"))/.exec(rest)
|
|
39
|
+
: /^has (?:unknown )?field "([^"]+)"/.exec(rest)
|
|
40
|
+
const named = said === null ? null : [said[0], said[1] ?? JSON.parse(said[2]!)]
|
|
41
|
+
// `plugins[2].name`: an element of an array, framed at the element (a
|
|
42
|
+
// plugin is a call, so no key inside it is searched).
|
|
43
|
+
const element = setting === undefined ? null : /^([^[]+)\[(\d+)\]/.exec(setting)
|
|
44
|
+
const fieldPath =
|
|
45
|
+
taskName !== undefined
|
|
46
|
+
? ['tasks', JSON.parse(taskName)]
|
|
47
|
+
: element !== null
|
|
48
|
+
? element[1]!.split('.')
|
|
49
|
+
: (tasksPath ?? setting)?.split('.')
|
|
50
|
+
const segments = [
|
|
51
|
+
...(fieldPath ?? []),
|
|
52
|
+
...(named === null || element !== null ? [] : [named[1]!]),
|
|
53
|
+
]
|
|
29
54
|
if (segments.length === 0) return undefined
|
|
30
55
|
let source: string
|
|
31
56
|
try {
|
|
@@ -33,7 +58,7 @@ export function configErrorFrame(message: string, cwd: string): string | undefin
|
|
|
33
58
|
} catch {
|
|
34
59
|
return undefined
|
|
35
60
|
}
|
|
36
|
-
const at = locate(source, segments)
|
|
61
|
+
const at = locate(source, segments, element === null ? undefined : Number(element[2]))
|
|
37
62
|
if (at === undefined) return undefined
|
|
38
63
|
return framed(file, source, at, message.slice(file.length), cwd)
|
|
39
64
|
}
|
|
@@ -43,8 +68,70 @@ export function configErrorFrame(message: string, cwd: string): string | undefin
|
|
|
43
68
|
const POSITION =
|
|
44
69
|
/^(?:Project|Workspace) config (\/[^\n]*?\/(?:vx\.config|\.vx\/workspace)\.[cm]?[jt]s):(\d+):(\d+)(: [^]*)$/
|
|
45
70
|
|
|
71
|
+
// The loader's `Project config <file> threw: <message> (at <file>:<line>:<col>)`:
|
|
72
|
+
// the config's own throw, placed by its stack in the config itself.
|
|
73
|
+
const THREW =
|
|
74
|
+
/^(?:Project|Workspace) config (\/[^\n]*?\/(?:vx\.config|\.vx\/workspace)\.[cm]?[jt]s) threw: ([^\n]*?) \(at (\/[^\n]*?):(\d+):(\d+)\)([^]*)$/
|
|
75
|
+
|
|
76
|
+
// The loader's `<kind> config <file>: cannot find '<pkg>' …`: placed at the
|
|
77
|
+
// specifier. And `<kind> config at <file> did not export a default object`:
|
|
78
|
+
// at the `export default` that exported something else, when there is one.
|
|
79
|
+
const UNPROVIDED =
|
|
80
|
+
/^(?:Project|Workspace) config (\/[^\n]*?\/(?:vx\.config|\.vx\/workspace)\.[cm]?[jt]s): (cannot find '([^'\n]+)'[^]*)$/
|
|
81
|
+
const NOT_OBJECT =
|
|
82
|
+
/^(?:Project|Workspace) config at (\/[^\n]*?\/(?:vx\.config|\.vx\/workspace)\.[cm]?[jt]s) (did not export a default object[^]*)$/
|
|
83
|
+
|
|
84
|
+
/** The file, the source offset `find` places in it, and what follows `<file>:<line>:<col>`. */
|
|
85
|
+
function sourceFrame(
|
|
86
|
+
file: string,
|
|
87
|
+
find: (source: string) => number,
|
|
88
|
+
rest: string,
|
|
89
|
+
cwd: string,
|
|
90
|
+
): string | undefined {
|
|
91
|
+
let source: string
|
|
92
|
+
try {
|
|
93
|
+
source = readFileSync(file, 'utf8')
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
const at = find(source)
|
|
98
|
+
if (at < 0) return undefined
|
|
99
|
+
const before = source.slice(0, at)
|
|
100
|
+
const line = before.split('\n').length
|
|
101
|
+
return framed(file, source, { line, col: at - before.lastIndexOf('\n') }, rest, cwd)
|
|
102
|
+
}
|
|
103
|
+
|
|
46
104
|
function positionFrame(message: string, cwd: string): string | undefined {
|
|
47
|
-
const
|
|
105
|
+
const unprovided = UNPROVIDED.exec(message)
|
|
106
|
+
if (unprovided !== null) {
|
|
107
|
+
const [, file, said, pkg] = unprovided as unknown as [string, string, string, string]
|
|
108
|
+
const spec = new RegExp(
|
|
109
|
+
`(?<=\\b(?:from|import|import\\(|require\\()\\s*)(['"])${escapeRe(pkg)}\\1`,
|
|
110
|
+
)
|
|
111
|
+
return sourceFrame(file, (src) => spec.exec(src)?.index ?? -1, `: ${said}`, cwd)
|
|
112
|
+
}
|
|
113
|
+
const notObject = NOT_OBJECT.exec(message)
|
|
114
|
+
if (notObject !== null) {
|
|
115
|
+
const [, file, said] = notObject as unknown as [string, string, string]
|
|
116
|
+
return sourceFrame(
|
|
117
|
+
file,
|
|
118
|
+
(src) => /^export\s+default\b/m.exec(src)?.index ?? -1,
|
|
119
|
+
`: ${said}`,
|
|
120
|
+
cwd,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
const threw = THREW.exec(message)
|
|
124
|
+
const pos =
|
|
125
|
+
threw !== null && threw[3] === threw[1]
|
|
126
|
+
? [
|
|
127
|
+
threw[0],
|
|
128
|
+
threw[1],
|
|
129
|
+
threw[4],
|
|
130
|
+
threw[5],
|
|
131
|
+
// A cause's own `(at …)` names the same file: relative there too.
|
|
132
|
+
`: threw: ${threw[2]}${threw[6]!.replaceAll(threw[1]!, path.relative(cwd, threw[1]!))}`,
|
|
133
|
+
]
|
|
134
|
+
: POSITION.exec(message)
|
|
48
135
|
if (pos === null) return undefined
|
|
49
136
|
const [, file, line, col, rest] = pos as unknown as [string, string, string, string, string]
|
|
50
137
|
let source: string
|
|
@@ -86,37 +173,127 @@ function framed(
|
|
|
86
173
|
|
|
87
174
|
/**
|
|
88
175
|
* Where the last of `segments` is declared as a key, each one searched
|
|
89
|
-
*
|
|
90
|
-
* longest run of segments that names a key wins.
|
|
176
|
+
* inside the object literal the one before holds. A task name holds dots
|
|
177
|
+
* (`lint.oxfmt`), so the longest run of segments that names a key wins.
|
|
178
|
+
* A last segment the object leaves out (`outputs is required`) is placed
|
|
179
|
+
* at its parent: searched past it, the next task's `outputs` was framed.
|
|
91
180
|
*/
|
|
92
181
|
function locate(
|
|
93
182
|
source: string,
|
|
94
183
|
segments: readonly string[],
|
|
184
|
+
/** The element of the array the last segment holds; past its end, the key. */
|
|
185
|
+
index?: number,
|
|
95
186
|
): { line: number; col: number } | undefined {
|
|
96
187
|
let pos = 0
|
|
188
|
+
let end = source.length
|
|
97
189
|
let found = -1
|
|
98
190
|
let i = 0
|
|
99
191
|
while (i < segments.length) {
|
|
100
192
|
let next = -1
|
|
101
193
|
for (let j = segments.length; j > i && next < 0; j--) {
|
|
102
194
|
const name = segments.slice(i, j).join('.')
|
|
103
|
-
|
|
195
|
+
// An empty key is quoted, or every `:` matched; a control character
|
|
196
|
+
// is in the source as its escape (`'\u001b'`).
|
|
197
|
+
const spelled = JSON.stringify(name).slice(1, -1)
|
|
198
|
+
const names = spelled === name ? escapeRe(name) : `${escapeRe(name)}|${escapeRe(spelled)}`
|
|
199
|
+
const re = new RegExp(
|
|
200
|
+
`(?<![\\w$.])(['"\`]${name === '' ? '' : '?'})(?:${names})\\1\\s*:`,
|
|
201
|
+
'g',
|
|
202
|
+
)
|
|
104
203
|
re.lastIndex = pos
|
|
105
204
|
const m = re.exec(source)
|
|
106
|
-
if (m === null) continue
|
|
205
|
+
if (m === null || m.index >= end) continue
|
|
107
206
|
found = m.index + m[1]!.length
|
|
108
207
|
pos = m.index + m[0].length
|
|
208
|
+
end = literalEnd(source, pos)
|
|
109
209
|
next = j
|
|
110
210
|
}
|
|
111
|
-
if (next < 0)
|
|
211
|
+
if (next < 0) {
|
|
212
|
+
if (found < 0 || i !== segments.length - 1) return undefined
|
|
213
|
+
break
|
|
214
|
+
}
|
|
112
215
|
i = next
|
|
113
216
|
}
|
|
114
217
|
if (found < 0) return undefined
|
|
218
|
+
if (index !== undefined && i === segments.length) {
|
|
219
|
+
const el = elementStart(source, pos, index)
|
|
220
|
+
if (el !== undefined) found = el
|
|
221
|
+
}
|
|
115
222
|
const before = source.slice(0, found)
|
|
116
223
|
const line = before.split('\n').length
|
|
117
224
|
return { line, col: found - before.lastIndexOf('\n') }
|
|
118
225
|
}
|
|
119
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Where the object or array literal starting at `from` (past blanks) ends;
|
|
229
|
+
* `from` itself when the value is no literal (`exec: 'x'`, `cache: shared`),
|
|
230
|
+
* so no key is searched inside it.
|
|
231
|
+
*/
|
|
232
|
+
function literalEnd(source: string, from: number): number {
|
|
233
|
+
let i = from
|
|
234
|
+
while (i < source.length && /\s/.test(source[i]!)) i++
|
|
235
|
+
if (source[i] !== '{' && source[i] !== '[') return from
|
|
236
|
+
let depth = 0
|
|
237
|
+
for (; i < source.length; i++) {
|
|
238
|
+
const c = source[i]!
|
|
239
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
240
|
+
for (i++; i < source.length && source[i] !== c; i++) if (source[i] === '\\') i++
|
|
241
|
+
} else if (c === '/' && source[i + 1] === '/') {
|
|
242
|
+
i = source.indexOf('\n', i)
|
|
243
|
+
if (i < 0) return source.length
|
|
244
|
+
} else if (c === '/' && source[i + 1] === '*') {
|
|
245
|
+
i = source.indexOf('*/', i + 2)
|
|
246
|
+
if (i < 0) return source.length
|
|
247
|
+
i++
|
|
248
|
+
} else if (c === '{' || c === '[') depth++
|
|
249
|
+
else if (c === '}' || c === ']') {
|
|
250
|
+
depth--
|
|
251
|
+
if (depth === 0) return i
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return source.length
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Where element `index` of the array literal at `from` (past blanks) starts, if it has one. */
|
|
258
|
+
function elementStart(source: string, from: number, index: number): number | undefined {
|
|
259
|
+
let i = from
|
|
260
|
+
while (i < source.length && /\s/.test(source[i]!)) i++
|
|
261
|
+
if (source[i] !== '[') return undefined
|
|
262
|
+
let depth = 0
|
|
263
|
+
let n = 0
|
|
264
|
+
// Each element starts at the first code past `[` or a top-level `,`.
|
|
265
|
+
let pending = true
|
|
266
|
+
for (i++; i < source.length; i++) {
|
|
267
|
+
const c = source[i]!
|
|
268
|
+
if (c === '/' && source[i + 1] === '/') {
|
|
269
|
+
i = source.indexOf('\n', i)
|
|
270
|
+
if (i < 0) return undefined
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
if (c === '/' && source[i + 1] === '*') {
|
|
274
|
+
i = source.indexOf('*/', i + 2)
|
|
275
|
+
if (i < 0) return undefined
|
|
276
|
+
i++
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
if (/\s/.test(c)) continue
|
|
280
|
+
if (depth === 0 && (c === ']' || c === ')' || c === '}')) return undefined
|
|
281
|
+
if (pending) {
|
|
282
|
+
if (n === index) return i
|
|
283
|
+
pending = false
|
|
284
|
+
}
|
|
285
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
286
|
+
for (i++; i < source.length && source[i] !== c; i++) if (source[i] === '\\') i++
|
|
287
|
+
} else if (c === '{' || c === '[' || c === '(') depth++
|
|
288
|
+
else if (c === '}' || c === ']' || c === ')') depth--
|
|
289
|
+
else if (c === ',' && depth === 0) {
|
|
290
|
+
n++
|
|
291
|
+
pending = true
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
120
297
|
function escapeRe(s: string): string {
|
|
121
298
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
122
299
|
}
|
|
@@ -37,9 +37,14 @@ export function nearest(
|
|
|
37
37
|
// Two edits rewrite half of a word of four letters or fewer (`--poll`
|
|
38
38
|
// read as `--all`, `vx test` as `last`), unless they swap two letters.
|
|
39
39
|
const short = maxEdits === 2 && name.replace(/^-+/, '').length <= 4
|
|
40
|
+
// The same word in another case is the nearest whatever its length:
|
|
41
|
+
// `--ALL` is three edits from `--all`, `BUILD` five from `build`.
|
|
42
|
+
const lower = name.toLowerCase()
|
|
43
|
+
const list = [...candidates]
|
|
44
|
+
for (const c of list) if (c !== name && c.toLowerCase() === lower) return c
|
|
40
45
|
let best: string | undefined
|
|
41
46
|
let bestD = maxEdits + 1
|
|
42
|
-
for (const c of
|
|
47
|
+
for (const c of list) {
|
|
43
48
|
// Capped one past the budget, so "past it" never reads as within it:
|
|
44
49
|
// `--continue-on-error` was nine edits from `--concurrency`, capped to
|
|
45
50
|
// the three `vx run`'s same-prefix hint allows, and offered.
|
package/src/util/index.ts
CHANGED
|
@@ -27,25 +27,11 @@ export {
|
|
|
27
27
|
} from './errors.js'
|
|
28
28
|
export { configErrorFrame, configRefusal } from './config-frame.js'
|
|
29
29
|
export { envFlagOn } from './env-flag.js'
|
|
30
|
-
export {
|
|
31
|
-
detectColors,
|
|
32
|
-
dumbTerminal,
|
|
33
|
-
ERROR,
|
|
34
|
-
identityColor,
|
|
35
|
-
paint,
|
|
36
|
-
SUCCESS,
|
|
37
|
-
WARN,
|
|
38
|
-
type ColorSupport,
|
|
39
|
-
} from './colors.js'
|
|
40
|
-
export { barWidth, gradientRule, ruleWidth } from './report.js'
|
|
41
|
-
import * as ui from './ui.js'
|
|
42
|
-
export { refusalNotice, sayLine, sayNotice } from './refusal-notice.js'
|
|
43
|
-
export { ui }
|
|
44
30
|
export { keysSorted, xxh3, xxh3hex } from './hash.js'
|
|
45
31
|
export {
|
|
46
32
|
beginRun,
|
|
47
33
|
mark,
|
|
48
|
-
|
|
34
|
+
timingLines,
|
|
49
35
|
restartTimings,
|
|
50
36
|
span,
|
|
51
37
|
stageTimes,
|
|
@@ -86,6 +72,7 @@ export {
|
|
|
86
72
|
} from './paths.js'
|
|
87
73
|
export {
|
|
88
74
|
claimExitForSignal,
|
|
75
|
+
dieOf,
|
|
89
76
|
exitClaimedBySignal,
|
|
90
77
|
killGraceMs,
|
|
91
78
|
settleWithin,
|
package/src/util/paths.ts
CHANGED
|
@@ -202,7 +202,7 @@ export function isLiteralPattern(glob: string): boolean {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
/**
|
|
205
|
-
* The alphabet of a TASK glob (`inputs.files`, `
|
|
205
|
+
* The alphabet of a TASK glob (`inputs.files`, `outputs.files`,
|
|
206
206
|
* `workspaceFiles`), behind {@link isLiteralPattern}, for the callers that
|
|
207
207
|
* need the POSITION of the first wildcard rather than a verdict. Every site
|
|
208
208
|
* that asks "must this declaration be matched?" reads this one regex: item
|
|
@@ -523,15 +523,33 @@ export function braceAlternatives(pattern: string): string[] {
|
|
|
523
523
|
return expandBraces(pattern, false)
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Where the `[`…`]` class opening at `i` ends, as `bracket-literal.ts`
|
|
528
|
+
* reads one (a `]` right after the `[` is a member); `i` for a `[` with no
|
|
529
|
+
* `]`, which `Bun.Glob` takes literally. A `,` `{` or `}` inside a class
|
|
530
|
+
* is a member: split there, `src/{a[,]b,x}.ts` became `src/a[.ts` and
|
|
531
|
+
* `src/]b.ts`, so an edit to `src/a,b.ts` kept the key (turborepo#13763).
|
|
532
|
+
*/
|
|
533
|
+
function classEnd(pattern: string, i: number): number {
|
|
534
|
+
const end = pattern.indexOf(']', i + 2)
|
|
535
|
+
return end === -1 ? i : end
|
|
536
|
+
}
|
|
537
|
+
|
|
526
538
|
function expandBraces(pattern: string, slashOnly: boolean): string[] {
|
|
527
539
|
if (pattern.includes('\\')) return [pattern]
|
|
528
|
-
for (let open =
|
|
540
|
+
for (let open = 0; open < pattern.length; open++) {
|
|
541
|
+
if (pattern[open] === '[') {
|
|
542
|
+
open = classEnd(pattern, open)
|
|
543
|
+
continue
|
|
544
|
+
}
|
|
545
|
+
if (pattern[open] !== '{') continue
|
|
529
546
|
let depth = 0
|
|
530
547
|
const cuts: number[] = []
|
|
531
548
|
let close = -1
|
|
532
549
|
for (let i = open; i < pattern.length && close === -1; i++) {
|
|
533
550
|
const c = pattern[i]
|
|
534
|
-
if (c === '
|
|
551
|
+
if (c === '[') i = classEnd(pattern, i)
|
|
552
|
+
else if (c === '{') depth++
|
|
535
553
|
else if (c === '}' && --depth === 0) close = i
|
|
536
554
|
else if (c === ',' && depth === 1) cuts.push(i)
|
|
537
555
|
}
|
package/src/util/secret-mask.ts
CHANGED
|
@@ -115,15 +115,51 @@ export function secretMask(
|
|
|
115
115
|
for (const name in source) {
|
|
116
116
|
const value = source[name]
|
|
117
117
|
if (secretNamed(name)) add(value)
|
|
118
|
-
if (value?.includes('://') === true && value.includes('@'))
|
|
119
|
-
for (const
|
|
120
|
-
}
|
|
118
|
+
if (value?.includes('://') === true && value.includes('@'))
|
|
119
|
+
for (const pw of urlPasswords(value)) add(pw)
|
|
121
120
|
}
|
|
122
121
|
for (const name of named) add(source[name])
|
|
123
122
|
}
|
|
124
123
|
// This process's secrets: the environment's and the held ones.
|
|
125
124
|
if (sources.includes(process.env)) for (const value of held) add(value)
|
|
126
125
|
if (values.size === 0) return null
|
|
126
|
+
const found = [...values]
|
|
127
|
+
for (const m of masks) if (sameValues(m.values, found)) return m.mask
|
|
128
|
+
if (masks.length >= MASKS_KEPT) masks.shift()
|
|
129
|
+
const mask = maskOf(values)
|
|
130
|
+
masks.push({ values: found, mask })
|
|
131
|
+
return mask
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Masks built, with the values each masks: every task of a run asks for
|
|
136
|
+
* the same one, and building it (a sort, a RegExp the engine compiles
|
|
137
|
+
* again) took 30 ms of a cold run's main thread at 500 tasks. The values
|
|
138
|
+
* are read fresh on every call; only the same values reuse a mask.
|
|
139
|
+
*/
|
|
140
|
+
const masks: Array<{ values: readonly string[]; mask: SecretMask }> = []
|
|
141
|
+
const MASKS_KEPT = 8
|
|
142
|
+
|
|
143
|
+
function sameValues(a: readonly string[], b: readonly string[]): boolean {
|
|
144
|
+
if (a.length !== b.length) return false
|
|
145
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Each URL value's passwords, found once: the value decides them. */
|
|
150
|
+
const urlPasswordsOf = new Map<string, string[]>()
|
|
151
|
+
|
|
152
|
+
function urlPasswords(value: string): string[] {
|
|
153
|
+
let found = urlPasswordsOf.get(value)
|
|
154
|
+
if (found === undefined) {
|
|
155
|
+
found = []
|
|
156
|
+
for (const m of value.matchAll(URL_PASSWORD)) if (m[1] !== undefined) found.push(m[1])
|
|
157
|
+
urlPasswordsOf.set(value, found)
|
|
158
|
+
}
|
|
159
|
+
return found
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function maskOf(values: ReadonlySet<string>): SecretMask {
|
|
127
163
|
// Longest first, so a value holding another is replaced whole.
|
|
128
164
|
const sorted = [...values].sort((a, b) => b.length - a.length)
|
|
129
165
|
const pattern = new RegExp(
|
package/src/util/settle.ts
CHANGED
|
@@ -51,7 +51,8 @@ export function killGraceMs(defaultMs: number): number {
|
|
|
51
51
|
export async function settleWithin(p: Promise<unknown>, ms: number): Promise<boolean> {
|
|
52
52
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
53
53
|
const deadline = new Promise<false>((resolve) => {
|
|
54
|
-
|
|
54
|
+
// A delay past a 32-bit int fires at 1 ms: the longest the timer holds instead.
|
|
55
|
+
timer = setTimeout(() => resolve(false), Math.min(ms, MAX_TIMEOUT_MS))
|
|
55
56
|
})
|
|
56
57
|
try {
|
|
57
58
|
return (await Promise.race([p.then(() => true), deadline])) !== false
|
|
@@ -69,6 +70,37 @@ export async function settleWithin(p: Promise<unknown>, ms: number): Promise<boo
|
|
|
69
70
|
// with the run's verdict (1), so bin.ts asks before it exits.
|
|
70
71
|
let signalOwnsExit = false
|
|
71
72
|
|
|
73
|
+
/** How long `dieOf` waits for its own signal to land before the caller exits anyway. */
|
|
74
|
+
const SELF_SIGNAL_WAIT_MS = 1000
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Die of `signal`: its listeners off, so Bun restores the default action,
|
|
78
|
+
* the signal sent to this process, and the caller held until it lands. A
|
|
79
|
+
* kill is not always delivered before it returns: a signal another thread
|
|
80
|
+
* takes, or one a tracer holds (the sandbox's `strace -f` sees every
|
|
81
|
+
* signal before the tracee does), lands later, and the `process.exit`
|
|
82
|
+
* after it won: vx exited with the signal's code (130), not of the
|
|
83
|
+
* signal, and a script running it
|
|
84
|
+
* went on (CI, PR #4867). Bounded: the caller's exit code is the fallback
|
|
85
|
+
* should the signal never land. `testTimerMs('SELF_SIGNAL_DELAY')` sends
|
|
86
|
+
* it from a helper that late instead, the window a test widens.
|
|
87
|
+
*/
|
|
88
|
+
export function dieOf(signal: 'SIGINT' | 'SIGTERM' | 'SIGHUP'): void {
|
|
89
|
+
process.removeAllListeners(signal)
|
|
90
|
+
const lateMs = testTimerMs('SELF_SIGNAL_DELAY', 0)
|
|
91
|
+
if (lateMs > 0) {
|
|
92
|
+
Bun.spawn(['sh', '-c', `sleep ${lateMs / 1000}; kill -${signal.slice(3)} ${process.pid}`], {
|
|
93
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
94
|
+
})
|
|
95
|
+
} else {
|
|
96
|
+
process.kill(process.pid, signal)
|
|
97
|
+
}
|
|
98
|
+
// Synchronous: an `exit` listener's loop never turns again. A signal
|
|
99
|
+
// that lands ends the process here; the wait costs nothing then.
|
|
100
|
+
const until = performance.now() + lateMs + SELF_SIGNAL_WAIT_MS
|
|
101
|
+
while (performance.now() < until) Bun.sleepSync(5)
|
|
102
|
+
}
|
|
103
|
+
|
|
72
104
|
/** A signal's handler will end the process: bin.ts leaves the exit to it. */
|
|
73
105
|
export function claimExitForSignal(): void {
|
|
74
106
|
signalOwnsExit = true
|
package/src/util/tail.ts
CHANGED
|
@@ -32,8 +32,11 @@ export function createTail(): Tail {
|
|
|
32
32
|
return { chunks: [], chars: 0, dropped: 0 }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function appendTail(t: Tail, chunk: string,
|
|
35
|
+
export function appendTail(t: Tail, chunk: string, cap = PERSISTENT_TAIL_CHARS): void {
|
|
36
36
|
if (chunk.length === 0) return
|
|
37
|
+
// Every bound below is `chars > limit`, which NaN never is: a NaN cap
|
|
38
|
+
// kept everything, the unbounded heap this exists to prevent.
|
|
39
|
+
const limit = Number.isNaN(cap) ? 0 : cap
|
|
37
40
|
t.chunks.push(chunk)
|
|
38
41
|
t.chars += chunk.length
|
|
39
42
|
// Evict WHOLE chunks from the head — no concatenation until the flush
|
package/src/util/timing.ts
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
// prints (a push per stage, ~15 a run): a telemetry sink draws them as
|
|
10
10
|
// spans (`stageTimes`). The per-call `span`s stay off unless enabled.
|
|
11
11
|
|
|
12
|
-
import { detectColors, paint } from './colors.js'
|
|
13
12
|
const enabled = process.env.VX_DEBUG_TIMING === 'true' || process.env.VX_DEBUG_TIMING === '1'
|
|
14
13
|
let t0 = Bun.nanoseconds()
|
|
15
14
|
// The wall clock at t0, read then: `performance.timeOrigin` plus the
|
|
@@ -88,9 +87,9 @@ export function span(label: string): () => void {
|
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
/**
|
|
92
|
-
export function
|
|
93
|
-
if (!enabled || marks.length === 0) return
|
|
90
|
+
/** The stage table's lines (once, at the end of a run); none unless enabled. */
|
|
91
|
+
export function timingLines(): string[] | undefined {
|
|
92
|
+
if (!enabled || marks.length === 0) return undefined
|
|
94
93
|
const width = Math.max(...marks.map(([l]) => l.length))
|
|
95
94
|
let prev = 0
|
|
96
95
|
const lines = ['[vx timing] stage'.padEnd(width + 14) + ' own cumulative']
|
|
@@ -118,16 +117,5 @@ export function printTimings(): void {
|
|
|
118
117
|
' (wall per call, summed; concurrent calls overlap — compare spans, not totals)',
|
|
119
118
|
)
|
|
120
119
|
}
|
|
121
|
-
|
|
122
|
-
process.stderr.write(
|
|
123
|
-
`${lines
|
|
124
|
-
.map((l) =>
|
|
125
|
-
l.startsWith('[vx timing]')
|
|
126
|
-
? `${paint('#06b6d4', '[vx timing]', c, { bold: true })}${paint('', l.slice(11), c, { dim: true })}`
|
|
127
|
-
: l.startsWith(' (')
|
|
128
|
-
? paint('', l, c, { dim: true })
|
|
129
|
-
: l,
|
|
130
|
-
)
|
|
131
|
-
.join('\n')}\n`,
|
|
132
|
-
)
|
|
120
|
+
return lines
|
|
133
121
|
}
|