@vzn/vx 0.0.22 → 0.0.24
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/src/cli/watch.ts +74 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "An open, extensible monorepo task runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.24",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.24",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.24",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.24"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/watch.ts
CHANGED
|
@@ -504,9 +504,20 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
504
504
|
// closing it properly needs the workspaceWide decision made without loading
|
|
505
505
|
// configs.
|
|
506
506
|
process.stdout.write('vx watch: initial run...\n\n')
|
|
507
|
-
|
|
507
|
+
// A run that throws (a config that does not parse) is a failed cycle, as
|
|
508
|
+
// it is once the loop runs: watch keeps watching, so the fix re-runs.
|
|
509
|
+
// Uncaught, it ended watch at start while the same break mid-watch did
|
|
510
|
+
// not (item 1017).
|
|
511
|
+
let held: HeldPersistent | undefined
|
|
512
|
+
try {
|
|
513
|
+
held = (await runOrchestrator(opts)).persistent
|
|
514
|
+
} catch (err) {
|
|
515
|
+
process.stderr.write(
|
|
516
|
+
`vx watch: cycle failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
517
|
+
)
|
|
518
|
+
}
|
|
508
519
|
if (stop.signal.aborted) {
|
|
509
|
-
await
|
|
520
|
+
await held?.stop(forwardedSignal(stop.signal.reason))
|
|
510
521
|
return 0
|
|
511
522
|
}
|
|
512
523
|
|
|
@@ -519,7 +530,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
519
530
|
const ws = await loadCliWorkspace(workspaceRoot)
|
|
520
531
|
return await runWatchLoop({
|
|
521
532
|
opts,
|
|
522
|
-
held
|
|
533
|
+
held,
|
|
523
534
|
stop: stop.signal,
|
|
524
535
|
workspaceRoot,
|
|
525
536
|
projects: watched,
|
|
@@ -538,11 +549,13 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
538
549
|
// scope is the one resolved at start; a new package joins it only as a
|
|
539
550
|
// dependency of it.
|
|
540
551
|
rediscover: async () => {
|
|
541
|
-
const
|
|
552
|
+
const workspace = await loadWorkspace(workspaceRoot)
|
|
553
|
+
const all = await listProjects(workspace)
|
|
542
554
|
const sweep = await sweepConfigs(all, workspaceRoot, load)
|
|
543
555
|
const now = await watchedProjects(workspaceRoot, all, inScope(all), load, sweep.staged)
|
|
544
556
|
return {
|
|
545
557
|
projects: now,
|
|
558
|
+
memberBases: memberBaseDirs(workspace),
|
|
546
559
|
workspaceWide: sweep.workspaceWide,
|
|
547
560
|
workspaceInputs: sweep.workspaceInputs,
|
|
548
561
|
outputs: sweep.outputs,
|
|
@@ -751,7 +764,10 @@ export function makeRootEventFilter(
|
|
|
751
764
|
// this line load-bearing.
|
|
752
765
|
if (
|
|
753
766
|
!rel.includes('/') &&
|
|
754
|
-
(isWorkspaceFingerprintFile(rel) ||
|
|
767
|
+
(isWorkspaceFingerprintFile(rel) ||
|
|
768
|
+
isWorkspaceConfigFile(rel) ||
|
|
769
|
+
rel === 'package.json' ||
|
|
770
|
+
claimedRootFiles.has(rel))
|
|
755
771
|
) {
|
|
756
772
|
return true
|
|
757
773
|
}
|
|
@@ -793,7 +809,7 @@ interface WatchLoopArgs {
|
|
|
793
809
|
* loaded once per process, so the set is fixed for the loop.
|
|
794
810
|
*/
|
|
795
811
|
claimedRootFiles: ReadonlySet<string>
|
|
796
|
-
/** The directory each `<dir>/*` package glob names; a member coming or going there is a cycle. */
|
|
812
|
+
/** The directory each `<dir>/*` package glob names at start; a member coming or going there is a cycle. */
|
|
797
813
|
memberBases: readonly string[]
|
|
798
814
|
/** Every package's directory, in scope or not: a member base's other entries are packages still to come. */
|
|
799
815
|
packageDirs: ReadonlySet<string>
|
|
@@ -803,6 +819,7 @@ interface WatchLoopArgs {
|
|
|
803
819
|
|
|
804
820
|
interface Rediscovered {
|
|
805
821
|
projects: readonly ProjectMeta[]
|
|
822
|
+
memberBases: readonly string[]
|
|
806
823
|
workspaceWide: boolean
|
|
807
824
|
workspaceInputs: readonly string[]
|
|
808
825
|
outputs: ReadonlyMap<string, readonly string[]>
|
|
@@ -822,21 +839,29 @@ const PROJECT_CONFIGS: ReadonlySet<string> = new Set(PROJECT_CONFIG_FILENAMES)
|
|
|
822
839
|
* watcher, a new output must stop being an event) or the workspace config
|
|
823
840
|
* (a plugin's `project` stage). Until item 891 only a member directory
|
|
824
841
|
* coming or going re-read the set, so each of these waited for a restart
|
|
825
|
-
* while the loop looked alive.
|
|
842
|
+
* while the loop looked alive. A root fingerprint file too, for
|
|
843
|
+
* `pnpm-workspace.yaml`: a glob added there was a cycle that ran the new
|
|
844
|
+
* packages and watched none of them (item 1018).
|
|
826
845
|
*/
|
|
827
846
|
function shapesWatchedSet(filename: string): boolean {
|
|
828
847
|
const base = path.basename(filename)
|
|
829
|
-
return
|
|
848
|
+
return (
|
|
849
|
+
base === 'package.json' ||
|
|
850
|
+
isWorkspaceFingerprintFile(base) ||
|
|
851
|
+
PROJECT_CONFIGS.has(base) ||
|
|
852
|
+
isWorkspaceConfigFile(base)
|
|
853
|
+
)
|
|
830
854
|
}
|
|
831
855
|
|
|
832
856
|
const CLOSED: WatchHandle = { close() {} }
|
|
833
857
|
|
|
834
858
|
async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
835
|
-
const { opts, stop, workspaceRoot, projects, cacheDir
|
|
859
|
+
const { opts, stop, workspaceRoot, projects, cacheDir } = args
|
|
836
860
|
// The watched set as of the last cycle: `rearm` replaces these after an
|
|
837
861
|
// event that can change it, and every filter below reads the current one.
|
|
838
862
|
let workspaceWide = args.workspaceWide
|
|
839
863
|
let packageDirs = args.packageDirs
|
|
864
|
+
let memberBases = args.memberBases
|
|
840
865
|
let projectDirs = args.projectDirs
|
|
841
866
|
let workspaceInputs = args.workspaceInputs
|
|
842
867
|
let outputs = args.outputs
|
|
@@ -1174,6 +1199,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
1174
1199
|
if (
|
|
1175
1200
|
isWorkspaceFingerprintFile(filename) ||
|
|
1176
1201
|
isWorkspaceConfigFile(filename) ||
|
|
1202
|
+
filename === 'package.json' ||
|
|
1177
1203
|
args.claimedRootFiles.has(filename)
|
|
1178
1204
|
) {
|
|
1179
1205
|
if (shapesWatchedSet(filename) || filename === LOCKFILE_NAME) reread = true
|
|
@@ -1323,6 +1349,8 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
1323
1349
|
configImportFiles = next.configImports
|
|
1324
1350
|
wsConfigImportFiles = next.workspaceConfigImports
|
|
1325
1351
|
packageDirs = next.packageDirs
|
|
1352
|
+
memberBases = next.memberBases
|
|
1353
|
+
armBases()
|
|
1326
1354
|
isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs)
|
|
1327
1355
|
matters = makeRootEventFilter(
|
|
1328
1356
|
workspaceRoot,
|
|
@@ -1362,28 +1390,45 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
|
|
|
1362
1390
|
// triggers re-reads the workspace (`rearm`) so the new package's own
|
|
1363
1391
|
// edits are cycles from then on. Until 2026-09-10 the watched set was
|
|
1364
1392
|
// fixed when the loop armed: the next cycle ran the new package, and
|
|
1365
|
-
// every edit inside it after that was silence.
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1393
|
+
// every edit inside it after that was silence. The bases themselves are
|
|
1394
|
+
// re-read with the set: a glob added to the list watched nothing new
|
|
1395
|
+
// until a restart (item 1018).
|
|
1396
|
+
const baseArms = new Map<string, WatchHandle>()
|
|
1397
|
+
const armBases = (): void => {
|
|
1398
|
+
const want = new Set(memberBases)
|
|
1399
|
+
for (const [base, handle] of baseArms) {
|
|
1400
|
+
if (want.has(base)) continue
|
|
1401
|
+
handle.close()
|
|
1402
|
+
baseArms.delete(base)
|
|
1403
|
+
}
|
|
1404
|
+
for (const base of want) {
|
|
1405
|
+
if (baseArms.has(base)) continue
|
|
1406
|
+
let members = memberEntries(base)
|
|
1407
|
+
try {
|
|
1408
|
+
baseArms.set(
|
|
1409
|
+
base,
|
|
1410
|
+
arm(base, false, (filename) => {
|
|
1411
|
+
if (isIgnoredWatchPath(filename)) return
|
|
1412
|
+
// Only a member coming or going. On macOS a non-recursive watcher
|
|
1413
|
+
// also reports a member whose CONTENTS changed (FSEvents names the
|
|
1414
|
+
// directory a write landed in), so a task writing into its own
|
|
1415
|
+
// project — or the arm's own probe file — read as a member event
|
|
1416
|
+
// and cost an uncached task one execution per cycle (CI,
|
|
1417
|
+
// 2026-09-10).
|
|
1418
|
+
const now = memberEntries(base)
|
|
1419
|
+
if (sameMembers(members, now)) return
|
|
1420
|
+
members = now
|
|
1421
|
+
reread = true
|
|
1422
|
+
trigger(`${path.relative(workspaceRoot, base)}/${filename}`, path.join(base, filename))
|
|
1423
|
+
}),
|
|
1424
|
+
)
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
1427
|
+
process.stderr.write(`vx watch: cannot watch ${base}: ${msg}\n`)
|
|
1428
|
+
}
|
|
1385
1429
|
}
|
|
1386
1430
|
}
|
|
1431
|
+
armBases()
|
|
1387
1432
|
armPending(false)
|
|
1388
1433
|
armImports()
|
|
1389
1434
|
|