@vzn/vx 0.0.637 → 0.0.639
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/docs/caching.md +84 -28
- package/docs/cli.md +273 -408
- package/docs/execution.md +19 -44
- package/docs/features.md +13 -15
- package/docs/patterns.md +5 -7
- package/docs/schema.md +68 -61
- package/package.json +5 -5
- package/schemas/last.json +7 -4
- package/schemas/plan.json +2 -2
- package/skills/vx/SKILL.md +19 -6
- package/src/bin.ts +13 -7
- package/src/cache/cache.ts +60 -11
- package/src/cache/git-inputs.ts +6 -0
- package/src/cache/index.ts +10 -2
- package/src/cache/inputs.ts +0 -24
- package/src/cache/layer.ts +32 -12
- package/src/cache/layered-cache.ts +112 -315
- package/src/cache/plugin-cache.ts +240 -0
- package/src/cache/policy.ts +9 -0
- package/src/cache/remote-lane.ts +383 -0
- package/src/cache/run-history.ts +92 -8
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +44 -13
- package/src/cache/scoped-cache.ts +22 -2
- package/src/cli/cache.ts +33 -16
- package/src/cli/completions.ts +11 -7
- package/src/cli/foreign-flags.ts +10 -22
- package/src/cli/help.ts +22 -38
- package/src/cli/index.ts +25 -11
- package/src/cli/info.ts +8 -6
- package/src/cli/init.ts +14 -15
- package/src/cli/last.ts +20 -10
- package/src/cli/lock.ts +39 -27
- package/src/cli/plan-format.ts +10 -61
- package/src/cli/plugin-commands.ts +77 -6
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +31 -191
- package/src/cli/select.ts +18 -9
- package/src/cli/show.ts +72 -40
- package/src/cli/task-verb.ts +4 -4
- package/src/cli/upgrade.ts +132 -39
- package/src/cli/watch-cycle.ts +2 -1
- package/src/cli/watch-filter.ts +7 -2
- package/src/cli/watch-judge.ts +6 -1
- package/src/cli/watch.ts +73 -34
- package/src/cli/why.ts +14 -5
- package/src/cli/workspace-config.ts +2 -2
- package/src/config.ts +0 -6
- package/src/exec/executor.ts +1 -1
- package/src/exec/sandbox-runtime.ts +1 -1
- package/src/exec/sandbox-violations.ts +3 -1
- package/src/graph/index.ts +9 -1
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +12 -16
- package/src/graph/task-graph.ts +119 -21
- package/src/graph/task-status.ts +95 -0
- package/src/{orchestrator → history}/failure-mode.ts +20 -11
- package/src/{orchestrator → history}/history.ts +61 -5
- package/src/history/index.ts +48 -0
- package/src/{orchestrator → history}/metrics.ts +14 -23
- package/src/{orchestrator → history}/miss-reason.ts +1 -1
- package/src/index.ts +21 -29
- package/src/orchestrator/affected-tasks.ts +8 -3
- package/src/orchestrator/doctor.ts +2 -2
- package/src/orchestrator/execute-args.ts +80 -0
- package/src/orchestrator/execute-task.ts +47 -429
- package/src/orchestrator/fingerprint-watch.ts +2 -2
- package/src/orchestrator/index.ts +3 -74
- package/src/orchestrator/miss-save.ts +33 -3
- package/src/orchestrator/options.ts +1 -36
- package/src/orchestrator/output-host.ts +4 -3
- package/src/orchestrator/persistent-task.ts +321 -0
- package/src/orchestrator/persistent.ts +130 -3
- package/src/orchestrator/placement.ts +3 -3
- package/src/orchestrator/plan.ts +18 -5
- package/src/orchestrator/plugin-host.ts +77 -47
- package/src/orchestrator/plugin.ts +86 -14
- package/src/orchestrator/prepare.ts +74 -31
- package/src/orchestrator/projects.ts +3 -0
- package/src/orchestrator/remote-prefetch.ts +23 -6
- package/src/orchestrator/run-artifacts.ts +5 -177
- package/src/orchestrator/run-context.ts +55 -0
- package/src/orchestrator/run-failures.ts +15 -3
- package/src/orchestrator/run-records.ts +36 -4
- package/src/orchestrator/run-report.ts +81 -179
- package/src/orchestrator/run-settings.ts +1 -16
- package/src/orchestrator/run-stop.ts +153 -0
- package/src/orchestrator/run.ts +313 -555
- package/src/orchestrator/sandbox-fix.ts +0 -16
- package/src/orchestrator/signals.ts +14 -6
- package/src/orchestrator/stable-keys.ts +107 -3
- package/src/orchestrator/steps.ts +50 -0
- package/src/orchestrator/task-hash.ts +3 -16
- package/src/orchestrator/task-log-buffer.ts +1 -1
- package/src/orchestrator/task-process.ts +94 -0
- package/src/orchestrator/telemetry-host.ts +1 -1
- package/src/orchestrator/telemetry.ts +6 -83
- package/src/{orchestrator → report}/events.ts +41 -8
- package/src/{orchestrator → report}/forecast.ts +35 -2
- package/src/{orchestrator → report}/framed-output.ts +39 -34
- package/src/report/index.ts +36 -0
- package/src/{orchestrator → report}/logger.ts +166 -21
- package/src/{orchestrator → report}/status-line.ts +84 -7
- package/src/{orchestrator → report}/summary.ts +179 -63
- package/src/report/tally.ts +83 -0
- package/src/settings.ts +13 -23
- package/src/{orchestrator → util}/colors.ts +49 -4
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +32 -0
- package/src/util/git-ignored.ts +143 -0
- package/src/util/index.ts +25 -3
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +39 -0
- package/src/util/settle.ts +14 -39
- package/src/util/tail.ts +1 -1
- package/src/util/ui.ts +197 -0
- package/src/workspace/affected.ts +20 -23
- package/src/workspace/dependency-rules.ts +12 -4
- package/src/workspace/migration.ts +87 -91
- package/src/workspace/workspace.ts +5 -4
- package/schemas/summary.json +0 -133
- package/src/cache/chained-cache.ts +0 -276
- package/src/orchestrator/tally.ts +0 -112
- /package/src/{orchestrator → report}/path-links.ts +0 -0
- /package/src/{orchestrator → report}/plain-output.ts +0 -0
- /package/src/{orchestrator → report}/terminal-signals.ts +0 -0
package/src/cli/help.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
CORE_VERBS,
|
|
3
|
+
detectColors,
|
|
4
|
+
errorDocument,
|
|
5
|
+
maskedLine,
|
|
6
|
+
nearest,
|
|
7
|
+
paint,
|
|
8
|
+
refusalNotice,
|
|
9
|
+
ui,
|
|
10
|
+
wantsJson,
|
|
11
|
+
type ColorSupport,
|
|
12
|
+
} from '../util/index.js'
|
|
3
13
|
|
|
4
14
|
export async function printHelp(
|
|
5
15
|
pluginCommands: readonly string[] = [],
|
|
6
16
|
verb?: string,
|
|
7
17
|
): Promise<void> {
|
|
8
18
|
const text = verb === undefined ? helpText(pluginCommands) : verbHelpText(verb)
|
|
9
|
-
// The orchestrator is already loaded by the plugin-verb lookup every
|
|
10
|
-
// caller awaits first, so this import costs `vx --version` nothing.
|
|
11
|
-
const { detectColors, paint } = await import('../orchestrator/index.js')
|
|
12
19
|
process.stdout.write(paintHelp(text, helpColors(process.stdout, detectColors), paint))
|
|
13
20
|
}
|
|
14
21
|
|
|
@@ -33,7 +40,7 @@ const TERM = '#22d3ee' // cyan-400 — a flag, a selector, an example
|
|
|
33
40
|
* `(for <verb>)` dim), `vx <verb>` bold cyan, and the term an option or
|
|
34
41
|
* example line opens with cyan. Prose and continuation lines stay plain.
|
|
35
42
|
*/
|
|
36
|
-
export function paintHelp(text: string, colors: ColorSupport, paint:
|
|
43
|
+
export function paintHelp(text: string, colors: ColorSupport, paint: Paint): string {
|
|
37
44
|
if (!colors.enabled) return text
|
|
38
45
|
const lines = text.split('\n')
|
|
39
46
|
return lines
|
|
@@ -164,12 +171,6 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
164
171
|
" --timeout <duration> A task's time limit (30s, 10m), for tasks",
|
|
165
172
|
' without their own exec.timeout. A task past it',
|
|
166
173
|
' is killed and fails.',
|
|
167
|
-
' --kill-grace <duration> How long a task that ignores SIGTERM gets',
|
|
168
|
-
' before SIGKILL (default 2s).',
|
|
169
|
-
" --teardown-timeout <duration> The bound on each plugin's end-of-run flush",
|
|
170
|
-
' (default 3s).',
|
|
171
|
-
' --ready-notice <duration> When vx says a persistent task still waits for',
|
|
172
|
-
' readyWhen (default 10s).',
|
|
173
174
|
' --continue[=<mode>] What a failed task takes down: never | deps-ok',
|
|
174
175
|
' (default, skips its dependents) | always (bare',
|
|
175
176
|
' form; dependents run, never save).',
|
|
@@ -183,18 +184,9 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
183
184
|
'Planning (for run — skips execution):',
|
|
184
185
|
" --dry[=text|json] Print the task graph and each task's predicted hit",
|
|
185
186
|
' or miss.',
|
|
186
|
-
' --graph[=<path>] Emit Graphviz DOT (stdout if no path).',
|
|
187
187
|
'',
|
|
188
188
|
'Artifacts (for run):',
|
|
189
|
-
' --format <fmt> pretty (default) | json: print the --summarize',
|
|
190
|
-
' document on stdout when the run ends; every other',
|
|
191
|
-
' line goes to stderr.',
|
|
192
|
-
' --summarize[=<path>] Write per-run JSON to <cacheDir>/runs/<run_id>.json.',
|
|
193
189
|
' --profile[=<path>] Write Chrome-trace JSON (default profile.json).',
|
|
194
|
-
' --report[=markdown] Print a markdown run report to stdout after the run.',
|
|
195
|
-
' --report-file <path> Append that report to <path> ($GITHUB_STEP_SUMMARY).',
|
|
196
|
-
' --tag <k=v> Label this invocation (repeatable); recorded on its',
|
|
197
|
-
' history row.',
|
|
198
190
|
'',
|
|
199
191
|
'Extensions (plugins):',
|
|
200
192
|
' vx core runs tasks in-process and nothing else. A dashboard, remote',
|
|
@@ -216,12 +208,11 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
216
208
|
'Watch mode:',
|
|
217
209
|
' vx watch <task> Initial run, then re-run on filesystem changes.',
|
|
218
210
|
' Uses the same flags as `vx run` except --dry /',
|
|
219
|
-
' --
|
|
220
|
-
'
|
|
221
|
-
'
|
|
222
|
-
'
|
|
223
|
-
'
|
|
224
|
-
' outputs + replay logs).',
|
|
211
|
+
' --profile / --verbosity (they plan or report one',
|
|
212
|
+
' run), plus --poll (watch.poll: poll instead of',
|
|
213
|
+
' the OS watcher). Press Ctrl+C to stop.',
|
|
214
|
+
' Cache means most re-runs are near-zero cost',
|
|
215
|
+
' (cache hits restore outputs + replay logs).',
|
|
225
216
|
'',
|
|
226
217
|
'Cache management:',
|
|
227
218
|
' vx cache clean --older-than 30d Evict entries last accessed > 30 days ago.',
|
|
@@ -395,16 +386,7 @@ export function documentedFlags(verb: string, text = helpText()): string[] {
|
|
|
395
386
|
* The `vx run` flags `vx watch` refuses: each plans or reports ONE run
|
|
396
387
|
* (`watchRefusal` in watch.ts, held to this list by a test).
|
|
397
388
|
*/
|
|
398
|
-
export const WATCH_REFUSED_FLAGS: readonly string[] = [
|
|
399
|
-
'--dry',
|
|
400
|
-
'--graph',
|
|
401
|
-
'--summarize',
|
|
402
|
-
'--profile',
|
|
403
|
-
'--report',
|
|
404
|
-
'--report-file',
|
|
405
|
-
'--verbosity',
|
|
406
|
-
'--format',
|
|
407
|
-
]
|
|
389
|
+
export const WATCH_REFUSED_FLAGS: readonly string[] = ['--dry', '--profile', '--verbosity']
|
|
408
390
|
|
|
409
391
|
/**
|
|
410
392
|
* The flags one verb accepts: those on its own Usage line, and for a verb
|
|
@@ -445,7 +427,9 @@ export function refuse(
|
|
|
445
427
|
code: string,
|
|
446
428
|
): number {
|
|
447
429
|
const line = `vx ${verb}: ${message}`
|
|
448
|
-
|
|
430
|
+
ui.print('err', refusalNotice(`vx ${verb}`, message))
|
|
449
431
|
if (wantsJson(args)) process.stdout.write(errorDocument(code, maskedLine(line)))
|
|
450
432
|
return 1
|
|
451
433
|
}
|
|
434
|
+
|
|
435
|
+
type Paint = typeof paint
|
package/src/cli/index.ts
CHANGED
|
@@ -8,11 +8,14 @@ import { FOREIGN_VERBS } from './foreign-flags.js'
|
|
|
8
8
|
import {
|
|
9
9
|
errorDocument,
|
|
10
10
|
hideEnvFromTasks,
|
|
11
|
+
sayNotice,
|
|
11
12
|
isFsRefusal,
|
|
12
13
|
isOutOfFds,
|
|
13
14
|
isUserError,
|
|
14
15
|
maskedLine,
|
|
15
16
|
nearest,
|
|
17
|
+
refusalNotice,
|
|
18
|
+
ui,
|
|
16
19
|
UserError,
|
|
17
20
|
wantsJson,
|
|
18
21
|
} from '../util/index.js'
|
|
@@ -74,7 +77,8 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
74
77
|
await findWorkspaceRoot(process.cwd())
|
|
75
78
|
} catch (err) {
|
|
76
79
|
if (!isUserError(err)) throw err
|
|
77
|
-
|
|
80
|
+
const parts = err.parts ?? { headline: err.message }
|
|
81
|
+
ui.print('err', refusalNotice('vx', err.message, { ...parts, why: 'see vx help' }))
|
|
78
82
|
return 1
|
|
79
83
|
}
|
|
80
84
|
await printHelp(await (await plugins()).pluginCommandHelp())
|
|
@@ -97,19 +101,23 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
97
101
|
const unknown = resolved === null || 'declaredVerbs' in resolved
|
|
98
102
|
const pointer = FOREIGN_VERBS[verb]
|
|
99
103
|
if (unknown && pointer !== undefined) {
|
|
100
|
-
|
|
104
|
+
sayNotice('info', 'vx', pointer)
|
|
101
105
|
return 1
|
|
102
106
|
}
|
|
103
107
|
if (unknown && verb.startsWith('-')) {
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
sayNotice(
|
|
109
|
+
'error',
|
|
110
|
+
'vx help',
|
|
111
|
+
`unknown flag: ${verb}${flagHint('help', verb)} (see \`vx help\`)`,
|
|
106
112
|
)
|
|
107
113
|
return 1
|
|
108
114
|
}
|
|
109
115
|
if (unknown) {
|
|
110
116
|
const declared = resolved === null ? [] : resolved.declaredVerbs
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
sayNotice(
|
|
118
|
+
'error',
|
|
119
|
+
'vx help',
|
|
120
|
+
`unknown command: ${verb}${didYouMeanVerb(verb, declared)} (see \`vx help\`)`,
|
|
113
121
|
)
|
|
114
122
|
return 1
|
|
115
123
|
}
|
|
@@ -123,8 +131,10 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
123
131
|
// takes no more of is refused, as every other verb refuses one.
|
|
124
132
|
const extra = rest[0]
|
|
125
133
|
if (extra !== undefined) {
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
sayNotice(
|
|
135
|
+
'error',
|
|
136
|
+
'vx version',
|
|
137
|
+
`${refusedWord(extra)}: ${extra}${flagHint('version', extra)}${seeHelp('version')}`,
|
|
128
138
|
)
|
|
129
139
|
return 1
|
|
130
140
|
}
|
|
@@ -193,8 +203,12 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
193
203
|
// points at the file that broke it.
|
|
194
204
|
// Every answer below is "no such verb here": under --format json the
|
|
195
205
|
// agent reads it as one code, as a verb's own refusal gives one.
|
|
196
|
-
const unknown = (
|
|
197
|
-
|
|
206
|
+
const unknown = (
|
|
207
|
+
text: string,
|
|
208
|
+
code = 'VX_E_UNKNOWN_COMMAND',
|
|
209
|
+
tone: 'error' | 'info' = 'error',
|
|
210
|
+
): number => {
|
|
211
|
+
sayNotice(tone, 'vx', text.replace(/^vx: /, '').trimEnd())
|
|
198
212
|
if (wantsJson(rest))
|
|
199
213
|
process.stdout.write(errorDocument(code, maskedLine(text.split('\n')[0]!)))
|
|
200
214
|
return 1
|
|
@@ -207,7 +221,7 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
207
221
|
// their chance — a workspace that declares a plugin verb of that name keeps it.
|
|
208
222
|
const moved = FOREIGN_VERBS[command]
|
|
209
223
|
if (moved !== undefined) {
|
|
210
|
-
return unknown(`${moved}${loadNote}\n
|
|
224
|
+
return unknown(`${moved}${loadNote}\n`, undefined, 'info')
|
|
211
225
|
}
|
|
212
226
|
// A task typed where the verb goes (`turbo build`, `nx build app`),
|
|
213
227
|
// `turbo dev` included: a repo's own `dev` task beats the no-service note.
|
package/src/cli/info.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
// from the orchestrator's `collectInfo` (doctor.ts); this file parses the flags and renders the rows.
|
|
5
5
|
|
|
6
6
|
import os from 'node:os'
|
|
7
|
-
import { collectInfo, type
|
|
7
|
+
import { collectInfo, type InfoFacts } from '../orchestrator/index.js'
|
|
8
|
+
import { type FlakyTask } from '../history/index.js'
|
|
8
9
|
import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
|
|
9
10
|
import { namedCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
|
|
10
11
|
import { formatBytes } from './format.js'
|
|
11
|
-
import { MIN_BUN } from '../util/index.js'
|
|
12
|
+
import { detectColors, MIN_BUN, ui, type ColorSupport } from '../util/index.js'
|
|
12
13
|
import { envName } from '../settings.js'
|
|
13
14
|
|
|
14
15
|
interface InfoArgs {
|
|
@@ -55,12 +56,14 @@ export async function infoCmd(args: readonly string[]): Promise<number> {
|
|
|
55
56
|
warn: warnToStderr,
|
|
56
57
|
})
|
|
57
58
|
process.stdout.write(
|
|
58
|
-
parsed.format === 'json'
|
|
59
|
+
parsed.format === 'json'
|
|
60
|
+
? `${JSON.stringify(facts)}\n`
|
|
61
|
+
: `${renderInfo(facts, detectColors(process.stdout))}\n`,
|
|
59
62
|
)
|
|
60
63
|
return 0
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
export function renderInfo(f: InfoFacts): string {
|
|
66
|
+
export function renderInfo(f: InfoFacts, colors: ColorSupport = { enabled: false }): string {
|
|
64
67
|
const rows: [string, string][] = [
|
|
65
68
|
[
|
|
66
69
|
'vx',
|
|
@@ -122,8 +125,7 @@ export function renderInfo(f: InfoFacts): string {
|
|
|
122
125
|
['sandbox', describeSandbox(f.sandbox)],
|
|
123
126
|
['projects lock', f.lockfile ? 'yes' : 'no'],
|
|
124
127
|
]
|
|
125
|
-
|
|
126
|
-
return rows.map(([label, value]) => `${`${label}:`.padEnd(labelW + 1)} ${value}`).join('\n')
|
|
128
|
+
return ui.report('vx info', rows).lines(colors).join('\n')
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
/** `812 (1.2 GB)`, or against `cacheRetention.maxSize`: `812 (1.2 GB of 10 GB, 12%)`. */
|
package/src/cli/init.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { lockAfterMigration } from './lock.js'
|
|
|
15
15
|
import { PLUGIN_TEMPLATES } from './plugin-templates.js'
|
|
16
16
|
import { isCompiledBinary } from './upgrade.js'
|
|
17
17
|
import { VERSION } from '../version.js'
|
|
18
|
-
import { DOT_VX_GITATTRIBUTES, DOT_VX_GITIGNORE, UserError } from '../util/index.js'
|
|
18
|
+
import { DOT_VX_GITATTRIBUTES, DOT_VX_GITIGNORE, ui, UserError, sayNotice } from '../util/index.js'
|
|
19
19
|
import {
|
|
20
20
|
findWorkspaceRoot,
|
|
21
21
|
PROJECT_CONFIG_FILENAMES,
|
|
@@ -125,7 +125,7 @@ async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promi
|
|
|
125
125
|
if (!args.force) {
|
|
126
126
|
for (const [rel] of files) {
|
|
127
127
|
if (await Bun.file(path.join(root, rel)).exists()) {
|
|
128
|
-
|
|
128
|
+
sayNotice('error', 'vx init', `${rel} exists. --force overwrites it`)
|
|
129
129
|
return 1
|
|
130
130
|
}
|
|
131
131
|
}
|
|
@@ -193,27 +193,26 @@ async function starter(root: string, noGit: boolean, args: InitArgs): Promise<nu
|
|
|
193
193
|
)
|
|
194
194
|
return 0
|
|
195
195
|
}
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
]
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
report.push(args.dry ? `would write ${named} (dry run, nothing written).` : `wrote ${named}.`)
|
|
202
|
-
if (kept.length > 0) report.push(`kept ${kept.join(', ')}.`)
|
|
203
|
-
if (replaced.length > 0)
|
|
204
|
-
report.push(`${args.dry ? 'would replace' : 'replaced'} ${replaced.join(', ')}.`)
|
|
196
|
+
const rows: ui.FieldRow[] = [['from', 'no package.json: one project, the root']]
|
|
197
|
+
if (args.dry) rows.push(['dry run', 'nothing written'])
|
|
198
|
+
if (files.length > 0) rows.push(['files', ...files.map((f) => f.path)])
|
|
199
|
+
if (kept.length > 0) rows.push(['kept', ...kept])
|
|
200
|
+
if (replaced.length > 0) rows.push(['replaced', ...replaced])
|
|
205
201
|
if (args.dry) {
|
|
206
202
|
for (const f of files) process.stdout.write(`── ${f.path} ──\n${f.contents}\n`)
|
|
207
|
-
|
|
203
|
+
ui.print('out', ui.report(`vx init ${VERSION}`, [...rows, null, ['next', next]]))
|
|
208
204
|
return 0
|
|
209
205
|
}
|
|
210
206
|
for (const f of files) await Bun.write(path.join(root, f.path), f.contents)
|
|
211
207
|
for (const f of replaced) await unlink(path.join(root, f))
|
|
212
208
|
// The lock that follows loads the workspace file just written.
|
|
213
209
|
workspaceFileWritten()
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
210
|
+
const lock = await lockAfterMigration(root)
|
|
211
|
+
rows.push(['lock', lock.code === 0 ? lock.said.replace(/^locked /, '') : 'not written'], null, [
|
|
212
|
+
'next',
|
|
213
|
+
lock.code === 0 ? next : `fix the above, then vx lock and ${next}`,
|
|
214
|
+
])
|
|
215
|
+
ui.print('out', ui.report(`vx init ${VERSION}`, rows))
|
|
217
216
|
return 0
|
|
218
217
|
}
|
|
219
218
|
|
package/src/cli/last.ts
CHANGED
|
@@ -9,22 +9,24 @@ import { Cache } from '../cache/index.js'
|
|
|
9
9
|
import { formatBytes } from './format.js'
|
|
10
10
|
import { flagHint, formatValue, seeHelp } from './help.js'
|
|
11
11
|
import {
|
|
12
|
-
currentBranch,
|
|
13
|
-
exitSignal,
|
|
14
12
|
type FailureMode,
|
|
15
13
|
getInvocation,
|
|
16
14
|
getRun,
|
|
17
15
|
type InvocationDetail,
|
|
18
16
|
listInvocations,
|
|
19
17
|
LocalHistoryProvider,
|
|
20
|
-
outcomeWord,
|
|
21
18
|
type RunSummaryRow,
|
|
19
|
+
} from '../history/index.js'
|
|
20
|
+
import {
|
|
21
|
+
currentBranch,
|
|
22
|
+
exitSignal,
|
|
22
23
|
resolveRunId,
|
|
23
24
|
runFailures,
|
|
24
25
|
shortRunId,
|
|
25
26
|
taskLog,
|
|
26
27
|
} from '../orchestrator/index.js'
|
|
27
|
-
import {
|
|
28
|
+
import { outcomeWord } from '../report/index.js'
|
|
29
|
+
import { formatElapsed, refusal, splitTaskId, UserError, sayNotice, ui } from '../util/index.js'
|
|
28
30
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
29
31
|
import { cliCacheDir, parseCacheDirFlag } from './workspace-config.js'
|
|
30
32
|
|
|
@@ -242,7 +244,7 @@ async function printStats(
|
|
|
242
244
|
return 0
|
|
243
245
|
}
|
|
244
246
|
if (stats.length === 0) {
|
|
245
|
-
|
|
247
|
+
ui.print('out', ui.status('info', 'vx last', 'no runs yet'))
|
|
246
248
|
return 0
|
|
247
249
|
}
|
|
248
250
|
const pct = (r: number): string => `${Math.round(r * 100)}%`
|
|
@@ -319,6 +321,7 @@ function reasonParts(t: RunSummaryRow): string {
|
|
|
319
321
|
} else if (t.status === 'skipped' && t.blockedBy !== null) {
|
|
320
322
|
parts.push(`after ${t.blockedBy} failed`)
|
|
321
323
|
}
|
|
324
|
+
if (t.remoteError !== null) parts.push(`remote \u2717 ${t.remoteError}`)
|
|
322
325
|
return parts.map((p) => ` ${p}`).join('')
|
|
323
326
|
}
|
|
324
327
|
|
|
@@ -468,8 +471,10 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
468
471
|
return 0
|
|
469
472
|
}
|
|
470
473
|
if (log.source === null) {
|
|
471
|
-
|
|
472
|
-
|
|
474
|
+
sayNotice(
|
|
475
|
+
'info',
|
|
476
|
+
'vx last',
|
|
477
|
+
`${log.taskId} ended ${log.status}. vx keeps output only for failed and cached tasks`,
|
|
473
478
|
)
|
|
474
479
|
}
|
|
475
480
|
process.stdout.write(log.output)
|
|
@@ -486,9 +491,8 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
486
491
|
return 0
|
|
487
492
|
}
|
|
488
493
|
if (invocations.length === 0) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
)
|
|
494
|
+
const none = parsed.failed === true && !noRuns() ? 'no failed runs' : 'no runs yet'
|
|
495
|
+
ui.print('out', ui.status('info', 'vx last', none))
|
|
492
496
|
return 0
|
|
493
497
|
}
|
|
494
498
|
const lines = formatRunList(
|
|
@@ -504,6 +508,12 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
504
508
|
: parsed.failed === true
|
|
505
509
|
? (failedInvocations(db, 1)[0] ?? null)
|
|
506
510
|
: (listInvocations(db, { limit: 1 })[0] ?? null)
|
|
511
|
+
if ((inv === null || inv === undefined) && noRuns()) {
|
|
512
|
+
throw refusal(
|
|
513
|
+
{ tone: 'info', headline: 'no runs yet', fix: 'vx run' },
|
|
514
|
+
{ code: 'VX_E_NO_HISTORY', message: 'vx last: no recorded runs yet — run something first' },
|
|
515
|
+
)
|
|
516
|
+
}
|
|
507
517
|
if (inv === null || inv === undefined) {
|
|
508
518
|
throw new UserError(
|
|
509
519
|
noRuns()
|
package/src/cli/lock.ts
CHANGED
|
@@ -17,11 +17,14 @@ import {
|
|
|
17
17
|
} from '../orchestrator/index.js'
|
|
18
18
|
import { resolveSetting, runRules } from '../settings.js'
|
|
19
19
|
import {
|
|
20
|
-
|
|
20
|
+
refusalNotice,
|
|
21
21
|
DOT_VX_GITATTRIBUTES,
|
|
22
22
|
isUserError,
|
|
23
23
|
relPosix,
|
|
24
|
+
type RefusalParts,
|
|
24
25
|
secretMask,
|
|
26
|
+
ui,
|
|
27
|
+
sayNotice,
|
|
25
28
|
} from '../util/index.js'
|
|
26
29
|
import {
|
|
27
30
|
findProjectConfigs,
|
|
@@ -84,7 +87,7 @@ export async function lockCmd(args: readonly string[]): Promise<number> {
|
|
|
84
87
|
|
|
85
88
|
/**
|
|
86
89
|
* Projects with tasks and no vx.config: a plugin's tasks, which load live
|
|
87
|
-
* in every run, so both verbs name them — `
|
|
90
|
+
* in every run, so both verbs name them — `0 project configs`
|
|
88
91
|
* on a turbo() repo read like an audit of something (2026-09-16). One with
|
|
89
92
|
* no task is left out: solidjs/solid's solid-ssr, which has no script Turbo
|
|
90
93
|
* runs, read as a project whose tasks come live from the plugin `project` stage.
|
|
@@ -100,13 +103,14 @@ function bareProjects(all: readonly ProjectMeta[], tasked: ReadonlySet<string>):
|
|
|
100
103
|
export async function lockWorkspace(
|
|
101
104
|
root: string,
|
|
102
105
|
warn: (message: string) => void = warnToStderr,
|
|
106
|
+
say: (line: string) => void = (line) => ui.print('out', ui.status('ok', 'vx lock', line)),
|
|
103
107
|
): Promise<number> {
|
|
104
108
|
const check = await audit(root, warn)
|
|
105
109
|
if (check.broken !== null) {
|
|
106
|
-
|
|
110
|
+
notWritten(check.broken, check.brokenParts)
|
|
107
111
|
return 1
|
|
108
112
|
}
|
|
109
|
-
return await writeLock(root, check)
|
|
113
|
+
return await writeLock(root, check, say)
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
/**
|
|
@@ -159,6 +163,7 @@ async function audit(root: string, warn: (message: string) => void): Promise<Gra
|
|
|
159
163
|
closure: false,
|
|
160
164
|
configs: lock,
|
|
161
165
|
warn,
|
|
166
|
+
lock: true,
|
|
162
167
|
})
|
|
163
168
|
for (const entry of projects.values()) resolved.set(entry.name, entry.config)
|
|
164
169
|
const names = new Set<string>()
|
|
@@ -193,7 +198,8 @@ async function audit(root: string, warn: (message: string) => void): Promise<Gra
|
|
|
193
198
|
rules,
|
|
194
199
|
})
|
|
195
200
|
} catch (err) {
|
|
196
|
-
if (isUserError(err))
|
|
201
|
+
if (isUserError(err))
|
|
202
|
+
return { ...out, broken: err.message, ...(err.parts ? { brokenParts: err.parts } : {}) }
|
|
197
203
|
throw err
|
|
198
204
|
}
|
|
199
205
|
return out
|
|
@@ -202,6 +208,7 @@ async function audit(root: string, warn: (message: string) => void): Promise<Gra
|
|
|
202
208
|
interface GraphCheck {
|
|
203
209
|
/** A graph rule's refusal, or a config that does not load; null when sound. */
|
|
204
210
|
broken: string | null
|
|
211
|
+
brokenParts?: RefusalParts
|
|
205
212
|
/** The lock the configs evaluate to here. */
|
|
206
213
|
fresh: Lockfile
|
|
207
214
|
/** Every project, as a run discovers them from `fresh`. */
|
|
@@ -219,23 +226,25 @@ function configured(all: readonly ProjectMeta[]): ConfiguredMeta[] {
|
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
/**
|
|
222
|
-
* `vx init`'s last step: the lock, as `vx lock` writes it
|
|
223
|
-
*
|
|
224
|
-
* written configs in place.
|
|
229
|
+
* `vx init`'s last step: the lock, as `vx lock` writes it, its line
|
|
230
|
+
* returned for the report's `lock` row. A refusal (a graph rule, a config
|
|
231
|
+
* that does not load) is printed and leaves the written configs in place.
|
|
225
232
|
*/
|
|
226
|
-
export async function lockAfterMigration(root: string): Promise<number> {
|
|
233
|
+
export async function lockAfterMigration(root: string): Promise<{ code: number; said: string }> {
|
|
234
|
+
let said = ''
|
|
227
235
|
try {
|
|
228
|
-
|
|
236
|
+
const code = await lockWorkspace(root, warnToStderr, (line) => (said = line))
|
|
237
|
+
return { code, said }
|
|
229
238
|
} catch (err) {
|
|
230
239
|
if (!isUserError(err)) throw err
|
|
231
|
-
|
|
232
|
-
return 1
|
|
240
|
+
notWritten(err.message, err.parts)
|
|
241
|
+
return { code: 1, said }
|
|
233
242
|
}
|
|
234
243
|
}
|
|
235
244
|
|
|
236
|
-
/**
|
|
237
|
-
function
|
|
238
|
-
|
|
245
|
+
/** The lock refused: the refusal's own parts, or its config line and frame, or its message. */
|
|
246
|
+
function notWritten(message: string, parts: RefusalParts | undefined): void {
|
|
247
|
+
ui.print('err', refusalNotice('vx lock', message, parts, { headline: 'lock not written' }))
|
|
239
248
|
}
|
|
240
249
|
|
|
241
250
|
/** Names shown per note: a turbo() repo can have hundreds of bare projects. */
|
|
@@ -295,7 +304,11 @@ async function writeIfMissing(file: string, text: string): Promise<void> {
|
|
|
295
304
|
}
|
|
296
305
|
}
|
|
297
306
|
|
|
298
|
-
async function writeLock(
|
|
307
|
+
async function writeLock(
|
|
308
|
+
root: string,
|
|
309
|
+
check: GraphCheck,
|
|
310
|
+
say: (line: string) => void,
|
|
311
|
+
): Promise<number> {
|
|
299
312
|
const metas = configured(check.all)
|
|
300
313
|
const bare = bareProjects(check.all, check.tasked)
|
|
301
314
|
// By project name, as the refusal names them.
|
|
@@ -310,11 +323,12 @@ async function writeLock(root: string, check: GraphCheck): Promise<number> {
|
|
|
310
323
|
for (const l of secretsIn(Object.fromEntries(reviews.map((r) => [r.name, r.config]))))
|
|
311
324
|
if (!named.has(l.at)) leaks.push(l)
|
|
312
325
|
if (leaks.length > 0) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
326
|
+
notWritten('', {
|
|
327
|
+
headline: 'these configs evaluated to a secret value',
|
|
328
|
+
what: leaks.map((l) => l.at),
|
|
329
|
+
why: 'the lock is committed, so the secret would be too',
|
|
330
|
+
fix: `let the shell expand it ($${leaks[0]!.name} in the command, the name in exec.env.passThrough) instead of reading process.env in the config`,
|
|
331
|
+
})
|
|
318
332
|
return 1
|
|
319
333
|
}
|
|
320
334
|
await writeLockfile(root, check.fresh)
|
|
@@ -323,9 +337,7 @@ async function writeLock(root: string, check: GraphCheck): Promise<number> {
|
|
|
323
337
|
writeIfMissing(path.join(root, '.vx', '.gitattributes'), DOT_VX_GITATTRIBUTES),
|
|
324
338
|
])
|
|
325
339
|
const n = metas.length
|
|
326
|
-
|
|
327
|
-
`vx lock: locked ${n} project config${n === 1 ? '' : 's'} → ${LOCKFILE_NAME}${bareNote(bare)}\n`,
|
|
328
|
-
)
|
|
340
|
+
say(`${n} project config${n === 1 ? '' : 's'} → ${LOCKFILE_NAME}${bareNote(bare)}`)
|
|
329
341
|
return 0
|
|
330
342
|
}
|
|
331
343
|
|
|
@@ -389,7 +401,7 @@ async function checkLock(root: string, json: boolean): Promise<number> {
|
|
|
389
401
|
const lock = await readLockfile(root)
|
|
390
402
|
if (!lock) {
|
|
391
403
|
const problem = `no ${LOCKFILE_NAME} at ${root} — run \`vx lock\` first`
|
|
392
|
-
if (!json)
|
|
404
|
+
if (!json) sayNotice('error', 'vx lock --check', problem)
|
|
393
405
|
return answer([problem])
|
|
394
406
|
}
|
|
395
407
|
const failures: string[] = []
|
|
@@ -451,12 +463,12 @@ async function checkLock(root: string, json: boolean): Promise<number> {
|
|
|
451
463
|
}
|
|
452
464
|
if (json) return answer(failures)
|
|
453
465
|
if (failures.length > 0) {
|
|
454
|
-
for (const f of failures)
|
|
466
|
+
for (const f of failures) sayNotice('error', 'vx lock --check', f)
|
|
455
467
|
return 1
|
|
456
468
|
}
|
|
457
469
|
const n = metas.length
|
|
458
470
|
const audited = `${n} project${n === 1 ? '' : 's'}`
|
|
459
471
|
const skipped = bare.length === 0 ? '' : `; not audited, no vx.config: ${bareNames(bare)}`
|
|
460
|
-
|
|
472
|
+
ui.print('out', ui.status('ok', 'vx lock --check', `up to date (${audited}${skipped})`))
|
|
461
473
|
return 0
|
|
462
474
|
}
|