@vzn/vx 0.0.645 → 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 +39 -50
- 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 +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -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/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- 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 +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- 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 +326 -65
- 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 +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- 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 +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- 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 +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- 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 +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- 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 +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- 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 +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- 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 +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- 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 +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -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 +65 -4
- 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 +282 -0
- 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 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
package/src/cli/plan-format.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// Formatters for `--dry` (human / JSON).
|
|
2
2
|
|
|
3
3
|
import type { AffectedReason, CacheStatus, RunPlan } from '../orchestrator/index.js'
|
|
4
|
-
import {
|
|
4
|
+
import { skippedReason } from '../report/index.js'
|
|
5
|
+
import {
|
|
6
|
+
formatBytes,
|
|
7
|
+
formatElapsed,
|
|
8
|
+
maskedCommand,
|
|
9
|
+
paint,
|
|
10
|
+
SUCCESS,
|
|
11
|
+
ui,
|
|
12
|
+
WARN,
|
|
13
|
+
type ColorSupport,
|
|
14
|
+
} from '../util/index.js'
|
|
5
15
|
|
|
6
16
|
/**
|
|
7
17
|
* Human-readable preview. One line per real task (groups hidden, same
|
|
@@ -16,25 +26,36 @@ import { formatBytes, formatElapsed, maskedCommand } from '../util/index.js'
|
|
|
16
26
|
* 3 task(s) planned, 2 cache hits (1 local, 1 remote), 1 would run.
|
|
17
27
|
* predicted: ~1.20s wall · ~1.20s total execution
|
|
18
28
|
*/
|
|
19
|
-
export function formatPlanText(plan: RunPlan): string {
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
export function formatPlanText(plan: RunPlan, c: ColorSupport = { enabled: false }): string {
|
|
30
|
+
// A group the platform skip reaches is still a group: it runs nothing.
|
|
31
|
+
const real = plan.tasks.filter(
|
|
32
|
+
(t) =>
|
|
33
|
+
t.cacheStatus !== 'group' &&
|
|
34
|
+
(t.cacheStatus !== 'skipped' || t.node.config.exec !== undefined),
|
|
35
|
+
)
|
|
36
|
+
const dim = (s: string): string => paint('', s, c, { dim: true })
|
|
37
|
+
if (real.length === 0) return `${dim('No tasks planned.')}\n`
|
|
22
38
|
|
|
23
39
|
const idWidth = Math.max(...real.map((t) => t.node.id.length))
|
|
24
40
|
const tagWidth = 24
|
|
25
41
|
|
|
26
|
-
const lines: string[] = ['would run:']
|
|
42
|
+
const lines: string[] = [paint('', 'would run:', c, { bold: true })]
|
|
27
43
|
let local = 0
|
|
28
44
|
let remote = 0
|
|
29
45
|
let miss = 0
|
|
30
46
|
let nocache = 0
|
|
31
47
|
let noop = 0
|
|
32
48
|
let idle = 0
|
|
49
|
+
let platform = 0
|
|
33
50
|
for (const t of real) {
|
|
34
51
|
// `@noop`: the run succeeds it without running, whatever its key says.
|
|
35
|
-
const skipped = t.executor === 'noop'
|
|
52
|
+
const skipped = t.executor === 'noop' && t.cacheStatus !== 'skipped'
|
|
36
53
|
const sym = skipped ? '·' : symbolFor(t.cacheStatus)
|
|
37
|
-
const desc = skipped
|
|
54
|
+
const desc = skipped
|
|
55
|
+
? 'noop — would not run'
|
|
56
|
+
: t.cacheStatus === 'skipped'
|
|
57
|
+
? `skipped — ${skippedReason(t)}`
|
|
58
|
+
: describe(t.cacheStatus)
|
|
38
59
|
const shortHash = t.hash ? t.hash.slice(0, 8) : ''
|
|
39
60
|
const executes = !skipped && (t.cacheStatus === 'miss' || t.cacheStatus === 'no-cache')
|
|
40
61
|
const eta = executes && t.p50Ms !== undefined ? ` ~${formatElapsed(t.p50Ms)}` : ''
|
|
@@ -43,17 +64,18 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
43
64
|
// tier, and a task placed on a named executor is what the reader can
|
|
44
65
|
// act on. Present only when the workspace declared a choice.
|
|
45
66
|
const where = t.executor !== undefined ? ` @${t.executor}` : ''
|
|
67
|
+
const hue = skipped ? '' : HUE[t.cacheStatus]
|
|
46
68
|
lines.push(
|
|
47
|
-
` ${sym} ${t.node.id.
|
|
69
|
+
` ${paint(hue, sym, c, { dim: hue === '' })} ${ui.inline(ui.id(t.node.id), c)}${' '.repeat(idWidth - t.node.id.length)} ${paint(hue, desc.padEnd(tagWidth), c, { dim: hue === '' })} ${dim(shortHash)}${dim(eta)}${where}`,
|
|
48
70
|
)
|
|
49
71
|
// Optional one-line description from the task config, indented
|
|
50
72
|
// under the id so the eye picks up the task → blurb mapping.
|
|
51
73
|
const taskDesc = shownDescription(t)
|
|
52
74
|
if (taskDesc) {
|
|
53
|
-
lines.push(` ${' '.repeat(idWidth)} ${taskDesc}`)
|
|
75
|
+
lines.push(` ${' '.repeat(idWidth)} ${dim(taskDesc)}`)
|
|
54
76
|
}
|
|
55
77
|
if (t.affected !== undefined) {
|
|
56
|
-
lines.push(` ${' '.repeat(idWidth)} affected: ${describeReason(t.affected)}`)
|
|
78
|
+
lines.push(` ${' '.repeat(idWidth)} ${dim(`affected: ${describeReason(t.affected)}`)}`)
|
|
57
79
|
}
|
|
58
80
|
if (skipped) noop++
|
|
59
81
|
else if (t.cacheStatus === 'hit-local') local++
|
|
@@ -61,6 +83,7 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
61
83
|
else if (t.cacheStatus === 'miss') miss++
|
|
62
84
|
else if (t.cacheStatus === 'no-cache') nocache++
|
|
63
85
|
else if (t.cacheStatus === 'not-started') idle++
|
|
86
|
+
else if (t.cacheStatus === 'skipped') platform++
|
|
64
87
|
}
|
|
65
88
|
|
|
66
89
|
const hitParts: string[] = []
|
|
@@ -77,8 +100,9 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
77
100
|
if (nocache > 0) summary.push(`${nocache} no-cache`)
|
|
78
101
|
if (noop > 0) summary.push(`${noop} noop`)
|
|
79
102
|
if (idle > 0) summary.push(`${idle} not started`)
|
|
103
|
+
if (platform > 0) summary.push(`${platform} skipped`)
|
|
80
104
|
lines.push('')
|
|
81
|
-
lines.push(summary.join(', ') + '.')
|
|
105
|
+
lines.push(dim(summary.join(', ') + '.'))
|
|
82
106
|
|
|
83
107
|
// `--cache-download` is invisible without this: the eligibility gate silently
|
|
84
108
|
// keeps producers eager, so a user who asked for `none` and got no
|
|
@@ -89,11 +113,14 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
89
113
|
if (deferred.length > 0 || downgrades.length > 0) {
|
|
90
114
|
lines.push('')
|
|
91
115
|
lines.push(
|
|
92
|
-
|
|
93
|
-
|
|
116
|
+
dim(
|
|
117
|
+
`download: ${deferred.length} task(s) would keep outputs remote` +
|
|
118
|
+
(downgrades.length > 0 ? `, ${downgrades.length} kept eager:` : '.'),
|
|
119
|
+
),
|
|
94
120
|
)
|
|
95
|
-
for (const d of downgrades.slice(0, 3))
|
|
96
|
-
|
|
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`))
|
|
97
124
|
}
|
|
98
125
|
|
|
99
126
|
// Time prediction — only when history gave us something to say: at least
|
|
@@ -106,7 +133,7 @@ export function formatPlanText(plan: RunPlan): string {
|
|
|
106
133
|
if (p.unknownCount > 0) {
|
|
107
134
|
parts.push(`${p.unknownCount} task${p.unknownCount === 1 ? '' : 's'} without history (+?)`)
|
|
108
135
|
}
|
|
109
|
-
lines.push(parts.join(' · '))
|
|
136
|
+
lines.push(dim(parts.join(' · ')))
|
|
110
137
|
}
|
|
111
138
|
return lines.join('\n') + '\n'
|
|
112
139
|
}
|
|
@@ -149,6 +176,8 @@ export interface PlanTaskJson {
|
|
|
149
176
|
download?: 'deferred'
|
|
150
177
|
description?: string
|
|
151
178
|
affected?: AffectedReason
|
|
179
|
+
otherPlatform?: { readonly needs: readonly string[]; readonly host: string }
|
|
180
|
+
blockedBy?: string
|
|
152
181
|
}
|
|
153
182
|
|
|
154
183
|
/** `affectedBase`: the ref a bare `--affected` guessed (or was given), so a reader knows what the diff was against. */
|
|
@@ -157,42 +186,51 @@ export function formatPlanJson(
|
|
|
157
186
|
affectedBase?: string,
|
|
158
187
|
): string {
|
|
159
188
|
return (
|
|
160
|
-
JSON.stringify(
|
|
161
|
-
{
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
2,
|
|
192
|
-
) + '\n'
|
|
189
|
+
JSON.stringify({
|
|
190
|
+
...(affectedBase !== undefined ? { affectedBase } : {}),
|
|
191
|
+
tasks: plan.tasks.map((t): PlanTaskJson => {
|
|
192
|
+
const description = shownDescription(t)
|
|
193
|
+
return {
|
|
194
|
+
id: t.node.id,
|
|
195
|
+
project: t.node.projectName,
|
|
196
|
+
task: t.node.taskName,
|
|
197
|
+
hash: t.hash,
|
|
198
|
+
cacheStatus: t.cacheStatus,
|
|
199
|
+
deps: t.deps,
|
|
200
|
+
...(t.p50Ms !== undefined ? { p50Ms: t.p50Ms } : {}),
|
|
201
|
+
...(t.executor !== undefined ? { executor: t.executor } : {}),
|
|
202
|
+
...(t.download !== undefined ? { download: t.download } : {}),
|
|
203
|
+
...(description !== undefined ? { description } : {}),
|
|
204
|
+
...(t.affected !== undefined ? { affected: t.affected } : {}),
|
|
205
|
+
...(t.otherPlatform !== undefined ? { otherPlatform: t.otherPlatform } : {}),
|
|
206
|
+
...(t.blockedBy !== undefined ? { blockedBy: t.blockedBy } : {}),
|
|
207
|
+
}
|
|
208
|
+
}),
|
|
209
|
+
...(plan.predicted !== undefined ? { predicted: plan.predicted } : {}),
|
|
210
|
+
// The gate's refusals belong on the SCRIPTING surface too: a CI job
|
|
211
|
+
// asking "did --cache-download=none actually defer anything, and if not
|
|
212
|
+
// why" reads this, not the human table. (This object enumerates its
|
|
213
|
+
// fields deliberately — the plan's internal shape is not the wire —
|
|
214
|
+
// which is exactly why a new PlannedTask field does not appear here
|
|
215
|
+
// for free, and why `download` above had to be added by hand.)
|
|
216
|
+
...(plan.downloadDowngrades !== undefined
|
|
217
|
+
? { downloadDowngrades: plan.downloadDowngrades }
|
|
218
|
+
: {}),
|
|
219
|
+
}) + '\n'
|
|
193
220
|
)
|
|
194
221
|
}
|
|
195
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
|
+
|
|
196
234
|
function symbolFor(s: CacheStatus): string {
|
|
197
235
|
switch (s) {
|
|
198
236
|
case 'hit-local':
|
|
@@ -207,6 +245,8 @@ function symbolFor(s: CacheStatus): string {
|
|
|
207
245
|
return '○'
|
|
208
246
|
case 'not-started':
|
|
209
247
|
return '−'
|
|
248
|
+
case 'skipped':
|
|
249
|
+
return '·'
|
|
210
250
|
}
|
|
211
251
|
}
|
|
212
252
|
|
|
@@ -224,5 +264,7 @@ function describe(s: CacheStatus): string {
|
|
|
224
264
|
return 'group task'
|
|
225
265
|
case 'not-started':
|
|
226
266
|
return 'not started (unneeded)'
|
|
267
|
+
case 'skipped':
|
|
268
|
+
return 'skipped'
|
|
227
269
|
}
|
|
228
270
|
}
|
|
@@ -9,9 +9,23 @@
|
|
|
9
9
|
|
|
10
10
|
import type { CachePolicy, PluginCache, ResolvedRemote } from '../cache/index.js'
|
|
11
11
|
import type { WorkspaceConfig } from '../config.js'
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
BranchFacts,
|
|
14
|
+
VxPlugin,
|
|
15
|
+
PluginCommand,
|
|
16
|
+
CommandContext,
|
|
17
|
+
CacheFacts,
|
|
18
|
+
TaskArtifacts,
|
|
19
|
+
} from '../orchestrator/index.js'
|
|
13
20
|
import { configuredBaseBranch, findWorkspaceRoot } from '../workspace/index.js'
|
|
14
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
machineParallelism,
|
|
23
|
+
maskedLine,
|
|
24
|
+
SecretBroker,
|
|
25
|
+
thrownMessage,
|
|
26
|
+
ui,
|
|
27
|
+
UserError,
|
|
28
|
+
} from '../util/index.js'
|
|
15
29
|
import { resolveSetting } from '../settings.js'
|
|
16
30
|
|
|
17
31
|
export interface ResolvedPluginCommand {
|
|
@@ -45,7 +59,7 @@ async function workspacePlugins(cwd: string): Promise<
|
|
|
45
59
|
| {
|
|
46
60
|
workspaceRoot: string
|
|
47
61
|
cacheDir: string
|
|
48
|
-
storeRoot: Promise<string | null>
|
|
62
|
+
storeRoot: () => Promise<string | null>
|
|
49
63
|
workspaceConfig: WorkspaceConfig | null
|
|
50
64
|
concurrency: number
|
|
51
65
|
plugins: readonly VxPlugin[]
|
|
@@ -65,7 +79,7 @@ async function workspacePlugins(cwd: string): Promise<
|
|
|
65
79
|
const { loadCliWorkspace } = await import('./workspace-config.js')
|
|
66
80
|
ws = await loadCliWorkspace(workspaceRoot)
|
|
67
81
|
} catch (err) {
|
|
68
|
-
return { loadError:
|
|
82
|
+
return { loadError: thrownMessage(err) }
|
|
69
83
|
}
|
|
70
84
|
return {
|
|
71
85
|
workspaceRoot,
|
|
@@ -97,8 +111,10 @@ export async function resolvePluginCommand(
|
|
|
97
111
|
const declaredVerbs = new Set<string>()
|
|
98
112
|
for (const plugin of ws.plugins) {
|
|
99
113
|
for (const declared of Object.keys(plugin.commands ?? {})) declaredVerbs.add(declared)
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
// Own keys only: `vx toString` found Object.prototype's and crashed.
|
|
115
|
+
const commands = plugin.commands
|
|
116
|
+
if (commands === undefined || !Object.hasOwn(commands, verb)) continue
|
|
117
|
+
const command = commands[verb]!
|
|
102
118
|
return {
|
|
103
119
|
plugin,
|
|
104
120
|
command,
|
|
@@ -111,6 +127,8 @@ export async function resolvePluginCommand(
|
|
|
111
127
|
: [process.execPath, Bun.main],
|
|
112
128
|
warn,
|
|
113
129
|
cache: lazyCache(() => verbCache(ws, plugin.name)),
|
|
130
|
+
artifacts: lazyArtifacts(() => verbArtifacts(ws)),
|
|
131
|
+
artifactsFor: (facts) => lazyArtifacts(() => verbArtifacts(ws, facts)),
|
|
114
132
|
},
|
|
115
133
|
}
|
|
116
134
|
}
|
|
@@ -118,7 +136,7 @@ export async function resolvePluginCommand(
|
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
const warn = (m: string): void => {
|
|
121
|
-
|
|
139
|
+
ui.say('warn', maskedLine(m))
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
/** Read-only: a verb reads what runs stored, and only a run's policy may write. */
|
|
@@ -138,7 +156,7 @@ async function verbCache(
|
|
|
138
156
|
ws: Exclude<Awaited<ReturnType<typeof workspacePlugins>>, UnresolvedPluginCommand | null>,
|
|
139
157
|
plugin: string,
|
|
140
158
|
): Promise<PluginCache> {
|
|
141
|
-
const [{ Cache, pluginCache }, { cacheRefs, resolveCache, resolveCacheScope }] =
|
|
159
|
+
const [{ Cache, pluginCache }, { cacheRefs, openRemotes, resolveCache, resolveCacheScope }] =
|
|
142
160
|
await Promise.all([import('../cache/index.js'), import('../orchestrator/index.js')])
|
|
143
161
|
const scope = resolveCacheScope(
|
|
144
162
|
ws.workspaceRoot,
|
|
@@ -152,7 +170,7 @@ async function verbCache(
|
|
|
152
170
|
ws.workspaceRoot,
|
|
153
171
|
undefined,
|
|
154
172
|
'open',
|
|
155
|
-
await ws.storeRoot,
|
|
173
|
+
await ws.storeRoot(),
|
|
156
174
|
)
|
|
157
175
|
const remotes: ResolvedRemote[] = []
|
|
158
176
|
await resolveCache(ws.plugins, {
|
|
@@ -160,14 +178,123 @@ async function verbCache(
|
|
|
160
178
|
cacheDir: ws.cacheDir,
|
|
161
179
|
warn,
|
|
162
180
|
scope,
|
|
163
|
-
refs: cacheRefs(ws.workspaceRoot, configuredBaseBranch(ws.workspaceConfig)),
|
|
181
|
+
refs: cacheRefs(ws.workspaceRoot, configuredBaseBranch(ws.workspaceConfig), ws.branch.refs),
|
|
164
182
|
localCache,
|
|
165
183
|
policy: VERB_POLICY,
|
|
166
184
|
remotes,
|
|
167
185
|
})
|
|
186
|
+
await openVerbRemotes(remotes, openRemotes)
|
|
168
187
|
return pluginCache(plugin, { db: localCache.dbHandle(), policy: VERB_POLICY, remotes, scope })
|
|
169
188
|
}
|
|
170
189
|
|
|
190
|
+
/**
|
|
191
|
+
* The verb's `ctx.artifacts`: the workspace's cache as a run opens it, its
|
|
192
|
+
* policy and scope, a laptop writing no remote unless the policy names it.
|
|
193
|
+
* With `facts` (`ctx.artifactsFor`), under another run's refs and lent access.
|
|
194
|
+
*/
|
|
195
|
+
async function verbArtifacts(
|
|
196
|
+
ws: Exclude<Awaited<ReturnType<typeof workspacePlugins>>, UnresolvedPluginCommand | null>,
|
|
197
|
+
facts?: () => CacheFacts | undefined,
|
|
198
|
+
): Promise<TaskArtifacts> {
|
|
199
|
+
const [
|
|
200
|
+
{ Cache, FULL_CACHE_POLICY, parseCachePolicy },
|
|
201
|
+
{
|
|
202
|
+
cacheRefs,
|
|
203
|
+
namesRemote,
|
|
204
|
+
openRemotes,
|
|
205
|
+
resolveCache,
|
|
206
|
+
resolveCacheScope,
|
|
207
|
+
taskArtifacts,
|
|
208
|
+
workspaceCachePolicy,
|
|
209
|
+
},
|
|
210
|
+
{ onCI },
|
|
211
|
+
] = await Promise.all([
|
|
212
|
+
import('../cache/index.js'),
|
|
213
|
+
import('../orchestrator/index.js'),
|
|
214
|
+
import('./run.js'),
|
|
215
|
+
])
|
|
216
|
+
// `VX_CACHE_POLICY` beats the workspace's `cache.policy`, as it does for a
|
|
217
|
+
// run, refused as `parseRunArgs` refuses it (cli/run.ts).
|
|
218
|
+
const envSpec = process.env['VX_CACHE_POLICY'] || undefined
|
|
219
|
+
let asked: CachePolicy
|
|
220
|
+
if (envSpec === undefined) {
|
|
221
|
+
asked = workspaceCachePolicy(ws.workspaceConfig) ?? FULL_CACHE_POLICY
|
|
222
|
+
} else {
|
|
223
|
+
if (!envSpec.split(',').some((seg) => seg.trim().length > 0))
|
|
224
|
+
throw new UserError(
|
|
225
|
+
'VX_CACHE_POLICY needs a spec like local:r, local:rw, remote:, or local:,remote:rw',
|
|
226
|
+
'VX_E_USAGE',
|
|
227
|
+
)
|
|
228
|
+
try {
|
|
229
|
+
asked = parseCachePolicy(envSpec)
|
|
230
|
+
} catch (err) {
|
|
231
|
+
throw new UserError(
|
|
232
|
+
`VX_CACHE_POLICY: ${err instanceof Error ? err.message : String(err)}`,
|
|
233
|
+
'VX_E_USAGE',
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const policy: CachePolicy =
|
|
238
|
+
onCI(process.env['CI']) || namesRemote(envSpec ?? ws.workspaceConfig?.cache?.policy)
|
|
239
|
+
? asked
|
|
240
|
+
: { ...asked, remoteWrite: false }
|
|
241
|
+
const scope = resolveCacheScope(
|
|
242
|
+
ws.workspaceRoot,
|
|
243
|
+
ws.branch.trusted === true ? null : ws.branch.scope,
|
|
244
|
+
ws.plugins.some((p) => p.cache !== undefined),
|
|
245
|
+
configuredBaseBranch(ws.workspaceConfig),
|
|
246
|
+
)
|
|
247
|
+
const localCache = new Cache(
|
|
248
|
+
ws.cacheDir,
|
|
249
|
+
{ read: policy.localRead, write: policy.localWrite },
|
|
250
|
+
ws.workspaceRoot,
|
|
251
|
+
undefined,
|
|
252
|
+
'open',
|
|
253
|
+
await ws.storeRoot(),
|
|
254
|
+
)
|
|
255
|
+
const remotes: ResolvedRemote[] = []
|
|
256
|
+
const cache = await resolveCache(ws.plugins, {
|
|
257
|
+
workspaceRoot: ws.workspaceRoot,
|
|
258
|
+
cacheDir: ws.cacheDir,
|
|
259
|
+
warn,
|
|
260
|
+
scope,
|
|
261
|
+
refs:
|
|
262
|
+
facts?.()?.refs ??
|
|
263
|
+
cacheRefs(ws.workspaceRoot, configuredBaseBranch(ws.workspaceConfig), ws.branch.refs),
|
|
264
|
+
localCache,
|
|
265
|
+
policy,
|
|
266
|
+
remotes,
|
|
267
|
+
...(facts === undefined ? {} : { lentBy: () => facts()?.lent }),
|
|
268
|
+
})
|
|
269
|
+
await openVerbRemotes(remotes, openRemotes)
|
|
270
|
+
return taskArtifacts(cache, ws.workspaceRoot)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Each remote's `open`, as a run's: a remote that takes its token there
|
|
275
|
+
* (turboCache's OIDC exchange, a lent token) had none, so an agent's
|
|
276
|
+
* uploads went nowhere and its run could not restore them.
|
|
277
|
+
*/
|
|
278
|
+
async function openVerbRemotes(
|
|
279
|
+
remotes: readonly ResolvedRemote[],
|
|
280
|
+
open: (typeof import('../orchestrator/index.js'))['openRemotes'],
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
const ci = (await import('./run.js')).onCI(process.env['CI'])
|
|
283
|
+
await open(remotes, ci, new SecretBroker(ci, warn))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function lazyArtifacts(open: () => Promise<TaskArtifacts>): TaskArtifacts {
|
|
287
|
+
let opened: Promise<TaskArtifacts> | undefined
|
|
288
|
+
const artifacts = (): Promise<TaskArtifacts> => (opened ??= open())
|
|
289
|
+
return {
|
|
290
|
+
save: async (key, task, run) => (await artifacts()).save(key, task, run),
|
|
291
|
+
restore: async (key, task) => (await artifacts()).restore(key, task),
|
|
292
|
+
output: async (key) => (await artifacts()).output(key),
|
|
293
|
+
share: async (key) => (await artifacts()).share(key),
|
|
294
|
+
resend: async (key) => (await artifacts()).resend(key),
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
171
298
|
function lazyCache(open: () => Promise<PluginCache>): PluginCache {
|
|
172
299
|
let opened: Promise<PluginCache> | undefined
|
|
173
300
|
const cache = (): Promise<PluginCache> => (opened ??= open())
|
|
@@ -202,8 +329,30 @@ export async function pluginCommandHelp(cwd = process.cwd()): Promise<string[]>
|
|
|
202
329
|
for (const [verb, command] of Object.entries(plugin.commands ?? {})) {
|
|
203
330
|
if (seen.has(verb)) continue
|
|
204
331
|
seen.add(verb)
|
|
205
|
-
lines.push(
|
|
332
|
+
lines.push(
|
|
333
|
+
...helpLines(` vx ${verb.padEnd(17)} `, `${command.description} (${plugin.name})`),
|
|
334
|
+
)
|
|
206
335
|
}
|
|
207
336
|
}
|
|
208
337
|
return lines
|
|
209
338
|
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* One help entry: `head` then `text` wrapped at word breaks under the
|
|
342
|
+
* column `head` ends at, at core's reference width, so a plugin's long
|
|
343
|
+
* description reads as core's own entries do.
|
|
344
|
+
*/
|
|
345
|
+
function helpLines(head: string, text: string, width = 88): string[] {
|
|
346
|
+
const indent = ' '.repeat(head.length)
|
|
347
|
+
const lines: string[] = []
|
|
348
|
+
let line = head
|
|
349
|
+
for (const word of text.split(/\s+/)) {
|
|
350
|
+
const fresh = line === head || line === indent
|
|
351
|
+
if (!fresh && line.length + 1 + word.length > width) {
|
|
352
|
+
lines.push(line)
|
|
353
|
+
line = indent + word
|
|
354
|
+
} else line += fresh ? word : ` ${word}`
|
|
355
|
+
}
|
|
356
|
+
lines.push(line)
|
|
357
|
+
return lines
|
|
358
|
+
}
|