@vzn/vx 0.0.651 → 0.0.653
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
package/src/cli/why.ts
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { indexDb, Cache } from '../cache/index.js'
|
|
9
9
|
import { anchoredSpecError, flagHint, formatValue, seeHelp } from './help.js'
|
|
10
|
-
import { buildTaskGraph, isGroupTask, splitTaskId } from '../graph/index.js'
|
|
10
|
+
import { buildTaskGraph, isDefaultBuild, isGroupTask, splitTaskId } from '../graph/index.js'
|
|
11
11
|
import {
|
|
12
12
|
cacheKeyDiff,
|
|
13
13
|
explainCacheKey,
|
|
14
14
|
inputWriters,
|
|
15
15
|
latestRunId,
|
|
16
|
+
LocalHistoryProvider,
|
|
17
|
+
recordedTasks,
|
|
16
18
|
rootCauses,
|
|
17
19
|
upstreamTask,
|
|
18
20
|
whyDidThisRerun,
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
import { resolveRunId } from '../orchestrator/index.js'
|
|
21
23
|
import { MASKED, nearMatches, printable, refusal, UserError } from '../util/index.js'
|
|
22
24
|
import * as ui from '../ui/index.js'
|
|
25
|
+
import type { InputWriter, RootCause } from '../history/index.js'
|
|
23
26
|
import type { Style } from '../ui/index.js'
|
|
24
27
|
import { runRules } from '../settings.js'
|
|
25
28
|
import { buildPackageGraph, findWorkspaceRoot } from '../workspace/index.js'
|
|
@@ -139,7 +142,7 @@ function suggest(query: string, ids: readonly string[], invoked: boolean): strin
|
|
|
139
142
|
// No near name: say where to look, or that there is nothing yet.
|
|
140
143
|
// A run of only group tasks records no task row: "nothing has run" was
|
|
141
144
|
// false there, and its advice re-ran the group to the same answer.
|
|
142
|
-
if (ids.length > 0) return '; `vx last --
|
|
145
|
+
if (ids.length > 0) return '; `vx last --stats` lists the tasks that ran'
|
|
143
146
|
return invoked
|
|
144
147
|
? '; no recorded run executed a task (a group records none: ask about a task it depends on)'
|
|
145
148
|
: `; nothing has run here yet (vx run ${query}, then vx why)`
|
|
@@ -155,7 +158,8 @@ async function resolveTarget(
|
|
|
155
158
|
cache: Cache,
|
|
156
159
|
target: string,
|
|
157
160
|
cwdProject: () => Promise<string | null>,
|
|
158
|
-
declared: () => Promise<
|
|
161
|
+
declared: () => Promise<Declared>,
|
|
162
|
+
declaresId: (id: string) => Promise<Declared>,
|
|
159
163
|
): Promise<string> {
|
|
160
164
|
const db = indexDb(cache)
|
|
161
165
|
if (target.includes('#')) {
|
|
@@ -164,22 +168,40 @@ async function resolveTarget(
|
|
|
164
168
|
// the error's suggestions and the bare-name form only.
|
|
165
169
|
const [project, task] = splitTaskId(target)
|
|
166
170
|
const hit = db
|
|
167
|
-
.query('SELECT 1 FROM runs WHERE project = ? AND task = ? ORDER BY id DESC LIMIT 1')
|
|
171
|
+
.query('SELECT 1 FROM main.runs WHERE project = ? AND task = ? ORDER BY id DESC LIMIT 1')
|
|
168
172
|
.get(project, task)
|
|
169
173
|
if (hit !== null) return target
|
|
174
|
+
if ((await declaresId(target)) === 'group') throw groupRefusal(target)
|
|
170
175
|
} else {
|
|
171
176
|
// A bare name reads only its own task's rows: the DISTINCT over every
|
|
172
177
|
// pair sorted the whole history (24 of 26 ms at 58k rows), and its list
|
|
173
178
|
// serves only the miss's suggestions below.
|
|
174
179
|
const matches = (
|
|
175
|
-
db
|
|
180
|
+
db
|
|
181
|
+
.query('SELECT DISTINCT project FROM main.runs WHERE task = ? ORDER BY project')
|
|
182
|
+
.all(target) as {
|
|
176
183
|
project: string
|
|
177
184
|
}[]
|
|
178
185
|
).map((r) => `${r.project}#${target}`)
|
|
186
|
+
// Asked even when no project ran it: the cwd project's own that never
|
|
187
|
+
// ran read as a name nobody declares.
|
|
188
|
+
const here = await cwdProject()
|
|
189
|
+
if (here !== null) {
|
|
190
|
+
const own = `${here}#${target}`
|
|
191
|
+
if (matches.includes(own)) return own
|
|
192
|
+
// `vx run build` here runs this project's own: another project's
|
|
193
|
+
// runs were explained in its place, unsaid.
|
|
194
|
+
const kind = await declaresId(own)
|
|
195
|
+
if (kind === 'group') throw groupRefusal(own)
|
|
196
|
+
if (kind !== false) {
|
|
197
|
+
throw refusal(
|
|
198
|
+
{ tone: 'info', headline: `no runs of ${own} yet`, fix: `vx run ${target}` },
|
|
199
|
+
{ code: 'VX_E_NO_HISTORY', message: `vx why: no recorded runs for "${own}"` },
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
179
203
|
if (matches.length === 1) return matches[0]!
|
|
180
204
|
if (matches.length > 1) {
|
|
181
|
-
const here = await cwdProject()
|
|
182
|
-
if (here !== null && matches.includes(`${here}#${target}`)) return `${here}#${target}`
|
|
183
205
|
throw new UserError(
|
|
184
206
|
`vx why: "${target}" ran in ${matches.length} projects — pick one:\n` +
|
|
185
207
|
matches.map((m) => ` ${m}`).join('\n'),
|
|
@@ -187,17 +209,18 @@ async function resolveTarget(
|
|
|
187
209
|
)
|
|
188
210
|
}
|
|
189
211
|
}
|
|
190
|
-
const
|
|
191
|
-
.query('SELECT DISTINCT project, task FROM runs ORDER BY project, task')
|
|
192
|
-
.all() as Array<{ project: string; task: string }>
|
|
193
|
-
const ids = pairs.map((p) => `${p.project}#${p.task}`)
|
|
212
|
+
const ids = new LocalHistoryProvider(db).recentTaskIds()
|
|
194
213
|
const invoked = ids.length === 0 && db.query('SELECT 1 FROM invocations LIMIT 1').get() !== null
|
|
195
214
|
if (ids.length === 0 && !invoked) {
|
|
196
215
|
// `vx run` of a task no project declares is refused: its advice was that.
|
|
197
216
|
const known = await declared()
|
|
217
|
+
// Where the cwd's project does not declare it (a monorepo root), a bare
|
|
218
|
+
// `vx run build` is refused too: that advice sent the user in a circle.
|
|
219
|
+
const here = known && !target.includes('#') ? await cwdProject() : null
|
|
220
|
+
const all = here !== null && (await declaresId(`${here}#${target}`)) === false ? ' --all' : ''
|
|
198
221
|
throw refusal(
|
|
199
222
|
known
|
|
200
|
-
? { tone: 'info', headline: `no runs of ${target} yet`, fix: `vx run ${target}` }
|
|
223
|
+
? { tone: 'info', headline: `no runs of ${target} yet`, fix: `vx run ${target}${all}` }
|
|
201
224
|
: {
|
|
202
225
|
tone: 'info',
|
|
203
226
|
headline: `no project declares ${target}`,
|
|
@@ -271,16 +294,12 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
271
294
|
target,
|
|
272
295
|
() => findCwdProject(process.cwd()),
|
|
273
296
|
() => declares(root, target),
|
|
297
|
+
// The cwd project's own: a bare `vx run` there skips its default build.
|
|
298
|
+
(id) => declares(root, id, true),
|
|
274
299
|
)
|
|
275
300
|
let runId: string | null
|
|
276
301
|
if (parsed.runId !== undefined) {
|
|
277
|
-
|
|
278
|
-
// --log` prints its id, so the whole id is taken from its rows too.
|
|
279
|
-
runId =
|
|
280
|
-
resolveRunId(db, parsed.runId, 'vx why') ??
|
|
281
|
-
(db.query('SELECT 1 FROM runs WHERE run_id = ? LIMIT 1').get(parsed.runId) !== null
|
|
282
|
-
? parsed.runId
|
|
283
|
-
: null)
|
|
302
|
+
runId = resolveRunId(db, parsed.runId, 'vx why')
|
|
284
303
|
// An unknown id read as a known run missing the task (X-18).
|
|
285
304
|
if (runId === null) {
|
|
286
305
|
throw new UserError(
|
|
@@ -329,18 +348,16 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
329
348
|
// each moved dependency in this run down to the tasks whose own
|
|
330
349
|
// inputs moved, so one `vx why` names the file, not the next task.
|
|
331
350
|
// A group records no run: the graph names what it gathers.
|
|
351
|
+
const recorded = recordedTasks(db, runId)
|
|
332
352
|
const unrecorded = diff.entries.filter(
|
|
333
|
-
(e) =>
|
|
334
|
-
e.kind === 'upstream' &&
|
|
335
|
-
e.change === 'changed' &&
|
|
336
|
-
!cacheKeyDiff(db, runId, upstreamTask(e.name)).found,
|
|
353
|
+
(e) => e.kind === 'upstream' && e.change === 'changed' && !recorded.has(upstreamTask(e.name)),
|
|
337
354
|
)
|
|
338
355
|
const groups = unrecorded.length > 0 ? await groupMembers(root, taskId) : new Map()
|
|
339
356
|
const roots = rootCauses(db, runId, taskId, diff.entries, groups)
|
|
340
|
-
const writers = inputWriters(db, root, taskId, diff.entries, roots)
|
|
357
|
+
const writers = inputWriters(db, runId, root, taskId, diff.entries, roots)
|
|
341
358
|
|
|
342
359
|
if (parsed.format === 'json') {
|
|
343
|
-
ui.
|
|
360
|
+
await ui.dataPieces('out', jsonPieces({ taskId, runId, why, diff }, roots, writers))
|
|
344
361
|
return 0
|
|
345
362
|
}
|
|
346
363
|
|
|
@@ -407,8 +424,18 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
407
424
|
const what = r.entries.map((e) =>
|
|
408
425
|
e.name === e.kind ? e.kind : `${e.kind} ${printable(e.name)}`,
|
|
409
426
|
)
|
|
427
|
+
// A 20,000-task chain printed whole is one 389 KB line.
|
|
428
|
+
const chain = r.chain
|
|
429
|
+
const ids =
|
|
430
|
+
chain.length > CHAIN_SHOWN
|
|
431
|
+
? [
|
|
432
|
+
...chain.slice(0, 2).map((id) => ui.id(id)),
|
|
433
|
+
ui.dim(`${chain.length - 5} more`),
|
|
434
|
+
...chain.slice(-3).map((id) => ui.id(id)),
|
|
435
|
+
]
|
|
436
|
+
: chain.map((id) => ui.id(id))
|
|
410
437
|
return [
|
|
411
|
-
...
|
|
438
|
+
...ids.flatMap((id, i) => [...(i > 0 ? [ui.dim(' \u2190 ')] : []), id]),
|
|
412
439
|
ui.dim(' \u2190 '),
|
|
413
440
|
ui.strong(
|
|
414
441
|
`${what.slice(0, 3).join(', ')}${what.length > 3 ? ` and ${what.length - 3} more` : ''}`,
|
|
@@ -470,6 +497,33 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
470
497
|
|
|
471
498
|
const ROOTS_SHOWN = 5
|
|
472
499
|
|
|
500
|
+
/** Longer root-cause chains print their two ends around a count. */
|
|
501
|
+
const CHAIN_SHOWN = 6
|
|
502
|
+
|
|
503
|
+
/** Bytes of roots per piece written. */
|
|
504
|
+
const PIECE_BYTES = 1 << 20
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* `{ ...head, roots, writers }` as one `JSON.stringify` of it gives, in
|
|
508
|
+
* pieces: every root carries its whole chain, so a 20,000-task chain's
|
|
509
|
+
* roots hold 2×10⁸ ids, more than one string holds.
|
|
510
|
+
*/
|
|
511
|
+
function* jsonPieces(
|
|
512
|
+
head: object,
|
|
513
|
+
roots: readonly RootCause[],
|
|
514
|
+
writers: readonly InputWriter[],
|
|
515
|
+
): Generator<string> {
|
|
516
|
+
let piece = `${JSON.stringify(head).slice(0, -1)},"roots":[`
|
|
517
|
+
for (let i = 0; i < roots.length; i++) {
|
|
518
|
+
piece += `${i > 0 ? ',' : ''}${JSON.stringify(roots[i])}`
|
|
519
|
+
if (piece.length >= PIECE_BYTES) {
|
|
520
|
+
yield piece
|
|
521
|
+
piece = ''
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
yield `${piece}],"writers":${JSON.stringify(writers)}}\n`
|
|
525
|
+
}
|
|
526
|
+
|
|
473
527
|
const CHANGE_HUE: Record<string, Style> = { added: 'ok', removed: 'error', changed: 'warn' }
|
|
474
528
|
|
|
475
529
|
type DiffEntry = ReturnType<typeof cacheKeyDiff>['entries'][number]
|
|
@@ -508,21 +562,41 @@ function upstreamTodo(upstream: readonly DiffEntry[], unrecorded: readonly DiffE
|
|
|
508
562
|
return out
|
|
509
563
|
}
|
|
510
564
|
|
|
511
|
-
/**
|
|
512
|
-
|
|
565
|
+
/** A declared group (no `exec`) records no run: `group` when every declaration is one. */
|
|
566
|
+
type Declared = false | 'task' | 'group'
|
|
567
|
+
|
|
568
|
+
/** Whether a project declares `target` (`pkg#task`, or a bare task); `task` when no config loads. */
|
|
569
|
+
async function declares(root: string, target: string, own = false): Promise<Declared> {
|
|
513
570
|
try {
|
|
514
571
|
const projects = await loadCliProjects(root, await discoverCliProjects(root), 'all')
|
|
515
572
|
const [project, task] = target.includes('#') ? splitTaskId(target) : [undefined, target]
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
573
|
+
const found = [...projects.values()].flatMap((p) => {
|
|
574
|
+
const t = p.config.tasks?.[task]
|
|
575
|
+
return (project === undefined || p.name === project) &&
|
|
576
|
+
Object.hasOwn(p.config.tasks ?? {}, task) &&
|
|
577
|
+
!(own && isDefaultBuild(t))
|
|
578
|
+
? [t]
|
|
579
|
+
: []
|
|
580
|
+
})
|
|
581
|
+
if (found.length === 0) return false
|
|
582
|
+
// The default build is a group too, but no one declared it.
|
|
583
|
+
return found.every((t) => t?.exec === undefined && !isDefaultBuild(t)) ? 'group' : 'task'
|
|
520
584
|
} catch {
|
|
521
585
|
// quiet: with no lock or a broken config, the run says why itself
|
|
522
|
-
return
|
|
586
|
+
return 'task'
|
|
523
587
|
}
|
|
524
588
|
}
|
|
525
589
|
|
|
590
|
+
function groupRefusal(id: string): UserError {
|
|
591
|
+
return refusal(
|
|
592
|
+
{ tone: 'info', headline: `${id} is a group, which records no run`, fix: `vx show ${id}` },
|
|
593
|
+
{
|
|
594
|
+
code: 'VX_E_NO_HISTORY',
|
|
595
|
+
message: `vx why: ${id} is a group, which records no run (vx show ${id} names what it gathers)`,
|
|
596
|
+
},
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
|
|
526
600
|
/**
|
|
527
601
|
* Each group in `taskId`'s graph with the tasks it gathers. Empty when a
|
|
528
602
|
* config no longer loads: the advice then names the group to `vx show`.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import type { WorkspaceConfig } from '../config.js'
|
|
10
|
+
import type { LazyGitEnumeration } from '../cache/index.js'
|
|
10
11
|
import { UserError } from '../util/index.js'
|
|
11
12
|
import { sayLine } from '../ui/index.js'
|
|
12
13
|
import {
|
|
@@ -65,10 +66,19 @@ export async function loadCliWorkspace(
|
|
|
65
66
|
export async function discoverCliProjects(
|
|
66
67
|
root: string,
|
|
67
68
|
load: CliLoadOptions = {},
|
|
69
|
+
/** The whole-tree enumeration the caller started; the run reuses it with the discovery. */
|
|
70
|
+
git?: LazyGitEnumeration,
|
|
68
71
|
): Promise<ProjectMeta[]> {
|
|
69
72
|
const warn = load.warn ?? warnToStderr
|
|
70
73
|
const ws = await loadCliWorkspace(root, warn)
|
|
71
|
-
return await discoverProjects(
|
|
74
|
+
return await discoverProjects(
|
|
75
|
+
root,
|
|
76
|
+
lockReader(root),
|
|
77
|
+
ws.plugins,
|
|
78
|
+
stageCacheDir(ws, load),
|
|
79
|
+
warn,
|
|
80
|
+
git,
|
|
81
|
+
)
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
/**
|
package/src/config.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface WorkspaceConfig {
|
|
|
28
28
|
baseBranch?: string
|
|
29
29
|
run?: RunSettings
|
|
30
30
|
cache?: CacheSettings
|
|
31
|
+
/** Input defaults for every task. */
|
|
32
|
+
inputs?: WorkspaceInputs
|
|
33
|
+
/** Output defaults for every task. */
|
|
34
|
+
outputs?: WorkspaceOutputs
|
|
31
35
|
lock?: LockSettings
|
|
32
36
|
watch?: WatchSettings
|
|
33
37
|
/**
|
|
@@ -76,6 +80,18 @@ export interface RunSettings {
|
|
|
76
80
|
rules?: WorkspaceRules
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/** `WorkspaceConfig.inputs`. */
|
|
84
|
+
export interface WorkspaceInputs {
|
|
85
|
+
/** The `inputs.stability` of every task that does not set its own. */
|
|
86
|
+
stability?: InputStability
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `WorkspaceConfig.outputs`. */
|
|
90
|
+
export interface WorkspaceOutputs {
|
|
91
|
+
/** The `outputs.clean` of every task that does not set its own. */
|
|
92
|
+
clean?: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
/** `WorkspaceConfig.cache`: where the cache lives and what a run does with it. */
|
|
80
96
|
export interface CacheSettings {
|
|
81
97
|
/**
|
|
@@ -93,10 +109,6 @@ export interface CacheSettings {
|
|
|
93
109
|
policy?: string
|
|
94
110
|
/** Which remote hits land on disk: `all` (the default), only the requested tasks' (`toplevel`), or `none`. */
|
|
95
111
|
download?: 'all' | 'toplevel' | 'none'
|
|
96
|
-
/** The `cache.inputStability` of every task that does not set its own. */
|
|
97
|
-
inputStability?: InputStability
|
|
98
|
-
/** The `cache.outputs.clean` of every task that does not set its own. */
|
|
99
|
-
outputs?: { clean?: boolean }
|
|
100
112
|
/**
|
|
101
113
|
* Which runs keep a failed task's log and the files its outputs left in
|
|
102
114
|
* the remote: CI runs (`'ci'`, the default), runs off CI (`'local'`), or
|
|
@@ -305,13 +317,15 @@ export interface TaskConfig {
|
|
|
305
317
|
* keyed group: its inputs are a key its dependents fold, with no process.
|
|
306
318
|
*/
|
|
307
319
|
inputs?: InputsConfig
|
|
320
|
+
/** What the task produces: saved on a miss, restored on a hit. Needs `cache: true`. */
|
|
321
|
+
outputs?: TaskOutputs
|
|
308
322
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* Requires `exec` and `
|
|
312
|
-
* runs nothing to cache.
|
|
323
|
+
* `true` caches the task. **Caching is opt-in.** Omitted or `false`, the
|
|
324
|
+
* task always runs and nothing is read from or written to the cache.
|
|
325
|
+
* Requires `exec`, `inputs` and `outputs`: the key is the task's inputs,
|
|
326
|
+
* and a group runs nothing to cache.
|
|
313
327
|
*/
|
|
314
|
-
cache?:
|
|
328
|
+
cache?: boolean
|
|
315
329
|
}
|
|
316
330
|
|
|
317
331
|
/**
|
|
@@ -335,7 +349,7 @@ export interface TaskConfig {
|
|
|
335
349
|
* from the other coupled them both ways — a declaration added for
|
|
336
350
|
* caching silently widened the sandbox, and a path the task needed had
|
|
337
351
|
* to be laundered through the cache key to get it (2026-09-05).
|
|
338
|
-
* A declared `
|
|
352
|
+
* A declared `outputs` is NOT a write grant; the request
|
|
339
353
|
* `sandbox-request.ts` builds carries no write of its own and binds
|
|
340
354
|
* `allow.write` alone. This comment claimed the opposite — here and on
|
|
341
355
|
* both grant fields — while the file beside it and `schema.md` both said
|
|
@@ -391,6 +405,13 @@ export interface SandboxConfig {
|
|
|
391
405
|
weakerWhenNested?: boolean
|
|
392
406
|
/** Accept weaker network isolation (host proxy) for lower overhead. */
|
|
393
407
|
weakerNetworkIsolation?: boolean
|
|
408
|
+
/**
|
|
409
|
+
* Refuse every read outside the workspace too (`/usr`, `~/.cache`, the
|
|
410
|
+
* system toolchain), not only the workspace's other projects. A path
|
|
411
|
+
* outside it is readable only when `allow.read` names it; the shell a
|
|
412
|
+
* task runs in stays readable, so a task can start.
|
|
413
|
+
*/
|
|
414
|
+
hermetic?: boolean
|
|
394
415
|
}
|
|
395
416
|
|
|
396
417
|
/** The denials `sandbox.ignore` can leave out of the report: patterns per class. */
|
|
@@ -410,9 +431,9 @@ export interface SandboxGrants {
|
|
|
410
431
|
read?: readonly string[]
|
|
411
432
|
/**
|
|
412
433
|
* Writable paths or globs. The task can write NOTHING without these:
|
|
413
|
-
* declaring `
|
|
434
|
+
* declaring `outputs.files` grants no write, and a task whose
|
|
414
435
|
* only declaration is its outputs writes into a tmpfs that evaporates
|
|
415
|
-
* (vx then warns that `
|
|
436
|
+
* (vx then warns that `outputs` matched no files).
|
|
416
437
|
*/
|
|
417
438
|
write?: readonly string[]
|
|
418
439
|
/**
|
|
@@ -476,6 +497,15 @@ export interface ExecConfig {
|
|
|
476
497
|
* it on a worker that is. Omitted → anywhere.
|
|
477
498
|
*/
|
|
478
499
|
platforms?: readonly Platform[]
|
|
500
|
+
/**
|
|
501
|
+
* The worker type to run it on, by name (`'ubuntu'`), when an executor
|
|
502
|
+
* that names worker types (`TaskExecutor.workers`, `@vzn/vx-dte`'s
|
|
503
|
+
* `workers`) runs it: only that type's machines take it, and a run
|
|
504
|
+
* whose executor has no such type, or none on one of `platforms`, is
|
|
505
|
+
* refused before it starts. Placement only: not in the key, and a run
|
|
506
|
+
* on this machine ignores it. Omitted → the first type that fits.
|
|
507
|
+
*/
|
|
508
|
+
worker?: string
|
|
479
509
|
/** Environment exposed to the child process. */
|
|
480
510
|
env?: ExecEnv
|
|
481
511
|
/**
|
|
@@ -585,17 +615,6 @@ export interface ExecEnv {
|
|
|
585
615
|
secret?: readonly string[]
|
|
586
616
|
}
|
|
587
617
|
|
|
588
|
-
export interface CacheConfig {
|
|
589
|
-
/** What this task produces. Captured for restore on a cache hit. */
|
|
590
|
-
outputs: CacheOutputs
|
|
591
|
-
/**
|
|
592
|
-
* What a write to an input while the command runs means. Each field,
|
|
593
|
-
* highest first: its env var, this, the workspace's `inputStability`,
|
|
594
|
-
* the default (`'immutable'`, `'warn'`).
|
|
595
|
-
*/
|
|
596
|
-
inputStability?: InputStability
|
|
597
|
-
}
|
|
598
|
-
|
|
599
618
|
export interface InputStability {
|
|
600
619
|
/**
|
|
601
620
|
* `'immutable'` (the default): any write to an input during the command
|
|
@@ -615,7 +634,7 @@ export interface InputStability {
|
|
|
615
634
|
severity?: 'warn' | 'error'
|
|
616
635
|
}
|
|
617
636
|
|
|
618
|
-
export interface
|
|
637
|
+
export interface TaskOutputs {
|
|
619
638
|
/**
|
|
620
639
|
* Files produced by this task, as project-relative globs. Pass `[]`
|
|
621
640
|
* when the task has no files to capture (e.g. `lint`, `typecheck`).
|
|
@@ -640,7 +659,7 @@ export interface CacheOutputs {
|
|
|
640
659
|
* (tsc's `.tsbuildinfo`) instead of removing them first. A run that
|
|
641
660
|
* leaves a file it found untouched saves nothing, so an entry holds only
|
|
642
661
|
* what a clean run would leave. A retry still starts clean. Omitted →
|
|
643
|
-
* the workspace's `
|
|
662
|
+
* the workspace's `outputs.clean`, else `true`.
|
|
644
663
|
*/
|
|
645
664
|
clean?: boolean
|
|
646
665
|
}
|
|
@@ -716,6 +735,13 @@ export interface InputsConfig {
|
|
|
716
735
|
* tool versions, OS info, etc.
|
|
717
736
|
*/
|
|
718
737
|
workspaceRuntime?: readonly string[]
|
|
738
|
+
/**
|
|
739
|
+
* What a write to an input while the command runs means for the save.
|
|
740
|
+
* Each field, highest first: its env var, this, the workspace's
|
|
741
|
+
* `inputs.stability`, the default (`'immutable'`, `'warn'`). Needs
|
|
742
|
+
* `cache: true`.
|
|
743
|
+
*/
|
|
744
|
+
stability?: InputStability
|
|
719
745
|
}
|
|
720
746
|
|
|
721
747
|
/**
|
|
@@ -785,9 +811,7 @@ export function defineProject<const T extends ProjectConfig>(
|
|
|
785
811
|
dependsOn?: readonly DependsOnEntry<Extract<keyof NonNullable<T['tasks']>, string>>[]
|
|
786
812
|
exec?: Known<At<NonNullable<T['tasks']>[K], 'exec'>, ExecConfig>
|
|
787
813
|
inputs?: Known<At<NonNullable<T['tasks']>[K], 'inputs'>, InputsConfig>
|
|
788
|
-
|
|
789
|
-
outputs?: Known<At<At<NonNullable<T['tasks']>[K], 'cache'>, 'outputs'>, CacheOutputs>
|
|
790
|
-
}
|
|
814
|
+
outputs?: Known<At<NonNullable<T['tasks']>[K], 'outputs'>, TaskOutputs>
|
|
791
815
|
}
|
|
792
816
|
}
|
|
793
817
|
},
|
|
@@ -801,9 +825,11 @@ export function defineWorkspace<T extends WorkspaceConfig>(
|
|
|
801
825
|
run?: Known<At<T, 'run'>, RunSettings> & {
|
|
802
826
|
rules?: Known<At<At<T, 'run'>, 'rules'>, WorkspaceRules>
|
|
803
827
|
}
|
|
804
|
-
cache?: Known<At<T, 'cache'>, CacheSettings>
|
|
805
|
-
|
|
828
|
+
cache?: Known<At<T, 'cache'>, CacheSettings>
|
|
829
|
+
inputs?: Known<At<T, 'inputs'>, WorkspaceInputs> & {
|
|
830
|
+
stability?: Known<At<At<T, 'inputs'>, 'stability'>, InputStability>
|
|
806
831
|
}
|
|
832
|
+
outputs?: Known<At<T, 'outputs'>, WorkspaceOutputs>
|
|
807
833
|
lock?: Known<At<T, 'lock'>, LockSettings>
|
|
808
834
|
watch?: Known<At<T, 'watch'>, WatchSettings>
|
|
809
835
|
},
|
package/src/exec/env.ts
CHANGED
|
@@ -39,7 +39,9 @@ export interface BuildEnvOptions {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function buildIsolatedEnv(opts: BuildEnvOptions): NodeJS.ProcessEnv {
|
|
42
|
-
|
|
42
|
+
// No prototype: on `{}`, a name `__proto__` hit the setter and reached
|
|
43
|
+
// no task, though the key folded it.
|
|
44
|
+
const out: NodeJS.ProcessEnv = Object.create(null)
|
|
43
45
|
|
|
44
46
|
for (const name of ESSENTIAL_ENV) {
|
|
45
47
|
const value = opts.source[name]
|