@vzn/vx 0.0.642 → 0.0.644
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/package.json +5 -5
- package/schemas/info.json +0 -11
- package/skills/vx/SKILL.md +1 -1
- package/src/cache/archive.ts +153 -2
- package/src/cache/cache.ts +417 -183
- package/src/cache/file-hashes.ts +2 -14
- package/src/cache/fold-memo.ts +7 -3
- package/src/cache/index.ts +10 -2
- package/src/cache/key-fold.ts +4 -1
- package/src/cache/layer.ts +11 -4
- package/src/cache/layered-cache.ts +82 -8
- package/src/cache/plugin-cache.ts +51 -43
- package/src/cache/remote-lane.ts +18 -13
- package/src/cache/run-store.ts +4 -1
- package/src/cache/scoped-cache.ts +74 -239
- package/src/cache/tar-stream.ts +12 -7
- package/src/cli/format.ts +1 -2
- package/src/cli/help.ts +8 -4
- package/src/cli/info.ts +2 -19
- package/src/cli/plugin-commands.ts +11 -6
- package/src/cli/run.ts +7 -12
- package/src/cli/select.ts +7 -9
- package/src/cli/upgrade.ts +38 -8
- package/src/cli/workspace-config.ts +8 -2
- package/src/config.ts +22 -26
- package/src/exec/sandbox-paths.ts +2 -2
- package/src/graph/scheduler.ts +3 -1
- package/src/graph/task-graph.ts +15 -11
- package/src/index.ts +5 -1
- package/src/orchestrator/affected-tasks.ts +23 -2
- package/src/orchestrator/doctor.ts +0 -17
- package/src/orchestrator/execute-task.ts +1 -1
- package/src/orchestrator/index.ts +4 -0
- package/src/orchestrator/options.ts +15 -1
- package/src/orchestrator/plugin-host.ts +11 -4
- package/src/orchestrator/plugin.ts +23 -2
- package/src/orchestrator/prepare.ts +81 -18
- package/src/orchestrator/projects.ts +26 -5
- package/src/orchestrator/remote-prefetch.ts +11 -4
- package/src/orchestrator/run-context.ts +85 -0
- package/src/orchestrator/run-settings.ts +3 -0
- package/src/orchestrator/run.ts +9 -26
- package/src/orchestrator/task-hash.ts +6 -0
- package/src/report/logger.ts +0 -4
- package/src/report/summary.ts +3 -11
- package/src/settings.ts +54 -25
- package/src/util/index.ts +1 -0
- package/src/util/paths.ts +42 -33
- package/src/util/size.ts +4 -5
- package/src/workspace/affected.ts +120 -63
- package/src/workspace/config-schema.ts +44 -52
- package/src/workspace/index.ts +3 -0
package/src/cli/upgrade.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { access, chmod, chown, link, mkdtemp, rename, rm, stat } from 'node:fs/p
|
|
|
21
21
|
import path from 'node:path'
|
|
22
22
|
import { flagHint, seeHelp } from './help.js'
|
|
23
23
|
import { UserError, sayNotice, ui } from '../util/index.js'
|
|
24
|
+
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
24
25
|
import { VERSION } from '../version.js'
|
|
25
26
|
|
|
26
27
|
const REGISTRY = 'https://registry.npmjs.org'
|
|
@@ -596,11 +597,11 @@ export async function upgradeManaged(
|
|
|
596
597
|
version: string,
|
|
597
598
|
current = VERSION,
|
|
598
599
|
step = '',
|
|
599
|
-
): Promise<
|
|
600
|
+
): Promise<boolean> {
|
|
600
601
|
const plugins = install.plugins ?? []
|
|
601
602
|
if (version === current && plugins.every((p) => installedVersion(install.cwd, p) === version)) {
|
|
602
603
|
ui.print('out', ui.status('info', 'vx upgrade', `${step}already at ${current}`))
|
|
603
|
-
return
|
|
604
|
+
return false
|
|
604
605
|
}
|
|
605
606
|
const argv = upgradeArgv(install, version)
|
|
606
607
|
// PATH as it is now: `Bun.which` alone reads the one the process started with.
|
|
@@ -636,6 +637,7 @@ export async function upgradeManaged(
|
|
|
636
637
|
}`,
|
|
637
638
|
)
|
|
638
639
|
}
|
|
640
|
+
return true
|
|
639
641
|
}
|
|
640
642
|
|
|
641
643
|
/**
|
|
@@ -648,7 +650,7 @@ async function upgradeBinary(
|
|
|
648
650
|
current = VERSION,
|
|
649
651
|
step = '',
|
|
650
652
|
key: SigningKey = NPM_SIGNING_KEY,
|
|
651
|
-
): Promise<
|
|
653
|
+
): Promise<boolean> {
|
|
652
654
|
const pkg = platformPackage()
|
|
653
655
|
let installed: string | undefined
|
|
654
656
|
const replaced = await replaceBinary(
|
|
@@ -679,6 +681,7 @@ async function upgradeBinary(
|
|
|
679
681
|
? ui.status('ok', 'vx upgrade', `${step}installed ${installed}`)
|
|
680
682
|
: ui.status('info', 'vx upgrade', `${step}already at ${current}`),
|
|
681
683
|
)
|
|
684
|
+
return replaced
|
|
682
685
|
}
|
|
683
686
|
|
|
684
687
|
/** The version `bin --version` answers, without the hand-off note a handed-off run adds. */
|
|
@@ -738,9 +741,9 @@ export async function upgradeOnlyGlobal(
|
|
|
738
741
|
}
|
|
739
742
|
|
|
740
743
|
/** `run`, its refusal naming `what` after the verb: a run that moves two installs says which failed. */
|
|
741
|
-
async function named(what: string, run: () => Promise<
|
|
744
|
+
async function named<T>(what: string, run: () => Promise<T>): Promise<T> {
|
|
742
745
|
try {
|
|
743
|
-
await run()
|
|
746
|
+
return await run()
|
|
744
747
|
} catch (err) {
|
|
745
748
|
if (!(err instanceof UserError)) throw err
|
|
746
749
|
throw new UserError(err.message.replace(/^vx upgrade: /, `vx upgrade: ${what}: `), err.code)
|
|
@@ -787,19 +790,46 @@ export async function upgradeCmd(argv: readonly string[]): Promise<number> {
|
|
|
787
790
|
const from = handedOffFrom
|
|
788
791
|
if (from !== undefined) {
|
|
789
792
|
const version = await targetVersion(tag)
|
|
790
|
-
await named('project vx', () =>
|
|
793
|
+
const moved = await named('project vx', () =>
|
|
791
794
|
install === null
|
|
792
795
|
? upgradeBinary(dest, version, VERSION, 'project ')
|
|
793
796
|
: upgradeManaged(install, version, VERSION, 'project '),
|
|
794
797
|
)
|
|
795
798
|
await upgradeGlobal(from, version)
|
|
799
|
+
if (moved) await relock(newVx(install, dest))
|
|
796
800
|
return 0
|
|
797
801
|
}
|
|
798
|
-
|
|
799
|
-
|
|
802
|
+
const moved =
|
|
803
|
+
install !== null
|
|
804
|
+
? await upgradeManaged(install, await targetVersion(tag))
|
|
805
|
+
: await upgradeBinary(dest, versionSpec(tag))
|
|
806
|
+
if (moved) await relock(newVx(install, dest))
|
|
800
807
|
return 0
|
|
801
808
|
}
|
|
802
809
|
|
|
810
|
+
/**
|
|
811
|
+
* The vx an upgrade just installed: the replaced binary, or the project's
|
|
812
|
+
* `node_modules/.bin/vx` (a manager's store path names the old version).
|
|
813
|
+
*/
|
|
814
|
+
const newVx = (install: ManagedInstall | null, dest: string): string =>
|
|
815
|
+
install === null ? dest : path.join(install.cwd, 'node_modules', '.bin', 'vx')
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Lock the workspace with the vx just installed at `bin` (M, 2026-10-18):
|
|
819
|
+
* a release may record the lock differently. `-g` never re-locks. A lock
|
|
820
|
+
* that fails leaves the upgrade done and says what to run.
|
|
821
|
+
*/
|
|
822
|
+
export async function relock(bin: string, cwd = process.cwd()): Promise<void> {
|
|
823
|
+
const root = await findWorkspaceRoot(cwd).catch(() => null)
|
|
824
|
+
if (root === null) return
|
|
825
|
+
const proc = Bun.spawnSync({
|
|
826
|
+
cmd: [bin, 'lock'],
|
|
827
|
+
cwd: root,
|
|
828
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
829
|
+
})
|
|
830
|
+
if (proc.exitCode !== 0) sayNotice('warn', 'vx upgrade', 'vx lock failed: run vx lock')
|
|
831
|
+
}
|
|
832
|
+
|
|
803
833
|
/**
|
|
804
834
|
* `bin --version` when it answers as vx within `timeoutMs`, else null. The
|
|
805
835
|
* answer decides whether a replaced binary is rolled back, so it is
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
lockOfDiscovery,
|
|
16
16
|
lockReader,
|
|
17
17
|
} from '../orchestrator/index.js'
|
|
18
|
-
import type { VxPlugin } from '../orchestrator/index.js'
|
|
18
|
+
import type { BranchFacts, VxPlugin } from '../orchestrator/index.js'
|
|
19
19
|
import {
|
|
20
20
|
buildPackageGraph,
|
|
21
21
|
type ProjectEntry,
|
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
interface CliWorkspace {
|
|
28
28
|
workspaceConfig: WorkspaceConfig | null
|
|
29
29
|
plugins: readonly VxPlugin[]
|
|
30
|
+
/** What a plugin's `config` stage said of the branch: its cache scope and base commit. */
|
|
31
|
+
branch: BranchFacts
|
|
30
32
|
cacheDir: string
|
|
31
33
|
/** The shared store a run opens `cacheDir` with (`resolveStoreRoot`), asked on load. */
|
|
32
34
|
storeRoot: Promise<string | null>
|
|
@@ -37,10 +39,14 @@ export const warnToStderr = (message: string): void => {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export async function loadCliWorkspace(workspaceRoot: string): Promise<CliWorkspace> {
|
|
40
|
-
const { workspaceConfig, plugins } = await loadWorkspacePlugins(
|
|
42
|
+
const { workspaceConfig, plugins, branch } = await loadWorkspacePlugins(
|
|
43
|
+
workspaceRoot,
|
|
44
|
+
warnToStderr,
|
|
45
|
+
)
|
|
41
46
|
return {
|
|
42
47
|
workspaceConfig,
|
|
43
48
|
plugins,
|
|
49
|
+
branch,
|
|
44
50
|
cacheDir: resolveCacheDir(workspaceRoot, workspaceConfig),
|
|
45
51
|
storeRoot: resolveStoreRoot(workspaceRoot, workspaceConfig),
|
|
46
52
|
}
|
package/src/config.ts
CHANGED
|
@@ -14,6 +14,13 @@ export type Duration = `${number}${'ms' | 's' | 'm' | 'h' | 'd'}`
|
|
|
14
14
|
* Nothing here is folded into a cache key.
|
|
15
15
|
*/
|
|
16
16
|
export interface WorkspaceConfig {
|
|
17
|
+
/**
|
|
18
|
+
* The repository's default branch (`'develop'`). A run on it has no
|
|
19
|
+
* cache scope and a bare `--affected` has no default; elsewhere both
|
|
20
|
+
* compare with the merge base of `origin/<baseBranch>` and HEAD.
|
|
21
|
+
* Omitted → `origin/HEAD`'s branch, then `main`, then `master`.
|
|
22
|
+
*/
|
|
23
|
+
baseBranch?: string
|
|
17
24
|
run?: RunSettings
|
|
18
25
|
cache?: CacheSettings
|
|
19
26
|
lock?: LockSettings
|
|
@@ -42,13 +49,6 @@ export interface RunSettings {
|
|
|
42
49
|
* `schedule` plugin's weights still come first; this breaks their ties.
|
|
43
50
|
*/
|
|
44
51
|
schedule?: 'most-work' | 'critical-path' | 'direct-dependents' | 'ready-order'
|
|
45
|
-
/**
|
|
46
|
-
* The git ref a bare `--affected` compares with (`origin/develop`).
|
|
47
|
-
* `--affected=<base>` overrides it. Omitted → `origin/HEAD`, then the
|
|
48
|
-
* first of `origin/main`, `origin/master`, `main`, `master` that is not
|
|
49
|
-
* HEAD, else `HEAD~1`.
|
|
50
|
-
*/
|
|
51
|
-
affectedBase?: string
|
|
52
52
|
/**
|
|
53
53
|
* A task's time limit, for tasks without their own `exec.timeout` (which
|
|
54
54
|
* beats every run-level source). A runaway-process guard: a timed-out
|
|
@@ -79,15 +79,6 @@ export interface CacheSettings {
|
|
|
79
79
|
* `.vx/cache` and its entries in the user's shared store.
|
|
80
80
|
*/
|
|
81
81
|
dir?: string
|
|
82
|
-
/**
|
|
83
|
-
* Evict from the local cache at the end of every run, whatever its
|
|
84
|
-
* policy (a remote hit is ingested under any), by the same policy
|
|
85
|
-
* `vx cache clean` takes as flags: entries not used for `olderThan`
|
|
86
|
-
* (`'30d'`, `'12h'`), then least-recently-used entries until the cache
|
|
87
|
-
* is under `maxSize` (`'10GB'`, `'500MB'`). Either or both. Omitted →
|
|
88
|
-
* nothing is evicted unless you run `vx cache clean`.
|
|
89
|
-
*/
|
|
90
|
-
retention?: { olderThan?: Duration; maxSize?: string }
|
|
91
82
|
/**
|
|
92
83
|
* Which layers a run reads and writes (`'local:rw,remote:r'`; a layer it
|
|
93
84
|
* leaves out stays on); `--no-cache` and `--force` still apply on top.
|
|
@@ -95,15 +86,6 @@ export interface CacheSettings {
|
|
|
95
86
|
* first, then each remote in declared order.
|
|
96
87
|
*/
|
|
97
88
|
policy?: string
|
|
98
|
-
/**
|
|
99
|
-
* Where this run's cache writes land. Omitted, a run reads and writes the
|
|
100
|
-
* default keys. A name (`'pr-123'`) reads the default key first, then the
|
|
101
|
-
* scope's, and writes only the scope's, so a PR never writes what the
|
|
102
|
-
* default branch reads. Whether a run writes at all is `policy`'s call.
|
|
103
|
-
* A client-side convention, not a security boundary: the boundary is a
|
|
104
|
-
* cache token the server limits.
|
|
105
|
-
*/
|
|
106
|
-
scope?: string
|
|
107
89
|
/** Which remote hits land on disk: `all` (the default), only the requested tasks' (`toplevel`), or `none`. */
|
|
108
90
|
download?: 'all' | 'toplevel' | 'none'
|
|
109
91
|
/** The `cache.inputStability` of every task that does not set its own. */
|
|
@@ -116,6 +98,21 @@ export interface CacheSettings {
|
|
|
116
98
|
* both (`'always'`). Only a run whose policy writes a remote keeps any.
|
|
117
99
|
*/
|
|
118
100
|
upload?: { failures?: 'ci' | 'local' | 'always' }
|
|
101
|
+
/**
|
|
102
|
+
* How the local cache keeps an entry. `embeddings` (default `'32KiB'`):
|
|
103
|
+
* an entry up to this size inside the index; `false`, none. `decompress` (default true): a larger one as its
|
|
104
|
+
* plain files, which a hit copies or clones out with no decode; off, as
|
|
105
|
+
* one `.tar.zst`. `verify` (default `'stat'`): what a hit on plain files
|
|
106
|
+
* checks first, each file's size and mtime, every byte (`'hash'`), or
|
|
107
|
+
* nothing (`'none'`).
|
|
108
|
+
*/
|
|
109
|
+
store?: { embeddings?: false | string; decompress?: boolean; verify?: 'stat' | 'hash' | 'none' }
|
|
110
|
+
/**
|
|
111
|
+
* Cache transfers at once. `download` and `upload` (per remote) default
|
|
112
|
+
* to min(96, max(cores × 3, 64)); `restore`, local hits writing their
|
|
113
|
+
* outputs, to twice `run.concurrency`.
|
|
114
|
+
*/
|
|
115
|
+
concurrency?: { download?: number; upload?: number; restore?: number }
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
/** `WorkspaceConfig.lock`. */
|
|
@@ -795,7 +792,6 @@ export function defineWorkspace<T extends WorkspaceConfig>(
|
|
|
795
792
|
rules?: Known<At<At<T, 'run'>, 'rules'>, WorkspaceRules>
|
|
796
793
|
}
|
|
797
794
|
cache?: Known<At<T, 'cache'>, CacheSettings> & {
|
|
798
|
-
retention?: Known<At<At<T, 'cache'>, 'retention'>, NonNullable<CacheSettings['retention']>>
|
|
799
795
|
inputStability?: Known<At<At<T, 'cache'>, 'inputStability'>, InputStability>
|
|
800
796
|
}
|
|
801
797
|
lock?: Known<At<T, 'lock'>, LockSettings>
|
|
@@ -16,8 +16,8 @@ import path from 'node:path'
|
|
|
16
16
|
* move that spelling into the scan, which finds nothing before the task has
|
|
17
17
|
* written, and turn a working grant into `Read-only file system`. The two
|
|
18
18
|
* predicates answer different questions: whether a declaration must be
|
|
19
|
-
* MATCHED against other declarations (495; `GLOB_WILDCARDS
|
|
20
|
-
*
|
|
19
|
+
* MATCHED against other declarations (495; `GLOB_WILDCARDS`), and whether a
|
|
20
|
+
* grant can be mounted (here).
|
|
21
21
|
* Item 577 gave each question one spelling.
|
|
22
22
|
*/
|
|
23
23
|
export const MOUNT_WILDCARDS = /[*?[\]]/
|
package/src/graph/scheduler.ts
CHANGED
|
@@ -239,6 +239,8 @@ export type ContinueMode = 'never' | 'deps-ok' | 'always'
|
|
|
239
239
|
export interface ScheduleOptions {
|
|
240
240
|
nodes: Map<string, TaskNode>
|
|
241
241
|
concurrency: number
|
|
242
|
+
/** Restore-tier tasks at once; twice `concurrency` unset. */
|
|
243
|
+
restoreConcurrency?: number
|
|
242
244
|
/**
|
|
243
245
|
* An aborted run dispatches nothing further: every task not yet
|
|
244
246
|
* started completes `aborted` (the ones in flight are being killed by
|
|
@@ -604,7 +606,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
604
606
|
// the two lanes share its one slot, where a lane each let an 80 MB
|
|
605
607
|
// restore run inside another task's execution (item 1103).
|
|
606
608
|
let activeRestore = 0
|
|
607
|
-
const restoreConcurrency = 2 * concurrency
|
|
609
|
+
const restoreConcurrency = options.restoreConcurrency ?? 2 * concurrency
|
|
608
610
|
const serial = concurrency === 1
|
|
609
611
|
const execRoom = (): boolean => (serial ? active + activeRestore < 1 : active < concurrency)
|
|
610
612
|
const restoreRoom = (): boolean =>
|
package/src/graph/task-graph.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
checkIgnoreSync,
|
|
5
5
|
ignoreProbes,
|
|
6
6
|
isLiteralPattern,
|
|
7
|
+
literalPath,
|
|
7
8
|
listedSync,
|
|
8
9
|
normalizeGlob,
|
|
9
10
|
relPosix,
|
|
@@ -915,15 +916,17 @@ export function outputsOverlap(rawA: string, rawB: string): boolean {
|
|
|
915
916
|
// A built path proves overlap only between globs as written: a literal's
|
|
916
917
|
// `/**` twin reads it as a directory, and `dist/types.d.ts` met
|
|
917
918
|
// `dist/**/*.js` at `dist/types.d.ts/x.js`, a file no one can write.
|
|
918
|
-
const bothGlobs =
|
|
919
|
+
const bothGlobs = literalPath(rawA) === null && literalPath(rawB) === null
|
|
919
920
|
for (const a of asTrees([rawA])) {
|
|
920
921
|
for (const b of asTrees([rawB])) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
if (
|
|
925
|
-
} else if (
|
|
926
|
-
if (taskGlob(
|
|
922
|
+
const la = literalPath(a)
|
|
923
|
+
const lb = literalPath(b)
|
|
924
|
+
if (la !== null && lb !== null) {
|
|
925
|
+
if (la === lb) return true
|
|
926
|
+
} else if (la !== null) {
|
|
927
|
+
if (taskGlob(b).match(la)) return true
|
|
928
|
+
} else if (lb !== null) {
|
|
929
|
+
if (taskGlob(a).match(lb)) return true
|
|
927
930
|
} else if (a === b || covers(a, b) || covers(b, a) || (bothGlobs && witnessed(a, b)))
|
|
928
931
|
return true
|
|
929
932
|
}
|
|
@@ -1202,13 +1205,13 @@ function collideAcross(
|
|
|
1202
1205
|
* found no match outside the head this cuts, and thousands once `\` and
|
|
1203
1206
|
* `!` were left out (item 746).
|
|
1204
1207
|
*/
|
|
1205
|
-
const GLOB_HEAD_END = /[*?{}\\!]/
|
|
1208
|
+
const GLOB_HEAD_END = /[*?{}[\]\\!]/
|
|
1206
1209
|
|
|
1207
1210
|
/** `firstSegment`'s answer for a glob that can start anywhere. */
|
|
1208
1211
|
const ANYWHERE = ''
|
|
1209
1212
|
|
|
1210
1213
|
/** Where a glob's first segment ends: a separator, or anything `GLOB_HEAD_END` stops at. */
|
|
1211
|
-
const FIRST_SEGMENT_END = /[*?{}\\!/]/
|
|
1214
|
+
const FIRST_SEGMENT_END = /[*?{}[\]\\!/]/
|
|
1212
1215
|
|
|
1213
1216
|
const firstSegments = new Map<string, string>()
|
|
1214
1217
|
|
|
@@ -1315,8 +1318,9 @@ function overlapCandidates<T>(
|
|
|
1315
1318
|
const s = sideOf?.(tasks[i]!) === true ? 1 : 0
|
|
1316
1319
|
side.push(s)
|
|
1317
1320
|
for (const tree of asTrees(globsOf(tasks[i]!) ?? [])) {
|
|
1318
|
-
|
|
1319
|
-
|
|
1321
|
+
const lit = literalPath(tree)
|
|
1322
|
+
if (lit !== null) {
|
|
1323
|
+
literals.push([i, lit])
|
|
1320
1324
|
continue
|
|
1321
1325
|
}
|
|
1322
1326
|
file(globs[s]!, tree, i)
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ export { PLUGIN_HOOKS, type PluginHook } from './config.js'
|
|
|
20
20
|
// the second, so the cgroup walk has one implementation.
|
|
21
21
|
export { machineMemoryBytes, machineParallelism } from './util/index.js'
|
|
22
22
|
// How core reads a task glob — its spelling, and which characters are
|
|
23
|
-
// wildcards (`[` `]`
|
|
23
|
+
// wildcards (`[` `]` and an escape are) — for a plugin that turns declared
|
|
24
24
|
// outputs into something else (`@vzn/vx-reapi`'s literal `output_paths`).
|
|
25
25
|
export { isLiteralPattern, normalizeGlob, restorableOutput } from './util/index.js'
|
|
26
26
|
|
|
@@ -46,6 +46,7 @@ export type {
|
|
|
46
46
|
} from './config.js'
|
|
47
47
|
export { defineProject, defineWorkspace } from './config.js'
|
|
48
48
|
export { definePlugin, refuseUnknownOptions } from './orchestrator/index.js'
|
|
49
|
+
export { branchCacheScope } from './workspace/index.js'
|
|
49
50
|
|
|
50
51
|
// Programmatic engine API: run / plan / prepare (docs/cli.md § Programmatic
|
|
51
52
|
// API). The graph primitives, the cache-key hashing seam and the git / host
|
|
@@ -83,6 +84,7 @@ export type {
|
|
|
83
84
|
CacheLayer,
|
|
84
85
|
PluginCache,
|
|
85
86
|
RemoteCacheLayer,
|
|
87
|
+
RemoteKey,
|
|
86
88
|
RemoteOpenContext,
|
|
87
89
|
RunRecord,
|
|
88
90
|
InvocationRecord,
|
|
@@ -171,6 +173,8 @@ export type {
|
|
|
171
173
|
ScheduleHookContext,
|
|
172
174
|
AdmitContext,
|
|
173
175
|
WorkspaceHookContext,
|
|
176
|
+
ConfigContext,
|
|
177
|
+
BranchFacts,
|
|
174
178
|
DiscoverContext,
|
|
175
179
|
DiscoveredProject,
|
|
176
180
|
PluginSetupContext,
|
|
@@ -41,6 +41,7 @@ const KEYED_BY_EVERY_TASK: ReadonlySet<string> = new Set(PROJECT_CONFIG_FILENAME
|
|
|
41
41
|
function wholeProjects(
|
|
42
42
|
changes: AffectedChanges,
|
|
43
43
|
projects: ReadonlyMap<string, ProjectEntry>,
|
|
44
|
+
byKeys: boolean,
|
|
44
45
|
): Map<string, string | undefined> {
|
|
45
46
|
// Each whole project, with the path no task claims when one did it.
|
|
46
47
|
const whole = new Map<string, string | undefined>([...changes.whole].map((n) => [n, undefined]))
|
|
@@ -50,7 +51,7 @@ function wholeProjects(
|
|
|
50
51
|
(t) => t.exec !== undefined && t.inputs !== undefined,
|
|
51
52
|
)
|
|
52
53
|
const unclaimed = (rel: string): boolean =>
|
|
53
|
-
KEYED_BY_EVERY_TASK.has(rel) || !keyed.some((t) => declaresInput(t, rel, null))
|
|
54
|
+
KEYED_BY_EVERY_TASK.has(rel) || (!byKeys && !keyed.some((t) => declaresInput(t, rel, null)))
|
|
54
55
|
const rel = rels.find(unclaimed)
|
|
55
56
|
if (rel !== undefined) whole.set(name, rel)
|
|
56
57
|
}
|
|
@@ -99,8 +100,10 @@ export function affectedRoots(
|
|
|
99
100
|
projects: ReadonlyMap<string, ProjectEntry>,
|
|
100
101
|
packageGraph: PackageGraph,
|
|
101
102
|
explain?: AffectedExplain,
|
|
103
|
+
/** Only what moves a key: a changed path no task declares reaches no task. */
|
|
104
|
+
byKeys = false,
|
|
102
105
|
): string[] {
|
|
103
|
-
const whole = wholeProjects(changes, projects)
|
|
106
|
+
const whole = wholeProjects(changes, projects, byKeys)
|
|
104
107
|
const seeds = explain === undefined ? null : new Map<string, AffectedReason>()
|
|
105
108
|
const through = explain === undefined ? null : new Map<string, string>()
|
|
106
109
|
const inProject = (name: string, rel: string | undefined): AffectedReason => {
|
|
@@ -226,6 +229,24 @@ export function affectedRoots(
|
|
|
226
229
|
return kept
|
|
227
230
|
}
|
|
228
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The tasks of `nodes` whose key the change can move (a branch's cache
|
|
234
|
+
* scope, owner 2026-10-17): a changed path one of its inputs declares, a
|
|
235
|
+
* whole project, or a task its closure holds. Unlike `--affected`, a path
|
|
236
|
+
* no task declares (a README) reaches none: its key is the default
|
|
237
|
+
* branch's, so it reads the default key.
|
|
238
|
+
*/
|
|
239
|
+
export function keyedByChange(
|
|
240
|
+
nodes: ReadonlyMap<string, TaskNode>,
|
|
241
|
+
changes: AffectedChanges,
|
|
242
|
+
projects: ReadonlyMap<string, ProjectEntry>,
|
|
243
|
+
packageGraph: PackageGraph,
|
|
244
|
+
): Set<string> {
|
|
245
|
+
return new Set(
|
|
246
|
+
affectedRoots(nodes, [...nodes.keys()], changes, projects, packageGraph, undefined, true),
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
229
250
|
/**
|
|
230
251
|
* The requested tasks `--affected` keeps, in their order: one the user
|
|
231
252
|
* named (`pkg#task`), one in a project another include selected outright
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
isUserError,
|
|
19
19
|
machineMemoryBytes,
|
|
20
20
|
machineParallelism,
|
|
21
|
-
parseSize,
|
|
22
21
|
treeBytes,
|
|
23
22
|
} from '../util/index.js'
|
|
24
23
|
import { vxPath } from '../handoff.js'
|
|
@@ -99,8 +98,6 @@ export interface InfoFacts {
|
|
|
99
98
|
string,
|
|
100
99
|
{ value: string | number | boolean | null; source: 'env' | 'workspace' | 'default' }
|
|
101
100
|
>
|
|
102
|
-
/** `cache.retention`, with `maxSize` in bytes; null when none is set. */
|
|
103
|
-
cacheRetention: { olderThan?: string; maxSize?: string; maxBytes?: number } | null
|
|
104
101
|
orphans: { artifacts: number; bytes: number }
|
|
105
102
|
runs24h: number
|
|
106
103
|
hits24h: number
|
|
@@ -261,7 +258,6 @@ async function collectWorkspaceInfo(
|
|
|
261
258
|
cacheEntries: stats.entryCount,
|
|
262
259
|
cacheBytes: stats.totalBytes,
|
|
263
260
|
settings: settingsFact(workspaceConfig),
|
|
264
|
-
cacheRetention: retentionFact(workspaceConfig),
|
|
265
261
|
// A row-less artifact is indexed again only when its task asks for its
|
|
266
262
|
// key; the rest only `vx cache clean` reclaims (after an upgrade's
|
|
267
263
|
// schema reset, most often).
|
|
@@ -395,16 +391,3 @@ function settingsFact(config: WorkspaceConfig | null): InfoFacts['settings'] {
|
|
|
395
391
|
}
|
|
396
392
|
return out
|
|
397
393
|
}
|
|
398
|
-
|
|
399
|
-
function retentionFact(config: WorkspaceConfig | null): InfoFacts['cacheRetention'] {
|
|
400
|
-
const raw = (path: string): string | undefined =>
|
|
401
|
-
process.env[envName(path)] || (atPath(config, path) as string | undefined)
|
|
402
|
-
const olderThan = raw('cache.retention.olderThan')
|
|
403
|
-
const maxSize = raw('cache.retention.maxSize')
|
|
404
|
-
if (olderThan === undefined && maxSize === undefined) return null
|
|
405
|
-
return {
|
|
406
|
-
...(olderThan !== undefined ? { olderThan } : {}),
|
|
407
|
-
// Validated before the run (env) or at load (workspace), so it parses.
|
|
408
|
-
...(maxSize !== undefined ? { maxSize, maxBytes: parseSize(maxSize)! } : {}),
|
|
409
|
-
}
|
|
410
|
-
}
|
|
@@ -335,7 +335,7 @@ async function restoreProbed(
|
|
|
335
335
|
|
|
336
336
|
/**
|
|
337
337
|
* A hit restored, or null when its artifact was removed after the probe (a
|
|
338
|
-
*
|
|
338
|
+
* `vx cache clean`, here or for a workspace sharing the cache directory): a
|
|
339
339
|
* miss. The restore has wiped the declared outputs by then; the run path
|
|
340
340
|
* cleans them again and the task writes them.
|
|
341
341
|
*/
|
|
@@ -50,6 +50,8 @@ export {
|
|
|
50
50
|
type ScheduleHookContext,
|
|
51
51
|
type AdmitContext,
|
|
52
52
|
type WorkspaceHookContext,
|
|
53
|
+
type ConfigContext,
|
|
54
|
+
type BranchFacts,
|
|
53
55
|
type DiscoverContext,
|
|
54
56
|
type DiscoveredProject,
|
|
55
57
|
type InstallPluginsArgs,
|
|
@@ -117,6 +119,8 @@ export {
|
|
|
117
119
|
captureWorkspaceIdentity,
|
|
118
120
|
currentBranch,
|
|
119
121
|
detectCi,
|
|
122
|
+
localBranchScope,
|
|
123
|
+
resolveCacheScope,
|
|
120
124
|
} from './run-context.js'
|
|
121
125
|
export type { CiContext, GitContext, HostContext, WorkspaceIdentity } from './run-context.js'
|
|
122
126
|
export {
|
|
@@ -90,8 +90,16 @@ export interface RunOptions {
|
|
|
90
90
|
* saying it is not.
|
|
91
91
|
*/
|
|
92
92
|
remoteRequested?: boolean
|
|
93
|
-
/** This run's
|
|
93
|
+
/** This run's cache scope, above `VX_CACHE_SCOPE`, a plugin's and the branch's (an embedder's). */
|
|
94
94
|
cacheScope?: string
|
|
95
|
+
/**
|
|
96
|
+
* What the branch changed, for a scoped run: the tasks whose key it can
|
|
97
|
+
* move read the scope's key, every other task the default key (one
|
|
98
|
+
* lookup each). Absent, the run diffs the branch from `scopeBase`.
|
|
99
|
+
*/
|
|
100
|
+
scopeChanges?: AffectedChanges
|
|
101
|
+
/** The commit a scoped run diffs the branch from (`--affected`'s base); absent, a plugin's, else the merge base with the default branch. */
|
|
102
|
+
scopeBase?: string
|
|
95
103
|
/**
|
|
96
104
|
* The default policy's remote write is off (`remote:r`): the CLI passes it
|
|
97
105
|
* off CI (`CI` unset, `0` or `false`) unless `--cache-policy` or
|
|
@@ -122,6 +130,12 @@ export interface RunOptions {
|
|
|
122
130
|
* Locally-placed tasks always write in place and ignore this.
|
|
123
131
|
*/
|
|
124
132
|
download?: 'all' | 'toplevel' | 'none'
|
|
133
|
+
/** Remote downloads at once (`cache.concurrency.download`). */
|
|
134
|
+
downloadConcurrency?: number
|
|
135
|
+
/** Uploads at once, per remote (`cache.concurrency.upload`). */
|
|
136
|
+
uploadConcurrency?: number
|
|
137
|
+
/** Local hits restoring at once (`cache.concurrency.restore`); twice `concurrency` unset. */
|
|
138
|
+
restoreConcurrency?: number
|
|
125
139
|
/**
|
|
126
140
|
* Run intent, derived by the CLI from selection flags: 'broad' iff
|
|
127
141
|
* `--all` / `--filter` / `--affected` was passed, else 'focused'.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import path from 'node:path'
|
|
11
11
|
import {
|
|
12
|
+
type BranchScope,
|
|
12
13
|
type Cache,
|
|
13
14
|
type CacheLayer,
|
|
14
15
|
type CachePolicy,
|
|
@@ -34,7 +35,7 @@ import type {
|
|
|
34
35
|
RunReport,
|
|
35
36
|
ScheduleHookContext,
|
|
36
37
|
VxPlugin,
|
|
37
|
-
|
|
38
|
+
ConfigContext,
|
|
38
39
|
} from './plugin.js'
|
|
39
40
|
import type { ProjectMeta } from '../workspace/index.js'
|
|
40
41
|
|
|
@@ -137,7 +138,7 @@ function assertEditedInPlace(
|
|
|
137
138
|
export async function applyConfigHooks(
|
|
138
139
|
plugins: readonly VxPlugin[],
|
|
139
140
|
workspace: WorkspaceConfig,
|
|
140
|
-
ctx:
|
|
141
|
+
ctx: ConfigContext,
|
|
141
142
|
/** Runs after EACH plugin's edit, so a refusal can name the plugin that made it. */
|
|
142
143
|
afterEach?: (plugin: VxPlugin) => void,
|
|
143
144
|
): Promise<void> {
|
|
@@ -555,6 +556,10 @@ export interface CacheHost extends CacheContext {
|
|
|
555
556
|
readonly policy: CachePolicy
|
|
556
557
|
/** Filled with each remote and what its policy lets it do, for the plugins' `ctx.cache`. */
|
|
557
558
|
readonly remotes?: ResolvedRemote[]
|
|
559
|
+
/** The run's branch scope, which picks each remote key. */
|
|
560
|
+
readonly branch?: BranchScope
|
|
561
|
+
/** Uploads at once, per remote (`cache.concurrency.upload`). */
|
|
562
|
+
readonly uploadConcurrency?: number
|
|
558
563
|
}
|
|
559
564
|
|
|
560
565
|
/**
|
|
@@ -569,7 +574,7 @@ export async function resolveCache(
|
|
|
569
574
|
plugins: readonly VxPlugin[],
|
|
570
575
|
host: CacheHost,
|
|
571
576
|
): Promise<CacheLayer> {
|
|
572
|
-
const { localCache, policy, remotes, ...ctx } = host
|
|
577
|
+
const { localCache, policy, remotes, branch, uploadConcurrency, ...ctx } = host
|
|
573
578
|
const resolved: ResolvedRemote[] = []
|
|
574
579
|
for (const plugin of plugins) {
|
|
575
580
|
if (plugin.cache === undefined) continue
|
|
@@ -586,7 +591,9 @@ export async function resolveCache(
|
|
|
586
591
|
resolved.push(entry)
|
|
587
592
|
remotes?.push(entry)
|
|
588
593
|
}
|
|
589
|
-
return resolved.length === 0
|
|
594
|
+
return resolved.length === 0
|
|
595
|
+
? localCache
|
|
596
|
+
: new LayeredCache(localCache, resolved, branch, uploadConcurrency)
|
|
590
597
|
}
|
|
591
598
|
|
|
592
599
|
/** The plugin each resolved executor came from, so a hook of it that throws is named. */
|
|
@@ -61,8 +61,10 @@ export interface VxPlugin {
|
|
|
61
61
|
* The workspace config, before anything is derived from it (concurrency,
|
|
62
62
|
* cacheDir, timeout, …). `plugins` is already fixed by the time this runs.
|
|
63
63
|
* Core re-validates after each plugin, as it does a user's file.
|
|
64
|
+
* `ctx.setBranch` names what the CI knows of the branch: its cache scope
|
|
65
|
+
* and the commit it is compared with.
|
|
64
66
|
*/
|
|
65
|
-
config?(workspace: WorkspaceConfig, ctx:
|
|
67
|
+
config?(workspace: WorkspaceConfig, ctx: ConfigContext): void | Promise<void>
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
70
|
* Projects beyond the directories holding a vx.config, their names, and
|
|
@@ -340,6 +342,25 @@ export interface WorkspaceHookContext {
|
|
|
340
342
|
warn(message: string): void
|
|
341
343
|
}
|
|
342
344
|
|
|
345
|
+
/**
|
|
346
|
+
* What a CI knows of the run's branch (owner, 2026-10-17): `scope`, the
|
|
347
|
+
* cache scope (`'vznjs/vx:feat-x'`; `VX_CACHE_SCOPE` beats it, and it
|
|
348
|
+
* beats the one git names), and `base`, the commit the branch is compared
|
|
349
|
+
* with (a pull request's base), for a bare `--affected` and for which
|
|
350
|
+
* tasks read the scope. `trusted`: the run has no scope, whatever git
|
|
351
|
+
* names, and reads and writes only the default keys (a merge queue's run,
|
|
352
|
+
* owner 2026-10-18). A later plugin's field wins.
|
|
353
|
+
*/
|
|
354
|
+
export interface BranchFacts {
|
|
355
|
+
scope?: string
|
|
356
|
+
base?: string
|
|
357
|
+
trusted?: boolean
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface ConfigContext extends WorkspaceHookContext {
|
|
361
|
+
setBranch(facts: BranchFacts): void
|
|
362
|
+
}
|
|
363
|
+
|
|
343
364
|
export interface DiscoverContext extends WorkspaceHookContext {
|
|
344
365
|
/** Where vx's cache lives: a plugin may keep what it read to answer here. */
|
|
345
366
|
readonly cacheDir: string
|
|
@@ -485,7 +506,7 @@ export interface ScheduleHookContext extends BaseContext {
|
|
|
485
506
|
/** A `cache` hook's context: the workspace and the run's scope, never the local store or the policy, which core owns. */
|
|
486
507
|
export interface CacheContext extends BaseContext {
|
|
487
508
|
/**
|
|
488
|
-
* The run's
|
|
509
|
+
* The run's cache scope (`'vznjs/vx:feat-x'`), `undefined` when none. Core already keys a scoped run's
|
|
489
510
|
* writes apart; a remote reads it to pick a credential or check a token's
|
|
490
511
|
* claims (an OIDC branch) for every call it makes.
|
|
491
512
|
*/
|