@vzn/vx 0.0.636 → 0.0.638
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 +39 -11
- package/docs/caching.md +246 -213
- package/docs/cli.md +791 -705
- package/docs/execution.md +68 -76
- package/docs/features.md +47 -42
- package/docs/patterns.md +26 -25
- package/docs/schema.md +392 -205
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +4 -4
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +28 -13
- package/src/bin.ts +30 -9
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +124 -53
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +15 -1
- package/src/cache/inputs.ts +102 -36
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +15 -18
- package/src/cache/layered-cache.ts +20 -102
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +32 -27
- package/src/cache/run-history.ts +104 -14
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +58 -31
- package/src/cache/scoped-cache.ts +237 -0
- package/src/cli/cache.ts +173 -34
- package/src/cli/completions.ts +11 -7
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +110 -70
- package/src/cli/index.ts +67 -24
- package/src/cli/info.ts +43 -27
- package/src/cli/init.ts +115 -407
- package/src/cli/last.ts +157 -9
- package/src/cli/lock.ts +356 -122
- package/src/cli/plan-format.ts +7 -5
- package/src/cli/plugin-commands.ts +81 -14
- package/src/cli/plugin-templates.ts +11 -11
- package/src/cli/run.ts +116 -160
- package/src/cli/select.ts +50 -47
- package/src/cli/show.ts +83 -59
- package/src/cli/task-verb.ts +6 -13
- package/src/cli/upgrade.ts +415 -149
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +39 -61
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +18 -4
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +227 -230
- package/src/cli/why.ts +38 -18
- package/src/cli/workspace-config.ts +33 -70
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +27 -7
- package/src/graph/task-graph.ts +76 -24
- package/src/handoff.ts +169 -0
- package/src/index.ts +35 -58
- package/src/orchestrator/affected-tasks.ts +15 -8
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +147 -69
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +9 -5
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +126 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +20 -5
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +80 -41
- package/src/orchestrator/metrics.ts +78 -9
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +100 -66
- package/src/orchestrator/plugin.ts +173 -67
- package/src/orchestrator/prepare.ts +163 -78
- package/src/orchestrator/projects.ts +105 -99
- package/src/orchestrator/remote-prefetch.ts +11 -6
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +244 -119
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +49 -7
- package/src/orchestrator/summary.ts +115 -106
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +4 -4
- package/src/settings.ts +428 -0
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +33 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +16 -3
- package/src/util/paths.ts +10 -0
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ui.ts +186 -0
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +84 -232
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +65 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +307 -215
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +363 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/src/util/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ export {
|
|
|
13
13
|
isPermissionError,
|
|
14
14
|
isUserError,
|
|
15
15
|
PERMISSION_HINT,
|
|
16
|
+
refusal,
|
|
17
|
+
type RefusalParts,
|
|
16
18
|
UserError,
|
|
17
19
|
gitSpawnRefusal,
|
|
18
20
|
isExecutableMissing,
|
|
@@ -21,7 +23,12 @@ export {
|
|
|
21
23
|
OUT_OF_FDS_HINT,
|
|
22
24
|
wantsJson,
|
|
23
25
|
} from './errors.js'
|
|
24
|
-
export { configErrorFrame } from './config-frame.js'
|
|
26
|
+
export { configErrorFrame, configRefusal } from './config-frame.js'
|
|
27
|
+
export { detectColors, ERROR, paint, SUCCESS, WARN, type ColorSupport } from './colors.js'
|
|
28
|
+
export { gradientRule } from './report.js'
|
|
29
|
+
import * as ui from './ui.js'
|
|
30
|
+
export { refusalNotice, sayLine, sayNotice } from './refusal-notice.js'
|
|
31
|
+
export { ui }
|
|
25
32
|
export { xxh3, xxh3hex } from './hash.js'
|
|
26
33
|
export {
|
|
27
34
|
beginRun,
|
|
@@ -45,6 +52,8 @@ export {
|
|
|
45
52
|
outputMatcher,
|
|
46
53
|
printable,
|
|
47
54
|
relPosix,
|
|
55
|
+
DOT_VX_GITATTRIBUTES,
|
|
56
|
+
DOT_VX_GITIGNORE,
|
|
48
57
|
splitNegations,
|
|
49
58
|
staticPrefix,
|
|
50
59
|
stripTrailingSlash,
|
|
@@ -58,10 +67,12 @@ export {
|
|
|
58
67
|
claimExitForSignal,
|
|
59
68
|
exitClaimedBySignal,
|
|
60
69
|
killGraceMs,
|
|
70
|
+
setSettleBounds,
|
|
71
|
+
type SettleBound,
|
|
61
72
|
settleWithin,
|
|
62
73
|
teardownTimeoutMs,
|
|
63
74
|
} from './settle.js'
|
|
64
|
-
export { formatBytes, parseDuration, parseSize } from './size.js'
|
|
75
|
+
export { formatBytes, parseDuration, parseSize, treeBytes } from './size.js'
|
|
65
76
|
export {
|
|
66
77
|
cgroupCpuQuota,
|
|
67
78
|
cgroupMemoryLimitBytes,
|
|
@@ -73,11 +84,13 @@ export { appendTail, createTail, resetTail, tailText, type Tail } from './tail.j
|
|
|
73
84
|
export { ulid } from './ulid.js'
|
|
74
85
|
export { splitTaskId } from './task-id.js'
|
|
75
86
|
export { editDistance, listed, nearMatches, nearest } from './edit-distance.js'
|
|
76
|
-
export { CORE_VERBS
|
|
87
|
+
export { CORE_VERBS } from './verbs.js'
|
|
77
88
|
export { isUnsupportedBun, MIN_BUN, unsupportedBunMessage } from './bun-version.js'
|
|
78
89
|
export { executablePath, shellArgv, taskShell } from './which.js'
|
|
79
90
|
export { procfsIsOwn } from './procfs.js'
|
|
80
91
|
export { hangupIgnored } from './hangup.js'
|
|
92
|
+
export { ESSENTIAL_ENV } from './essential-env.js'
|
|
93
|
+
export { gitEnv, helperEnv, hideEnvFromTasks } from './hide-env.js'
|
|
81
94
|
export { realPath, realpathOf } from './real-path.js'
|
|
82
95
|
export {
|
|
83
96
|
maskedCommand,
|
package/src/util/paths.ts
CHANGED
|
@@ -11,6 +11,16 @@ export function toPosix(p: string): string {
|
|
|
11
11
|
return IS_POSIX ? p : p.split(path.sep).join('/')
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* `.vx/.gitignore`: git ignores the machine-local `.vx/cache` and nothing
|
|
16
|
+
* else in `.vx`, so the workspace config and the lock beside it are
|
|
17
|
+
* committed. `vx init` and the cache write it when missing.
|
|
18
|
+
*/
|
|
19
|
+
export const DOT_VX_GITIGNORE = '/cache\n'
|
|
20
|
+
|
|
21
|
+
/** `.vx/.gitattributes`' text: GitHub collapses the machine lock in a diff. */
|
|
22
|
+
export const DOT_VX_GITATTRIBUTES = 'projects.lock.json linguist-generated=true\n'
|
|
23
|
+
|
|
14
24
|
export function relPosix(from: string, to: string): string {
|
|
15
25
|
return toPosix(path.relative(from, to))
|
|
16
26
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Every refusal vx prints, as one `ui.notice`: the parts a refusal
|
|
2
|
+
// carries, a config refusal's frame, or a plain message laid out a fact a
|
|
3
|
+
// line. The flat message stays what `--format json` and logs carry.
|
|
4
|
+
|
|
5
|
+
import { configRefusal } from './config-frame.js'
|
|
6
|
+
import type { RefusalParts } from './errors.js'
|
|
7
|
+
import { notice, print, type Block, type Inline, type Tone } from './ui.js'
|
|
8
|
+
|
|
9
|
+
// A sentence break before a capital, a backtick or a paren: vx joins
|
|
10
|
+
// its facts with these.
|
|
11
|
+
const BREAK = /(?<=[.?!])\s+(?=[A-Z`(])/
|
|
12
|
+
// `(see \`vx run --help\`)`: where to read more.
|
|
13
|
+
const SEE = /\s*\(see `([^`]+)`\)/
|
|
14
|
+
// `(did you mean --concurrency?)`, `. Did you mean build?`: the fix.
|
|
15
|
+
const MEANT = /\s*(?:\(did you mean ([^?]+)\?\)|(?:^|[.;—]\s*)[Dd]id you mean ([^?]+)\?)/
|
|
16
|
+
|
|
17
|
+
const bare = (s: string): string => s.trim().replace(/[.:]$/, '')
|
|
18
|
+
|
|
19
|
+
/** A plain message in parts: its first sentence heads, each later one is a line. */
|
|
20
|
+
export function messageParts(message: string): RefusalParts {
|
|
21
|
+
const [head = '', ...rest] = message.split('\n')
|
|
22
|
+
let first = head
|
|
23
|
+
const see = SEE.exec(first)
|
|
24
|
+
if (see !== null) first = first.replace(SEE, '')
|
|
25
|
+
const meant = MEANT.exec(first)
|
|
26
|
+
if (meant !== null) first = first.replace(MEANT, '')
|
|
27
|
+
const sentences = first
|
|
28
|
+
.split(BREAK)
|
|
29
|
+
.map(bare)
|
|
30
|
+
.filter((s) => s !== '')
|
|
31
|
+
const [headline = bare(first), ...what] = sentences
|
|
32
|
+
for (const l of rest) {
|
|
33
|
+
if (l.trim() !== '') what.push(...l.replace(/^ {2}/, '').split(BREAK).map(bare))
|
|
34
|
+
}
|
|
35
|
+
const fix = meant !== null ? (meant[1] ?? meant[2])!.trim() : undefined
|
|
36
|
+
return {
|
|
37
|
+
headline,
|
|
38
|
+
...(what.length > 0 ? { what } : {}),
|
|
39
|
+
...(see !== null ? { why: `see ${see[1]}` } : {}),
|
|
40
|
+
...(fix !== undefined ? { fix } : {}),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `✗ <verb> <headline>` with what is wrong, why, and the fix under it.
|
|
46
|
+
* `headline` set: that is the status line and the refusal's own headline
|
|
47
|
+
* joins what is wrong (`vx lock lock not written`).
|
|
48
|
+
*/
|
|
49
|
+
export function refusalNotice(
|
|
50
|
+
verb: string,
|
|
51
|
+
message: string,
|
|
52
|
+
parts?: RefusalParts,
|
|
53
|
+
opts: { headline?: string; cwd?: string } = {},
|
|
54
|
+
): Block {
|
|
55
|
+
const c = parts === undefined ? configRefusal(message, opts.cwd ?? process.cwd()) : undefined
|
|
56
|
+
if (c !== undefined) {
|
|
57
|
+
const what: Inline[] = [{ style: 'bold', text: c.where }, c.problem]
|
|
58
|
+
return notice({
|
|
59
|
+
tone: 'error',
|
|
60
|
+
verb,
|
|
61
|
+
headline: opts.headline ?? 'config refused',
|
|
62
|
+
what,
|
|
63
|
+
...(c.allowed !== undefined ? { why: `allowed: ${c.allowed}` } : {}),
|
|
64
|
+
code: c.frame,
|
|
65
|
+
...(c.fix !== undefined ? { fix: c.fix } : {}),
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
const p = parts ?? messageParts(message)
|
|
69
|
+
const what = opts.headline !== undefined ? [p.headline, ...(p.what ?? [])] : (p.what ?? [])
|
|
70
|
+
return notice({
|
|
71
|
+
tone: p.tone ?? 'error',
|
|
72
|
+
verb,
|
|
73
|
+
headline: opts.headline ?? p.headline,
|
|
74
|
+
...(what.length > 0 ? { what } : {}),
|
|
75
|
+
...(p.why !== undefined ? { why: p.why } : {}),
|
|
76
|
+
...(p.fix !== undefined ? { fix: p.fix } : {}),
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A message of `tone` from `verb`, a fact a line. */
|
|
81
|
+
function messageNotice(tone: Tone, verb: string, message: string): Block {
|
|
82
|
+
const p = messageParts(message)
|
|
83
|
+
return notice({
|
|
84
|
+
tone,
|
|
85
|
+
verb,
|
|
86
|
+
headline: p.headline,
|
|
87
|
+
...(p.what !== undefined ? { what: p.what } : {}),
|
|
88
|
+
...(p.why !== undefined ? { why: p.why } : {}),
|
|
89
|
+
...(p.fix !== undefined ? { fix: p.fix } : {}),
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A line that may open with its verb (`vx why: …`), as `messageNotice` draws it. */
|
|
94
|
+
export function lineNotice(tone: Tone, line: string, verb = 'vx'): Block {
|
|
95
|
+
const named = /^(vx(?: [a-z][\w-]*)?): /.exec(line)
|
|
96
|
+
return messageNotice(
|
|
97
|
+
tone,
|
|
98
|
+
named?.[1] ?? verb,
|
|
99
|
+
named === null ? line : line.slice(named[0].length),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `messageNotice` on stderr. */
|
|
104
|
+
export function sayNotice(tone: Tone, verb: string, message: string): void {
|
|
105
|
+
print('err', messageNotice(tone, verb, message))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `lineNotice` on stderr. */
|
|
109
|
+
export function sayLine(tone: Tone, line: string, verb = 'vx'): void {
|
|
110
|
+
print('err', lineNotice(tone, line, verb))
|
|
111
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { paint, type ColorSupport } from './colors.js'
|
|
2
|
+
|
|
3
|
+
// Brand gradient for the rule: identity violet → pink (the project /
|
|
4
|
+
// task hues), faded across the dash run. Plain dashes when colors
|
|
5
|
+
// are off.
|
|
6
|
+
const GRADIENT_FROM = [0xa7, 0x8b, 0xfa] as const
|
|
7
|
+
const GRADIENT_TO = [0xf4, 0x72, 0xb6] as const
|
|
8
|
+
// Rule ends flush with the bars: 2 indent + 8 label + 2 gap + 50 cells.
|
|
9
|
+
const RULE_DASHES = 57
|
|
10
|
+
|
|
11
|
+
/** Total visible rule width: `─ <mark> ` + dashes. Shared by the run summary and every report. */
|
|
12
|
+
export function gradientRule(colors: ColorSupport, mark = 'vx'): string {
|
|
13
|
+
const head = `─ `
|
|
14
|
+
const dashes = '─'.repeat(Math.max(8, RULE_DASHES + 2 - mark.length))
|
|
15
|
+
if (!colors.enabled) return `${head}${mark} ${dashes}`
|
|
16
|
+
const chunks = 8
|
|
17
|
+
const per = Math.ceil(RULE_DASHES / chunks)
|
|
18
|
+
let out = ''
|
|
19
|
+
for (let i = 0; i < chunks; i++) {
|
|
20
|
+
const seg = dashes.slice(i * per, (i + 1) * per)
|
|
21
|
+
if (seg.length === 0) break
|
|
22
|
+
const f = i / (chunks - 1)
|
|
23
|
+
const hex = `#${GRADIENT_FROM.map((c, j) =>
|
|
24
|
+
Math.round(c + (GRADIENT_TO[j]! - c) * f)
|
|
25
|
+
.toString(16)
|
|
26
|
+
.padStart(2, '0'),
|
|
27
|
+
).join('')}`
|
|
28
|
+
out += paint(hex, seg, colors)
|
|
29
|
+
}
|
|
30
|
+
return `${paint(`#${GRADIENT_FROM.map((c) => c.toString(16).padStart(2, '0')).join('')}`, head, colors)}${paint('', mark, colors, { bold: true })} ${out}`
|
|
31
|
+
}
|
package/src/util/settle.ts
CHANGED
|
@@ -2,49 +2,68 @@
|
|
|
2
2
|
// wrote; it must not hold the run's exit hostage.
|
|
3
3
|
|
|
4
4
|
import { MAX_TIMEOUT_MS } from './num.js'
|
|
5
|
+
import { parseDuration } from './size.js'
|
|
5
6
|
|
|
6
7
|
/** Default upper bound on one end-of-run flush/teardown await. */
|
|
7
8
|
const DEFAULT_TIMEOUT_MS = 3000
|
|
8
9
|
|
|
10
|
+
/** A bound the run resolved from its flag or `.vx/workspace`. */
|
|
11
|
+
export interface SettleBound {
|
|
12
|
+
readonly ms: number
|
|
13
|
+
readonly from: 'flag' | 'workspace'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let teardownSet: SettleBound | undefined
|
|
17
|
+
let killGraceSet: SettleBound | undefined
|
|
18
|
+
|
|
9
19
|
/**
|
|
10
|
-
* The
|
|
11
|
-
*
|
|
20
|
+
* The run's `run.teardownTimeout` and `run.killGrace` from its flag or
|
|
21
|
+
* workspace (the orchestrator resolves them; util sits below settings).
|
|
22
|
+
* The variable, read per call, sits between the two, so a test drives the
|
|
23
|
+
* deadline instead of waiting it out.
|
|
12
24
|
*/
|
|
13
|
-
export function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
export function setSettleBounds(b: { teardown?: SettleBound; killGrace?: SettleBound }): void {
|
|
26
|
+
teardownSet = b.teardown
|
|
27
|
+
killGraceSet = b.killGrace
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A flag's bound, else the variable's (a positive duration inside the
|
|
32
|
+
* timer's range: the CLI refuses any other before a run, and an embedder's
|
|
33
|
+
* falls back), else the workspace's.
|
|
34
|
+
*/
|
|
35
|
+
function bound(name: string, set: SettleBound | undefined, zero = false): number | undefined {
|
|
36
|
+
if (set?.from === 'flag') return set.ms
|
|
37
|
+
const raw = process.env[name]
|
|
38
|
+
if (raw !== undefined && raw !== '') {
|
|
39
|
+
const n = parseDuration(raw)
|
|
40
|
+
if (n !== null && (n > 0 || zero) && n <= MAX_TIMEOUT_MS) return n
|
|
26
41
|
}
|
|
27
|
-
return
|
|
42
|
+
return set?.ms
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
/**
|
|
31
|
-
* The
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
46
|
+
* The bound on one end-of-run flush or teardown (`run.teardownTimeout`).
|
|
47
|
+
* There is no "no limit": the deadline exists so a plugin's flush cannot
|
|
48
|
+
* hold the run's exit hostage, so a timer past the platform's range (which
|
|
49
|
+
* would fire at once and drop every buffered record) is no value.
|
|
50
|
+
*/
|
|
51
|
+
export function teardownTimeoutMs(): number {
|
|
52
|
+
return bound('VX_RUN_TEARDOWN_TIMEOUT', teardownSet, true) ?? DEFAULT_TIMEOUT_MS
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The SIGTERM→SIGKILL grace a run grants a child that ignores SIGTERM
|
|
57
|
+
* (`run.killGrace`): the one-shot timeout escalation (`exec/runner.ts`),
|
|
58
|
+
* the signal teardown (`orchestrator/signals.ts`) and the end-of-run
|
|
59
|
+
* persistent shutdown (`orchestrator/persistent.ts`) share it. Two seconds
|
|
35
60
|
* is generous for a real server's cleanup and is what every run waits when
|
|
36
|
-
* a child wedges
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* a third of the suite's wall time for a claim a 200 ms grace proves too.
|
|
61
|
+
* a child wedges; the tests that prove the escalation used to wait the
|
|
62
|
+
* full two seconds each, a third of the suite's wall time for a claim a
|
|
63
|
+
* 200 ms grace proves too.
|
|
40
64
|
*/
|
|
41
65
|
export function killGraceMs(defaultMs: number): number {
|
|
42
|
-
|
|
43
|
-
if (raw !== undefined && /^[0-9]+$/.test(raw)) {
|
|
44
|
-
const n = Number(raw)
|
|
45
|
-
if (n > 0 && n <= MAX_TIMEOUT_MS) return n
|
|
46
|
-
}
|
|
47
|
-
return defaultMs
|
|
66
|
+
return bound('VX_RUN_KILL_GRACE', killGraceSet) ?? defaultMs
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
/**
|
package/src/util/size.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
1
3
|
import { parseDecimalInt } from './num.js'
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -20,25 +22,34 @@ export function parseSize(input: string): number | null {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
|
-
* Parse `<n><unit>` where unit is s/m/h/d, case-insensitively (`parseSize`
|
|
25
|
+
* Parse `<n><unit>` where unit is ms/s/m/h/d, case-insensitively (`parseSize`
|
|
24
26
|
* has always been case-insensitive; this matched only lowercase, so `30D`
|
|
25
27
|
* was rejected while `1GB` and `1gb` both worked).
|
|
26
28
|
*/
|
|
27
29
|
export function parseDuration(input: string): number | null {
|
|
28
|
-
const m = input.match(/^(\d+)([smhd])$/i)
|
|
30
|
+
const m = input.match(/^(\d+)(ms|[smhd])$/i)
|
|
29
31
|
if (!m) return null
|
|
30
32
|
const n = parseDecimalInt(m[1]!)
|
|
31
33
|
if (n === null) return null
|
|
32
34
|
// Lowercase before the switch: with the /i flag an uppercase `M` would
|
|
33
35
|
// otherwise fall through to the days branch.
|
|
34
36
|
const unit = m[2]!.toLowerCase()
|
|
35
|
-
const mult =
|
|
37
|
+
const mult =
|
|
38
|
+
unit === 'ms'
|
|
39
|
+
? 1
|
|
40
|
+
: unit === 's'
|
|
41
|
+
? 1000
|
|
42
|
+
: unit === 'm'
|
|
43
|
+
? 60_000
|
|
44
|
+
: unit === 'h'
|
|
45
|
+
? 3_600_000
|
|
46
|
+
: 86_400_000
|
|
36
47
|
return n * mult
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/**
|
|
40
51
|
* Human-readable byte size with KB/MB/GB/TB/PB suffixes and one
|
|
41
|
-
* decimal of precision below 10 of any unit. Used by `vx info`, `vx cache
|
|
52
|
+
* decimal of precision below 10 of any unit. Used by `vx info`, `vx cache clean` and the
|
|
42
53
|
* run's `cacheRetention` line.
|
|
43
54
|
*/
|
|
44
55
|
export function formatBytes(n: number): string {
|
|
@@ -51,3 +62,11 @@ export function formatBytes(n: number): string {
|
|
|
51
62
|
}
|
|
52
63
|
return `${v.toFixed(0)} PB`
|
|
53
64
|
}
|
|
65
|
+
|
|
66
|
+
/** Bytes under `p` on disk, links not followed; 0 when it is missing. */
|
|
67
|
+
export function treeBytes(p: string): number {
|
|
68
|
+
const st = lstatSync(p, { throwIfNoEntry: false })
|
|
69
|
+
if (st === undefined) return 0
|
|
70
|
+
if (!st.isDirectory()) return st.size
|
|
71
|
+
return readdirSync(p).reduce((n, e) => n + treeBytes(path.join(p, e)), 0)
|
|
72
|
+
}
|
package/src/util/timing.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Stage timing for the run path, printed to stderr when `
|
|
1
|
+
// Stage timing for the run path, printed to stderr when `VX_DEBUG_TIMING` is
|
|
2
|
+
// true (`debug.timing`; util reads it itself, below src/settings.ts).
|
|
2
3
|
//
|
|
3
4
|
// The CPU profiler attributes a tight loop's cost unreliably and hides
|
|
4
5
|
// where an `await` waited; a stage table answers "where did the warm run
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
// prints (a push per stage, ~15 a run): a telemetry sink draws them as
|
|
9
10
|
// spans (`stageTimes`). The per-call `span`s stay off unless enabled.
|
|
10
11
|
|
|
11
|
-
const enabled = process.env.
|
|
12
|
+
const enabled = process.env.VX_DEBUG_TIMING === 'true' || process.env.VX_DEBUG_TIMING === '1'
|
|
12
13
|
let t0 = Bun.nanoseconds()
|
|
13
14
|
// The wall clock at t0, read then: `performance.timeOrigin` plus the
|
|
14
15
|
// monotonic clock drifted 34 ms from `Date.now()` on macOS CI, so stages
|
package/src/util/ui.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// vx's output components (org/devex/log-design.md). A verb or a plugin
|
|
2
|
+
// composes these instead of writing raw lines: each component owns its
|
|
3
|
+
// color, indent and spacing, so every surface shares one look. Colors
|
|
4
|
+
// are decided at print time, per stream.
|
|
5
|
+
|
|
6
|
+
import { detectColors, ERROR, paint, SUCCESS, WARN, type ColorSupport } from './colors.js'
|
|
7
|
+
import { gradientRule } from './report.js'
|
|
8
|
+
|
|
9
|
+
export type Tone = 'ok' | 'warn' | 'error' | 'info'
|
|
10
|
+
|
|
11
|
+
const INFO = '#06b6d4'
|
|
12
|
+
const PROJECT = '#a78bfa'
|
|
13
|
+
const TASK = '#f472b6'
|
|
14
|
+
const HUE: Record<Tone, string> = { ok: SUCCESS, warn: WARN, error: ERROR, info: INFO }
|
|
15
|
+
const SYMBOL: Record<Tone, string> = { ok: '✓', warn: '!', error: '✗', info: '›' }
|
|
16
|
+
const INDENT = ' '
|
|
17
|
+
const LABEL_MIN = 8
|
|
18
|
+
|
|
19
|
+
type Style = Tone | 'dim' | 'bold' | 'id'
|
|
20
|
+
|
|
21
|
+
/** Styled text inside a component: a plain string, a token, or a run of them. */
|
|
22
|
+
export type Inline = string | { readonly style: Style; readonly text: string } | readonly Inline[]
|
|
23
|
+
|
|
24
|
+
/** A laid-out piece of output; `render` and `print` space blocks apart. */
|
|
25
|
+
export interface Block {
|
|
26
|
+
readonly lines: (colors: ColorSupport) => string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A `project#task` id, in the run's project and task hues. */
|
|
30
|
+
export const id = (text: string): Inline => ({ style: 'id', text })
|
|
31
|
+
/** A path, glob or file: bold. */
|
|
32
|
+
export const path = (text: string): Inline => ({ style: 'bold', text })
|
|
33
|
+
/** A command to type: bold. */
|
|
34
|
+
export const cmd = (text: string): Inline => ({ style: 'bold', text })
|
|
35
|
+
/** A secondary fact. */
|
|
36
|
+
export const dim = (text: string): Inline => ({ style: 'dim', text })
|
|
37
|
+
/** Text in a tone's color. */
|
|
38
|
+
export const tone = (t: Tone, text: string): Inline => ({ style: t, text })
|
|
39
|
+
|
|
40
|
+
function inline(x: Inline, c: ColorSupport): string {
|
|
41
|
+
if (typeof x === 'string') return x
|
|
42
|
+
if (Array.isArray(x)) return (x as readonly Inline[]).map((p) => inline(p, c)).join('')
|
|
43
|
+
const { style, text } = x as { style: Style; text: string }
|
|
44
|
+
switch (style) {
|
|
45
|
+
case 'dim':
|
|
46
|
+
return paint('', text, c, { dim: true })
|
|
47
|
+
case 'bold':
|
|
48
|
+
return paint('', text, c, { bold: true })
|
|
49
|
+
case 'id': {
|
|
50
|
+
const hash = text.lastIndexOf('#')
|
|
51
|
+
if (hash < 0) return paint(PROJECT, text, c)
|
|
52
|
+
return paint(PROJECT, text.slice(0, hash), c) + paint(TASK, text.slice(hash), c)
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
return paint(HUE[style], text, c)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const block = (lines: (c: ColorSupport) => string[]): Block => ({ lines })
|
|
60
|
+
const lines = (x: Inline, c: ColorSupport): string[] => inline(x, c).split('\n')
|
|
61
|
+
const visible = (s: string): number => Bun.stringWidth(s)
|
|
62
|
+
|
|
63
|
+
/** `✗ vx lock lock not written`: the tone's symbol, the bold verb, the headline. */
|
|
64
|
+
export function status(t: Tone, verb: string, headline: Inline): Block {
|
|
65
|
+
return block((c) => [
|
|
66
|
+
`${paint(HUE[t], SYMBOL[t], c)} ${paint('', verb, c, { bold: true })} ${inline(headline, c)}`,
|
|
67
|
+
])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Indented short lines: what is wrong, or what happened. */
|
|
71
|
+
export function text(...items: Inline[]): Block {
|
|
72
|
+
return block((c) => items.flatMap((i) => lines(i, c).map((l) => `${INDENT}${l}`)))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** An indented dim line: why it matters. */
|
|
76
|
+
export function why(line: Inline): Block {
|
|
77
|
+
return block((c) => lines(line, c).map((l) => `${INDENT}${paint('', l, c, { dim: true })}`))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A label and its values; values after the first sit under it. `null` is a blank line. */
|
|
81
|
+
export type FieldRow = readonly [label: string, ...values: Inline[]] | null
|
|
82
|
+
|
|
83
|
+
/** A dim label column with the values aligned beside it; `next` and `fix` are bold. */
|
|
84
|
+
export function fields(rows: readonly FieldRow[]): Block {
|
|
85
|
+
return block((c) => {
|
|
86
|
+
const width = Math.max(LABEL_MIN, ...rows.map((r) => (r === null ? 0 : r[0].length)))
|
|
87
|
+
const out: string[] = []
|
|
88
|
+
for (const row of rows) {
|
|
89
|
+
if (row === null) {
|
|
90
|
+
out.push('')
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
const [label, ...values] = row
|
|
94
|
+
const strong = label === 'next' || label === 'fix'
|
|
95
|
+
values
|
|
96
|
+
.flatMap((v) => lines(v, c))
|
|
97
|
+
.forEach((v, i) => {
|
|
98
|
+
const head =
|
|
99
|
+
i === 0 ? paint('', label.padEnd(width), c, { dim: true }) : ' '.repeat(width)
|
|
100
|
+
const value = strong ? paint('', v, c, { bold: true }) : v
|
|
101
|
+
out.push(`${INDENT}${head} ${value}`.trimEnd())
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
return out
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `fix <action>`: what to do about it. */
|
|
109
|
+
export function fix(action: Inline): Block {
|
|
110
|
+
return labelled('fix', action)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `next <command>`: the command to type next. */
|
|
114
|
+
export function next(command: Inline): Block {
|
|
115
|
+
return labelled('next', command)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function labelled(label: string, value: Inline): Block {
|
|
119
|
+
return block((c) => [
|
|
120
|
+
`${INDENT}${paint('', label, c, { dim: true })} ${paint('', inline(value, c), c, { bold: true })}`,
|
|
121
|
+
])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A report: a blank line, the brand rule naming `mark`, then its fields. */
|
|
125
|
+
export function report(mark: string, rows: readonly FieldRow[]): Block {
|
|
126
|
+
return block((c) => ['', gradientRule(c, mark), ...fields(rows).lines(c)])
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Aligned columns under a dim header. */
|
|
130
|
+
export function table(columns: readonly string[], rows: readonly (readonly Inline[])[]): Block {
|
|
131
|
+
return block((c) => {
|
|
132
|
+
const cells = rows.map((r) => r.map((x) => inline(x, c)))
|
|
133
|
+
const widths = columns.map((h, i) =>
|
|
134
|
+
Math.max(h.length, ...cells.map((r) => visible(r[i] ?? ''))),
|
|
135
|
+
)
|
|
136
|
+
const pad = (s: string, i: number): string =>
|
|
137
|
+
i === columns.length - 1 ? s : s + ' '.repeat(widths[i]! - visible(s))
|
|
138
|
+
return [
|
|
139
|
+
`${INDENT}${paint('', columns.map((h, i) => pad(h, i)).join(' '), c, { dim: true })}`.trimEnd(),
|
|
140
|
+
...cells.map((r) => `${INDENT}${r.map(pad).join(' ')}`.trimEnd()),
|
|
141
|
+
]
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A source frame (`> 1 | …` and its caret), indented. */
|
|
146
|
+
export function code(frame: string): Block {
|
|
147
|
+
return block(() => frame.split('\n').map((l) => (l === '' ? '' : `${INDENT}${l}`)))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface NoticeParts {
|
|
151
|
+
tone: Tone
|
|
152
|
+
verb: string
|
|
153
|
+
headline: Inline
|
|
154
|
+
/** What is wrong, one fact a line. */
|
|
155
|
+
what?: readonly Inline[]
|
|
156
|
+
why?: Inline
|
|
157
|
+
/** A source frame, below the facts. */
|
|
158
|
+
code?: string
|
|
159
|
+
fix?: Inline
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A refusal or a warning: status, then what and why, the frame, and the fix, a blank line apart. */
|
|
163
|
+
export function notice(p: NoticeParts): Block {
|
|
164
|
+
return block((c) => {
|
|
165
|
+
const parts: Block[] = [status(p.tone, p.verb, p.headline)]
|
|
166
|
+
const body = [
|
|
167
|
+
...(p.what !== undefined ? [text(...p.what)] : []),
|
|
168
|
+
...(p.why !== undefined ? [why(p.why)] : []),
|
|
169
|
+
]
|
|
170
|
+
if (body.length > 0) parts.push(block((cc) => body.flatMap((b) => b.lines(cc))))
|
|
171
|
+
if (p.code !== undefined) parts.push(code(p.code))
|
|
172
|
+
if (p.fix !== undefined) parts.push(fix(p.fix))
|
|
173
|
+
return parts.flatMap((b, i) => (i === 0 ? b.lines(c) : ['', ...b.lines(c)]))
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The blocks as text, a blank line apart, ending in a newline. */
|
|
178
|
+
export function render(blocks: readonly Block[], colors: ColorSupport): string {
|
|
179
|
+
return `${blocks.map((b) => b.lines(colors).join('\n')).join('\n\n')}\n`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Write the blocks to stdout (`out`) or stderr (`err`), colored for that stream. */
|
|
183
|
+
export function print(stream: 'out' | 'err', ...blocks: Block[]): void {
|
|
184
|
+
const s = stream === 'out' ? process.stdout : process.stderr
|
|
185
|
+
s.write(render(blocks, detectColors(s)))
|
|
186
|
+
}
|
package/src/util/ulid.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Run-id generator. Thin wrapper over `Bun.randomUUIDv7()` so the
|
|
2
2
|
// `runId` column on `runs` carries a timestamp-ordered identifier —
|
|
3
|
-
//
|
|
3
|
+
// time-range queries can range-scan without an index on the
|
|
4
4
|
// time column, and the IDs lexicographically sort by creation time
|
|
5
5
|
// for grouping.
|
|
6
6
|
//
|
package/src/util/verbs.ts
CHANGED
|
@@ -19,18 +19,3 @@ export const CORE_VERBS = [
|
|
|
19
19
|
'help',
|
|
20
20
|
'version',
|
|
21
21
|
] as const
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Verbs core owned once and a package owns now. Not core verbs: a plugin
|
|
25
|
-
* may declare them, and the dispatcher prints the
|
|
26
|
-
* pointer only when no declared plugin claimed the name.
|
|
27
|
-
*/
|
|
28
|
-
export const MOVED_VERBS: Readonly<Record<string, string>> = {
|
|
29
|
-
migrate:
|
|
30
|
-
'vx migrate moved to @vzn/vx-migrate: run `bunx @vzn/vx-migrate` (turbo.json or an Nx ' +
|
|
31
|
-
'project graph → vx.config.ts); `vx init` reads package.json scripts',
|
|
32
|
-
prune:
|
|
33
|
-
'vx prune comes from @vzn/vx-lockfile: declare pnpm(), bun(), npm() or yarn() in ' +
|
|
34
|
-
'vx.workspace.ts',
|
|
35
|
-
stats: 'vx stats was removed (H-19): run `vx info`, which prints the same report',
|
|
36
|
-
}
|