@vzn/vx 0.0.649 → 0.0.651
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 +5 -20
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/src/bin.ts +9 -31
- package/src/cache/cache.ts +225 -51
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/fold-memo.ts +8 -1
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/index.ts +12 -0
- package/src/cache/inputs.ts +3 -0
- package/src/cache/layer.ts +11 -0
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +120 -0
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-hits.ts +9 -1
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +11 -1
- package/src/cache/run-notes.ts +123 -0
- package/src/cache/schema.ts +54 -1
- package/src/cli/cache.ts +26 -16
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +12 -23
- package/src/cli/index.ts +4 -9
- package/src/cli/info.ts +15 -7
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +224 -223
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +10 -3
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +38 -42
- 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 +2 -1
- package/src/cli/watch.ts +83 -44
- package/src/cli/why.ts +126 -92
- package/src/cli/workspace-config.ts +2 -1
- package/src/exec/executor.ts +2 -1
- 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-runtime.ts +11 -2
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +12 -2
- package/src/graph/task-graph.ts +27 -1
- package/src/handoff.ts +10 -7
- package/src/history/history.ts +39 -2
- package/src/history/index.ts +3 -0
- package/src/history/metrics.ts +9 -1
- package/src/index.ts +12 -3
- package/src/orchestrator/doctor.ts +3 -3
- package/src/orchestrator/execute-task.ts +16 -2
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +3 -0
- package/src/orchestrator/index.ts +3 -1
- package/src/orchestrator/live-usage.ts +98 -0
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +176 -102
- package/src/orchestrator/miss-save.ts +31 -3
- package/src/orchestrator/persistent-task.ts +3 -1
- package/src/orchestrator/plan.ts +5 -1
- package/src/orchestrator/plugin-host.ts +61 -0
- package/src/orchestrator/plugin.ts +49 -4
- package/src/orchestrator/prepare.ts +18 -2
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +35 -114
- package/src/orchestrator/run-records.ts +10 -0
- package/src/orchestrator/run.ts +89 -26
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-hash.ts +17 -3
- package/src/report/forecast.ts +12 -33
- package/src/report/framed-output.ts +388 -161
- package/src/report/index.ts +8 -2
- package/src/report/logger.ts +100 -36
- package/src/report/status-line.ts +81 -365
- package/src/report/summary.ts +104 -131
- 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 +65 -10
- package/src/util/edit-distance.ts +6 -1
- package/src/util/hidden-prompt.ts +8 -2
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +20 -2
- package/src/util/secret-mask.ts +39 -3
- package/src/util/secrets.ts +3 -0
- package/src/util/settle.ts +31 -0
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/config-schema.ts +4 -2
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +28 -0
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +4 -3
- package/src/workspace/workspace.ts +3 -0
- package/src/orchestrator/run-steps.ts +0 -93
- package/src/util/colors.ts +0 -130
- 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
package/src/util/colors.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// Tiny color helper — gates Bun.color through a `ColorSupport` token
|
|
2
|
-
// so the formatters stay pure. The token is computed once at logger
|
|
3
|
-
// construction; tests pass `{ enabled: false }` so assertions match
|
|
4
|
-
// plain strings.
|
|
5
|
-
//
|
|
6
|
-
// Bun.color('red', 'ansi') is broken in current Bun versions (emits
|
|
7
|
-
// the color number as a raw byte instead of decimal text), so we use
|
|
8
|
-
// 'ansi-16m' which emits 24-bit truecolor sequences. Every modern
|
|
9
|
-
// terminal supports them, and the `tput colors`-style downgrade isn't
|
|
10
|
-
// worth the complexity for our handful of UI accents.
|
|
11
|
-
|
|
12
|
-
const RESET = '\x1b[0m'
|
|
13
|
-
const BOLD = '\x1b[1m'
|
|
14
|
-
const DIM = '\x1b[2m'
|
|
15
|
-
|
|
16
|
-
// State hues every report shares with the run summary.
|
|
17
|
-
export const SUCCESS = '#22c55e'
|
|
18
|
-
export const WARN = '#eab308'
|
|
19
|
-
export const ERROR = '#ef4444'
|
|
20
|
-
|
|
21
|
-
export interface ColorSupport {
|
|
22
|
-
enabled: boolean
|
|
23
|
-
/** The terminal's width, read live; none off a terminal. Bars and rules fit it. */
|
|
24
|
-
readonly columns?: number | undefined
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface PaintOptions {
|
|
28
|
-
bold?: boolean
|
|
29
|
-
dim?: boolean
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Standard env precedence:
|
|
34
|
-
* NO_COLOR non-empty → off (overrides FORCE_COLOR)
|
|
35
|
-
* FORCE_COLOR=0 or =false → off, even on a TTY
|
|
36
|
-
* FORCE_COLOR any other value → on (the empty string too)
|
|
37
|
-
* a CI whose log renders ANSI (`ciPaints`) → on
|
|
38
|
-
* else: on iff stdout is a TTY that is not `TERM=dumb`.
|
|
39
|
-
* `0` and `false` are the convention's "off" (supports-color, chalk,
|
|
40
|
-
* Node); reading every non-empty value as on painted a piped CI log that
|
|
41
|
-
* set `FORCE_COLOR=0` (nx#35292's bug).
|
|
42
|
-
*/
|
|
43
|
-
export function detectColors(stream: NodeJS.WriteStream = process.stdout): ColorSupport {
|
|
44
|
-
const force = process.env['FORCE_COLOR']
|
|
45
|
-
const enabled = process.env['NO_COLOR']
|
|
46
|
-
? false
|
|
47
|
-
: force !== undefined
|
|
48
|
-
? force !== '0' && force !== 'false'
|
|
49
|
-
: ciPaints() || (stream.isTTY === true && !dumbTerminal())
|
|
50
|
-
return {
|
|
51
|
-
enabled,
|
|
52
|
-
get columns() {
|
|
53
|
-
return stream.isTTY === true ? stream.columns : undefined
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* A CI whose log viewer renders ANSI colour though the job's output is a
|
|
60
|
-
* pipe (supports-color reads the same variables): its log was plain.
|
|
61
|
-
*/
|
|
62
|
-
function ciPaints(): boolean {
|
|
63
|
-
const env = process.env
|
|
64
|
-
return (
|
|
65
|
-
env['GITHUB_ACTIONS'] === 'true' ||
|
|
66
|
-
env['GITLAB_CI'] === 'true' ||
|
|
67
|
-
env['BUILDKITE'] === 'true' ||
|
|
68
|
-
env['CIRCLECI'] === 'true'
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* `TERM=dumb` (Emacs `M-x shell`, some CI log viewers) can neither paint
|
|
74
|
-
* nor move the cursor: vx prints to it as off a terminal.
|
|
75
|
-
*/
|
|
76
|
-
export function dumbTerminal(): boolean {
|
|
77
|
-
return process.env['TERM'] === 'dumb'
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Bun.color() is pure for a given (hex, mode) pair but does FFI work
|
|
81
|
-
// on each call. The framed-output renderer paints the same handful of
|
|
82
|
-
// accent colors thousands of times per run; memoizing here turns those
|
|
83
|
-
// into hash-map hits.
|
|
84
|
-
const ansiCache = new Map<string, string>()
|
|
85
|
-
function ansiFor(color: string): string {
|
|
86
|
-
const cached = ansiCache.get(color)
|
|
87
|
-
if (cached !== undefined) return cached
|
|
88
|
-
const v = Bun.color(color, 'ansi-16m') ?? ''
|
|
89
|
-
ansiCache.set(color, v)
|
|
90
|
-
return v
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function paint(
|
|
94
|
-
color: string,
|
|
95
|
-
text: string,
|
|
96
|
-
colors: ColorSupport,
|
|
97
|
-
opts: PaintOptions = {},
|
|
98
|
-
): string {
|
|
99
|
-
// Empty text paints nothing: an escape pair around no text is bytes and no
|
|
100
|
-
// mark (a full bar's empty remainder).
|
|
101
|
-
if (!colors.enabled || text === '') return text
|
|
102
|
-
let prefix = ''
|
|
103
|
-
if (opts.bold) prefix += BOLD
|
|
104
|
-
if (opts.dim) prefix += DIM
|
|
105
|
-
if (color) prefix += ansiFor(color)
|
|
106
|
-
if (prefix.length === 0) return text
|
|
107
|
-
return `${prefix}${text}${RESET}`
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Identity hues for the two halves of a `project#task` id, never a status
|
|
112
|
-
* colour: one name, one hue, in every run, list and row (2026-10-15:
|
|
113
|
-
* every `#build` matches every other `#build`).
|
|
114
|
-
*/
|
|
115
|
-
const IDENTITY_PALETTE = [
|
|
116
|
-
'#a78bfa', // violet-400
|
|
117
|
-
'#60a5fa', // blue-400
|
|
118
|
-
'#e879f9', // fuchsia-400
|
|
119
|
-
'#818cf8', // indigo-400
|
|
120
|
-
'#c084fc', // purple-400
|
|
121
|
-
'#93c5fd', // blue-300
|
|
122
|
-
'#f472b6', // pink-400
|
|
123
|
-
] as const
|
|
124
|
-
|
|
125
|
-
/** A project's or a target's hue: a fold of the name into the palette. */
|
|
126
|
-
export function identityColor(name: string): string {
|
|
127
|
-
let h = 0
|
|
128
|
-
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0
|
|
129
|
-
return IDENTITY_PALETTE[h % IDENTITY_PALETTE.length]!
|
|
130
|
-
}
|
package/src/util/report.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
// The bars' label column: 2 indent + 8 label + 2 gap.
|
|
9
|
-
const LABEL = 12
|
|
10
|
-
|
|
11
|
-
/** A meter's cells: 50, or fewer so the bar fits a narrower terminal. */
|
|
12
|
-
export function barWidth(colors: ColorSupport): number {
|
|
13
|
-
const columns = colors.columns
|
|
14
|
-
return columns === undefined ? 50 : Math.max(10, Math.min(50, columns - LABEL))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** A rule's width, flush with the bars: the footer's and a task frame's section rules. */
|
|
18
|
-
export function ruleWidth(colors: ColorSupport): number {
|
|
19
|
-
return LABEL + barWidth(colors)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** `─ <mark> ` + dashes, flush with the bars. Shared by the run summary and every report. */
|
|
23
|
-
export function gradientRule(colors: ColorSupport, mark = 'vx'): string {
|
|
24
|
-
const head = `─ `
|
|
25
|
-
const width = ruleWidth(colors)
|
|
26
|
-
const dashes = '─'.repeat(Math.max(8, width - 3 - mark.length))
|
|
27
|
-
if (!colors.enabled) return `${head}${mark} ${dashes}`
|
|
28
|
-
const chunks = 8
|
|
29
|
-
// The gradient spans the rule as if its mark were `vx`.
|
|
30
|
-
const per = Math.ceil((width - 5) / chunks)
|
|
31
|
-
let out = ''
|
|
32
|
-
for (let i = 0; i < chunks; i++) {
|
|
33
|
-
const seg = dashes.slice(i * per, (i + 1) * per)
|
|
34
|
-
if (seg.length === 0) break
|
|
35
|
-
const f = i / (chunks - 1)
|
|
36
|
-
const hex = `#${GRADIENT_FROM.map((c, j) =>
|
|
37
|
-
Math.round(c + (GRADIENT_TO[j]! - c) * f)
|
|
38
|
-
.toString(16)
|
|
39
|
-
.padStart(2, '0'),
|
|
40
|
-
).join('')}`
|
|
41
|
-
out += paint(hex, seg, colors)
|
|
42
|
-
}
|
|
43
|
-
return `${paint(`#${GRADIENT_FROM.map((c) => c.toString(16).padStart(2, '0')).join('')}`, head, colors)}${paint('', mark, colors, { bold: true })} ${out}`
|
|
44
|
-
}
|
package/src/util/ui.ts
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
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 {
|
|
7
|
-
detectColors,
|
|
8
|
-
ERROR,
|
|
9
|
-
identityColor,
|
|
10
|
-
paint,
|
|
11
|
-
SUCCESS,
|
|
12
|
-
WARN,
|
|
13
|
-
type ColorSupport,
|
|
14
|
-
} from './colors.js'
|
|
15
|
-
import { gradientRule } from './report.js'
|
|
16
|
-
|
|
17
|
-
export type Tone = 'ok' | 'warn' | 'error' | 'info'
|
|
18
|
-
|
|
19
|
-
const INFO = '#06b6d4'
|
|
20
|
-
const HUE: Record<Tone, string> = { ok: SUCCESS, warn: WARN, error: ERROR, info: INFO }
|
|
21
|
-
const SYMBOL: Record<Tone, string> = { ok: '✓', warn: '!', error: '✗', info: '›' }
|
|
22
|
-
const INDENT = ' '
|
|
23
|
-
const LABEL_MIN = 8
|
|
24
|
-
|
|
25
|
-
type Style = Tone | 'dim' | 'bold' | 'id'
|
|
26
|
-
|
|
27
|
-
/** Styled text inside a component: a plain string, a token, or a run of them. */
|
|
28
|
-
export type Inline = string | { readonly style: Style; readonly text: string } | readonly Inline[]
|
|
29
|
-
|
|
30
|
-
/** A laid-out piece of output; `render` and `print` space blocks apart. */
|
|
31
|
-
export interface Block {
|
|
32
|
-
readonly lines: (colors: ColorSupport) => string[]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** A `project` or `project#task` id, each half in its name's hue, the `#` dim. */
|
|
36
|
-
export const id = (text: string): Inline => ({ style: 'id', text })
|
|
37
|
-
/** A path, glob or file: bold. */
|
|
38
|
-
export const path = (text: string): Inline => ({ style: 'bold', text })
|
|
39
|
-
/** A command to type: bold. */
|
|
40
|
-
export const cmd = (text: string): Inline => ({ style: 'bold', text })
|
|
41
|
-
/** A secondary fact. */
|
|
42
|
-
export const dim = (text: string): Inline => ({ style: 'dim', text })
|
|
43
|
-
/** Where a task runs (`small#1`): its name's hue in every row and block; `local` plain. */
|
|
44
|
-
export const place = (text: string): Inline => (text === 'local' ? text : id(text))
|
|
45
|
-
/** Text in a tone's color. */
|
|
46
|
-
export const tone = (t: Tone, text: string): Inline => ({ style: t, text })
|
|
47
|
-
|
|
48
|
-
// oxlint-disable-next-line no-control-regex
|
|
49
|
-
const CONTROL = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* `s` with every control byte but a tab and a line break as `\xNN`: a
|
|
53
|
-
* notice quotes paths and names, anyone's bytes, and one holding `ESC ]0;`
|
|
54
|
-
* retitled the terminal. The colors are the components' own, added after.
|
|
55
|
-
*/
|
|
56
|
-
const plainText = (s: string): string =>
|
|
57
|
-
s.replace(CONTROL, (ch) => `\\x${ch.charCodeAt(0).toString(16).padStart(2, '0')}`)
|
|
58
|
-
|
|
59
|
-
/** One inline as text, colored for `c`. */
|
|
60
|
-
export function inline(x: Inline, c: ColorSupport): string {
|
|
61
|
-
if (typeof x === 'string') return plainText(x)
|
|
62
|
-
if (Array.isArray(x)) return (x as readonly Inline[]).map((p) => inline(p, c)).join('')
|
|
63
|
-
const { style } = x as { style: Style; text: string }
|
|
64
|
-
const text = plainText((x as { text: string }).text)
|
|
65
|
-
switch (style) {
|
|
66
|
-
case 'dim':
|
|
67
|
-
return paint('', text, c, { dim: true })
|
|
68
|
-
case 'bold':
|
|
69
|
-
return paint('', text, c, { bold: true })
|
|
70
|
-
case 'id': {
|
|
71
|
-
const hash = text.lastIndexOf('#')
|
|
72
|
-
if (hash < 0) return paint(identityColor(text), text, c)
|
|
73
|
-
const task = text.slice(hash + 1)
|
|
74
|
-
return (
|
|
75
|
-
paint(identityColor(text.slice(0, hash)), text.slice(0, hash), c) +
|
|
76
|
-
paint('', '#', c, { dim: true }) +
|
|
77
|
-
paint(identityColor(task), task, c)
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
default:
|
|
81
|
-
return paint(HUE[style], text, c)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const block = (lines: (c: ColorSupport) => string[]): Block => ({ lines })
|
|
86
|
-
const lines = (x: Inline, c: ColorSupport): string[] => inline(x, c).split('\n')
|
|
87
|
-
const visible = (s: string): number => Bun.stringWidth(s)
|
|
88
|
-
|
|
89
|
-
/** `✗ vx lock lock not written`: the tone's symbol, the bold verb, the headline. */
|
|
90
|
-
export function status(t: Tone, verb: string, headline: Inline): Block {
|
|
91
|
-
return block((c) => [
|
|
92
|
-
`${paint(HUE[t], SYMBOL[t], c)} ${paint('', verb, c, { bold: true })} ${inline(headline, c)}`,
|
|
93
|
-
])
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* A line vx says mid-run (`[vx] cache save failed: …`, `vx watch: …`), its
|
|
98
|
-
* words as they always read and its tag in the tone's hue; a line with no
|
|
99
|
-
* tag is painted whole. One string, no newline: the run's own streams write it.
|
|
100
|
-
*/
|
|
101
|
-
export function tagged(t: Tone, line: string, c: ColorSupport): string {
|
|
102
|
-
const body = line.replace(/\n+$/, '')
|
|
103
|
-
const tag = /^(?:\[vx\]|vx(?: [a-z][\w-]*){0,2}:|[\w@/.-]+: )/.exec(body)?.[0].trimEnd()
|
|
104
|
-
return tag === undefined
|
|
105
|
-
? paint(HUE[t], body, c)
|
|
106
|
-
: `${paint(HUE[t], tag, c, { bold: true })}${body.slice(tag.length)}`
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** `tagged` on a stream (stderr unless told), colored for it. */
|
|
110
|
-
export function say(t: Tone, line: string, stream: 'out' | 'err' = 'err'): void {
|
|
111
|
-
const s = stream === 'out' ? process.stdout : process.stderr
|
|
112
|
-
s.write(`${tagged(t, line, detectColors(s))}\n`)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* A file or excerpt shown whole (`vx init --dry`, `vx docs`): its title rule
|
|
117
|
-
* bold, its body behind a dim gutter. Uncolored it is the bare text, so a
|
|
118
|
-
* pipe gets the bytes it would copy.
|
|
119
|
-
*/
|
|
120
|
-
export function excerpt(title: string, body: string): Block {
|
|
121
|
-
return block((c) => {
|
|
122
|
-
if (!c.enabled) return [`── ${title} ──`, ...body.split('\n')]
|
|
123
|
-
const gutter = paint(INFO, '│', c, { dim: true })
|
|
124
|
-
return [
|
|
125
|
-
paint(INFO, `── ${title} ──`, c, { bold: true }),
|
|
126
|
-
...body
|
|
127
|
-
.split('\n')
|
|
128
|
-
.map((l, i, all) => (i === all.length - 1 && l === '' ? l : `${gutter} ${l}`)),
|
|
129
|
-
]
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Indented short lines: what is wrong, or what happened. */
|
|
134
|
-
export function text(...items: Inline[]): Block {
|
|
135
|
-
return block((c) =>
|
|
136
|
-
items.flatMap((i) =>
|
|
137
|
-
lines(typeof i === 'string' ? prose(i) : i, c).map((l) => `${INDENT}${l}`),
|
|
138
|
-
),
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Plain words dim, each `command` in them bold: no line of vx reads plain white. */
|
|
143
|
-
function prose(text: string): Inline {
|
|
144
|
-
return text.split(/(`[^`\n]+`)/).map((p, i) => (i % 2 === 1 ? cmd(p) : p === '' ? p : dim(p)))
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** An indented dim line: why it matters. */
|
|
148
|
-
export function why(line: Inline): Block {
|
|
149
|
-
return block((c) => lines(line, c).map((l) => `${INDENT}${paint('', l, c, { dim: true })}`))
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** A label and its values; values after the first sit under it. `null` is a blank line. */
|
|
153
|
-
export type FieldRow = readonly [label: string, ...values: Inline[]] | null
|
|
154
|
-
|
|
155
|
-
/** A dim label column with the values aligned beside it; `next` and `fix` are bold. */
|
|
156
|
-
export function fields(rows: readonly FieldRow[]): Block {
|
|
157
|
-
return block((c) => {
|
|
158
|
-
const width = Math.max(LABEL_MIN, ...rows.map((r) => (r === null ? 0 : r[0].length)))
|
|
159
|
-
const out: string[] = []
|
|
160
|
-
for (const row of rows) {
|
|
161
|
-
if (row === null) {
|
|
162
|
-
out.push('')
|
|
163
|
-
continue
|
|
164
|
-
}
|
|
165
|
-
const [label, ...values] = row
|
|
166
|
-
const strong = label === 'next' || label === 'fix'
|
|
167
|
-
values
|
|
168
|
-
.flatMap((v) => lines(v, c))
|
|
169
|
-
.forEach((v, i) => {
|
|
170
|
-
const head =
|
|
171
|
-
i === 0 ? paint('', label.padEnd(width), c, { dim: true }) : ' '.repeat(width)
|
|
172
|
-
const value = strong ? paint('', v, c, { bold: true }) : v
|
|
173
|
-
out.push(`${INDENT}${head} ${value}`.trimEnd())
|
|
174
|
-
})
|
|
175
|
-
}
|
|
176
|
-
return out
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** `fix <action>`: what to do about it. */
|
|
181
|
-
export function fix(action: Inline): Block {
|
|
182
|
-
return labelled('fix', action)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** `next <command>`: the command to type next. */
|
|
186
|
-
export function next(command: Inline): Block {
|
|
187
|
-
return labelled('next', command)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function labelled(label: string, value: Inline): Block {
|
|
191
|
-
return block((c) => [
|
|
192
|
-
`${INDENT}${paint('', label, c, { dim: true })} ${paint('', inline(value, c), c, { bold: true })}`,
|
|
193
|
-
])
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** A report: a blank line, the brand rule naming `mark`, then its fields. */
|
|
197
|
-
export function report(mark: string, rows: readonly FieldRow[]): Block {
|
|
198
|
-
return block((c) => ['', gradientRule(c, mark), ...fields(rows).lines(c)])
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** Aligned columns under a dim header. */
|
|
202
|
-
export function table(columns: readonly string[], rows: readonly (readonly Inline[])[]): Block {
|
|
203
|
-
return block((c) => {
|
|
204
|
-
const cells = rows.map((r) => r.map((x) => inline(x, c)))
|
|
205
|
-
const widths = columns.map((h, i) =>
|
|
206
|
-
Math.max(h.length, ...cells.map((r) => visible(r[i] ?? ''))),
|
|
207
|
-
)
|
|
208
|
-
const pad = (s: string, i: number): string =>
|
|
209
|
-
i === columns.length - 1 ? s : s + ' '.repeat(widths[i]! - visible(s))
|
|
210
|
-
return [
|
|
211
|
-
`${INDENT}${paint('', columns.map((h, i) => pad(h, i)).join(' '), c, { dim: true })}`.trimEnd(),
|
|
212
|
-
...cells.map((r) => `${INDENT}${r.map(pad).join(' ')}`.trimEnd()),
|
|
213
|
-
]
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** A source frame (`> 1 | …` and its caret), indented. */
|
|
218
|
-
export function code(frame: string): Block {
|
|
219
|
-
return block(() => frame.split('\n').map((l) => (l === '' ? '' : `${INDENT}${l}`)))
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export interface NoticeParts {
|
|
223
|
-
tone: Tone
|
|
224
|
-
verb: string
|
|
225
|
-
headline: Inline
|
|
226
|
-
/** What is wrong, one fact a line. */
|
|
227
|
-
what?: readonly Inline[]
|
|
228
|
-
why?: Inline
|
|
229
|
-
/** A source frame, below the facts. */
|
|
230
|
-
code?: string
|
|
231
|
-
fix?: Inline
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** A refusal or a warning: status, then what and why, the frame, and the fix, a blank line apart. */
|
|
235
|
-
export function notice(p: NoticeParts): Block {
|
|
236
|
-
return block((c) => {
|
|
237
|
-
const parts: Block[] = [status(p.tone, p.verb, p.headline)]
|
|
238
|
-
const body = [
|
|
239
|
-
...(p.what !== undefined ? [text(...p.what)] : []),
|
|
240
|
-
...(p.why !== undefined ? [why(p.why)] : []),
|
|
241
|
-
]
|
|
242
|
-
if (body.length > 0) parts.push(block((cc) => body.flatMap((b) => b.lines(cc))))
|
|
243
|
-
if (p.code !== undefined) parts.push(code(p.code))
|
|
244
|
-
if (p.fix !== undefined) parts.push(fix(p.fix))
|
|
245
|
-
return parts.flatMap((b, i) => (i === 0 ? b.lines(c) : ['', ...b.lines(c)]))
|
|
246
|
-
})
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** The blocks as text, a blank line apart, ending in a newline. */
|
|
250
|
-
export function render(blocks: readonly Block[], colors: ColorSupport): string {
|
|
251
|
-
return `${blocks.map((b) => b.lines(colors).join('\n')).join('\n\n')}\n`
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** Write the blocks to stdout (`out`) or stderr (`err`), colored for that stream. */
|
|
255
|
-
export function print(stream: 'out' | 'err', ...blocks: Block[]): void {
|
|
256
|
-
const s = stream === 'out' ? process.stdout : process.stderr
|
|
257
|
-
s.write(render(blocks, detectColors(s)))
|
|
258
|
-
}
|
|
File without changes
|
|
File without changes
|