@vzn/vx 0.0.650 → 0.0.652
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 +7 -23
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/schemas/show.json +14 -20
- package/src/bin.ts +21 -31
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +221 -30
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +77 -14
- package/src/cache/layer.ts +17 -6
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +5 -3
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +56 -17
- package/src/cache/schema.ts +15 -2
- package/src/cli/cache.ts +28 -18
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +14 -25
- package/src/cli/index.ts +28 -9
- package/src/cli/info.ts +16 -8
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +246 -204
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +8 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +45 -52
- package/src/cli/task-verb.ts +22 -0
- package/src/cli/upgrade.ts +3 -1
- package/src/cli/watch-fs.ts +6 -1
- package/src/cli/watch-judge.ts +3 -2
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +94 -52
- package/src/cli/why.ts +123 -89
- package/src/cli/workspace-config.ts +2 -1
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +42 -2
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +58 -5
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/pipes.ts +152 -0
- package/src/exec/runner.ts +199 -182
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +317 -23
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/definition.ts +16 -3
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +17 -11
- package/src/graph/task-graph.ts +51 -24
- package/src/handoff.ts +10 -7
- package/src/history/failure-mode.ts +24 -1
- package/src/history/history.ts +5 -0
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +14 -6
- package/src/index.ts +7 -3
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +67 -20
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/live-usage.ts +16 -11
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +108 -29
- package/src/orchestrator/miss-save.ts +58 -11
- package/src/orchestrator/persistent-task.ts +5 -1
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +7 -3
- package/src/orchestrator/plugin-host.ts +67 -2
- package/src/orchestrator/plugin.ts +53 -2
- package/src/orchestrator/prepare.ts +16 -1
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +62 -13
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +88 -59
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +145 -41
- package/src/orchestrator/sandbox-request.ts +52 -9
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +21 -7
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +6 -14
- package/src/report/forecast.ts +10 -32
- package/src/report/framed-output.ts +283 -175
- package/src/report/index.ts +8 -3
- package/src/report/logger.ts +74 -39
- package/src/report/plain-output.ts +3 -1
- package/src/report/status-line.ts +49 -357
- package/src/report/summary.ts +105 -136
- package/src/settings.ts +8 -8
- package/src/ui/ansi.ts +48 -0
- package/src/ui/components.ts +506 -0
- package/src/ui/index.ts +79 -0
- package/src/ui/kit.ts +37 -0
- package/src/ui/live-region.ts +363 -0
- package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
- package/src/ui/terminal.ts +51 -0
- package/src/ui/theme.ts +224 -0
- package/src/ui/version-line.ts +28 -0
- package/src/util/config-frame.ts +191 -14
- package/src/util/edit-distance.ts +6 -1
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +21 -3
- package/src/util/secret-mask.ts +39 -3
- package/src/util/settle.ts +33 -1
- package/src/util/tail.ts +4 -1
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +70 -63
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +36 -1
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +16 -9
- package/src/workspace/workspace.ts +3 -0
- package/src/util/colors.ts +0 -133
- package/src/util/report.ts +0 -44
- package/src/util/ui.ts +0 -258
- /package/src/{report/path-links.ts → ui/links.ts} +0 -0
- /package/src/{report → ui}/terminal-signals.ts +0 -0
package/src/cli/lock.ts
CHANGED
|
@@ -8,10 +8,16 @@ import path from 'node:path'
|
|
|
8
8
|
import type { ProjectConfig } from '../config.js'
|
|
9
9
|
import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
|
|
10
10
|
import { buildTaskGraph, expandRequested, isDefaultBuild } from '../graph/index.js'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
applyGraphStage,
|
|
13
|
+
claimedDigests,
|
|
14
|
+
discoverProjects,
|
|
15
|
+
fingerprintClaims,
|
|
16
|
+
hasHook,
|
|
17
|
+
loadProjects,
|
|
18
|
+
} from '../orchestrator/index.js'
|
|
12
19
|
import { resolveSetting, runRules } from '../settings.js'
|
|
13
20
|
import {
|
|
14
|
-
refusalNotice,
|
|
15
21
|
DOT_VX_GITATTRIBUTES,
|
|
16
22
|
fsRefusalHint,
|
|
17
23
|
isFsRefusal,
|
|
@@ -23,11 +29,11 @@ import {
|
|
|
23
29
|
type UserError,
|
|
24
30
|
secretMask,
|
|
25
31
|
type SecretMask,
|
|
26
|
-
ui,
|
|
27
|
-
sayNotice,
|
|
28
32
|
refuseSymlink,
|
|
29
33
|
writeNoFollow,
|
|
30
34
|
} from '../util/index.js'
|
|
35
|
+
import * as ui from '../ui/index.js'
|
|
36
|
+
import { refusalNotice, sayNotice } from '../ui/index.js'
|
|
31
37
|
import {
|
|
32
38
|
ConfigsRefused,
|
|
33
39
|
findProjectConfigs,
|
|
@@ -43,6 +49,11 @@ import {
|
|
|
43
49
|
checkDependencyRules,
|
|
44
50
|
computeNestedProjectDirs,
|
|
45
51
|
writeLockfile,
|
|
52
|
+
DEPENDENCIES_LOCK_NAME,
|
|
53
|
+
dependenciesLock,
|
|
54
|
+
parseDependenciesLock,
|
|
55
|
+
writeDependenciesLock,
|
|
56
|
+
type DependenciesLock,
|
|
46
57
|
type Lockfile,
|
|
47
58
|
type ProjectMeta,
|
|
48
59
|
} from '../workspace/index.js'
|
|
@@ -140,6 +151,7 @@ async function audit(root: string, warn: (message: string) => void): Promise<Gra
|
|
|
140
151
|
reviewFiles: true,
|
|
141
152
|
strayReviews: [],
|
|
142
153
|
shadowed: [],
|
|
154
|
+
dependencies: null,
|
|
143
155
|
}
|
|
144
156
|
try {
|
|
145
157
|
const { workspaceConfig, plugins, cacheDir } = await loadCliWorkspace(root)
|
|
@@ -178,6 +190,7 @@ async function audit(root: string, warn: (message: string) => void): Promise<Gra
|
|
|
178
190
|
})
|
|
179
191
|
for (const entry of projects.values()) resolved.set(entry.name, entry.config)
|
|
180
192
|
out.loaded = true
|
|
193
|
+
out.dependencies = await dependenciesOf(root, out.all, plugins, cacheDir, warn)
|
|
181
194
|
checkBracketLiterals(root, projects.values())
|
|
182
195
|
const names = new Set<string>()
|
|
183
196
|
for (const entry of projects.values()) {
|
|
@@ -252,6 +265,30 @@ interface GraphCheck {
|
|
|
252
265
|
strayReviews: string[]
|
|
253
266
|
/** Config files a higher-precedence one beside them hides (`findProjectConfigs`). */
|
|
254
267
|
shadowed: { file: string; read: string }[]
|
|
268
|
+
/** What the dependencies resolve to here; null when the configs did not load. */
|
|
269
|
+
dependencies: DependenciesLock | null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Each project's edges and each claimed file's digests, as `--affected` reads them back. */
|
|
273
|
+
async function dependenciesOf(
|
|
274
|
+
root: string,
|
|
275
|
+
all: readonly ProjectMeta[],
|
|
276
|
+
plugins: Parameters<typeof fingerprintClaims>[0],
|
|
277
|
+
cacheDir: string,
|
|
278
|
+
warn: (message: string) => void,
|
|
279
|
+
): Promise<DependenciesLock> {
|
|
280
|
+
const ctx = {
|
|
281
|
+
workspaceRoot: root,
|
|
282
|
+
cacheDir,
|
|
283
|
+
warn,
|
|
284
|
+
projects: all.map((p) => ({ name: p.name, dir: p.dir })),
|
|
285
|
+
}
|
|
286
|
+
const files = new Map<string, NonNullable<Awaited<ReturnType<typeof claimedDigests>>>>()
|
|
287
|
+
for (const [file, plugin] of fingerprintClaims(plugins)) {
|
|
288
|
+
const answer = await claimedDigests(plugin, file, ctx)
|
|
289
|
+
if (answer !== undefined) files.set(file, answer)
|
|
290
|
+
}
|
|
291
|
+
return dependenciesLock(root, all, files)
|
|
255
292
|
}
|
|
256
293
|
|
|
257
294
|
function configured(all: readonly ProjectMeta[]): ConfiguredMeta[] {
|
|
@@ -415,6 +452,7 @@ async function writeLock(
|
|
|
415
452
|
return 1
|
|
416
453
|
}
|
|
417
454
|
await writeLockfile(root, check.fresh)
|
|
455
|
+
if (check.dependencies !== null) await writeDependenciesLock(root, check.dependencies)
|
|
418
456
|
await writeIfMissing(path.join(root, '.vx', '.gitattributes'), DOT_VX_GITATTRIBUTES)
|
|
419
457
|
const n = metas.length
|
|
420
458
|
say(`${n} project config${n === 1 ? '' : 's'} → ${LOCKFILE_NAME}${bareNote(bare)}`)
|
|
@@ -517,7 +555,7 @@ async function checkLock(root: string, json: boolean): Promise<number> {
|
|
|
517
555
|
notAudited: bare.length,
|
|
518
556
|
problems,
|
|
519
557
|
}
|
|
520
|
-
|
|
558
|
+
ui.data('out', `${JSON.stringify(out)}\n`)
|
|
521
559
|
}
|
|
522
560
|
return problems.length === 0 ? 0 : 1
|
|
523
561
|
}
|
|
@@ -600,6 +638,18 @@ async function checkLock(root: string, json: boolean): Promise<number> {
|
|
|
600
638
|
failures.push(`the lock holds ${what}, which has no vx.config now — run 'vx lock'`)
|
|
601
639
|
}
|
|
602
640
|
}
|
|
641
|
+
// Named only over a lock that holds: a config drift moves the record too.
|
|
642
|
+
if (check.dependencies !== null && failures.length === 0) {
|
|
643
|
+
const text = await Bun.file(path.join(root, DEPENDENCIES_LOCK_NAME))
|
|
644
|
+
.bytes()
|
|
645
|
+
.catch(() => null)
|
|
646
|
+
// Compared as JSON: a formatter may lay the file out its own way.
|
|
647
|
+
const read = parseDependenciesLock(text)
|
|
648
|
+
if (read === null || !Bun.deepEquals(read, check.dependencies, true))
|
|
649
|
+
failures.push(
|
|
650
|
+
`${DEPENDENCIES_LOCK_NAME} ${text === null ? 'is missing' : 'is not what the dependencies resolve to here'} — run 'vx lock'`,
|
|
651
|
+
)
|
|
652
|
+
}
|
|
603
653
|
if (json)
|
|
604
654
|
// Root-relative, as every other problem: the loader names the absolute path.
|
|
605
655
|
return answer([
|
package/src/cli/plan-format.ts
CHANGED
|
@@ -1,45 +1,37 @@
|
|
|
1
1
|
// Formatters for `--dry` (human / JSON).
|
|
2
2
|
|
|
3
3
|
import type { AffectedReason, CacheStatus, RunPlan } from '../orchestrator/index.js'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
paint,
|
|
10
|
-
SUCCESS,
|
|
11
|
-
ui,
|
|
12
|
-
WARN,
|
|
13
|
-
type ColorSupport,
|
|
14
|
-
} from '../util/index.js'
|
|
4
|
+
import { computeReverseDepCount } from '../graph/index.js'
|
|
5
|
+
import { planList, type PlannedRow, skippedReason } from '../report/index.js'
|
|
6
|
+
import { formatBytes, formatElapsed, maskedCommand } from '../util/index.js'
|
|
7
|
+
import * as ui from '../ui/index.js'
|
|
8
|
+
import type { ColorSupport } from '../ui/index.js'
|
|
15
9
|
|
|
16
10
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
11
|
+
* `vx run --dry`'s screen: the `Plan` panel, each task on the running
|
|
12
|
+
* list's grid (what the run would do with it, its usual time, where it
|
|
13
|
+
* would run), then the counts and the predicted time.
|
|
20
14
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
15
|
+
* ─ Plan 3 ─────────────────────────────────────────────────────
|
|
16
|
+
* time usual status cache cpu mem outputs blocks worker task
|
|
17
|
+
* ⇢ cached local local @vzn/vx#lint
|
|
18
|
+
* ⇣ cached remote 12MB local @vzn/vx#test
|
|
19
|
+
* ⏺︎ ~1.2s run miss local @vzn/vx#build
|
|
25
20
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
21
|
+
* tasks 3 planned · 2 cache hits (1 local, 1 remote, 12MB) · 1 would run
|
|
22
|
+
* predicted ~1.20s wall · ~1.20s total execution
|
|
28
23
|
*/
|
|
29
|
-
export function
|
|
24
|
+
export function planReport(plan: RunPlan): ui.Block {
|
|
30
25
|
// A group the platform skip reaches is still a group: it runs nothing.
|
|
31
26
|
const real = plan.tasks.filter(
|
|
32
27
|
(t) =>
|
|
33
28
|
t.cacheStatus !== 'group' &&
|
|
34
29
|
(t.cacheStatus !== 'skipped' || t.node.config.exec !== undefined),
|
|
35
30
|
)
|
|
36
|
-
|
|
37
|
-
if (real.length === 0) return `${dim('No tasks planned.')}\n`
|
|
38
|
-
|
|
39
|
-
const idWidth = Math.max(...real.map((t) => t.node.id.length))
|
|
40
|
-
const tagWidth = 24
|
|
31
|
+
if (real.length === 0) return ui.section('Plan', ui.text('No tasks planned.'))
|
|
41
32
|
|
|
42
|
-
const
|
|
33
|
+
const blocks = downstream(plan)
|
|
34
|
+
const rows: PlannedRow[] = []
|
|
43
35
|
let local = 0
|
|
44
36
|
let remote = 0
|
|
45
37
|
let miss = 0
|
|
@@ -50,33 +42,28 @@ export function formatPlanText(plan: RunPlan, c: ColorSupport = { enabled: false
|
|
|
50
42
|
for (const t of real) {
|
|
51
43
|
// `@noop`: the run succeeds it without running, whatever its key says.
|
|
52
44
|
const skipped = t.executor === 'noop' && t.cacheStatus !== 'skipped'
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
: describe(t.cacheStatus)
|
|
59
|
-
const shortHash = t.hash ? t.hash.slice(0, 8) : ''
|
|
45
|
+
const notes: string[] = []
|
|
46
|
+
if (t.cacheStatus === 'skipped') notes.push(skippedReason(t))
|
|
47
|
+
const desc = shownDescription(t)
|
|
48
|
+
if (desc !== undefined && desc !== '') notes.push(desc)
|
|
49
|
+
if (t.affected !== undefined) notes.push(`affected: ${describeReason(t.affected)}`)
|
|
60
50
|
const executes = !skipped && (t.cacheStatus === 'miss' || t.cacheStatus === 'no-cache')
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
if (t.affected !== undefined) {
|
|
78
|
-
lines.push(` ${' '.repeat(idWidth)} ${dim(`affected: ${describeReason(t.affected)}`)}`)
|
|
79
|
-
}
|
|
51
|
+
rows.push({
|
|
52
|
+
node: t.node,
|
|
53
|
+
...(skipped ? NOOP : LOOK[t.cacheStatus]),
|
|
54
|
+
...(executes && t.p50Ms !== undefined ? { usualMs: t.p50Ms } : {}),
|
|
55
|
+
...(t.cacheStatus === 'hit-remote' && t.remoteBytes !== undefined
|
|
56
|
+
? { outputBytes: t.remoteBytes }
|
|
57
|
+
: {}),
|
|
58
|
+
blocks: blocks.get(t.node.id) ?? 0,
|
|
59
|
+
// Placement by executor name; a task that will not run reaches no
|
|
60
|
+
// machine, as a skip's row in the run has none.
|
|
61
|
+
worker:
|
|
62
|
+
skipped || t.cacheStatus === 'skipped' || t.cacheStatus === 'not-started'
|
|
63
|
+
? ''
|
|
64
|
+
: (t.executor ?? 'local'),
|
|
65
|
+
note: notes.join(' · '),
|
|
66
|
+
})
|
|
80
67
|
if (skipped) noop++
|
|
81
68
|
else if (t.cacheStatus === 'hit-local') local++
|
|
82
69
|
else if (t.cacheStatus === 'hit-remote') remote++
|
|
@@ -94,15 +81,14 @@ export function formatPlanText(plan: RunPlan, c: ColorSupport = { enabled: false
|
|
|
94
81
|
hitParts.push(bytes > 0 ? `${remote} remote, ${formatBytes(bytes)}` : `${remote} remote`)
|
|
95
82
|
}
|
|
96
83
|
const hits = local + remote
|
|
97
|
-
const summary: string[] = [`${real.length}
|
|
84
|
+
const summary: string[] = [`${real.length} planned`]
|
|
98
85
|
if (hits > 0) summary.push(`${hits} cache hit${hits === 1 ? '' : 's'} (${hitParts.join(', ')})`)
|
|
99
86
|
if (miss > 0) summary.push(`${miss} would run`)
|
|
100
87
|
if (nocache > 0) summary.push(`${nocache} no-cache`)
|
|
101
88
|
if (noop > 0) summary.push(`${noop} noop`)
|
|
102
89
|
if (idle > 0) summary.push(`${idle} not started`)
|
|
103
90
|
if (platform > 0) summary.push(`${platform} skipped`)
|
|
104
|
-
|
|
105
|
-
lines.push(dim(summary.join(', ') + '.'))
|
|
91
|
+
const facts: ui.FieldRow[] = [['tasks', summary.join(' · ')]]
|
|
106
92
|
|
|
107
93
|
// `--cache-download` is invisible without this: the eligibility gate silently
|
|
108
94
|
// keeps producers eager, so a user who asked for `none` and got no
|
|
@@ -111,16 +97,13 @@ export function formatPlanText(plan: RunPlan, c: ColorSupport = { enabled: false
|
|
|
111
97
|
const deferred = real.filter((t) => t.download === 'deferred')
|
|
112
98
|
const downgrades = plan.downloadDowngrades ?? []
|
|
113
99
|
if (deferred.length > 0 || downgrades.length > 0) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
),
|
|
120
|
-
)
|
|
121
|
-
for (const d of downgrades.slice(0, 3))
|
|
122
|
-
lines.push(` ${ui.inline(ui.id(d.taskId), c)} ${dim(`— ${d.reason}`)}`)
|
|
123
|
-
if (downgrades.length > 3) lines.push(dim(` …and ${downgrades.length - 3} more`))
|
|
100
|
+
facts.push([
|
|
101
|
+
'download',
|
|
102
|
+
`${deferred.length} task(s) would keep outputs remote` +
|
|
103
|
+
(downgrades.length > 0 ? `, ${downgrades.length} kept eager:` : '.'),
|
|
104
|
+
...downgrades.slice(0, 3).map((d) => [ui.id(d.taskId), ui.dim(` — ${d.reason}`)]),
|
|
105
|
+
...(downgrades.length > 3 ? [`…and ${downgrades.length - 3} more`] : []),
|
|
106
|
+
])
|
|
124
107
|
}
|
|
125
108
|
|
|
126
109
|
// Time prediction — only when history gave us something to say: at least
|
|
@@ -128,15 +111,63 @@ export function formatPlanText(plan: RunPlan, c: ColorSupport = { enabled: false
|
|
|
128
111
|
const p = plan.predicted
|
|
129
112
|
const executes = miss + nocache
|
|
130
113
|
if (p !== undefined && executes > 0 && executes > p.unknownCount) {
|
|
131
|
-
const parts = [
|
|
114
|
+
const parts = [`~${formatElapsed(p.wallMs)} wall`]
|
|
132
115
|
parts.push(`~${formatElapsed(p.workMs)} total execution`)
|
|
133
116
|
if (p.unknownCount > 0) {
|
|
134
117
|
parts.push(`${p.unknownCount} task${p.unknownCount === 1 ? '' : 's'} without history (+?)`)
|
|
135
118
|
}
|
|
136
|
-
|
|
119
|
+
facts.push(['predicted', parts.join(' · ')])
|
|
137
120
|
}
|
|
138
|
-
return
|
|
121
|
+
return ui.section('Plan', String(real.length), planList(rows), ui.fields(facts))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* How many tasks are downstream of each, all of them (groups too), by the
|
|
126
|
+
* run's own count: a closure walked per task was quadratic, and a 10,000
|
|
127
|
+
* task chain's `--dry` spent 7 s here.
|
|
128
|
+
*/
|
|
129
|
+
function downstream(plan: RunPlan): Map<string, number> {
|
|
130
|
+
return computeReverseDepCount(
|
|
131
|
+
new Map(plan.tasks.map((t) => [t.node.id, { ...t.node, deps: [...t.deps] }])),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The plan as plain text: what a pipe, a test or `grep` reads. */
|
|
136
|
+
export function formatPlanText(plan: RunPlan, c: ColorSupport = { enabled: false }): string {
|
|
137
|
+
return ui.render([planReport(plan)], c)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type Look = Pick<PlannedRow, 'glyph' | 'status' | 'tone' | 'cache' | 'cacheTone'>
|
|
141
|
+
|
|
142
|
+
/** Each outcome on the run's grid: its glyph and words, as the run would print them. */
|
|
143
|
+
const LOOK: Record<CacheStatus, Look> = {
|
|
144
|
+
'hit-local': {
|
|
145
|
+
glyph: '\u21e2',
|
|
146
|
+
status: 'cached',
|
|
147
|
+
tone: 'ok',
|
|
148
|
+
cache: 'local',
|
|
149
|
+
cacheTone: 'local',
|
|
150
|
+
},
|
|
151
|
+
'hit-remote': {
|
|
152
|
+
glyph: '\u21e3',
|
|
153
|
+
status: 'cached',
|
|
154
|
+
tone: 'ok',
|
|
155
|
+
cache: 'remote',
|
|
156
|
+
cacheTone: 'remote',
|
|
157
|
+
},
|
|
158
|
+
miss: { glyph: '\u23fa\ufe0e', status: 'run', tone: 'info', cache: 'miss', cacheTone: 'muted' },
|
|
159
|
+
'no-cache': {
|
|
160
|
+
glyph: '\u23fa\ufe0e',
|
|
161
|
+
status: 'run',
|
|
162
|
+
tone: 'info',
|
|
163
|
+
cache: 'no-cache',
|
|
164
|
+
cacheTone: 'muted',
|
|
165
|
+
},
|
|
166
|
+
group: { glyph: '\u25cb', status: 'group', tone: 'muted', cache: '', cacheTone: '' },
|
|
167
|
+
'not-started': { glyph: '\u25cc', status: 'not run', tone: 'muted', cache: '', cacheTone: '' },
|
|
168
|
+
skipped: { glyph: '\u2298', status: 'skipped', tone: 'warn', cache: '', cacheTone: '' },
|
|
139
169
|
}
|
|
170
|
+
const NOOP: Look = { glyph: '\u00b7', status: 'noop', tone: 'muted', cache: '', cacheTone: '' }
|
|
140
171
|
|
|
141
172
|
/** One `--affected --dry` reason line: the cause, then the edge chain that carried it. */
|
|
142
173
|
function describeReason(r: AffectedReason): string {
|
|
@@ -219,52 +250,3 @@ export function formatPlanJson(
|
|
|
219
250
|
}) + '\n'
|
|
220
251
|
)
|
|
221
252
|
}
|
|
222
|
-
|
|
223
|
-
/** A hit is green, a task that would run is yellow, the rest dim. */
|
|
224
|
-
const HUE: Record<CacheStatus, string> = {
|
|
225
|
-
'hit-local': SUCCESS,
|
|
226
|
-
'hit-remote': SUCCESS,
|
|
227
|
-
miss: WARN,
|
|
228
|
-
'no-cache': WARN,
|
|
229
|
-
group: '',
|
|
230
|
-
'not-started': '',
|
|
231
|
-
skipped: '',
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function symbolFor(s: CacheStatus): string {
|
|
235
|
-
switch (s) {
|
|
236
|
-
case 'hit-local':
|
|
237
|
-
return '✓'
|
|
238
|
-
case 'hit-remote':
|
|
239
|
-
return '↓'
|
|
240
|
-
case 'miss':
|
|
241
|
-
return '▶'
|
|
242
|
-
case 'no-cache':
|
|
243
|
-
return '∅'
|
|
244
|
-
case 'group':
|
|
245
|
-
return '○'
|
|
246
|
-
case 'not-started':
|
|
247
|
-
return '−'
|
|
248
|
-
case 'skipped':
|
|
249
|
-
return '·'
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function describe(s: CacheStatus): string {
|
|
254
|
-
switch (s) {
|
|
255
|
-
case 'hit-local':
|
|
256
|
-
return 'cache hit (local)'
|
|
257
|
-
case 'hit-remote':
|
|
258
|
-
return 'cache hit (remote)'
|
|
259
|
-
case 'miss':
|
|
260
|
-
return 'cache miss — would exec'
|
|
261
|
-
case 'no-cache':
|
|
262
|
-
return 'no-cache (would exec)'
|
|
263
|
-
case 'group':
|
|
264
|
-
return 'group task'
|
|
265
|
-
case 'not-started':
|
|
266
|
-
return 'not started (unneeded)'
|
|
267
|
-
case 'skipped':
|
|
268
|
-
return 'skipped'
|
|
269
|
-
}
|
|
270
|
-
}
|
|
@@ -24,9 +24,9 @@ import {
|
|
|
24
24
|
maskedLine,
|
|
25
25
|
SecretBroker,
|
|
26
26
|
thrownMessage,
|
|
27
|
-
ui,
|
|
28
27
|
UserError,
|
|
29
28
|
} from '../util/index.js'
|
|
29
|
+
import * as ui from '../ui/index.js'
|
|
30
30
|
import { resolveSetting } from '../settings.js'
|
|
31
31
|
|
|
32
32
|
export interface ResolvedPluginCommand {
|
|
@@ -284,7 +284,12 @@ async function openVerbRemotes(
|
|
|
284
284
|
await open(remotes, ci, new SecretBroker(ci, warn))
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
/**
|
|
288
|
+
* `Required`, so a member `TaskArtifacts` gains cannot be left out here:
|
|
289
|
+
* this wrapper dropped `uploaded`, and `vx dte worker` took every upload a
|
|
290
|
+
* remote reset as landed (CI, 2026-10-29).
|
|
291
|
+
*/
|
|
292
|
+
function lazyArtifacts(open: () => Promise<TaskArtifacts>): Required<TaskArtifacts> {
|
|
288
293
|
let opened: Promise<TaskArtifacts> | undefined
|
|
289
294
|
const artifacts = (): Promise<TaskArtifacts> => (opened ??= open())
|
|
290
295
|
return {
|
|
@@ -293,6 +298,7 @@ function lazyArtifacts(open: () => Promise<TaskArtifacts>): TaskArtifacts {
|
|
|
293
298
|
output: async (key) => (await artifacts()).output(key),
|
|
294
299
|
share: async (key) => (await artifacts()).share(key),
|
|
295
300
|
resend: async (key) => (await artifacts()).resend(key),
|
|
301
|
+
uploaded: async (key) => (await artifacts()).uploaded?.(key),
|
|
296
302
|
}
|
|
297
303
|
}
|
|
298
304
|
|