@vzn/vx 0.0.649 → 0.0.651
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -20
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/src/bin.ts +9 -31
- package/src/cache/cache.ts +225 -51
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/fold-memo.ts +8 -1
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/index.ts +12 -0
- package/src/cache/inputs.ts +3 -0
- package/src/cache/layer.ts +11 -0
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +120 -0
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-hits.ts +9 -1
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +11 -1
- package/src/cache/run-notes.ts +123 -0
- package/src/cache/schema.ts +54 -1
- package/src/cli/cache.ts +26 -16
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +12 -23
- package/src/cli/index.ts +4 -9
- package/src/cli/info.ts +15 -7
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +224 -223
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +10 -3
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +38 -42
- package/src/cli/task-verb.ts +22 -0
- package/src/cli/upgrade.ts +3 -1
- package/src/cli/watch-fs.ts +6 -1
- package/src/cli/watch-judge.ts +2 -1
- package/src/cli/watch.ts +83 -44
- package/src/cli/why.ts +126 -92
- package/src/cli/workspace-config.ts +2 -1
- package/src/exec/executor.ts +2 -1
- package/src/exec/kill-tree.ts +58 -5
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/pipes.ts +152 -0
- package/src/exec/runner.ts +199 -182
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-runtime.ts +11 -2
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +12 -2
- package/src/graph/task-graph.ts +27 -1
- package/src/handoff.ts +10 -7
- package/src/history/history.ts +39 -2
- package/src/history/index.ts +3 -0
- package/src/history/metrics.ts +9 -1
- package/src/index.ts +12 -3
- package/src/orchestrator/doctor.ts +3 -3
- package/src/orchestrator/execute-task.ts +16 -2
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +3 -0
- package/src/orchestrator/index.ts +3 -1
- package/src/orchestrator/live-usage.ts +98 -0
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +176 -102
- package/src/orchestrator/miss-save.ts +31 -3
- package/src/orchestrator/persistent-task.ts +3 -1
- package/src/orchestrator/plan.ts +5 -1
- package/src/orchestrator/plugin-host.ts +61 -0
- package/src/orchestrator/plugin.ts +49 -4
- package/src/orchestrator/prepare.ts +18 -2
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +35 -114
- package/src/orchestrator/run-records.ts +10 -0
- package/src/orchestrator/run.ts +89 -26
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-hash.ts +17 -3
- package/src/report/forecast.ts +12 -33
- package/src/report/framed-output.ts +388 -161
- package/src/report/index.ts +8 -2
- package/src/report/logger.ts +100 -36
- package/src/report/status-line.ts +81 -365
- package/src/report/summary.ts +104 -131
- package/src/ui/ansi.ts +48 -0
- package/src/ui/components.ts +506 -0
- package/src/ui/index.ts +79 -0
- package/src/ui/kit.ts +37 -0
- package/src/ui/live-region.ts +363 -0
- package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
- package/src/ui/terminal.ts +51 -0
- package/src/ui/theme.ts +224 -0
- package/src/ui/version-line.ts +28 -0
- package/src/util/config-frame.ts +65 -10
- package/src/util/edit-distance.ts +6 -1
- package/src/util/hidden-prompt.ts +8 -2
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +20 -2
- package/src/util/secret-mask.ts +39 -3
- package/src/util/secrets.ts +3 -0
- package/src/util/settle.ts +31 -0
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/config-schema.ts +4 -2
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +28 -0
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +4 -3
- package/src/workspace/workspace.ts +3 -0
- package/src/orchestrator/run-steps.ts +0 -93
- package/src/util/colors.ts +0 -130
- package/src/util/report.ts +0 -44
- package/src/util/ui.ts +0 -258
- /package/src/{report/path-links.ts → ui/links.ts} +0 -0
- /package/src/{report → ui}/terminal-signals.ts +0 -0
package/src/cache/remote-hits.ts
CHANGED
|
@@ -12,7 +12,15 @@ export interface RemoteHit {
|
|
|
12
12
|
readonly durationMs?: number
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/** What a layered cache asks of the remote-hit rows; the rows stay core's (`IndexRemoteHits`). */
|
|
16
|
+
export interface RemoteHits {
|
|
17
|
+
/** The keys of `hashes` a remote last said it holds. */
|
|
18
|
+
get(hashes: readonly string[]): Map<string, RemoteHit>
|
|
19
|
+
record(asked: readonly string[], found: ReadonlyMap<string, RemoteHit>, now?: number): void
|
|
20
|
+
forget(hash: string): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class IndexRemoteHits implements RemoteHits {
|
|
16
24
|
constructor(
|
|
17
25
|
private readonly db: Database,
|
|
18
26
|
/** Off for a reading verb or an index this user cannot write. */
|
package/src/cache/remote-lane.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// class with the endpoint named. The order across remotes, the local store
|
|
5
5
|
// and provenance are `LayeredCache`'s; nothing here knows another remote.
|
|
6
6
|
|
|
7
|
-
import { machineParallelism, thrownMessage
|
|
7
|
+
import { machineParallelism, thrownMessage } from '../util/index.js'
|
|
8
|
+
import * as ui from '../ui/index.js'
|
|
8
9
|
import type { RemoteKeyTraffic, RemoteTraffic } from './cache.js'
|
|
9
10
|
import type { ArtifactInfo, RemoteCacheLayer } from './layered-cache.js'
|
|
10
11
|
|
package/src/cache/run-history.ts
CHANGED
|
@@ -41,6 +41,11 @@ const RUN_COLUMNS = [
|
|
|
41
41
|
'keyless_by',
|
|
42
42
|
'def',
|
|
43
43
|
'cpu_share',
|
|
44
|
+
'queued_ms',
|
|
45
|
+
'output_bytes',
|
|
46
|
+
'blocks',
|
|
47
|
+
'flaky_passes',
|
|
48
|
+
'flaky_failures',
|
|
44
49
|
]
|
|
45
50
|
|
|
46
51
|
const INSERT_RUNS = `INSERT INTO runs(${RUN_COLUMNS.join(', ')}) VALUES `
|
|
@@ -282,7 +287,7 @@ export class RunHistory {
|
|
|
282
287
|
}
|
|
283
288
|
}
|
|
284
289
|
|
|
285
|
-
/** Bind a RunRecord to one `RUN_TUPLE`'s
|
|
290
|
+
/** Bind a RunRecord to one `RUN_TUPLE`'s positional parameters, in `RUN_COLUMNS` order. */
|
|
286
291
|
function bindRun(run: RunRecord, digestValue: (v: string) => string): SQLQueryBindings[] {
|
|
287
292
|
return [
|
|
288
293
|
// The ONE place the no-key sentinel is applied, so the column's
|
|
@@ -321,6 +326,11 @@ function bindRun(run: RunRecord, digestValue: (v: string) => string): SQLQueryBi
|
|
|
321
326
|
run.keylessBy ?? null,
|
|
322
327
|
run.def ?? null,
|
|
323
328
|
run.cpuShare ?? null,
|
|
329
|
+
run.queuedMs ?? null,
|
|
330
|
+
run.outputBytes ?? null,
|
|
331
|
+
run.blocks ?? null,
|
|
332
|
+
run.flakyPasses ?? null,
|
|
333
|
+
run.flakyFailures ?? null,
|
|
324
334
|
]
|
|
325
335
|
}
|
|
326
336
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// What a run said that its `runs` rows do not hold, for `vx last`: the steps
|
|
2
|
+
// it did not wait on (`vx:prefetch`, 2026-10-16), which never show in a
|
|
3
|
+
// run's output; its warnings, which scroll past on a terminal (an agent
|
|
4
|
+
// reading the run afterwards saw only the footer's count, 2026-10-28); and
|
|
5
|
+
// each failed task's output and the files it names. Rows of cache.db's
|
|
6
|
+
// `steps`, `warnings` and `failures` (v50; files under the cache dir
|
|
7
|
+
// before), so `vx last --sql` reads them too. The newest `KEPT` runs that
|
|
8
|
+
// had any are kept, per table. A run with none writes nothing.
|
|
9
|
+
|
|
10
|
+
import type { Database } from 'bun:sqlite'
|
|
11
|
+
|
|
12
|
+
/** Runs whose steps, warnings or failures are kept; run ids are UUIDv7, so they sort by age. */
|
|
13
|
+
const KEPT = 50
|
|
14
|
+
|
|
15
|
+
/** One of vx's own steps that ran beside the tasks. */
|
|
16
|
+
export interface RunStep {
|
|
17
|
+
id: string
|
|
18
|
+
durationMs: number
|
|
19
|
+
ok: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A failed task's kept output: plain, bounded, secrets masked. */
|
|
23
|
+
export interface KeptFailure {
|
|
24
|
+
taskId: string
|
|
25
|
+
inputMoved?: string
|
|
26
|
+
output: string
|
|
27
|
+
locations: readonly unknown[]
|
|
28
|
+
sandboxFix?: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RunNotes {
|
|
32
|
+
readonly steps?: readonly RunStep[]
|
|
33
|
+
readonly warnings?: readonly string[]
|
|
34
|
+
readonly failures?: readonly KeptFailure[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Drop every row of `table` older than its newest `KEPT` runs. */
|
|
38
|
+
function trim(db: Database, table: 'steps' | 'warnings' | 'failures'): void {
|
|
39
|
+
db.query(
|
|
40
|
+
`DELETE FROM ${table} WHERE run_id < (SELECT run_id FROM (SELECT DISTINCT run_id FROM ${table} ORDER BY run_id DESC LIMIT 1 OFFSET ${KEPT - 1}))`,
|
|
41
|
+
).run()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Write `notes` for `runId` in one transaction, then keep the newest `KEPT` runs of each table written. */
|
|
45
|
+
export function keepRunNotes(db: Database, runId: string, notes: RunNotes): void {
|
|
46
|
+
const steps = notes.steps ?? []
|
|
47
|
+
const warnings = notes.warnings ?? []
|
|
48
|
+
const failures = notes.failures ?? []
|
|
49
|
+
if (steps.length + warnings.length + failures.length === 0) return
|
|
50
|
+
db.transaction(() => {
|
|
51
|
+
if (steps.length > 0) {
|
|
52
|
+
const insert = db.query(
|
|
53
|
+
'INSERT OR REPLACE INTO steps(run_id, seq, step, duration_ms, ok) VALUES (?, ?, ?, ?, ?)',
|
|
54
|
+
)
|
|
55
|
+
steps.forEach((s, i) => insert.run(runId, i, s.id, Math.round(s.durationMs), s.ok ? 1 : 0))
|
|
56
|
+
trim(db, 'steps')
|
|
57
|
+
}
|
|
58
|
+
if (warnings.length > 0) {
|
|
59
|
+
const insert = db.query('INSERT OR REPLACE INTO warnings(run_id, seq, text) VALUES (?, ?, ?)')
|
|
60
|
+
warnings.forEach((w, i) => insert.run(runId, i, w))
|
|
61
|
+
trim(db, 'warnings')
|
|
62
|
+
}
|
|
63
|
+
if (failures.length > 0) {
|
|
64
|
+
const insert = db.query(
|
|
65
|
+
`INSERT OR REPLACE INTO failures(run_id, task_id, input_moved, output, locations, sandbox_fix)
|
|
66
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
67
|
+
)
|
|
68
|
+
for (const f of failures) {
|
|
69
|
+
insert.run(
|
|
70
|
+
runId,
|
|
71
|
+
f.taskId,
|
|
72
|
+
f.inputMoved ?? null,
|
|
73
|
+
f.output,
|
|
74
|
+
JSON.stringify(f.locations),
|
|
75
|
+
f.sandboxFix === undefined ? null : JSON.stringify(f.sandboxFix),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
trim(db, 'failures')
|
|
79
|
+
}
|
|
80
|
+
}).immediate()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The background steps `runId` kept, in the order they ended; none when it had none. */
|
|
84
|
+
export function keptSteps(db: Database, runId: string): RunStep[] {
|
|
85
|
+
const rows = db
|
|
86
|
+
.query('SELECT step, duration_ms, ok FROM steps WHERE run_id = ? ORDER BY seq')
|
|
87
|
+
.all(runId) as { step: string; duration_ms: number; ok: number }[]
|
|
88
|
+
return rows.map((r) => ({ id: r.step, durationMs: r.duration_ms, ok: r.ok === 1 }))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The warnings `runId` said, colour stripped; none when it said none. */
|
|
92
|
+
export function keptWarnings(db: Database, runId: string): string[] {
|
|
93
|
+
const rows = db.query('SELECT text FROM warnings WHERE run_id = ? ORDER BY seq').all(runId) as {
|
|
94
|
+
text: string
|
|
95
|
+
}[]
|
|
96
|
+
return rows.map((r) => r.text)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** What `runId` kept of its failed tasks, by task id; empty when it kept none. */
|
|
100
|
+
export function keptFailures(db: Database, runId: string): Map<string, KeptFailure> {
|
|
101
|
+
const rows = db
|
|
102
|
+
.query(
|
|
103
|
+
'SELECT task_id, input_moved, output, locations, sandbox_fix FROM failures WHERE run_id = ?',
|
|
104
|
+
)
|
|
105
|
+
.all(runId) as {
|
|
106
|
+
task_id: string
|
|
107
|
+
input_moved: string | null
|
|
108
|
+
output: string
|
|
109
|
+
locations: string
|
|
110
|
+
sandbox_fix: string | null
|
|
111
|
+
}[]
|
|
112
|
+
const kept = new Map<string, KeptFailure>()
|
|
113
|
+
for (const r of rows) {
|
|
114
|
+
kept.set(r.task_id, {
|
|
115
|
+
taskId: r.task_id,
|
|
116
|
+
...(r.input_moved !== null ? { inputMoved: r.input_moved } : {}),
|
|
117
|
+
output: r.output,
|
|
118
|
+
locations: JSON.parse(r.locations) as unknown[],
|
|
119
|
+
...(r.sandbox_fix !== null ? { sandboxFix: JSON.parse(r.sandbox_fix) as unknown } : {}),
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
return kept
|
|
123
|
+
}
|
package/src/cache/schema.ts
CHANGED
|
@@ -119,7 +119,17 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
119
119
|
def TEXT,
|
|
120
120
|
-- v49: the share of a core each runnable thread got where it ran
|
|
121
121
|
-- (ExecuteResult.machine.cpuShare); NULL = all it wanted.
|
|
122
|
-
cpu_share REAL
|
|
122
|
+
cpu_share REAL,
|
|
123
|
+
-- v51: the rest of the results row. How long the task waited ready
|
|
124
|
+
-- for a slot (NULL: it started at once); its output files' bytes,
|
|
125
|
+
-- uncompressed (a hit's restored, a miss's saved; NULL: no entry);
|
|
126
|
+
-- the tasks downstream of it in the run; the flaky record its note
|
|
127
|
+
-- quoted (NULL: not proven flaky).
|
|
128
|
+
queued_ms INTEGER,
|
|
129
|
+
output_bytes INTEGER,
|
|
130
|
+
blocks INTEGER,
|
|
131
|
+
flaky_passes INTEGER,
|
|
132
|
+
flaky_failures INTEGER
|
|
123
133
|
);
|
|
124
134
|
-- Two whole-table indexes only, and both APPEND: every row of a run carries the
|
|
125
135
|
-- same run_id and a started_at newer than everything before it, so
|
|
@@ -261,6 +271,49 @@ export function createTables(db: Database, store: 'main' | 'store' = 'main'): vo
|
|
|
261
271
|
arch TEXT,
|
|
262
272
|
vx_version TEXT NOT NULL
|
|
263
273
|
);
|
|
274
|
+
-- v50: what a run said that its runs rows do not hold (run-notes.ts),
|
|
275
|
+
-- for \`vx last\`: the steps it did not wait on (\`vx:prefetch\`), its
|
|
276
|
+
-- warnings, colour stripped, and each failed task's output, plain,
|
|
277
|
+
-- bounded and masked, with the files it names (JSON). Rows of the
|
|
278
|
+
-- newest 50 runs that had any; run ids are UUIDv7, so they sort by age.
|
|
279
|
+
CREATE TABLE IF NOT EXISTS main.steps (
|
|
280
|
+
run_id TEXT NOT NULL,
|
|
281
|
+
seq INTEGER NOT NULL,
|
|
282
|
+
step TEXT NOT NULL,
|
|
283
|
+
duration_ms INTEGER NOT NULL,
|
|
284
|
+
ok INTEGER NOT NULL,
|
|
285
|
+
PRIMARY KEY (run_id, seq)
|
|
286
|
+
);
|
|
287
|
+
CREATE TABLE IF NOT EXISTS main.warnings (
|
|
288
|
+
run_id TEXT NOT NULL,
|
|
289
|
+
seq INTEGER NOT NULL,
|
|
290
|
+
text TEXT NOT NULL,
|
|
291
|
+
PRIMARY KEY (run_id, seq)
|
|
292
|
+
);
|
|
293
|
+
CREATE TABLE IF NOT EXISTS main.failures (
|
|
294
|
+
run_id TEXT NOT NULL,
|
|
295
|
+
task_id TEXT NOT NULL,
|
|
296
|
+
input_moved TEXT,
|
|
297
|
+
output TEXT NOT NULL,
|
|
298
|
+
locations TEXT NOT NULL,
|
|
299
|
+
sandbox_fix TEXT,
|
|
300
|
+
PRIMARY KEY (run_id, task_id)
|
|
301
|
+
);
|
|
302
|
+
-- v50: a lockfile claimant's parse (orchestrator/lockfile-claim.ts): one
|
|
303
|
+
-- row per lockfile and claimant, read while \`version\` and \`lock\` (the
|
|
304
|
+
-- file's xxh3) match, replaced when they do not. JSON: the extra files'
|
|
305
|
+
-- hashes, the importers' digests and edges, the names each records.
|
|
306
|
+
CREATE TABLE IF NOT EXISTS main.lockfile_memos (
|
|
307
|
+
file TEXT NOT NULL,
|
|
308
|
+
claimant TEXT NOT NULL,
|
|
309
|
+
version INTEGER NOT NULL,
|
|
310
|
+
lock TEXT NOT NULL,
|
|
311
|
+
extras TEXT NOT NULL,
|
|
312
|
+
importers TEXT NOT NULL,
|
|
313
|
+
edges TEXT NOT NULL,
|
|
314
|
+
recorded TEXT,
|
|
315
|
+
PRIMARY KEY (file, claimant)
|
|
316
|
+
);
|
|
264
317
|
CREATE INDEX IF NOT EXISTS main.invocations_started ON invocations(started_at);
|
|
265
318
|
CREATE INDEX IF NOT EXISTS main.invocations_branch ON invocations(branch);
|
|
266
319
|
CREATE INDEX IF NOT EXISTS main.invocations_ci ON invocations(ci);
|
package/src/cli/cache.ts
CHANGED
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
parseSize,
|
|
12
12
|
relPosix,
|
|
13
13
|
treeBytes,
|
|
14
|
-
ui,
|
|
15
14
|
} from '../util/index.js'
|
|
15
|
+
import * as ui from '../ui/index.js'
|
|
16
16
|
import { acquireRunLock } from '../orchestrator/index.js'
|
|
17
17
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
18
18
|
import {
|
|
@@ -270,9 +270,7 @@ function printPruned(
|
|
|
270
270
|
): void {
|
|
271
271
|
if (json) {
|
|
272
272
|
const { evicted, bytesFreed, orphans, orphanBytes } = result
|
|
273
|
-
|
|
274
|
-
`${JSON.stringify({ dry: dry, evicted, bytesFreed, orphans, orphanBytes })}\n`,
|
|
275
|
-
)
|
|
273
|
+
ui.data('out', `${JSON.stringify({ dry: dry, evicted, bytesFreed, orphans, orphanBytes })}\n`)
|
|
276
274
|
return
|
|
277
275
|
}
|
|
278
276
|
const plural = (n: number, one: string, many: string): string => `${n} ${n === 1 ? one : many}`
|
|
@@ -321,6 +319,25 @@ async function clear(
|
|
|
321
319
|
storeRoot: string | null,
|
|
322
320
|
parsed: PruneArgs,
|
|
323
321
|
): Promise<number> {
|
|
322
|
+
// A clear beside a run deletes what the run is restoring or saving; it
|
|
323
|
+
// waits for the run's lock, as a trim does, and only then reads what to
|
|
324
|
+
// delete: read first, what the run saved meanwhile went uncounted.
|
|
325
|
+
const release =
|
|
326
|
+
parsed.dry === true ? async () => {} : await acquireRunLock(root, { log: warnToStderr })
|
|
327
|
+
try {
|
|
328
|
+
return clearHeld(args, root, dir, storeRoot, parsed)
|
|
329
|
+
} finally {
|
|
330
|
+
await release()
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function clearHeld(
|
|
335
|
+
args: readonly string[],
|
|
336
|
+
root: string,
|
|
337
|
+
dir: string,
|
|
338
|
+
storeRoot: string | null,
|
|
339
|
+
parsed: PruneArgs,
|
|
340
|
+
): number {
|
|
324
341
|
// The store the index recorded and today's (a changed HOME, an origin
|
|
325
342
|
// added) both go; a directory with no index is not a vx cache to delete.
|
|
326
343
|
const targets: string[] = []
|
|
@@ -376,23 +393,16 @@ async function clear(
|
|
|
376
393
|
? readdirSync(t).map((e) => path.join(t, e))
|
|
377
394
|
: [t]
|
|
378
395
|
const bytes = doomed.reduce((n, t) => n + entries(t).reduce((m, e) => m + treeBytes(e), 0), 0)
|
|
379
|
-
if (!dry
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
try {
|
|
384
|
-
for (const t of doomed) {
|
|
385
|
-
for (const e of entries(t)) rmSync(e, { recursive: true, force: true })
|
|
386
|
-
if (t === named && readdirSync(t).length === 0) rmSync(t, { recursive: true })
|
|
387
|
-
}
|
|
388
|
-
} finally {
|
|
389
|
-
await release()
|
|
396
|
+
if (!dry) {
|
|
397
|
+
for (const t of doomed) {
|
|
398
|
+
for (const e of entries(t)) rmSync(e, { recursive: true, force: true })
|
|
399
|
+
if (t === named && readdirSync(t).length === 0) rmSync(t, { recursive: true })
|
|
390
400
|
}
|
|
391
401
|
}
|
|
392
402
|
const shown = doomed.map((t) => (path.relative(root, t).startsWith('..') ? t : relPosix(root, t)))
|
|
393
403
|
if (parsed.format === 'json') {
|
|
394
404
|
const result: ClearResult = { dry: dry, cleared: shown, bytesFreed: bytes }
|
|
395
|
-
|
|
405
|
+
ui.data('out', `${JSON.stringify(result)}\n`)
|
|
396
406
|
} else
|
|
397
407
|
ui.print(
|
|
398
408
|
'out',
|
package/src/cli/completions.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
// Tab is the wrong price).
|
|
7
7
|
|
|
8
8
|
import { acceptedFlags, CORE_VERBS, flagHint, refuse, seeHelp } from './help.js'
|
|
9
|
-
import { nearest
|
|
9
|
+
import { nearest } from '../util/index.js'
|
|
10
|
+
import * as ui from '../ui/index.js'
|
|
11
|
+
import { sayNotice } from '../ui/index.js'
|
|
10
12
|
import { setting } from '../settings.js'
|
|
11
13
|
|
|
12
14
|
function enumValues(path: string): readonly string[] {
|
|
@@ -167,6 +169,6 @@ export async function completionsCmd(
|
|
|
167
169
|
const verbs = [...CORE_VERBS.filter((v) => v !== 'help' && v !== 'version'), 'help', 'version']
|
|
168
170
|
// No overlap to skip: the workspace load refuses a plugin verb named like a core one.
|
|
169
171
|
verbs.push(...pluginVerbs)
|
|
170
|
-
|
|
172
|
+
ui.data('out', completionScript(shell as CompletionShell, verbs))
|
|
171
173
|
return 0
|
|
172
174
|
}
|
package/src/cli/docs.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// whole, best first.
|
|
6
6
|
|
|
7
7
|
import { VERSION } from '../version.js'
|
|
8
|
-
import {
|
|
8
|
+
import { parseDecimalInt } from '../util/index.js'
|
|
9
|
+
import * as ui from '../ui/index.js'
|
|
9
10
|
import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
|
|
10
11
|
|
|
11
12
|
const SITE = 'https://vznjs.github.io/vx/'
|
|
@@ -238,7 +239,7 @@ export async function docsCmd(
|
|
|
238
239
|
const hits = searchDocs(pages, parsed.terms, parsed.limit)
|
|
239
240
|
const query = parsed.terms.join(' ')
|
|
240
241
|
if (parsed.json) {
|
|
241
|
-
|
|
242
|
+
ui.data('out', `${JSON.stringify({ query, hits })}\n`)
|
|
242
243
|
return 0
|
|
243
244
|
}
|
|
244
245
|
if (hits.length === 0) {
|
|
@@ -249,15 +250,13 @@ export async function docsCmd(
|
|
|
249
250
|
)
|
|
250
251
|
return 0
|
|
251
252
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
260
|
-
.join('\n'),
|
|
253
|
+
// Each hit an excerpt of the page, its link under it: off colour the
|
|
254
|
+
// bare markdown, as a pipe copies it.
|
|
255
|
+
ui.print(
|
|
256
|
+
'out',
|
|
257
|
+
...hits.map((h) =>
|
|
258
|
+
ui.excerpt(`${h.page}.md${h.heading === '' ? '' : ` § ${h.heading}`} · ${h.url}`, h.text),
|
|
259
|
+
),
|
|
261
260
|
)
|
|
262
261
|
return 0
|
|
263
262
|
}
|
package/src/cli/help.ts
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
errorDocument,
|
|
5
|
-
maskedLine,
|
|
6
|
-
nearest,
|
|
7
|
-
paint,
|
|
8
|
-
refusalNotice,
|
|
9
|
-
ui,
|
|
10
|
-
wantsJson,
|
|
11
|
-
type ColorSupport,
|
|
12
|
-
} from '../util/index.js'
|
|
1
|
+
import { CORE_VERBS, errorDocument, maskedLine, nearest, wantsJson } from '../util/index.js'
|
|
2
|
+
import * as ui from '../ui/index.js'
|
|
3
|
+
import { detectColors, paint, refusalNotice, type ColorSupport } from '../ui/index.js'
|
|
13
4
|
|
|
14
5
|
export async function printHelp(
|
|
15
6
|
pluginCommands: readonly string[] = [],
|
|
16
7
|
verb?: string,
|
|
17
8
|
): Promise<void> {
|
|
18
9
|
const text = verb === undefined ? helpText(pluginCommands) : verbHelpText(verb)
|
|
19
|
-
|
|
10
|
+
// The reference a script greps: painted on a terminal only, and strips
|
|
11
|
+
// back to `text` exactly, so it goes out as written.
|
|
12
|
+
ui.data('out', paintHelp(text, helpColors(process.stdout, detectColors), paint))
|
|
20
13
|
}
|
|
21
14
|
|
|
22
15
|
/**
|
|
@@ -31,9 +24,6 @@ export function helpColors(
|
|
|
31
24
|
return { enabled: stream.isTTY === true && detect(stream as NodeJS.WriteStream).enabled }
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
const VERB = '#06b6d4' // cyan-500 — the orchestrator's accent
|
|
35
|
-
const TERM = '#22d3ee' // cyan-400 — a flag, a selector, an example
|
|
36
|
-
|
|
37
27
|
/**
|
|
38
28
|
* The reference with ANSI accents, line by line and never reflowed, so the
|
|
39
29
|
* painted text strips back to `text` exactly: section headings bold (their
|
|
@@ -48,13 +38,12 @@ export function paintHelp(text: string, colors: ColorSupport, paint: Paint): str
|
|
|
48
38
|
if (line === '') return line
|
|
49
39
|
if (i === 0) {
|
|
50
40
|
const [name, ...rest] = line.split(' ')
|
|
51
|
-
return [paint(
|
|
41
|
+
return [paint('info', name!, colors, true), ...rest].join(' ')
|
|
52
42
|
}
|
|
53
43
|
const heading = /^([A-Z][^:]*?)( \(for [^)]*\))?:$/.exec(line)
|
|
54
44
|
if (heading !== null) {
|
|
55
|
-
const qualifier =
|
|
56
|
-
|
|
57
|
-
return `${paint('', heading[1]!, colors, { bold: true })}${qualifier}${paint('', ':', colors, { bold: true })}`
|
|
45
|
+
const qualifier = heading[2] === undefined ? '' : paint('muted', heading[2], colors)
|
|
46
|
+
return `${paint('strong', heading[1]!, colors)}${qualifier}${paint('strong', ':', colors)}`
|
|
58
47
|
}
|
|
59
48
|
// ` term description`: a term is a flag, a `vx` form, a selector.
|
|
60
49
|
const row = /^(\s+)(\S(?:.*?\S)?)(\s{2,}\S.*)?$/.exec(line)
|
|
@@ -66,11 +55,11 @@ export function paintHelp(text: string, colors: ColorSupport, paint: Paint): str
|
|
|
66
55
|
if (description === '' && !(CORE_VERBS as readonly string[]).includes(verb)) return line
|
|
67
56
|
const head = `vx ${verb}`
|
|
68
57
|
const tail = term!.slice(head.length)
|
|
69
|
-
return `${indent}${paint(
|
|
58
|
+
return `${indent}${paint('info', head, colors, true)}${tail === '' ? '' : paint('term', tail, colors)}${description}`
|
|
70
59
|
}
|
|
71
60
|
if (description === '') return line
|
|
72
61
|
if (/^(-|\(|[\w-]+#)/.test(term!))
|
|
73
|
-
return `${indent}${paint(
|
|
62
|
+
return `${indent}${paint('term', term!, colors)}${description}`
|
|
74
63
|
return line
|
|
75
64
|
})
|
|
76
65
|
.join('\n')
|
|
@@ -475,7 +464,7 @@ export function refuse(
|
|
|
475
464
|
): number {
|
|
476
465
|
const line = `vx ${verb}: ${message}`
|
|
477
466
|
ui.print('err', refusalNotice(`vx ${verb}`, message))
|
|
478
|
-
if (wantsJson(args))
|
|
467
|
+
if (wantsJson(args)) ui.data('out', errorDocument(code, maskedLine(line)))
|
|
479
468
|
return 1
|
|
480
469
|
}
|
|
481
470
|
|
package/src/cli/index.ts
CHANGED
|
@@ -5,21 +5,19 @@ import { VERSION } from '../version.js'
|
|
|
5
5
|
import { checkSettingsEnv } from '../settings.js'
|
|
6
6
|
import { CORE_VERBS, flagHint, printHelp, refuse, refusedWord, seeHelp } from './help.js'
|
|
7
7
|
import {
|
|
8
|
-
detectColors,
|
|
9
8
|
errorDocument,
|
|
10
9
|
hideEnvFromTasks,
|
|
11
|
-
sayNotice,
|
|
12
10
|
isFsRefusal,
|
|
13
11
|
isOutOfFds,
|
|
14
12
|
isUserError,
|
|
15
13
|
maskedLine,
|
|
16
14
|
nearest,
|
|
17
|
-
refusalNotice,
|
|
18
15
|
thrownMessage,
|
|
19
|
-
ui,
|
|
20
16
|
UserError,
|
|
21
17
|
wantsJson,
|
|
22
18
|
} from '../util/index.js'
|
|
19
|
+
import * as ui from '../ui/index.js'
|
|
20
|
+
import { sayNotice, refusalNotice } from '../ui/index.js'
|
|
23
21
|
|
|
24
22
|
// Every verb is imported when invoked, `run` included, and so is the
|
|
25
23
|
// plugin-verb lookup: each pulls in the orchestrator and the workspace
|
|
@@ -163,9 +161,7 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
163
161
|
)
|
|
164
162
|
}
|
|
165
163
|
// Painted, and the same words piped: a script reads `vx <version>`.
|
|
166
|
-
|
|
167
|
-
`${ui.inline([ui.cmd('vx'), ' ', ui.tone('info', VERSION)], detectColors(process.stdout))}\n`,
|
|
168
|
-
)
|
|
164
|
+
ui.print('out', ui.line(ui.cmd('vx'), ' ', ui.tone('info', VERSION)))
|
|
169
165
|
return 0
|
|
170
166
|
}
|
|
171
167
|
case 'run':
|
|
@@ -238,8 +234,7 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
238
234
|
): number => {
|
|
239
235
|
sayNotice(tone, 'vx', text.replace(/^vx: /, '').trimEnd())
|
|
240
236
|
// `vx --format json` puts the flag where the verb goes.
|
|
241
|
-
if (wantsJson([command, ...rest]))
|
|
242
|
-
process.stdout.write(errorDocument(code, maskedLine(json)))
|
|
237
|
+
if (wantsJson([command, ...rest])) ui.data('out', errorDocument(code, maskedLine(json)))
|
|
243
238
|
return 1
|
|
244
239
|
}
|
|
245
240
|
const loadNote =
|
package/src/cli/info.ts
CHANGED
|
@@ -9,7 +9,9 @@ import { type FlakyTask } from '../history/index.js'
|
|
|
9
9
|
import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
|
|
10
10
|
import { namedCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
|
|
11
11
|
import { formatBytes } from './format.js'
|
|
12
|
-
import {
|
|
12
|
+
import { MIN_BUN } from '../util/index.js'
|
|
13
|
+
import * as ui from '../ui/index.js'
|
|
14
|
+
import { type ColorSupport } from '../ui/index.js'
|
|
13
15
|
import { envName } from '../settings.js'
|
|
14
16
|
|
|
15
17
|
interface InfoArgs {
|
|
@@ -55,15 +57,18 @@ export async function infoCmd(args: readonly string[]): Promise<number> {
|
|
|
55
57
|
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
56
58
|
warn: warnToStderr,
|
|
57
59
|
})
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
? `${JSON.stringify(facts)}\n`
|
|
61
|
-
: `${renderInfo(facts, detectColors(process.stdout))}\n`,
|
|
62
|
-
)
|
|
60
|
+
if (parsed.format === 'json') ui.data('out', `${JSON.stringify(facts)}\n`)
|
|
61
|
+
else ui.print('out', infoReport(facts))
|
|
63
62
|
return 0
|
|
64
63
|
}
|
|
65
64
|
|
|
65
|
+
/** `vx info` as text: the report's lines for `colors`. */
|
|
66
66
|
export function renderInfo(f: InfoFacts, colors: ColorSupport = { enabled: false }): string {
|
|
67
|
+
return infoReport(f).lines(colors).join('\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The `vx info` report: one field a fact. */
|
|
71
|
+
function infoReport(f: InfoFacts): ui.Block {
|
|
67
72
|
const rows: [string, string][] = [
|
|
68
73
|
[
|
|
69
74
|
'vx',
|
|
@@ -125,7 +130,7 @@ export function renderInfo(f: InfoFacts, colors: ColorSupport = { enabled: false
|
|
|
125
130
|
typeof f.lockError === 'string' ? `refused: ${f.lockError}` : f.lockfile ? 'yes' : 'no',
|
|
126
131
|
],
|
|
127
132
|
]
|
|
128
|
-
return ui.report('vx info', rows)
|
|
133
|
+
return ui.report('vx info', rows)
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
/** `812 (1.2 GB)`, or why the index cannot be read and its fix. */
|
|
@@ -195,6 +200,9 @@ function trimCores(n: number): string {
|
|
|
195
200
|
|
|
196
201
|
/** `2 — app#test (3 of 7 runs failed on unchanged inputs); api#e2e (1 of 4)`, or `none`. */
|
|
197
202
|
function describeProjects(f: InfoFacts): string {
|
|
203
|
+
// The count is the lock's: with none read, 0 would say the workspace has no project.
|
|
204
|
+
if (f.lockError != null) return '— (the lock is refused)'
|
|
205
|
+
if (!f.lockfile) return '— (no lock: `vx lock`)'
|
|
198
206
|
return `${f.projects} (${f.tasks} task${f.tasks === 1 ? '' : 's'})`
|
|
199
207
|
}
|
|
200
208
|
|
package/src/cli/init.ts
CHANGED
|
@@ -23,17 +23,14 @@ import {
|
|
|
23
23
|
} from './upgrade.js'
|
|
24
24
|
import { VERSION } from '../version.js'
|
|
25
25
|
import {
|
|
26
|
-
detectColors,
|
|
27
26
|
DOT_VX_GITATTRIBUTES,
|
|
28
27
|
DOT_VX_GITIGNORE,
|
|
29
|
-
paint,
|
|
30
28
|
refuseSymlink,
|
|
31
|
-
sayNotice,
|
|
32
|
-
SUCCESS,
|
|
33
|
-
ui,
|
|
34
29
|
UserError,
|
|
35
30
|
writeNoFollow,
|
|
36
31
|
} from '../util/index.js'
|
|
32
|
+
import * as ui from '../ui/index.js'
|
|
33
|
+
import { sayNotice } from '../ui/index.js'
|
|
37
34
|
import {
|
|
38
35
|
findWorkspaceRoot,
|
|
39
36
|
PROJECT_CONFIG_FILENAMES,
|
|
@@ -279,8 +276,9 @@ async function install(
|
|
|
279
276
|
let said = ''
|
|
280
277
|
const decoder = new TextDecoder()
|
|
281
278
|
for await (const chunk of proc.stderr) {
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
const text = decoder.decode(chunk, { stream: true })
|
|
280
|
+
ui.data('err', text)
|
|
281
|
+
said += text
|
|
284
282
|
}
|
|
285
283
|
const code = await proc.exited
|
|
286
284
|
if (code !== 0) {
|
|
@@ -351,13 +349,22 @@ async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promi
|
|
|
351
349
|
const factory = /^export function (\w+)/m.exec(template.plugin)![1]!
|
|
352
350
|
const workspaceFile =
|
|
353
351
|
WORKSPACE_CONFIG_FILENAMES.find((n) => existsSync(path.join(root, n))) ?? '.vx/workspace.ts'
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
352
|
+
const names = files.flatMap(([rel], i) => [...(i > 0 ? [' and '] : []), ui.strong(rel)])
|
|
353
|
+
ui.print('out', ui.line(ui.tone('ok', args.dry ? 'would write' : 'wrote', true), ' ', ...names))
|
|
354
|
+
ui.print(
|
|
355
|
+
'out',
|
|
356
|
+
ui.line(
|
|
357
|
+
'Declare it in ',
|
|
358
|
+
ui.path(workspaceFile),
|
|
359
|
+
': ',
|
|
360
|
+
ui.cmd(`import { ${factory} } from '../plugins/${seam}.ts'`),
|
|
361
|
+
', then ',
|
|
362
|
+
ui.cmd(`plugins: [${factory}(…)]`),
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
ui.print(
|
|
366
|
+
'out',
|
|
367
|
+
ui.line('Test it: ', ui.cmd(`bun test plugins/${seam}.test.ts`), ' (needs @vzn/vx installed)'),
|
|
361
368
|
)
|
|
362
369
|
return 0
|
|
363
370
|
}
|
|
@@ -460,7 +467,8 @@ async function starter(
|
|
|
460
467
|
const declare = `declare a task in ${kept.find((f) => f.startsWith('vx.config.')) ?? config}, then vx lock && vx run <task>`
|
|
461
468
|
const next = `${noGit ? 'git init, ' : ''}${hasScripts(pkgRoot) ? `bunx @vzn/vx-migrate${args.mjs ? ' --mjs' : ''} to make package.json scripts tasks, or ${declare}` : declare}`
|
|
462
469
|
if (args.json === true) {
|
|
463
|
-
|
|
470
|
+
ui.data(
|
|
471
|
+
'out',
|
|
464
472
|
`${JSON.stringify({ dry: true, source: 'no turbo.json or nx.json', files, kept, replaced, todos: [], notes: [], next, installs })}\n`,
|
|
465
473
|
)
|
|
466
474
|
return 0
|