@vzn/vx 0.0.646 → 0.0.648
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 +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
package/src/report/logger.ts
CHANGED
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
flakyNote,
|
|
13
13
|
pathNote,
|
|
14
14
|
whereNote,
|
|
15
|
+
formatTaskHashLine,
|
|
15
16
|
formatTaskHitLine,
|
|
16
17
|
formatTaskSkippedLine,
|
|
18
|
+
leavesStyle,
|
|
19
|
+
RESET,
|
|
17
20
|
type OutputChunk,
|
|
18
21
|
} from './framed-output.js'
|
|
19
22
|
import {
|
|
@@ -29,8 +32,8 @@ import {
|
|
|
29
32
|
formatSummarySection,
|
|
30
33
|
neverStarted,
|
|
31
34
|
ranUnsaved,
|
|
32
|
-
type CacheBucket,
|
|
33
35
|
type RunContext,
|
|
36
|
+
type TaskBucket,
|
|
34
37
|
} from './summary.js'
|
|
35
38
|
import { isGroupTask } from '../graph/index.js'
|
|
36
39
|
import {
|
|
@@ -41,7 +44,9 @@ import {
|
|
|
41
44
|
tailText,
|
|
42
45
|
type Tail,
|
|
43
46
|
detectColors,
|
|
47
|
+
envFlagOn,
|
|
44
48
|
type ColorSupport,
|
|
49
|
+
ui,
|
|
45
50
|
} from '../util/index.js'
|
|
46
51
|
import { isCacheHit } from '../graph/index.js'
|
|
47
52
|
import { failedLabel, outcomeWord, ranNoCache } from './events.js'
|
|
@@ -167,11 +172,6 @@ export interface OutputView {
|
|
|
167
172
|
ci?: boolean
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
/** CI=0 / CI=false count as "not CI" — several vendors use CI=true. */
|
|
171
|
-
function truthyEnv(v: string | undefined): boolean {
|
|
172
|
-
return v !== undefined && v !== '' && v !== '0' && v !== 'false'
|
|
173
|
-
}
|
|
174
|
-
|
|
175
175
|
// ── GitHub Actions workflow-command escaping ────────────────────────
|
|
176
176
|
// The runner parses `::`-prefixed LINES out of the log, so anything
|
|
177
177
|
// interpolated into one has to be encoded or it changes the command's shape.
|
|
@@ -214,8 +214,8 @@ export function resolveOutputView(
|
|
|
214
214
|
},
|
|
215
215
|
env: Record<string, string | undefined> = process.env,
|
|
216
216
|
): OutputView {
|
|
217
|
-
const ci =
|
|
218
|
-
const gha =
|
|
217
|
+
const ci = envFlagOn(env['CI'])
|
|
218
|
+
const gha = envFlagOn(env['GITHUB_ACTIONS'])
|
|
219
219
|
const mk = (mode: OutputView['mode']): OutputView => ({
|
|
220
220
|
mode,
|
|
221
221
|
...(gha ? { gha: true } : {}),
|
|
@@ -234,7 +234,7 @@ export function resolveOutputView(
|
|
|
234
234
|
const FORECAST_AFTER_MS = 1000
|
|
235
235
|
/** A run shorter than this ends without a desktop notification. */
|
|
236
236
|
const NOTIFY_AFTER_MS = 10_000
|
|
237
|
-
// A step of vx's own quicker than this shows nowhere (
|
|
237
|
+
// A step of vx's own quicker than this shows nowhere (2026-10-15).
|
|
238
238
|
const STEP_SHOWN_MS = 150
|
|
239
239
|
|
|
240
240
|
export function defaultLogger(
|
|
@@ -347,16 +347,15 @@ export function defaultLogger(
|
|
|
347
347
|
let forecastFn: Forecast | undefined
|
|
348
348
|
let graphForecast: (() => Forecast) | undefined
|
|
349
349
|
let forecastTimer: ReturnType<typeof setTimeout> | null = null
|
|
350
|
-
|
|
350
|
+
// When the forecast says the run ends; past it, the region counts up.
|
|
351
|
+
let deadlineMs: number | undefined
|
|
351
352
|
let critical: string | undefined
|
|
352
353
|
// Tasks the region showed on the critical path: their finished rows say so too.
|
|
353
354
|
const onPath = new Set<string>()
|
|
354
355
|
let blockedCount = 0
|
|
355
356
|
let expected: ReadonlyMap<string, number> | undefined
|
|
356
357
|
let blocked: readonly BlockedTask[] | undefined
|
|
357
|
-
|
|
358
|
-
// task until it starts, in that row, or leaves the list, so nothing hops.
|
|
359
|
-
const heldRows: (string | null)[] = []
|
|
358
|
+
let blockedBy: ReadonlyMap<string, number> | undefined
|
|
360
359
|
let etaAtMs = 0
|
|
361
360
|
const signals = writer.enabled
|
|
362
361
|
? terminalSignals(opts.env ?? process.env)
|
|
@@ -365,18 +364,11 @@ export function defaultLogger(
|
|
|
365
364
|
let progressShown = -1
|
|
366
365
|
let progressRed = false
|
|
367
366
|
|
|
368
|
-
// Every interactive view renders the fixed-height
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// 10 rows; excess running tasks queue for a freed slot, each shown
|
|
373
|
-
// in its own row after the slots. Focused flow keeps its lifecycle:
|
|
374
|
-
// the region dies the moment a requested node starts streaming.
|
|
367
|
+
// Every interactive view renders the fixed-height running list: one
|
|
368
|
+
// row per worker, sized at runStart from concurrency, so its height
|
|
369
|
+
// never changes; a task keeps its slot for its whole life. Excess
|
|
370
|
+
// running tasks queue for a freed slot, each shown after the slots.
|
|
375
371
|
let slots: (WorkerSlot | null)[] = []
|
|
376
|
-
// Rows shown: the most workers busy at once so far. An idle row past it
|
|
377
|
-
// was noise (a 10-core machine drew 9 on a one-chain run); it never
|
|
378
|
-
// shrinks, so a row never jumps.
|
|
379
|
-
let shownSlots = 0
|
|
380
372
|
const slotQueue: WorkerSlot[] = []
|
|
381
373
|
let succeeded = 0
|
|
382
374
|
// Executed with no `cache` block: never a miss, counted apart.
|
|
@@ -388,12 +380,12 @@ export function defaultLogger(
|
|
|
388
380
|
let notSaved = 0
|
|
389
381
|
let skippedCount = 0
|
|
390
382
|
// Each finished task's cache bucket by its key: its remote traffic moves it.
|
|
391
|
-
const buckets = new Map<string,
|
|
383
|
+
const buckets = new Map<string, TaskBucket>()
|
|
392
384
|
// The graph is done and vx's own closing steps run: the region stays,
|
|
393
|
-
// footer and step rows, until the footer replaces it (
|
|
385
|
+
// footer and step rows, until the footer replaces it (2026-10-15).
|
|
394
386
|
let tail = false
|
|
395
387
|
// The run was stopped: the region stays, its running rows say
|
|
396
|
-
// `aborting`, and nothing is forecast or held (
|
|
388
|
+
// `aborting`, and nothing is forecast or held (2026-10-17).
|
|
397
389
|
let stopping = false
|
|
398
390
|
// Counted live once the run is stopping, as the footer counts them.
|
|
399
391
|
let stopped = 0
|
|
@@ -415,7 +407,8 @@ export function defaultLogger(
|
|
|
415
407
|
// deferred to runEnd (owner: '✗ line, continue, all full frames at
|
|
416
408
|
// the end'). 'full'/CI keeps frames inline.
|
|
417
409
|
const deferredFailures: string[] = []
|
|
418
|
-
|
|
410
|
+
// Each ready server's outcome: one whose child has exited (`serverExit`) is no longer pinned.
|
|
411
|
+
const pinnedPersistent: TaskOutcome[] = []
|
|
419
412
|
let flushedFailures = false
|
|
420
413
|
// A persistent task (dev server, watcher, daemon) is the one task kind
|
|
421
414
|
// nothing bounds: a one-shot command's output ends when it exits, but a
|
|
@@ -472,10 +465,27 @@ export function defaultLogger(
|
|
|
472
465
|
}
|
|
473
466
|
const printsKept = (tail: { outcome?: TaskOutcome }): boolean =>
|
|
474
467
|
flushedPersistent && tail.outcome !== undefined && view.mode !== 'errors-only'
|
|
468
|
+
// A server writing before this run started its task is one an earlier run
|
|
469
|
+
// left running (`vx watch` keeps its dev server): it streams as a kept
|
|
470
|
+
// one, live above the rows. Buffered as a task yet to run, it was never shown.
|
|
471
|
+
const heldOver = (node: TaskNode): boolean =>
|
|
472
|
+
node.config.exec?.persistent !== undefined && view.mode !== 'errors-only'
|
|
475
473
|
|
|
474
|
+
// A forced refresh the region has not been built for yet: the build
|
|
475
|
+
// takes the forecast pass the event asked for.
|
|
476
|
+
let forcePending = false
|
|
477
|
+
// The region is built when the writer draws it (`setRegion`'s function
|
|
478
|
+
// form), from the state as it is then: the events a forced-redraw floor
|
|
479
|
+
// coalesces build nothing.
|
|
476
480
|
const refresh = (forced: boolean): void => {
|
|
477
481
|
if (statusDead) return
|
|
478
|
-
|
|
482
|
+
const first = forecastFn === undefined && graphForecast !== undefined && !tail && !stopping
|
|
483
|
+
forcePending ||= forced || first
|
|
484
|
+
writer.setRegion(buildRegion, { force: forced || first })
|
|
485
|
+
}
|
|
486
|
+
const buildRegion = (): readonly string[] => {
|
|
487
|
+
let force = forcePending
|
|
488
|
+
forcePending = false
|
|
479
489
|
const now = Date.now()
|
|
480
490
|
// A task its executor still holds has not begun: its clock starts at
|
|
481
491
|
// the first frame a machine runs it.
|
|
@@ -486,16 +496,13 @@ export function defaultLogger(
|
|
|
486
496
|
x.waiting = where(x.id) === null
|
|
487
497
|
if (x.waiting) x.startedMs = now
|
|
488
498
|
}
|
|
489
|
-
//
|
|
490
|
-
// knows, without history
|
|
491
|
-
if (forecastFn === undefined && graphForecast !== undefined && !tail && !stopping)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
force = true
|
|
497
|
-
break
|
|
498
|
-
}
|
|
499
|
+
// The rows' blocked counts and planned tasks from the first frame: the
|
|
500
|
+
// graph knows, without history.
|
|
501
|
+
if (forecastFn === undefined && graphForecast !== undefined && !tail && !stopping) {
|
|
502
|
+
forecastFn = graphForecast()
|
|
503
|
+
graphForecast = undefined
|
|
504
|
+
force = true
|
|
505
|
+
}
|
|
499
506
|
// Twice a second at most: one pass over the unfinished graph.
|
|
500
507
|
if (forecastFn !== undefined && !tail && !stopping && (force || now - etaAtMs >= 500)) {
|
|
501
508
|
etaAtMs = now
|
|
@@ -503,16 +510,23 @@ export function defaultLogger(
|
|
|
503
510
|
for (const s of slots) if (s !== null) running.set(s.id, s.startedMs)
|
|
504
511
|
for (const s of slotQueue) running.set(s.id, s.startedMs)
|
|
505
512
|
const f = forecastFn(finished, running, now)
|
|
506
|
-
|
|
513
|
+
deadlineMs =
|
|
514
|
+
f.etaMs === undefined
|
|
515
|
+
? undefined
|
|
516
|
+
: f.etaMs > 0
|
|
517
|
+
? now + f.etaMs
|
|
518
|
+
: Math.min(deadlineMs ?? now, now)
|
|
507
519
|
critical = f.critical
|
|
508
520
|
blockedCount = f.waiting
|
|
509
521
|
expected = f.expected
|
|
510
522
|
blocked = f.blocked
|
|
523
|
+
blockedBy = f.blockedBy
|
|
511
524
|
}
|
|
525
|
+
const etaMs = deadlineMs === undefined ? undefined : deadlineMs - now
|
|
512
526
|
if (signals.progress && total > 0) {
|
|
513
527
|
const elapsed = now - startedAtMs
|
|
514
528
|
const pct = Math.floor(
|
|
515
|
-
etaMs !== undefined &&
|
|
529
|
+
etaMs !== undefined && etaMs > 0
|
|
516
530
|
? (elapsed / (elapsed + etaMs)) * 100
|
|
517
531
|
: (done / total) * 100,
|
|
518
532
|
)
|
|
@@ -532,7 +546,7 @@ export function defaultLogger(
|
|
|
532
546
|
if (x.waiting === true) scheduled++
|
|
533
547
|
else running++
|
|
534
548
|
}
|
|
535
|
-
if (critical !== undefined && running >
|
|
549
|
+
if (critical !== undefined && running > 0) onPath.add(critical)
|
|
536
550
|
// The live summary IS the final summary's section, built from the
|
|
537
551
|
// same formatter so the region visually becomes the printout.
|
|
538
552
|
const summaryLines =
|
|
@@ -557,7 +571,11 @@ export function defaultLogger(
|
|
|
557
571
|
...(running > 0 ? { running } : {}),
|
|
558
572
|
...(scheduled > 0 ? { scheduled } : {}),
|
|
559
573
|
...(blockedCount > 0 && !tail && !stopping ? { blocked: blockedCount } : {}),
|
|
560
|
-
...(etaMs !== undefined && !tail && !stopping
|
|
574
|
+
...(etaMs !== undefined && !tail && !stopping
|
|
575
|
+
? etaMs > 0
|
|
576
|
+
? { etaMs }
|
|
577
|
+
: { overMs: -etaMs }
|
|
578
|
+
: {}),
|
|
561
579
|
buckets,
|
|
562
580
|
spread:
|
|
563
581
|
spreadCount > 0
|
|
@@ -568,60 +586,30 @@ export function defaultLogger(
|
|
|
568
586
|
colors,
|
|
569
587
|
runContext,
|
|
570
588
|
)
|
|
571
|
-
const held = blocked === undefined || stopping ? undefined : holdRows(blocked)
|
|
572
589
|
const shownSteps: WorkerSlot[] = []
|
|
573
590
|
for (const [id, startedMs] of steps)
|
|
574
591
|
if (now - startedMs >= STEP_SHOWN_MS) shownSteps.push({ id, startedMs })
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
},
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
592
|
+
return formatStatusRegion(
|
|
593
|
+
{
|
|
594
|
+
pinnedPersistent: pinnedPersistent
|
|
595
|
+
.filter((o) => o.serverExit === undefined)
|
|
596
|
+
.map((o) => o.node.id),
|
|
597
|
+
// Every running task has a row: one beyond the slots follows them.
|
|
598
|
+
slots: [...slots, ...slotQueue],
|
|
599
|
+
size: slots.length,
|
|
600
|
+
...(blocked !== undefined && !stopping ? { next: blocked } : {}),
|
|
601
|
+
...(blockedBy !== undefined && !stopping ? { blockedBy } : {}),
|
|
602
|
+
...(stopping ? { stopping } : {}),
|
|
603
|
+
...(shownSteps.length > 0 ? { steps: shownSteps } : {}),
|
|
604
|
+
nowMs: Date.now(),
|
|
605
|
+
summaryLines,
|
|
606
|
+
...(runContext?.whereRunning !== undefined ? { where: runContext.whereRunning } : {}),
|
|
607
|
+
...(expected !== undefined ? { expected } : {}),
|
|
608
|
+
...(critical !== undefined ? { critical } : {}),
|
|
609
|
+
},
|
|
610
|
+
colors,
|
|
594
611
|
)
|
|
595
612
|
}
|
|
596
|
-
// Each idle worker row holds a blocked task, in scheduler order. A row
|
|
597
|
-
// with none left to hold leaves the region (M, 2026-10-15): blocked tasks
|
|
598
|
-
// only dwindle, so it is never needed again, and the rows below close up.
|
|
599
|
-
const holdRows = (list: readonly BlockedTask[]): (BlockedTask | null)[] => {
|
|
600
|
-
const started = new Set<string>()
|
|
601
|
-
for (const s of slots) if (s !== null) started.add(s.id)
|
|
602
|
-
for (const s of slotQueue) started.add(s.id)
|
|
603
|
-
const open = new Map<string, BlockedTask>()
|
|
604
|
-
for (const b of list) if (!started.has(b.id)) open.set(b.id, b)
|
|
605
|
-
const rows: (BlockedTask | null)[] = []
|
|
606
|
-
for (let i = 0; i < shownSlots; i++) {
|
|
607
|
-
const kept = slots[i] === null ? open.get(heldRows[i] ?? '') : undefined
|
|
608
|
-
rows.push(kept ?? null)
|
|
609
|
-
if (kept !== undefined) open.delete(kept.id)
|
|
610
|
-
}
|
|
611
|
-
const rest = open.values()
|
|
612
|
-
for (let i = 0; i < shownSlots; i++)
|
|
613
|
-
if (slots[i] === null && rows[i] === null) rows[i] = rest.next().value ?? null
|
|
614
|
-
for (let i = shownSlots - 1; i >= 0; i--) {
|
|
615
|
-
if (slots[i] !== null || rows[i] !== null) continue
|
|
616
|
-
rows.splice(i, 1)
|
|
617
|
-
slots.splice(i, 1)
|
|
618
|
-
slots.push(null)
|
|
619
|
-
shownSlots--
|
|
620
|
-
}
|
|
621
|
-
heldRows.length = 0
|
|
622
|
-
for (const r of rows) heldRows.push(r?.id ?? null)
|
|
623
|
-
return rows
|
|
624
|
-
}
|
|
625
613
|
const tick = (): void => {
|
|
626
614
|
if (!writer.enabled || statusDead || ticker !== null) return
|
|
627
615
|
// Keeps the per-worker elapsed time ticking between task events
|
|
@@ -652,14 +640,36 @@ export function defaultLogger(
|
|
|
652
640
|
}
|
|
653
641
|
|
|
654
642
|
// The task log opens with one blank line before its first row, on a
|
|
655
|
-
// terminal and off one (
|
|
643
|
+
// terminal and off one (2026-10-14). A note vx prints before it is
|
|
656
644
|
// no task row (`status`). Without a footer, the caller frames the log
|
|
657
645
|
// (`vx watch` opens each cycle with its own line).
|
|
658
646
|
let logOpened = opts.footer === false
|
|
659
647
|
// The log ends on a blank line (a block's own): a blank status line
|
|
660
648
|
// after it (the footer's) would make two; one blank line apart is the rule.
|
|
661
649
|
let endsBlank = false
|
|
650
|
+
// A live-streamed task's partial line, and any colour it left on, end
|
|
651
|
+
// before anything of vx's own: a row or a block written after it (a step
|
|
652
|
+
// that ends while the task streams, another task's failure, a kept
|
|
653
|
+
// server's line) was glued to it, `building... time status cache
|
|
654
|
+
// task`, and took its colour.
|
|
655
|
+
let streamStyled = false
|
|
656
|
+
const endStreamLine = (): void => {
|
|
657
|
+
const end = (streamStyled ? RESET : '') + (streamMidLine ? '\n' : '')
|
|
658
|
+
streamStyled = false
|
|
659
|
+
streamMidLine = false
|
|
660
|
+
if (end.length > 0) logWrite(end)
|
|
661
|
+
}
|
|
662
662
|
const taskWrite = (chunk: string): void => {
|
|
663
|
+
endStreamLine()
|
|
664
|
+
logWrite(chunk)
|
|
665
|
+
}
|
|
666
|
+
const streamWrite = (chunk: string): void => {
|
|
667
|
+
streamedSinceBlock = true
|
|
668
|
+
if (chunk.length > 0) streamMidLine = !chunk.endsWith('\n')
|
|
669
|
+
if (chunk.includes('\x1b[')) streamStyled = leavesStyle(chunk)
|
|
670
|
+
logWrite(chunk)
|
|
671
|
+
}
|
|
672
|
+
const logWrite = (chunk: string): void => {
|
|
663
673
|
if (chunk.length > 0) endsBlank = chunk.endsWith('\n\n')
|
|
664
674
|
if (!logOpened && chunk.length > 0) {
|
|
665
675
|
logOpened = true
|
|
@@ -673,7 +683,7 @@ export function defaultLogger(
|
|
|
673
683
|
lineEmitted = true
|
|
674
684
|
}
|
|
675
685
|
// The list's header, once over its first row, where the live region
|
|
676
|
-
// heads its own rows the same way (
|
|
686
|
+
// heads its own rows the same way (2026-10-21).
|
|
677
687
|
let headed = !writer.enabled
|
|
678
688
|
const emitRow = (line: string): void => {
|
|
679
689
|
if (!headed) {
|
|
@@ -686,6 +696,7 @@ export function defaultLogger(
|
|
|
686
696
|
// write so a stray newline doesn't sneak into the output.
|
|
687
697
|
const emitBlock = (block: string): void => {
|
|
688
698
|
if (block.length === 0) return
|
|
699
|
+
endStreamLine()
|
|
689
700
|
const lead = lineEmitted || streamedSinceBlock ? '\n' : ''
|
|
690
701
|
taskWrite(`${lead}${block}\n`)
|
|
691
702
|
lineEmitted = false
|
|
@@ -739,21 +750,15 @@ export function defaultLogger(
|
|
|
739
750
|
flushKept()
|
|
740
751
|
// A live-streamed server keeps writing after its frame closes, so a
|
|
741
752
|
// summary row or "exited with code" can follow its partial line.
|
|
742
|
-
|
|
743
|
-
writer.write('\n')
|
|
744
|
-
streamMidLine = false
|
|
745
|
-
}
|
|
753
|
+
endStreamLine()
|
|
746
754
|
if (line === '' && endsBlank) return
|
|
747
755
|
endsBlank = line === ''
|
|
748
756
|
writer.write(`${line}\n`)
|
|
749
757
|
},
|
|
750
758
|
warn(line) {
|
|
751
759
|
flushKept()
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
streamMidLine = false
|
|
755
|
-
}
|
|
756
|
-
writer.writeTo(errOut, `${line}\n`)
|
|
760
|
+
endStreamLine()
|
|
761
|
+
writer.writeTo(errOut, `${ui.tagged('warn', line, colors)}\n`)
|
|
757
762
|
},
|
|
758
763
|
runStart(info) {
|
|
759
764
|
total = info.total
|
|
@@ -761,9 +766,7 @@ export function defaultLogger(
|
|
|
761
766
|
runContext = info.context
|
|
762
767
|
startedAtMs = Date.now()
|
|
763
768
|
started = true
|
|
764
|
-
|
|
765
|
-
slots = Array.from({ length: cap }, () => null)
|
|
766
|
-
shownSlots = 0
|
|
769
|
+
slots = Array.from({ length: Math.max(1, info.concurrency ?? 10) }, () => null)
|
|
767
770
|
tick()
|
|
768
771
|
refresh(true)
|
|
769
772
|
},
|
|
@@ -775,7 +778,12 @@ export function defaultLogger(
|
|
|
775
778
|
stepEnd(id, { durationMs, ok }) {
|
|
776
779
|
steps.delete(id)
|
|
777
780
|
if (durationMs < STEP_SHOWN_MS) return
|
|
778
|
-
|
|
781
|
+
// `hash-only` is one line per task: only a step that failed adds one.
|
|
782
|
+
if (
|
|
783
|
+
ok
|
|
784
|
+
? view.mode !== 'none' && view.mode !== 'errors-only' && view.mode !== 'hash-only'
|
|
785
|
+
: view.mode !== 'none'
|
|
786
|
+
)
|
|
779
787
|
emitRow(formatStepLine(id, durationMs, ok, colors))
|
|
780
788
|
refresh(true)
|
|
781
789
|
},
|
|
@@ -783,7 +791,7 @@ export function defaultLogger(
|
|
|
783
791
|
if (stopping) return
|
|
784
792
|
stopping = true
|
|
785
793
|
blocked = undefined
|
|
786
|
-
|
|
794
|
+
blockedBy = undefined
|
|
787
795
|
refresh(true)
|
|
788
796
|
},
|
|
789
797
|
runDone() {
|
|
@@ -797,34 +805,25 @@ export function defaultLogger(
|
|
|
797
805
|
// What is coalesced lands before the task writes past it.
|
|
798
806
|
writer.settle()
|
|
799
807
|
}
|
|
800
|
-
// Focused flow:
|
|
801
|
-
//
|
|
802
|
-
//
|
|
808
|
+
// Focused flow: a requested node streams raw, and the region stays
|
|
809
|
+
// under its output (the writer erases and redraws it around each
|
|
810
|
+
// write): taken down, the summary and the run's id were gone until
|
|
811
|
+
// the task ended.
|
|
803
812
|
if (streamsLive(node)) {
|
|
804
|
-
killStatus()
|
|
805
813
|
// Open the live frame: full task info even when the command
|
|
806
814
|
// streams nothing (or the hit replays nothing).
|
|
807
815
|
emitLine(formatFrameOpen(node, colors, forwardArgs))
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
// its outcome would leave a never-ready one uncapped forever.
|
|
812
|
-
if (node.config.exec?.persistent !== undefined && !persistentTails.has(node.id)) {
|
|
816
|
+
} else if (node.config.exec?.persistent !== undefined && !persistentTails.has(node.id)) {
|
|
817
|
+
// Bound a persistent task's output from its FIRST chunk. Waiting for
|
|
818
|
+
// its outcome would leave a never-ready one uncapped forever.
|
|
813
819
|
persistentTails.set(node.id, { node, out: createTail(), err: createTail() })
|
|
814
820
|
}
|
|
821
|
+
// The rows are the status region's; with none, nothing reads them.
|
|
822
|
+
if (statusDead) return
|
|
815
823
|
const slot: WorkerSlot = { id: node.id, startedMs: Date.now() }
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
let free = own >= 0 && slots[own] === null ? own : -1
|
|
820
|
-
if (free < 0)
|
|
821
|
-
free = slots.findIndex((s, i) => s === null && i < shownSlots && heldRows[i] == null)
|
|
822
|
-
if (free < 0) free = slots.findLastIndex((s, i) => s === null && i < shownSlots)
|
|
823
|
-
if (free < 0) free = slots.indexOf(null)
|
|
824
|
-
if (free >= 0) {
|
|
825
|
-
slots[free] = slot
|
|
826
|
-
shownSlots = Math.max(shownSlots, free + 1)
|
|
827
|
-
} else slotQueue.push(slot)
|
|
824
|
+
const free = slots.indexOf(null)
|
|
825
|
+
if (free >= 0) slots[free] = slot
|
|
826
|
+
else slotQueue.push(slot)
|
|
828
827
|
refresh(true)
|
|
829
828
|
},
|
|
830
829
|
holdStatus: () => writer.hold(),
|
|
@@ -861,7 +860,7 @@ export function defaultLogger(
|
|
|
861
860
|
tail = true
|
|
862
861
|
slots = []
|
|
863
862
|
blocked = undefined
|
|
864
|
-
|
|
863
|
+
blockedBy = undefined
|
|
865
864
|
slotQueue.length = 0
|
|
866
865
|
refresh(true)
|
|
867
866
|
} else if (!tail) killStatus()
|
|
@@ -920,9 +919,7 @@ export function defaultLogger(
|
|
|
920
919
|
taskStdout(node, chunk) {
|
|
921
920
|
if (discardsOutput) return
|
|
922
921
|
if (streamsLive(node)) {
|
|
923
|
-
|
|
924
|
-
if (chunk.length > 0) streamMidLine = !chunk.endsWith('\n')
|
|
925
|
-
taskWrite(chunk)
|
|
922
|
+
streamWrite(chunk)
|
|
926
923
|
return
|
|
927
924
|
}
|
|
928
925
|
const tail = persistentTails.get(node.id)
|
|
@@ -931,14 +928,13 @@ export function defaultLogger(
|
|
|
931
928
|
else appendTail(tail.out, chunk)
|
|
932
929
|
return
|
|
933
930
|
}
|
|
931
|
+
if (heldOver(node)) return streamKept(node, chunk)
|
|
934
932
|
pushChunk(node.id, chunk, false)
|
|
935
933
|
},
|
|
936
934
|
taskStderr(node, chunk) {
|
|
937
935
|
if (discardsOutput) return
|
|
938
936
|
if (streamsLive(node)) {
|
|
939
|
-
|
|
940
|
-
if (chunk.length > 0) streamMidLine = !chunk.endsWith('\n')
|
|
941
|
-
taskWrite(chunk)
|
|
937
|
+
streamWrite(chunk)
|
|
942
938
|
return
|
|
943
939
|
}
|
|
944
940
|
const tail = persistentTails.get(node.id)
|
|
@@ -947,6 +943,7 @@ export function defaultLogger(
|
|
|
947
943
|
else appendTail(tail.err, chunk)
|
|
948
944
|
return
|
|
949
945
|
}
|
|
946
|
+
if (heldOver(node)) return streamKept(node, chunk)
|
|
950
947
|
pushChunk(node.id, chunk, true)
|
|
951
948
|
},
|
|
952
949
|
taskComplete(node, outcome) {
|
|
@@ -997,23 +994,21 @@ export function defaultLogger(
|
|
|
997
994
|
(view.mode === 'focused' && isPrimary(node)) ||
|
|
998
995
|
(framesTerminal && holdsTerminal(node)))
|
|
999
996
|
if (framed && streamsLive(node)) {
|
|
1000
|
-
if (streamMidLine) {
|
|
1001
|
-
taskWrite('\n')
|
|
1002
|
-
streamMidLine = false
|
|
1003
|
-
}
|
|
1004
997
|
emitFrameClose(formatFrameClose(node, outcome, colors))
|
|
1005
998
|
return
|
|
1006
999
|
}
|
|
1007
1000
|
if (framed && output.some((c) => c.text.length > 0)) {
|
|
1008
1001
|
emitBlock(
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1002
|
+
fenced(
|
|
1003
|
+
formatTaskBlock(
|
|
1004
|
+
node,
|
|
1005
|
+
outcome,
|
|
1006
|
+
{ output, ...dropped },
|
|
1007
|
+
colors,
|
|
1008
|
+
true,
|
|
1009
|
+
forwardArgs,
|
|
1010
|
+
links,
|
|
1011
|
+
),
|
|
1017
1012
|
),
|
|
1018
1013
|
)
|
|
1019
1014
|
return
|
|
@@ -1035,7 +1030,7 @@ export function defaultLogger(
|
|
|
1035
1030
|
// A persistent task's outcome arrives at READY; the child
|
|
1036
1031
|
// keeps running until the orchestrator SIGTERMs it at run
|
|
1037
1032
|
// end — pin it so its liveness stays visible.
|
|
1038
|
-
pinnedPersistent.push(
|
|
1033
|
+
pinnedPersistent.push(outcome)
|
|
1039
1034
|
// …and capture what it writes from here on. A live-streaming
|
|
1040
1035
|
// node already owns the terminal, so buffering it would emit
|
|
1041
1036
|
// its output twice.
|
|
@@ -1045,16 +1040,19 @@ export function defaultLogger(
|
|
|
1045
1040
|
}
|
|
1046
1041
|
// Free the task's slot; the longest-waiting queued task
|
|
1047
1042
|
// (if any) takes it over, keeping lowest-index-first reuse.
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1043
|
+
if (!statusDead) {
|
|
1044
|
+
const si = slots.findIndex((s) => s !== null && s.id === node.id)
|
|
1045
|
+
if (si >= 0) slots[si] = slotQueue.shift() ?? null
|
|
1046
|
+
else {
|
|
1047
|
+
const qi = slotQueue.findIndex((s) => s.id === node.id)
|
|
1048
|
+
if (qi >= 0) slotQueue.splice(qi, 1)
|
|
1049
|
+
}
|
|
1053
1050
|
}
|
|
1054
1051
|
const bucket = cacheBucketOf(outcome)
|
|
1055
1052
|
if (bucket !== undefined) {
|
|
1056
|
-
|
|
1057
|
-
buckets.set(
|
|
1053
|
+
const entry = { task: node.id, bucket }
|
|
1054
|
+
if (outcome.hash !== undefined) buckets.set(outcome.hash, entry)
|
|
1055
|
+
buckets.set(node.id, entry)
|
|
1058
1056
|
}
|
|
1059
1057
|
switch (outcome.status) {
|
|
1060
1058
|
case 'success':
|
|
@@ -1085,10 +1083,6 @@ export function defaultLogger(
|
|
|
1085
1083
|
}
|
|
1086
1084
|
if (isGroupTask(node)) return
|
|
1087
1085
|
if (holdsTerminal(node) && streamsLive(node) && outcome.status !== 'skipped') {
|
|
1088
|
-
if (streamMidLine) {
|
|
1089
|
-
taskWrite('\n')
|
|
1090
|
-
streamMidLine = false
|
|
1091
|
-
}
|
|
1092
1086
|
emitFrameClose(formatFrameClose(node, outcome, colors))
|
|
1093
1087
|
return
|
|
1094
1088
|
}
|
|
@@ -1100,13 +1094,12 @@ export function defaultLogger(
|
|
|
1100
1094
|
// The word carries the outcome (skips/failures included); the
|
|
1101
1095
|
// key is what this mode exists to show — absent for a task
|
|
1102
1096
|
// that never computed one (skipped, uncacheable).
|
|
1103
|
-
|
|
1104
|
-
emitLine(`${outcomeWord(outcome)} ${node.id}${key}`)
|
|
1097
|
+
emitLine(formatTaskHashLine(node, outcome, colors))
|
|
1105
1098
|
return
|
|
1106
1099
|
}
|
|
1107
1100
|
case 'errors-only':
|
|
1108
1101
|
if (outcome.status === 'skipped') {
|
|
1109
|
-
emitRow(formatTaskSkippedLine(node, colors, outcome
|
|
1102
|
+
emitRow(formatTaskSkippedLine(node, colors, outcome))
|
|
1110
1103
|
return
|
|
1111
1104
|
}
|
|
1112
1105
|
if (outcome.status !== 'failed') return
|
|
@@ -1145,7 +1138,7 @@ export function defaultLogger(
|
|
|
1145
1138
|
} else if (outcome.status === 'success') {
|
|
1146
1139
|
emitRow(formatTaskExecutedLine(node, outcome, colors, onPath.has(node.id)))
|
|
1147
1140
|
} else if (outcome.status === 'skipped') {
|
|
1148
|
-
emitRow(formatTaskSkippedLine(node, colors, outcome
|
|
1141
|
+
emitRow(formatTaskSkippedLine(node, colors, outcome))
|
|
1149
1142
|
}
|
|
1150
1143
|
return
|
|
1151
1144
|
case 'focused':
|
|
@@ -1154,7 +1147,7 @@ export function defaultLogger(
|
|
|
1154
1147
|
// stopped), so no frame-open fired — and a skip has no output,
|
|
1155
1148
|
// so a one-liner carries everything a frame would.
|
|
1156
1149
|
if (outcome.status === 'skipped') {
|
|
1157
|
-
emitRow(formatTaskSkippedLine(node, colors, outcome
|
|
1150
|
+
emitRow(formatTaskSkippedLine(node, colors, outcome))
|
|
1158
1151
|
return
|
|
1159
1152
|
}
|
|
1160
1153
|
if (streamsLive(node)) {
|
|
@@ -1163,10 +1156,6 @@ export function defaultLogger(
|
|
|
1163
1156
|
// this close — full task info for every outcome, cached
|
|
1164
1157
|
// and up-to-date included (owner: "always full frame
|
|
1165
1158
|
// for a single task").
|
|
1166
|
-
if (streamMidLine) {
|
|
1167
|
-
taskWrite('\n')
|
|
1168
|
-
streamMidLine = false
|
|
1169
|
-
}
|
|
1170
1159
|
emitFrameClose(formatFrameClose(node, outcome, colors))
|
|
1171
1160
|
return
|
|
1172
1161
|
}
|
|
@@ -1192,14 +1181,16 @@ export function defaultLogger(
|
|
|
1192
1181
|
// forceCommand: a requested task's frame shows `$ cmd`
|
|
1193
1182
|
// whether it ran or was cached — same frame every run.
|
|
1194
1183
|
emitBlock(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1184
|
+
fenced(
|
|
1185
|
+
formatTaskBlock(
|
|
1186
|
+
node,
|
|
1187
|
+
outcome,
|
|
1188
|
+
{ output, ...dropped },
|
|
1189
|
+
colors,
|
|
1190
|
+
true,
|
|
1191
|
+
forwardArgs,
|
|
1192
|
+
links,
|
|
1193
|
+
),
|
|
1203
1194
|
),
|
|
1204
1195
|
)
|
|
1205
1196
|
return
|
|
@@ -1207,7 +1198,7 @@ export function defaultLogger(
|
|
|
1207
1198
|
// Dependency-pulled nodes: silent on success; a skip gets its
|
|
1208
1199
|
// row, failures the ◼ row now and their frame replayed at runEnd.
|
|
1209
1200
|
if (outcome.status === 'skipped') {
|
|
1210
|
-
emitRow(formatTaskSkippedLine(node, colors, outcome
|
|
1201
|
+
emitRow(formatTaskSkippedLine(node, colors, outcome))
|
|
1211
1202
|
} else if (outcome.status === 'failed') {
|
|
1212
1203
|
emitRow(failureRow(node, outcome))
|
|
1213
1204
|
deferredFailures.push(
|
|
@@ -1235,7 +1226,7 @@ export function defaultLogger(
|
|
|
1235
1226
|
return
|
|
1236
1227
|
}
|
|
1237
1228
|
if (outcome.status === 'skipped') {
|
|
1238
|
-
emitRow(formatTaskSkippedLine(node, colors, outcome
|
|
1229
|
+
emitRow(formatTaskSkippedLine(node, colors, outcome))
|
|
1239
1230
|
return
|
|
1240
1231
|
}
|
|
1241
1232
|
const block = formatTaskBlock(
|