@vzn/vx 0.0.591 → 0.0.595
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/launcher.cjs +9 -2
- package/package.json +5 -5
- package/src/bin.ts +26 -11
- package/src/cache/archive.ts +18 -7
- package/src/cache/cache.ts +137 -101
- package/src/cache/config-evals.ts +19 -4
- package/src/cache/git-inputs.ts +33 -17
- package/src/cache/inputs.ts +157 -54
- package/src/cache/layered-cache.ts +25 -2
- package/src/cache/tar-stream.ts +4 -2
- package/src/cli/cache.ts +4 -16
- package/src/cli/completions.ts +42 -6
- package/src/cli/foreign-flags.ts +56 -6
- package/src/cli/help.ts +27 -8
- package/src/cli/info.ts +4 -6
- package/src/cli/init.ts +4 -4
- package/src/cli/last.ts +49 -20
- package/src/cli/lock.ts +3 -3
- package/src/cli/plan-format.ts +9 -0
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +80 -50
- package/src/cli/select.ts +44 -5
- package/src/cli/show.ts +47 -9
- package/src/cli/upgrade.ts +2 -3
- package/src/cli/watch-filter.ts +21 -7
- package/src/cli/watch-fs.ts +32 -8
- package/src/cli/watch-judge.ts +1 -1
- package/src/cli/watch.ts +40 -15
- package/src/cli/why.ts +4 -13
- package/src/cli/workspace-config.ts +1 -1
- package/src/config.ts +5 -5
- package/src/exec/env.ts +8 -5
- package/src/exec/index.ts +1 -0
- package/src/exec/proc-sample.ts +5 -1
- package/src/exec/runner.ts +126 -47
- package/src/exec/sandbox-deny-scan.ts +2 -2
- package/src/exec/sandbox-paths.ts +11 -5
- package/src/exec/sandbox-runtime.ts +89 -23
- package/src/exec/sandbox-violations.ts +23 -9
- package/src/graph/index.ts +1 -0
- package/src/graph/scheduler.ts +14 -4
- package/src/graph/task-graph.ts +3 -2
- package/src/index.ts +9 -0
- package/src/orchestrator/affected-tasks.ts +34 -10
- package/src/orchestrator/doctor.ts +2 -1
- package/src/orchestrator/execute-task.ts +43 -10
- package/src/orchestrator/hit-restore.ts +9 -5
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/local-shortcircuit.ts +4 -1
- package/src/orchestrator/lockfile-claim.ts +45 -49
- package/src/orchestrator/logger.ts +4 -1
- package/src/orchestrator/miss-save.ts +10 -3
- package/src/orchestrator/placement.ts +17 -3
- package/src/orchestrator/plan.ts +25 -1
- package/src/orchestrator/plugin-host.ts +41 -3
- package/src/orchestrator/plugin.ts +11 -14
- package/src/orchestrator/projects.ts +18 -9
- package/src/orchestrator/run-context.ts +2 -2
- package/src/orchestrator/run-lock.ts +14 -4
- package/src/orchestrator/run.ts +94 -16
- package/src/orchestrator/sandbox-request.ts +6 -10
- package/src/orchestrator/signals.ts +5 -2
- package/src/orchestrator/summary.ts +14 -1
- package/src/util/index.ts +7 -1
- package/src/util/secret-mask.ts +36 -0
- package/src/util/settle.ts +15 -0
- package/src/workspace/affected.ts +47 -19
- package/src/workspace/config-cache.ts +25 -4
- package/src/workspace/config-eval.ts +4 -0
- package/src/workspace/config-imports.ts +2 -2
- package/src/workspace/config-schema.ts +47 -10
- package/src/workspace/filter.ts +5 -4
- package/src/workspace/migrate-scripts.ts +23 -6
- package/src/workspace/package-graph.ts +15 -3
- package/src/workspace/project-loader.ts +89 -2
- package/src/workspace/workspace.ts +121 -14
package/src/cli/cache.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { Cache } from '../cache/index.js'
|
|
3
|
-
import { flagHint, refusedWord, seeHelp } from './help.js'
|
|
3
|
+
import { flagHint, formatValue, refusedWord, seeHelp } from './help.js'
|
|
4
4
|
import { nearest, parseDuration, parseSize } from '../util/index.js'
|
|
5
5
|
import { acquireRunLock } from '../orchestrator/index.js'
|
|
6
6
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
@@ -125,10 +125,8 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
|
|
|
125
125
|
} else if (a === '--dry-run') {
|
|
126
126
|
out.dryRun = true
|
|
127
127
|
} else if (a === '--format' || a?.startsWith('--format=')) {
|
|
128
|
-
const v = a === '--format' ? args[++i] : a.slice('--format='.length)
|
|
129
|
-
if (v
|
|
130
|
-
return { error: `--format must be pretty or json${seeHelp('cache')}` }
|
|
131
|
-
}
|
|
128
|
+
const v = formatValue(a === '--format' ? args[++i] : a.slice('--format='.length), 'cache')
|
|
129
|
+
if (typeof v === 'object') return v
|
|
132
130
|
out.format = v
|
|
133
131
|
} else {
|
|
134
132
|
const cd = parseCacheDirFlag(args, i)
|
|
@@ -187,19 +185,9 @@ async function pruneCmd(args: readonly string[]): Promise<number> {
|
|
|
187
185
|
printPruned({ evicted: 0, bytesFreed: 0, orphans: 0, orphanBytes: 0 }, dry, json)
|
|
188
186
|
return 0
|
|
189
187
|
}
|
|
190
|
-
if (parsed.dryRun === true) {
|
|
191
|
-
const earlier = await Cache.orphansBeforeReset(dir)
|
|
192
|
-
if (earlier !== null) {
|
|
193
|
-
warnToStderr(
|
|
194
|
-
`[vx] the cache index is schema ${earlier.found} from another vx version: the prune resets it first, and every artifact past the hour's grace is then an orphan`,
|
|
195
|
-
)
|
|
196
|
-
printPruned({ evicted: 0, bytesFreed: 0, ...earlier }, true, json)
|
|
197
|
-
return 0
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
188
|
const cache =
|
|
201
189
|
parsed.dryRun === true
|
|
202
|
-
? new Cache(dir, undefined, undefined, undefined, '
|
|
190
|
+
? new Cache(dir, undefined, undefined, undefined, 'preview', storeRoot)
|
|
203
191
|
: new Cache(dir, undefined, root, undefined, 'open', storeRoot)
|
|
204
192
|
// A prune deletes rows and artifacts; a cache this user cannot write is
|
|
205
193
|
// refused up front with the directory named, as a run refuses it, rather
|
package/src/cli/completions.ts
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
import { acceptedFlags, CORE_VERBS, flagHint, seeHelp } from './help.js'
|
|
9
9
|
import { nearest } from '../util/index.js'
|
|
10
10
|
|
|
11
|
+
/** A flag whose value is one of a fixed set; the tests hold each to its parser. */
|
|
12
|
+
export const FLAG_VALUES: Readonly<Record<string, readonly string[]>> = {
|
|
13
|
+
'--format': ['pretty', 'json'],
|
|
14
|
+
'--output-logs': ['full', 'errors-only', 'hash-only', 'none'],
|
|
15
|
+
'--download': ['all', 'toplevel', 'none'],
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
export type CompletionShell = 'bash' | 'zsh' | 'fish'
|
|
12
19
|
const SHELLS: readonly CompletionShell[] = ['bash', 'zsh', 'fish']
|
|
13
20
|
|
|
@@ -36,6 +43,14 @@ export function completionScript(shell: CompletionShell, verbs: readonly string[
|
|
|
36
43
|
...((CORE_VERBS as readonly string[]).includes(v) ? verbFlags(v) : ['--help']),
|
|
37
44
|
],
|
|
38
45
|
}))
|
|
46
|
+
// `vx run --output-logs <Tab>` offered the flags again; a flag with a
|
|
47
|
+
// fixed value set offers that set.
|
|
48
|
+
const values = table.flatMap((t) =>
|
|
49
|
+
t.words.flatMap((w) => {
|
|
50
|
+
const vs = FLAG_VALUES[w]
|
|
51
|
+
return vs === undefined ? [] : [{ verb: t.verb, flag: w, values: vs.join(' ') }]
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
39
54
|
switch (shell) {
|
|
40
55
|
case 'bash':
|
|
41
56
|
return [
|
|
@@ -43,10 +58,23 @@ export function completionScript(shell: CompletionShell, verbs: readonly string[
|
|
|
43
58
|
'_vx() {',
|
|
44
59
|
' local cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
45
60
|
' local verb="${COMP_WORDS[1]}"',
|
|
61
|
+
' local prev="${COMP_WORDS[COMP_CWORD-1]}"',
|
|
46
62
|
' if [ "$COMP_CWORD" -eq 1 ]; then',
|
|
47
63
|
` COMPREPLY=($(compgen -W "${verbs.join(' ')}" -- "$cur"))`,
|
|
48
64
|
' return',
|
|
49
65
|
' fi',
|
|
66
|
+
// bash splits `--format=js` at the `=` (COMP_WORDBREAKS).
|
|
67
|
+
' if [ "$cur" = "=" ]; then',
|
|
68
|
+
' cur=""',
|
|
69
|
+
' elif [ "$prev" = "=" ]; then',
|
|
70
|
+
' prev="${COMP_WORDS[COMP_CWORD-2]}"',
|
|
71
|
+
' fi',
|
|
72
|
+
' case "$verb $prev" in',
|
|
73
|
+
...values.map(
|
|
74
|
+
(v) =>
|
|
75
|
+
` "${v.verb} ${v.flag}") COMPREPLY=($(compgen -W "${v.values}" -- "$cur")); return ;;`,
|
|
76
|
+
),
|
|
77
|
+
' esac',
|
|
50
78
|
' case "$verb" in',
|
|
51
79
|
...table.map(
|
|
52
80
|
(t) => ` ${t.verb}) COMPREPLY=($(compgen -W "${t.words.join(' ')}" -- "$cur")) ;;`,
|
|
@@ -66,6 +94,9 @@ export function completionScript(shell: CompletionShell, verbs: readonly string[
|
|
|
66
94
|
` compadd -- ${verbs.join(' ')}`,
|
|
67
95
|
' return',
|
|
68
96
|
' fi',
|
|
97
|
+
' case "${words[2]} ${words[CURRENT-1]}" in',
|
|
98
|
+
...values.map((v) => ` "${v.verb} ${v.flag}") compadd -- ${v.values}; return ;;`),
|
|
99
|
+
' esac',
|
|
69
100
|
' case "${words[2]}" in',
|
|
70
101
|
...table.map((t) => ` ${t.verb}) compadd -- ${t.words.join(' ')} ;;`),
|
|
71
102
|
' esac',
|
|
@@ -86,11 +117,14 @@ export function completionScript(shell: CompletionShell, verbs: readonly string[
|
|
|
86
117
|
'complete -c vx -f',
|
|
87
118
|
...verbs.map((v) => `complete -c vx -n __fish_use_subcommand -a ${v}`),
|
|
88
119
|
...table.flatMap((t) =>
|
|
89
|
-
t.words.map((w) =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
120
|
+
t.words.map((w) => {
|
|
121
|
+
const on = `complete -c vx -n "__fish_seen_subcommand_from ${t.verb}"`
|
|
122
|
+
if (!w.startsWith('--')) return `${on} -a ${w}`
|
|
123
|
+
const vs = FLAG_VALUES[w]
|
|
124
|
+
return vs === undefined
|
|
125
|
+
? `${on} -l ${w.slice(2)}`
|
|
126
|
+
: `${on} -l ${w.slice(2)} -x -a "${vs.join(' ')}"`
|
|
127
|
+
}),
|
|
94
128
|
),
|
|
95
129
|
'',
|
|
96
130
|
].join('\n')
|
|
@@ -111,7 +145,9 @@ export async function completionsCmd(
|
|
|
111
145
|
return 1
|
|
112
146
|
}
|
|
113
147
|
const one = shell !== undefined && args.length === 1
|
|
114
|
-
|
|
148
|
+
// One edit: shell names are three or four letters, so two edits took
|
|
149
|
+
// `tcsh` to `bash`, a different shell.
|
|
150
|
+
const best = one ? nearest(shell, SHELLS, 1) : undefined
|
|
115
151
|
process.stderr.write(
|
|
116
152
|
`vx completions: expected one shell — bash, zsh or fish${one ? ` (got ${shell})` : ''}${best === undefined ? '' : `. Did you mean ${best}?`}${seeHelp('completions')}\n`,
|
|
117
153
|
)
|
package/src/cli/foreign-flags.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// on left a `turbo run --dry-run` user at a dead end. The table renders
|
|
7
7
|
// cli.md's parity section (`renderForeignFlags`), pinned by a test.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
interface ForeignFlag {
|
|
10
10
|
runner: 'turbo' | 'nx'
|
|
11
11
|
/** Every spelling, first the one shown. */
|
|
12
12
|
names: readonly string[]
|
|
@@ -19,6 +19,13 @@ export interface ForeignFlag {
|
|
|
19
19
|
outcome: 'same' | 'alias' | 'refuse'
|
|
20
20
|
/** The vx spelling, or what to use instead. */
|
|
21
21
|
vx: string
|
|
22
|
+
/**
|
|
23
|
+
* The runner reads `=true` / `=false` on it (Turbo's `--force [FORCE]`,
|
|
24
|
+
* Nx's yargs booleans): `=false` is the flag left out, `=true` the flag
|
|
25
|
+
* bare. `--skip-nx-cache=false` forced every task, `--summarize=true`
|
|
26
|
+
* wrote the summary to a file named `true`.
|
|
27
|
+
*/
|
|
28
|
+
bool?: true
|
|
22
29
|
/** For an alias: the vx argv. For a refusal: none. */
|
|
23
30
|
to?: (value: string | undefined) => string[]
|
|
24
31
|
}
|
|
@@ -29,6 +36,17 @@ const list = (v: string | undefined): string[] =>
|
|
|
29
36
|
.map((s) => s.trim())
|
|
30
37
|
.filter((s) => s !== '')
|
|
31
38
|
|
|
39
|
+
// Nx's `--projects` labels (find-matching-projects, 23.2.1): `name:` and
|
|
40
|
+
// `directory:` say which match to make; vx's filter says it by form. `tag:`
|
|
41
|
+
// is a filter form already.
|
|
42
|
+
const nxPattern = (p: string): string => {
|
|
43
|
+
const neg = p.startsWith('!') ? '!' : ''
|
|
44
|
+
const s = neg === '' ? p : p.slice(1)
|
|
45
|
+
if (s.startsWith('name:')) return `${neg}${s.slice(5)}`
|
|
46
|
+
if (s.startsWith('directory:')) return `${neg}./${s.slice(10)}`
|
|
47
|
+
return p
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
const GRAPH_IMAGE = /\.(svg|png|jpe?g|pdf|json|html|mermaid)$/i
|
|
33
51
|
|
|
34
52
|
export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
@@ -100,6 +118,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
100
118
|
runner: 'turbo',
|
|
101
119
|
names: ['--force'],
|
|
102
120
|
value: false,
|
|
121
|
+
bool: true,
|
|
103
122
|
outcome: 'same',
|
|
104
123
|
vx: '`--force`: skip cache reads, keep writes',
|
|
105
124
|
},
|
|
@@ -114,6 +133,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
114
133
|
runner: 'turbo',
|
|
115
134
|
names: ['--summarize'],
|
|
116
135
|
value: false,
|
|
136
|
+
bool: true,
|
|
117
137
|
outcome: 'same',
|
|
118
138
|
vx: '`--summarize[=<path>]`',
|
|
119
139
|
},
|
|
@@ -133,7 +153,14 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
133
153
|
outcome: 'same',
|
|
134
154
|
vx: '`--output-logs full|errors-only|hash-only|none`',
|
|
135
155
|
},
|
|
136
|
-
{
|
|
156
|
+
{
|
|
157
|
+
runner: 'turbo',
|
|
158
|
+
names: ['--no-cache'],
|
|
159
|
+
value: false,
|
|
160
|
+
outcome: 'same',
|
|
161
|
+
// Turbo 2.11's own help: "Equivalent to `--cache=local:r,remote:r`".
|
|
162
|
+
vx: "`--no-cache` reads nothing either; Turbo's (reads, no writes) is `--cache local:r,remote:r`",
|
|
163
|
+
},
|
|
137
164
|
{
|
|
138
165
|
runner: 'turbo',
|
|
139
166
|
names: ['--cache'],
|
|
@@ -268,6 +295,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
268
295
|
runner: 'turbo',
|
|
269
296
|
names: ['--remote-only'],
|
|
270
297
|
value: false,
|
|
298
|
+
bool: true,
|
|
271
299
|
outcome: 'refuse',
|
|
272
300
|
vx: 'use `--cache local:,remote:rw`',
|
|
273
301
|
},
|
|
@@ -275,6 +303,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
275
303
|
runner: 'turbo',
|
|
276
304
|
names: ['--remote-cache-read-only'],
|
|
277
305
|
value: false,
|
|
306
|
+
bool: true,
|
|
278
307
|
outcome: 'refuse',
|
|
279
308
|
vx: 'use `--cache local:rw,remote:r`',
|
|
280
309
|
},
|
|
@@ -396,8 +425,16 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
396
425
|
names: ['-p', '--projects'],
|
|
397
426
|
value: true,
|
|
398
427
|
outcome: 'alias',
|
|
399
|
-
vx:
|
|
400
|
-
|
|
428
|
+
vx: "`--filter <pattern>`, one per project (`directory:<d>` is `./<d>`, `name:<n>` is `<n>`); a list opening with `!` starts from all (`--filter '*'`)",
|
|
429
|
+
// Nx reads a leading exclusion as "all projects but": `-p '!b,a'` is
|
|
430
|
+
// every project except b, where vx's filters alone would select a.
|
|
431
|
+
to: (v) => {
|
|
432
|
+
const ps = list(v)
|
|
433
|
+
return [...(ps[0]?.startsWith('!') ? ['*'] : []), ...ps].flatMap((p) => [
|
|
434
|
+
'--filter',
|
|
435
|
+
nxPattern(p),
|
|
436
|
+
])
|
|
437
|
+
},
|
|
401
438
|
},
|
|
402
439
|
{
|
|
403
440
|
runner: 'nx',
|
|
@@ -405,7 +442,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
405
442
|
value: true,
|
|
406
443
|
outcome: 'alias',
|
|
407
444
|
vx: "`--filter '!<pattern>'`, one per project",
|
|
408
|
-
to: (v) => list(v).flatMap((p) => ['--filter', `!${p}`]),
|
|
445
|
+
to: (v) => list(v).flatMap((p) => ['--filter', `!${nxPattern(p)}`]),
|
|
409
446
|
},
|
|
410
447
|
{
|
|
411
448
|
runner: 'nx',
|
|
@@ -436,6 +473,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
436
473
|
runner: 'nx',
|
|
437
474
|
names: ['--skip-nx-cache'],
|
|
438
475
|
value: false,
|
|
476
|
+
bool: true,
|
|
439
477
|
outcome: 'alias',
|
|
440
478
|
vx: '`--force`',
|
|
441
479
|
to: () => ['--force'],
|
|
@@ -445,6 +483,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
445
483
|
runner: 'nx',
|
|
446
484
|
names: ['--nx-bail'],
|
|
447
485
|
value: false,
|
|
486
|
+
bool: true,
|
|
448
487
|
outcome: 'alias',
|
|
449
488
|
vx: '`--continue=never`',
|
|
450
489
|
to: () => ['--continue=never'],
|
|
@@ -482,6 +521,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
482
521
|
runner: 'nx',
|
|
483
522
|
names: ['--exclude-task-dependencies'],
|
|
484
523
|
value: false,
|
|
524
|
+
bool: true,
|
|
485
525
|
outcome: 'alias',
|
|
486
526
|
vx: '`--exclude-dependencies`',
|
|
487
527
|
to: () => ['--exclude-dependencies'],
|
|
@@ -490,6 +530,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
490
530
|
runner: 'nx',
|
|
491
531
|
names: ['--skip-remote-cache'],
|
|
492
532
|
value: false,
|
|
533
|
+
bool: true,
|
|
493
534
|
outcome: 'alias',
|
|
494
535
|
vx: '`--cache local:rw,remote:`',
|
|
495
536
|
to: () => ['--cache', 'local:rw,remote:'],
|
|
@@ -605,6 +646,13 @@ export function translateForeign(args: readonly string[]): string[] | { error: s
|
|
|
605
646
|
name = kebab
|
|
606
647
|
a = inline === undefined ? kebab : `${kebab}=${inline}`
|
|
607
648
|
}
|
|
649
|
+
if (
|
|
650
|
+
(inline === 'true' || inline === 'false') &&
|
|
651
|
+
FOREIGN_FLAGS.some((f) => f.bool && f.names.includes(name))
|
|
652
|
+
) {
|
|
653
|
+
if (inline === 'false') continue
|
|
654
|
+
a = name
|
|
655
|
+
}
|
|
608
656
|
const entries = FOREIGN_FLAGS.filter((f) => f.names.includes(name) || f.names.includes(a))
|
|
609
657
|
const next = before[i + 1]
|
|
610
658
|
const spaced = inline === undefined && next !== undefined && !next.startsWith('-')
|
|
@@ -627,7 +675,9 @@ export function translateForeign(args: readonly string[]): string[] | { error: s
|
|
|
627
675
|
/** cli.md's parity table, one row per entry. */
|
|
628
676
|
export function renderForeignFlags(): string {
|
|
629
677
|
const rows = FOREIGN_FLAGS.map((f) => {
|
|
630
|
-
const flag =
|
|
678
|
+
const flag =
|
|
679
|
+
f.shown ??
|
|
680
|
+
f.names.map((n) => `\`${n}${f.value ? ' <v>' : f.bool ? '[=<bool>]' : ''}\``).join(', ')
|
|
631
681
|
return `| ${f.runner} | ${flag.replaceAll('|', '\\|')} | ${f.outcome} | ${f.vx.replaceAll('|', '\\|')} |`
|
|
632
682
|
})
|
|
633
683
|
return ['| runner | flag | outcome | in vx |', '| --- | --- | --- | --- |', ...rows].join('\n')
|
package/src/cli/help.ts
CHANGED
|
@@ -110,10 +110,10 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
110
110
|
' vx cache prune [--older-than <duration>] [--max-size <size>] [--dry-run] [--format pretty|json] [--cache-dir <path>]',
|
|
111
111
|
' vx lock [--check]',
|
|
112
112
|
' vx init [--dry] [--force] [--mjs] [--plugin <seam>]',
|
|
113
|
-
' vx upgrade [
|
|
114
|
-
' vx show [PROJECT[#TASK] | TASK] [--format pretty|json]',
|
|
113
|
+
' vx upgrade [TAG]',
|
|
114
|
+
' vx show [PROJECT[#TASK] | TASK] [--filter <pattern>] [--affected[=<ref>]] [--format pretty|json]',
|
|
115
115
|
' vx info [--format pretty|json] [--cache-dir <path>]',
|
|
116
|
-
' vx why (TASK | PKG#TASK) [--run
|
|
116
|
+
' vx why (TASK | PKG#TASK) [--run RUNID] [--format pretty|json] [--cache-dir <path>]',
|
|
117
117
|
' vx last [RUNID] [--list[=N]] [--failed] [--format pretty|json] [--cache-dir <path>]',
|
|
118
118
|
' vx completions bash|zsh|fish',
|
|
119
119
|
' vx help [VERB]',
|
|
@@ -140,8 +140,8 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
140
140
|
' to its flags; unnamed layers keep their value. Examples:',
|
|
141
141
|
' local:rw,remote:r (remote read-only), remote: (remote off),',
|
|
142
142
|
' local:r (local read-only).',
|
|
143
|
-
' --cache-dir <path> Cache directory override (cwd-relative). Beats
|
|
144
|
-
' cacheDir field
|
|
143
|
+
' --cache-dir <path> Cache directory override (cwd-relative). Beats VX_CACHE_DIR,',
|
|
144
|
+
' the workspace cacheDir field and the ~/.vx/<id>/cache default.',
|
|
145
145
|
' --retry <n> Re-run a failed task up to <n> more times (default for tasks',
|
|
146
146
|
" without their own exec.retries; explicit config wins). Doesn't",
|
|
147
147
|
' affect cache keys.',
|
|
@@ -209,22 +209,26 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
209
209
|
' evaluation, not the lock), one block per task.',
|
|
210
210
|
" vx show <pkg>#<task> Print a single task's resolved config.",
|
|
211
211
|
' vx show <task> That task in every project declaring it.',
|
|
212
|
+
" --filter <pattern> The list (or a task's projects) as `vx run --filter` selects.",
|
|
213
|
+
' --affected[=<ref>] Only what `vx run --affected` selects: changed and dependents.',
|
|
212
214
|
' --format <fmt> pretty (default) | json.',
|
|
213
215
|
' vx info Workspace doctor: vx/bun/git versions, project +',
|
|
214
216
|
' task counts, cache dir/entries/size, recent runs,',
|
|
215
217
|
' lock status.',
|
|
216
218
|
' --format <fmt> pretty (default) | json.',
|
|
217
219
|
" vx why <task> Why did this task re-run? Compares the task's latest",
|
|
218
|
-
' run (or --run
|
|
220
|
+
' run (or --run RUNID) against its previous run: names the',
|
|
219
221
|
' exact changed cache-key components (files / env / runtime',
|
|
220
222
|
' / upstream) from the persisted input fingerprints.',
|
|
221
223
|
' --format <fmt> pretty (default) | json.',
|
|
222
|
-
" vx last [
|
|
224
|
+
" vx last [RUNID] Replay a recorded run's summary from the local history —",
|
|
223
225
|
' header (command, when, branch, counts) plus the per-task',
|
|
224
226
|
' table, failures first. No re-execution, no cache probe.',
|
|
225
227
|
' --list[=N] List the N most recent runs (default 10) with run ids.',
|
|
226
228
|
' --failed Only failed runs: the latest one, or with --list the list.',
|
|
227
229
|
' --format <fmt> pretty (default) | json.',
|
|
230
|
+
' info, why and last:',
|
|
231
|
+
' --cache-dir <path> Read the cache a run with the same flag uses.',
|
|
228
232
|
'',
|
|
229
233
|
'Migration:',
|
|
230
234
|
' vx init Generate vx.workspace.ts + one vx.config.ts per package',
|
|
@@ -233,6 +237,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
233
237
|
' only vx.workspace.ts, declaring turbo() or nx().',
|
|
234
238
|
' --dry Print the generated files instead of writing them.',
|
|
235
239
|
' --force Overwrite existing vx.config.* files.',
|
|
240
|
+
' --mjs Write .mjs files (no type import) instead of .ts.',
|
|
236
241
|
' --plugin <seam> Instead: write plugins/<seam>.ts, a runnable plugin, and its',
|
|
237
242
|
' test (executor, cache, telemetry, schedule, admit, commands,',
|
|
238
243
|
' project, graph, key).',
|
|
@@ -267,6 +272,20 @@ export function seeHelp(verb: string): string {
|
|
|
267
272
|
return ` (see \`vx ${verb} --help\`)`
|
|
268
273
|
}
|
|
269
274
|
|
|
275
|
+
/** `--format`'s value, or the one refusal every verb gives for a bad one. */
|
|
276
|
+
export function formatValue(
|
|
277
|
+
v: string | undefined,
|
|
278
|
+
verb: string,
|
|
279
|
+
): 'pretty' | 'json' | { error: string } {
|
|
280
|
+
if (v === 'pretty' || v === 'json') return v
|
|
281
|
+
return {
|
|
282
|
+
error:
|
|
283
|
+
v === undefined || v === ''
|
|
284
|
+
? `--format requires a value: pretty or json${seeHelp(verb)}`
|
|
285
|
+
: `--format must be pretty or json (got ${v})${seeHelp(verb)}`,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
270
289
|
/** How a verb names a word it refuses: a flag, or a positional it takes no more of. */
|
|
271
290
|
export function refusedWord(arg: string): string {
|
|
272
291
|
return arg.startsWith('-') ? 'unknown flag' : 'unexpected argument'
|
|
@@ -339,7 +358,7 @@ export const WATCH_REFUSED_FLAGS: readonly string[] = [
|
|
|
339
358
|
* The flags one verb accepts: those on its own Usage line, and for a verb
|
|
340
359
|
* that takes `[OPTIONS]` (run, watch) the documented run flags — less the
|
|
341
360
|
* ones watch refuses. The Usage line is the verb's synopsis; a section's
|
|
342
|
-
* prose names other verbs' flags (`vx lock --check`, `--run
|
|
361
|
+
* prose names other verbs' flags (`vx lock --check`, `--run RUNID` under
|
|
343
362
|
* `vx why`) and is not read.
|
|
344
363
|
*/
|
|
345
364
|
export function acceptedFlags(verb: string): string[] {
|
package/src/cli/info.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
// too; this file parses the flags and renders the rows.
|
|
6
6
|
|
|
7
7
|
import { collectInfo, type FlakyTask, type InfoFacts } from '../orchestrator/index.js'
|
|
8
|
-
import { flagHint, refusedWord, seeHelp } from './help.js'
|
|
8
|
+
import { flagHint, formatValue, refusedWord, seeHelp } from './help.js'
|
|
9
9
|
import { namedCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
|
|
10
10
|
import { formatBytes } from './format.js'
|
|
11
11
|
import { MIN_BUN } from '../util/index.js'
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
interface InfoArgs {
|
|
14
14
|
format: 'pretty' | 'json'
|
|
15
15
|
/** `--cache-dir`: report on the cache a run with the same flag uses. */
|
|
16
16
|
cacheDir?: string
|
|
@@ -22,10 +22,8 @@ export function parseInfoArgs(args: readonly string[]): InfoArgs {
|
|
|
22
22
|
for (let i = 0; i < args.length; i++) {
|
|
23
23
|
const a = args[i]
|
|
24
24
|
if (a === '--format' || a?.startsWith('--format=')) {
|
|
25
|
-
const v = a === '--format' ? args[++i] : a.slice('--format='.length)
|
|
26
|
-
if (v
|
|
27
|
-
return { ...out, error: `--format must be pretty or json${seeHelp('info')}` }
|
|
28
|
-
}
|
|
25
|
+
const v = formatValue(a === '--format' ? args[++i] : a.slice('--format='.length), 'info')
|
|
26
|
+
if (typeof v === 'object') return { ...out, ...v }
|
|
29
27
|
out.format = v
|
|
30
28
|
continue
|
|
31
29
|
}
|
package/src/cli/init.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { mkdir, unlink } from 'node:fs/promises'
|
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import { flagHint, seeHelp } from './help.js'
|
|
13
13
|
import { PLUGIN_TEMPLATES } from './plugin-templates.js'
|
|
14
|
-
import { isUserError, UserError } from '../util/index.js'
|
|
14
|
+
import { isUserError, relPosix, UserError } from '../util/index.js'
|
|
15
15
|
import {
|
|
16
16
|
applyMigration,
|
|
17
17
|
discoverProjects,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
WORKSPACE_CONFIG_FILENAMES,
|
|
23
23
|
} from '../workspace/index.js'
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
interface InitArgs {
|
|
26
26
|
dry: boolean
|
|
27
27
|
force: boolean
|
|
28
28
|
/** `vx.config.mjs` instead of `.ts` — see `ApplyMigrationArgs.format`. */
|
|
@@ -369,7 +369,7 @@ function namelessNotes(root: string, dirs: readonly string[]): string[] {
|
|
|
369
369
|
const withScripts = dirs
|
|
370
370
|
.filter((dir) => {
|
|
371
371
|
const scripts = (
|
|
372
|
-
JSON.parse(readText(path.join(dir, 'package.json')) || '{}') as {
|
|
372
|
+
JSON.parse(readText(path.join(dir, 'package.json')).replace(/^\uFEFF/, '') || '{}') as {
|
|
373
373
|
scripts?: unknown
|
|
374
374
|
}
|
|
375
375
|
).scripts
|
|
@@ -379,7 +379,7 @@ function namelessNotes(root: string, dirs: readonly string[]): string[] {
|
|
|
379
379
|
Object.values(scripts).some((v) => typeof v === 'string' && v !== '')
|
|
380
380
|
)
|
|
381
381
|
})
|
|
382
|
-
.map((dir) =>
|
|
382
|
+
.map((dir) => relPosix(root, dir))
|
|
383
383
|
.sort()
|
|
384
384
|
// The root's own manifest is the empty relative path: a nameless
|
|
385
385
|
// single-package repo read `not mapped: — its package.json…` (M-54).
|
package/src/cli/last.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `vx last [
|
|
1
|
+
// `vx last [RUNID]` — replay a recorded run's summary from the terminal,
|
|
2
2
|
// without re-executing anything. Comparison gap #12: with the dashboard's
|
|
3
3
|
// run-detail page gone (the 2026-08-23 cloud removal), the local run
|
|
4
4
|
// history in cache.db is the only replay surface, and this verb reads it.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { Database } from 'bun:sqlite'
|
|
8
8
|
import { Cache } from '../cache/index.js'
|
|
9
9
|
import { formatBytes } from './format.js'
|
|
10
|
-
import { flagHint, seeHelp } from './help.js'
|
|
10
|
+
import { flagHint, formatValue, seeHelp } from './help.js'
|
|
11
11
|
import {
|
|
12
12
|
exitSignal,
|
|
13
13
|
getInvocation,
|
|
@@ -70,16 +70,8 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
70
70
|
continue
|
|
71
71
|
}
|
|
72
72
|
if (a === '--format' || a.startsWith('--format=')) {
|
|
73
|
-
const fv = a === '--format' ? args[++i] : a.slice(9)
|
|
74
|
-
if (fv
|
|
75
|
-
return {
|
|
76
|
-
...out,
|
|
77
|
-
error:
|
|
78
|
-
fv === undefined || fv === ''
|
|
79
|
-
? '--format requires a value (pretty | json)'
|
|
80
|
-
: `invalid --format: ${fv} (expected pretty | json)`,
|
|
81
|
-
}
|
|
82
|
-
}
|
|
73
|
+
const fv = formatValue(a === '--format' ? args[++i] : a.slice(9), 'last')
|
|
74
|
+
if (typeof fv === 'object') return { ...out, ...fv }
|
|
83
75
|
out.format = fv
|
|
84
76
|
continue
|
|
85
77
|
}
|
|
@@ -241,6 +233,47 @@ function hitsLine(
|
|
|
241
233
|
return `${hits} (${inv.upToDateCount} up-to-date, ${restored} restored${layers})`
|
|
242
234
|
}
|
|
243
235
|
|
|
236
|
+
/**
|
|
237
|
+
* `vx last --list`'s rows, in columns: the counts vary with what a run
|
|
238
|
+
* hit and failed, so unpadded they pushed each run's duration and command
|
|
239
|
+
* to a different column and the list could not be scanned down.
|
|
240
|
+
*/
|
|
241
|
+
export function formatRunList(
|
|
242
|
+
runs: readonly {
|
|
243
|
+
inv: Pick<
|
|
244
|
+
InvocationDetail,
|
|
245
|
+
| 'exitOk'
|
|
246
|
+
| 'startedAt'
|
|
247
|
+
| 'taskCount'
|
|
248
|
+
| 'failedCount'
|
|
249
|
+
| 'totalDurationMs'
|
|
250
|
+
| 'command'
|
|
251
|
+
| 'hitCount'
|
|
252
|
+
| 'upToDateCount'
|
|
253
|
+
| 'restoredLocalCount'
|
|
254
|
+
| 'restoredRemoteCount'
|
|
255
|
+
>
|
|
256
|
+
id: string
|
|
257
|
+
}[],
|
|
258
|
+
): string[] {
|
|
259
|
+
const rows = runs.map(({ inv, id }) => ({
|
|
260
|
+
head: `${inv.exitOk ? 'ok ' : 'FAILED'} ${fmtWhen(inv.startedAt)}`,
|
|
261
|
+
id,
|
|
262
|
+
counts:
|
|
263
|
+
`${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}` +
|
|
264
|
+
(inv.failedCount > 0 ? ` · ${inv.failedCount} failed` : ''),
|
|
265
|
+
ms: fmtMs(inv.totalDurationMs),
|
|
266
|
+
command: inv.command,
|
|
267
|
+
}))
|
|
268
|
+
const idW = Math.max(...rows.map((r) => r.id.length))
|
|
269
|
+
const countsW = Math.max(...rows.map((r) => r.counts.length))
|
|
270
|
+
const msW = Math.max(...rows.map((r) => r.ms.length))
|
|
271
|
+
return rows.map(
|
|
272
|
+
(r) =>
|
|
273
|
+
`${r.head} ${r.id.padEnd(idW)} ${r.counts.padEnd(countsW)} ${r.ms.padStart(msW)} $ ${r.command}`,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
244
277
|
export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
245
278
|
const parsed = parseLastArgs(args)
|
|
246
279
|
if (parsed.error !== undefined) throw new UserError(`vx last: ${parsed.error}`)
|
|
@@ -269,14 +302,10 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
269
302
|
)
|
|
270
303
|
return 0
|
|
271
304
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
`${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}` +
|
|
277
|
-
`${inv.failedCount > 0 ? ` · ${inv.failedCount} failed` : ''} · ${fmtMs(inv.totalDurationMs)} $ ${inv.command}\n`,
|
|
278
|
-
)
|
|
279
|
-
}
|
|
305
|
+
const lines = formatRunList(
|
|
306
|
+
invocations.map((inv) => ({ inv, id: shortRunId(db, inv.runId) })),
|
|
307
|
+
)
|
|
308
|
+
process.stdout.write(`${lines.join('\n')}\n`)
|
|
280
309
|
return 0
|
|
281
310
|
}
|
|
282
311
|
|
package/src/cli/lock.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
type ProjectMeta,
|
|
21
21
|
} from '../workspace/index.js'
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
interface LockArgs {
|
|
24
24
|
check: boolean
|
|
25
25
|
error?: string
|
|
26
26
|
}
|
|
@@ -99,7 +99,7 @@ async function writeLock(root: string, metas: ConfiguredMeta[], bare: number): P
|
|
|
99
99
|
await writeLockfile(root, lock)
|
|
100
100
|
const n = metas.length
|
|
101
101
|
process.stdout.write(
|
|
102
|
-
`vx: locked ${n} project config${n === 1 ? '' : 's'} → ${LOCKFILE_NAME}${bareNote(bare)}\n`,
|
|
102
|
+
`vx lock: locked ${n} project config${n === 1 ? '' : 's'} → ${LOCKFILE_NAME}${bareNote(bare)}\n`,
|
|
103
103
|
)
|
|
104
104
|
return 0
|
|
105
105
|
}
|
|
@@ -199,6 +199,6 @@ async function checkLock(root: string, metas: ConfiguredMeta[], bare: number): P
|
|
|
199
199
|
const n = metas.length
|
|
200
200
|
const audited = `${n} project${n === 1 ? '' : 's'}`
|
|
201
201
|
const skipped = bare === 0 ? '' : `; ${bare} without a vx.config not audited`
|
|
202
|
-
process.stdout.write(`vx
|
|
202
|
+
process.stdout.write(`vx lock --check: up to date (${audited}${skipped})\n`)
|
|
203
203
|
return 0
|
|
204
204
|
}
|
package/src/cli/plan-format.ts
CHANGED
|
@@ -29,6 +29,7 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
29
29
|
let miss = 0
|
|
30
30
|
let nocache = 0
|
|
31
31
|
let noop = 0
|
|
32
|
+
let idle = 0
|
|
32
33
|
for (const t of real) {
|
|
33
34
|
// `@noop`: the run succeeds it without running, whatever its key says.
|
|
34
35
|
const skipped = t.executor === 'noop'
|
|
@@ -56,6 +57,7 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
56
57
|
else if (t.cacheStatus === 'hit-remote') remote++
|
|
57
58
|
else if (t.cacheStatus === 'miss') miss++
|
|
58
59
|
else if (t.cacheStatus === 'no-cache') nocache++
|
|
60
|
+
else if (t.cacheStatus === 'not-started') idle++
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
const hitParts: string[] = []
|
|
@@ -67,6 +69,7 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
67
69
|
if (miss > 0) summary.push(`${miss} would run`)
|
|
68
70
|
if (nocache > 0) summary.push(`${nocache} no-cache`)
|
|
69
71
|
if (noop > 0) summary.push(`${noop} noop`)
|
|
72
|
+
if (idle > 0) summary.push(`${idle} not started`)
|
|
70
73
|
lines.push('')
|
|
71
74
|
lines.push(summary.join(', ') + '.')
|
|
72
75
|
|
|
@@ -199,6 +202,8 @@ function symbolFor(s: CacheStatus): string {
|
|
|
199
202
|
return '·'
|
|
200
203
|
case 'group':
|
|
201
204
|
return '○'
|
|
205
|
+
case 'not-started':
|
|
206
|
+
return '−'
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
|
|
@@ -214,6 +219,8 @@ function describe(s: CacheStatus): string {
|
|
|
214
219
|
return 'no-cache (would exec)'
|
|
215
220
|
case 'group':
|
|
216
221
|
return 'group task'
|
|
222
|
+
case 'not-started':
|
|
223
|
+
return 'not started (unneeded)'
|
|
217
224
|
}
|
|
218
225
|
}
|
|
219
226
|
|
|
@@ -229,5 +236,7 @@ function dotColor(s: CacheStatus): string {
|
|
|
229
236
|
return '#e5e7eb' // gray-200
|
|
230
237
|
case 'group':
|
|
231
238
|
return '#f5d0fe' // fuchsia-200
|
|
239
|
+
case 'not-started':
|
|
240
|
+
return '#ffffff'
|
|
232
241
|
}
|
|
233
242
|
}
|