@vzn/vx 0.0.652 → 0.0.653
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 +2 -2
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +1 -0
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +25 -3
- package/src/cache/archive.ts +196 -31
- package/src/cache/cache.ts +863 -225
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +22 -5
- package/src/cache/index.ts +10 -1
- package/src/cache/inputs.ts +573 -152
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +23 -10
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +50 -10
- package/src/cache/schema.ts +42 -9
- package/src/cli/cache.ts +147 -34
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +25 -2
- package/src/cli/index.ts +38 -7
- package/src/cli/info.ts +4 -3
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +184 -41
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +63 -18
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +72 -8
- package/src/cli/watch-set.ts +6 -4
- package/src/cli/watch.ts +179 -25
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +9 -0
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +76 -15
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-runtime.ts +134 -37
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +173 -38
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +327 -141
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +15 -5
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +189 -69
- package/src/index.ts +2 -1
- package/src/orchestrator/admission.ts +13 -9
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +41 -12
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/excluded-keys.ts +14 -2
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +564 -123
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +136 -87
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +25 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +33 -53
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +31 -17
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +81 -15
- package/src/orchestrator/plan.ts +37 -15
- package/src/orchestrator/plugin-host.ts +232 -108
- package/src/orchestrator/plugin.ts +53 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +43 -14
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +12 -10
- package/src/orchestrator/run-report.ts +2 -1
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +443 -107
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +286 -59
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +20 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +17 -9
- package/src/orchestrator/task-artifacts.ts +15 -2
- package/src/orchestrator/task-hash.ts +187 -30
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +80 -12
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +20 -3
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +165 -157
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +107 -30
- package/src/report/plain-output.ts +13 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +57 -14
- package/src/settings.ts +42 -6
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +17 -3
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +50 -6
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +16 -0
- package/src/util/size.ts +12 -2
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +126 -25
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +10 -1
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +46 -25
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
package/src/cli/watch.ts
CHANGED
|
@@ -16,6 +16,7 @@ import fs from 'node:fs'
|
|
|
16
16
|
import path from 'node:path'
|
|
17
17
|
import { parseRunArgs, resolveRunOptions, type RunArgs, spacedHint } from './run.js'
|
|
18
18
|
import { refuse, seeHelp } from './help.js'
|
|
19
|
+
import { fsClockNow, namedIgnoredReach, repoFacts } from '../cache/index.js'
|
|
19
20
|
import {
|
|
20
21
|
fingerprintClaims,
|
|
21
22
|
forwardedSignal,
|
|
@@ -37,12 +38,13 @@ import {
|
|
|
37
38
|
isProjectConfigFile,
|
|
38
39
|
isWorkspaceConfigFile,
|
|
39
40
|
gitFiles,
|
|
41
|
+
lockedExtras,
|
|
40
42
|
makeFence,
|
|
41
43
|
makeRootEventFilter,
|
|
42
44
|
makeWatchIgnore,
|
|
43
45
|
shapesWatchedSet,
|
|
44
46
|
} from './watch-filter.js'
|
|
45
|
-
import { CLOSED,
|
|
47
|
+
import { CLOSED, type WatchHandle, WatcherPool } from './watch-fs.js'
|
|
46
48
|
import { ChangeJudge } from './watch-judge.js'
|
|
47
49
|
import { type WatchCycle, watchCycle } from './watch-cycle.js'
|
|
48
50
|
import { hangupIgnored, maskedLine, printable, restartTimings } from '../util/index.js'
|
|
@@ -70,6 +72,14 @@ function sayAllCached(opts: RunOptions, outcomes: readonly { status: string }[])
|
|
|
70
72
|
ui.print('out', ui.line(' ', n === 1 ? '1 task' : `${n} tasks`, ' cached, nothing ran'))
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
function isDirectory(abs: string): boolean {
|
|
76
|
+
try {
|
|
77
|
+
return fs.statSync(abs).isDirectory()
|
|
78
|
+
} catch {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
/** Wait this long after the last filesystem event before re-running. */
|
|
74
84
|
const DEBOUNCE_MS = 150
|
|
75
85
|
/**
|
|
@@ -148,6 +158,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
148
158
|
handleSignals: false,
|
|
149
159
|
signal: stop.signal,
|
|
150
160
|
holdPersistent: true,
|
|
161
|
+
watch: true,
|
|
151
162
|
}
|
|
152
163
|
// Every cycle's `invocations` row names the verb, as `vx run`'s does;
|
|
153
164
|
// run()'s process.argv fallback put the bin's absolute path there, so
|
|
@@ -157,7 +168,10 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
157
168
|
// worth; every cycle after an edit must load and list live.
|
|
158
169
|
delete opts.staged
|
|
159
170
|
delete opts.discovered
|
|
160
|
-
|
|
171
|
+
// `on`, not `once`: a second signal during the teardown is absorbed (an
|
|
172
|
+
// abort is once), where `once` left it to Bun's default, exit 130 with
|
|
173
|
+
// the cycle's children mid-grace.
|
|
174
|
+
process.on('SIGINT', () => stop.abort('SIGINT'))
|
|
161
175
|
// Said once the loop is down, the last line: written as the signal
|
|
162
176
|
// landed, the aborted cycle's summary printed below it.
|
|
163
177
|
const stopped = (): number => {
|
|
@@ -167,11 +181,11 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
167
181
|
}
|
|
168
182
|
return 0
|
|
169
183
|
}
|
|
170
|
-
process.
|
|
184
|
+
process.on('SIGTERM', () => stop.abort('SIGTERM'))
|
|
171
185
|
// A task runs in its own session (exec/kill-tree.ts): the terminal
|
|
172
186
|
// closing reaches the loop alone, and the loop passes it on — unless vx
|
|
173
187
|
// was started with it ignored (nohup).
|
|
174
|
-
if (!hangupIgnored()) process.
|
|
188
|
+
if (!hangupIgnored()) process.on('SIGHUP', () => stop.abort('SIGHUP'))
|
|
175
189
|
|
|
176
190
|
// Enumerate projects-in-scope so we know what dirs to watch: the bare
|
|
177
191
|
// tasks' scope (`opts.projects`; undefined means "every project", or
|
|
@@ -223,8 +237,9 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
223
237
|
// ahead of the run to decide which watchers to install, and that marks every
|
|
224
238
|
// config loaded — so the initial run's own loads become REPEATs and pay a
|
|
225
239
|
// worker round-trip each (see config-eval.ts). Trading a hot-path regression
|
|
226
|
-
// on every `vx watch` for a window a user rarely edits into is a bad deal
|
|
227
|
-
//
|
|
240
|
+
// on every `vx watch` for a window a user rarely edits into is a bad deal.
|
|
241
|
+
// An edit to the inputs of a task the run was running is caught anyway:
|
|
242
|
+
// the run leaves that task unsaved, and the loop starts on it (`drifted`).
|
|
228
243
|
// The exception: once the run waits on a server's readiness alone, every
|
|
229
244
|
// config is loaded, and the loop arms with the run as its cycle in
|
|
230
245
|
// flight. Unarmed, a server that never got ready held the initial run
|
|
@@ -244,8 +259,11 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
244
259
|
// Uncaught, it ended watch at start while the same break mid-watch did
|
|
245
260
|
// not (item 1017).
|
|
246
261
|
let refusedToStart = false
|
|
262
|
+
/** A task whose inputs the user edited while the initial run ran it: no watcher heard the edit. */
|
|
263
|
+
let drifted: string | undefined
|
|
247
264
|
initial.held = runOrchestrator(initial.cycle.opts).then(
|
|
248
265
|
async (run) => {
|
|
266
|
+
drifted = run.outcomes.find((o) => o.unkeyed === true)?.node.id
|
|
249
267
|
if (run.refused !== undefined) {
|
|
250
268
|
const hint = run.refused.startsWith('no projects declare')
|
|
251
269
|
? await spacedHint(parsed, cwd)
|
|
@@ -288,10 +306,14 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
288
306
|
sayNotice('error', 'vx watch', 'no projects in scope')
|
|
289
307
|
return 1
|
|
290
308
|
}
|
|
291
|
-
await armLoop(undefined, held)
|
|
309
|
+
await armLoop(undefined, held, drifted)
|
|
292
310
|
return stopped()
|
|
293
311
|
|
|
294
|
-
async function armLoop(
|
|
312
|
+
async function armLoop(
|
|
313
|
+
initial?: InitialCycle,
|
|
314
|
+
held?: HeldPersistent,
|
|
315
|
+
drifted?: string,
|
|
316
|
+
): Promise<void> {
|
|
295
317
|
// `--affected`'s diff is the tree at start; judged again, it held every
|
|
296
318
|
// later edit out of the scope it picked. A cycle is an edit, and the
|
|
297
319
|
// cache keys decide what in the scope it re-runs.
|
|
@@ -309,6 +331,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
309
331
|
held,
|
|
310
332
|
initial,
|
|
311
333
|
tasks: openingTasks,
|
|
334
|
+
drifted,
|
|
312
335
|
stop: stop.signal,
|
|
313
336
|
workspaceRoot,
|
|
314
337
|
projects: watched,
|
|
@@ -330,12 +353,18 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
330
353
|
const all = await discoverCliProjects(workspaceRoot, load)
|
|
331
354
|
const sweep = await sweepConfigs(all, workspaceRoot, load, opts.tasks)
|
|
332
355
|
const now = await watchedProjects(workspaceRoot, all, inScope(all), load, sweep.staged)
|
|
356
|
+
const wsNow = await loadCliWorkspace(workspaceRoot, quiet)
|
|
333
357
|
return {
|
|
334
358
|
projects: now,
|
|
335
359
|
// A plugin the workspace config gained since claims its file from
|
|
336
360
|
// the cycle that loaded it; read once, its edits started nothing
|
|
337
361
|
// until a restart.
|
|
338
|
-
claimedRootFiles: claimsOf(
|
|
362
|
+
claimedRootFiles: claimsOf(wsNow.plugins),
|
|
363
|
+
// A re-lock that records a new `file:` directory or patch.
|
|
364
|
+
extraFiles: lockedExtras(workspaceRoot),
|
|
365
|
+
// The dir the cycle just wrote its store to: kept from the start,
|
|
366
|
+
// a `cache.dir` edit let those writes start the next cycle.
|
|
367
|
+
cacheDir: opts.cacheDir ?? wsNow.cacheDir,
|
|
339
368
|
workspaceWide: sweep.workspaceWide,
|
|
340
369
|
workspaceInputs: sweep.workspaceInputs,
|
|
341
370
|
outputs: sweep.outputs,
|
|
@@ -351,6 +380,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
351
380
|
// the one edit that changes what a cycle runs; unheard, the loop ran
|
|
352
381
|
// the old lock until a restart (item 971).
|
|
353
382
|
claimedRootFiles: claimsOf(ws.plugins),
|
|
383
|
+
extraFiles: lockedExtras(workspaceRoot),
|
|
354
384
|
// The RESOLVED cache dir, not the `.vx` literal — see `makeWatchIgnore`.
|
|
355
385
|
cacheDir: opts.cacheDir ?? ws.cacheDir,
|
|
356
386
|
poll: resolveSetting('watch.poll', ws.workspaceConfig, parsed.poll).value === true,
|
|
@@ -375,6 +405,8 @@ interface WatchLoopArgs {
|
|
|
375
405
|
initial: InitialCycle | undefined
|
|
376
406
|
/** The initial run's task count, once it has started. */
|
|
377
407
|
tasks: () => number | undefined
|
|
408
|
+
/** A task the initial run left unsaved because its inputs moved under it (`TaskOutcome.unkeyed`). */
|
|
409
|
+
drifted: string | undefined
|
|
378
410
|
/** Aborted by the SIGINT/SIGTERM handlers `watchCmd` installed; the loop drains its cycle and resolves. */
|
|
379
411
|
stop: AbortSignal
|
|
380
412
|
workspaceRoot: string
|
|
@@ -404,6 +436,8 @@ interface WatchLoopArgs {
|
|
|
404
436
|
* loaded once per process, so the set is fixed for the loop.
|
|
405
437
|
*/
|
|
406
438
|
claimedRootFiles: ReadonlySet<string>
|
|
439
|
+
/** The files a claimed file's recorded digests read besides it (`lockedExtras`), absolute. */
|
|
440
|
+
extraFiles: ReadonlySet<string>
|
|
407
441
|
/** Every project, in scope or not: a project's key leaves out what lies in one nested under it. */
|
|
408
442
|
fenceDirs: readonly string[]
|
|
409
443
|
/** The watched set again, after a cycle that followed an event which can change it. */
|
|
@@ -414,9 +448,20 @@ interface WatchLoopArgs {
|
|
|
414
448
|
|
|
415
449
|
const fenceDirs = (all: readonly ProjectMeta[]): string[] => all.map((p) => p.dir)
|
|
416
450
|
|
|
451
|
+
/** The input globs that name a `node_modules` segment, per directory: what the tree watcher enters. */
|
|
452
|
+
const modulesNamed = (inputs: ReadonlyMap<string, ReadonlyArray<readonly string[]>>): string =>
|
|
453
|
+
JSON.stringify(
|
|
454
|
+
[...inputs].map(([dir, tasks]) => [
|
|
455
|
+
dir,
|
|
456
|
+
tasks.flat().filter((g) => g.split('/').includes('node_modules')),
|
|
457
|
+
]),
|
|
458
|
+
)
|
|
459
|
+
|
|
417
460
|
interface Rediscovered {
|
|
418
461
|
projects: readonly ProjectMeta[]
|
|
462
|
+
cacheDir: string
|
|
419
463
|
claimedRootFiles: ReadonlySet<string>
|
|
464
|
+
extraFiles: ReadonlySet<string>
|
|
420
465
|
workspaceWide: boolean
|
|
421
466
|
workspaceInputs: readonly string[]
|
|
422
467
|
outputs: ReadonlyMap<string, readonly string[]>
|
|
@@ -429,9 +474,10 @@ interface Rediscovered {
|
|
|
429
474
|
}
|
|
430
475
|
|
|
431
476
|
async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
432
|
-
const { opts, stop, workspaceRoot, projects
|
|
477
|
+
const { opts, stop, workspaceRoot, projects } = args
|
|
433
478
|
// The watched set as of the last cycle: `rearm` replaces these after an
|
|
434
479
|
// event that can change it, and every filter below reads the current one.
|
|
480
|
+
let cacheDir = args.cacheDir
|
|
435
481
|
let workspaceWide = args.workspaceWide
|
|
436
482
|
let projectDirs = args.projectDirs
|
|
437
483
|
let workspaceInputs = args.workspaceInputs
|
|
@@ -440,6 +486,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
440
486
|
let uncached = args.uncached
|
|
441
487
|
let namedIgnored = args.namedIgnored
|
|
442
488
|
let claimedRootFiles = args.claimedRootFiles
|
|
489
|
+
let extraFiles = args.extraFiles
|
|
443
490
|
let configImportFiles = args.configImports
|
|
444
491
|
let wsConfigImportFiles = args.workspaceConfigImports
|
|
445
492
|
// A dev server stays up while the loop idles and across cycles; a cycle
|
|
@@ -517,11 +564,12 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
517
564
|
}
|
|
518
565
|
const trigger = (label: string, abs: string): void => {
|
|
519
566
|
if (!changes.pending.has(abs)) changes.pending.set(abs, label)
|
|
567
|
+
changes.sawEvent(abs)
|
|
520
568
|
if (running) return interruptIfWaiting()
|
|
521
569
|
if (debounceTimer) {
|
|
522
|
-
if (
|
|
570
|
+
if (performance.now() - windowOpened >= DEBOUNCE_MAX_MS) return
|
|
523
571
|
clearTimeout(debounceTimer)
|
|
524
|
-
} else windowOpened =
|
|
572
|
+
} else windowOpened = performance.now()
|
|
525
573
|
debounceTimer = setTimeout(() => {
|
|
526
574
|
debounceTimer = null
|
|
527
575
|
if (running) return
|
|
@@ -544,6 +592,10 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
544
592
|
// The initial run is collected even after a stop: its server is the
|
|
545
593
|
// loop's to stop.
|
|
546
594
|
while (initial !== undefined || (label !== undefined && !stop.aborted)) {
|
|
595
|
+
/** This cycle's failure, as said: a re-read failing on the same cause says nothing more. */
|
|
596
|
+
let failed: string | undefined
|
|
597
|
+
/** The judged cycle this run is (`ChangeJudge.cycle`): its window closes when the run ends. */
|
|
598
|
+
const judged = changes.cycle
|
|
547
599
|
if (initial !== undefined) {
|
|
548
600
|
held = await initial.held
|
|
549
601
|
initial = undefined
|
|
@@ -566,6 +618,8 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
566
618
|
`no .vx/workspace.ts in ${workspaceRoot}; restore it, or stop vx watch`,
|
|
567
619
|
)
|
|
568
620
|
}
|
|
621
|
+
// A failed re-read is said once, by the re-read after the
|
|
622
|
+
// cycle: said here too, it printed above the cycle's header.
|
|
569
623
|
if (reread) await rearm(false)
|
|
570
624
|
ui.print('out', ui.gap())
|
|
571
625
|
ui.say('info', `changed ${label}`, 'out')
|
|
@@ -576,9 +630,11 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
576
630
|
// as a file born and gone since, and ran nothing.
|
|
577
631
|
if (
|
|
578
632
|
atArm !== undefined &&
|
|
579
|
-
changes.changed.some((p) => path.basename(p) === '.gitignore')
|
|
580
|
-
)
|
|
633
|
+
changes.changed.some((p) => path.basename(p) === '.gitignore' || p === excludeFile)
|
|
634
|
+
) {
|
|
581
635
|
for (const p of gitFiles(workspaceRoot)?.listed ?? []) atArm.listed.add(p)
|
|
636
|
+
reachOf = undefined
|
|
637
|
+
}
|
|
582
638
|
restartTimings()
|
|
583
639
|
// On the mtime clock, as the arm is: from `Date.now()` a write the
|
|
584
640
|
// run made within a tick of it carried an earlier mtime and read as
|
|
@@ -589,11 +645,18 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
589
645
|
// takes it from there. The run stops the others before its graph.
|
|
590
646
|
const before = held
|
|
591
647
|
current = watchCycle(
|
|
592
|
-
{
|
|
648
|
+
{
|
|
649
|
+
...opts,
|
|
650
|
+
...(before ? { keep: before } : {}),
|
|
651
|
+
...(judged ? { commandWindows: judged.windows } : {}),
|
|
652
|
+
},
|
|
593
653
|
stop,
|
|
594
654
|
interruptIfWaiting,
|
|
595
655
|
)
|
|
596
656
|
const cycle = await runOrchestrator(current.opts)
|
|
657
|
+
changes.unseen(
|
|
658
|
+
cycle.outcomes.flatMap((o) => (o.driftedInput === undefined ? [] : [o.driftedInput])),
|
|
659
|
+
)
|
|
597
660
|
sayAllCached(opts, cycle.outcomes)
|
|
598
661
|
lastTasks = current.tasks ?? lastTasks
|
|
599
662
|
current = undefined
|
|
@@ -611,15 +674,20 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
611
674
|
// FS event (if any) will retry. The dispose() on SIGINT
|
|
612
675
|
// is the canonical exit; we don't unilaterally abort here.
|
|
613
676
|
// A config's own throw or a plugin's failure may quote a secret (L-11).
|
|
614
|
-
|
|
615
|
-
sayNotice('error', 'vx watch', `cycle failed: ${
|
|
677
|
+
failed = err instanceof Error ? err.message : String(err)
|
|
678
|
+
sayNotice('error', 'vx watch', `cycle failed: ${maskedLine(failed)}`)
|
|
616
679
|
}
|
|
680
|
+
if (judged !== undefined) judged.to = Math.min(judged.to, Date.now())
|
|
617
681
|
// After a failed cycle too: a package added with a config that does
|
|
618
682
|
// not load yet fails its cycle, and unarmed, the fix to that config
|
|
619
683
|
// was never an event.
|
|
620
684
|
if (reread && !stop.aborted && hasWorkspaceFile()) {
|
|
621
685
|
reread = false
|
|
622
|
-
await rearm()
|
|
686
|
+
const err = await rearm()
|
|
687
|
+
// No lock is the cycle's own failure to say; said here too, it was two lines.
|
|
688
|
+
const why = err instanceof Error ? err.message : String(err)
|
|
689
|
+
if (err !== undefined && why !== NO_LOCK && why !== failed)
|
|
690
|
+
sayCannot(`cannot re-read the workspace`, err)
|
|
623
691
|
}
|
|
624
692
|
// A re-lock since this cycle began: the re-arm can close the watcher
|
|
625
693
|
// its event was still on its way to (FSEvents' latency, a poller's
|
|
@@ -679,15 +747,30 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
679
747
|
const arm = (dir: string, recursive: boolean, onEvent: (filename: string) => void): WatchHandle =>
|
|
680
748
|
pool.arm(dir, recursive, onEvent)
|
|
681
749
|
|
|
750
|
+
// Only the named entries a key reads ignored files under: `src` of
|
|
751
|
+
// `src/**` over a listed `src` reaches none, and a dev server's ignored
|
|
752
|
+
// `src/dev.log` started ~3 cycles a second. Reckoned per sweep.
|
|
753
|
+
let reachOf: readonly string[] | undefined
|
|
754
|
+
let reach: readonly string[] = []
|
|
682
755
|
/** 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. */
|
|
683
756
|
const armedAt = fsClockNow(cacheDir)
|
|
684
757
|
/** Which settled paths are changes (watch-judge.ts); `pending` holds what fired since. */
|
|
685
758
|
const changes = new ChangeJudge({
|
|
686
759
|
workspaceRoot,
|
|
687
760
|
armedAt,
|
|
761
|
+
clock: () => fsClockNow(cacheDir),
|
|
688
762
|
held: () => held !== undefined,
|
|
689
763
|
uncached: () => uncached,
|
|
690
|
-
namedIgnored: () =>
|
|
764
|
+
namedIgnored: () => {
|
|
765
|
+
if (reachOf !== namedIgnored) {
|
|
766
|
+
reachOf = namedIgnored
|
|
767
|
+
reach =
|
|
768
|
+
atArm === undefined
|
|
769
|
+
? namedIgnored
|
|
770
|
+
: namedIgnoredReach(workspaceRoot, namedIgnored, atArm.listed)
|
|
771
|
+
}
|
|
772
|
+
return reach
|
|
773
|
+
},
|
|
691
774
|
fenced: (ownDir, abs) => fenced(ownDir, abs),
|
|
692
775
|
awaited: (abs) => ancestorNames.get(path.dirname(abs))?.has(path.basename(abs)) === true,
|
|
693
776
|
...(atArm !== undefined ? { existedAtArm: atArm.listed, trackedAtArm: atArm.tracked } : {}),
|
|
@@ -700,6 +783,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
700
783
|
// went live in 8 ms (tests/watch-one-stream.test.ts). Polling keeps its
|
|
701
784
|
// per-project walks, which skip what lies between projects.
|
|
702
785
|
const oneStream = process.platform === 'darwin' && !args.poll
|
|
786
|
+
const treeArms = process.platform === 'linux' && !args.poll
|
|
703
787
|
/** The projects the one stream hands events to, by their real directory. */
|
|
704
788
|
let streamed: { proj: ProjectMeta; real: string }[] = []
|
|
705
789
|
const realRoot = ((): string => {
|
|
@@ -1037,6 +1121,64 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
1037
1121
|
}
|
|
1038
1122
|
}
|
|
1039
1123
|
|
|
1124
|
+
/**
|
|
1125
|
+
* The files a claimed file's recorded digests read besides it (a `file:`
|
|
1126
|
+
* dependency's directory, a patch; `lockedExtras`): a run keys them, and
|
|
1127
|
+
* no project's arm hears one outside its tree. A directory recursively,
|
|
1128
|
+
* a file (or a path gone for now) from its directory; armed again when a
|
|
1129
|
+
* re-lock changes the set.
|
|
1130
|
+
*/
|
|
1131
|
+
const extraArms = new Map<string, { at: string; handle: WatchHandle }>()
|
|
1132
|
+
const armExtras = (): void => {
|
|
1133
|
+
const want = new Map<string, { at: string; dir: boolean }>()
|
|
1134
|
+
for (const abs of extraFiles) {
|
|
1135
|
+
const dir = isDirectory(abs)
|
|
1136
|
+
want.set(abs, { at: dir ? abs : path.dirname(abs), dir })
|
|
1137
|
+
}
|
|
1138
|
+
for (const [abs, armed] of extraArms) {
|
|
1139
|
+
if (want.get(abs)?.at === armed.at && !stale(armed.at)) continue
|
|
1140
|
+
armed.handle.close()
|
|
1141
|
+
extraArms.delete(abs)
|
|
1142
|
+
}
|
|
1143
|
+
for (const [abs, { at, dir }] of want) {
|
|
1144
|
+
if (extraArms.has(abs) || inodeOf(at) === undefined) continue
|
|
1145
|
+
const name = path.basename(abs)
|
|
1146
|
+
try {
|
|
1147
|
+
const handle = arm(at, dir, (filename) => {
|
|
1148
|
+
if (!dir && filename !== name) return
|
|
1149
|
+
const changed = dir ? path.join(abs, filename) : abs
|
|
1150
|
+
// A directory made where a file or nothing was armed: armed again, recursively.
|
|
1151
|
+
if (!dir && isDirectory(abs)) reread = true
|
|
1152
|
+
trigger(`root ${path.relative(workspaceRoot, changed)}`, changed)
|
|
1153
|
+
})
|
|
1154
|
+
extraArms.set(abs, { at, handle })
|
|
1155
|
+
armedAs.set(at, inodeOf(at) ?? '')
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
sayCannot(`cannot watch ${at}`, err)
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* `info/exclude` ignores as a `.gitignore` does, and keys are
|
|
1163
|
+
* gitignore-aware (X-136), so its edit is a cycle; the ignore filter drops
|
|
1164
|
+
* every path under `.git/`. Armed once, from the common dir the run's own
|
|
1165
|
+
* enumeration asked git for (`repoFacts`, memoised).
|
|
1166
|
+
*/
|
|
1167
|
+
const commonDir = repoFacts(workspaceRoot)?.commonDir
|
|
1168
|
+
const excludeFile =
|
|
1169
|
+
commonDir === undefined ? undefined : path.resolve(workspaceRoot, commonDir, 'info', 'exclude')
|
|
1170
|
+
const armExclude = (): void => {
|
|
1171
|
+
if (excludeFile === undefined || inodeOf(path.dirname(excludeFile)) === undefined) return
|
|
1172
|
+
try {
|
|
1173
|
+
arm(path.dirname(excludeFile), false, (filename) => {
|
|
1174
|
+
if (filename === 'exclude')
|
|
1175
|
+
trigger(`root ${path.relative(workspaceRoot, excludeFile)}`, excludeFile)
|
|
1176
|
+
})
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
sayCannot(`cannot watch ${path.dirname(excludeFile)}`, err)
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1040
1182
|
// Said once, when the loop first arms: a re-arm repeated it after every
|
|
1041
1183
|
// cycle that re-read the watched set, between task rows. `debug.watch`
|
|
1042
1184
|
// says each arm on stderr, for whoever needs to see the set change.
|
|
@@ -1055,23 +1197,26 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
1055
1197
|
)
|
|
1056
1198
|
}
|
|
1057
1199
|
|
|
1058
|
-
|
|
1200
|
+
/** Re-read the workspace and re-arm on it; a failed re-read is returned, for the caller to say. */
|
|
1201
|
+
const rearm = async (say = true): Promise<unknown> => {
|
|
1059
1202
|
let next: Rediscovered
|
|
1060
1203
|
try {
|
|
1061
1204
|
next = await args.rediscover()
|
|
1062
1205
|
} catch (err) {
|
|
1063
|
-
|
|
1064
|
-
if (!(err instanceof Error && err.message === NO_LOCK))
|
|
1065
|
-
sayCannot(`cannot re-read the workspace`, err)
|
|
1066
|
-
return
|
|
1206
|
+
return err
|
|
1067
1207
|
}
|
|
1208
|
+
cacheDir = next.cacheDir
|
|
1068
1209
|
projectDirs = next.projects.map((p) => p.dir)
|
|
1069
1210
|
workspaceInputs = next.workspaceInputs
|
|
1070
1211
|
outputs = next.outputs
|
|
1212
|
+
// Linux's tree watcher picks the `node_modules` it enters when it arms
|
|
1213
|
+
// (watch-fs.ts): an input newly named there needs the trees armed again.
|
|
1214
|
+
const modulesRenamed = treeArms && modulesNamed(next.inputs) !== modulesNamed(inputs)
|
|
1071
1215
|
inputs = next.inputs
|
|
1072
1216
|
uncached = next.uncached
|
|
1073
1217
|
namedIgnored = next.namedIgnored
|
|
1074
1218
|
claimedRootFiles = next.claimedRootFiles
|
|
1219
|
+
extraFiles = next.extraFiles
|
|
1075
1220
|
configImportFiles = next.configImports
|
|
1076
1221
|
wsConfigImportFiles = next.workspaceConfigImports
|
|
1077
1222
|
armAncestors()
|
|
@@ -1084,7 +1229,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
1084
1229
|
claimedRootFiles,
|
|
1085
1230
|
fenced,
|
|
1086
1231
|
)
|
|
1087
|
-
if (next.workspaceWide !== workspaceWide) {
|
|
1232
|
+
if (next.workspaceWide !== workspaceWide || modulesRenamed) {
|
|
1088
1233
|
// A task started or stopped declaring `workspaceFiles`: the other
|
|
1089
1234
|
// arm's shape. Until item 891 the choice was made once, at start, and
|
|
1090
1235
|
// a root file a config newly named was never an event.
|
|
@@ -1108,10 +1253,12 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
1108
1253
|
watched = next.projects
|
|
1109
1254
|
links = gitFiles(workspaceRoot)?.links ?? links
|
|
1110
1255
|
armLinks()
|
|
1256
|
+
armExtras()
|
|
1111
1257
|
// A new arm proves delivery like the first ones: an edit in the new
|
|
1112
1258
|
// project right after this cycle is seen, not lost in the gap.
|
|
1113
1259
|
await pool.proved()
|
|
1114
1260
|
if (say) sayArmed(next.projects.length)
|
|
1261
|
+
return undefined
|
|
1115
1262
|
}
|
|
1116
1263
|
|
|
1117
1264
|
armMode(projects)
|
|
@@ -1193,6 +1340,8 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
1193
1340
|
armAncestors()
|
|
1194
1341
|
armImports()
|
|
1195
1342
|
armLinks()
|
|
1343
|
+
armExtras()
|
|
1344
|
+
armExclude()
|
|
1196
1345
|
|
|
1197
1346
|
// The orchestrator's own writes never kick the loop: `makeWatchIgnore`
|
|
1198
1347
|
// closes over the RESOLVED cache dir (relocated or not) and the tasks'
|
|
@@ -1211,6 +1360,11 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
1211
1360
|
lastTasks = args.tasks()
|
|
1212
1361
|
sayWatching(projects.length)
|
|
1213
1362
|
sayArmed(projects.length)
|
|
1363
|
+
// An edit made while the initial run ran landed before any watcher, and
|
|
1364
|
+
// its mtime predates the arm, so no event or judgement can find it: the
|
|
1365
|
+
// run's own re-check did (it left the task unsaved), and that starts the
|
|
1366
|
+
// first cycle. Unheard, watch idled over output built from the old bytes.
|
|
1367
|
+
if (args.drifted !== undefined) void cycle(`${args.drifted} inputs, edited during the first run`)
|
|
1214
1368
|
|
|
1215
1369
|
return await new Promise<void>((resolve) => {
|
|
1216
1370
|
const cleanup = async (): Promise<void> => {
|