@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/index.ts
CHANGED
|
@@ -66,8 +66,15 @@ export { run, planRun, prepareRun } from './orchestrator/index.js'
|
|
|
66
66
|
// (`@vzn/vx-history` does; a plugin package reaches core only
|
|
67
67
|
// through this façade, which is what put these here).
|
|
68
68
|
export { currentBranch } from './orchestrator/index.js'
|
|
69
|
-
export {
|
|
70
|
-
export type {
|
|
69
|
+
export { localHistory } from './history/index.js'
|
|
70
|
+
export type {
|
|
71
|
+
HistoryProvider,
|
|
72
|
+
HistoryTable,
|
|
73
|
+
LocalHistory,
|
|
74
|
+
LocalHistoryOptions,
|
|
75
|
+
TaskHistory,
|
|
76
|
+
TaskResources,
|
|
77
|
+
} from './history/index.js'
|
|
71
78
|
export type { PreparedRun } from './orchestrator/index.js'
|
|
72
79
|
// `RunOptions.affected`: what `--affected`'s diff changed, per project.
|
|
73
80
|
export type { AffectedChanges } from './workspace/index.js'
|
|
@@ -138,7 +145,7 @@ export {
|
|
|
138
145
|
taskNameProblem,
|
|
139
146
|
} from './workspace/index.js'
|
|
140
147
|
// Output components: a plugin draws its lines with core's, so they look alike.
|
|
141
|
-
export { ui } from './
|
|
148
|
+
export { ui } from './ui/index.js'
|
|
142
149
|
export type {
|
|
143
150
|
ApplyMigrationArgs,
|
|
144
151
|
MigrationFormat,
|
|
@@ -179,10 +186,12 @@ export type {
|
|
|
179
186
|
CacheFacts,
|
|
180
187
|
TaskArtifacts,
|
|
181
188
|
FingerprintChange,
|
|
189
|
+
FingerprintDigests,
|
|
182
190
|
FingerprintClaim,
|
|
183
191
|
FingerprintContext,
|
|
184
192
|
GraphHookContext,
|
|
185
193
|
KeyHookContext,
|
|
194
|
+
LockedFile,
|
|
186
195
|
PluginCommand,
|
|
187
196
|
ProjectHookContext,
|
|
188
197
|
ScheduleHookContext,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import os from 'node:os'
|
|
9
9
|
import path from 'node:path'
|
|
10
|
-
import { Cache, CACHE_VERSION, SCHEMA_VERSION } from '../cache/index.js'
|
|
10
|
+
import { indexDb, Cache, CACHE_VERSION, SCHEMA_VERSION } from '../cache/index.js'
|
|
11
11
|
import { PLUGIN_HOOKS, type WorkspaceConfig } from '../config.js'
|
|
12
12
|
import {
|
|
13
13
|
cgroupCpuQuota,
|
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
machineMemoryBytes,
|
|
20
20
|
machineParallelism,
|
|
21
21
|
treeBytes,
|
|
22
|
-
ui,
|
|
23
22
|
} from '../util/index.js'
|
|
23
|
+
import * as ui from '../ui/index.js'
|
|
24
24
|
import { vxPath } from '../handoff.js'
|
|
25
25
|
import { VERSION } from '../version.js'
|
|
26
26
|
import { probeSandbox, resetSandbox, untracedReason } from '../exec/index.js'
|
|
@@ -245,7 +245,7 @@ async function collectWorkspaceInfo(
|
|
|
245
245
|
indexStore = cache.storeDir
|
|
246
246
|
stats = cache.stats()
|
|
247
247
|
orphans = await cache.orphanStats()
|
|
248
|
-
flaky = flakyTasks(cache
|
|
248
|
+
flaky = flakyTasks(indexDb(cache))
|
|
249
249
|
} finally {
|
|
250
250
|
cache.close()
|
|
251
251
|
}
|
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
bracketLiteralHint,
|
|
73
73
|
markUnsaved,
|
|
74
74
|
saveMiss,
|
|
75
|
+
unsavedOutputBytes,
|
|
75
76
|
type OutputDirSnapshot,
|
|
76
77
|
type SaveFacts,
|
|
77
78
|
} from './miss-save.js'
|
|
@@ -1347,6 +1348,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1347
1348
|
let movedFails: string | undefined
|
|
1348
1349
|
let own: Awaited<ReturnType<typeof ownOutputs>>
|
|
1349
1350
|
let saved = false
|
|
1351
|
+
// An unsaved miss's outputs, as a save's are counted: its row's `outputs`.
|
|
1352
|
+
let unsavedBytes: number | undefined
|
|
1350
1353
|
if (
|
|
1351
1354
|
effectiveExitCode === 0 &&
|
|
1352
1355
|
willSave &&
|
|
@@ -1408,7 +1411,17 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1408
1411
|
// entry is in; without one, the save is taken as done.
|
|
1409
1412
|
if (args.deferredSaves !== undefined) args.deferredSaves.set(node.id, landed)
|
|
1410
1413
|
else saved = true
|
|
1411
|
-
} else if (cfgCacheable && !remoteOnly && !deferralRequested && args.noDependants
|
|
1414
|
+
} else if (cfgCacheable && !remoteOnly && !deferralRequested && args.noDependants === true) {
|
|
1415
|
+
// Nothing reads after it, so nothing to mark: only its row's `outputs`.
|
|
1416
|
+
if (effectiveExitCode === 0)
|
|
1417
|
+
unsavedBytes = await unsavedOutputBytes({
|
|
1418
|
+
node,
|
|
1419
|
+
workspaceRoot: args.workspaceRoot,
|
|
1420
|
+
nestedProjectDirs: args.nestedProjectDirs,
|
|
1421
|
+
outputs,
|
|
1422
|
+
wsOutputs,
|
|
1423
|
+
})
|
|
1424
|
+
} else if (cfgCacheable && !remoteOnly && !deferralRequested) {
|
|
1412
1425
|
// Ran here and saves nothing — failed, a read-only policy, a tainted
|
|
1413
1426
|
// upstream, a key that no longer held — yet it wrote what a save would
|
|
1414
1427
|
// have marked, and may have rewritten an input the re-check before a
|
|
@@ -1422,7 +1435,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1422
1435
|
if (moved !== undefined) {
|
|
1423
1436
|
forgetUndeclaredWrites(args, wsOutputs.length > 0 ? 'workspace' : 'project')
|
|
1424
1437
|
} else {
|
|
1425
|
-
await markUnsaved({
|
|
1438
|
+
unsavedBytes = await markUnsaved({
|
|
1426
1439
|
node,
|
|
1427
1440
|
workspaceRoot: args.workspaceRoot,
|
|
1428
1441
|
nestedProjectDirs: args.nestedProjectDirs,
|
|
@@ -1572,6 +1585,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1572
1585
|
...(unkeyed ? { unkeyed: true as const } : {}),
|
|
1573
1586
|
...(keylessBy !== undefined ? { keylessBy } : {}),
|
|
1574
1587
|
...(saved ? { saved: true as const } : {}),
|
|
1588
|
+
...(unsavedBytes !== undefined ? { outputBytes: unsavedBytes } : {}),
|
|
1575
1589
|
...(effectiveExitCode !== 0 && output !== undefined
|
|
1576
1590
|
? { failedOutput: failedOutputLog(output, willSave || args.keepRun !== undefined, storeMask) }
|
|
1577
1591
|
: {}),
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
// it is never remembered.
|
|
13
13
|
|
|
14
14
|
import type { FoldMemo } from '../cache/index.js'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
buildTaskGraph,
|
|
17
|
+
computeReverseDepCount,
|
|
18
|
+
declaredTask,
|
|
19
|
+
type TaskNode,
|
|
20
|
+
} from '../graph/index.js'
|
|
16
21
|
import { xxh3hex } from '../util/index.js'
|
|
17
22
|
import type { ProjectMeta } from '../workspace/index.js'
|
|
18
23
|
import { codeIdentity } from './run-keys.js'
|
|
@@ -138,3 +143,47 @@ function rebuilt(
|
|
|
138
143
|
}
|
|
139
144
|
return nodes
|
|
140
145
|
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The run's dependents count, the scheduler's baseline and every row's
|
|
149
|
+
* `blocks`, read back with the graph it was counted on: the closure over a
|
|
150
|
+
* 9,603-task graph (11k nodes) was ~100 ms of every warm run, the same each
|
|
151
|
+
* time (2026-10-29). `token` names the graph's edges (`memoGraph`, undefined
|
|
152
|
+
* once anything edited them), the node ids which of them this run kept
|
|
153
|
+
* (`--affected` prunes under the same token).
|
|
154
|
+
*/
|
|
155
|
+
export function graphCounts(
|
|
156
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
157
|
+
folds: FoldMemo | undefined,
|
|
158
|
+
token: string | undefined,
|
|
159
|
+
): Map<string, number> {
|
|
160
|
+
if (folds === undefined || token === undefined) return computeReverseDepCount(nodes)
|
|
161
|
+
// Not `counts`: those rows held a second count the rows no longer show.
|
|
162
|
+
const id = `\0dependents\0${token}`
|
|
163
|
+
const probe = xxh3hex([...nodes.keys()].join('\0'))
|
|
164
|
+
const row = folds.get(id, probe)
|
|
165
|
+
if (row !== undefined) {
|
|
166
|
+
const back = countsOf(row, nodes)
|
|
167
|
+
if (back !== undefined) return back
|
|
168
|
+
}
|
|
169
|
+
const counts = computeReverseDepCount(nodes)
|
|
170
|
+
folds.set(id, probe, JSON.stringify([...nodes.keys()].map((k) => counts.get(k)!)))
|
|
171
|
+
return counts
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function countsOf(
|
|
175
|
+
row: string,
|
|
176
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
177
|
+
): Map<string, number> | undefined {
|
|
178
|
+
let all: number[]
|
|
179
|
+
try {
|
|
180
|
+
all = JSON.parse(row) as number[]
|
|
181
|
+
} catch {
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
if (all?.length !== nodes.size) return undefined
|
|
185
|
+
const counts = new Map<string, number>()
|
|
186
|
+
let i = 0
|
|
187
|
+
for (const id of nodes.keys()) counts.set(id, all[i++]!)
|
|
188
|
+
return counts
|
|
189
|
+
}
|
|
@@ -29,6 +29,12 @@ export async function applyGraphStage(
|
|
|
29
29
|
// keyed and cleaned its outputs there: past the hard project boundary.
|
|
30
30
|
const dirs = new Map(args.projectMetas.map((m) => [m.name, path.resolve(m.dir)]))
|
|
31
31
|
const shown = (dir: string): string => relPosix(args.workspaceRoot, dir) || '.'
|
|
32
|
+
// The lock shares a task config two tasks declare alike: an edit in place
|
|
33
|
+
// works on this node's own copy.
|
|
34
|
+
if (plugins.some((p) => p.graph !== undefined)) {
|
|
35
|
+
for (const n of nodes.values())
|
|
36
|
+
if (!isDefaultBuild(n.config)) n.config = structuredClone(n.config)
|
|
37
|
+
}
|
|
32
38
|
await applyGraphHooks(
|
|
33
39
|
plugins,
|
|
34
40
|
nodes,
|
|
@@ -458,6 +458,9 @@ function hitOutcome(restore: RestoreHitArgs, restored: boolean): TaskOutcome {
|
|
|
458
458
|
...(hit.cpuMs !== undefined ? { storedCpuMs: hit.cpuMs } : {}),
|
|
459
459
|
...(hit.peakRssBytes !== undefined ? { storedPeakRssBytes: hit.peakRssBytes } : {}),
|
|
460
460
|
...(hit.sizeBytes !== undefined ? { artifactBytes: hit.sizeBytes } : {}),
|
|
461
|
+
...(hit.outputRows !== undefined
|
|
462
|
+
? { outputBytes: hit.outputRows.reduce((n, r) => n + r.size, 0) }
|
|
463
|
+
: {}),
|
|
461
464
|
...(hit.fetchMs !== undefined ? { fetchMs: hit.fetchMs } : {}),
|
|
462
465
|
hash,
|
|
463
466
|
restored: restored || hit.source === 'remote',
|
|
@@ -60,6 +60,7 @@ export {
|
|
|
60
60
|
type CommandContext,
|
|
61
61
|
type GraphHookContext,
|
|
62
62
|
type KeyHookContext,
|
|
63
|
+
type LockedFile,
|
|
63
64
|
type PluginCommand,
|
|
64
65
|
type ProjectHookContext,
|
|
65
66
|
type ScheduleHookContext,
|
|
@@ -78,6 +79,7 @@ export {
|
|
|
78
79
|
type RunReport,
|
|
79
80
|
type RunReportTask,
|
|
80
81
|
type FingerprintChange,
|
|
82
|
+
type FingerprintDigests,
|
|
81
83
|
type FingerprintClaim,
|
|
82
84
|
type FingerprintContext,
|
|
83
85
|
type VxPlugin,
|
|
@@ -95,6 +97,7 @@ export {
|
|
|
95
97
|
applyScheduleHooks,
|
|
96
98
|
claimedAffected,
|
|
97
99
|
claimedExtraFiles,
|
|
100
|
+
claimedDigests,
|
|
98
101
|
fingerprintClaims,
|
|
99
102
|
hasHook,
|
|
100
103
|
REMOTE_CACHE_METHODS,
|
|
@@ -146,7 +149,6 @@ export {
|
|
|
146
149
|
type TaskLog,
|
|
147
150
|
taskLog,
|
|
148
151
|
} from './run-failures.js'
|
|
149
|
-
export { type RunStep, runSteps, runWarnings } from './run-steps.js'
|
|
150
152
|
export { resolveRunId, shortRunId } from './run-id.js'
|
|
151
153
|
export { collectInfo, type CollectInfoOptions, type InfoFacts } from './doctor.js'
|
|
152
154
|
export { localFloor, type LocalFloor } from './local-floor.js'
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The live list's `cpu` and `mem`: each running task's process tree's
|
|
2
|
+
// average cores since it started and its peak memory so far, the values
|
|
3
|
+
// its results row shows once it ends (2026-10-29: the live row showed the
|
|
4
|
+
// last half second's, and a task's row changed meaning as it finished).
|
|
5
|
+
// Looked at on a timer only once the list asks for a row, so a run with
|
|
6
|
+
// no live list (a pipe, CI) registers pids and reads nothing.
|
|
7
|
+
// The look is async and off the dispatch path: a slow one skips a tick.
|
|
8
|
+
|
|
9
|
+
import { sampleTrees, type TreeUsage } from '../exec/index.js'
|
|
10
|
+
import type { LiveUsage } from '../report/index.js'
|
|
11
|
+
|
|
12
|
+
/** How often a tracked tree is looked at while the live list shows. */
|
|
13
|
+
const LOOK_MS = 500
|
|
14
|
+
|
|
15
|
+
type Every = (ms: number, fn: () => Promise<void>) => () => void
|
|
16
|
+
|
|
17
|
+
const everyInterval: Every = (ms, fn) => {
|
|
18
|
+
const timer = setInterval(() => void fn(), ms)
|
|
19
|
+
timer.unref()
|
|
20
|
+
return () => clearInterval(timer)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Tracked {
|
|
24
|
+
pid: number
|
|
25
|
+
startedAt: number
|
|
26
|
+
/** The most CPU time the tree has shown: a descendant that exits takes its share out of the sum. */
|
|
27
|
+
cpuMs: number
|
|
28
|
+
peakRssBytes: number
|
|
29
|
+
now?: LiveUsage
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createLiveUsage(
|
|
33
|
+
opts: {
|
|
34
|
+
every?: Every
|
|
35
|
+
now?: () => number
|
|
36
|
+
sample?: (pids: number[]) => Promise<Map<number, TreeUsage>>
|
|
37
|
+
} = {},
|
|
38
|
+
): {
|
|
39
|
+
track(taskId: string, pid: number): () => void
|
|
40
|
+
usage(taskId: string): LiveUsage | undefined
|
|
41
|
+
} {
|
|
42
|
+
const every = opts.every ?? everyInterval
|
|
43
|
+
const now = opts.now ?? Date.now
|
|
44
|
+
const sample = opts.sample ?? sampleTrees
|
|
45
|
+
const tracked = new Map<string, Tracked>()
|
|
46
|
+
let asked = false
|
|
47
|
+
let looking = false
|
|
48
|
+
let stop: (() => void) | undefined
|
|
49
|
+
|
|
50
|
+
const look = async (): Promise<void> => {
|
|
51
|
+
if (looking) return
|
|
52
|
+
looking = true
|
|
53
|
+
try {
|
|
54
|
+
const entries = [...tracked]
|
|
55
|
+
const usage = await sample(entries.map(([, t]) => t.pid))
|
|
56
|
+
const ts = now()
|
|
57
|
+
for (const [id, t] of entries) {
|
|
58
|
+
const u = usage.get(t.pid)
|
|
59
|
+
if (u === undefined || tracked.get(id) !== t) continue
|
|
60
|
+
t.cpuMs = Math.max(t.cpuMs, u.cpuMs)
|
|
61
|
+
t.peakRssBytes = Math.max(t.peakRssBytes, u.rssBytes)
|
|
62
|
+
if (ts > t.startedAt) {
|
|
63
|
+
const cores = t.cpuMs / (ts - t.startedAt)
|
|
64
|
+
t.now = { cores: Math.round(cores * 100) / 100, rssBytes: t.peakRssBytes }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// A failed look is a missing point, never a broken run.
|
|
69
|
+
} finally {
|
|
70
|
+
looking = false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const start = (): void => {
|
|
74
|
+
if (asked && stop === undefined && tracked.size > 0) stop = every(LOOK_MS, look)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
track(taskId, pid) {
|
|
79
|
+
const t: Tracked = { pid, startedAt: now(), cpuMs: 0, peakRssBytes: 0 }
|
|
80
|
+
tracked.set(taskId, t)
|
|
81
|
+
start()
|
|
82
|
+
return () => {
|
|
83
|
+
if (tracked.get(taskId) === t) tracked.delete(taskId)
|
|
84
|
+
if (tracked.size === 0 && stop !== undefined) {
|
|
85
|
+
stop()
|
|
86
|
+
stop = undefined
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
usage(taskId) {
|
|
91
|
+
if (!asked) {
|
|
92
|
+
asked = true
|
|
93
|
+
start()
|
|
94
|
+
}
|
|
95
|
+
return tracked.get(taskId)?.now
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// A run keys a claimed file from what `vx lock` recorded of it in
|
|
2
|
+
// `.vx/dependencies.lock.json`, as it takes configs from the config lock:
|
|
3
|
+
// the digests are read, never derived, so no run parses a lockfile for
|
|
4
|
+
// its key. The record holds only while the file's bytes hash to what was
|
|
5
|
+
// recorded: one that moved (an install) refuses the run before any key is
|
|
6
|
+
// folded, naming the file and `vx lock`. A present file with no record,
|
|
7
|
+
// or no lock at all, refuses when a claimant asks for it: an absent record
|
|
8
|
+
// is never read as "no dependencies". The other files the digests read
|
|
9
|
+
// (`extras`: a `link:` directory, a patch) are handed to the claimant with
|
|
10
|
+
// their recorded hashes: a linked directory is edited with no install, so
|
|
11
|
+
// one that moved is the claimant's to digest anew, not a refusal.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
DEPENDENCIES_LOCK_NAME,
|
|
15
|
+
type DependenciesLock,
|
|
16
|
+
lockKey,
|
|
17
|
+
parseDependenciesLock,
|
|
18
|
+
recordedDigests,
|
|
19
|
+
} from '../workspace/index.js'
|
|
20
|
+
import { UserError, xxh3hex } from '../util/index.js'
|
|
21
|
+
import type { KeyHookContext, LockedFile } from './plugin.js'
|
|
22
|
+
|
|
23
|
+
const refused = (message: string): UserError => new UserError(message, 'VX_E_LOCK')
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `KeyHookContext.locked` for one run, over the claimed files' bytes as
|
|
27
|
+
* the run read them (`readClaimedFiles`; absent ones are missing) and the
|
|
28
|
+
* lock's bytes (null when there is none). Each recorded claimed file is
|
|
29
|
+
* checked first: one whose bytes moved, or that is gone, throws here.
|
|
30
|
+
*/
|
|
31
|
+
export function lockedFiles(
|
|
32
|
+
workspaceRoot: string,
|
|
33
|
+
claimed: readonly string[],
|
|
34
|
+
bytes: ReadonlyMap<string, Uint8Array>,
|
|
35
|
+
lockBytes: Uint8Array | null,
|
|
36
|
+
): KeyHookContext['locked'] {
|
|
37
|
+
const lock: DependenciesLock | null = parseDependenciesLock(lockBytes)
|
|
38
|
+
const records = new Map<string, LockedFile>()
|
|
39
|
+
for (const file of claimed) {
|
|
40
|
+
const rec = lock === null ? undefined : recordedDigests(lock, file)
|
|
41
|
+
if (rec === undefined) continue
|
|
42
|
+
const now = bytes.get(file)
|
|
43
|
+
if (now === undefined || xxh3hex(now) !== rec.hash) {
|
|
44
|
+
throw refused(`${file} changed since \`vx lock\`: run \`vx lock\``)
|
|
45
|
+
}
|
|
46
|
+
records.set(file, {
|
|
47
|
+
extras: rec.extras,
|
|
48
|
+
digest(projectDir) {
|
|
49
|
+
const dir = lockKey(workspaceRoot, projectDir)
|
|
50
|
+
if (!rec.has(dir))
|
|
51
|
+
throw refused(`${dir} is not in ${DEPENDENCIES_LOCK_NAME}: run \`vx lock\``)
|
|
52
|
+
return rec.of(dir)
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
return async (file) => {
|
|
57
|
+
const rec = records.get(file)
|
|
58
|
+
if (rec !== undefined || !bytes.has(file)) return rec
|
|
59
|
+
if (lockBytes === null) throw refused(`no ${DEPENDENCIES_LOCK_NAME}: run \`vx lock\``)
|
|
60
|
+
if (lock === null) throw refused(`${DEPENDENCIES_LOCK_NAME} is invalid, run \`vx lock\``)
|
|
61
|
+
throw refused(`${file} is not in ${DEPENDENCIES_LOCK_NAME}: run \`vx lock\``)
|
|
62
|
+
}
|
|
63
|
+
}
|