@vzn/vx 0.0.636 → 0.0.637
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 +39 -11
- package/docs/caching.md +213 -207
- package/docs/cli.md +699 -655
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +26 -25
- package/docs/schema.md +376 -198
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +2 -2
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -63
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +103 -149
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +24 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +158 -63
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +113 -72
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +121 -95
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +57 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +242 -146
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +362 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/src/cli/watch.ts
CHANGED
|
@@ -24,20 +24,12 @@ import {
|
|
|
24
24
|
type RunOptions,
|
|
25
25
|
shellQuote,
|
|
26
26
|
} from '../orchestrator/index.js'
|
|
27
|
-
import {
|
|
28
|
-
findWorkspaceRoot,
|
|
29
|
-
type LoadReads,
|
|
30
|
-
loadWorkspace,
|
|
31
|
-
LOCKFILE_NAME,
|
|
32
|
-
memberBaseDirs,
|
|
33
|
-
type ProjectMeta,
|
|
34
|
-
} from '../workspace/index.js'
|
|
27
|
+
import { findWorkspaceRoot, LOCKFILE_NAME, NO_LOCK, type ProjectMeta } from '../workspace/index.js'
|
|
35
28
|
import { type CliLoadOptions, discoverCliProjects, loadCliWorkspace } from './workspace-config.js'
|
|
29
|
+
import { resolveSetting } from '../settings.js'
|
|
36
30
|
import {
|
|
37
|
-
isIgnoredWatchPath,
|
|
38
31
|
isWorkspaceConfigFile,
|
|
39
32
|
gitFiles,
|
|
40
|
-
isWorkspaceFingerprintFile,
|
|
41
33
|
makeFence,
|
|
42
34
|
makeRootEventFilter,
|
|
43
35
|
makeWatchIgnore,
|
|
@@ -47,7 +39,7 @@ import { CLOSED, fsClockNow, type WatchHandle, WatcherPool } from './watch-fs.js
|
|
|
47
39
|
import { ChangeJudge } from './watch-judge.js'
|
|
48
40
|
import { type WatchCycle, watchCycle } from './watch-cycle.js'
|
|
49
41
|
import { hangupIgnored, maskedLine, restartTimings } from '../util/index.js'
|
|
50
|
-
import {
|
|
42
|
+
import { sweepConfigs, watchedProjects } from './watch-set.js'
|
|
51
43
|
|
|
52
44
|
/** One line for a watcher or re-read the OS refused; the loop goes on without it. */
|
|
53
45
|
function sayCannot(what: string, err: unknown): void {
|
|
@@ -165,13 +157,18 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
165
157
|
// closure `--filter 'app...'` walks, computed below once the initial
|
|
166
158
|
// run has staged the configs. Plus the workspace root, for lockfile
|
|
167
159
|
// changes.
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
160
|
+
const workspaceRoot = await findWorkspaceRoot(cwd)
|
|
161
|
+
// No lock yet is no project yet: the first cycle fails saying so, and
|
|
162
|
+
// the lock `vx lock` writes is the event that arms the set.
|
|
163
|
+
let unlocked = false
|
|
164
|
+
const allProjects = await discoverCliProjects(workspaceRoot).catch((err: unknown) => {
|
|
165
|
+
if (!(err instanceof Error) || err.message !== NO_LOCK) throw err
|
|
166
|
+
unlocked = true
|
|
167
|
+
return []
|
|
168
|
+
})
|
|
169
|
+
// `//#check` is the root project's task, under the name the run gives
|
|
170
|
+
// it (prepare.ts, D-39): taken literally, no project was in scope and
|
|
171
|
+
// the watch exited 1 after a green run.
|
|
175
172
|
const rootDir = path.resolve(workspaceRoot)
|
|
176
173
|
const rootName = allProjects.find((p) => path.resolve(p.dir) === rootDir)?.name
|
|
177
174
|
const anchored = opts.tasks
|
|
@@ -202,16 +199,16 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
202
199
|
// config is loaded, and the loop arms with the run as its cycle in
|
|
203
200
|
// flight. Unarmed, a server that never got ready held the initial run
|
|
204
201
|
// for good, and the fix to it was never heard (X-139).
|
|
205
|
-
process.stdout.write('vx watch: initial run...\n\n')
|
|
206
202
|
let loop: Promise<void> | undefined
|
|
207
203
|
const initial: InitialCycle = {
|
|
208
204
|
cycle: watchCycle(opts, stop.signal, () => {
|
|
209
205
|
if (initial.onWaiting !== undefined) initial.onWaiting()
|
|
210
|
-
else if (loop === undefined && scope.length > 0 && !stop.signal.aborted)
|
|
206
|
+
else if (loop === undefined && (scope.length > 0 || unlocked) && !stop.signal.aborted)
|
|
211
207
|
loop = armLoop(initial)
|
|
212
208
|
}),
|
|
213
209
|
held: Promise.resolve(undefined),
|
|
214
210
|
}
|
|
211
|
+
const openingTasks = (): number | undefined => initial.cycle.tasks
|
|
215
212
|
// A run that throws (a config that does not parse) is a failed cycle, as
|
|
216
213
|
// it is once the loop runs: watch keeps watching, so the fix re-runs.
|
|
217
214
|
// Uncaught, it ended watch at start while the same break mid-watch did
|
|
@@ -248,7 +245,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
248
245
|
if (refusedToStart) return 1
|
|
249
246
|
// After the initial run, so a `pkg#task` naming no project gets the
|
|
250
247
|
// run's own refusal and its "did you mean".
|
|
251
|
-
if (scope.length === 0) {
|
|
248
|
+
if (scope.length === 0 && !unlocked) {
|
|
252
249
|
await held?.stop()
|
|
253
250
|
process.stderr.write(`vx watch: no projects in scope\n`)
|
|
254
251
|
return 1
|
|
@@ -263,22 +260,17 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
263
260
|
delete opts.affected
|
|
264
261
|
delete opts.selectedOutright
|
|
265
262
|
|
|
266
|
-
const load: CliLoadOptions = {
|
|
267
|
-
...(opts.cacheDir !== undefined ? { cacheDir: opts.cacheDir } : {}),
|
|
268
|
-
...(opts.frozen === true ? { frozen: true } : {}),
|
|
269
|
-
}
|
|
263
|
+
const load: CliLoadOptions = opts.cacheDir !== undefined ? { cacheDir: opts.cacheDir } : {}
|
|
270
264
|
const swept = await sweepConfigs(allProjects, workspaceRoot, load, opts.tasks)
|
|
271
265
|
const watched = await watchedProjects(workspaceRoot, allProjects, scope, load, swept.staged)
|
|
272
266
|
const ws = await loadCliWorkspace(workspaceRoot)
|
|
273
267
|
const claimsOf = (plugins: Parameters<typeof fingerprintClaims>[0]): Set<string> =>
|
|
274
|
-
new Set([
|
|
275
|
-
...fingerprintClaims(plugins).keys(),
|
|
276
|
-
...(opts.frozen === true ? [LOCKFILE_NAME] : []),
|
|
277
|
-
])
|
|
268
|
+
new Set([...fingerprintClaims(plugins).keys(), LOCKFILE_NAME])
|
|
278
269
|
await runWatchLoop({
|
|
279
270
|
opts,
|
|
280
271
|
held,
|
|
281
272
|
initial,
|
|
273
|
+
tasks: openingTasks,
|
|
282
274
|
stop: stop.signal,
|
|
283
275
|
workspaceRoot,
|
|
284
276
|
projects: watched,
|
|
@@ -290,16 +282,13 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
290
282
|
uncached: swept.uncached,
|
|
291
283
|
configImports: swept.configImports,
|
|
292
284
|
workspaceConfigImports: swept.workspaceConfigImports,
|
|
293
|
-
memberBases: memberBaseDirs(workspace),
|
|
294
|
-
packageDirs: new Set(allProjects.map((p) => p.dir)),
|
|
295
285
|
fenceDirs: fenceDirs(allProjects),
|
|
296
|
-
// The workspace as the cycle that just ran saw it: a
|
|
297
|
-
//
|
|
298
|
-
// scope is the one resolved at start; a new
|
|
299
|
-
// dependency of it.
|
|
286
|
+
// The workspace as the cycle that just ran saw it: a project the lock
|
|
287
|
+
// gained or lost since the loop armed joins or leaves the watched
|
|
288
|
+
// set. The scope is the one resolved at start; a new project joins
|
|
289
|
+
// it only as a dependency of it.
|
|
300
290
|
rediscover: async () => {
|
|
301
|
-
const
|
|
302
|
-
const all = await discoverCliProjects(workspace)
|
|
291
|
+
const all = await discoverCliProjects(workspaceRoot)
|
|
303
292
|
const sweep = await sweepConfigs(all, workspaceRoot, load, opts.tasks)
|
|
304
293
|
const now = await watchedProjects(workspaceRoot, all, inScope(all), load, sweep.staged)
|
|
305
294
|
return {
|
|
@@ -308,7 +297,6 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
308
297
|
// the cycle that loaded it; read once, its edits started nothing
|
|
309
298
|
// until a restart.
|
|
310
299
|
claimedRootFiles: claimsOf((await loadCliWorkspace(workspaceRoot)).plugins),
|
|
311
|
-
memberBases: memberBaseDirs(workspace),
|
|
312
300
|
workspaceWide: sweep.workspaceWide,
|
|
313
301
|
workspaceInputs: sweep.workspaceInputs,
|
|
314
302
|
outputs: sweep.outputs,
|
|
@@ -316,16 +304,16 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
316
304
|
uncached: sweep.uncached,
|
|
317
305
|
configImports: sweep.configImports,
|
|
318
306
|
workspaceConfigImports: sweep.workspaceConfigImports,
|
|
319
|
-
packageDirs: new Set(all.map((p) => p.dir)),
|
|
320
307
|
fenceDirs: fenceDirs(all),
|
|
321
308
|
}
|
|
322
309
|
},
|
|
323
|
-
//
|
|
310
|
+
// Every cycle's configs are the lock's, so a re-lock is
|
|
324
311
|
// the one edit that changes what a cycle runs; unheard, the loop ran
|
|
325
312
|
// the old lock until a restart (item 971).
|
|
326
313
|
claimedRootFiles: claimsOf(ws.plugins),
|
|
327
314
|
// The RESOLVED cache dir, not the `.vx` literal — see `makeWatchIgnore`.
|
|
328
315
|
cacheDir: opts.cacheDir ?? ws.cacheDir,
|
|
316
|
+
poll: resolveSetting('watch.poll', ws.workspaceConfig, parsed.poll).value === true,
|
|
329
317
|
})
|
|
330
318
|
}
|
|
331
319
|
}
|
|
@@ -345,6 +333,8 @@ interface WatchLoopArgs {
|
|
|
345
333
|
held: HeldPersistent | undefined
|
|
346
334
|
/** The initial run, still in flight: the loop's first cycle. */
|
|
347
335
|
initial: InitialCycle | undefined
|
|
336
|
+
/** The initial run's task count, once it has started. */
|
|
337
|
+
tasks: () => number | undefined
|
|
348
338
|
/** Aborted by the SIGINT/SIGTERM handlers `watchCmd` installed; the loop drains its cycle and resolves. */
|
|
349
339
|
stop: AbortSignal
|
|
350
340
|
workspaceRoot: string
|
|
@@ -372,14 +362,12 @@ interface WatchLoopArgs {
|
|
|
372
362
|
* loaded once per process, so the set is fixed for the loop.
|
|
373
363
|
*/
|
|
374
364
|
claimedRootFiles: ReadonlySet<string>
|
|
375
|
-
/** The directory each `<dir>/*` package glob names at start; a member coming or going there is a cycle. */
|
|
376
|
-
memberBases: readonly string[]
|
|
377
|
-
/** Every package's directory, in scope or not: a member base's other entries are packages still to come. */
|
|
378
|
-
packageDirs: ReadonlySet<string>
|
|
379
365
|
/** Every project, in scope or not: a project's key leaves out what lies in one nested under it. */
|
|
380
366
|
fenceDirs: readonly string[]
|
|
381
367
|
/** The watched set again, after a cycle that followed an event which can change it. */
|
|
382
368
|
rediscover: () => Promise<Rediscovered>
|
|
369
|
+
/** `watch.poll`: poll from the start instead of the OS watcher. */
|
|
370
|
+
poll: boolean
|
|
383
371
|
}
|
|
384
372
|
|
|
385
373
|
const fenceDirs = (all: readonly ProjectMeta[]): string[] => all.map((p) => p.dir)
|
|
@@ -387,7 +375,6 @@ const fenceDirs = (all: readonly ProjectMeta[]): string[] => all.map((p) => p.di
|
|
|
387
375
|
interface Rediscovered {
|
|
388
376
|
projects: readonly ProjectMeta[]
|
|
389
377
|
claimedRootFiles: ReadonlySet<string>
|
|
390
|
-
memberBases: readonly string[]
|
|
391
378
|
workspaceWide: boolean
|
|
392
379
|
workspaceInputs: readonly string[]
|
|
393
380
|
outputs: ReadonlyMap<string, readonly string[]>
|
|
@@ -395,7 +382,6 @@ interface Rediscovered {
|
|
|
395
382
|
uncached: ReadonlySet<string>
|
|
396
383
|
configImports: readonly string[]
|
|
397
384
|
workspaceConfigImports: readonly string[]
|
|
398
|
-
packageDirs: ReadonlySet<string>
|
|
399
385
|
fenceDirs: readonly string[]
|
|
400
386
|
}
|
|
401
387
|
|
|
@@ -404,8 +390,6 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
404
390
|
// The watched set as of the last cycle: `rearm` replaces these after an
|
|
405
391
|
// event that can change it, and every filter below reads the current one.
|
|
406
392
|
let workspaceWide = args.workspaceWide
|
|
407
|
-
let packageDirs = args.packageDirs
|
|
408
|
-
let memberBases = args.memberBases
|
|
409
393
|
let projectDirs = args.projectDirs
|
|
410
394
|
let workspaceInputs = args.workspaceInputs
|
|
411
395
|
let outputs = args.outputs
|
|
@@ -485,7 +469,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
485
469
|
// only after, it fell in the gap and ran nothing (X-138). The re-read
|
|
486
470
|
// after the run stays, for what the run itself changed.
|
|
487
471
|
if (reread) await rearm(false)
|
|
488
|
-
process.stdout.write(`\
|
|
472
|
+
process.stdout.write(`\nchanged ${label}\n`)
|
|
489
473
|
if (stop.aborted) break
|
|
490
474
|
restartTimings()
|
|
491
475
|
// On the mtime clock, as the arm is: from `Date.now()` a write the
|
|
@@ -502,6 +486,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
502
486
|
interruptIfWaiting,
|
|
503
487
|
)
|
|
504
488
|
const cycle = await runOrchestrator(current.opts)
|
|
489
|
+
lastTasks = current.tasks ?? lastTasks
|
|
505
490
|
current = undefined
|
|
506
491
|
held = cycle.persistent
|
|
507
492
|
// What this cycle did not take (a server whose task did not run).
|
|
@@ -553,7 +538,9 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
553
538
|
// project's dir recursively, so a `node_modules` write under a
|
|
554
539
|
// project would otherwise trigger every save during `bun install` —
|
|
555
540
|
// and vx's own cache writes would trigger a cycle that writes again.
|
|
556
|
-
|
|
541
|
+
/** What existed and was tracked at the arm, so a file born and gone since is no deletion and a tracked one is the user's edit (watch-judge.ts). */
|
|
542
|
+
const atArm = gitFiles(workspaceRoot)
|
|
543
|
+
let isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs, atArm?.tracked)
|
|
557
544
|
let fenced = makeFence(args.fenceDirs)
|
|
558
545
|
let matters = makeRootEventFilter(
|
|
559
546
|
workspaceRoot,
|
|
@@ -569,14 +556,12 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
569
556
|
// unconditional segments AND this run's declared output containers. Read
|
|
570
557
|
// through `isIgnoredPath` rather than captured, because `rearm` replaces
|
|
571
558
|
// the filter when the selection changes.
|
|
572
|
-
const pool = new WatcherPool((dir, rel) => isIgnoredPath(dir, rel))
|
|
559
|
+
const pool = new WatcherPool((dir, rel) => isIgnoredPath(dir, rel), args.poll)
|
|
573
560
|
const arm = (dir: string, recursive: boolean, onEvent: (filename: string) => void): WatchHandle =>
|
|
574
561
|
pool.arm(dir, recursive, onEvent)
|
|
575
562
|
|
|
576
563
|
/** The instant the watchers go live, on the mtime clock (see `fsClockNow`): a path last modified before it is the initial run's, not an edit. */
|
|
577
564
|
const armedAt = fsClockNow(cacheDir)
|
|
578
|
-
/** What existed and was tracked at the arm, so a file born and gone since is no deletion and a tracked one is the user's edit (watch-judge.ts). */
|
|
579
|
-
const atArm = gitFiles(workspaceRoot)
|
|
580
565
|
/** Which settled paths are changes (watch-judge.ts); `pending` holds what fired since. */
|
|
581
566
|
const changes = new ChangeJudge({
|
|
582
567
|
workspaceRoot,
|
|
@@ -588,6 +573,38 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
588
573
|
})
|
|
589
574
|
/** Per-project arms by directory, so `rearm` can add and drop them. */
|
|
590
575
|
const perProject = new Map<string, WatchHandle>()
|
|
576
|
+
// Each FSEvents stream goes live slower than the one before it: per
|
|
577
|
+
// project, 751 of 3000 missed their 2 s proof on a macos-latest runner
|
|
578
|
+
// and fell back to polling, a full core at idle; one stream at the root
|
|
579
|
+
// went live in 8 ms (tests/watch-one-stream.test.ts). Polling keeps its
|
|
580
|
+
// per-project walks, which skip what lies between projects.
|
|
581
|
+
const oneStream = process.platform === 'darwin' && !args.poll
|
|
582
|
+
/** The projects the one stream hands events to, by their real directory. */
|
|
583
|
+
let streamed: { proj: ProjectMeta; real: string }[] = []
|
|
584
|
+
const realRoot = ((): string => {
|
|
585
|
+
try {
|
|
586
|
+
return fs.realpathSync(workspaceRoot)
|
|
587
|
+
} catch {
|
|
588
|
+
return path.resolve(workspaceRoot)
|
|
589
|
+
}
|
|
590
|
+
})()
|
|
591
|
+
// The stream names a file by its real place under the root, so a project
|
|
592
|
+
// whose real directory lies outside the root keeps its own arm.
|
|
593
|
+
const toStream = (all: readonly ProjectMeta[]): { proj: ProjectMeta; real: string }[] => {
|
|
594
|
+
const out: { proj: ProjectMeta; real: string }[] = []
|
|
595
|
+
for (const proj of all) {
|
|
596
|
+
let real: string
|
|
597
|
+
try {
|
|
598
|
+
real = fs.realpathSync(proj.dir)
|
|
599
|
+
} catch {
|
|
600
|
+
continue
|
|
601
|
+
}
|
|
602
|
+
const rel = path.relative(realRoot, real)
|
|
603
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) continue
|
|
604
|
+
out.push({ proj, real })
|
|
605
|
+
}
|
|
606
|
+
return out
|
|
607
|
+
}
|
|
591
608
|
// An OS watch holds the directory's inode, not its path: one removed and
|
|
592
609
|
// made again (`rm -rf packages && git checkout packages`) keeps reporting
|
|
593
610
|
// for the deleted one. Each arm notes the directory it holds, and a
|
|
@@ -605,32 +622,68 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
605
622
|
}
|
|
606
623
|
}
|
|
607
624
|
const stale = (dir: string): boolean => armedAs.get(dir) !== inodeOf(dir)
|
|
608
|
-
/** The root arm: recursive when workspace-wide, else the root's own files only. */
|
|
625
|
+
/** The root arm: recursive when workspace-wide or the one stream, else the root's own files only. */
|
|
609
626
|
let rootArm: WatchHandle = CLOSED
|
|
627
|
+
let dotVxArm: WatchHandle = CLOSED
|
|
628
|
+
const projectEvent = (proj: ProjectMeta, filename: string): void => {
|
|
629
|
+
const abs = path.join(proj.dir, filename)
|
|
630
|
+
if (isIgnoredPath(proj.dir, filename) || fenced(proj.dir, abs)) return
|
|
631
|
+
// A module the config imports from inside the project is the config
|
|
632
|
+
// too: its edit can add an output or a `workspaceFiles` input.
|
|
633
|
+
if (shapesWatchedSet(filename) || configImportFiles.includes(abs)) reread = true
|
|
634
|
+
trigger(`${proj.name} ${filename}`, abs)
|
|
635
|
+
}
|
|
610
636
|
const armProject = (proj: ProjectMeta): void => {
|
|
637
|
+
// Gone for now: its ancestor's arm (`armAncestors`) hears it return.
|
|
638
|
+
if (inodeOf(proj.dir) === undefined) return
|
|
611
639
|
try {
|
|
612
|
-
const handle = arm(proj.dir, true, (filename) =>
|
|
613
|
-
const abs = path.join(proj.dir, filename)
|
|
614
|
-
if (isIgnoredPath(proj.dir, filename) || fenced(proj.dir, abs)) return
|
|
615
|
-
// A module the config imports from inside the project is the config
|
|
616
|
-
// too: its edit can add an output or a `workspaceFiles` input.
|
|
617
|
-
if (shapesWatchedSet(filename) || configImportFiles.includes(abs)) reread = true
|
|
618
|
-
trigger(`${proj.name} ${filename}`, abs)
|
|
619
|
-
})
|
|
640
|
+
const handle = arm(proj.dir, true, (filename) => projectEvent(proj, filename))
|
|
620
641
|
perProject.set(proj.dir, handle)
|
|
621
642
|
armedAs.set(proj.dir, inodeOf(proj.dir) ?? '')
|
|
622
643
|
} catch (err) {
|
|
623
644
|
sayCannot(`cannot watch ${proj.dir}`, err)
|
|
624
645
|
}
|
|
625
646
|
}
|
|
647
|
+
const rootFileEvent = (filename: string): void => {
|
|
648
|
+
if (
|
|
649
|
+
!isWorkspaceConfigFile(filename) &&
|
|
650
|
+
filename !== '.gitignore' &&
|
|
651
|
+
!claimedRootFiles.has(filename)
|
|
652
|
+
)
|
|
653
|
+
return
|
|
654
|
+
if (shapesWatchedSet(filename) || filename === LOCKFILE_NAME || claimedRootFiles.has(filename))
|
|
655
|
+
reread = true
|
|
656
|
+
trigger(`root ${filename}`, path.join(workspaceRoot, filename))
|
|
657
|
+
}
|
|
658
|
+
const dotVxEvent = (filename: string): void => {
|
|
659
|
+
const rel = `.vx/${filename}`
|
|
660
|
+
if (!isWorkspaceConfigFile(rel) && !claimedRootFiles.has(rel)) return
|
|
661
|
+
reread = true
|
|
662
|
+
trigger(`root ${rel}`, path.join(workspaceRoot, rel))
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* The one stream's events, handed out as the per-project arms, the
|
|
666
|
+
* root's own files and `.vx/` would have heard them: an event lies in
|
|
667
|
+
* every watched project whose tree holds it (a nested one's fence drops
|
|
668
|
+
* it from the outer), so each listener decides exactly as its own arm did.
|
|
669
|
+
*/
|
|
670
|
+
const streamEvent = (filename: string): void => {
|
|
671
|
+
const rel = filename.split(path.sep).join('/')
|
|
672
|
+
if (!rel.includes('/')) rootFileEvent(rel)
|
|
673
|
+
else if (rel.startsWith('.vx/') && rel.indexOf('/', 4) < 0) dotVxEvent(rel.slice(4))
|
|
674
|
+
const abs = path.join(realRoot, filename)
|
|
675
|
+
for (const { proj, real } of streamed) {
|
|
676
|
+
if (abs.startsWith(real + path.sep)) projectEvent(proj, path.relative(real, abs))
|
|
677
|
+
}
|
|
678
|
+
}
|
|
626
679
|
const armRoot = (): void => {
|
|
627
680
|
try {
|
|
628
681
|
rootArm = workspaceWide
|
|
629
682
|
? // workspaceFiles inputs in play: a root-relative glob can name a
|
|
630
683
|
// file anywhere, so one recursive root watcher replaces the
|
|
631
|
-
// per-project ones (it also covers
|
|
632
|
-
//
|
|
633
|
-
// trees, root
|
|
684
|
+
// per-project ones (it also covers claimed root files).
|
|
685
|
+
// `matters` keeps the events a key can see —
|
|
686
|
+
// project trees, claimed root files, the declared globs — and drops
|
|
634
687
|
// the rest of the tree; the ignore filter then keeps node_modules
|
|
635
688
|
// / .git / .vx and declared outputs out of what remains.
|
|
636
689
|
arm(workspaceRoot, true, (filename) => {
|
|
@@ -639,88 +692,50 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
639
692
|
if (
|
|
640
693
|
shapesWatchedSet(filename) ||
|
|
641
694
|
filename === LOCKFILE_NAME ||
|
|
695
|
+
claimedRootFiles.has(filename) ||
|
|
642
696
|
configImportFiles.includes(abs)
|
|
643
697
|
)
|
|
644
698
|
reread = true
|
|
645
699
|
trigger(`root ${filename}`, abs)
|
|
646
700
|
})
|
|
647
|
-
:
|
|
648
|
-
|
|
649
|
-
//
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
isWorkspaceConfigFile(filename) ||
|
|
654
|
-
filename === 'package.json' ||
|
|
655
|
-
filename === '.gitignore' ||
|
|
656
|
-
claimedRootFiles.has(filename)
|
|
657
|
-
) {
|
|
658
|
-
if (shapesWatchedSet(filename) || filename === LOCKFILE_NAME) reread = true
|
|
659
|
-
trigger(`root ${filename}`, path.join(workspaceRoot, filename))
|
|
660
|
-
}
|
|
661
|
-
})
|
|
701
|
+
: oneStream
|
|
702
|
+
? arm(workspaceRoot, true, streamEvent)
|
|
703
|
+
: // The root itself, non-recursive, beside the per-project arms, so
|
|
704
|
+
// claimed root files (a lockfile) trigger re-runs even when no
|
|
705
|
+
// project dir saw the change.
|
|
706
|
+
arm(workspaceRoot, false, rootFileEvent)
|
|
662
707
|
} catch (err) {
|
|
663
708
|
sayCannot(`cannot watch workspace root`, err)
|
|
664
709
|
}
|
|
710
|
+
// The workspace config and the lock sit in `.vx/`, which the root's
|
|
711
|
+
// non-recursive arm does not see into.
|
|
712
|
+
if (workspaceWide || oneStream) return
|
|
713
|
+
try {
|
|
714
|
+
dotVxArm = arm(path.join(workspaceRoot, '.vx'), false, dotVxEvent)
|
|
715
|
+
} catch (err) {
|
|
716
|
+
sayCannot(`cannot watch ${path.join(workspaceRoot, '.vx')}`, err)
|
|
717
|
+
}
|
|
665
718
|
}
|
|
666
719
|
// One recursive watcher per project, unless workspace-wide: each project
|
|
667
720
|
// owns its own subtree; the root is never watched recursively for them
|
|
668
|
-
// (it would cover every project + node_modules + caches).
|
|
721
|
+
// (it would cover every project + node_modules + caches). On macOS one
|
|
722
|
+
// recursive stream at the root stands for all of them (`oneStream`).
|
|
669
723
|
const armMode = (watched: readonly ProjectMeta[]): void => {
|
|
670
|
-
if (!workspaceWide)
|
|
724
|
+
if (!workspaceWide) {
|
|
725
|
+
streamed = oneStream ? toStream(watched) : []
|
|
726
|
+
const inStream = new Set(streamed.map((s) => s.proj))
|
|
727
|
+
for (const proj of watched) if (!inStream.has(proj)) armProject(proj)
|
|
728
|
+
}
|
|
671
729
|
armRoot()
|
|
672
730
|
}
|
|
673
731
|
const dropMode = (): void => {
|
|
732
|
+
streamed = []
|
|
674
733
|
for (const handle of perProject.values()) handle.close()
|
|
675
734
|
perProject.clear()
|
|
676
735
|
rootArm.close()
|
|
677
736
|
rootArm = CLOSED
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* A directory under a member base with no package in it yet. The base's
|
|
682
|
-
* own watcher is not recursive, so the `package.json` that makes it a
|
|
683
|
-
* package is an event only here; an editor or `git checkout` that makes
|
|
684
|
-
* the directory first and the manifest after left the package unwatched
|
|
685
|
-
* for good (item 891). On a rearm a new entry is armed before its
|
|
686
|
-
* manifest is checked, so one that landed after the re-read is an event
|
|
687
|
-
* or is seen by the check. At start there is no re-read to race, and a
|
|
688
|
-
* manifest already there is a directory the globs exclude.
|
|
689
|
-
*/
|
|
690
|
-
const pending = new Map<string, WatchHandle>()
|
|
691
|
-
const armPending = (afterReread: boolean): void => {
|
|
692
|
-
const want = new Set<string>()
|
|
693
|
-
for (const base of memberBases)
|
|
694
|
-
for (const name of memberEntries(base)) {
|
|
695
|
-
const dir = path.join(base, name)
|
|
696
|
-
if (!packageDirs.has(dir)) want.add(dir)
|
|
697
|
-
}
|
|
698
|
-
for (const [dir, handle] of pending) {
|
|
699
|
-
if (want.has(dir) && !stale(dir)) continue
|
|
700
|
-
handle.close()
|
|
701
|
-
pending.delete(dir)
|
|
702
|
-
}
|
|
703
|
-
for (const dir of want) {
|
|
704
|
-
if (pending.has(dir)) continue
|
|
705
|
-
const manifest = path.join(dir, 'package.json')
|
|
706
|
-
const arrived = (): void => {
|
|
707
|
-
reread = true
|
|
708
|
-
trigger(path.relative(workspaceRoot, manifest), manifest)
|
|
709
|
-
}
|
|
710
|
-
try {
|
|
711
|
-
pending.set(
|
|
712
|
-
dir,
|
|
713
|
-
arm(dir, false, (filename) => {
|
|
714
|
-
if (filename === 'package.json') arrived()
|
|
715
|
-
}),
|
|
716
|
-
)
|
|
717
|
-
armedAs.set(dir, inodeOf(dir) ?? '')
|
|
718
|
-
} catch (err) {
|
|
719
|
-
sayCannot(`cannot watch ${dir}`, err)
|
|
720
|
-
continue
|
|
721
|
-
}
|
|
722
|
-
if (afterReread && fs.existsSync(manifest)) arrived()
|
|
723
|
-
}
|
|
737
|
+
dotVxArm.close()
|
|
738
|
+
dotVxArm = CLOSED
|
|
724
739
|
}
|
|
725
740
|
|
|
726
741
|
/**
|
|
@@ -782,10 +797,13 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
782
797
|
armAncestors()
|
|
783
798
|
}
|
|
784
799
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
800
|
+
// Said once the loop is armed, and again after a cycle that re-read the
|
|
801
|
+
// watched set (a member came or went, the lock or a plugin changed).
|
|
802
|
+
let lastTasks: number | undefined
|
|
803
|
+
const sayWatching = (count: number): void => {
|
|
804
|
+
const tasks = lastTasks === undefined ? '' : ` · ${lastTasks} task${lastTasks === 1 ? '' : 's'}`
|
|
805
|
+
process.stdout.write(`watching ${count} project${count === 1 ? '' : 's'}${tasks}\n`)
|
|
806
|
+
}
|
|
789
807
|
|
|
790
808
|
const rearm = async (say = true): Promise<void> => {
|
|
791
809
|
let next: Rediscovered
|
|
@@ -803,10 +821,8 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
803
821
|
claimedRootFiles = next.claimedRootFiles
|
|
804
822
|
configImportFiles = next.configImports
|
|
805
823
|
wsConfigImportFiles = next.workspaceConfigImports
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
armBases()
|
|
809
|
-
isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs)
|
|
824
|
+
armAncestors()
|
|
825
|
+
isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs, atArm?.tracked)
|
|
810
826
|
fenced = makeFence(next.fenceDirs)
|
|
811
827
|
matters = makeRootEventFilter(
|
|
812
828
|
workspaceRoot,
|
|
@@ -823,53 +839,60 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
823
839
|
workspaceWide = next.workspaceWide
|
|
824
840
|
armMode(next.projects)
|
|
825
841
|
} else if (!workspaceWide) {
|
|
826
|
-
|
|
842
|
+
streamed = oneStream ? toStream(next.projects) : []
|
|
843
|
+
const inStream = new Set(streamed.map((s) => s.proj))
|
|
844
|
+
const own = next.projects.filter((p) => !inStream.has(p))
|
|
845
|
+
const keep = new Set(own.map((p) => p.dir))
|
|
827
846
|
for (const [dir, handle] of perProject) {
|
|
828
847
|
if (keep.has(dir) && !stale(dir)) continue
|
|
829
848
|
handle.close()
|
|
830
849
|
perProject.delete(dir)
|
|
831
850
|
}
|
|
832
|
-
for (const proj of
|
|
851
|
+
for (const proj of own) if (!perProject.has(proj.dir)) armProject(proj)
|
|
833
852
|
}
|
|
834
|
-
armPending(true)
|
|
835
853
|
armImports()
|
|
836
854
|
// A new arm proves delivery like the first ones: an edit in the new
|
|
837
|
-
//
|
|
855
|
+
// project right after this cycle is seen, not lost in the gap.
|
|
838
856
|
await pool.proved()
|
|
839
|
-
if (say)
|
|
857
|
+
if (say) sayWatching(next.projects.length)
|
|
840
858
|
}
|
|
841
859
|
|
|
842
860
|
armMode(projects)
|
|
843
861
|
|
|
844
|
-
// A
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
|
|
854
|
-
//
|
|
855
|
-
// the deleted directory) nor to the root arm, which drops every name but
|
|
856
|
-
// its own files: `watching 0 project(s)` and silence until a restart. So
|
|
857
|
-
// each base, and each directory a config imports from, is watched from
|
|
858
|
-
// its nearest directory that exists, for the next name on the way down:
|
|
859
|
-
// its parent while it is there, an ancestor when the parent went too
|
|
860
|
-
// (`apps/` removed under `apps/web/*`).
|
|
862
|
+
// A project directory, or one a config imports from, removed and made
|
|
863
|
+
// again (`rm -rf packages && git checkout packages`) is no event to its
|
|
864
|
+
// own watch (it holds the deleted directory) nor to the root arm, which
|
|
865
|
+
// drops every name but its own files: silence until a restart. So each
|
|
866
|
+
// is watched from its nearest directory that exists, for the next name
|
|
867
|
+
// on the way down: its parent while it is there, an ancestor when the
|
|
868
|
+
// parent went too (`apps/` removed under `apps/web/site`). Every
|
|
869
|
+
// directory above it up to the root is watched for the next name too:
|
|
870
|
+
// FSEvents reports nothing for a watched directory's children when the
|
|
871
|
+
// directory itself goes with them, so on macOS the parent's arm alone
|
|
872
|
+
// never heard `packages/` removed under `packages/a` (inotify did).
|
|
861
873
|
const ancestorArms = new Map<string, WatchHandle>()
|
|
862
874
|
let ancestorNames = new Map<string, Set<string>>()
|
|
863
875
|
const armAncestors = (): void => {
|
|
864
876
|
ancestorNames = new Map()
|
|
865
|
-
|
|
877
|
+
const rootDir = path.resolve(workspaceRoot)
|
|
878
|
+
const below = projectDirs.filter((d) => path.resolve(d) !== rootDir)
|
|
879
|
+
for (const base of [...below, ...importNames.keys()]) {
|
|
866
880
|
let dir = path.dirname(base)
|
|
867
881
|
let name = path.basename(base)
|
|
868
882
|
while (inodeOf(dir) === undefined && dir !== workspaceRoot && dir !== path.dirname(dir)) {
|
|
869
883
|
name = path.basename(dir)
|
|
870
884
|
dir = path.dirname(dir)
|
|
871
885
|
}
|
|
872
|
-
|
|
886
|
+
for (;;) {
|
|
887
|
+
const names = ancestorNames.get(dir) ?? new Set<string>()
|
|
888
|
+
// A directory already holding this name was climbed from here before.
|
|
889
|
+
const climbed = names.has(name)
|
|
890
|
+
ancestorNames.set(dir, names.add(name))
|
|
891
|
+
// Outside the root (a preset a config imports from `../`) only the nearest arm.
|
|
892
|
+
if (climbed || !dir.startsWith(workspaceRoot + path.sep)) break
|
|
893
|
+
name = path.basename(dir)
|
|
894
|
+
dir = path.dirname(dir)
|
|
895
|
+
}
|
|
873
896
|
}
|
|
874
897
|
for (const [dir, handle] of ancestorArms) {
|
|
875
898
|
if (ancestorNames.has(dir) && !stale(dir)) continue
|
|
@@ -882,7 +905,8 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
882
905
|
ancestorArms.set(
|
|
883
906
|
dir,
|
|
884
907
|
arm(dir, false, (filename) => {
|
|
885
|
-
|
|
908
|
+
// A project's parent's `.gitignore` shapes what its keys read, as the root's does.
|
|
909
|
+
if (!ancestorNames.get(dir)?.has(filename) && filename !== '.gitignore') return
|
|
886
910
|
reread = true
|
|
887
911
|
const abs = path.join(dir, filename)
|
|
888
912
|
trigger(path.relative(workspaceRoot, abs), abs)
|
|
@@ -894,59 +918,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
894
918
|
}
|
|
895
919
|
}
|
|
896
920
|
}
|
|
897
|
-
|
|
898
|
-
armAncestors()
|
|
899
|
-
const want = new Set(memberBases)
|
|
900
|
-
for (const [base, handle] of baseArms) {
|
|
901
|
-
if (want.has(base) && !stale(base)) continue
|
|
902
|
-
handle.close()
|
|
903
|
-
baseArms.delete(base)
|
|
904
|
-
}
|
|
905
|
-
for (const base of want) {
|
|
906
|
-
if (baseArms.has(base)) continue
|
|
907
|
-
// Gone for now: its parent's arm hears it come back.
|
|
908
|
-
if (inodeOf(base) === undefined) continue
|
|
909
|
-
let members = memberEntries(base)
|
|
910
|
-
try {
|
|
911
|
-
baseArms.set(
|
|
912
|
-
base,
|
|
913
|
-
arm(base, false, (filename) => {
|
|
914
|
-
if (isIgnoredWatchPath(filename)) return
|
|
915
|
-
// Inputs are gitignore-aware: an edit here changes what the
|
|
916
|
-
// members' keys read, and `vx run` would miss on it.
|
|
917
|
-
if (filename === '.gitignore') {
|
|
918
|
-
trigger(
|
|
919
|
-
path.relative(workspaceRoot, path.join(base, filename)),
|
|
920
|
-
path.join(base, filename),
|
|
921
|
-
)
|
|
922
|
-
return
|
|
923
|
-
}
|
|
924
|
-
// Only a member coming or going. On macOS a non-recursive watcher
|
|
925
|
-
// also reports a member whose CONTENTS changed (FSEvents names the
|
|
926
|
-
// directory a write landed in), so a task writing into its own
|
|
927
|
-
// project — or the arm's own probe file — read as a member event
|
|
928
|
-
// and cost an uncached task one execution per cycle (CI,
|
|
929
|
-
// 2026-09-10).
|
|
930
|
-
const now = memberEntries(base)
|
|
931
|
-
// The same names, but a directory still without a package made
|
|
932
|
-
// again (or replaced by a rename): its pending watch holds the
|
|
933
|
-
// deleted one, and the `package.json` that lands next is heard
|
|
934
|
-
// only by a new arm.
|
|
935
|
-
const entry = path.join(base, filename)
|
|
936
|
-
if (sameMembers(members, now) && !(pending.has(entry) && stale(entry))) return
|
|
937
|
-
members = now
|
|
938
|
-
reread = true
|
|
939
|
-
trigger(`${path.relative(workspaceRoot, base)}/${filename}`, path.join(base, filename))
|
|
940
|
-
}),
|
|
941
|
-
)
|
|
942
|
-
armedAs.set(base, inodeOf(base) ?? '')
|
|
943
|
-
} catch (err) {
|
|
944
|
-
sayCannot(`cannot watch ${base}`, err)
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
armBases()
|
|
949
|
-
armPending(false)
|
|
921
|
+
armAncestors()
|
|
950
922
|
armImports()
|
|
951
923
|
|
|
952
924
|
// The orchestrator's own writes never kick the loop: `makeWatchIgnore`
|
|
@@ -963,7 +935,8 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
963
935
|
interruptIfWaiting()
|
|
964
936
|
}
|
|
965
937
|
|
|
966
|
-
|
|
938
|
+
lastTasks = args.tasks()
|
|
939
|
+
sayWatching(projects.length)
|
|
967
940
|
|
|
968
941
|
return await new Promise<void>((resolve) => {
|
|
969
942
|
const cleanup = async (): Promise<void> => {
|