@vzn/vx 0.0.16 → 0.0.21
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/LICENSE +1 -1
- package/README.md +135 -124
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +11 -7
- package/src/bin.ts +14 -4
- package/src/cache/archive.ts +659 -0
- package/src/cache/cache.ts +769 -1101
- package/src/cache/chained-cache.ts +194 -0
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +24 -5
- package/src/cache/inputs.ts +373 -456
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +96 -43
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/tar-stream.ts +358 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +75 -21
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +195 -128
- package/src/cli/index.ts +116 -46
- package/src/cli/info.ts +122 -63
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +172 -0
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +72 -6
- package/src/cli/plugin-commands.ts +103 -0
- package/src/cli/run.ts +245 -271
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +9 -7
- package/src/cli/watch.ts +858 -110
- package/src/cli/why.ts +223 -0
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +187 -150
- package/src/exec/env.ts +18 -1
- package/src/exec/executor.ts +224 -0
- package/src/exec/index.ts +24 -2
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +166 -40
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +642 -260
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +4 -10
- package/src/graph/scheduler.ts +232 -203
- package/src/graph/task-graph.ts +222 -8
- package/src/index.ts +146 -153
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +147 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +72 -8
- package/src/orchestrator/execute-task.ts +374 -549
- package/src/orchestrator/failure-mode.ts +202 -0
- package/src/orchestrator/framed-output.ts +126 -69
- package/src/orchestrator/history.ts +126 -59
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +77 -93
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +208 -35
- package/src/orchestrator/metrics.ts +102 -1388
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +55 -34
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +357 -120
- package/src/orchestrator/plugin.ts +298 -58
- package/src/orchestrator/prepare.ts +202 -106
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/run-artifacts.ts +63 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +74 -47
- package/src/orchestrator/run.ts +534 -417
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +129 -31
- package/src/orchestrator/status-line.ts +49 -9
- package/src/orchestrator/summary.ts +90 -26
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +111 -15
- package/src/orchestrator/task-log-buffer.ts +288 -0
- package/src/orchestrator/telemetry-host.ts +34 -6
- package/src/orchestrator/telemetry.ts +182 -36
- package/src/orchestrator/upstream.ts +72 -31
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +70 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +15 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +64 -0
- package/src/util/settle.ts +67 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +250 -23
- package/src/workspace/config-cache.ts +313 -0
- package/src/workspace/config-eval.ts +217 -0
- package/src/workspace/config-imports.ts +162 -0
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +65 -10
- package/src/workspace/fingerprint.ts +53 -4
- package/src/workspace/index.ts +42 -3
- package/src/workspace/lockfile.ts +25 -9
- package/src/workspace/migrate-scripts.ts +134 -0
- package/src/workspace/migration.ts +314 -0
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +114 -41
- package/src/workspace/project-loader.ts +224 -450
- package/src/workspace/workspace.ts +298 -72
- package/src/cache/cas-backend.ts +0 -97
- package/src/cache/digest.ts +0 -47
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/cli/migrate-nx.ts +0 -450
- package/src/cli/migrate-turbo.ts +0 -364
- package/src/cli/migrate.ts +0 -219
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/resources.ts +0 -59
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/verify.ts +0 -164
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
package/src/cli/run.ts
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { appendFile } from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
applyFilters,
|
|
6
|
-
buildPackageGraph,
|
|
7
|
-
defaultAffectedBase,
|
|
8
|
-
findWorkspaceRoot,
|
|
9
|
-
listProjects,
|
|
10
|
-
loadProjectConfig,
|
|
11
|
-
loadWorkspace,
|
|
12
|
-
loadWorkspaceConfig,
|
|
13
|
-
parseFilter,
|
|
14
|
-
resolveCacheDir,
|
|
15
|
-
type ProjectMeta,
|
|
16
|
-
} from '../workspace/index.js'
|
|
3
|
+
import { documentedFlags, seeHelp } from './help.js'
|
|
4
|
+
import { defaultAffectedBase, findWorkspaceRoot } from '../workspace/index.js'
|
|
17
5
|
import {
|
|
18
6
|
planRun,
|
|
19
7
|
formatRunReportMarkdown,
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
outcomeLabel,
|
|
9
|
+
projectOutcome,
|
|
10
|
+
run as runOrchestrator,
|
|
22
11
|
type OutcomeView,
|
|
23
12
|
type RunOptions,
|
|
24
13
|
type RunResult,
|
|
25
|
-
type VxPlugin,
|
|
26
14
|
} from '../orchestrator/index.js'
|
|
27
15
|
import type { ContinueMode } from '../graph/index.js'
|
|
28
16
|
import { type CachePolicy, FULL_CACHE_POLICY, parseCachePolicy } from '../cache/index.js'
|
|
29
|
-
import {
|
|
17
|
+
import { findCwdProject, pickTask, resolveFilters } from './select.js'
|
|
18
|
+
import { MAX_TIMEOUT_MS, parseDecimalInt, nearest, machineParallelism } from '../util/index.js'
|
|
30
19
|
import { formatGraphDot, formatPlanJson, formatPlanText } from './plan-format.js'
|
|
31
|
-
import { localBackend } from './backend.js'
|
|
32
20
|
|
|
33
21
|
export interface RunArgs {
|
|
34
22
|
/** Failure propagation (`--continue[=never|deps-ok|always]`). */
|
|
@@ -54,6 +42,8 @@ export interface RunArgs {
|
|
|
54
42
|
* / `--force` in precedence order. Defaults to all-on.
|
|
55
43
|
*/
|
|
56
44
|
cache: CachePolicy
|
|
45
|
+
/** `--cache` named a remote axis (`remote:r`, `:w`, `:rw`): the run says so when no plugin serves one. */
|
|
46
|
+
remoteRequested?: boolean
|
|
57
47
|
/** `--cache-dir <path>`: override the cache directory (cwd-relative). */
|
|
58
48
|
cacheDir: string | undefined
|
|
59
49
|
frozen: boolean
|
|
@@ -68,24 +58,8 @@ export interface RunArgs {
|
|
|
68
58
|
* env and workspace `timeout` defaults. Undefined when not passed.
|
|
69
59
|
*/
|
|
70
60
|
timeout: number | undefined
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* bytes) for `exec.resources.memory` reservations. Defaults to
|
|
74
|
-
* os.totalmem() when not passed — pass it in cgroup-limited containers.
|
|
75
|
-
*/
|
|
76
|
-
memory: number | undefined
|
|
77
|
-
/**
|
|
78
|
-
* `--verify[=determinism|inputs|fingerprint|all]`: cache-correctness
|
|
79
|
-
* verification. Undefined when not passed. `determinism` re-runs and
|
|
80
|
-
* content-compares outputs; `inputs` sandboxes with the declared-input
|
|
81
|
-
* baseline and flags undeclared reads; `fingerprint` ships output-tree
|
|
82
|
-
* fingerprints for the cross-machine diff (no re-run; the determinism
|
|
83
|
-
* modes set it too, for free). `allow` (from `--verify-allow=<pkg#task>,…`)
|
|
84
|
-
* exempts tasks from failing the run on a divergence.
|
|
85
|
-
*/
|
|
86
|
-
verify: { determinism: boolean; inputs: boolean; fingerprint: boolean } | undefined
|
|
87
|
-
verifyAllow: string[]
|
|
88
|
-
outputLogs?: 'full' | 'errors-only' | 'none'
|
|
61
|
+
outputLogs?: 'full' | 'errors-only' | 'none' | 'hash-only'
|
|
62
|
+
download?: 'all' | 'toplevel' | 'none'
|
|
89
63
|
forwardArgs: string[]
|
|
90
64
|
verbosity: number
|
|
91
65
|
dry: 'text' | 'json' | undefined
|
|
@@ -102,9 +76,39 @@ export interface RunArgs {
|
|
|
102
76
|
tags: Record<string, string>
|
|
103
77
|
/** `--report[=markdown]`. Undefined when the flag wasn't passed. */
|
|
104
78
|
report: 'markdown' | undefined
|
|
79
|
+
/**
|
|
80
|
+
* `--report-file=<path>`. Undefined when the flag wasn't passed. Writes
|
|
81
|
+
* the same markdown as `--report`, but to a file — because redirecting
|
|
82
|
+
* vx's stdout captures the whole run log, not just the report.
|
|
83
|
+
*/
|
|
84
|
+
reportFile: string | undefined
|
|
105
85
|
error?: string
|
|
106
86
|
}
|
|
107
87
|
|
|
88
|
+
// The camelCase spelling the flag shipped with (retired 2026-09-10): two
|
|
89
|
+
// edits past the suggester's reach, so it is named outright. A regex, not a
|
|
90
|
+
// quoted literal, so the doc-drift test does not count it as an accepted flag.
|
|
91
|
+
const RETIRED_EXCLUDE_DEPENDENCIES = /^--excludeDependencies(=|$)/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `--concurrency <n>` is a positive decimal integer, or `<n>%` of the
|
|
95
|
+
* cores this process may use (`50%` on eight cores is 4, never below 1;
|
|
96
|
+
* over 100% is allowed for I/O-bound work; a container's CPU quota caps
|
|
97
|
+
* the count the way it caps the default). The percent form resolves
|
|
98
|
+
* here, at parse time, so the run sees one number and the summary says
|
|
99
|
+
* what it ran with.
|
|
100
|
+
*/
|
|
101
|
+
export function parseConcurrency(v: string, cpus = machineParallelism()): number | null {
|
|
102
|
+
const pct = /^(\d+)%$/.exec(v)
|
|
103
|
+
if (pct) {
|
|
104
|
+
const n = parseDecimalInt(pct[1]!)
|
|
105
|
+
if (n === null || n < 1) return null
|
|
106
|
+
return Math.max(1, Math.round((Math.max(1, cpus) * n) / 100))
|
|
107
|
+
}
|
|
108
|
+
const n = parseDecimalInt(v)
|
|
109
|
+
return n === null || n < 1 ? null : n
|
|
110
|
+
}
|
|
111
|
+
|
|
108
112
|
export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
109
113
|
const out: RunArgs = {
|
|
110
114
|
tasks: [],
|
|
@@ -117,9 +121,6 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
117
121
|
frozen: false,
|
|
118
122
|
retries: undefined,
|
|
119
123
|
timeout: undefined,
|
|
120
|
-
memory: undefined,
|
|
121
|
-
verify: undefined,
|
|
122
|
-
verifyAllow: [],
|
|
123
124
|
forwardArgs: [],
|
|
124
125
|
verbosity: 0,
|
|
125
126
|
dry: undefined,
|
|
@@ -129,6 +130,7 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
129
130
|
affected: undefined,
|
|
130
131
|
tags: {},
|
|
131
132
|
report: undefined,
|
|
133
|
+
reportFile: undefined,
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
const sepIdx = args.indexOf('--')
|
|
@@ -146,22 +148,39 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
146
148
|
|
|
147
149
|
for (let i = 0; i < before.length; i++) {
|
|
148
150
|
const a = before[i]
|
|
149
|
-
if (a === '--filter') {
|
|
150
|
-
const v = before[++i]
|
|
151
|
-
if (v === undefined) return { ...out, error:
|
|
151
|
+
if (a === '--filter' || a?.startsWith('--filter=')) {
|
|
152
|
+
const v = a === '--filter' ? before[++i] : a.slice('--filter='.length)
|
|
153
|
+
if (v === undefined || v === '') return { ...out, error: `--filter requires a value` }
|
|
152
154
|
out.filters.push(v)
|
|
153
|
-
} else if (a === '--concurrency') {
|
|
154
|
-
const v = before[++i]
|
|
155
|
-
if (v === undefined) return { ...out, error:
|
|
156
|
-
const n =
|
|
157
|
-
if (
|
|
158
|
-
out.concurrency =
|
|
155
|
+
} else if (a === '--concurrency' || a?.startsWith('--concurrency=')) {
|
|
156
|
+
const v = a === '--concurrency' ? before[++i] : a.slice('--concurrency='.length)
|
|
157
|
+
if (v === undefined) return { ...out, error: `--concurrency requires a value` }
|
|
158
|
+
const n = parseConcurrency(v)
|
|
159
|
+
if (n === null) return { ...out, error: `invalid concurrency: ${v}` }
|
|
160
|
+
out.concurrency = n
|
|
159
161
|
} else if (a === '--all') {
|
|
160
162
|
out.all = true
|
|
161
|
-
} else if (a
|
|
163
|
+
} else if (RETIRED_EXCLUDE_DEPENDENCIES.test(a ?? '')) {
|
|
164
|
+
return {
|
|
165
|
+
...out,
|
|
166
|
+
error: `unknown flag: ${a} (the flag is --exclude-dependencies)${seeHelp('run')}`,
|
|
167
|
+
}
|
|
168
|
+
} else if (a === '--exclude-dependencies') {
|
|
162
169
|
out.excludeDependencies = 'all'
|
|
163
|
-
} else if (a?.startsWith('--
|
|
164
|
-
const raw = a.slice('--
|
|
170
|
+
} else if (a?.startsWith('--exclude-dependencies=')) {
|
|
171
|
+
const raw = a.slice('--exclude-dependencies='.length)
|
|
172
|
+
// An empty value is genuinely ambiguous — "drop every edge" (the
|
|
173
|
+
// bare flag) and "drop none" (an empty list) are equally defensible
|
|
174
|
+
// readings, and silently picking either does the opposite of what
|
|
175
|
+
// half the callers mean. Reject and name both explicit forms; the
|
|
176
|
+
// sibling value flags (--retry=, --timeout=, --cache-dir=)
|
|
177
|
+
// reject an empty `=` value the same way.
|
|
178
|
+
if (raw === '') {
|
|
179
|
+
return {
|
|
180
|
+
...out,
|
|
181
|
+
error: `--exclude-dependencies= needs a value — pass bare --exclude-dependencies to drop every dependsOn edge, or omit the flag to keep them`,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
165
184
|
out.excludeDependencies = raw
|
|
166
185
|
.split(',')
|
|
167
186
|
.map((s) => s.trim())
|
|
@@ -171,71 +190,70 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
171
190
|
} else if (a === '--retry' || a?.startsWith('--retry=')) {
|
|
172
191
|
const v = a === '--retry' ? before[++i] : a.slice('--retry='.length)
|
|
173
192
|
if (v === undefined) return { ...out, error: `--retry requires a value` }
|
|
174
|
-
const n =
|
|
175
|
-
if (
|
|
193
|
+
const n = parseDecimalInt(v)
|
|
194
|
+
if (n === null) {
|
|
176
195
|
return { ...out, error: `--retry must be a non-negative integer, got: ${v}` }
|
|
177
196
|
}
|
|
178
197
|
out.retries = n
|
|
179
198
|
} else if (a === '--timeout' || a?.startsWith('--timeout=')) {
|
|
180
199
|
const v = a === '--timeout' ? before[++i] : a.slice('--timeout='.length)
|
|
181
200
|
if (v === undefined) return { ...out, error: `--timeout requires a value` }
|
|
182
|
-
const n =
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
out.timeout = n
|
|
187
|
-
} else if (a === '--memory' || a?.startsWith('--memory=')) {
|
|
188
|
-
const v = a === '--memory' ? before[++i] : a.slice('--memory='.length)
|
|
189
|
-
if (v === undefined || v === '') return { ...out, error: `--memory requires a value` }
|
|
190
|
-
const bytes = parseSize(v)
|
|
191
|
-
if (bytes === null || bytes <= 0) {
|
|
192
|
-
return { ...out, error: `--memory must be a size like 8GB or 512MB, got: ${v}` }
|
|
193
|
-
}
|
|
194
|
-
out.memory = bytes
|
|
195
|
-
} else if (a === '--verify' || a?.startsWith('--verify=')) {
|
|
196
|
-
const what = a === '--verify' ? 'determinism' : a.slice('--verify='.length)
|
|
197
|
-
if (what === 'determinism') {
|
|
198
|
-
out.verify = { determinism: true, inputs: false, fingerprint: true }
|
|
199
|
-
} else if (what === 'inputs') {
|
|
200
|
-
out.verify = { determinism: false, inputs: true, fingerprint: false }
|
|
201
|
-
} else if (what === 'fingerprint') {
|
|
202
|
-
out.verify = { determinism: false, inputs: false, fingerprint: true }
|
|
203
|
-
} else if (what === 'all') {
|
|
204
|
-
out.verify = { determinism: true, inputs: true, fingerprint: true }
|
|
205
|
-
} else {
|
|
201
|
+
const n = parseDecimalInt(v)
|
|
202
|
+
if (n !== null && n > MAX_TIMEOUT_MS) {
|
|
203
|
+
// Not "effectively no limit" — setTimeout reduces a delay past 2^31-1
|
|
204
|
+
// to 1 ms, so this would kill every task the moment it spawned.
|
|
206
205
|
return {
|
|
207
206
|
...out,
|
|
208
|
-
error:
|
|
207
|
+
error:
|
|
208
|
+
`--timeout ${n} exceeds the maximum timer delay (${MAX_TIMEOUT_MS} ms, ~24.8 days); ` +
|
|
209
|
+
`larger values are silently reduced to 1 ms. Omit --timeout for no limit.`,
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (v !== 'full' && v !== 'errors-only' && v !== 'none') {
|
|
220
|
-
return { ...out, error: `--output-logs must be full, errors-only, or none` }
|
|
212
|
+
if (n === null || n <= 0) {
|
|
213
|
+
return { ...out, error: `--timeout must be a positive integer (ms), got: ${v}` }
|
|
214
|
+
}
|
|
215
|
+
out.timeout = n
|
|
216
|
+
} else if (a === '--output-logs' || a?.startsWith('--output-logs=')) {
|
|
217
|
+
const v = a === '--output-logs' ? before[++i] : a.slice('--output-logs='.length)
|
|
218
|
+
if (v !== 'full' && v !== 'errors-only' && v !== 'none' && v !== 'hash-only') {
|
|
219
|
+
return { ...out, error: `--output-logs must be full, errors-only, hash-only, or none` }
|
|
221
220
|
}
|
|
222
221
|
out.outputLogs = v
|
|
222
|
+
} else if (a === '--download' || a?.startsWith('--download=')) {
|
|
223
|
+
const v = a === '--download' ? before[++i] : a.slice('--download='.length)
|
|
224
|
+
if (v !== 'all' && v !== 'toplevel' && v !== 'none') {
|
|
225
|
+
return { ...out, error: `--download must be all, toplevel, or none` }
|
|
226
|
+
}
|
|
227
|
+
out.download = v
|
|
223
228
|
} else if (a === '--cache-dir' || a?.startsWith('--cache-dir=')) {
|
|
224
229
|
const v = a === '--cache-dir' ? before[++i] : a.slice('--cache-dir='.length)
|
|
225
230
|
if (v === undefined || v === '') return { ...out, error: `--cache-dir requires a value` }
|
|
231
|
+
// Unlike every other value flag, a cache dir is an arbitrary
|
|
232
|
+
// string — nothing about its shape rejects a swallowed flag. An
|
|
233
|
+
// unquoted empty shell var (`--cache-dir $EMPTY --force`) would
|
|
234
|
+
// otherwise create a directory literally named `--force` and drop
|
|
235
|
+
// the flag. A path starting with `-` needs the `=` form.
|
|
236
|
+
if (a === '--cache-dir' && v.startsWith('-')) {
|
|
237
|
+
return { ...out, error: `--cache-dir requires a path, got flag: ${v}` }
|
|
238
|
+
}
|
|
226
239
|
out.cacheDir = v
|
|
227
|
-
} else if (a === '--cache') {
|
|
228
|
-
const v = before[++i]
|
|
229
|
-
if (v === undefined) return { ...out, error:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
} else if (a === '--cache' || a?.startsWith('--cache=')) {
|
|
241
|
+
const v = a === '--cache' ? before[++i] : a.slice('--cache='.length)
|
|
242
|
+
if (v === undefined) return { ...out, error: `--cache requires a value` }
|
|
243
|
+
// A spec with no segment at all applies NOTHING, so `--cache=`
|
|
244
|
+
// (an empty shell var, or someone reaching for "no cache") left
|
|
245
|
+
// all four axes ON — the opposite of the intent. Reject it; an
|
|
246
|
+
// empty flag list for a NAMED layer (`local:`) stays valid and
|
|
247
|
+
// means that layer off.
|
|
248
|
+
if (!v.split(',').some((seg) => seg.trim().length > 0)) {
|
|
249
|
+
return {
|
|
250
|
+
...out,
|
|
251
|
+
error: `--cache needs a spec like local:r, local:rw, remote:, or local:,remote:rw — pass --no-cache to disable every axis`,
|
|
252
|
+
}
|
|
234
253
|
}
|
|
235
|
-
} else if (a?.startsWith('--cache=')) {
|
|
236
|
-
const v = a.slice('--cache='.length)
|
|
237
254
|
try {
|
|
238
255
|
cachePolicy = parseCachePolicy(v, cachePolicy)
|
|
256
|
+
if (/remote:[rw]/.test(v)) out.remoteRequested = true
|
|
239
257
|
} catch (err) {
|
|
240
258
|
cacheSpecError = err instanceof Error ? err.message : String(err)
|
|
241
259
|
}
|
|
@@ -244,7 +262,14 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
244
262
|
} else if (a === '--force') {
|
|
245
263
|
forceFlag = true
|
|
246
264
|
} else if (a === '--continue') {
|
|
247
|
-
// Bare --continue = 'always' (the Turbo convention).
|
|
265
|
+
// Bare --continue = 'always' (the Turbo convention). Its mode comes
|
|
266
|
+
// with '=' only: `--continue never` would read `never` as a TASK, and
|
|
267
|
+
// the run would then say "No projects declare task(s): never" — true,
|
|
268
|
+
// and no help at all.
|
|
269
|
+
const next = before[i + 1]
|
|
270
|
+
if (next === 'never' || next === 'deps-ok' || next === 'always') {
|
|
271
|
+
return { ...out, error: `--continue takes its mode with '=': --continue=${next}` }
|
|
272
|
+
}
|
|
248
273
|
out.continueMode = 'always'
|
|
249
274
|
} else if (a?.startsWith('--continue=')) {
|
|
250
275
|
const v = a.slice('--continue='.length)
|
|
@@ -252,11 +277,11 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
252
277
|
return { ...out, error: `--continue must be never, deps-ok, or always` }
|
|
253
278
|
}
|
|
254
279
|
out.continueMode = v
|
|
255
|
-
} else if (a === '--verbosity') {
|
|
256
|
-
const v = before[++i]
|
|
257
|
-
if (v === undefined) return { ...out, error:
|
|
258
|
-
const n =
|
|
259
|
-
if (
|
|
280
|
+
} else if (a === '--verbosity' || a?.startsWith('--verbosity=')) {
|
|
281
|
+
const v = a === '--verbosity' ? before[++i] : a.slice('--verbosity='.length)
|
|
282
|
+
if (v === undefined) return { ...out, error: `--verbosity requires a value` }
|
|
283
|
+
const n = parseDecimalInt(v)
|
|
284
|
+
if (n === null) return { ...out, error: `invalid verbosity: ${v}` }
|
|
260
285
|
out.verbosity = n
|
|
261
286
|
} else if (a === '--dry') {
|
|
262
287
|
out.dry = 'text'
|
|
@@ -277,7 +302,11 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
277
302
|
} else if (a === '--profile') {
|
|
278
303
|
out.profile = 'profile.json'
|
|
279
304
|
} else if (a?.startsWith('--profile=')) {
|
|
280
|
-
|
|
305
|
+
// An optional-value flag: an empty `=` means "no value given", so it
|
|
306
|
+
// takes the same documented default as the bare form. Resolving ''
|
|
307
|
+
// against cwd would name the cwd DIRECTORY and die with EISDIR after
|
|
308
|
+
// the whole run — and `--summarize=` already degrades to its default.
|
|
309
|
+
out.profile = a.slice('--profile='.length) || 'profile.json'
|
|
281
310
|
} else if (a === '--affected') {
|
|
282
311
|
out.affected = ''
|
|
283
312
|
} else if (a?.startsWith('--affected=')) {
|
|
@@ -289,6 +318,16 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
289
318
|
const eq = raw.indexOf('=')
|
|
290
319
|
if (eq <= 0) return { ...out, error: `invalid --tag (expected k=v): ${raw}` }
|
|
291
320
|
out.tags[raw.slice(0, eq)] = raw.slice(eq + 1)
|
|
321
|
+
} else if (a === '--report-file' || a?.startsWith('--report-file=')) {
|
|
322
|
+
const v = a === '--report-file' ? before[++i] : a.slice('--report-file='.length)
|
|
323
|
+
if (v === undefined || v === '') return { ...out, error: `--report-file requires a path` }
|
|
324
|
+
// Same reasoning as --cache-dir: a path is an arbitrary string, so
|
|
325
|
+
// nothing about its shape rejects a swallowed flag. A path starting
|
|
326
|
+
// with `-` needs the `=` form.
|
|
327
|
+
if (a === '--report-file' && v.startsWith('-')) {
|
|
328
|
+
return { ...out, error: `--report-file requires a path, got flag: ${v}` }
|
|
329
|
+
}
|
|
330
|
+
out.reportFile = v
|
|
292
331
|
} else if (a === '--report') {
|
|
293
332
|
out.report = 'markdown'
|
|
294
333
|
} else if (a?.startsWith('--report=')) {
|
|
@@ -298,7 +337,7 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
298
337
|
}
|
|
299
338
|
out.report = fmt
|
|
300
339
|
} else if (a !== undefined && a.startsWith('-')) {
|
|
301
|
-
return { ...out, error: `unknown flag: ${a}` }
|
|
340
|
+
return { ...out, error: `unknown flag: ${a}${didYouMeanFlag(a)}${seeHelp('run')}` }
|
|
302
341
|
} else if (a !== undefined) {
|
|
303
342
|
out.tasks.push(a)
|
|
304
343
|
}
|
|
@@ -351,9 +390,11 @@ export function detectFlow(
|
|
|
351
390
|
* honor the same selection semantics (cwd / `--all` / `--filter` /
|
|
352
391
|
* `--affected` / anchored positionals).
|
|
353
392
|
*
|
|
354
|
-
* Returns
|
|
355
|
-
*
|
|
356
|
-
*
|
|
393
|
+
* Returns the resolved options, an error message, or `nothingSelected` —
|
|
394
|
+
* the selection legitimately resolved to zero projects (nothing changed
|
|
395
|
+
* since the `--affected` base), which is a clean exit, not a failure. The
|
|
396
|
+
* caller prefixes messages with its subcommand name (`vx run: <msg>` /
|
|
397
|
+
* `vx watch: <msg>`).
|
|
357
398
|
*
|
|
358
399
|
* Assumes the caller has already populated `parsed.tasks` (e.g. via
|
|
359
400
|
* the interactive picker for `vx run`).
|
|
@@ -362,7 +403,7 @@ export async function resolveRunOptions(
|
|
|
362
403
|
parsed: RunArgs,
|
|
363
404
|
cwd: string,
|
|
364
405
|
tasks: readonly string[],
|
|
365
|
-
): Promise<RunOptions | { error: string }> {
|
|
406
|
+
): Promise<RunOptions | { error: string } | { nothingSelected: string }> {
|
|
366
407
|
for (const t of tasks) {
|
|
367
408
|
const idx = t.indexOf('#')
|
|
368
409
|
if (idx >= 0) {
|
|
@@ -387,13 +428,32 @@ export async function resolveRunOptions(
|
|
|
387
428
|
// Project scope applies to bare task names only. Anchored entries
|
|
388
429
|
// (pkg#task) resolve directly to their own project regardless.
|
|
389
430
|
const bareTasks = tasks.filter((t) => !t.includes('#'))
|
|
431
|
+
const anchoredTasks = tasks.filter((t) => t.includes('#'))
|
|
390
432
|
let projects: string[] | undefined
|
|
433
|
+
let staged: RunOptions['staged']
|
|
391
434
|
if (bareTasks.length === 0) {
|
|
392
435
|
projects = undefined
|
|
393
436
|
} else if (filterStrings.length > 0) {
|
|
394
|
-
const resolved = await resolveFilters(cwd, filterStrings
|
|
395
|
-
|
|
396
|
-
|
|
437
|
+
const resolved = await resolveFilters(cwd, filterStrings, {
|
|
438
|
+
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
439
|
+
...(parsed.frozen ? { frozen: true } : {}),
|
|
440
|
+
})
|
|
441
|
+
if ('error' in resolved) return { error: resolved.error }
|
|
442
|
+
if ('empty' in resolved) {
|
|
443
|
+
// "Nothing changed" is a clean exit for the BARE tasks the filter
|
|
444
|
+
// scopes — but it must never cancel an explicitly anchored
|
|
445
|
+
// `pkg#task`, which resolves to its own project regardless of
|
|
446
|
+
// scope. An empty project list keeps the bare tasks unselected
|
|
447
|
+
// while the anchored ones still run.
|
|
448
|
+
if (anchoredTasks.length === 0) return { nothingSelected: resolved.empty }
|
|
449
|
+
process.stderr.write(`vx: ${resolved.empty} — running ${anchoredTasks.join(', ')} only\n`)
|
|
450
|
+
projects = []
|
|
451
|
+
} else {
|
|
452
|
+
projects = resolved.names
|
|
453
|
+
// The graph walk staged every config; the run reuses those entries
|
|
454
|
+
// instead of evaluating and staging them a second time.
|
|
455
|
+
staged = resolved.staged
|
|
456
|
+
}
|
|
397
457
|
} else if (parsed.all) {
|
|
398
458
|
projects = undefined
|
|
399
459
|
} else {
|
|
@@ -411,9 +471,11 @@ export async function resolveRunOptions(
|
|
|
411
471
|
cwd,
|
|
412
472
|
tasks: [...tasks],
|
|
413
473
|
cache: parsed.cache,
|
|
474
|
+
...(parsed.remoteRequested ? { remoteRequested: true } : {}),
|
|
414
475
|
flow: detectFlow(parsed),
|
|
415
476
|
...(parsed.frozen ? { frozen: true } : {}),
|
|
416
477
|
...(parsed.outputLogs !== undefined ? { outputLogs: parsed.outputLogs } : {}),
|
|
478
|
+
...(parsed.download !== undefined ? { download: parsed.download } : {}),
|
|
417
479
|
...(parsed.continueMode !== undefined ? { continueMode: parsed.continueMode } : {}),
|
|
418
480
|
forwardArgs: parsed.forwardArgs,
|
|
419
481
|
}
|
|
@@ -423,18 +485,10 @@ export async function resolveRunOptions(
|
|
|
423
485
|
opts.excludeDependencies = parsed.excludeDependencies
|
|
424
486
|
}
|
|
425
487
|
if (projects !== undefined) opts.projects = projects
|
|
488
|
+
if (staged !== undefined) opts.staged = staged
|
|
426
489
|
if (parsed.retries !== undefined) opts.retries = parsed.retries
|
|
427
490
|
if (parsed.timeout !== undefined) opts.timeout = parsed.timeout
|
|
428
|
-
if (parsed.memory !== undefined) opts.memory = parsed.memory
|
|
429
491
|
if (parsed.cacheDir !== undefined) opts.cacheDir = parsed.cacheDir
|
|
430
|
-
if (parsed.verify !== undefined) {
|
|
431
|
-
opts.verify = {
|
|
432
|
-
determinism: parsed.verify.determinism,
|
|
433
|
-
inputs: parsed.verify.inputs,
|
|
434
|
-
fingerprint: parsed.verify.fingerprint,
|
|
435
|
-
allow: new Set(parsed.verifyAllow),
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
492
|
if (parsed.concurrency !== undefined) opts.concurrency = parsed.concurrency
|
|
439
493
|
if (parsed.summarize !== undefined) opts.summarize = parsed.summarize
|
|
440
494
|
if (parsed.profile !== undefined) opts.profile = parsed.profile
|
|
@@ -461,7 +515,14 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
461
515
|
process.stderr.write(`vx run: missing task name (stdin is not a TTY)\n`)
|
|
462
516
|
return 1
|
|
463
517
|
}
|
|
464
|
-
const picked = await pickTask(
|
|
518
|
+
const picked = await pickTask(
|
|
519
|
+
cwd,
|
|
520
|
+
{},
|
|
521
|
+
{
|
|
522
|
+
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
523
|
+
...(parsed.frozen ? { frozen: true } : {}),
|
|
524
|
+
},
|
|
525
|
+
)
|
|
465
526
|
if (!picked) return 1
|
|
466
527
|
tasks = [`${picked.project}#${picked.task}`]
|
|
467
528
|
}
|
|
@@ -471,6 +532,10 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
471
532
|
process.stderr.write(`vx run: ${resolved.error}\n`)
|
|
472
533
|
return 1
|
|
473
534
|
}
|
|
535
|
+
if ('nothingSelected' in resolved) {
|
|
536
|
+
process.stderr.write(`vx run: ${resolved.nothingSelected}\n`)
|
|
537
|
+
return 0
|
|
538
|
+
}
|
|
474
539
|
const opts = resolved
|
|
475
540
|
// The raw invocation, recorded on the `invocations` row so dashboards
|
|
476
541
|
// show what was actually run. `args` is everything after `run`.
|
|
@@ -480,6 +545,12 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
480
545
|
// graph + probe the cache; the difference is just the formatter.
|
|
481
546
|
if (parsed.dry !== undefined || parsed.graph !== undefined) {
|
|
482
547
|
const plan = await planRun(opts)
|
|
548
|
+
if (plan.unresolvedTasks !== undefined && plan.unresolvedTasks.length > 0) {
|
|
549
|
+
process.stderr.write(
|
|
550
|
+
`vx run: no projects declare task(s): ${plan.unresolvedTasks.join(', ')}.\n`,
|
|
551
|
+
)
|
|
552
|
+
return 1
|
|
553
|
+
}
|
|
483
554
|
if (plan.tasks.length === 0) {
|
|
484
555
|
process.stderr.write(`vx run: no projects declare task(s): ${tasks.join(', ')}.\n`)
|
|
485
556
|
return 1
|
|
@@ -499,134 +570,41 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
499
570
|
return 0
|
|
500
571
|
}
|
|
501
572
|
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
const workspaceConfig = await loadWorkspaceConfig(root)
|
|
509
|
-
const plugins = (workspaceConfig?.plugins ?? []) as readonly VxPlugin[]
|
|
510
|
-
const backend = await resolvePluginBackend(
|
|
511
|
-
plugins,
|
|
512
|
-
{
|
|
513
|
-
workspaceRoot: root,
|
|
514
|
-
cacheDir: resolveCacheDir(root, workspaceConfig),
|
|
515
|
-
warn: (m) => process.stderr.write(`${m}\n`),
|
|
516
|
-
request,
|
|
517
|
-
},
|
|
518
|
-
() => Promise.resolve(localBackend()),
|
|
519
|
-
)
|
|
520
|
-
const result = await backend.run(request)
|
|
573
|
+
// A run executes in THIS process, always. Where an individual task's
|
|
574
|
+
// command runs is the `executor` capability's business (per task, with
|
|
575
|
+
// the scheduler, cache, retries and telemetry unchanged above it); there
|
|
576
|
+
// is no whole-run delegation seam to consult.
|
|
577
|
+
const summary = await runOrchestrator(opts)
|
|
578
|
+
const result: RunResult = { ok: summary.ok, outcomes: summary.outcomes.map(projectOutcome) }
|
|
521
579
|
if (parsed.verbosity > 0) printSummary(result)
|
|
522
|
-
// Report generation is post-run, gated on the
|
|
523
|
-
// absent.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
async function resolveFilters(
|
|
550
|
-
cwd: string,
|
|
551
|
-
raw: string[],
|
|
552
|
-
): Promise<{ names?: string[]; error?: string }> {
|
|
553
|
-
const root = await findWorkspaceRoot(cwd)
|
|
554
|
-
const projects = await loadWorkspaceProjects(cwd)
|
|
555
|
-
const graph = buildPackageGraph(projects)
|
|
556
|
-
const parsed = raw.map((r) => parseFilter(r, root))
|
|
557
|
-
|
|
558
|
-
// Resolve every `[<since>]` filter against git before the pure
|
|
559
|
-
// applyFilters pass runs. One spawn per distinct ref — usually
|
|
560
|
-
// there's only one anyway.
|
|
561
|
-
const affectedByFilter = new Map<(typeof parsed)[number], Set<string>>()
|
|
562
|
-
for (const f of parsed) {
|
|
563
|
-
if (f.gitSince === undefined) continue
|
|
564
|
-
try {
|
|
565
|
-
const names = await affectedProjects({ workspaceRoot: root, since: f.gitSince, projects })
|
|
566
|
-
affectedByFilter.set(f, names)
|
|
567
|
-
} catch (err) {
|
|
568
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
569
|
-
return { error: msg }
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const selected = applyFilters({ filters: parsed, projects, graph, affectedByFilter })
|
|
574
|
-
if (selected.size === 0) {
|
|
575
|
-
return { error: `no projects matched filter(s): ${raw.join(', ')}` }
|
|
576
|
-
}
|
|
577
|
-
return { names: [...selected].sort() }
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
interface PickedTask {
|
|
581
|
-
project: string
|
|
582
|
-
task: string
|
|
583
|
-
description?: string
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
export async function pickTask(
|
|
587
|
-
cwd: string,
|
|
588
|
-
io: { input?: NodeJS.ReadableStream; output?: NodeJS.WritableStream } = {},
|
|
589
|
-
): Promise<PickedTask | null> {
|
|
590
|
-
const projects = await loadWorkspaceProjects(cwd)
|
|
591
|
-
const entries: PickedTask[] = []
|
|
592
|
-
for (const meta of projects) {
|
|
593
|
-
if (!meta.configPath) continue
|
|
594
|
-
const config = await loadProjectConfig(meta.configPath)
|
|
595
|
-
const taskNames = Object.keys(config.tasks ?? {}).sort()
|
|
596
|
-
for (const t of taskNames) {
|
|
597
|
-
const desc = config.tasks?.[t]?.description
|
|
598
|
-
entries.push({ project: meta.name, task: t, ...(desc ? { description: desc } : {}) })
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
if (entries.length === 0) {
|
|
602
|
-
process.stderr.write(`vx run: no tasks declared in any project\n`)
|
|
603
|
-
return null
|
|
604
|
-
}
|
|
605
|
-
const out = io.output ?? process.stdout
|
|
606
|
-
const numW = String(entries.length).length
|
|
607
|
-
const idW = Math.max(...entries.map((e) => `${e.project}#${e.task}`.length))
|
|
608
|
-
out.write('Tasks:\n')
|
|
609
|
-
entries.forEach((e, i) => {
|
|
610
|
-
const n = String(i + 1).padStart(numW, ' ')
|
|
611
|
-
const id = `${e.project}#${e.task}`.padEnd(idW)
|
|
612
|
-
const desc = e.description ? ` ${e.description}` : ''
|
|
613
|
-
out.write(` ${n}. ${id}${desc}\n`)
|
|
614
|
-
})
|
|
615
|
-
const rl = readline.createInterface({
|
|
616
|
-
input: io.input ?? process.stdin,
|
|
617
|
-
output: io.output ?? process.stdout,
|
|
618
|
-
})
|
|
619
|
-
try {
|
|
620
|
-
const answer = (await rl.question(`Pick a task [1-${entries.length}]: `)).trim()
|
|
621
|
-
const n = Number(answer)
|
|
622
|
-
if (!Number.isInteger(n) || n < 1 || n > entries.length) {
|
|
623
|
-
process.stderr.write(`vx run: invalid selection: ${answer}\n`)
|
|
624
|
-
return null
|
|
580
|
+
// Report generation is post-run, gated on the flags — zero cost when
|
|
581
|
+
// both are absent. Rendered once, however many sinks asked for it.
|
|
582
|
+
if (parsed.report === 'markdown' || parsed.reportFile !== undefined) {
|
|
583
|
+
const md = formatRunReportMarkdown(result)
|
|
584
|
+
// `--report` writes stdout. The report ITSELF is machine-clean, but
|
|
585
|
+
// stdout is not vx's alone — the status logger writes there too, so
|
|
586
|
+
// `--report=markdown >> "$GITHUB_STEP_SUMMARY"` captures every frame,
|
|
587
|
+
// meter bar and `::group::` command above the table. `--report-file`
|
|
588
|
+
// is the redirect-free form.
|
|
589
|
+
if (parsed.report === 'markdown') process.stdout.write(md)
|
|
590
|
+
if (parsed.reportFile !== undefined) {
|
|
591
|
+
const target = path.resolve(cwd, parsed.reportFile)
|
|
592
|
+
try {
|
|
593
|
+
// APPEND, not overwrite: the flag exists for `$GITHUB_STEP_SUMMARY`,
|
|
594
|
+
// which GitHub documents as append-only and which other steps in the
|
|
595
|
+
// same job also write to. Truncating it would silently destroy their
|
|
596
|
+
// content, and would make replacing the documented `>>` recipe a
|
|
597
|
+
// behaviour change rather than a drop-in.
|
|
598
|
+
await appendFile(target, md)
|
|
599
|
+
} catch (err) {
|
|
600
|
+
// Same contract as --summarize / --profile: the run already
|
|
601
|
+
// happened, so a write failure is reported, not fatal.
|
|
602
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
603
|
+
process.stderr.write(`vx: failed to write report to ${target}: ${message}\n`)
|
|
604
|
+
}
|
|
625
605
|
}
|
|
626
|
-
return entries[n - 1] ?? null
|
|
627
|
-
} finally {
|
|
628
|
-
rl.close()
|
|
629
606
|
}
|
|
607
|
+
return result.ok ? 0 : 1
|
|
630
608
|
}
|
|
631
609
|
|
|
632
610
|
function printSummary(summary: RunResult): void {
|
|
@@ -658,26 +636,22 @@ function printSummary(summary: RunResult): void {
|
|
|
658
636
|
}
|
|
659
637
|
|
|
660
638
|
function formatRow(o: OutcomeView): { task: string; status: string; duration: string } {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return {
|
|
679
|
-
task: o.taskId,
|
|
680
|
-
status,
|
|
681
|
-
duration: `${o.durationMs}ms`,
|
|
682
|
-
}
|
|
639
|
+
return { task: o.taskId, status: outcomeLabel(o), duration: `${o.durationMs}ms` }
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** `(did you mean --concurrency?)` for a flag within two edits of a documented one. */
|
|
643
|
+
function didYouMeanFlag(flag: string): string {
|
|
644
|
+
const name = flag.replace(/=.*$/, '')
|
|
645
|
+
const flags = documentedFlags('run')
|
|
646
|
+
// The usual two edits; a third only between flags that share their
|
|
647
|
+
// first five characters, so `--retries` reaches `--retry` (the i/y)
|
|
648
|
+
// while `--zzz` does not reach `--all` — a plain three-edit budget did.
|
|
649
|
+
const best =
|
|
650
|
+
nearest(name, flags) ??
|
|
651
|
+
nearest(
|
|
652
|
+
name,
|
|
653
|
+
flags.filter((f) => f.slice(0, 5) === name.slice(0, 5)),
|
|
654
|
+
3,
|
|
655
|
+
)
|
|
656
|
+
return best === undefined ? '' : ` (did you mean ${best}?)`
|
|
683
657
|
}
|