@vzn/vx 0.0.652 → 0.0.653
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 +2 -2
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +1 -0
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +25 -3
- package/src/cache/archive.ts +196 -31
- package/src/cache/cache.ts +863 -225
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +22 -5
- package/src/cache/index.ts +10 -1
- package/src/cache/inputs.ts +573 -152
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +23 -10
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +50 -10
- package/src/cache/schema.ts +42 -9
- package/src/cli/cache.ts +147 -34
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +25 -2
- package/src/cli/index.ts +38 -7
- package/src/cli/info.ts +4 -3
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +184 -41
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +63 -18
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +72 -8
- package/src/cli/watch-set.ts +6 -4
- package/src/cli/watch.ts +179 -25
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +9 -0
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +76 -15
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-runtime.ts +134 -37
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +173 -38
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +327 -141
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +15 -5
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +189 -69
- package/src/index.ts +2 -1
- package/src/orchestrator/admission.ts +13 -9
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +41 -12
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/excluded-keys.ts +14 -2
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +564 -123
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +136 -87
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +25 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +33 -53
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +31 -17
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +81 -15
- package/src/orchestrator/plan.ts +37 -15
- package/src/orchestrator/plugin-host.ts +232 -108
- package/src/orchestrator/plugin.ts +53 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +43 -14
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +12 -10
- package/src/orchestrator/run-report.ts +2 -1
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +443 -107
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +286 -59
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +20 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +17 -9
- package/src/orchestrator/task-artifacts.ts +15 -2
- package/src/orchestrator/task-hash.ts +187 -30
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +80 -12
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +20 -3
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +165 -157
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +107 -30
- package/src/report/plain-output.ts +13 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +57 -14
- package/src/settings.ts +42 -6
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +17 -3
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +50 -6
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +16 -0
- package/src/util/size.ts +12 -2
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +126 -25
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +10 -1
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +46 -25
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// workers, which run tasks another machine placed. A run arms the sandbox
|
|
3
3
|
// runtime before it builds a sandboxed request; a worker is no run, so it
|
|
4
4
|
// arms here. It starts a request's servers (`ExecuteRequest.servers`) once
|
|
5
|
-
// each, before the task, each inside its own sandbox, and stops
|
|
6
|
-
// it closes.
|
|
5
|
+
// each, before the task, each inside its own sandbox, and stops each when
|
|
6
|
+
// the run says no task needs it, or when it closes.
|
|
7
7
|
|
|
8
8
|
import type { ExecConfig } from '../config.js'
|
|
9
9
|
import {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type ServerSpec,
|
|
19
19
|
wrapSandboxedCommand,
|
|
20
20
|
} from '../exec/index.js'
|
|
21
|
+
import { stopServer } from './persistent.js'
|
|
21
22
|
import { placeholderSweeper, prepareSandbox, sandboxRequestFor } from './sandbox-request.js'
|
|
22
23
|
import { forwardedSignal, terminateChildren } from './signals.js'
|
|
23
24
|
|
|
@@ -28,6 +29,8 @@ export interface LocalFloor {
|
|
|
28
29
|
execute(req: ExecuteRequest): Promise<ExecuteResult>
|
|
29
30
|
/** Arms the sandbox runtime for `sandboxes` from the next sandboxed task on; its servers keep running. */
|
|
30
31
|
rearm(sandboxes: readonly NonNullable<ExecConfig['sandbox']>[]): Promise<void>
|
|
32
|
+
/** Stops server `taskId` if this started it: no task left needs it. A later task that needs it starts it again. */
|
|
33
|
+
stop(taskId: string): Promise<void>
|
|
31
34
|
/** Stops the servers this started and the sandbox runtime this armed. */
|
|
32
35
|
close(): Promise<void>
|
|
33
36
|
}
|
|
@@ -38,10 +41,13 @@ export interface LocalFloor {
|
|
|
38
41
|
* unix-socket filter are one per process, set from their union before the
|
|
39
42
|
* first sandboxed task (`prepareSandbox`). A request that carries a
|
|
40
43
|
* `signal` but no `liveChildren` has its children stopped here when it
|
|
41
|
-
* aborts, the way a run's stop ends its own.
|
|
44
|
+
* aborts, the way a run's stop ends its own. `onServer` hears each
|
|
45
|
+
* server's pid as it spawns, and `undefined` as `stop` ends it: a worker
|
|
46
|
+
* meters what its servers use.
|
|
42
47
|
*/
|
|
43
48
|
export function localFloor(
|
|
44
49
|
sandboxes: readonly NonNullable<ExecConfig['sandbox']>[] = [],
|
|
50
|
+
onServer?: (taskId: string, pid: number | undefined) => void,
|
|
45
51
|
): LocalFloor {
|
|
46
52
|
const local = localExecutor()
|
|
47
53
|
const prepare = (list: typeof sandboxes) =>
|
|
@@ -103,6 +109,7 @@ export function localFloor(
|
|
|
103
109
|
if (spawn.child === undefined) void gone()
|
|
104
110
|
else {
|
|
105
111
|
children.push(spawn.child)
|
|
112
|
+
onServer?.(spec.taskId, spawn.child.pid)
|
|
106
113
|
void spawn.child.exited.then(gone, gone)
|
|
107
114
|
}
|
|
108
115
|
try {
|
|
@@ -190,6 +197,17 @@ export function localFloor(
|
|
|
190
197
|
if (armer?.armed === true) await resetSandbox()
|
|
191
198
|
armer = prepare(list)
|
|
192
199
|
},
|
|
200
|
+
async stop(taskId) {
|
|
201
|
+
const up = servers.get(taskId)
|
|
202
|
+
if (up === undefined) return
|
|
203
|
+
servers.delete(taskId)
|
|
204
|
+
const child = await up.catch(() => undefined)
|
|
205
|
+
if (child === undefined) return
|
|
206
|
+
const i = children.indexOf(child)
|
|
207
|
+
if (i >= 0) children.splice(i, 1)
|
|
208
|
+
onServer?.(taskId, undefined)
|
|
209
|
+
await stopServer(child)
|
|
210
|
+
},
|
|
193
211
|
async close() {
|
|
194
212
|
if (children.length > 0) await terminateChildren(() => children)
|
|
195
213
|
if (armer?.armed === true) await resetSandbox()
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// `vx lock` adds them to the memo.
|
|
22
22
|
|
|
23
23
|
import path from 'node:path'
|
|
24
|
-
import { stat } from 'node:fs/promises'
|
|
24
|
+
import { readdir, realpath, stat } from 'node:fs/promises'
|
|
25
25
|
import { type LockfileMemo, readLockfileMemo, writeLockfileMemo } from '../cache/index.js'
|
|
26
26
|
import { relPosix, UserError, xxh3, xxh3hex } from '../util/index.js'
|
|
27
27
|
import type { TaskNode } from '../graph/index.js'
|
|
@@ -88,6 +88,11 @@ export interface LockfileImporter {
|
|
|
88
88
|
readonly deps: Readonly<Record<string, string>>
|
|
89
89
|
/** Its peers that resolve to an importer, each with no installed entry of the same name. */
|
|
90
90
|
readonly peers: Readonly<Record<string, string>>
|
|
91
|
+
/**
|
|
92
|
+
* The importers it reaches only through installed packages, sorted: a
|
|
93
|
+
* registry package's dependency the install resolved to a workspace.
|
|
94
|
+
*/
|
|
95
|
+
readonly transitive?: readonly string[]
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
/** What a lockfile plugin spreads into `definePlugin` (`fingerprint`, `key`), and `importers` for its own stages. */
|
|
@@ -152,13 +157,34 @@ function unreadable(rel: string, err: unknown): UserError {
|
|
|
152
157
|
* Every file under `dir` but its `node_modules` and `.git`, as `rel\0hash`
|
|
153
158
|
* lines in sorted order. Links are followed: a linked file is what a task
|
|
154
159
|
* reads, and the scan skipped it, so an edit to its target moved no key.
|
|
160
|
+
* The walk never enters `node_modules` or `.git`: a glob of `**` that
|
|
161
|
+
* dropped them after listed every file under them (30k files there: a warm
|
|
162
|
+
* run 307 → 183 ms, min of 10). A linked directory that is one of its own
|
|
163
|
+
* ancestors is not entered again.
|
|
155
164
|
*/
|
|
156
165
|
async function hashDir(dir: string): Promise<string> {
|
|
157
166
|
const rels: string[] = []
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
const walk = async (rel: string, real: string, ancestors: readonly string[]): Promise<void> => {
|
|
168
|
+
const entries = await readdir(path.join(dir, rel), { withFileTypes: true })
|
|
169
|
+
await Promise.all(
|
|
170
|
+
entries.map(async (e) => {
|
|
171
|
+
if (e.name === 'node_modules' || e.name === '.git') return
|
|
172
|
+
const child = rel === '' ? e.name : `${rel}/${e.name}`
|
|
173
|
+
if (e.isFile()) return void rels.push(child)
|
|
174
|
+
if (e.isDirectory()) return walk(child, path.join(real, e.name), [...ancestors, real])
|
|
175
|
+
// A link, or an entry the filesystem gave no type: what it names decides.
|
|
176
|
+
const abs = path.join(dir, child)
|
|
177
|
+
const st = await stat(abs).catch(() => undefined)
|
|
178
|
+
if (st?.isFile()) rels.push(child)
|
|
179
|
+
else if (st?.isDirectory()) {
|
|
180
|
+
const target = await realpath(abs)
|
|
181
|
+
if (target !== real && !ancestors.includes(target))
|
|
182
|
+
await walk(child, target, [...ancestors, real])
|
|
183
|
+
}
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
161
186
|
}
|
|
187
|
+
await walk('', await realpath(dir), [])
|
|
162
188
|
rels.sort()
|
|
163
189
|
const lines = await Promise.all(
|
|
164
190
|
rels.map(async (rel) => `${rel}\0${xxh3hex(await Bun.file(path.join(dir, rel)).bytes())}`),
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// a later run replays. Moved out of execute-task.ts 2026-09-09 as pure
|
|
8
8
|
// code motion.
|
|
9
9
|
|
|
10
|
-
import { existsSync,
|
|
10
|
+
import { existsSync, statSync } from 'node:fs'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import {
|
|
13
13
|
type CacheLayer,
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
relPosix,
|
|
27
27
|
span,
|
|
28
28
|
splitNegations,
|
|
29
|
-
staticPrefix,
|
|
30
29
|
taskGlob,
|
|
31
30
|
} from '../util/index.js'
|
|
32
31
|
import { type Logger, warnOf } from '../report/index.js'
|
|
@@ -137,7 +136,7 @@ export interface SaveMissArgs {
|
|
|
137
136
|
* (a same-project downstream task reads both), and the pack + write +
|
|
138
137
|
* index go to the run's save lane (save-lane.ts).
|
|
139
138
|
*/
|
|
140
|
-
deferSave?: ((save: () => Promise<void
|
|
139
|
+
deferSave?: ((save: () => Promise<void>, taskId: string) => Promise<boolean>) | undefined
|
|
141
140
|
/** Time the save and size its artifact, for a telemetry sink: one stat. */
|
|
142
141
|
measure?: boolean | undefined
|
|
143
142
|
}
|
|
@@ -167,6 +166,11 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
167
166
|
projectDir: node.projectDir,
|
|
168
167
|
outputs: a.outputs,
|
|
169
168
|
nestedProjectDirs: a.nestedProjectDirs,
|
|
169
|
+
inputs: {
|
|
170
|
+
projectDir: node.projectDir,
|
|
171
|
+
workspaceRoot: a.workspaceRoot,
|
|
172
|
+
declared: node.config.inputs,
|
|
173
|
+
},
|
|
170
174
|
special: true,
|
|
171
175
|
})
|
|
172
176
|
const outputFiles = tree.files
|
|
@@ -176,6 +180,11 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
176
180
|
: await resolveWorkspaceOutputTree({
|
|
177
181
|
workspaceRoot: a.workspaceRoot,
|
|
178
182
|
outputs: a.wsOutputs,
|
|
183
|
+
inputs: {
|
|
184
|
+
projectDir: node.projectDir,
|
|
185
|
+
workspaceRoot: a.workspaceRoot,
|
|
186
|
+
declared: node.config.inputs,
|
|
187
|
+
},
|
|
179
188
|
special: true,
|
|
180
189
|
})
|
|
181
190
|
const wsOutputFiles = wsTree.files
|
|
@@ -213,17 +222,12 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
213
222
|
// `outputs` is not a write grant; `exec.sandbox.allow.write` is.
|
|
214
223
|
const sandboxed = node.config.exec?.sandbox !== undefined
|
|
215
224
|
const grantsWrite = (node.config.exec?.sandbox?.allow?.write?.length ?? 0) > 0
|
|
216
|
-
// A `workspaceFiles` directory linked out of the workspace drops every
|
|
217
|
-
// file (M-65); a project one is refused by the resolver (X-88).
|
|
218
|
-
const linkedOut = outputDirLinkedOut(a.workspaceRoot, a.wsOutputs)
|
|
219
225
|
warnOf(log)(
|
|
220
226
|
`[vx] ${node.id}: outputs matched no files (${[...a.outputs, ...a.wsOutputs].join(', ')}) — ` +
|
|
221
227
|
`an empty artifact is saved; a later hit restores nothing` +
|
|
222
|
-
(
|
|
223
|
-
? ` —
|
|
224
|
-
:
|
|
225
|
-
? ` — the task is sandboxed and declares no exec.sandbox.allow.write, so its writes never reached disk`
|
|
226
|
-
: '') +
|
|
228
|
+
(sandboxed && !grantsWrite
|
|
229
|
+
? ` — the task is sandboxed and declares no exec.sandbox.allow.write, so its writes never reached disk`
|
|
230
|
+
: '') +
|
|
227
231
|
bracketLiteralHint([
|
|
228
232
|
...a.outputs.map((g) => [node.projectDir, g] as const),
|
|
229
233
|
...a.wsOutputs.map((g) => [a.workspaceRoot, g] as const),
|
|
@@ -231,6 +235,7 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
231
235
|
)
|
|
232
236
|
}
|
|
233
237
|
const facts: SaveFacts = {}
|
|
238
|
+
let kept = false
|
|
234
239
|
const save = async (): Promise<void> => {
|
|
235
240
|
const saveStart = a.measure === true ? performance.now() : 0
|
|
236
241
|
const endSave = span('miss: save')
|
|
@@ -262,6 +267,9 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
262
267
|
endSave()
|
|
263
268
|
const bytes = cache.outputBytes?.(a.hash)
|
|
264
269
|
if (bytes !== undefined) facts.outputBytes = bytes
|
|
270
|
+
// A `--force` run that found the key stored kept that entry: the
|
|
271
|
+
// footer's `↑` counts saves, and this was none.
|
|
272
|
+
if (cache.kept?.(a.hash) === true) kept = true
|
|
265
273
|
if (a.measure === true) {
|
|
266
274
|
facts.saveMs = Math.round(performance.now() - saveStart)
|
|
267
275
|
// No local artifact (local writes off): no size to say.
|
|
@@ -300,11 +308,13 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
300
308
|
// the entry before the outcome.
|
|
301
309
|
if (a.deferSave !== undefined) {
|
|
302
310
|
return {
|
|
303
|
-
landed: a
|
|
311
|
+
landed: a
|
|
312
|
+
.deferSave(save, node.id)
|
|
313
|
+
.then((stored) => (stored && !kept ? { ...facts, saved: true } : facts)),
|
|
304
314
|
}
|
|
305
315
|
}
|
|
306
316
|
await save()
|
|
307
|
-
return { landed: Promise.resolve({ ...facts, saved: true }) }
|
|
317
|
+
return { landed: Promise.resolve(kept ? facts : { ...facts, saved: true }) }
|
|
308
318
|
}
|
|
309
319
|
|
|
310
320
|
/** What `markUnsaved` reads of a miss: where its declared outputs are. */
|
|
@@ -342,10 +352,20 @@ async function resolveUnsaved(
|
|
|
342
352
|
projectDir: a.node.projectDir,
|
|
343
353
|
outputs: a.outputs,
|
|
344
354
|
nestedProjectDirs: a.nestedProjectDirs,
|
|
355
|
+
inputs: {
|
|
356
|
+
projectDir: a.node.projectDir,
|
|
357
|
+
workspaceRoot: a.workspaceRoot,
|
|
358
|
+
declared: a.node.config.inputs,
|
|
359
|
+
},
|
|
345
360
|
})
|
|
346
361
|
const wsOutputFiles = await resolveWorkspaceOutputs({
|
|
347
362
|
workspaceRoot: a.workspaceRoot,
|
|
348
363
|
outputs: a.wsOutputs,
|
|
364
|
+
inputs: {
|
|
365
|
+
projectDir: a.node.projectDir,
|
|
366
|
+
workspaceRoot: a.workspaceRoot,
|
|
367
|
+
declared: a.node.config.inputs,
|
|
368
|
+
},
|
|
349
369
|
})
|
|
350
370
|
endResolve()
|
|
351
371
|
return { outputFiles, wsOutputFiles }
|
|
@@ -399,46 +419,6 @@ function markWritten(
|
|
|
399
419
|
}
|
|
400
420
|
}
|
|
401
421
|
|
|
402
|
-
/**
|
|
403
|
-
* The first `workspaceFiles` directory that is a symlink resolving outside
|
|
404
|
-
* the workspace root: every file under it is dropped as outside, and the
|
|
405
|
-
* empty-artifact warning blamed the glob (M-65).
|
|
406
|
-
*/
|
|
407
|
-
function outputDirLinkedOut(
|
|
408
|
-
baseDir: string,
|
|
409
|
-
outputs: readonly string[],
|
|
410
|
-
): { dir: string; target: string; base: string } | undefined {
|
|
411
|
-
const realProject = realpathOrNull(baseDir) ?? baseDir
|
|
412
|
-
// `dist` names the tree `dist/**`: the same reading the resolver gives.
|
|
413
|
-
for (const pattern of asTrees(outputs.filter((o) => !o.startsWith('!')))) {
|
|
414
|
-
const literal = staticPrefix(pattern)
|
|
415
|
-
.split('/')
|
|
416
|
-
.filter((p) => p !== '' && p !== '.')
|
|
417
|
-
for (let i = 1; i <= literal.length; i++) {
|
|
418
|
-
const dir = literal.slice(0, i).join('/')
|
|
419
|
-
const abs = path.join(baseDir, dir)
|
|
420
|
-
try {
|
|
421
|
-
if (!lstatSync(abs).isSymbolicLink()) continue
|
|
422
|
-
} catch {
|
|
423
|
-
break
|
|
424
|
-
}
|
|
425
|
-
const target = realpathOrNull(abs)
|
|
426
|
-
if (target !== null && target !== realProject && !target.startsWith(realProject + path.sep)) {
|
|
427
|
-
return { dir, target, base: 'the workspace' }
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return undefined
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function realpathOrNull(p: string): string | null {
|
|
435
|
-
try {
|
|
436
|
-
return realpathSync(p)
|
|
437
|
-
} catch {
|
|
438
|
-
return null
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
422
|
/**
|
|
443
423
|
* The clause a "matched no files" warning gains when a glob holds an
|
|
444
424
|
* unescaped `[` and, read as a literal path, names something on disk:
|
|
@@ -8,6 +8,7 @@ import type { TaskOutcome } from '../graph/index.js'
|
|
|
8
8
|
import type { AffectedReason } from './affected-tasks.js'
|
|
9
9
|
import type { EventBus, Logger } from '../report/index.js'
|
|
10
10
|
import type { TelemetrySink } from './telemetry.js'
|
|
11
|
+
import type { CommandWindows } from './command-windows.js'
|
|
11
12
|
import type { AffectedChanges, ProjectEntry, ProjectMeta } from '../workspace/index.js'
|
|
12
13
|
import type { RunSettings } from '../config.js'
|
|
13
14
|
|
|
@@ -220,6 +221,11 @@ export interface RunOptions {
|
|
|
220
221
|
* is replaced only when the next cycle starts.
|
|
221
222
|
*/
|
|
222
223
|
holdPersistent?: boolean
|
|
224
|
+
/**
|
|
225
|
+
* The run is a `vx watch` cycle: an input edited while a task ran starts
|
|
226
|
+
* the next cycle, and the line withholding the task's save says so.
|
|
227
|
+
*/
|
|
228
|
+
watch?: boolean
|
|
223
229
|
/**
|
|
224
230
|
* The servers an earlier `holdPersistent` run handed back. Each one still
|
|
225
231
|
* up whose task this run would start again, with the same resolved
|
|
@@ -232,6 +238,13 @@ export interface RunOptions {
|
|
|
232
238
|
* not take.
|
|
233
239
|
*/
|
|
234
240
|
keep?: HeldPersistent
|
|
241
|
+
/**
|
|
242
|
+
* Where the run records when each command ran (and each restore or deferred fetch)
|
|
243
|
+
* and where it may write (command-windows.ts). The watch loop passes its
|
|
244
|
+
* own, so a write that lands during the cycle and no command of it may
|
|
245
|
+
* have made is the user's edit, whatever the file settles to.
|
|
246
|
+
*/
|
|
247
|
+
commandWindows?: CommandWindows
|
|
235
248
|
/**
|
|
236
249
|
* Print the per-task table (`--verbosity 1`) just above the footer: the
|
|
237
250
|
* footer is the run's last word, so the caller cannot print it after.
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// and core's renderer is the run's, byte for byte.
|
|
4
4
|
|
|
5
5
|
import { terminalSubscriber, type RunEventSubscriber } from '../report/index.js'
|
|
6
|
-
import type { Logger } from '../report/index.js'
|
|
6
|
+
import type { Logger, RunEvent } from '../report/index.js'
|
|
7
7
|
import { settleWithin, teardownTimeoutMs, thrownMessage } from '../util/index.js'
|
|
8
8
|
import type { OutputContext, VxPlugin } from './plugin.js'
|
|
9
|
+
import { readOnlyEvents } from './read-only.js'
|
|
9
10
|
|
|
10
11
|
const REQUIRED = ['status', 'taskStdout', 'taskStderr', 'taskComplete'] as const
|
|
11
12
|
const OPTIONAL = [
|
|
@@ -87,24 +88,35 @@ export function guardedOutput(
|
|
|
87
88
|
}
|
|
88
89
|
// An `async` method's rejection reached no one: an unhandled rejection
|
|
89
90
|
// ended the run with a stack, exit 1, after its tasks had passed. It
|
|
90
|
-
// fails the Logger as a throw does
|
|
91
|
+
// fails the Logger as a throw does, and core draws the event it was
|
|
92
|
+
// handed, as after a throw: a rejected `taskComplete` lost its task's row.
|
|
93
|
+
let current: RunEvent | undefined
|
|
91
94
|
const logger = Object.create(claimed.logger) as Logger
|
|
92
95
|
for (const k of [...REQUIRED, ...OPTIONAL, 'warn'] as const) {
|
|
93
96
|
const method = claimed.logger[k] as ((...args: unknown[]) => unknown) | undefined
|
|
94
97
|
if (typeof method !== 'function') continue
|
|
95
98
|
;(logger as unknown as Record<string, unknown>)[k] = (...args: unknown[]): void => {
|
|
99
|
+
const event = current
|
|
96
100
|
const ret = method.apply(claimed.logger, args)
|
|
97
|
-
if (ret instanceof Promise)
|
|
101
|
+
if (ret instanceof Promise)
|
|
102
|
+
ret.catch((err: unknown) => {
|
|
103
|
+
fail(err)
|
|
104
|
+
if (event !== undefined) fallback(event)
|
|
105
|
+
})
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
const render = terminalSubscriber(logger)
|
|
109
|
+
const viewOf = readOnlyEvents()
|
|
101
110
|
return (event) => {
|
|
102
111
|
if (failed) return fallback(event)
|
|
112
|
+
current = event
|
|
103
113
|
try {
|
|
104
|
-
render(event)
|
|
114
|
+
render(viewOf(event))
|
|
105
115
|
} catch (err) {
|
|
106
116
|
fail(err)
|
|
107
117
|
fallback(event)
|
|
118
|
+
} finally {
|
|
119
|
+
current = undefined
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// a literal RS doubled. Output that never touched stderr is its own text.
|
|
6
6
|
|
|
7
7
|
import { type BoundedCapture, type CapturedChunk, isDroppedOutputChunk } from '../exec/index.js'
|
|
8
|
-
import {
|
|
8
|
+
import { maskStreams, type SecretMask } from '../util/index.js'
|
|
9
9
|
|
|
10
10
|
const RS = '\x1e'
|
|
11
11
|
|
|
@@ -43,59 +43,6 @@ export function maskedOutputLog(chunks: readonly CapturedChunk[], mask: SecretMa
|
|
|
43
43
|
return h + back + chunks[at]!.text + lead + t
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* Each stream's run of adjacent non-empty chunks joined, with `mask`
|
|
48
|
-
* applied to each stream's text as a whole: a value is masked where it
|
|
49
|
-
* starts and its rest dropped from the runs it reaches. The live stream
|
|
50
|
-
* masks chunk by chunk and lets a held piece go after a pause, so a value
|
|
51
|
-
* written in two pieces reached the capture in two chunks, and a write to
|
|
52
|
-
* the other stream between them made them two runs. A hit replays each
|
|
53
|
-
* stream to its own descriptor, so `vx run > log` joined them again.
|
|
54
|
-
*/
|
|
55
|
-
export function maskStreams(
|
|
56
|
-
chunks: readonly CapturedChunk[],
|
|
57
|
-
mask: SecretMask | undefined,
|
|
58
|
-
): CapturedChunk[] {
|
|
59
|
-
if (mask === undefined) return chunks.filter((c) => c.text.length > 0)
|
|
60
|
-
const runs: CapturedChunk[] = []
|
|
61
|
-
for (const c of chunks) {
|
|
62
|
-
if (c.text.length === 0) continue
|
|
63
|
-
const last = runs.at(-1)
|
|
64
|
-
if (last !== undefined && last.err === c.err) last.text += c.text
|
|
65
|
-
else runs.push({ text: c.text, err: c.err })
|
|
66
|
-
}
|
|
67
|
-
for (const err of [false, true]) {
|
|
68
|
-
const own = runs.filter((r) => r.err === err)
|
|
69
|
-
const spans = mask.spans(own.map((r) => r.text).join(''))
|
|
70
|
-
if (spans.length === 0) continue
|
|
71
|
-
let at = 0
|
|
72
|
-
let i = 0
|
|
73
|
-
for (const r of own) {
|
|
74
|
-
const end = at + r.text.length
|
|
75
|
-
let text = ''
|
|
76
|
-
let from = at
|
|
77
|
-
for (; i < spans.length && spans[i]![0] < end; i++) {
|
|
78
|
-
const [s, e] = spans[i]!
|
|
79
|
-
if (s >= from) text += r.text.slice(from - at, s - at) + MASKED
|
|
80
|
-
from = Math.max(from, Math.min(e, end))
|
|
81
|
-
// A value that runs past this run is cut from the next one too.
|
|
82
|
-
if (e > end) break
|
|
83
|
-
}
|
|
84
|
-
r.text = text + r.text.slice(from - at)
|
|
85
|
-
at = end
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// A run emptied whole leaves its neighbours, the other stream, adjacent.
|
|
89
|
-
const kept: CapturedChunk[] = []
|
|
90
|
-
for (const r of runs) {
|
|
91
|
-
if (r.text.length === 0) continue
|
|
92
|
-
const last = kept.at(-1)
|
|
93
|
-
if (last !== undefined && last.err === r.err) last.text += r.text
|
|
94
|
-
else kept.push(r)
|
|
95
|
-
}
|
|
96
|
-
return kept
|
|
97
|
-
}
|
|
98
|
-
|
|
99
46
|
/** The chunks `encodeOutputLog` wrote, adjacent same-stream text joined. */
|
|
100
47
|
export function decodeOutputLog(log: string): CapturedChunk[] {
|
|
101
48
|
if (!log.includes(RS)) return log.length === 0 ? [] : [{ text: log, err: false }]
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from './sandbox-request.js'
|
|
31
31
|
import { shellVerdict } from './shell-verdict.js'
|
|
32
32
|
import { forwardedSignal, terminateChildren } from './signals.js'
|
|
33
|
-
import { computeTaskHash } from './task-hash.js'
|
|
33
|
+
import { computeTaskHash, keyOf } from './task-hash.js'
|
|
34
34
|
import {
|
|
35
35
|
forgetUndeclaredWrites,
|
|
36
36
|
isAborted,
|
|
@@ -52,16 +52,18 @@ export function serverKey(args: ExecuteArgs): Promise<string | undefined> | stri
|
|
|
52
52
|
if (args.noDependants === true) return undefined
|
|
53
53
|
return (
|
|
54
54
|
args.upfrontKey ??
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
keyOf(
|
|
56
|
+
computeTaskHash({
|
|
57
|
+
node: args.node,
|
|
58
|
+
upstream: args.upstream,
|
|
59
|
+
workspaceRoot: args.workspaceRoot,
|
|
60
|
+
cache: args.cache,
|
|
61
|
+
forwardArgs: args.forwardArgs,
|
|
62
|
+
nestedProjectDirs: args.nestedProjectDirs,
|
|
63
|
+
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
64
|
+
...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
65
67
|
)
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -129,6 +131,7 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
129
131
|
await args.armSandbox?.()
|
|
130
132
|
// A persistent task never declares `cache` (the loader refuses it), so
|
|
131
133
|
// it keeps the whole linked grant: no key of its can be stale.
|
|
134
|
+
const deps = await dependencyReads(args.readableDeps?.(node) ?? [], args.workspaceRoot)
|
|
132
135
|
const sb = await sandboxRequestFor(
|
|
133
136
|
node,
|
|
134
137
|
step.sandbox,
|
|
@@ -136,7 +139,8 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
136
139
|
undefined,
|
|
137
140
|
args.nestedProjectDirs,
|
|
138
141
|
args.cacheDir,
|
|
139
|
-
|
|
142
|
+
deps.reads,
|
|
143
|
+
deps.reported,
|
|
140
144
|
)
|
|
141
145
|
placeholders = sb.placeholders
|
|
142
146
|
serverSandbox = sb.sandbox
|
|
@@ -181,6 +185,17 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
181
185
|
: {}),
|
|
182
186
|
...(signalChannel ? { signalChannel } : {}),
|
|
183
187
|
...(args.terminal === true ? { terminal: true } : {}),
|
|
188
|
+
// What the pre-ready text left held goes out first, so the run can
|
|
189
|
+
// tell it from what the server writes since ready.
|
|
190
|
+
...(args.serverReady !== undefined
|
|
191
|
+
? {
|
|
192
|
+
onReady: () => {
|
|
193
|
+
serverMasked?.end()
|
|
194
|
+
ends.end()
|
|
195
|
+
args.serverReady!(node.id)
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
: {}),
|
|
184
199
|
}
|
|
185
200
|
if (step.persistent.readyWhen !== undefined) {
|
|
186
201
|
persistentOpts.readyWhen = step.persistent.readyWhen
|
|
@@ -244,7 +259,9 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
244
259
|
readyWhen === undefined
|
|
245
260
|
? undefined
|
|
246
261
|
: setTimeout(() => {
|
|
247
|
-
|
|
262
|
+
// A server that traps SIGTERM runs out the kill grace after the
|
|
263
|
+
// timeout gave up; the wait is over, and its failure says so.
|
|
264
|
+
if (isAborted(args.stopSignal) || spawn.gaveUp()) return
|
|
248
265
|
const after = noticeMs < 1000 ? `${noticeMs} ms` : `${noticeMs / 1000} s`
|
|
249
266
|
const unbounded = effectiveTimeout === undefined ? ', with no exec.timeout' : ''
|
|
250
267
|
warnOf(log)(
|
|
@@ -288,10 +305,7 @@ export async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutc
|
|
|
288
305
|
// (an embedder) never saw a bare stderr write at all. What waited on
|
|
289
306
|
// an open line goes first.
|
|
290
307
|
ends.end()
|
|
291
|
-
log.taskStderr(
|
|
292
|
-
node,
|
|
293
|
-
ends.own(`[vx] ${node.id}: persistent task failed to become ready: ${message}\n`),
|
|
294
|
-
)
|
|
308
|
+
log.taskStderr(node, ends.own(`[vx] ${node.id}: not ready: ${message}\n`))
|
|
295
309
|
// A server's 127, 126 or signal death needs the line a one-shot task's
|
|
296
310
|
// gets (execute-task.ts): a dev server whose tool sits in a sibling's
|
|
297
311
|
// bin said only "exit 127".
|