@vzn/vx 0.0.646 → 0.0.648
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
package/src/cache/schema.ts
CHANGED
|
@@ -24,7 +24,8 @@ export function lazyStatement(db: Database, sql: string): ReturnType<Database['p
|
|
|
24
24
|
* Create every table. `store` names the schema holding the ENTRY tables
|
|
25
25
|
* (`entries`, `entry_stdout`, `output_files`, `entry_inputs`, `store_meta`)
|
|
26
26
|
* the task timings (`task_durations`), the flaky record (`task_outcomes`),
|
|
27
|
-
* the blob sizes (`blob_sizes`)
|
|
27
|
+
* the blob sizes (`blob_sizes`), the plugins' blobs (`plugin_blobs`) and
|
|
28
|
+
* the remotes' hits (`remote_hits`):
|
|
28
29
|
* `main` when one `cache.db` holds everything (a `cacheDir`), `store` when
|
|
29
30
|
* they live in the shared store attached beside it (v32). The rest is the
|
|
30
31
|
* workspace's own: its history, its memos, and what its disk looked like
|
|
@@ -112,7 +113,13 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
112
113
|
placement_cores INTEGER,
|
|
113
114
|
placement_memory INTEGER,
|
|
114
115
|
-- v46: the task with no inputs its key folded (TaskOutcome.keylessBy).
|
|
115
|
-
keyless_by TEXT
|
|
116
|
+
keyless_by TEXT,
|
|
117
|
+
-- v48: the task's definition (taskDefinition): a history reader
|
|
118
|
+
-- counts only the rows of the task as it is now.
|
|
119
|
+
def TEXT,
|
|
120
|
+
-- v49: the share of a core each runnable thread got where it ran
|
|
121
|
+
-- (ExecuteResult.machine.cpuShare); NULL = all it wanted.
|
|
122
|
+
cpu_share REAL
|
|
116
123
|
);
|
|
117
124
|
-- Two whole-table indexes only, and both APPEND: every row of a run carries the
|
|
118
125
|
-- same run_id and a started_at newer than everything before it, so
|
|
@@ -163,7 +170,7 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
163
170
|
-- worktree size git recorded (A-60). Fixed for its OID, so a warm
|
|
164
171
|
-- run asks git for none; swept with file_hashes by seen_at, the
|
|
165
172
|
-- time the row was written. v38: in the store, so a worktree reads
|
|
166
|
-
-- what another learned (
|
|
173
|
+
-- what another learned (2026-10-14).
|
|
167
174
|
CREATE TABLE IF NOT EXISTS ${store}.blob_sizes (
|
|
168
175
|
oid TEXT PRIMARY KEY,
|
|
169
176
|
size INTEGER NOT NULL,
|
|
@@ -280,7 +287,7 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
280
287
|
);
|
|
281
288
|
-- What a duration forecast reads (orchestrator/history.ts), one row per
|
|
282
289
|
-- executed success, shared by every worktree of the repo so a new one
|
|
283
|
-
-- forecasts on its first run (
|
|
290
|
+
-- forecasts on its first run (2026-10-14). Append-only: never
|
|
284
291
|
-- updated, pruned by age with the run history. \`source\` tags a local
|
|
285
292
|
-- run 'branch:<name>' ('branch:' detached or outside git); \`sha\` is its
|
|
286
293
|
-- commit. A forecast reads a task's newest rows of one source.
|
|
@@ -297,7 +304,9 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
297
304
|
cpu_parallelism REAL,
|
|
298
305
|
peak_rss_bytes INTEGER,
|
|
299
306
|
saturated_cores INTEGER,
|
|
300
|
-
saturated_memory_bytes INTEGER
|
|
307
|
+
saturated_memory_bytes INTEGER,
|
|
308
|
+
-- v48: the task's definition (taskDefinition), as on \`runs\`.
|
|
309
|
+
def TEXT
|
|
301
310
|
);
|
|
302
311
|
CREATE INDEX IF NOT EXISTS ${store}.task_durations_task
|
|
303
312
|
ON task_durations(project, task, source, id);
|
|
@@ -309,7 +318,7 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
309
318
|
-- v38: the flaky record (orchestrator/failure-mode.ts): each keyed,
|
|
310
319
|
-- cached, unblocked outcome that observed its key, shared by every
|
|
311
320
|
-- worktree so a key that passed in one and failed in another is flaky
|
|
312
|
-
-- in both (
|
|
321
|
+
-- in both (2026-10-14). Every executed success or failure, and
|
|
313
322
|
-- a key's first hit (run-history.ts): a later hit replays a pass the
|
|
314
323
|
-- record holds, so a warm run on keys that passed writes no row.
|
|
315
324
|
-- Append-only, ordered by id, pruned by age with the run history.
|
|
@@ -331,6 +340,16 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
331
340
|
bytes BLOB NOT NULL,
|
|
332
341
|
at INTEGER NOT NULL
|
|
333
342
|
);
|
|
343
|
+
-- What a remote's has-many said it holds (remote-hits.ts), shared by
|
|
344
|
+
-- every worktree: a key here is a hit until a has-many or a GET says it
|
|
345
|
+
-- is gone, and a key not here is a miss. Only hits are kept. Pruned by
|
|
346
|
+
-- \`seen_at\` with the run history.
|
|
347
|
+
CREATE TABLE IF NOT EXISTS ${store}.remote_hits (
|
|
348
|
+
hash TEXT PRIMARY KEY,
|
|
349
|
+
size INTEGER,
|
|
350
|
+
duration_ms INTEGER,
|
|
351
|
+
seen_at INTEGER NOT NULL
|
|
352
|
+
);
|
|
334
353
|
${artifactTables(store)}
|
|
335
354
|
`)
|
|
336
355
|
}
|
package/src/cache/tar-stream.ts
CHANGED
|
@@ -38,6 +38,8 @@ const BLOCK = 512
|
|
|
38
38
|
* numbers; a MiB is far past any writer's.
|
|
39
39
|
*/
|
|
40
40
|
const MAX_EXTENDED_HEADER = 1024 * 1024
|
|
41
|
+
/** The largest millisecond count a Date holds (ECMA-262 §21.4.1.1). */
|
|
42
|
+
const DATE_MAX_MS = 8.64e15
|
|
41
43
|
// `ignoreBOM`: a default decoder drops a leading U+FEFF, and a name or
|
|
42
44
|
// prefix field that began with one read back without it.
|
|
43
45
|
const decoder = new TextDecoder('utf-8', { ignoreBOM: true })
|
|
@@ -232,6 +234,9 @@ class TarDecoder {
|
|
|
232
234
|
const type = h[156] === 0 ? '0' : String.fromCharCode(h[156]!)
|
|
233
235
|
const size = this.pendingSize ?? octal(h, 124, 12)
|
|
234
236
|
const mtimeMs = octal(h, 136, 12) * 1000
|
|
237
|
+
// Only GNU base-256 reaches past it, and a restore's utimes threw on
|
|
238
|
+
// such a Date after the renames before it had landed.
|
|
239
|
+
if (mtimeMs > DATE_MAX_MS) throw new TarFormatError(`header mtime past what a Date holds`)
|
|
235
240
|
const padded = Math.ceil(size / BLOCK) * BLOCK
|
|
236
241
|
// POSIX `ustar\0` only: old GNU's `ustar ` keeps atime and ctime at
|
|
237
242
|
// 345, and `-G` fills them, so a prefix read there named every entry
|
package/src/cache/umask.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// The process umask, read when a restore starts: Bun reads it by setting 0
|
|
2
2
|
// and putting it back in one synchronous call (project-loader.ts
|
|
3
3
|
// `currentUmask`), so no spawn on this thread sees the 0, and a run
|
|
4
|
-
// evaluates no config in a worker.
|
|
5
|
-
// shim has no umask.
|
|
4
|
+
// evaluates no config in a worker.
|
|
6
5
|
const proc = globalThis.process as { umask?: () => number } | undefined
|
|
7
6
|
let umask = 0
|
|
8
7
|
|
package/src/cli/cache.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { existsSync, readdirSync, rmSync } from 'node:fs'
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, rmSync } from 'node:fs'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { Cache, vxCacheEntries } from '../cache/index.js'
|
|
5
5
|
import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
|
|
6
6
|
import {
|
|
7
7
|
errorCode,
|
|
8
|
+
isUserError,
|
|
8
9
|
nearest,
|
|
9
10
|
parseDuration,
|
|
10
11
|
parseSize,
|
|
@@ -57,11 +58,11 @@ const CACHE_ELSEWHERE: Record<string, string> = {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
function cacheSubHint(sub: string): string {
|
|
60
|
-
|
|
61
|
-
if (
|
|
61
|
+
// Own keys only: `vx cache constructor` printed `Object`'s source.
|
|
62
|
+
if (Object.hasOwn(CACHE_ELSEWHERE, sub)) return ` — ${CACHE_ELSEWHERE[sub]}`
|
|
62
63
|
const best = nearest(sub, ['clean'])
|
|
63
64
|
// `clean` is the one subcommand: name it when nothing is near (M-58).
|
|
64
|
-
return best === undefined ? '.
|
|
65
|
+
return best === undefined ? '. Its one subcommand is `vx cache clean`' : `. Did you mean ${best}?`
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export async function cacheCmd(args: readonly string[]): Promise<number> {
|
|
@@ -78,6 +79,16 @@ export async function cacheCmd(args: readonly string[]): Promise<number> {
|
|
|
78
79
|
'VX_E_USAGE',
|
|
79
80
|
)
|
|
80
81
|
default:
|
|
82
|
+
// `vx cache --older-than 30d` dropped the verb, not misspelled it:
|
|
83
|
+
// name the clean command those flags make, when they parse as one.
|
|
84
|
+
if (sub.startsWith('-') && parsePruneArgs(args).error === undefined) {
|
|
85
|
+
return refuse(
|
|
86
|
+
'cache',
|
|
87
|
+
args,
|
|
88
|
+
`missing subcommand before ${sub}. Did you mean \`vx cache clean ${args.join(' ')}\`?${seeHelp('cache')}`,
|
|
89
|
+
'VX_E_USAGE',
|
|
90
|
+
)
|
|
91
|
+
}
|
|
81
92
|
return refuse(
|
|
82
93
|
'cache',
|
|
83
94
|
args,
|
|
@@ -90,7 +101,7 @@ export async function cacheCmd(args: readonly string[]): Promise<number> {
|
|
|
90
101
|
interface PruneArgs {
|
|
91
102
|
olderThanMs?: number
|
|
92
103
|
maxBytes?: number
|
|
93
|
-
|
|
104
|
+
dry?: boolean
|
|
94
105
|
format?: 'pretty' | 'json'
|
|
95
106
|
/** `--cache-dir`: prune the cache a run with the same flag uses. */
|
|
96
107
|
cacheDir?: string
|
|
@@ -149,8 +160,8 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
|
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
out.maxBytes = bytes
|
|
152
|
-
} else if (a === '--dry
|
|
153
|
-
out.
|
|
163
|
+
} else if (a === '--dry') {
|
|
164
|
+
out.dry = true
|
|
154
165
|
} else if (a === '--global') {
|
|
155
166
|
out.global = true
|
|
156
167
|
} else if (a === '--format' || a?.startsWith('--format=')) {
|
|
@@ -205,7 +216,7 @@ async function cleanCmd(args: readonly string[]): Promise<number> {
|
|
|
205
216
|
else {
|
|
206
217
|
const ws = await loadCliWorkspace(root)
|
|
207
218
|
dir = ws.cacheDir
|
|
208
|
-
storeRoot = await ws.storeRoot
|
|
219
|
+
storeRoot = await ws.storeRoot()
|
|
209
220
|
}
|
|
210
221
|
if (parsed.olderThanMs === undefined && parsed.maxBytes === undefined) {
|
|
211
222
|
return clear(args, root, dir, storeRoot, parsed)
|
|
@@ -213,15 +224,17 @@ async function cleanCmd(args: readonly string[]): Promise<number> {
|
|
|
213
224
|
// A workspace that never ran has nothing to prune, and opening the cache
|
|
214
225
|
// to find that out made it: `.vx/cache` with a database and a
|
|
215
226
|
// `.gitignore`, where item 900 had held the dry run to making nothing
|
|
216
|
-
// (and a read-only checkout was refused as unwritable instead).
|
|
217
|
-
|
|
227
|
+
// (and a read-only checkout was refused as unwritable instead). So has a
|
|
228
|
+
// directory holding nothing vx wrote (`--cache-dir build`): opened, it got
|
|
229
|
+
// an index and a `*` .gitignore that hid the user's files from git.
|
|
230
|
+
const dry = parsed.dry === true
|
|
218
231
|
const json = parsed.format === 'json'
|
|
219
|
-
if (
|
|
232
|
+
if (vxCacheEntries(dir).length === 0 && (storeRoot === null || !existsSync(storeRoot))) {
|
|
220
233
|
printPruned({ evicted: 0, bytesFreed: 0, orphans: 0, orphanBytes: 0 }, dry, json)
|
|
221
234
|
return 0
|
|
222
235
|
}
|
|
223
236
|
const cache =
|
|
224
|
-
parsed.
|
|
237
|
+
parsed.dry === true
|
|
225
238
|
? new Cache(dir, undefined, undefined, undefined, 'preview', storeRoot)
|
|
226
239
|
: new Cache(dir, undefined, root, undefined, 'open', storeRoot)
|
|
227
240
|
// A prune deletes rows and artifacts; a cache this user cannot write is
|
|
@@ -229,19 +242,19 @@ async function cleanCmd(args: readonly string[]): Promise<number> {
|
|
|
229
242
|
// than dying in the first DELETE with SQLite's "readonly database" and a
|
|
230
243
|
// stack (an unprivileged user on a root-owned `.vx`, 2026-09-16). A dry
|
|
231
244
|
// run only reads, and reads a read-only cache fine.
|
|
232
|
-
if (!parsed.
|
|
245
|
+
if (!parsed.dry) cache.assertWritable()
|
|
233
246
|
// A prune beside a run on this workspace evicts what the run has just
|
|
234
247
|
// probed as hits, whose `accessed_at` bumps it has not flushed yet; the
|
|
235
248
|
// run then re-runs those tasks (upstream survey, nx#36688). So a prune
|
|
236
249
|
// takes the run's lock and waits its turn. A dry run deletes nothing.
|
|
237
|
-
const releaseRunLock = parsed.
|
|
250
|
+
const releaseRunLock = parsed.dry
|
|
238
251
|
? async (): Promise<void> => {}
|
|
239
252
|
: await acquireRunLock(root, { log: warnToStderr })
|
|
240
253
|
try {
|
|
241
|
-
const opts: { olderThanMs?: number; maxBytes?: number;
|
|
254
|
+
const opts: { olderThanMs?: number; maxBytes?: number; dry?: boolean } = {}
|
|
242
255
|
if (parsed.olderThanMs !== undefined) opts.olderThanMs = parsed.olderThanMs
|
|
243
256
|
if (parsed.maxBytes !== undefined) opts.maxBytes = parsed.maxBytes
|
|
244
|
-
if (parsed.
|
|
257
|
+
if (parsed.dry) opts.dry = true
|
|
245
258
|
printPruned(await cache.prune(opts), dry, json)
|
|
246
259
|
} finally {
|
|
247
260
|
cache.close()
|
|
@@ -258,7 +271,7 @@ function printPruned(
|
|
|
258
271
|
if (json) {
|
|
259
272
|
const { evicted, bytesFreed, orphans, orphanBytes } = result
|
|
260
273
|
process.stdout.write(
|
|
261
|
-
`${JSON.stringify({
|
|
274
|
+
`${JSON.stringify({ dry: dry, evicted, bytesFreed, orphans, orphanBytes })}\n`,
|
|
262
275
|
)
|
|
263
276
|
return
|
|
264
277
|
}
|
|
@@ -286,7 +299,7 @@ function freedStatus(dry: boolean, bytes: number, any: boolean): ui.Block {
|
|
|
286
299
|
|
|
287
300
|
/** `vx cache clean --format json` with no bound (schemas/cache.json `clear`). */
|
|
288
301
|
export interface ClearResult {
|
|
289
|
-
|
|
302
|
+
dry: boolean
|
|
290
303
|
/** Workspace-relative inside the workspace, else absolute. */
|
|
291
304
|
cleared: string[]
|
|
292
305
|
bytesFreed: number
|
|
@@ -308,17 +321,27 @@ async function clear(
|
|
|
308
321
|
storeRoot: string | null,
|
|
309
322
|
parsed: PruneArgs,
|
|
310
323
|
): Promise<number> {
|
|
311
|
-
// The store the index recorded
|
|
312
|
-
//
|
|
324
|
+
// The store the index recorded and today's (a changed HOME, an origin
|
|
325
|
+
// added) both go; a directory with no index is not a vx cache to delete.
|
|
313
326
|
const targets: string[] = []
|
|
314
327
|
if (existsSync(path.join(dir, 'cache.db'))) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
328
|
+
// An index SQLite cannot read is what its own refusal sends here to
|
|
329
|
+
// clear: today's store stands in for the one it recorded.
|
|
330
|
+
let store: string | undefined = storeRoot ?? undefined
|
|
331
|
+
try {
|
|
332
|
+
const cache = new Cache(dir, undefined, undefined, undefined, 'preview', storeRoot)
|
|
333
|
+
store = cache.storeDir
|
|
334
|
+
cache.close()
|
|
335
|
+
} catch (err) {
|
|
336
|
+
if (!isUserError(err)) throw err
|
|
337
|
+
}
|
|
318
338
|
targets.push(dir)
|
|
319
339
|
// The index is a file in the checkout (committed, or restored from a CI
|
|
320
340
|
// cache): the store it names is deleted only when it is one.
|
|
321
341
|
if (store !== undefined && isStore(store)) targets.push(store)
|
|
342
|
+
// Today's store too: the next run reads it, whichever store the index
|
|
343
|
+
// last recorded (a moved HOME, an origin added).
|
|
344
|
+
if (storeRoot !== null) targets.push(storeRoot)
|
|
322
345
|
} else if (storeRoot !== null) targets.push(storeRoot)
|
|
323
346
|
if (parsed.global === true) {
|
|
324
347
|
const vxHome = path.join(process.env['HOME'] || homedir(), '.vx')
|
|
@@ -338,12 +361,20 @@ async function clear(
|
|
|
338
361
|
'VX_E_CONFIG',
|
|
339
362
|
)
|
|
340
363
|
}
|
|
341
|
-
const dry = parsed.
|
|
364
|
+
const dry = parsed.dry === true
|
|
342
365
|
// `.vx/cache` and the stores are vx's own; any other directory is the
|
|
343
366
|
// user's, cleared entry by entry and removed only if that empties it.
|
|
344
|
-
|
|
367
|
+
// A `.vx/cache` linked to a volume (a CI cache mount) is emptied through
|
|
368
|
+
// the link: removing the link alone left the cache where it points.
|
|
369
|
+
const own = path.join(dotVx, 'cache')
|
|
370
|
+
const named = path.resolve(dir) === own ? undefined : path.resolve(dir)
|
|
371
|
+
const linked = named === undefined && existsSync(own) && lstatSync(own).isSymbolicLink()
|
|
345
372
|
const entries = (t: string): string[] =>
|
|
346
|
-
t === named
|
|
373
|
+
t === named
|
|
374
|
+
? vxCacheEntries(t).map((e) => path.join(t, e))
|
|
375
|
+
: linked && t === own
|
|
376
|
+
? readdirSync(t).map((e) => path.join(t, e))
|
|
377
|
+
: [t]
|
|
347
378
|
const bytes = doomed.reduce((n, t) => n + entries(t).reduce((m, e) => m + treeBytes(e), 0), 0)
|
|
348
379
|
if (!dry && doomed.length > 0) {
|
|
349
380
|
// A clear beside a run deletes what the run is restoring or saving; it
|
|
@@ -360,7 +391,7 @@ async function clear(
|
|
|
360
391
|
}
|
|
361
392
|
const shown = doomed.map((t) => (path.relative(root, t).startsWith('..') ? t : relPosix(root, t)))
|
|
362
393
|
if (parsed.format === 'json') {
|
|
363
|
-
const result: ClearResult = {
|
|
394
|
+
const result: ClearResult = { dry: dry, cleared: shown, bytesFreed: bytes }
|
|
364
395
|
process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
365
396
|
} else
|
|
366
397
|
ui.print(
|
package/src/cli/completions.ts
CHANGED
|
@@ -5,14 +5,21 @@
|
|
|
5
5
|
// project names are not (a completion that evaluates configs on every
|
|
6
6
|
// Tab is the wrong price).
|
|
7
7
|
|
|
8
|
-
import { acceptedFlags, CORE_VERBS, flagHint, seeHelp } from './help.js'
|
|
8
|
+
import { acceptedFlags, CORE_VERBS, flagHint, refuse, seeHelp } from './help.js'
|
|
9
9
|
import { nearest, sayNotice } from '../util/index.js'
|
|
10
|
+
import { setting } from '../settings.js'
|
|
11
|
+
|
|
12
|
+
function enumValues(path: string): readonly string[] {
|
|
13
|
+
const kind = setting(path).kind
|
|
14
|
+
return kind.type === 'enum' ? kind.values : []
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
/** A flag whose value is one of a fixed set; the tests hold each to its parser. */
|
|
12
18
|
export const FLAG_VALUES: Readonly<Record<string, readonly string[]>> = {
|
|
13
19
|
'--format': ['pretty', 'json'],
|
|
14
20
|
'--output-logs': ['full', 'errors-only', 'hash-only', 'none'],
|
|
15
21
|
'--cache-download': ['all', 'toplevel', 'none'],
|
|
22
|
+
'--schedule': enumValues('run.schedule'),
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export type CompletionShell = 'bash' | 'zsh' | 'fish'
|
|
@@ -139,12 +146,12 @@ export async function completionsCmd(
|
|
|
139
146
|
if (args.length !== 1 || !(SHELLS as readonly string[]).includes(shell ?? '')) {
|
|
140
147
|
const flag = args.find((a) => a.startsWith('-'))
|
|
141
148
|
if (flag !== undefined) {
|
|
142
|
-
|
|
143
|
-
'
|
|
144
|
-
|
|
149
|
+
return refuse(
|
|
150
|
+
'completions',
|
|
151
|
+
args,
|
|
145
152
|
`unknown flag: ${flag}${flagHint('completions', flag)}${seeHelp('completions')}`,
|
|
153
|
+
'VX_E_USAGE',
|
|
146
154
|
)
|
|
147
|
-
return 1
|
|
148
155
|
}
|
|
149
156
|
const one = shell !== undefined && args.length === 1
|
|
150
157
|
// One edit: shell names are three or four letters, so two edits took
|
package/src/cli/docs.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// `vx docs <query> [--limit N] [--format pretty|json]` — search vx's
|
|
2
2
|
// reference as this version wrote it: packages/vx/docs at its git tag on
|
|
3
|
-
// GitHub, read online (
|
|
3
|
+
// GitHub, read online (2026-10-17: nothing is embedded). Each page is
|
|
4
4
|
// cut at its headings, and the sections holding every query word print
|
|
5
5
|
// whole, best first.
|
|
6
6
|
|
|
7
7
|
import { VERSION } from '../version.js'
|
|
8
|
+
import { detectColors, paint, parseDecimalInt, ui } from '../util/index.js'
|
|
8
9
|
import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
|
|
9
10
|
|
|
10
11
|
const SITE = 'https://vznjs.github.io/vx/'
|
|
@@ -19,7 +20,7 @@ interface DocsArgs {
|
|
|
19
20
|
error?: string
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function parseDocsArgs(args: readonly string[]): DocsArgs {
|
|
23
|
+
export function parseDocsArgs(args: readonly string[]): DocsArgs {
|
|
23
24
|
const out: DocsArgs = { terms: [], limit: DEFAULT_LIMIT, json: false }
|
|
24
25
|
for (let i = 0; i < args.length; i++) {
|
|
25
26
|
const a = args[i]!
|
|
@@ -29,11 +30,17 @@ function parseDocsArgs(args: readonly string[]): DocsArgs {
|
|
|
29
30
|
out.json = fv === 'json'
|
|
30
31
|
} else if (a === '--limit' || a.startsWith('--limit=')) {
|
|
31
32
|
const v = a === '--limit' ? args[++i] : a.slice('--limit='.length)
|
|
32
|
-
|
|
33
|
-
if (!Number.isInteger(n) || n < 1) {
|
|
33
|
+
if (v === undefined || v === '') {
|
|
34
34
|
return {
|
|
35
35
|
...out,
|
|
36
|
-
error: `--limit
|
|
36
|
+
error: `--limit requires a value: a whole number above 0${seeHelp('docs')}`,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const n = parseDecimalInt(v)
|
|
40
|
+
if (n === null || n < 1) {
|
|
41
|
+
return {
|
|
42
|
+
...out,
|
|
43
|
+
error: `--limit takes a whole number above 0 (got ${v})${seeHelp('docs')}`,
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
out.limit = n
|
|
@@ -52,7 +59,7 @@ export interface DocsHit {
|
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
function words(s: string): string[] {
|
|
55
|
-
return s.toLowerCase().match(/[
|
|
62
|
+
return s.toLowerCase().match(/[\p{L}\p{N}][\p{L}\p{N}_.-]*/gu) ?? []
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
/** GitHub's heading anchor, which Starlight shares: lowercase, punctuation dropped, spaces to `-`. */
|
|
@@ -63,7 +70,7 @@ function anchor(heading: string): string {
|
|
|
63
70
|
.replace(/ /g, '-')
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
/** A page's URL on the site, as
|
|
73
|
+
/** A page's URL on the site, as private/vx-docs/scripts/import-docs.ts serves it. */
|
|
67
74
|
function pageUrl(page: string): string {
|
|
68
75
|
const served: Record<string, string> = {
|
|
69
76
|
README: 'overview',
|
|
@@ -117,6 +124,8 @@ export async function loadPages(
|
|
|
117
124
|
interface Section {
|
|
118
125
|
page: string
|
|
119
126
|
heading: string
|
|
127
|
+
/** The heading's id on the site; absent for the page's opening. */
|
|
128
|
+
id?: string
|
|
120
129
|
text: string
|
|
121
130
|
}
|
|
122
131
|
|
|
@@ -124,18 +133,31 @@ interface Section {
|
|
|
124
133
|
function sections(page: string, md: string): Section[] {
|
|
125
134
|
const out: Section[] = []
|
|
126
135
|
let heading = ''
|
|
136
|
+
let id: string | undefined
|
|
127
137
|
let lines: string[] = []
|
|
128
138
|
let fence = false
|
|
139
|
+
// The site numbers a repeated id (`usage`, `usage-1`) over every `##` to
|
|
140
|
+
// `######` heading of the page, as import-docs.ts's `headingIds` does:
|
|
141
|
+
// cli.md repeats Usage per verb, and each linked the first.
|
|
142
|
+
const seen = new Map<string, number>()
|
|
129
143
|
const flush = () => {
|
|
130
144
|
const text = lines.join('\n').trim()
|
|
131
|
-
if (text !== '') out.push({ page, heading, text })
|
|
145
|
+
if (text !== '') out.push({ page, heading, ...(id !== undefined ? { id } : {}), text })
|
|
132
146
|
}
|
|
133
147
|
for (const line of md.split('\n')) {
|
|
134
148
|
if (/^\s*(```|~~~)/.test(line)) fence = !fence
|
|
135
|
-
const h = fence ? null :
|
|
136
|
-
|
|
149
|
+
const h = fence ? null : /^(#{1,6}) (.+)$/.exec(line)
|
|
150
|
+
let slug: string | undefined
|
|
151
|
+
if (h !== null && h[1]!.length > 1) {
|
|
152
|
+
const base = anchor(h[2]!.replace(/`/g, '').trim())
|
|
153
|
+
const n = seen.get(base) ?? 0
|
|
154
|
+
seen.set(base, n + 1)
|
|
155
|
+
slug = n === 0 ? base : `${base}-${n}`
|
|
156
|
+
}
|
|
157
|
+
if (h !== null && h[1]!.length <= 4) {
|
|
137
158
|
flush()
|
|
138
|
-
heading = h[
|
|
159
|
+
heading = h[2]!.replace(/`/g, '').trim()
|
|
160
|
+
id = slug ?? anchor(heading)
|
|
139
161
|
lines = [line]
|
|
140
162
|
} else lines.push(line)
|
|
141
163
|
}
|
|
@@ -182,7 +204,7 @@ export function searchDocs(
|
|
|
182
204
|
return (exact.length > 0 ? exact : scored).slice(0, limit).map(({ s }) => ({
|
|
183
205
|
page: s.page,
|
|
184
206
|
heading: s.heading,
|
|
185
|
-
url: s.
|
|
207
|
+
url: s.id === undefined ? pageUrl(s.page) : `${pageUrl(s.page)}#${s.id}`,
|
|
186
208
|
text: s.text,
|
|
187
209
|
}))
|
|
188
210
|
}
|
|
@@ -220,16 +242,20 @@ export async function docsCmd(
|
|
|
220
242
|
return 0
|
|
221
243
|
}
|
|
222
244
|
if (hits.length === 0) {
|
|
223
|
-
|
|
245
|
+
ui.say(
|
|
246
|
+
'warn',
|
|
224
247
|
`vx docs: no section of the docs at ${ref} holds every word of "${query}"; try fewer words, or ${SITE}llms.txt\n`,
|
|
248
|
+
'out',
|
|
225
249
|
)
|
|
226
250
|
return 0
|
|
227
251
|
}
|
|
252
|
+
const c = detectColors(process.stdout)
|
|
228
253
|
process.stdout.write(
|
|
229
254
|
hits
|
|
230
|
-
.map(
|
|
231
|
-
|
|
232
|
-
|
|
255
|
+
.map((h) =>
|
|
256
|
+
c.enabled
|
|
257
|
+
? `${ui.render([ui.excerpt(`${h.page}.md${h.heading === '' ? '' : ` § ${h.heading}`}`, h.text)], c)}${paint('', h.url, c, { dim: true })}\n`
|
|
258
|
+
: `── ${h.page}.md${h.heading === '' ? '' : ` § ${h.heading}`} · ${h.url}\n\n${h.text}\n`,
|
|
233
259
|
)
|
|
234
260
|
.join('\n'),
|
|
235
261
|
)
|
package/src/cli/help.ts
CHANGED
|
@@ -114,14 +114,14 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
114
114
|
'Usage:',
|
|
115
115
|
' vx run [OPTIONS] [TASK | PKG#TASK ...] [-- forwarded-args...]',
|
|
116
116
|
' vx watch [OPTIONS] [--poll] TASK [-- forwarded-args...]',
|
|
117
|
-
' vx cache clean [--older-than <duration>] [--max-size <size>] [--global] [--dry
|
|
117
|
+
' vx cache clean [--older-than <duration>] [--max-size <size>] [--global] [--dry] [--format pretty|json] [--cache-dir <path>]',
|
|
118
118
|
' vx lock [--check] [--format pretty|json]',
|
|
119
|
-
' vx init [--dry [--format pretty|json]] [--force] [--mjs] [--native|--keep] [--plugin <
|
|
119
|
+
' vx init [--dry [--format pretty|json]] [--force] [--mjs] [--native|--keep] [--plugin <hook>]',
|
|
120
120
|
' vx upgrade [TAG] [-g|--global]',
|
|
121
121
|
' vx show [PROJECT[#TASK] | TASK] [--filter <pattern>] [--affected[=<ref>]] [--format pretty|json]',
|
|
122
122
|
' vx info [--format pretty|json] [--cache-dir <path>]',
|
|
123
123
|
' vx why (TASK | PKG#TASK) [--run RUNID] [--format pretty|json] [--cache-dir <path>]',
|
|
124
|
-
' vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--stats [<task>] [--branch <name>]] [--format pretty|json] [--cache-dir <path>]',
|
|
124
|
+
' vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--stats [<task>] [--branch <name>]] [--sql <query>] [--live [--follow]] [--format pretty|json] [--cache-dir <path>]',
|
|
125
125
|
' vx docs <query> [--limit N] [--format pretty|json]',
|
|
126
126
|
' vx completions bash|zsh|fish',
|
|
127
127
|
' vx help [VERB]',
|
|
@@ -186,7 +186,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
186
186
|
' summary table after the run.',
|
|
187
187
|
'',
|
|
188
188
|
'Planning (for run — skips execution):',
|
|
189
|
-
" --dry[=
|
|
189
|
+
" --dry[=pretty|json] Print the task graph and each task's predicted hit",
|
|
190
190
|
' or miss.',
|
|
191
191
|
'',
|
|
192
192
|
'Artifacts (for run):',
|
|
@@ -197,7 +197,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
197
197
|
' cache, distributed execution, and telemetry export are provided by',
|
|
198
198
|
' PLUGINS declared in .vx/workspace.ts — first-party or your own. Core',
|
|
199
199
|
' depends on none of them; a plain `vx run` never requires a plugin.',
|
|
200
|
-
' See the plugin guide: https://vznjs.github.io/vx/
|
|
200
|
+
' See the plugin guide: https://vznjs.github.io/vx/docs/plugins/',
|
|
201
201
|
'',
|
|
202
202
|
'Argument forwarding (for run):',
|
|
203
203
|
" Anything after `--` is forwarded (shell-quoted) to the task's exec",
|
|
@@ -215,11 +215,13 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
215
215
|
'Cache management:',
|
|
216
216
|
' vx cache clean --older-than 30d Evict entries last accessed > 30 days ago.',
|
|
217
217
|
' vx cache clean --max-size 1G Keep total cache under 1 GB (LRU eviction).',
|
|
218
|
-
" vx cache clean Clear .vx/cache and this repo's store
|
|
218
|
+
" vx cache clean Clear .vx/cache and this repo's store",
|
|
219
|
+
' ~/.vx/<id>/cache.',
|
|
219
220
|
" --global Clear every repo's store under ~/.vx.",
|
|
220
|
-
' --dry
|
|
221
|
+
' --dry Say what the policy would reap; delete nothing.',
|
|
221
222
|
' --format <fmt> pretty (default) | json.',
|
|
222
|
-
' --cache-dir <path> The cache a run with the same flag uses (why,
|
|
223
|
+
' --cache-dir <path> The cache a run with the same flag uses (why,',
|
|
224
|
+
' last, info too).',
|
|
223
225
|
'',
|
|
224
226
|
' Duration units: ms, s, m, h, d. Size units: K, M, G, T (powers of 1024).',
|
|
225
227
|
' Cache statistics — directory, entry count, size — are part of `vx info`.',
|
|
@@ -253,6 +255,10 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
253
255
|
' --stats [<task>] Per-task p50/p99, pass and hit rate, failure mode, peak',
|
|
254
256
|
' RSS and CPU over the last 50 runs; a task id or name narrows.',
|
|
255
257
|
" --branch <name> The branch --stats' p50 forecasts from (default: the run's).",
|
|
258
|
+
' --sql <query> One read-only SELECT over the history (runs, invocations,',
|
|
259
|
+
' entries, task_outcomes, ...): filter, group, trend.',
|
|
260
|
+
' --live The run in progress: running, ended, waiting tasks, warnings.',
|
|
261
|
+
" --follow With --live, the run's output and events until it ends.",
|
|
256
262
|
' --format <fmt> pretty (default) | json.',
|
|
257
263
|
' info, why and last:',
|
|
258
264
|
' --cache-dir <path> Read the cache a run with the same flag uses.',
|
|
@@ -268,7 +274,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
268
274
|
' --mjs Write .mjs files (no type import) instead of .ts.',
|
|
269
275
|
' --native Turbo or Nx: write a vx.config.ts per package (no question).',
|
|
270
276
|
' --keep Turbo or Nx: only .vx/workspace.ts, declaring turbo() or nx().',
|
|
271
|
-
' --plugin <
|
|
277
|
+
' --plugin <hook> Instead: write plugins/<hook>.ts, a runnable plugin, and its',
|
|
272
278
|
' test (executor, cache, telemetry, schedule, admit, commands,',
|
|
273
279
|
' project, graph, key).',
|
|
274
280
|
'',
|
|
@@ -298,11 +304,11 @@ export function helpText(pluginCommands: readonly string[] = []): string {
|
|
|
298
304
|
' into .vx/projects.lock.json, the only place a run reads configs',
|
|
299
305
|
' from. A run never evaluates one, never checks the rules',
|
|
300
306
|
' and never writes the lock: with none it fails.',
|
|
301
|
-
' vx lock --check Audit the lock:
|
|
302
|
-
'
|
|
303
|
-
' graph rules. Exits 1 on drift (e.g. configs that
|
|
304
|
-
' vars). Run it before the run in CI. With --format json
|
|
305
|
-
' {upToDate, audited, notAudited, problems}.',
|
|
307
|
+
' vx lock --check Audit the lock: re-evaluate every config here and compare',
|
|
308
|
+
' it with the frozen object, find a config added or gone, and',
|
|
309
|
+
' check the graph rules. Exits 1 on drift (e.g. configs that',
|
|
310
|
+
' read env vars). Run it before the run in CI. With --format json',
|
|
311
|
+
' it prints {upToDate, audited, notAudited, problems}.',
|
|
306
312
|
'',
|
|
307
313
|
...(pluginCommands.length > 0 ? ['Plugin commands:', ...pluginCommands, ''] : []),
|
|
308
314
|
].join('\n')
|
|
@@ -317,6 +323,27 @@ export function seeHelp(verb: string): string {
|
|
|
317
323
|
return ` (see \`vx ${verb} --help\`)`
|
|
318
324
|
}
|
|
319
325
|
|
|
326
|
+
/**
|
|
327
|
+
* The refusal for a `project#task` spec with an empty half, or undefined.
|
|
328
|
+
* An empty task wins over an empty project: `#` names no task to run in
|
|
329
|
+
* every project, so `bare(task)` would print a hint with no task in it.
|
|
330
|
+
*/
|
|
331
|
+
export function anchoredSpecError(
|
|
332
|
+
spec: string,
|
|
333
|
+
bare: (task: string) => string,
|
|
334
|
+
): string | undefined {
|
|
335
|
+
const idx = spec.indexOf('#')
|
|
336
|
+
if (idx < 0) return undefined
|
|
337
|
+
const project = spec.slice(0, idx)
|
|
338
|
+
const task = spec.slice(idx + 1)
|
|
339
|
+
if (!task) {
|
|
340
|
+
const list = project ? `vx show ${project}` : 'vx show'
|
|
341
|
+
return `missing task name after '#' in "${spec}" (${list} lists the tasks)`
|
|
342
|
+
}
|
|
343
|
+
if (!project) return `missing project name before '#' in "${spec}" (${bare(task)})`
|
|
344
|
+
return undefined
|
|
345
|
+
}
|
|
346
|
+
|
|
320
347
|
/** `--format`'s value, or the one refusal every verb gives for a bad one. */
|
|
321
348
|
export function formatValue(
|
|
322
349
|
v: string | undefined,
|
|
@@ -337,8 +364,8 @@ export function refusedWord(arg: string): string {
|
|
|
337
364
|
}
|
|
338
365
|
|
|
339
366
|
/**
|
|
340
|
-
* `(did you mean --concurrency?)` for a flag within two edits
|
|
341
|
-
* accepts; empty for anything else (a positional included). A third edit
|
|
367
|
+
* `(did you mean --concurrency?)` for a flag within two edits (one, for a
|
|
368
|
+
* name of four letters or fewer) of one `verb` accepts; empty for anything else (a positional included). A third edit
|
|
342
369
|
* is allowed only between flags that share their first five characters,
|
|
343
370
|
* so `--timeouts` reaches `--timeout` while `--zzz` does not reach `--all`.
|
|
344
371
|
*/
|
|
@@ -354,9 +381,21 @@ export function flagHint(verb: string, arg: string): string {
|
|
|
354
381
|
// from `--format`: a run with its cache reads turned off.
|
|
355
382
|
if ((name === '--format' || name === '--json') && flags.includes('--dry'))
|
|
356
383
|
return ' (the run as JSON is `vx last --format json` after it; the plan is --dry=json)'
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
384
|
+
// Watch takes run's flags but --dry, so the branch above missed it.
|
|
385
|
+
if ((name === '--format' || name === '--json') && verb === 'watch')
|
|
386
|
+
return " (vx watch prints no JSON; each cycle's run is `vx last --format json` after it)"
|
|
387
|
+
// A bare flag handed a value (`--check=yes`) is no edit from itself.
|
|
388
|
+
if (name !== arg && flags.includes(name)) return ` (did you mean ${name}?)`
|
|
389
|
+
// Another verb's own flag is no typo: `vx run --poll` was told "did you
|
|
390
|
+
// mean --all?". Named only when one other verb takes it.
|
|
391
|
+
const owners = CORE_VERBS.filter(
|
|
392
|
+
(v) => v !== verb.split(' ')[0] && acceptedFlags(v).includes(name),
|
|
393
|
+
)
|
|
394
|
+
if (owners.length === 1)
|
|
395
|
+
return `. \`vx ${owners[0] === 'cache' ? 'cache clean' : owners[0]}\` takes it`
|
|
396
|
+
// A flag one word too long (`--dry-run` for `--dry`) is too many edits
|
|
397
|
+
// away for either budget, and so is one cut short at a word.
|
|
398
|
+
const cut = flags.filter((f) => f.startsWith(`${name}-`) || name.startsWith(`${f}-`))
|
|
360
399
|
const best =
|
|
361
400
|
nearest(name, flags) ??
|
|
362
401
|
nearest(
|