@vzn/vx 0.0.594 → 0.0.596
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 +1 -1
- package/package.json +5 -5
- package/src/bin.ts +26 -11
- package/src/cache/archive.ts +2 -2
- package/src/cache/cache.ts +235 -174
- package/src/cache/chained-cache.ts +25 -7
- package/src/cache/config-evals.ts +19 -4
- package/src/cache/git-inputs.ts +19 -8
- package/src/cache/index.ts +1 -0
- package/src/cache/inputs.ts +122 -49
- package/src/cache/layer.ts +7 -5
- package/src/cache/layered-cache.ts +25 -2
- package/src/cache/policy.ts +5 -0
- package/src/cache/tar-stream.ts +1 -1
- package/src/cache/zstd.ts +10 -4
- package/src/cli/cache.ts +11 -15
- package/src/cli/completions.ts +42 -6
- package/src/cli/foreign-flags.ts +26 -5
- package/src/cli/help.ts +5 -3
- package/src/cli/init.ts +1 -1
- package/src/cli/last.ts +4 -1
- package/src/cli/plan-format.ts +9 -0
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +60 -42
- package/src/cli/select.ts +30 -3
- package/src/cli/show.ts +42 -3
- package/src/cli/watch-filter.ts +77 -13
- package/src/cli/watch-fs.ts +15 -7
- package/src/cli/watch-judge.ts +16 -9
- package/src/cli/watch.ts +22 -15
- package/src/config.ts +26 -26
- package/src/exec/env.ts +5 -0
- package/src/exec/index.ts +1 -0
- package/src/exec/proc-sample.ts +6 -2
- package/src/exec/runner.ts +98 -40
- package/src/exec/sandbox-paths.ts +12 -6
- package/src/exec/sandbox-runtime.ts +136 -44
- package/src/exec/sandbox-violations.ts +58 -11
- package/src/graph/index.ts +1 -0
- package/src/graph/scheduler.ts +29 -13
- package/src/graph/task-graph.ts +134 -25
- package/src/index.ts +6 -0
- package/src/orchestrator/admission.ts +12 -3
- package/src/orchestrator/events.ts +10 -1
- package/src/orchestrator/execute-task.ts +65 -20
- package/src/orchestrator/failure-mode.ts +8 -2
- package/src/orchestrator/framed-output.ts +3 -3
- package/src/orchestrator/hit-restore.ts +6 -3
- package/src/orchestrator/local-shortcircuit.ts +4 -1
- package/src/orchestrator/lockfile-claim.ts +43 -46
- package/src/orchestrator/logger.ts +11 -6
- package/src/orchestrator/metrics.ts +19 -8
- package/src/orchestrator/miss-save.ts +2 -2
- package/src/orchestrator/options.ts +7 -0
- package/src/orchestrator/plan.ts +28 -4
- package/src/orchestrator/plugin-host.ts +25 -3
- package/src/orchestrator/plugin.ts +39 -17
- package/src/orchestrator/prepare.ts +13 -4
- package/src/orchestrator/run-artifacts.ts +2 -1
- package/src/orchestrator/run.ts +72 -12
- package/src/orchestrator/sandbox-request.ts +65 -9
- package/src/orchestrator/save-lane.ts +5 -5
- package/src/orchestrator/signals.ts +5 -2
- package/src/orchestrator/summary.ts +16 -5
- package/src/orchestrator/telemetry.ts +16 -7
- package/src/util/index.ts +7 -1
- package/src/util/paths.ts +23 -3
- package/src/util/secret-mask.ts +36 -0
- package/src/util/settle.ts +15 -0
- package/src/workspace/affected.ts +30 -8
- package/src/workspace/config-cache.ts +25 -4
- package/src/workspace/config-imports.ts +8 -6
- package/src/workspace/config-schema.ts +74 -9
- package/src/workspace/filter.ts +19 -0
- package/src/workspace/migrate-scripts.ts +5 -3
- package/src/workspace/migration.ts +1 -1
- package/src/workspace/package-graph.ts +15 -3
- package/src/workspace/project-loader.ts +76 -2
- package/src/workspace/repo-id.ts +42 -4
- package/src/workspace/workspace.ts +98 -20
package/src/workspace/repo-id.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
lstatSync,
|
|
14
14
|
openSync,
|
|
15
15
|
readFileSync,
|
|
16
|
+
realpathSync,
|
|
16
17
|
statSync,
|
|
17
18
|
} from 'node:fs'
|
|
18
19
|
import path from 'node:path'
|
|
@@ -25,12 +26,20 @@ import path from 'node:path'
|
|
|
25
26
|
* git: those share nothing.
|
|
26
27
|
*/
|
|
27
28
|
export async function repoIdOf(root: string): Promise<string | null> {
|
|
28
|
-
|
|
29
|
+
// Git walks the physical path; the logical one found no `.git` through a
|
|
30
|
+
// symlink from outside the repository, and an alias inside it another id.
|
|
31
|
+
let real: string
|
|
32
|
+
try {
|
|
33
|
+
real = realpathSync(root)
|
|
34
|
+
} catch {
|
|
35
|
+
real = path.resolve(root)
|
|
36
|
+
}
|
|
37
|
+
const located = locateGitDir(real)
|
|
29
38
|
if (located === null) return null
|
|
30
39
|
const identity =
|
|
31
40
|
(await remoteIdentity(root, located.commonDir)) ?? (await firstCommit(located.commonDir, root))
|
|
32
41
|
if (identity === null) return null
|
|
33
|
-
const rel = path.relative(located.gitRoot,
|
|
42
|
+
const rel = path.relative(located.gitRoot, real).split(path.sep).join('/')
|
|
34
43
|
const key = sha256(`${identity}#${rel}`)
|
|
35
44
|
return sha256(key).slice(0, 16)
|
|
36
45
|
}
|
|
@@ -108,11 +117,40 @@ function readOwnedFile(p: string): string | null {
|
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* The remote's identity from the config file, else from `git remote -v`
|
|
122
|
+
* when the file cannot settle it: a remote none of whose urls parse (a
|
|
123
|
+
* rewrite may live in a file vx does not read, as Nx asks git), or
|
|
124
|
+
* remotes git could read from `config.worktree`.
|
|
125
|
+
*/
|
|
112
126
|
async function remoteIdentity(root: string, commonDir: string): Promise<string | null> {
|
|
113
127
|
const config = readOwnedFile(path.join(commonDir, 'config'))
|
|
114
128
|
const remotes = config === null ? null : parseGitConfigRemotes(config)
|
|
115
|
-
|
|
129
|
+
if (remotes !== null) {
|
|
130
|
+
const picked = pickRemote(remotes)
|
|
131
|
+
// A remote-less repo with nothing to pull one from elsewhere spawns
|
|
132
|
+
// nothing: git would answer the same, and that spawn was a cost per run.
|
|
133
|
+
if (picked !== null || (remotes.length === 0 && !worktreeConfigOn(config!))) return picked
|
|
134
|
+
}
|
|
135
|
+
return pickRemote(await remotesFromGit(root))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `extensions.worktreeConfig` set: git reads `config.worktree` too. */
|
|
139
|
+
function worktreeConfigOn(contents: string): boolean {
|
|
140
|
+
let inExtensions = false
|
|
141
|
+
for (const raw of contents.split('\n')) {
|
|
142
|
+
const line = raw.trim()
|
|
143
|
+
if (line.startsWith('[')) {
|
|
144
|
+
inExtensions = /^\[\s*extensions\s*\]/i.test(line)
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const m = /^worktreeconfig\s*(?:=\s*(\S*))?/i.exec(line)
|
|
148
|
+
if (inExtensions && m !== null && !/^(false|no|off|0)$/i.test(m[1] ?? 'true')) return true
|
|
149
|
+
}
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function pickRemote(urls: [string, string][]): string | null {
|
|
116
154
|
const found = new Map<string, string>()
|
|
117
155
|
let first: string | null = null
|
|
118
156
|
for (const [name, url] of urls) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { constants, type Dirent } from 'node:fs'
|
|
1
|
+
import { constants, existsSync, type Dirent } from 'node:fs'
|
|
2
2
|
import { access, readdir, realpath, stat } from 'node:fs/promises'
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
@@ -31,8 +31,18 @@ export interface Workspace {
|
|
|
31
31
|
root: string
|
|
32
32
|
/** Glob patterns relative to root that match project directories. */
|
|
33
33
|
packageGlobs: string[]
|
|
34
|
+
/** The package manager's catalogs, when the root declares any. */
|
|
35
|
+
catalogs?: Catalogs
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Catalog name → dependency → the spec a `catalog:<name>` entry stands
|
|
40
|
+
* for; `catalog:` alone is `default`. pnpm's from `pnpm-workspace.yaml`
|
|
41
|
+
* (`catalog`, `catalogs`), bun's from the root `package.json` (the same
|
|
42
|
+
* keys, top level or under `workspaces`).
|
|
43
|
+
*/
|
|
44
|
+
export type Catalogs = ReadonlyMap<string, Readonly<Record<string, unknown>>>
|
|
45
|
+
|
|
36
46
|
export interface ProjectMeta {
|
|
37
47
|
/** Canonical name from package.json. */
|
|
38
48
|
name: string
|
|
@@ -41,6 +51,8 @@ export interface ProjectMeta {
|
|
|
41
51
|
packageJson: PackageJson
|
|
42
52
|
/** Absolute path to vx.config.{ts,mts,js,mjs,cts,cjs} or null. */
|
|
43
53
|
configPath: string | null
|
|
54
|
+
/** The workspace's catalogs, which the package graph resolves `catalog:` specs through. */
|
|
55
|
+
catalogs?: Catalogs
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
/** A discovered project joined with its loaded vx config. */
|
|
@@ -168,9 +180,12 @@ async function claimsMember(
|
|
|
168
180
|
const normalized = pattern.replace(/\/+$/, '')
|
|
169
181
|
// `.` means the root itself is the project — never a directory below it.
|
|
170
182
|
if (normalized === '' || normalized === '.') continue
|
|
171
|
-
|
|
183
|
+
// Matched as discovery scans, on the manifest: `packages/**` lists
|
|
184
|
+
// `packages/package.json`, while against the directory `**` needs a
|
|
185
|
+
// segment below `packages`, and a run from there was its own root.
|
|
186
|
+
const glob = new Bun.Glob(`${normalized}/package.json`)
|
|
172
187
|
for (const rel of rels) {
|
|
173
|
-
if (!glob.match(rel)) continue
|
|
188
|
+
if (!glob.match(`${rel}/package.json`)) continue
|
|
174
189
|
// `match` has no `dot: false`: `packages/*` matched `packages/.tpl`,
|
|
175
190
|
// which discovery skips, and a run from inside it found a workspace
|
|
176
191
|
// that does not list it. Such a path asks discovery's own walk.
|
|
@@ -231,19 +246,34 @@ function excludedBy(rel: string, negative: readonly string[]): boolean {
|
|
|
231
246
|
* the root itself is the only project, hence `['.']`.
|
|
232
247
|
*/
|
|
233
248
|
async function readPackageGlobs(dir: string, reads?: LoadReads): Promise<string[] | null> {
|
|
249
|
+
return (await readRootManifests(dir, reads)).globs
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** `readPackageGlobs`, and the catalogs the same manifests declare. */
|
|
253
|
+
async function readRootManifests(
|
|
254
|
+
dir: string,
|
|
255
|
+
reads?: LoadReads,
|
|
256
|
+
): Promise<{ globs: string[] | null; catalogs: Catalogs | undefined }> {
|
|
234
257
|
const yamlPath = path.join(dir, 'pnpm-workspace.yaml')
|
|
235
258
|
const yaml = await readOnce(reads, yamlPath)
|
|
236
259
|
let yamlWithoutPackages = false
|
|
260
|
+
// pnpm reads its catalogs from this file alone, bun from package.json.
|
|
261
|
+
let catalogs: Catalogs | undefined
|
|
237
262
|
if (yaml !== null) {
|
|
238
263
|
const parsed = parseManifest(decoder.decode(yaml), yamlPath, Bun.YAML.parse)
|
|
239
264
|
if (parsed !== null && parsed !== undefined) {
|
|
240
265
|
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
241
266
|
throw new UserError(`${yamlPath}: must be a mapping (\`packages:\` and pnpm's settings)`)
|
|
242
267
|
}
|
|
268
|
+
catalogs = catalogsOf([parsed])
|
|
243
269
|
const packages = (parsed as { packages?: unknown }).packages
|
|
244
|
-
|
|
270
|
+
// A list commented out leaves `packages:` null, which pnpm reads as
|
|
271
|
+
// absent; vx refused it as "must be an array" (D-149).
|
|
272
|
+
if (packages !== undefined && packages !== null) {
|
|
273
|
+
return { globs: assertGlobList(packages, yamlPath, 'packages'), catalogs }
|
|
274
|
+
}
|
|
245
275
|
}
|
|
246
|
-
// No `packages
|
|
276
|
+
// No `packages:` (or an empty one): pnpm 10 keeps its settings and catalogs in this file
|
|
247
277
|
// for a single-package repo too. The root's package.json decides, as it
|
|
248
278
|
// would without the file; read as an empty list, the root found zero
|
|
249
279
|
// projects and every verb ran nothing and exited 0 (item 984).
|
|
@@ -251,18 +281,46 @@ async function readPackageGlobs(dir: string, reads?: LoadReads): Promise<string[
|
|
|
251
281
|
}
|
|
252
282
|
const pkgPath = path.join(dir, 'package.json')
|
|
253
283
|
const pkgBytes = await readOnce(reads, pkgPath)
|
|
254
|
-
if (pkgBytes === null) return yamlWithoutPackages ? [] : null
|
|
284
|
+
if (pkgBytes === null) return { globs: yamlWithoutPackages ? [] : null, catalogs }
|
|
255
285
|
const pkg = parsePackageJson(decoder.decode(pkgBytes), pkgPath)
|
|
256
286
|
const ws = pkg.workspaces as unknown
|
|
257
|
-
if (
|
|
287
|
+
if (yaml === null) {
|
|
288
|
+
catalogs = catalogsOf(
|
|
289
|
+
ws !== null && typeof ws === 'object' && !Array.isArray(ws) ? [pkg, ws] : [pkg],
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
if (ws === undefined || ws === null) return { globs: ['.'], catalogs }
|
|
258
293
|
if (ws && typeof ws === 'object' && !Array.isArray(ws) && 'packages' in ws) {
|
|
259
|
-
|
|
294
|
+
const globs = assertGlobList(
|
|
260
295
|
(ws as { packages?: unknown }).packages ?? [],
|
|
261
296
|
pkgPath,
|
|
262
297
|
'workspaces.packages',
|
|
263
298
|
)
|
|
299
|
+
return { globs, catalogs }
|
|
300
|
+
}
|
|
301
|
+
return { globs: assertGlobList(ws, pkgPath, 'workspaces'), catalogs }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The `catalog` and `catalogs` keys of the manifests (bun reads them at
|
|
306
|
+
* the top level and under `workspaces`), or undefined when none declares
|
|
307
|
+
* one. An entry that is not a mapping is no catalog: a `catalog:` spec
|
|
308
|
+
* through it keeps its edge, as one through an unknown catalog does.
|
|
309
|
+
*/
|
|
310
|
+
function catalogsOf(manifests: readonly object[]): Catalogs | undefined {
|
|
311
|
+
const isMap = (v: unknown): v is Record<string, unknown> =>
|
|
312
|
+
typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
313
|
+
let out: Map<string, Readonly<Record<string, unknown>>> | undefined
|
|
314
|
+
for (const m of manifests) {
|
|
315
|
+
const { catalog, catalogs } = m as { catalog?: unknown; catalogs?: unknown }
|
|
316
|
+
if (isMap(catalogs)) {
|
|
317
|
+
for (const [name, c] of Object.entries(catalogs))
|
|
318
|
+
if (isMap(c)) (out ??= new Map()).set(name, c)
|
|
319
|
+
}
|
|
320
|
+
// `catalog` is the default catalog to pnpm and bun.
|
|
321
|
+
if (isMap(catalog)) (out ??= new Map()).set('default', catalog)
|
|
264
322
|
}
|
|
265
|
-
return
|
|
323
|
+
return out
|
|
266
324
|
}
|
|
267
325
|
|
|
268
326
|
/**
|
|
@@ -292,8 +350,13 @@ export async function unreachedPackages(workspace: Workspace): Promise<string[]>
|
|
|
292
350
|
return found.sort()
|
|
293
351
|
}
|
|
294
352
|
|
|
295
|
-
/**
|
|
296
|
-
|
|
353
|
+
/**
|
|
354
|
+
* The line for `unreachedPackages`' finding: the cause, the packages, the
|
|
355
|
+
* glob to add, and where. A `pnpm-workspace.yaml` with no `packages:` (pnpm
|
|
356
|
+
* keeps its settings there) is where pnpm reads the globs: the hint said
|
|
357
|
+
* there was no such file and named package.json, beside ngrx's (2026-10-07).
|
|
358
|
+
*/
|
|
359
|
+
export function unreachedHint(unreached: readonly string[], root: string): string {
|
|
297
360
|
const n = unreached.length
|
|
298
361
|
const shown = unreached.slice(0, 3).join(', ') + (n > 3 ? ` and ${n - 3} more` : '')
|
|
299
362
|
const globs = [
|
|
@@ -301,11 +364,11 @@ export function unreachedHint(unreached: readonly string[]): string {
|
|
|
301
364
|
]
|
|
302
365
|
.map((g) => `"${g}"`)
|
|
303
366
|
.join(', ')
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
`
|
|
307
|
-
`
|
|
308
|
-
|
|
367
|
+
const rest = `so the root is the only project and ${n} package.json below it ${n === 1 ? 'is' : 'are'} not: ${shown}.`
|
|
368
|
+
return existsSync(path.join(root, 'pnpm-workspace.yaml'))
|
|
369
|
+
? `pnpm-workspace.yaml lists no \`packages\`, ${rest} Add \`packages: [${globs}]\` to pnpm-workspace.yaml and re-run.`
|
|
370
|
+
: `package.json declares no \`workspaces\` (and there is no pnpm-workspace.yaml), ${rest} ` +
|
|
371
|
+
`Add \`"workspaces": [${globs}]\` to package.json and re-run.`
|
|
309
372
|
}
|
|
310
373
|
|
|
311
374
|
/**
|
|
@@ -435,14 +498,14 @@ const repoIds = new Map<string, Promise<string | null>>()
|
|
|
435
498
|
* itself is treated as a single-project workspace.
|
|
436
499
|
*/
|
|
437
500
|
export async function loadWorkspace(root: string, reads?: LoadReads): Promise<Workspace> {
|
|
438
|
-
const packageGlobs = await
|
|
501
|
+
const { globs: packageGlobs, catalogs } = await readRootManifests(root, reads)
|
|
439
502
|
if (packageGlobs === null) {
|
|
440
503
|
// The CLI never lands here (findWorkspaceRoot returns only a dir that
|
|
441
504
|
// passes one of the two checks); a direct call of this public function
|
|
442
505
|
// on a bare dir does (D-58).
|
|
443
506
|
throw new UserError(`workspace root ${root} has neither pnpm-workspace.yaml nor package.json`)
|
|
444
507
|
}
|
|
445
|
-
return { root, packageGlobs }
|
|
508
|
+
return catalogs === undefined ? { root, packageGlobs } : { root, packageGlobs, catalogs }
|
|
446
509
|
}
|
|
447
510
|
|
|
448
511
|
// `<dir>/*` — the shape of nearly every workspace glob. Anything with another
|
|
@@ -720,6 +783,16 @@ export async function discoverProjects(
|
|
|
720
783
|
for (const entry of loaded) {
|
|
721
784
|
if (entry === null) continue
|
|
722
785
|
const { dir, pkg, configPath } = entry
|
|
786
|
+
// npm and pnpm take `../ext/*`, but `--affected` asks git from the root
|
|
787
|
+
// and sees nothing outside it: an edit there moved the task's key and
|
|
788
|
+
// selected nothing, green.
|
|
789
|
+
const rel = relPosix(workspace.root, dir)
|
|
790
|
+
if (rel === '..' || rel.startsWith('../') || path.isAbsolute(rel)) {
|
|
791
|
+
throw new UserError(
|
|
792
|
+
`workspace member ${rel} (${dir}) is outside the workspace root ${workspace.root}: ` +
|
|
793
|
+
'vx keeps every project under the root. Move the workspace root up to a directory that holds every member.',
|
|
794
|
+
)
|
|
795
|
+
}
|
|
723
796
|
if (!pkg.name) {
|
|
724
797
|
nameless?.push(dir)
|
|
725
798
|
// A nameless manifest can't be addressed, filtered, or made affected —
|
|
@@ -753,7 +826,7 @@ export async function discoverProjects(
|
|
|
753
826
|
}
|
|
754
827
|
if (group.length === 1) {
|
|
755
828
|
const { dir, pkg, configPath } = group[0]!
|
|
756
|
-
projects.push({ name, dir, packageJson: pkg, configPath })
|
|
829
|
+
projects.push(withCatalogs({ name, dir, packageJson: pkg, configPath }, workspace))
|
|
757
830
|
continue
|
|
758
831
|
}
|
|
759
832
|
// The root's own manifest is '' relative to itself, and the refusal read
|
|
@@ -884,5 +957,10 @@ export async function namedProject(
|
|
|
884
957
|
`${where} "${named.name}" at ${shown}: its package.json names it "${pkg.name ?? ''}"`,
|
|
885
958
|
)
|
|
886
959
|
}
|
|
887
|
-
return { name: named.name, dir, packageJson: pkg, configPath }
|
|
960
|
+
return withCatalogs({ name: named.name, dir, packageJson: pkg, configPath }, workspace)
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function withCatalogs(meta: ProjectMeta, workspace: Workspace): ProjectMeta {
|
|
964
|
+
if (workspace.catalogs !== undefined) meta.catalogs = workspace.catalogs
|
|
965
|
+
return meta
|
|
888
966
|
}
|