@vzn/vx 0.0.639 → 0.0.641
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 +1 -1
- package/docs/caching.md +158 -106
- package/docs/cli.md +80 -45
- package/docs/execution.md +11 -10
- package/docs/features.md +9 -8
- package/docs/patterns.md +1 -1
- package/docs/schema.md +252 -190
- package/docs/security.md +5 -4
- package/launcher.sh +9 -2
- package/package.json +5 -5
- package/schemas/show.json +14 -14
- package/skills/vx/SKILL.md +4 -3
- package/src/bin.ts +8 -6
- package/src/cache/archive.ts +248 -41
- package/src/cache/cache.ts +46 -19
- package/src/cache/file-hashes.ts +7 -12
- package/src/cache/fs-clock.ts +62 -0
- package/src/cache/git-inputs.ts +2 -2
- package/src/cache/index.ts +6 -0
- package/src/cache/inputs.ts +228 -111
- package/src/cache/key-fold.ts +23 -3
- package/src/cache/layer.ts +15 -8
- package/src/cache/layered-cache.ts +8 -7
- package/src/cache/linked-inputs.ts +87 -0
- package/src/cache/scoped-cache.ts +8 -2
- package/src/cli/foreign-flags.ts +1 -1
- package/src/cli/help.ts +7 -1
- package/src/cli/init.ts +23 -11
- package/src/cli/last.ts +24 -1
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/select.ts +16 -6
- package/src/cli/show.ts +9 -6
- package/src/cli/upgrade.ts +132 -21
- package/src/cli/watch-filter.ts +23 -12
- package/src/cli/watch-fs.ts +2 -1
- package/src/cli/watch-judge.ts +49 -5
- package/src/cli/watch-set.ts +18 -8
- package/src/cli/watch.ts +74 -2
- package/src/cli/why.ts +5 -3
- package/src/config.ts +19 -15
- package/src/exec/executor.ts +4 -4
- package/src/graph/dependency-spec.ts +2 -2
- package/src/graph/folds.ts +236 -0
- package/src/graph/index.ts +8 -1
- package/src/graph/scheduler.ts +41 -15
- package/src/graph/task-graph.ts +135 -73
- package/src/handoff.ts +5 -0
- package/src/index.ts +2 -2
- package/src/orchestrator/affected-tasks.ts +20 -19
- package/src/orchestrator/download-policy.ts +16 -22
- package/src/orchestrator/excluded-keys.ts +4 -5
- package/src/orchestrator/execute-task.ts +29 -25
- package/src/orchestrator/hit-restore.ts +101 -27
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/keyed-projects.ts +12 -51
- package/src/orchestrator/local-shortcircuit.ts +9 -1
- package/src/orchestrator/persistent-task.ts +3 -3
- package/src/orchestrator/placement.ts +2 -2
- package/src/orchestrator/plan.ts +1 -1
- package/src/orchestrator/plugin-host.ts +66 -27
- package/src/orchestrator/plugin.ts +12 -2
- package/src/orchestrator/prepare.ts +12 -5
- package/src/orchestrator/projects.ts +7 -10
- package/src/orchestrator/run-report.ts +145 -46
- package/src/orchestrator/run-steps.ts +57 -0
- package/src/orchestrator/run.ts +76 -44
- package/src/orchestrator/sandbox-fix.ts +1 -1
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/stable-keys.ts +59 -26
- package/src/orchestrator/steps.ts +26 -6
- package/src/orchestrator/task-hash.ts +31 -9
- package/src/orchestrator/telemetry-host.ts +3 -3
- package/src/orchestrator/telemetry.ts +9 -4
- package/src/orchestrator/upstream.ts +4 -181
- package/src/report/framed-output.ts +1 -1
- package/src/report/logger.ts +5 -2
- package/src/report/status-line.ts +7 -3
- package/src/report/summary.ts +6 -4
- package/src/settings.ts +11 -17
- package/src/util/index.ts +3 -0
- package/src/util/paths.ts +39 -5
- package/src/workspace/affected.ts +81 -2
- package/src/workspace/config-eval.ts +2 -2
- package/src/workspace/config-imports.ts +1 -1
- package/src/workspace/config-schema.ts +142 -143
- package/src/workspace/lockfile.ts +1 -1
- package/src/workspace/migration.ts +19 -3
- package/src/workspace/project-loader.ts +5 -4
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
MAX_SCOPED_PATHSPECS,
|
|
32
32
|
startGitEnumeration,
|
|
33
33
|
lazyGitEnumeration,
|
|
34
|
+
measureFsClockLag,
|
|
34
35
|
} from '../cache/index.js'
|
|
35
36
|
import {
|
|
36
37
|
buildPackageGraph,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
expandRequested,
|
|
51
52
|
type GraphRules,
|
|
52
53
|
isGroupTask,
|
|
54
|
+
refuseFiltersThroughGroups,
|
|
53
55
|
type TaskNode,
|
|
54
56
|
undeclaredDepsError,
|
|
55
57
|
unresolvedRequests,
|
|
@@ -61,6 +63,7 @@ import {
|
|
|
61
63
|
hasHook,
|
|
62
64
|
resolveCache,
|
|
63
65
|
teardownPlugins,
|
|
66
|
+
type ScheduleRun,
|
|
64
67
|
} from './plugin-host.js'
|
|
65
68
|
import {
|
|
66
69
|
discoverProjects,
|
|
@@ -83,7 +86,6 @@ import { currentBranch } from './run-context.js'
|
|
|
83
86
|
import { createHashCache, type HashCache } from './task-hash.js'
|
|
84
87
|
import { type Logger, warnOf } from '../report/index.js'
|
|
85
88
|
import type { RunOptions } from './options.js'
|
|
86
|
-
import { refuseFiltersThroughGroups } from './upstream.js'
|
|
87
89
|
import { runRules } from '../settings.js'
|
|
88
90
|
|
|
89
91
|
export interface PreparedRun {
|
|
@@ -114,9 +116,10 @@ export interface PreparedRun {
|
|
|
114
116
|
* merged over the structural baseline by the scheduler). Empty when no
|
|
115
117
|
* plugin declares the stage. Asked only by a run with a task to start:
|
|
116
118
|
* the stage reads history (148 ms at 8,002 tasks) to order work a
|
|
117
|
-
* nothing-changed run never dispatches. Runs the stage once
|
|
119
|
+
* nothing-changed run never dispatches. Runs the stage once: `run`
|
|
120
|
+
* times each plugin's call and hears its later `ctx.refine` sets.
|
|
118
121
|
*/
|
|
119
|
-
priorities: () => Promise<ReadonlyMap<string, number>>
|
|
122
|
+
priorities: (run?: ScheduleRun) => Promise<ReadonlyMap<string, number>>
|
|
120
123
|
/**
|
|
121
124
|
* The run's branch (`run.branch`, else git's, else the CI provider's):
|
|
122
125
|
* what its history records and the timings a forecast reads first.
|
|
@@ -256,6 +259,9 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
256
259
|
const cacheDir = options.cacheDir
|
|
257
260
|
? path.resolve(options.cwd, options.cacheDir)
|
|
258
261
|
: resolveCacheDir(workspaceRoot, workspaceConfig)
|
|
262
|
+
// Before any racy window is judged: a file system behind this process's
|
|
263
|
+
// clock dated a mid-command edit before the command (fs-clock.ts).
|
|
264
|
+
measureFsClockLag(cacheDir)
|
|
259
265
|
// Asked now, read when the cache opens: a repository with no remote is
|
|
260
266
|
// named by its root commit, a git spawn the discovery below overlaps.
|
|
261
267
|
const storeRoot = options.cacheDir ? null : resolveStoreRoot(workspaceRoot, workspaceConfig)
|
|
@@ -668,7 +674,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
668
674
|
let usesWorkspaceInputs = false
|
|
669
675
|
for (const n of nodes.values()) {
|
|
670
676
|
graphDirs.add(n.projectDir)
|
|
671
|
-
if ((n.config.
|
|
677
|
+
if ((n.config.inputs?.workspaceFiles?.length ?? 0) > 0) usesWorkspaceInputs = true
|
|
672
678
|
}
|
|
673
679
|
const projectDirs = [...graphDirs]
|
|
674
680
|
const enumerating =
|
|
@@ -743,7 +749,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
743
749
|
return own
|
|
744
750
|
}
|
|
745
751
|
let scheduled: Promise<ReadonlyMap<string, number>> | undefined
|
|
746
|
-
const priorities = (): Promise<ReadonlyMap<string, number>> =>
|
|
752
|
+
const priorities = (run?: ScheduleRun): Promise<ReadonlyMap<string, number>> =>
|
|
747
753
|
(scheduled ??= hasHook(plugins, 'schedule')
|
|
748
754
|
? applyScheduleHooks(
|
|
749
755
|
plugins,
|
|
@@ -756,6 +762,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
756
762
|
branch: branch(),
|
|
757
763
|
},
|
|
758
764
|
cacheFor,
|
|
765
|
+
run,
|
|
759
766
|
)
|
|
760
767
|
: Promise.resolve(new Map()))
|
|
761
768
|
|
|
@@ -258,7 +258,7 @@ export async function loadProjects(args: LoadProjectsArgs): Promise<LoadedProjec
|
|
|
258
258
|
// Turbo spells the root package `//` (`dependsOn: ['//#lint']`), as vx
|
|
259
259
|
// names the root project unless something names it; a named root's
|
|
260
260
|
// spelling is rewritten to its name once, here, for the closure, the
|
|
261
|
-
// graph and `
|
|
261
|
+
// graph and `inputs.tasks` alike. With no root project it stays,
|
|
262
262
|
// and the graph's refusal says so.
|
|
263
263
|
const root = path.resolve(workspaceRoot)
|
|
264
264
|
const rootName = args.projectMetas.find((m) => path.resolve(m.dir) === root)?.name
|
|
@@ -357,10 +357,7 @@ export async function loadProjects(args: LoadProjectsArgs): Promise<LoadedProjec
|
|
|
357
357
|
*/
|
|
358
358
|
const DEFAULT_BUILD: TaskConfig = Object.freeze({
|
|
359
359
|
dependsOn: Object.freeze(['^build']),
|
|
360
|
-
|
|
361
|
-
inputs: Object.freeze({ files: Object.freeze(['**']) }),
|
|
362
|
-
outputs: Object.freeze({ files: Object.freeze([]) }),
|
|
363
|
-
}),
|
|
360
|
+
inputs: Object.freeze({ files: Object.freeze(['**']) }),
|
|
364
361
|
})
|
|
365
362
|
|
|
366
363
|
/** The default `build`, which a menu of the tasks a user declared leaves out. */
|
|
@@ -374,7 +371,7 @@ function withDefaultBuild(config: ProjectConfig): ProjectConfig {
|
|
|
374
371
|
}
|
|
375
372
|
|
|
376
373
|
/**
|
|
377
|
-
* `config` with Turbo's `//#task` (in `dependsOn` and `
|
|
374
|
+
* `config` with Turbo's `//#task` (in `dependsOn` and `inputs.tasks`)
|
|
378
375
|
* spelled `<root>#task`; the same object when it holds none. A copy, not
|
|
379
376
|
* an edit: a loaded config may be a cached or a frozen one.
|
|
380
377
|
*/
|
|
@@ -386,14 +383,14 @@ function rootSpelled(config: ProjectConfig, rootName: string): ProjectConfig {
|
|
|
386
383
|
let tasks: NonNullable<ProjectConfig['tasks']> | undefined
|
|
387
384
|
for (const [name, task] of Object.entries(config.tasks ?? {})) {
|
|
388
385
|
const dependsOn = fix(task.dependsOn)
|
|
389
|
-
const inputTasks = fix(task.
|
|
390
|
-
if (dependsOn === task.dependsOn && inputTasks === task.
|
|
386
|
+
const inputTasks = fix(task.inputs?.tasks)
|
|
387
|
+
if (dependsOn === task.dependsOn && inputTasks === task.inputs?.tasks) continue
|
|
391
388
|
tasks ??= { ...config.tasks }
|
|
392
389
|
tasks[name] = {
|
|
393
390
|
...task,
|
|
394
391
|
...(dependsOn !== undefined ? { dependsOn: [...dependsOn] } : {}),
|
|
395
|
-
...(task.
|
|
396
|
-
? {
|
|
392
|
+
...(task.inputs !== undefined && inputTasks !== undefined
|
|
393
|
+
? { inputs: { ...task.inputs, tasks: [...inputTasks] } }
|
|
397
394
|
: {}),
|
|
398
395
|
}
|
|
399
396
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
// The report every plugin's teardown reads, and the run's own bytes kept
|
|
2
2
|
// in a remote that takes its writes (owner 2026-10-15): a failed task's
|
|
3
|
-
// log and the files its outputs left,
|
|
3
|
+
// log and the files its outputs left (`cache.upload.failures`), and the
|
|
4
|
+
// log of a task that saved no entry, on every run (M, 2026-10-16: "Always
|
|
5
|
+
// keep"). Each task's bytes leave when it ends, off its path, like its
|
|
6
|
+
// entry's upload (owner 2026-10-16); the run only waits for them where it
|
|
7
|
+
// waits for every upload, and the footer counts them as the task's remote
|
|
8
|
+
// traffic.
|
|
4
9
|
|
|
5
10
|
import {
|
|
11
|
+
type RemoteTraffic,
|
|
6
12
|
packRunOutputs,
|
|
7
13
|
resolveOutputs,
|
|
8
14
|
resolveWorkspaceOutputs,
|
|
@@ -14,17 +20,29 @@ import { relPosix } from '../util/index.js'
|
|
|
14
20
|
import { decodeOutputLog } from './output-log.js'
|
|
15
21
|
import type { RunReport, RunReportTask } from './plugin.js'
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
/** Stores one run entry; resolves whether a remote kept it. */
|
|
24
|
+
export type KeepRunEntry = (name: string, bytes: Uint8Array) => Promise<boolean>
|
|
25
|
+
|
|
26
|
+
export interface RunKeeperArgs {
|
|
18
27
|
readonly runId: string
|
|
19
|
-
readonly commit: string | null
|
|
20
|
-
readonly ciRun: CiContext
|
|
21
|
-
readonly ok: boolean
|
|
22
28
|
readonly workspaceRoot: string
|
|
23
29
|
readonly nestedDirsByProject: ReadonlyMap<string, string[]>
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
readonly keep: KeepRunEntry
|
|
31
|
+
/** Whether a failed task's log and outputs are kept (`cache.upload.failures`). */
|
|
32
|
+
readonly failures: boolean
|
|
33
|
+
/** The footer's remote traffic: each kept task counts under its id. */
|
|
34
|
+
readonly traffic: RemoteTraffic
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether a run on CI or off it keeps failures, by `cache.upload.failures` (default `'ci'`). */
|
|
38
|
+
export function keepsFailures(where: unknown, ci: boolean): boolean {
|
|
39
|
+
const at = where ?? 'ci'
|
|
40
|
+
return at === 'always' || (at === 'ci') === ci
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
/** Concurrent run-entry writes; more queue. */
|
|
44
|
+
const KEEP_CONCURRENCY = 8
|
|
45
|
+
|
|
28
46
|
/** The files `node`'s declared outputs match on disk now, workspace-relative and sorted. */
|
|
29
47
|
async function outputsLeft(
|
|
30
48
|
node: TaskNode,
|
|
@@ -40,52 +58,133 @@ async function outputsLeft(
|
|
|
40
58
|
return [...new Set([...own, ...ws].map((p) => relPosix(workspaceRoot, p)))].sort()
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
/** Keeps each task's run bytes as it ends, and names them in the run's report. */
|
|
62
|
+
export class RunKeeper {
|
|
63
|
+
private readonly kept = new Map<string, { log?: string; outputs?: string }>()
|
|
64
|
+
private readonly queue: (() => Promise<void>)[] = []
|
|
65
|
+
private readonly pending = new Set<Promise<void>>()
|
|
66
|
+
private active = 0
|
|
67
|
+
private stopped = false
|
|
68
|
+
|
|
69
|
+
constructor(private readonly args: RunKeeperArgs) {}
|
|
70
|
+
|
|
71
|
+
/** A task ended: queue what of it the run keeps. */
|
|
72
|
+
taskDone(o: TaskOutcome): void {
|
|
73
|
+
if (this.stopped || isGroupTask(o.node)) return
|
|
74
|
+
const { runId, workspaceRoot } = this.args
|
|
75
|
+
const id = o.node.id
|
|
76
|
+
const slot = (): { log?: string; outputs?: string } => {
|
|
77
|
+
let s = this.kept.get(id)
|
|
78
|
+
if (s === undefined) this.kept.set(id, (s = {}))
|
|
79
|
+
return s
|
|
57
80
|
}
|
|
58
|
-
tasks.push(task)
|
|
59
|
-
if (keep === undefined) continue
|
|
60
81
|
const runLog = o.runLog
|
|
61
82
|
if (runLog !== undefined) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
83
|
+
const text = decodeOutputLog(runLog)
|
|
84
|
+
.map((c) => c.text)
|
|
85
|
+
.join('')
|
|
86
|
+
if (text.length > 0) {
|
|
87
|
+
const name = runEntryName(runId, id, 'log')
|
|
88
|
+
const bytes = new TextEncoder().encode(text)
|
|
89
|
+
this.enqueue(id, async () => {
|
|
90
|
+
if (!(await this.args.keep(name, bytes))) return false
|
|
91
|
+
slot().log = name
|
|
92
|
+
return true
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (this.args.failures && o.status === 'failed' && o.where === undefined) {
|
|
97
|
+
// Read now: a later task may write where this one's outputs lie.
|
|
98
|
+
const packed = outputsLeft(
|
|
99
|
+
o.node,
|
|
100
|
+
workspaceRoot,
|
|
101
|
+
this.args.nestedDirsByProject.get(o.node.projectName) ?? [],
|
|
102
|
+
).then((paths) => packRunOutputs(workspaceRoot, paths))
|
|
103
|
+
const name = runEntryName(runId, id, 'outputs')
|
|
104
|
+
this.enqueue(id, async () => {
|
|
105
|
+
const bytes = await packed
|
|
106
|
+
if (bytes === null || !(await this.args.keep(name, bytes))) return false
|
|
107
|
+
slot().outputs = name
|
|
108
|
+
return true
|
|
69
109
|
})
|
|
70
110
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Resolves once every queued write has settled, or the run was stopped. */
|
|
114
|
+
async settled(): Promise<void> {
|
|
115
|
+
while (!this.stopped && this.pending.size > 0) await Promise.all(this.pending)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A stopped run starts no more writes and stops waiting on those in flight. */
|
|
119
|
+
stop(): void {
|
|
120
|
+
this.stopped = true
|
|
121
|
+
this.queue.length = 0
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** What was kept of each task, by id, for `reportOf`. */
|
|
125
|
+
keptOf(id: string): { log?: string; outputs?: string } | undefined {
|
|
126
|
+
return this.kept.get(id)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Queue `job`, which writes `id`'s bytes; a write that lands counts as `id`'s upload. */
|
|
130
|
+
private enqueue(id: string, job: () => Promise<boolean>): void {
|
|
131
|
+
let done!: () => void
|
|
132
|
+
const p = new Promise<void>((resolve) => (done = resolve))
|
|
133
|
+
this.pending.add(p)
|
|
134
|
+
const { traffic } = this.args
|
|
135
|
+
let key = traffic.keys.get(id)
|
|
136
|
+
if (key === undefined)
|
|
137
|
+
traffic.keys.set(id, (key = { uploading: 0, landed: false, failures: [] }))
|
|
138
|
+
const counted = key
|
|
139
|
+
counted.uploading++
|
|
140
|
+
traffic.inFlight++
|
|
141
|
+
this.queue.push(async () => {
|
|
142
|
+
try {
|
|
143
|
+
if (!this.stopped && (await job())) counted.landed = true
|
|
144
|
+
} catch {
|
|
145
|
+
// a record, never the run's verdict
|
|
146
|
+
} finally {
|
|
147
|
+
counted.uploading--
|
|
148
|
+
traffic.inFlight--
|
|
149
|
+
if (counted.uploading === 0 && !counted.landed && counted.failures.length === 0)
|
|
150
|
+
traffic.keys.delete(id)
|
|
151
|
+
this.pending.delete(p)
|
|
152
|
+
done()
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
this.pump()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private pump(): void {
|
|
159
|
+
while (this.active < KEEP_CONCURRENCY && this.queue.length > 0) {
|
|
160
|
+
const job = this.queue.shift()!
|
|
161
|
+
this.active++
|
|
162
|
+
void job().finally(() => {
|
|
163
|
+
this.active--
|
|
164
|
+
this.pump()
|
|
84
165
|
})
|
|
85
166
|
}
|
|
86
167
|
}
|
|
87
|
-
|
|
88
|
-
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The report: every task, with the names of what `keeper` kept of it. */
|
|
171
|
+
export function reportOf(
|
|
172
|
+
final: readonly TaskOutcome[],
|
|
173
|
+
run: { runId: string; commit: string | null; ciRun: CiContext; ok: boolean },
|
|
174
|
+
keeper: RunKeeper | undefined,
|
|
175
|
+
): RunReport {
|
|
176
|
+
const tasks: RunReportTask[] = []
|
|
177
|
+
for (const o of final) {
|
|
178
|
+
if (isGroupTask(o.node)) continue
|
|
179
|
+
const kept = keeper?.keptOf(o.node.id)
|
|
180
|
+
tasks.push({
|
|
181
|
+
id: o.node.id,
|
|
182
|
+
status: o.status,
|
|
183
|
+
durationMs: Math.round(o.durationMs),
|
|
184
|
+
...(o.hash !== undefined ? { key: o.hash } : {}),
|
|
185
|
+
...(kept?.log !== undefined ? { log: kept.log } : {}),
|
|
186
|
+
...(kept?.outputs !== undefined ? { outputs: kept.outputs } : {}),
|
|
187
|
+
})
|
|
89
188
|
}
|
|
90
|
-
return { runId, commit:
|
|
189
|
+
return { runId: run.runId, commit: run.commit, ciRun: run.ciRun, ok: run.ok, tasks }
|
|
91
190
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The steps a run did not wait on (`vx:prefetch`), for `vx last` (M,
|
|
2
|
+
// 2026-10-16): they never show in a run's output. One file per run that
|
|
3
|
+
// had one, `<cacheDir>/steps/<runId>.json`, the newest 50 kept, as
|
|
4
|
+
// run-failures.ts keeps failed output: files, not cache.db, so the index
|
|
5
|
+
// layout stays as it was. A run with none writes nothing.
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { isFsRefusal } from '../util/index.js'
|
|
10
|
+
|
|
11
|
+
/** Runs whose background steps are kept; run ids are UUIDv7, so names sort by age. */
|
|
12
|
+
const KEPT = 50
|
|
13
|
+
|
|
14
|
+
/** One of vx's own steps that ran beside the tasks. */
|
|
15
|
+
export interface RunStep {
|
|
16
|
+
id: string
|
|
17
|
+
durationMs: number
|
|
18
|
+
ok: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const dirOf = (cacheDir: string): string => path.join(cacheDir, 'steps')
|
|
22
|
+
|
|
23
|
+
/** Keep `steps` for `runId`; a refused write costs only them. */
|
|
24
|
+
export function writeRunSteps(
|
|
25
|
+
cacheDir: string,
|
|
26
|
+
runId: string,
|
|
27
|
+
steps: readonly RunStep[],
|
|
28
|
+
warn: (message: string) => void,
|
|
29
|
+
): void {
|
|
30
|
+
if (steps.length === 0) return
|
|
31
|
+
try {
|
|
32
|
+
const dir = dirOf(cacheDir)
|
|
33
|
+
mkdirSync(dir, { recursive: true })
|
|
34
|
+
const file = path.join(dir, `${runId}.json`)
|
|
35
|
+
const tmp = `${file}.${process.pid}.tmp`
|
|
36
|
+
writeFileSync(tmp, JSON.stringify(steps))
|
|
37
|
+
renameSync(tmp, file)
|
|
38
|
+
const names = readdirSync(dir)
|
|
39
|
+
.filter((n) => n.endsWith('.json'))
|
|
40
|
+
.sort()
|
|
41
|
+
for (const n of names.slice(0, -KEPT)) rmSync(path.join(dir, n), { force: true })
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (!isFsRefusal(err)) throw err
|
|
44
|
+
warn(`[vx] background steps not kept: ${err.message} — the verdict above stands`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The background steps `runId` kept; none when it had none or they are gone. */
|
|
49
|
+
export function runSteps(cacheDir: string, runId: string): RunStep[] {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(
|
|
52
|
+
readFileSync(path.join(dirOf(cacheDir), `${runId}.json`), 'utf8'),
|
|
53
|
+
) as RunStep[]
|
|
54
|
+
} catch {
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
}
|