@vzn/vx 0.0.564 → 0.0.566
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.566",
|
|
4
4
|
"description": "The fastest task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.566",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.566",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.566",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.566"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/watch-filter.ts
CHANGED
|
@@ -229,6 +229,7 @@ export function makeRootEventFilter(
|
|
|
229
229
|
projectDirs: readonly string[],
|
|
230
230
|
workspaceInputs: readonly string[],
|
|
231
231
|
claimedRootFiles: ReadonlySet<string> = new Set(),
|
|
232
|
+
fenced: (ownDir: string, abs: string) => boolean = () => false,
|
|
232
233
|
): (filename: string) => boolean {
|
|
233
234
|
const dirs = projectDirs.map((d) => path.resolve(d))
|
|
234
235
|
const globs = workspaceInputs
|
|
@@ -257,12 +258,33 @@ export function makeRootEventFilter(
|
|
|
257
258
|
return true
|
|
258
259
|
}
|
|
259
260
|
const abs = path.resolve(workspaceRoot, filename)
|
|
260
|
-
for (const d of dirs)
|
|
261
|
+
for (const d of dirs)
|
|
262
|
+
if (abs === d || (abs.startsWith(d + path.sep) && !fenced(d, abs))) return true
|
|
261
263
|
for (const g of globs) if (g.match(rel)) return true
|
|
262
264
|
return false
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Whether `abs`, under the project at `ownDir`, lies inside another
|
|
270
|
+
* project nested there: the key's boundary (`computeNestedProjectDirs`)
|
|
271
|
+
* leaves such a file out, so it is no edit of `ownDir`'s. A root project
|
|
272
|
+
* watched its whole tree and ran a cycle for every edit in a nested one
|
|
273
|
+
* (X-42). The fences are the projects with a config, which fence a key
|
|
274
|
+
* whatever the plugins; a config-less one may not, and an edit there
|
|
275
|
+
* still counts. The fence's own config is let through: it coming or going
|
|
276
|
+
* moves the boundary, and the cycle it starts re-reads the set.
|
|
277
|
+
*/
|
|
278
|
+
export function makeFence(fenceDirs: readonly string[]): (ownDir: string, abs: string) => boolean {
|
|
279
|
+
const fences = fenceDirs.map((d) => path.resolve(d))
|
|
280
|
+
return (ownDir, abs) => {
|
|
281
|
+
const config = PROJECT_CONFIGS.has(path.basename(abs)) ? path.dirname(abs) : undefined
|
|
282
|
+
return fences.some(
|
|
283
|
+
(f) => f !== config && f.startsWith(ownDir + path.sep) && abs.startsWith(f + path.sep),
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
266
288
|
/**
|
|
267
289
|
* A file whose edit can change what the loop watches: a `package.json`
|
|
268
290
|
* (a dependency added under `--filter` widens the closure), a project
|
package/src/cli/watch.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
isWorkspaceConfigFile,
|
|
38
38
|
gitFiles,
|
|
39
39
|
isWorkspaceFingerprintFile,
|
|
40
|
+
makeFence,
|
|
40
41
|
makeRootEventFilter,
|
|
41
42
|
makeWatchIgnore,
|
|
42
43
|
shapesWatchedSet,
|
|
@@ -242,6 +243,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
242
243
|
workspaceConfigImports: swept.workspaceConfigImports,
|
|
243
244
|
memberBases: memberBaseDirs(workspace),
|
|
244
245
|
packageDirs: new Set(allProjects.map((p) => p.dir)),
|
|
246
|
+
fenceDirs: fenceDirs(allProjects),
|
|
245
247
|
// The workspace as the cycle that just ran saw it: a package added or
|
|
246
248
|
// removed since the loop armed joins or leaves the watched set. The
|
|
247
249
|
// scope is the one resolved at start; a new package joins it only as a
|
|
@@ -266,6 +268,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
266
268
|
configImports: sweep.configImports,
|
|
267
269
|
workspaceConfigImports: sweep.workspaceConfigImports,
|
|
268
270
|
packageDirs: new Set(all.map((p) => p.dir)),
|
|
271
|
+
fenceDirs: fenceDirs(all),
|
|
269
272
|
}
|
|
270
273
|
},
|
|
271
274
|
// Under --frozen every cycle's configs are the lock's, so a re-lock is
|
|
@@ -312,10 +315,15 @@ interface WatchLoopArgs {
|
|
|
312
315
|
memberBases: readonly string[]
|
|
313
316
|
/** Every package's directory, in scope or not: a member base's other entries are packages still to come. */
|
|
314
317
|
packageDirs: ReadonlySet<string>
|
|
318
|
+
/** Every project with a config, in scope or not: a project's key leaves out what lies in one nested under it. */
|
|
319
|
+
fenceDirs: readonly string[]
|
|
315
320
|
/** The watched set again, after a cycle that followed an event which can change it. */
|
|
316
321
|
rediscover: () => Promise<Rediscovered>
|
|
317
322
|
}
|
|
318
323
|
|
|
324
|
+
const fenceDirs = (all: readonly ProjectMeta[]): string[] =>
|
|
325
|
+
all.filter((p) => p.configPath !== null).map((p) => p.dir)
|
|
326
|
+
|
|
319
327
|
interface Rediscovered {
|
|
320
328
|
projects: readonly ProjectMeta[]
|
|
321
329
|
claimedRootFiles: ReadonlySet<string>
|
|
@@ -328,6 +336,7 @@ interface Rediscovered {
|
|
|
328
336
|
configImports: readonly string[]
|
|
329
337
|
workspaceConfigImports: readonly string[]
|
|
330
338
|
packageDirs: ReadonlySet<string>
|
|
339
|
+
fenceDirs: readonly string[]
|
|
331
340
|
}
|
|
332
341
|
|
|
333
342
|
async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
@@ -424,7 +433,14 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
424
433
|
// project would otherwise trigger every save during `bun install` —
|
|
425
434
|
// and vx's own cache writes would trigger a cycle that writes again.
|
|
426
435
|
let isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs)
|
|
427
|
-
let
|
|
436
|
+
let fenced = makeFence(args.fenceDirs)
|
|
437
|
+
let matters = makeRootEventFilter(
|
|
438
|
+
workspaceRoot,
|
|
439
|
+
projectDirs,
|
|
440
|
+
workspaceInputs,
|
|
441
|
+
claimedRootFiles,
|
|
442
|
+
fenced,
|
|
443
|
+
)
|
|
428
444
|
/** Since the last cycle, a member came or went, or a file that shapes the watched set changed (`shapesWatchedSet`). */
|
|
429
445
|
let reread = false
|
|
430
446
|
|
|
@@ -472,9 +488,10 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
472
488
|
const armProject = (proj: ProjectMeta): void => {
|
|
473
489
|
try {
|
|
474
490
|
const handle = arm(proj.dir, true, (filename) => {
|
|
475
|
-
|
|
491
|
+
const abs = path.join(proj.dir, filename)
|
|
492
|
+
if (isIgnoredPath(proj.dir, filename) || fenced(proj.dir, abs)) return
|
|
476
493
|
if (shapesWatchedSet(filename)) reread = true
|
|
477
|
-
trigger(`${proj.name} ${filename}`,
|
|
494
|
+
trigger(`${proj.name} ${filename}`, abs)
|
|
478
495
|
})
|
|
479
496
|
perProject.set(proj.dir, handle)
|
|
480
497
|
armedAs.set(proj.dir, inodeOf(proj.dir) ?? '')
|
|
@@ -659,7 +676,14 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
659
676
|
memberBases = next.memberBases
|
|
660
677
|
armBases()
|
|
661
678
|
isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs)
|
|
662
|
-
|
|
679
|
+
fenced = makeFence(next.fenceDirs)
|
|
680
|
+
matters = makeRootEventFilter(
|
|
681
|
+
workspaceRoot,
|
|
682
|
+
projectDirs,
|
|
683
|
+
workspaceInputs,
|
|
684
|
+
claimedRootFiles,
|
|
685
|
+
fenced,
|
|
686
|
+
)
|
|
663
687
|
if (next.workspaceWide !== workspaceWide) {
|
|
664
688
|
// A task started or stopped declaring `workspaceFiles`: the other
|
|
665
689
|
// arm's shape. Until item 891 the choice was made once, at start, and
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// price is no live progress within a task. This matches Turbo's
|
|
26
26
|
// `--ui=stream` mode.
|
|
27
27
|
|
|
28
|
+
import { withForwardArgs } from '../exec/index.js'
|
|
28
29
|
import { isGroupTask, type TaskNode, type TaskOutcome } from '../graph/index.js'
|
|
29
30
|
import { maskedCommand } from '../util/index.js'
|
|
30
31
|
import { paint, type ColorSupport } from './colors.js'
|
|
@@ -82,6 +83,7 @@ export function formatTaskBlock(
|
|
|
82
83
|
// tasks set this so a requested task's frame is identical whether it
|
|
83
84
|
// ran or was cached — you asked for it, you see what it would run.
|
|
84
85
|
forceCommand = false,
|
|
86
|
+
forwardArgs: readonly string[] = [],
|
|
85
87
|
): string {
|
|
86
88
|
// Group tasks (no `exec`) do no work and have no body — they're
|
|
87
89
|
// organizational nodes the user wrote so a `vx run ci` invocation
|
|
@@ -105,11 +107,7 @@ export function formatTaskBlock(
|
|
|
105
107
|
if (forceCommand || outcome.status === 'success' || outcome.status === 'failed') {
|
|
106
108
|
// No section label for the command (owner cut it) — the dim `$ `
|
|
107
109
|
// line under the header reads as the command on its own.
|
|
108
|
-
lines.push(
|
|
109
|
-
'',
|
|
110
|
-
corner(`$ ${maskedCommand(node.config.exec?.command ?? '', node.config.exec?.env)}`),
|
|
111
|
-
'',
|
|
112
|
-
)
|
|
110
|
+
lines.push('', corner(`$ ${shownCommand(node, forwardArgs)}`), '')
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
pushStreamSection(lines, stdout, 'STDOUT', SUCCESS, body.droppedStdout ?? 0, colors)
|
|
@@ -444,9 +442,13 @@ export function formatTaskAbortedLine(
|
|
|
444
442
|
* formatTaskBlock, but emitted in real time around the live stream
|
|
445
443
|
* instead of buffered.
|
|
446
444
|
*/
|
|
447
|
-
export function formatFrameOpen(
|
|
445
|
+
export function formatFrameOpen(
|
|
446
|
+
node: TaskNode,
|
|
447
|
+
colors: ColorSupport = NO_COLOR,
|
|
448
|
+
forwardArgs: readonly string[] = [],
|
|
449
|
+
): string {
|
|
448
450
|
const corner = (t: string) => paint('', t, colors, { dim: true })
|
|
449
|
-
const cmd =
|
|
451
|
+
const cmd = shownCommand(node, forwardArgs)
|
|
450
452
|
const mark = isPersistentNode(node) ? `${paint(ACCENT, '▸', colors)} ` : ''
|
|
451
453
|
return `${corner('┌─')} ${mark}${paintTaskId(node, colors, { bold: true })} ${corner('>')} $ ${cmd}`
|
|
452
454
|
}
|
|
@@ -492,6 +494,15 @@ export function formatFrameClose(
|
|
|
492
494
|
return violations.length === 0 ? close : `${violations.join('\n')}\n${close}`
|
|
493
495
|
}
|
|
494
496
|
|
|
497
|
+
/** What ran: a requested task gets the args after `--`, as execute-task.ts appends them. */
|
|
498
|
+
function shownCommand(node: TaskNode, forwardArgs: readonly string[]): string {
|
|
499
|
+
const exec = node.config.exec
|
|
500
|
+
return maskedCommand(
|
|
501
|
+
withForwardArgs(exec?.command ?? '', node.requested ? forwardArgs : []),
|
|
502
|
+
exec?.env,
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
495
506
|
function isPersistentNode(node: TaskNode): boolean {
|
|
496
507
|
return node.config.exec?.persistent !== undefined
|
|
497
508
|
}
|
|
@@ -512,11 +523,12 @@ export function formatPersistentTailBlock(
|
|
|
512
523
|
body: TaskBlockBody,
|
|
513
524
|
dropped: { stdout?: number; stderr?: number } = {},
|
|
514
525
|
colors: ColorSupport = NO_COLOR,
|
|
526
|
+
forwardArgs: readonly string[] = [],
|
|
515
527
|
): string {
|
|
516
528
|
const stdout = body.stdout ?? ''
|
|
517
529
|
const stderr = body.stderr ?? ''
|
|
518
530
|
if (stdout.trim().length === 0 && stderr.trim().length === 0) return ''
|
|
519
|
-
const lines: string[] = [formatFrameOpen(node, colors)]
|
|
531
|
+
const lines: string[] = [formatFrameOpen(node, colors, forwardArgs)]
|
|
520
532
|
pushStreamSection(lines, stdout, 'STDOUT (since ready)', SUCCESS, dropped.stdout ?? 0, colors)
|
|
521
533
|
pushStreamSection(lines, stderr, 'STDERR (since ready)', ERROR, dropped.stderr ?? 0, colors)
|
|
522
534
|
lines.push(formatFrameClose(node, outcome, colors))
|
|
@@ -166,8 +166,15 @@ export function defaultLogger(
|
|
|
166
166
|
colors: ColorSupport = detectColors(),
|
|
167
167
|
view: OutputView = { mode: 'full' },
|
|
168
168
|
out: StatusStream = process.stdout,
|
|
169
|
-
opts: {
|
|
169
|
+
opts: {
|
|
170
|
+
forceFloorMs?: number
|
|
171
|
+
coalesce?: boolean
|
|
172
|
+
tty?: boolean
|
|
173
|
+
/** The args after `--`: a requested task's `$ <command>` line shows them, as it ran. */
|
|
174
|
+
forwardArgs?: readonly string[]
|
|
175
|
+
} = {},
|
|
170
176
|
): DefaultLogger {
|
|
177
|
+
const forwardArgs = opts.forwardArgs ?? []
|
|
171
178
|
// Per-task buffers, split by stream. Splitting lets the framed-output
|
|
172
179
|
// renderer put stdout under `├─ stdout` and stderr under `├─ stderr`.
|
|
173
180
|
// The price: chunks that interleaved at runtime get re-ordered (all
|
|
@@ -478,7 +485,7 @@ export function defaultLogger(
|
|
|
478
485
|
killStatus()
|
|
479
486
|
// Open the live frame: full task info even when the command
|
|
480
487
|
// streams nothing (or the hit replays nothing).
|
|
481
|
-
emitLine(formatFrameOpen(node, colors))
|
|
488
|
+
emitLine(formatFrameOpen(node, colors, forwardArgs))
|
|
482
489
|
return
|
|
483
490
|
}
|
|
484
491
|
// Bound a persistent task's output from its FIRST chunk. Waiting for
|
|
@@ -520,6 +527,7 @@ export function defaultLogger(
|
|
|
520
527
|
{ stdout: tailText(t.out), stderr: tailText(t.err) },
|
|
521
528
|
{ stdout: t.out.dropped, stderr: t.err.dropped },
|
|
522
529
|
colors,
|
|
530
|
+
forwardArgs,
|
|
523
531
|
),
|
|
524
532
|
),
|
|
525
533
|
)
|
|
@@ -619,7 +627,16 @@ export function defaultLogger(
|
|
|
619
627
|
return
|
|
620
628
|
}
|
|
621
629
|
if (framed && (stdout.length > 0 || stderr.length > 0)) {
|
|
622
|
-
emitBlock(
|
|
630
|
+
emitBlock(
|
|
631
|
+
formatTaskBlock(
|
|
632
|
+
node,
|
|
633
|
+
outcome,
|
|
634
|
+
{ stdout, stderr, ...dropped },
|
|
635
|
+
colors,
|
|
636
|
+
true,
|
|
637
|
+
forwardArgs,
|
|
638
|
+
),
|
|
639
|
+
)
|
|
623
640
|
return
|
|
624
641
|
}
|
|
625
642
|
// Otherwise a row in the task list, in every view that lists a
|
|
@@ -709,7 +726,14 @@ export function defaultLogger(
|
|
|
709
726
|
if (outcome.status !== 'failed') return
|
|
710
727
|
emitLine(failureRow(node, outcome))
|
|
711
728
|
deferredFailures.push(
|
|
712
|
-
formatTaskBlock(
|
|
729
|
+
formatTaskBlock(
|
|
730
|
+
node,
|
|
731
|
+
outcome,
|
|
732
|
+
{ stdout, stderr, ...dropped },
|
|
733
|
+
colors,
|
|
734
|
+
false,
|
|
735
|
+
forwardArgs,
|
|
736
|
+
),
|
|
713
737
|
)
|
|
714
738
|
return
|
|
715
739
|
case 'broad':
|
|
@@ -721,7 +745,14 @@ export function defaultLogger(
|
|
|
721
745
|
// ◼ row now; the full frame replays at runEnd.
|
|
722
746
|
emitLine(failureRow(node, outcome))
|
|
723
747
|
deferredFailures.push(
|
|
724
|
-
formatTaskBlock(
|
|
748
|
+
formatTaskBlock(
|
|
749
|
+
node,
|
|
750
|
+
outcome,
|
|
751
|
+
{ stdout, stderr, ...dropped },
|
|
752
|
+
colors,
|
|
753
|
+
false,
|
|
754
|
+
forwardArgs,
|
|
755
|
+
),
|
|
725
756
|
)
|
|
726
757
|
} else if (outcome.status === 'success') {
|
|
727
758
|
emitLine(formatTaskExecutedLine(node, outcome, colors))
|
|
@@ -758,13 +789,29 @@ export function defaultLogger(
|
|
|
758
789
|
if (outcome.status === 'failed') {
|
|
759
790
|
emitLine(failureRow(node, outcome))
|
|
760
791
|
deferredFailures.push(
|
|
761
|
-
formatTaskBlock(
|
|
792
|
+
formatTaskBlock(
|
|
793
|
+
node,
|
|
794
|
+
outcome,
|
|
795
|
+
{ stdout, stderr, ...dropped },
|
|
796
|
+
colors,
|
|
797
|
+
false,
|
|
798
|
+
forwardArgs,
|
|
799
|
+
),
|
|
762
800
|
)
|
|
763
801
|
return
|
|
764
802
|
}
|
|
765
803
|
// forceCommand: a requested task's frame shows `$ cmd`
|
|
766
804
|
// whether it ran or was cached — same frame every run.
|
|
767
|
-
emitBlock(
|
|
805
|
+
emitBlock(
|
|
806
|
+
formatTaskBlock(
|
|
807
|
+
node,
|
|
808
|
+
outcome,
|
|
809
|
+
{ stdout, stderr, ...dropped },
|
|
810
|
+
colors,
|
|
811
|
+
true,
|
|
812
|
+
forwardArgs,
|
|
813
|
+
),
|
|
814
|
+
)
|
|
768
815
|
return
|
|
769
816
|
}
|
|
770
817
|
// Dependency-pulled nodes: silent on success; a skip gets its
|
|
@@ -774,7 +821,14 @@ export function defaultLogger(
|
|
|
774
821
|
} else if (outcome.status === 'failed') {
|
|
775
822
|
emitLine(failureRow(node, outcome))
|
|
776
823
|
deferredFailures.push(
|
|
777
|
-
formatTaskBlock(
|
|
824
|
+
formatTaskBlock(
|
|
825
|
+
node,
|
|
826
|
+
outcome,
|
|
827
|
+
{ stdout, stderr, ...dropped },
|
|
828
|
+
colors,
|
|
829
|
+
false,
|
|
830
|
+
forwardArgs,
|
|
831
|
+
),
|
|
778
832
|
)
|
|
779
833
|
}
|
|
780
834
|
return
|
|
@@ -793,7 +847,14 @@ export function defaultLogger(
|
|
|
793
847
|
emitLine(formatTaskSkippedLine(node, colors, outcome.blockedBy))
|
|
794
848
|
return
|
|
795
849
|
}
|
|
796
|
-
const block = formatTaskBlock(
|
|
850
|
+
const block = formatTaskBlock(
|
|
851
|
+
node,
|
|
852
|
+
outcome,
|
|
853
|
+
{ stdout, stderr, ...dropped },
|
|
854
|
+
colors,
|
|
855
|
+
false,
|
|
856
|
+
forwardArgs,
|
|
857
|
+
)
|
|
797
858
|
if (block.length === 0) return
|
|
798
859
|
if (view.gha) {
|
|
799
860
|
// The block is the task's own output — fenced, so a task that
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -297,6 +297,7 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
297
297
|
? defaultLogger(colors, resolveOutputView(options), process.stdout, {
|
|
298
298
|
coalesce: true,
|
|
299
299
|
...(options.tty === true ? { tty: true } : {}),
|
|
300
|
+
...(options.forwardArgs !== undefined ? { forwardArgs: options.forwardArgs } : {}),
|
|
300
301
|
})
|
|
301
302
|
: null
|
|
302
303
|
const sink = options.log ?? terminal!
|