@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
package/src/cli/why.ts
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { indexDb, Cache } from '../cache/index.js'
|
|
9
9
|
import { anchoredSpecError, flagHint, formatValue, seeHelp } from './help.js'
|
|
10
|
-
import { buildTaskGraph, isGroupTask, splitTaskId } from '../graph/index.js'
|
|
10
|
+
import { buildTaskGraph, isDefaultBuild, isGroupTask, splitTaskId } from '../graph/index.js'
|
|
11
11
|
import {
|
|
12
12
|
cacheKeyDiff,
|
|
13
13
|
explainCacheKey,
|
|
14
14
|
inputWriters,
|
|
15
15
|
latestRunId,
|
|
16
|
+
LocalHistoryProvider,
|
|
17
|
+
recordedTasks,
|
|
16
18
|
rootCauses,
|
|
17
19
|
upstreamTask,
|
|
18
20
|
whyDidThisRerun,
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
import { resolveRunId } from '../orchestrator/index.js'
|
|
21
23
|
import { MASKED, nearMatches, printable, refusal, UserError } from '../util/index.js'
|
|
22
24
|
import * as ui from '../ui/index.js'
|
|
25
|
+
import type { InputWriter, RootCause } from '../history/index.js'
|
|
23
26
|
import type { Style } from '../ui/index.js'
|
|
24
27
|
import { runRules } from '../settings.js'
|
|
25
28
|
import { buildPackageGraph, findWorkspaceRoot } from '../workspace/index.js'
|
|
@@ -139,7 +142,7 @@ function suggest(query: string, ids: readonly string[], invoked: boolean): strin
|
|
|
139
142
|
// No near name: say where to look, or that there is nothing yet.
|
|
140
143
|
// A run of only group tasks records no task row: "nothing has run" was
|
|
141
144
|
// false there, and its advice re-ran the group to the same answer.
|
|
142
|
-
if (ids.length > 0) return '; `vx last --
|
|
145
|
+
if (ids.length > 0) return '; `vx last --stats` lists the tasks that ran'
|
|
143
146
|
return invoked
|
|
144
147
|
? '; no recorded run executed a task (a group records none: ask about a task it depends on)'
|
|
145
148
|
: `; nothing has run here yet (vx run ${query}, then vx why)`
|
|
@@ -155,7 +158,8 @@ async function resolveTarget(
|
|
|
155
158
|
cache: Cache,
|
|
156
159
|
target: string,
|
|
157
160
|
cwdProject: () => Promise<string | null>,
|
|
158
|
-
declared: () => Promise<
|
|
161
|
+
declared: () => Promise<Declared>,
|
|
162
|
+
declaresId: (id: string) => Promise<Declared>,
|
|
159
163
|
): Promise<string> {
|
|
160
164
|
const db = indexDb(cache)
|
|
161
165
|
if (target.includes('#')) {
|
|
@@ -164,22 +168,40 @@ async function resolveTarget(
|
|
|
164
168
|
// the error's suggestions and the bare-name form only.
|
|
165
169
|
const [project, task] = splitTaskId(target)
|
|
166
170
|
const hit = db
|
|
167
|
-
.query('SELECT 1 FROM runs WHERE project = ? AND task = ? ORDER BY id DESC LIMIT 1')
|
|
171
|
+
.query('SELECT 1 FROM main.runs WHERE project = ? AND task = ? ORDER BY id DESC LIMIT 1')
|
|
168
172
|
.get(project, task)
|
|
169
173
|
if (hit !== null) return target
|
|
174
|
+
if ((await declaresId(target)) === 'group') throw groupRefusal(target)
|
|
170
175
|
} else {
|
|
171
176
|
// A bare name reads only its own task's rows: the DISTINCT over every
|
|
172
177
|
// pair sorted the whole history (24 of 26 ms at 58k rows), and its list
|
|
173
178
|
// serves only the miss's suggestions below.
|
|
174
179
|
const matches = (
|
|
175
|
-
db
|
|
180
|
+
db
|
|
181
|
+
.query('SELECT DISTINCT project FROM main.runs WHERE task = ? ORDER BY project')
|
|
182
|
+
.all(target) as {
|
|
176
183
|
project: string
|
|
177
184
|
}[]
|
|
178
185
|
).map((r) => `${r.project}#${target}`)
|
|
186
|
+
// Asked even when no project ran it: the cwd project's own that never
|
|
187
|
+
// ran read as a name nobody declares.
|
|
188
|
+
const here = await cwdProject()
|
|
189
|
+
if (here !== null) {
|
|
190
|
+
const own = `${here}#${target}`
|
|
191
|
+
if (matches.includes(own)) return own
|
|
192
|
+
// `vx run build` here runs this project's own: another project's
|
|
193
|
+
// runs were explained in its place, unsaid.
|
|
194
|
+
const kind = await declaresId(own)
|
|
195
|
+
if (kind === 'group') throw groupRefusal(own)
|
|
196
|
+
if (kind !== false) {
|
|
197
|
+
throw refusal(
|
|
198
|
+
{ tone: 'info', headline: `no runs of ${own} yet`, fix: `vx run ${target}` },
|
|
199
|
+
{ code: 'VX_E_NO_HISTORY', message: `vx why: no recorded runs for "${own}"` },
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
179
203
|
if (matches.length === 1) return matches[0]!
|
|
180
204
|
if (matches.length > 1) {
|
|
181
|
-
const here = await cwdProject()
|
|
182
|
-
if (here !== null && matches.includes(`${here}#${target}`)) return `${here}#${target}`
|
|
183
205
|
throw new UserError(
|
|
184
206
|
`vx why: "${target}" ran in ${matches.length} projects — pick one:\n` +
|
|
185
207
|
matches.map((m) => ` ${m}`).join('\n'),
|
|
@@ -187,17 +209,18 @@ async function resolveTarget(
|
|
|
187
209
|
)
|
|
188
210
|
}
|
|
189
211
|
}
|
|
190
|
-
const
|
|
191
|
-
.query('SELECT DISTINCT project, task FROM runs ORDER BY project, task')
|
|
192
|
-
.all() as Array<{ project: string; task: string }>
|
|
193
|
-
const ids = pairs.map((p) => `${p.project}#${p.task}`)
|
|
212
|
+
const ids = new LocalHistoryProvider(db).recentTaskIds()
|
|
194
213
|
const invoked = ids.length === 0 && db.query('SELECT 1 FROM invocations LIMIT 1').get() !== null
|
|
195
214
|
if (ids.length === 0 && !invoked) {
|
|
196
215
|
// `vx run` of a task no project declares is refused: its advice was that.
|
|
197
216
|
const known = await declared()
|
|
217
|
+
// Where the cwd's project does not declare it (a monorepo root), a bare
|
|
218
|
+
// `vx run build` is refused too: that advice sent the user in a circle.
|
|
219
|
+
const here = known && !target.includes('#') ? await cwdProject() : null
|
|
220
|
+
const all = here !== null && (await declaresId(`${here}#${target}`)) === false ? ' --all' : ''
|
|
198
221
|
throw refusal(
|
|
199
222
|
known
|
|
200
|
-
? { tone: 'info', headline: `no runs of ${target} yet`, fix: `vx run ${target}` }
|
|
223
|
+
? { tone: 'info', headline: `no runs of ${target} yet`, fix: `vx run ${target}${all}` }
|
|
201
224
|
: {
|
|
202
225
|
tone: 'info',
|
|
203
226
|
headline: `no project declares ${target}`,
|
|
@@ -271,16 +294,12 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
271
294
|
target,
|
|
272
295
|
() => findCwdProject(process.cwd()),
|
|
273
296
|
() => declares(root, target),
|
|
297
|
+
// The cwd project's own: a bare `vx run` there skips its default build.
|
|
298
|
+
(id) => declares(root, id, true),
|
|
274
299
|
)
|
|
275
300
|
let runId: string | null
|
|
276
301
|
if (parsed.runId !== undefined) {
|
|
277
|
-
|
|
278
|
-
// --log` prints its id, so the whole id is taken from its rows too.
|
|
279
|
-
runId =
|
|
280
|
-
resolveRunId(db, parsed.runId, 'vx why') ??
|
|
281
|
-
(db.query('SELECT 1 FROM runs WHERE run_id = ? LIMIT 1').get(parsed.runId) !== null
|
|
282
|
-
? parsed.runId
|
|
283
|
-
: null)
|
|
302
|
+
runId = resolveRunId(db, parsed.runId, 'vx why')
|
|
284
303
|
// An unknown id read as a known run missing the task (X-18).
|
|
285
304
|
if (runId === null) {
|
|
286
305
|
throw new UserError(
|
|
@@ -329,18 +348,16 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
329
348
|
// each moved dependency in this run down to the tasks whose own
|
|
330
349
|
// inputs moved, so one `vx why` names the file, not the next task.
|
|
331
350
|
// A group records no run: the graph names what it gathers.
|
|
351
|
+
const recorded = recordedTasks(db, runId)
|
|
332
352
|
const unrecorded = diff.entries.filter(
|
|
333
|
-
(e) =>
|
|
334
|
-
e.kind === 'upstream' &&
|
|
335
|
-
e.change === 'changed' &&
|
|
336
|
-
!cacheKeyDiff(db, runId, upstreamTask(e.name)).found,
|
|
353
|
+
(e) => e.kind === 'upstream' && e.change === 'changed' && !recorded.has(upstreamTask(e.name)),
|
|
337
354
|
)
|
|
338
355
|
const groups = unrecorded.length > 0 ? await groupMembers(root, taskId) : new Map()
|
|
339
356
|
const roots = rootCauses(db, runId, taskId, diff.entries, groups)
|
|
340
|
-
const writers = inputWriters(db, root, taskId, diff.entries, roots)
|
|
357
|
+
const writers = inputWriters(db, runId, root, taskId, diff.entries, roots)
|
|
341
358
|
|
|
342
359
|
if (parsed.format === 'json') {
|
|
343
|
-
ui.
|
|
360
|
+
await ui.dataPieces('out', jsonPieces({ taskId, runId, why, diff }, roots, writers))
|
|
344
361
|
return 0
|
|
345
362
|
}
|
|
346
363
|
|
|
@@ -407,8 +424,18 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
407
424
|
const what = r.entries.map((e) =>
|
|
408
425
|
e.name === e.kind ? e.kind : `${e.kind} ${printable(e.name)}`,
|
|
409
426
|
)
|
|
427
|
+
// A 20,000-task chain printed whole is one 389 KB line.
|
|
428
|
+
const chain = r.chain
|
|
429
|
+
const ids =
|
|
430
|
+
chain.length > CHAIN_SHOWN
|
|
431
|
+
? [
|
|
432
|
+
...chain.slice(0, 2).map((id) => ui.id(id)),
|
|
433
|
+
ui.dim(`${chain.length - 5} more`),
|
|
434
|
+
...chain.slice(-3).map((id) => ui.id(id)),
|
|
435
|
+
]
|
|
436
|
+
: chain.map((id) => ui.id(id))
|
|
410
437
|
return [
|
|
411
|
-
...
|
|
438
|
+
...ids.flatMap((id, i) => [...(i > 0 ? [ui.dim(' \u2190 ')] : []), id]),
|
|
412
439
|
ui.dim(' \u2190 '),
|
|
413
440
|
ui.strong(
|
|
414
441
|
`${what.slice(0, 3).join(', ')}${what.length > 3 ? ` and ${what.length - 3} more` : ''}`,
|
|
@@ -470,6 +497,33 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
470
497
|
|
|
471
498
|
const ROOTS_SHOWN = 5
|
|
472
499
|
|
|
500
|
+
/** Longer root-cause chains print their two ends around a count. */
|
|
501
|
+
const CHAIN_SHOWN = 6
|
|
502
|
+
|
|
503
|
+
/** Bytes of roots per piece written. */
|
|
504
|
+
const PIECE_BYTES = 1 << 20
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* `{ ...head, roots, writers }` as one `JSON.stringify` of it gives, in
|
|
508
|
+
* pieces: every root carries its whole chain, so a 20,000-task chain's
|
|
509
|
+
* roots hold 2×10⁸ ids, more than one string holds.
|
|
510
|
+
*/
|
|
511
|
+
function* jsonPieces(
|
|
512
|
+
head: object,
|
|
513
|
+
roots: readonly RootCause[],
|
|
514
|
+
writers: readonly InputWriter[],
|
|
515
|
+
): Generator<string> {
|
|
516
|
+
let piece = `${JSON.stringify(head).slice(0, -1)},"roots":[`
|
|
517
|
+
for (let i = 0; i < roots.length; i++) {
|
|
518
|
+
piece += `${i > 0 ? ',' : ''}${JSON.stringify(roots[i])}`
|
|
519
|
+
if (piece.length >= PIECE_BYTES) {
|
|
520
|
+
yield piece
|
|
521
|
+
piece = ''
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
yield `${piece}],"writers":${JSON.stringify(writers)}}\n`
|
|
525
|
+
}
|
|
526
|
+
|
|
473
527
|
const CHANGE_HUE: Record<string, Style> = { added: 'ok', removed: 'error', changed: 'warn' }
|
|
474
528
|
|
|
475
529
|
type DiffEntry = ReturnType<typeof cacheKeyDiff>['entries'][number]
|
|
@@ -508,21 +562,41 @@ function upstreamTodo(upstream: readonly DiffEntry[], unrecorded: readonly DiffE
|
|
|
508
562
|
return out
|
|
509
563
|
}
|
|
510
564
|
|
|
511
|
-
/**
|
|
512
|
-
|
|
565
|
+
/** A declared group (no `exec`) records no run: `group` when every declaration is one. */
|
|
566
|
+
type Declared = false | 'task' | 'group'
|
|
567
|
+
|
|
568
|
+
/** Whether a project declares `target` (`pkg#task`, or a bare task); `task` when no config loads. */
|
|
569
|
+
async function declares(root: string, target: string, own = false): Promise<Declared> {
|
|
513
570
|
try {
|
|
514
571
|
const projects = await loadCliProjects(root, await discoverCliProjects(root), 'all')
|
|
515
572
|
const [project, task] = target.includes('#') ? splitTaskId(target) : [undefined, target]
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
573
|
+
const found = [...projects.values()].flatMap((p) => {
|
|
574
|
+
const t = p.config.tasks?.[task]
|
|
575
|
+
return (project === undefined || p.name === project) &&
|
|
576
|
+
Object.hasOwn(p.config.tasks ?? {}, task) &&
|
|
577
|
+
!(own && isDefaultBuild(t))
|
|
578
|
+
? [t]
|
|
579
|
+
: []
|
|
580
|
+
})
|
|
581
|
+
if (found.length === 0) return false
|
|
582
|
+
// The default build is a group too, but no one declared it.
|
|
583
|
+
return found.every((t) => t?.exec === undefined && !isDefaultBuild(t)) ? 'group' : 'task'
|
|
520
584
|
} catch {
|
|
521
585
|
// quiet: with no lock or a broken config, the run says why itself
|
|
522
|
-
return
|
|
586
|
+
return 'task'
|
|
523
587
|
}
|
|
524
588
|
}
|
|
525
589
|
|
|
590
|
+
function groupRefusal(id: string): UserError {
|
|
591
|
+
return refusal(
|
|
592
|
+
{ tone: 'info', headline: `${id} is a group, which records no run`, fix: `vx show ${id}` },
|
|
593
|
+
{
|
|
594
|
+
code: 'VX_E_NO_HISTORY',
|
|
595
|
+
message: `vx why: ${id} is a group, which records no run (vx show ${id} names what it gathers)`,
|
|
596
|
+
},
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
|
|
526
600
|
/**
|
|
527
601
|
* Each group in `taskId`'s graph with the tasks it gathers. Empty when a
|
|
528
602
|
* config no longer loads: the advice then names the group to `vx show`.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import type { WorkspaceConfig } from '../config.js'
|
|
10
|
+
import type { LazyGitEnumeration } from '../cache/index.js'
|
|
10
11
|
import { UserError } from '../util/index.js'
|
|
11
12
|
import { sayLine } from '../ui/index.js'
|
|
12
13
|
import {
|
|
@@ -65,10 +66,19 @@ export async function loadCliWorkspace(
|
|
|
65
66
|
export async function discoverCliProjects(
|
|
66
67
|
root: string,
|
|
67
68
|
load: CliLoadOptions = {},
|
|
69
|
+
/** The whole-tree enumeration the caller started; the run reuses it with the discovery. */
|
|
70
|
+
git?: LazyGitEnumeration,
|
|
68
71
|
): Promise<ProjectMeta[]> {
|
|
69
72
|
const warn = load.warn ?? warnToStderr
|
|
70
73
|
const ws = await loadCliWorkspace(root, warn)
|
|
71
|
-
return await discoverProjects(
|
|
74
|
+
return await discoverProjects(
|
|
75
|
+
root,
|
|
76
|
+
lockReader(root),
|
|
77
|
+
ws.plugins,
|
|
78
|
+
stageCacheDir(ws, load),
|
|
79
|
+
warn,
|
|
80
|
+
git,
|
|
81
|
+
)
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
/**
|
package/src/config.ts
CHANGED
|
@@ -497,6 +497,15 @@ export interface ExecConfig {
|
|
|
497
497
|
* it on a worker that is. Omitted → anywhere.
|
|
498
498
|
*/
|
|
499
499
|
platforms?: readonly Platform[]
|
|
500
|
+
/**
|
|
501
|
+
* The worker type to run it on, by name (`'ubuntu'`), when an executor
|
|
502
|
+
* that names worker types (`TaskExecutor.workers`, `@vzn/vx-dte`'s
|
|
503
|
+
* `workers`) runs it: only that type's machines take it, and a run
|
|
504
|
+
* whose executor has no such type, or none on one of `platforms`, is
|
|
505
|
+
* refused before it starts. Placement only: not in the key, and a run
|
|
506
|
+
* on this machine ignores it. Omitted → the first type that fits.
|
|
507
|
+
*/
|
|
508
|
+
worker?: string
|
|
500
509
|
/** Environment exposed to the child process. */
|
|
501
510
|
env?: ExecEnv
|
|
502
511
|
/**
|
package/src/exec/env.ts
CHANGED
|
@@ -39,7 +39,9 @@ export interface BuildEnvOptions {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function buildIsolatedEnv(opts: BuildEnvOptions): NodeJS.ProcessEnv {
|
|
42
|
-
|
|
42
|
+
// No prototype: on `{}`, a name `__proto__` hit the setter and reached
|
|
43
|
+
// no task, though the key folded it.
|
|
44
|
+
const out: NodeJS.ProcessEnv = Object.create(null)
|
|
43
45
|
|
|
44
46
|
for (const name of ESSENTIAL_ENV) {
|
|
45
47
|
const value = opts.source[name]
|
package/src/exec/executor.ts
CHANGED
|
@@ -48,6 +48,11 @@ export interface ExecuteSandbox {
|
|
|
48
48
|
* that builds the request again hands `sandboxRequestFor` too.
|
|
49
49
|
*/
|
|
50
50
|
readonly dependencyReads?: readonly string[]
|
|
51
|
+
/**
|
|
52
|
+
* The heads of those outputs (absolute): a refused read under one is a
|
|
53
|
+
* file beside them, reported. Handed `sandboxRequestFor` with them.
|
|
54
|
+
*/
|
|
55
|
+
readonly dependencyReported?: readonly string[]
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
/** One declared input file, as the cache key saw it. */
|
|
@@ -79,8 +84,9 @@ export interface TaskInputs {
|
|
|
79
84
|
* Dependencies whose cache keys are folded. `outputs` lists each one's
|
|
80
85
|
* declared output files (workspace-relative POSIX, as stored with its cache
|
|
81
86
|
* entry) — restored on disk before this task runs, so an input-shipping
|
|
82
|
-
* executor can include them in the input root.
|
|
83
|
-
*
|
|
87
|
+
* executor can include them in the input root. A non-cacheable
|
|
88
|
+
* dependency's are its declared outputs found on disk; a cacheable one
|
|
89
|
+
* with no entry in the local index lists none.
|
|
84
90
|
*/
|
|
85
91
|
readonly upstream: ReadonlyArray<{
|
|
86
92
|
readonly taskId: string
|
|
@@ -109,6 +115,12 @@ export interface ServerSpec {
|
|
|
109
115
|
readonly sandbox?: { readonly config: SandboxConfig; readonly nested: readonly string[] }
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
/**
|
|
119
|
+
* One task's command, as core hands it to an executor. A plugin
|
|
120
|
+
* executor's `outputs`, `envDefine` and `forwardArgs` are frozen copies:
|
|
121
|
+
* core saves the outputs after the command, and a config or the run's
|
|
122
|
+
* args are shared with other tasks.
|
|
123
|
+
*/
|
|
112
124
|
export interface ExecuteRequest {
|
|
113
125
|
/** `${project}#${task}` — for executors that route or log by task. */
|
|
114
126
|
readonly taskId: string
|
|
@@ -150,6 +162,12 @@ export interface ExecuteRequest {
|
|
|
150
162
|
* executor's `platforms`: its key includes it. Absent when the task names none.
|
|
151
163
|
*/
|
|
152
164
|
readonly platform?: string
|
|
165
|
+
/**
|
|
166
|
+
* The worker type to run it on, one of the executor's `workers`: the
|
|
167
|
+
* task's `exec.worker`, else the first type with one of its platforms.
|
|
168
|
+
* Absent when the executor names no types.
|
|
169
|
+
*/
|
|
170
|
+
readonly worker?: string
|
|
153
171
|
/**
|
|
154
172
|
* Which attempt of the task this is, from 2 when it retries; absent on
|
|
155
173
|
* the first. An executor that can, runs a retry somewhere other than
|
|
@@ -190,7 +208,8 @@ export interface ExecuteRequest {
|
|
|
190
208
|
readonly onStderr: (chunk: string) => void
|
|
191
209
|
/**
|
|
192
210
|
* Aborted when the run stops (Ctrl-C, SIGTERM, SIGHUP or an embedder's
|
|
193
|
-
* `RunOptions.signal`)
|
|
211
|
+
* `RunOptions.signal`), when `timeoutMs` elapses, or when fail-fast
|
|
212
|
+
* withdraws a task `running` says is not begun. An executor ends its
|
|
194
213
|
* work and returns: a child an executor spawned is not one core can
|
|
195
214
|
* reach (H-10, H-12). One already aborted is work not to start (B-55).
|
|
196
215
|
* Any exit after the timeout's abort, a 0 included, is a timeout. Core
|
|
@@ -236,10 +255,11 @@ export interface ExecuteResult extends RunResult {
|
|
|
236
255
|
*/
|
|
237
256
|
readonly outputs?: { kind: 'disk' } | { kind: 'deferred'; materialize: () => Promise<void> }
|
|
238
257
|
/**
|
|
239
|
-
* Tracked files the command changed where it ran,
|
|
240
|
-
* POSIX: an executor that runs it
|
|
241
|
-
* none of it. One the key read
|
|
242
|
-
*
|
|
258
|
+
* Tracked files the command changed where it ran, and untracked ones it
|
|
259
|
+
* found there, workspace-relative POSIX: an executor that runs it
|
|
260
|
+
* elsewhere says so, as this disk shows none of it. One the key read, or
|
|
261
|
+
* one its inputs declare, withholds the save under the key, as an input
|
|
262
|
+
* changed here does.
|
|
243
263
|
*/
|
|
244
264
|
readonly changed?: readonly string[]
|
|
245
265
|
/**
|
|
@@ -291,9 +311,12 @@ export interface TaskExecutor {
|
|
|
291
311
|
/** Runs the command somewhere else. Never offered a `pinnedLocal` task. */
|
|
292
312
|
readonly remote?: boolean
|
|
293
313
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
314
|
+
* Enforces `ExecuteRequest.sandbox` where it runs the command, as the
|
|
315
|
+
* local one does; the request's violations come back on the result. Only
|
|
316
|
+
* such an executor is offered a task that declares `exec.sandbox`, here
|
|
317
|
+
* or remote: one that ran it unsandboxed passed a boundary it never
|
|
318
|
+
* checked and cached the output. A `remote` one is offered the
|
|
319
|
+
* dependants of a sandboxed task too.
|
|
297
320
|
*/
|
|
298
321
|
readonly sandbox?: boolean
|
|
299
322
|
/**
|
|
@@ -338,6 +361,14 @@ export interface TaskExecutor {
|
|
|
338
361
|
* declares none is never offered such a task off this machine.
|
|
339
362
|
*/
|
|
340
363
|
readonly platforms?: readonly string[]
|
|
364
|
+
/**
|
|
365
|
+
* Its worker types in order of preference, each with the platforms its
|
|
366
|
+
* machines run (`@vzn/vx-dte`'s `workers`). A task naming `exec.worker`
|
|
367
|
+
* is offered it only when it has that type on one of the task's
|
|
368
|
+
* platforms; any other task goes to the first type that fits, on
|
|
369
|
+
* `ExecuteRequest.worker`. Its `platforms` must list every one of them.
|
|
370
|
+
*/
|
|
371
|
+
readonly workers?: ReadonlyArray<readonly [name: string, platforms: readonly string[]]>
|
|
341
372
|
/**
|
|
342
373
|
* How many tasks this executor runs at once. Its tasks then occupy a pool
|
|
343
374
|
* of this size instead of the local worker slots, so a remote pool is not
|
|
@@ -345,7 +376,11 @@ export interface TaskExecutor {
|
|
|
345
376
|
* is refused when the executor is resolved. Absent = the local pool.
|
|
346
377
|
*/
|
|
347
378
|
readonly capacity?: number
|
|
348
|
-
/**
|
|
379
|
+
/**
|
|
380
|
+
* Per-task opt-out at placement time. Absent = accepts every task it is
|
|
381
|
+
* offered. `false` or `undefined` declines; any other non-boolean is
|
|
382
|
+
* refused, naming the plugin.
|
|
383
|
+
*/
|
|
349
384
|
accepts?(task: TaskPlacement): boolean
|
|
350
385
|
/**
|
|
351
386
|
* The tasks placed here that will run and have NOT finished yet — called
|
|
@@ -406,8 +441,17 @@ export interface TaskExecutor {
|
|
|
406
441
|
* Where a task it was handed runs (`small#2`), for the live row; asked
|
|
407
442
|
* per frame. `null` while it holds the task for a machine to begin it:
|
|
408
443
|
* the row says `waiting`, and its clock starts when a machine begins it.
|
|
444
|
+
* Under `--continue=never` a failure aborts the `signal` of each task it
|
|
445
|
+
* answers `null` for, and the task is skipped, never started.
|
|
409
446
|
*/
|
|
410
447
|
running?(taskId: string): string | null | undefined
|
|
448
|
+
/**
|
|
449
|
+
* A `servers` executor's: no task left in the run uses persistent task
|
|
450
|
+
* `taskId`, so each copy it started beside a dependant stops now, not at
|
|
451
|
+
* teardown. Core asks once, as the run stops its own copy (`schema.md`,
|
|
452
|
+
* "Stopped once nothing needs it"). A throw is ignored.
|
|
453
|
+
*/
|
|
454
|
+
unneeded?(taskId: string): void
|
|
411
455
|
execute(req: ExecuteRequest): Promise<ExecuteResult>
|
|
412
456
|
}
|
|
413
457
|
|
|
@@ -431,7 +475,11 @@ export interface MachineReport {
|
|
|
431
475
|
/** Bytes in use at once, at the most and on average. */
|
|
432
476
|
memoryPeakBytes: number
|
|
433
477
|
memoryAvgBytes: number
|
|
434
|
-
/**
|
|
478
|
+
/**
|
|
479
|
+
* Its machine's busy core-time over the core-time it had, from its first
|
|
480
|
+
* task's send to its last one's end, 0 to 1 (its tasks' own where its
|
|
481
|
+
* machine said none): whether cores were left idle.
|
|
482
|
+
*/
|
|
435
483
|
utilization: number
|
|
436
484
|
/** Cores the executor booked for the tasks it ran, on average over its time online. */
|
|
437
485
|
bookedAvg?: number
|
|
@@ -574,8 +622,8 @@ export function assertExecuteResult(
|
|
|
574
622
|
|
|
575
623
|
/**
|
|
576
624
|
* Place one task: the first executor, in declaration order, that may take
|
|
577
|
-
* it — a `remote` executor is skipped for a `pinnedLocal` task,
|
|
578
|
-
* `accepts()` decides. Decided once per task before scheduling. The local
|
|
625
|
+
* it — a `remote` executor is skipped for a `pinnedLocal` task, any one
|
|
626
|
+
* `barred` names is skipped, then `accepts()` decides. Decided once per task before scheduling. The local
|
|
579
627
|
* executor accepts everything, so with it declared the only throw is an
|
|
580
628
|
* `accepts()` that threw: refused as `label` names that executor, where it
|
|
581
629
|
* surfaced as a bare stack naming no plugin (item 1022).
|
|
@@ -586,10 +634,12 @@ export function selectExecutor(
|
|
|
586
634
|
label: (executor: TaskExecutor) => string = (e) => `executor '${e.name}'`,
|
|
587
635
|
/** `task.pinnedLocal` as `executor` sees it: one that enforces the sandbox or carries servers lifts some pins. */
|
|
588
636
|
pinnedFor: (executor: TaskExecutor) => boolean = () => task.pinnedLocal,
|
|
637
|
+
/** Never offered the task: a sandboxed one is barred from an executor that does not enforce it. */
|
|
638
|
+
barred: (executor: TaskExecutor) => boolean = () => false,
|
|
589
639
|
): TaskExecutor {
|
|
590
640
|
for (const executor of executors) {
|
|
591
641
|
const pinned = pinnedFor(executor)
|
|
592
|
-
if (pinned && executor.remote === true) continue
|
|
642
|
+
if ((pinned && executor.remote === true) || barred(executor)) continue
|
|
593
643
|
if (executor.accepts === undefined) return executor
|
|
594
644
|
let taken: unknown
|
|
595
645
|
try {
|
|
@@ -602,6 +652,17 @@ export function selectExecutor(
|
|
|
602
652
|
taken.catch(() => {})
|
|
603
653
|
throw new Error('returned a Promise; accepts is synchronous')
|
|
604
654
|
}
|
|
655
|
+
// `'no'` was truthy and took the task; admit refuses the same answer.
|
|
656
|
+
if (taken !== undefined && typeof taken !== 'boolean') {
|
|
657
|
+
const t = typeof taken
|
|
658
|
+
const what =
|
|
659
|
+
taken === null
|
|
660
|
+
? 'null'
|
|
661
|
+
: Array.isArray(taken)
|
|
662
|
+
? 'an array'
|
|
663
|
+
: `${t === 'object' ? 'an' : 'a'} ${t}`
|
|
664
|
+
throw new Error(`returned ${what}, not true or false`)
|
|
665
|
+
}
|
|
605
666
|
} catch (err) {
|
|
606
667
|
const m = thrownMessage(err)
|
|
607
668
|
throw new UserError(`${label(executor)} failed in accepts for ${task.taskId}: ${m}`)
|
package/src/exec/kill-tree.ts
CHANGED
|
@@ -151,7 +151,7 @@ function stopGuard(): void {
|
|
|
151
151
|
|
|
152
152
|
function guardWrite(line: string): void {
|
|
153
153
|
if (typeof guardFd !== 'number') return
|
|
154
|
-
let deadline =
|
|
154
|
+
let deadline = performance.now() + GUARD_STALL_MS
|
|
155
155
|
// A turn's batch can be more than the queue takes at once: it goes in
|
|
156
156
|
// pieces as the guard drains it, and only a guard that takes nothing for
|
|
157
157
|
// the whole stall is gone. With one deadline for the batch, 2000 strikes
|
|
@@ -162,9 +162,9 @@ function guardWrite(line: string): void {
|
|
|
162
162
|
const n = writeSync(guardFd, bytes, 0, Math.min(bytes.length, GUARD_PIECE))
|
|
163
163
|
if (n === bytes.length) return
|
|
164
164
|
bytes = bytes.subarray(n)
|
|
165
|
-
deadline =
|
|
165
|
+
deadline = performance.now() + GUARD_STALL_MS
|
|
166
166
|
} catch (err) {
|
|
167
|
-
if ((err as NodeJS.ErrnoException).code !== 'EAGAIN' ||
|
|
167
|
+
if ((err as NodeJS.ErrnoException).code !== 'EAGAIN' || performance.now() >= deadline) break
|
|
168
168
|
Bun.sleepSync(1)
|
|
169
169
|
}
|
|
170
170
|
}
|
|
@@ -414,7 +414,7 @@ export async function untilGroupsGone(
|
|
|
414
414
|
children: readonly Child[],
|
|
415
415
|
graceMs: number,
|
|
416
416
|
): Promise<Child[]> {
|
|
417
|
-
const deadline =
|
|
417
|
+
const deadline = performance.now() + graceMs
|
|
418
418
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
419
419
|
// Not unref'd: it is what guarantees progress when every other handle
|
|
420
420
|
// has drained, and the grace is bounded either way.
|
|
@@ -426,8 +426,8 @@ export async function untilGroupsGone(
|
|
|
426
426
|
])
|
|
427
427
|
if (timer !== undefined) clearTimeout(timer)
|
|
428
428
|
let left = children.filter(groupAlive)
|
|
429
|
-
while (left.length > 0 &&
|
|
430
|
-
await Bun.sleep(Math.min(GROUP_POLL_MS, deadline -
|
|
429
|
+
while (left.length > 0 && performance.now() < deadline) {
|
|
430
|
+
await Bun.sleep(Math.min(GROUP_POLL_MS, deadline - performance.now()))
|
|
431
431
|
left = left.filter(groupAlive)
|
|
432
432
|
}
|
|
433
433
|
return left
|
|
@@ -443,7 +443,7 @@ export async function untilGroupsGone(
|
|
|
443
443
|
* and a group that ignored the SIGTERM went un-killed; there the group
|
|
444
444
|
* signal is the answer and the grace ends the wait.
|
|
445
445
|
*/
|
|
446
|
-
function groupAlive(child: Child): boolean {
|
|
446
|
+
export function groupAlive(child: Child): boolean {
|
|
447
447
|
if (!(child.pid > 0) || goneGroups.has(child)) return false
|
|
448
448
|
try {
|
|
449
449
|
process.kill(-child.pid, 0)
|