@vzn/vx 0.0.595 → 0.0.596
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +5 -5
- package/src/cache/cache.ts +139 -84
- package/src/cache/chained-cache.ts +25 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/inputs.ts +12 -12
- package/src/cache/layer.ts +7 -5
- package/src/cache/policy.ts +5 -0
- package/src/cache/zstd.ts +10 -4
- package/src/cli/cache.ts +10 -4
- package/src/cli/foreign-flags.ts +1 -1
- package/src/cli/last.ts +4 -1
- package/src/cli/run.ts +31 -15
- package/src/cli/watch-filter.ts +63 -12
- package/src/cli/watch-judge.ts +15 -8
- package/src/cli/watch.ts +4 -4
- package/src/config.ts +21 -21
- package/src/exec/proc-sample.ts +1 -1
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +116 -40
- package/src/exec/sandbox-violations.ts +58 -11
- package/src/graph/scheduler.ts +28 -12
- package/src/graph/task-graph.ts +134 -25
- package/src/orchestrator/admission.ts +12 -3
- package/src/orchestrator/events.ts +10 -1
- package/src/orchestrator/execute-task.ts +36 -13
- package/src/orchestrator/failure-mode.ts +8 -2
- package/src/orchestrator/framed-output.ts +3 -3
- package/src/orchestrator/logger.ts +11 -6
- package/src/orchestrator/metrics.ts +19 -8
- package/src/orchestrator/miss-save.ts +2 -2
- package/src/orchestrator/options.ts +7 -0
- package/src/orchestrator/plan.ts +3 -3
- package/src/orchestrator/plugin.ts +37 -16
- package/src/orchestrator/prepare.ts +13 -4
- package/src/orchestrator/run-artifacts.ts +2 -1
- package/src/orchestrator/run.ts +20 -4
- package/src/orchestrator/sandbox-request.ts +65 -9
- package/src/orchestrator/save-lane.ts +5 -5
- package/src/orchestrator/summary.ts +2 -4
- package/src/orchestrator/telemetry.ts +16 -7
- package/src/util/paths.ts +23 -3
- package/src/workspace/config-imports.ts +7 -5
- package/src/workspace/config-schema.ts +26 -3
- package/src/workspace/filter.ts +19 -0
- package/src/workspace/migrate-scripts.ts +4 -2
- package/src/workspace/migration.ts +1 -1
- package/src/workspace/project-loader.ts +4 -2
- package/src/workspace/repo-id.ts +42 -4
- package/src/workspace/workspace.ts +17 -10
package/src/cli/cache.ts
CHANGED
|
@@ -85,10 +85,15 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
|
|
|
85
85
|
if (a === '--older-than' || a?.startsWith('--older-than=')) {
|
|
86
86
|
const v = a === '--older-than' ? args[++i] : a.slice('--older-than='.length)
|
|
87
87
|
if (v === undefined || v === '') {
|
|
88
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
error: `--older-than requires a value: a duration like 30d, 24h or 60m${seeHelp('cache')}`,
|
|
90
|
+
}
|
|
89
91
|
}
|
|
90
92
|
const ms = parseDuration(v)
|
|
91
|
-
if (ms === null)
|
|
93
|
+
if (ms === null)
|
|
94
|
+
return {
|
|
95
|
+
error: `--older-than must be a duration like 30d, 24h or 60m (got ${v})${seeHelp('cache')}`,
|
|
96
|
+
}
|
|
92
97
|
// `0d` evicts everything ever cached. That is far more often a
|
|
93
98
|
// computed-to-zero retention than an intent, and no other flag
|
|
94
99
|
// combination expresses "wipe the cache" — so refuse it and name
|
|
@@ -102,10 +107,11 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
|
|
|
102
107
|
} else if (a === '--max-size' || a?.startsWith('--max-size=')) {
|
|
103
108
|
const v = a === '--max-size' ? args[++i] : a.slice('--max-size='.length)
|
|
104
109
|
if (v === undefined || v === '') {
|
|
105
|
-
return { error: `--max-size requires a value
|
|
110
|
+
return { error: `--max-size requires a value: a size like 500M or 1G${seeHelp('cache')}` }
|
|
106
111
|
}
|
|
107
112
|
const bytes = parseSize(v)
|
|
108
|
-
if (bytes === null)
|
|
113
|
+
if (bytes === null)
|
|
114
|
+
return { error: `--max-size must be a size like 500M or 1G (got ${v})${seeHelp('cache')}` }
|
|
109
115
|
if (bytes === 0) {
|
|
110
116
|
return {
|
|
111
117
|
error: `--max-size 0 would evict every entry — to clear the cache, delete the cache store \`vx info\` names`,
|
package/src/cli/foreign-flags.ts
CHANGED
|
@@ -375,7 +375,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
|
|
|
375
375
|
names: ['--preflight'],
|
|
376
376
|
value: false,
|
|
377
377
|
outcome: 'refuse',
|
|
378
|
-
vx: '`turboCache(
|
|
378
|
+
vx: 'set `turboCache({ preflight: true })` or `TURBO_PREFLIGHT=1`',
|
|
379
379
|
},
|
|
380
380
|
{
|
|
381
381
|
runner: 'turbo',
|
package/src/cli/last.ts
CHANGED
|
@@ -60,7 +60,10 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
60
60
|
if (spaced) i++
|
|
61
61
|
const n = Number(lv)
|
|
62
62
|
if (!Number.isInteger(n) || n < 1 || n > 500) {
|
|
63
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
...out,
|
|
65
|
+
error: `--list must be a whole number from 1 to 500 (got ${lv})${seeHelp('last')}`,
|
|
66
|
+
}
|
|
64
67
|
}
|
|
65
68
|
out.list = n
|
|
66
69
|
continue
|
package/src/cli/run.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type RunOptions,
|
|
13
13
|
type RunResult,
|
|
14
14
|
} from '../orchestrator/index.js'
|
|
15
|
-
import type { ContinueMode } from '../graph/index.js'
|
|
15
|
+
import type { ContinueMode, TaskOutcome } from '../graph/index.js'
|
|
16
16
|
import { type CachePolicy, FULL_CACHE_POLICY, parseCachePolicy } from '../cache/index.js'
|
|
17
17
|
import { affectedFilterFor, findCwdSelection, pickTask, resolveFilters } from './select.js'
|
|
18
18
|
import { nxTargetHint, taskNamesHere } from './task-verb.js'
|
|
@@ -486,6 +486,7 @@ export async function resolveRunOptions(
|
|
|
486
486
|
parsed: RunArgs,
|
|
487
487
|
cwd: string,
|
|
488
488
|
tasks: readonly string[],
|
|
489
|
+
verb: 'run' | 'watch' = 'run',
|
|
489
490
|
): Promise<RunOptions | { error: string } | { nothingSelected: string }> {
|
|
490
491
|
for (const t of tasks) {
|
|
491
492
|
const idx = t.indexOf('#')
|
|
@@ -557,8 +558,7 @@ export async function resolveRunOptions(
|
|
|
557
558
|
const nx = await nxTargetHint(tasks, cwd)
|
|
558
559
|
if (nx !== null) return { error: nx }
|
|
559
560
|
return {
|
|
560
|
-
error:
|
|
561
|
-
'not inside a project. Pass --all for every project, --filter <pattern> to filter, or run from within a project directory.',
|
|
561
|
+
error: `not inside a project: run from a project directory, or pass --all or --filter <pattern>${seeHelp(verb)}`,
|
|
562
562
|
}
|
|
563
563
|
}
|
|
564
564
|
projects = [cwdProject.name]
|
|
@@ -726,6 +726,25 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
726
726
|
return 0
|
|
727
727
|
}
|
|
728
728
|
|
|
729
|
+
// The report prints above the footer: nothing prints below it (owner).
|
|
730
|
+
// Rendered once, however many sinks asked for it; a kept server's crash
|
|
731
|
+
// after the footer re-renders the file's copy (C-53).
|
|
732
|
+
const wantsReport = parsed.report === 'markdown' || parsed.reportFile !== undefined
|
|
733
|
+
let reported: { outcomes: readonly TaskOutcome[]; md: string } | undefined
|
|
734
|
+
if (wantsReport) {
|
|
735
|
+
opts.beforeFooter = (outcomes, ok) => {
|
|
736
|
+
reported = {
|
|
737
|
+
outcomes,
|
|
738
|
+
md: formatRunReportMarkdown({ ok, outcomes: outcomes.map(projectOutcome) }),
|
|
739
|
+
}
|
|
740
|
+
// `--report` writes stdout. The report ITSELF is machine-clean, but
|
|
741
|
+
// stdout is not vx's alone — the status logger writes there too, so
|
|
742
|
+
// `--report=markdown >> "$GITHUB_STEP_SUMMARY"` captures every frame,
|
|
743
|
+
// meter bar and `::group::` command above the table. `--report-file`
|
|
744
|
+
// is the redirect-free form.
|
|
745
|
+
return parsed.report === 'markdown' ? reported.md : ''
|
|
746
|
+
}
|
|
747
|
+
}
|
|
729
748
|
// A run executes in THIS process, always. Where an individual task's
|
|
730
749
|
// command runs is the `executor` capability's business (per task, with
|
|
731
750
|
// the scheduler, cache, retries and telemetry unchanged above it); there
|
|
@@ -735,17 +754,14 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
735
754
|
process.stderr.write(`vx run: ${summary.refused}\n`)
|
|
736
755
|
return 1
|
|
737
756
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
// meter bar and `::group::` command above the table. `--report-file`
|
|
747
|
-
// is the redirect-free form.
|
|
748
|
-
if (parsed.report === 'markdown') process.stdout.write(md)
|
|
757
|
+
if (wantsReport) {
|
|
758
|
+
const md =
|
|
759
|
+
reported?.outcomes === summary.outcomes
|
|
760
|
+
? reported.md
|
|
761
|
+
: formatRunReportMarkdown({
|
|
762
|
+
ok: summary.ok,
|
|
763
|
+
outcomes: summary.outcomes.map(projectOutcome),
|
|
764
|
+
})
|
|
749
765
|
if (parsed.reportFile !== undefined) {
|
|
750
766
|
const target = path.resolve(cwd, parsed.reportFile)
|
|
751
767
|
try {
|
|
@@ -768,5 +784,5 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
768
784
|
}
|
|
769
785
|
}
|
|
770
786
|
}
|
|
771
|
-
return
|
|
787
|
+
return summary.ok ? 0 : 1
|
|
772
788
|
}
|
package/src/cli/watch-filter.ts
CHANGED
|
@@ -179,9 +179,14 @@ export function gitIgnored(workspaceRoot: string, paths: readonly string[]): Set
|
|
|
179
179
|
// ignored let a pid file in the same window start cycles again. The
|
|
180
180
|
// refused path is skipped and the rest asked again; a refusal before
|
|
181
181
|
// any record outside a work tree is git refusing them all.
|
|
182
|
-
|
|
182
|
+
// Git refuses a path beyond a symbolic link: its ignored pid file read
|
|
183
|
+
// as an edit and an uncached task re-ran forever. Asked at its real
|
|
184
|
+
// place, answered as given.
|
|
185
|
+
const real = paths.map(gitSpeller(workspaceRoot))
|
|
186
|
+
let from = 0
|
|
183
187
|
let inWorkTree: boolean | undefined
|
|
184
|
-
while (
|
|
188
|
+
while (from < paths.length) {
|
|
189
|
+
const rest = real.slice(from)
|
|
185
190
|
let proc: ReturnType<typeof Bun.spawnSync>
|
|
186
191
|
try {
|
|
187
192
|
proc = Bun.spawnSync({
|
|
@@ -201,8 +206,8 @@ export function gitIgnored(workspaceRoot: string, paths: readonly string[]): Set
|
|
|
201
206
|
const fields = new TextDecoder('utf-8', { ignoreBOM: true }).decode(proc.stdout).split('\0')
|
|
202
207
|
const records = Math.floor(fields.length / 4)
|
|
203
208
|
for (let i = 0; i < records; i++) {
|
|
204
|
-
const [source, , pattern
|
|
205
|
-
if (source !== '' && !pattern!.startsWith('!')) ignored.add(
|
|
209
|
+
const [source, , pattern] = fields.slice(i * 4, i * 4 + 4)
|
|
210
|
+
if (source !== '' && !pattern!.startsWith('!')) ignored.add(paths[from + i]!)
|
|
206
211
|
}
|
|
207
212
|
// 0: some ignored; 1: none. Anything else is git refusing.
|
|
208
213
|
if (proc.exitCode === 0 || proc.exitCode === 1) return ignored
|
|
@@ -216,7 +221,7 @@ export function gitIgnored(workspaceRoot: string, paths: readonly string[]): Set
|
|
|
216
221
|
}).exitCode === 0
|
|
217
222
|
if (!inWorkTree) return ignored
|
|
218
223
|
}
|
|
219
|
-
|
|
224
|
+
from += records + 1
|
|
220
225
|
}
|
|
221
226
|
return ignored
|
|
222
227
|
}
|
|
@@ -351,19 +356,59 @@ export function isWorkspaceConfigFile(name: string): boolean {
|
|
|
351
356
|
*/
|
|
352
357
|
const FINGERPRINT_FILES: ReadonlySet<string> = new Set(WORKSPACE_FINGERPRINT_FILES)
|
|
353
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Paths as git spells them under `root`: a project reached through a
|
|
361
|
+
* symlink below the root (`packages/x -> ../shared/x`) is watched at the
|
|
362
|
+
* link, and git names and answers for its files only at the real place.
|
|
363
|
+
* The root keeps its own spelling (macOS's `/var` is `/private/var`), the
|
|
364
|
+
* name its own (the file may be gone); a directory outside the root is
|
|
365
|
+
* kept as given. Directories are resolved once per speller.
|
|
366
|
+
*/
|
|
367
|
+
export function gitSpeller(root: string): (p: string) => string {
|
|
368
|
+
const dirs = new Map<string, string>()
|
|
369
|
+
let realRoot: string | undefined
|
|
370
|
+
return (p) => {
|
|
371
|
+
const dir = path.dirname(p)
|
|
372
|
+
let at = dirs.get(dir)
|
|
373
|
+
if (at === undefined) {
|
|
374
|
+
at = dir
|
|
375
|
+
try {
|
|
376
|
+
realRoot ??= realPath(root)
|
|
377
|
+
const rel = path.relative(realRoot, realPath(dir))
|
|
378
|
+
if (rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel)) {
|
|
379
|
+
at = path.join(root, rel)
|
|
380
|
+
}
|
|
381
|
+
} catch {}
|
|
382
|
+
dirs.set(dir, at)
|
|
383
|
+
}
|
|
384
|
+
return path.join(at, path.basename(p))
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
354
388
|
/**
|
|
355
389
|
* The files under the workspace git lists, tracked and untracked, ignored
|
|
356
390
|
* ones aside, and every directory above one (git lists no directory, and
|
|
357
391
|
* a project moved away whole is one event on its directory): what existed
|
|
358
392
|
* when watch armed, so a path born and gone since is told from a deletion
|
|
359
|
-
* (watch-judge.ts).
|
|
360
|
-
*
|
|
393
|
+
* (watch-judge.ts). `tracked` is the files git tracks: a user edits those,
|
|
394
|
+
* so a server cannot be blamed for one. Undefined when git cannot answer:
|
|
395
|
+
* no inventory, every gone path is a deletion as before.
|
|
361
396
|
*/
|
|
362
|
-
export function gitFiles(
|
|
397
|
+
export function gitFiles(
|
|
398
|
+
workspaceRoot: string,
|
|
399
|
+
): { listed: Set<string>; tracked: Set<string> } | undefined {
|
|
363
400
|
let proc: ReturnType<typeof Bun.spawnSync>
|
|
364
401
|
try {
|
|
365
402
|
proc = Bun.spawnSync({
|
|
366
|
-
cmd: [
|
|
403
|
+
cmd: [
|
|
404
|
+
executablePath('git'),
|
|
405
|
+
'ls-files',
|
|
406
|
+
'-z',
|
|
407
|
+
'-t',
|
|
408
|
+
'--cached',
|
|
409
|
+
'--others',
|
|
410
|
+
'--exclude-standard',
|
|
411
|
+
],
|
|
367
412
|
cwd: workspaceRoot,
|
|
368
413
|
stdout: 'pipe',
|
|
369
414
|
stderr: 'ignore',
|
|
@@ -373,13 +418,19 @@ export function gitFiles(workspaceRoot: string): Set<string> | undefined {
|
|
|
373
418
|
}
|
|
374
419
|
if (proc.exitCode !== 0) return undefined
|
|
375
420
|
const files = new Set<string>()
|
|
376
|
-
|
|
377
|
-
|
|
421
|
+
const tracked = new Set<string>()
|
|
422
|
+
for (const entry of new TextDecoder('utf-8', { ignoreBOM: true })
|
|
423
|
+
.decode(proc.stdout)
|
|
424
|
+
.split('\0')) {
|
|
425
|
+
if (entry.length === 0) continue
|
|
426
|
+
// `-t` prefixes a tag and a space; `?` is untracked.
|
|
427
|
+
const p = entry.slice(2)
|
|
378
428
|
// An untracked nested repository is listed as `dir/`.
|
|
379
429
|
let abs = path.join(workspaceRoot, p.endsWith('/') ? p.slice(0, -1) : p)
|
|
380
430
|
files.add(abs)
|
|
431
|
+
if (entry[0] !== '?') tracked.add(abs)
|
|
381
432
|
for (abs = path.dirname(abs); abs !== workspaceRoot && !files.has(abs); abs = path.dirname(abs))
|
|
382
433
|
files.add(abs)
|
|
383
434
|
}
|
|
384
|
-
return files
|
|
435
|
+
return { listed: files, tracked }
|
|
385
436
|
}
|
package/src/cli/watch-judge.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fs from 'node:fs'
|
|
6
6
|
import path from 'node:path'
|
|
7
7
|
import { xxh3 } from '../util/index.js'
|
|
8
|
-
import { gitIgnored } from './watch-filter.js'
|
|
8
|
+
import { gitIgnored, gitSpeller } from './watch-filter.js'
|
|
9
9
|
import { modifiedBefore } from './watch-fs.js'
|
|
10
10
|
|
|
11
11
|
const ABSENT = -1n
|
|
@@ -63,6 +63,8 @@ export interface JudgeContext {
|
|
|
63
63
|
fenced?(ownDir: string, abs: string): boolean
|
|
64
64
|
/** The files git listed at the arm; absent when git could not answer. */
|
|
65
65
|
existedAtArm?: ReadonlySet<string>
|
|
66
|
+
/** The files git tracked at the arm; absent when git could not answer. */
|
|
67
|
+
trackedAtArm?: ReadonlySet<string>
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export class ChangeJudge {
|
|
@@ -124,14 +126,14 @@ export class ChangeJudge {
|
|
|
124
126
|
// the moment between a judgement and that cycle's keys, and gone by the
|
|
125
127
|
// next judgement, was read and its deletion re-runs nothing. Git lists
|
|
126
128
|
// no ignored path, so one of those is a deletion as before.
|
|
127
|
-
private sameState(abs: string, ignored
|
|
129
|
+
private sameState(abs: string, ignored: boolean, spell: (p: string) => string): boolean {
|
|
128
130
|
const state = settledState(abs)
|
|
129
131
|
const prev = this.lastState.get(abs)
|
|
130
132
|
this.lastState.set(abs, state)
|
|
131
133
|
if (prev !== undefined) return prev === state
|
|
132
134
|
if (state === ABSENT) {
|
|
133
135
|
const listed = this.ctx.existedAtArm
|
|
134
|
-
return !ignored && listed !== undefined && !listed.has(abs) && !this.inNestedRepo(abs)
|
|
136
|
+
return !ignored && listed !== undefined && !listed.has(spell(abs)) && !this.inNestedRepo(abs)
|
|
135
137
|
}
|
|
136
138
|
return modifiedBefore(abs, this.ctx.armedAt)
|
|
137
139
|
}
|
|
@@ -146,13 +148,17 @@ export class ChangeJudge {
|
|
|
146
148
|
return false
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
|
|
151
|
+
/** `spell` (git's spelling, see `gitSpeller`) given: a held server's write counts outside a cycle too. */
|
|
152
|
+
private writtenDuringLastCycle(abs: string, spell?: (p: string) => string): boolean {
|
|
150
153
|
// A server the last cycle left running is still that cycle's: its
|
|
151
154
|
// writes land after the cycle ended, and with a closed window a dev
|
|
152
155
|
// server that rewrites a log in its project restarted itself forever
|
|
153
156
|
// with no word of it, 12 restarts in 8 s (item 948). The initial run's
|
|
154
|
-
// server is one too, from the arm on.
|
|
155
|
-
|
|
157
|
+
// server is one too, from the arm on. Not a tracked file: that is the
|
|
158
|
+
// user's edit, and three saves of one source file were told to
|
|
159
|
+
// .gitignore it as a server's write (WD-7).
|
|
160
|
+
const open =
|
|
161
|
+
spell !== undefined && this.ctx.held() && this.ctx.trackedAtArm?.has(spell(abs)) !== true
|
|
156
162
|
if (this.lastCycle === undefined && !open) return false
|
|
157
163
|
try {
|
|
158
164
|
const m = fs.statSync(abs).mtimeMs
|
|
@@ -165,6 +171,7 @@ export class ChangeJudge {
|
|
|
165
171
|
|
|
166
172
|
judge(): string | undefined {
|
|
167
173
|
const ignored = gitIgnored(this.ctx.workspaceRoot, [...this.pending.keys()])
|
|
174
|
+
const spell = gitSpeller(this.ctx.workspaceRoot)
|
|
168
175
|
let first: string | undefined
|
|
169
176
|
let firstAbs: string | undefined
|
|
170
177
|
// `sameState` before the `first` test, never after it: it is what
|
|
@@ -190,7 +197,7 @@ export class ChangeJudge {
|
|
|
190
197
|
let gone: [label: string, abs: string] | undefined
|
|
191
198
|
for (const [p, l] of this.pending) {
|
|
192
199
|
if (ignored.has(p) && !editForUncached(p)) continue
|
|
193
|
-
if (this.sameState(p, ignored.has(p)) || first !== undefined) continue
|
|
200
|
+
if (this.sameState(p, ignored.has(p), spell) || first !== undefined) continue
|
|
194
201
|
if (!fs.existsSync(p)) {
|
|
195
202
|
gone ??= [l, p]
|
|
196
203
|
continue
|
|
@@ -201,7 +208,7 @@ export class ChangeJudge {
|
|
|
201
208
|
if (first === undefined && gone !== undefined) [first, firstAbs] = gone
|
|
202
209
|
this.pending.clear()
|
|
203
210
|
const byServer = firstAbs !== undefined && this.ctx.held()
|
|
204
|
-
if (firstAbs === undefined || !this.writtenDuringLastCycle(firstAbs,
|
|
211
|
+
if (firstAbs === undefined || !this.writtenDuringLastCycle(firstAbs, spell)) {
|
|
205
212
|
this.streak.n = 0
|
|
206
213
|
return first
|
|
207
214
|
}
|
package/src/cli/watch.ts
CHANGED
|
@@ -103,7 +103,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const cwd = process.cwd()
|
|
106
|
-
const resolved = await resolveRunOptions(parsed, cwd, parsed.tasks)
|
|
106
|
+
const resolved = await resolveRunOptions(parsed, cwd, parsed.tasks, 'watch')
|
|
107
107
|
if ('error' in resolved) {
|
|
108
108
|
process.stderr.write(`vx watch: ${resolved.error}\n`)
|
|
109
109
|
return 1
|
|
@@ -471,8 +471,8 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
471
471
|
|
|
472
472
|
/** 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. */
|
|
473
473
|
const armedAt = fsClockNow(cacheDir)
|
|
474
|
-
/** What existed at the arm, so a file born and gone since is no deletion (watch-judge.ts). */
|
|
475
|
-
const
|
|
474
|
+
/** 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). */
|
|
475
|
+
const atArm = gitFiles(workspaceRoot)
|
|
476
476
|
/** Which settled paths are changes (watch-judge.ts); `pending` holds what fired since. */
|
|
477
477
|
const changes = new ChangeJudge({
|
|
478
478
|
workspaceRoot,
|
|
@@ -480,7 +480,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
480
480
|
held: () => held !== undefined,
|
|
481
481
|
uncached: () => uncached,
|
|
482
482
|
fenced: (ownDir, abs) => fenced(ownDir, abs),
|
|
483
|
-
...(
|
|
483
|
+
...(atArm !== undefined ? { existedAtArm: atArm.listed, trackedAtArm: atArm.tracked } : {}),
|
|
484
484
|
})
|
|
485
485
|
/** Per-project arms by directory, so `rearm` can add and drop them. */
|
|
486
486
|
const perProject = new Map<string, WatchHandle>()
|
package/src/config.ts
CHANGED
|
@@ -292,10 +292,10 @@ export interface SandboxConfig {
|
|
|
292
292
|
|
|
293
293
|
/** The denials `sandbox.ignore` can leave out of the report: patterns per class. */
|
|
294
294
|
export interface SandboxIgnore {
|
|
295
|
-
read?: string[]
|
|
296
|
-
write?: string[]
|
|
297
|
-
systemInfo?: string[]
|
|
298
|
-
network?: string[]
|
|
295
|
+
read?: readonly string[]
|
|
296
|
+
write?: readonly string[]
|
|
297
|
+
systemInfo?: readonly string[]
|
|
298
|
+
network?: readonly string[]
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
export interface SandboxGrants {
|
|
@@ -304,29 +304,29 @@ export interface SandboxGrants {
|
|
|
304
304
|
* `node_modules`, NOT `cache.inputs.files`. A task that reads its own
|
|
305
305
|
* source in the sandbox says so here.
|
|
306
306
|
*/
|
|
307
|
-
read?: string[]
|
|
307
|
+
read?: readonly string[]
|
|
308
308
|
/**
|
|
309
309
|
* Writable paths or globs. The task can write NOTHING without these:
|
|
310
310
|
* declaring `cache.outputs.files` grants no write, and a task whose
|
|
311
311
|
* only declaration is its outputs writes into a tmpfs that evaporates
|
|
312
312
|
* (vx then warns that `cache.outputs` matched no files).
|
|
313
313
|
*/
|
|
314
|
-
write?: string[]
|
|
314
|
+
write?: readonly string[]
|
|
315
315
|
/**
|
|
316
316
|
* Reachable domains, `['*.example.com']` a pattern. The proxy is the
|
|
317
317
|
* run's: it filters every sandboxed task against the union of the lists
|
|
318
318
|
* the run's tasks declare, so `true` adds no domain and reaches only
|
|
319
319
|
* those (none in a run with no list). Omitted adds none either.
|
|
320
320
|
*/
|
|
321
|
-
network?: true | string[]
|
|
321
|
+
network?: true | readonly string[]
|
|
322
322
|
/**
|
|
323
323
|
* Kernel information the task may query, by info type — the name that
|
|
324
324
|
* appears in a denial, e.g. `vfs.disk-space` (every Bun and git process
|
|
325
325
|
* probes free space at startup).
|
|
326
326
|
*/
|
|
327
|
-
systemInfo?: string[]
|
|
327
|
+
systemInfo?: readonly string[]
|
|
328
328
|
/** Unix socket paths the task may connect to. `true` allows any. */
|
|
329
|
-
unixSockets?: true | string[]
|
|
329
|
+
unixSockets?: true | readonly string[]
|
|
330
330
|
/**
|
|
331
331
|
* Bind and reach localhost ports — a test that boots a server for
|
|
332
332
|
* itself. A port LIST additionally makes those ports reachable from
|
|
@@ -335,9 +335,9 @@ export interface SandboxGrants {
|
|
|
335
335
|
* namespace, each listed port is bridged out to the host's loopback;
|
|
336
336
|
* on macOS the host already sees them and the list means `true`.
|
|
337
337
|
*/
|
|
338
|
-
localBinding?: boolean | number[]
|
|
338
|
+
localBinding?: boolean | readonly number[]
|
|
339
339
|
/** macOS: Mach service names the task may look up. */
|
|
340
|
-
machLookup?: string[]
|
|
340
|
+
machLookup?: readonly string[]
|
|
341
341
|
/** Acquire a TTY. Rare outside interactive tools. */
|
|
342
342
|
pty?: boolean
|
|
343
343
|
/** Write to `.git/config`. Most build tools should not. */
|
|
@@ -346,7 +346,7 @@ export interface SandboxGrants {
|
|
|
346
346
|
|
|
347
347
|
export interface SandboxDenials {
|
|
348
348
|
/** Domains blocked even if `allow.network` would admit them. */
|
|
349
|
-
network?: string[]
|
|
349
|
+
network?: readonly string[]
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
export interface ExecConfig {
|
|
@@ -458,7 +458,7 @@ export interface ExecEnv {
|
|
|
458
458
|
* folded into the cache key — for things like CI flags, regional
|
|
459
459
|
* vars, secrets that change between machines without affecting output.
|
|
460
460
|
*/
|
|
461
|
-
passThrough?: string[]
|
|
461
|
+
passThrough?: readonly string[]
|
|
462
462
|
/**
|
|
463
463
|
* Explicit `name: value` pairs. The values are set on the child AND
|
|
464
464
|
* folded into the cache key (they are literal in your config, captured
|
|
@@ -471,7 +471,7 @@ export interface ExecEnv {
|
|
|
471
471
|
* hold `TOKEN`, `SECRET`, `KEY`, `PASSWORD`, `PASSWD` or `CREDENTIAL`
|
|
472
472
|
* are masked without it; this reaches `GH_PAT`, `NPM_AUTH`.
|
|
473
473
|
*/
|
|
474
|
-
secret?: string[]
|
|
474
|
+
secret?: readonly string[]
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
export interface CacheConfig {
|
|
@@ -488,7 +488,7 @@ export interface CacheOutputs {
|
|
|
488
488
|
* Outputs are not filtered through gitignore — typical artifact dirs
|
|
489
489
|
* like `dist/` and `coverage/` are captured normally even when ignored.
|
|
490
490
|
*/
|
|
491
|
-
files: string[]
|
|
491
|
+
files: readonly string[]
|
|
492
492
|
/**
|
|
493
493
|
* Files produced OUTSIDE the project dir, as workspace-root-relative
|
|
494
494
|
* globs (the Turbo `$TURBO_ROOT$` / Nx `{workspaceRoot}` escape
|
|
@@ -500,7 +500,7 @@ export interface CacheOutputs {
|
|
|
500
500
|
* like overlapping `files` (vx cleans declared outputs before a run
|
|
501
501
|
* and before a restore, so the second would delete the first's).
|
|
502
502
|
*/
|
|
503
|
-
workspaceFiles?: string[]
|
|
503
|
+
workspaceFiles?: readonly string[]
|
|
504
504
|
}
|
|
505
505
|
|
|
506
506
|
export interface CacheInputs {
|
|
@@ -514,7 +514,7 @@ export interface CacheInputs {
|
|
|
514
514
|
* files are excluded automatically — a task cannot invalidate itself,
|
|
515
515
|
* and cannot read across project boundaries.
|
|
516
516
|
*/
|
|
517
|
-
files: string[]
|
|
517
|
+
files: readonly string[]
|
|
518
518
|
/**
|
|
519
519
|
* Workspace-root-relative globs (the Turbo `$TURBO_ROOT$` / Nx
|
|
520
520
|
* `{workspaceRoot}` equivalent), for inputs that live outside the
|
|
@@ -528,7 +528,7 @@ export interface CacheInputs {
|
|
|
528
528
|
* Declared outputs (both `outputs` fields) are excluded
|
|
529
529
|
* automatically (a task cannot invalidate itself).
|
|
530
530
|
*/
|
|
531
|
-
workspaceFiles?: string[]
|
|
531
|
+
workspaceFiles?: readonly string[]
|
|
532
532
|
/**
|
|
533
533
|
* Env var names whose runtime values from parent `process.env` are
|
|
534
534
|
* folded into the cache key. **Independent of `exec.env`** — declaring
|
|
@@ -536,7 +536,7 @@ export interface CacheInputs {
|
|
|
536
536
|
* invalidation. To both forward AND track, list the name in both
|
|
537
537
|
* `exec.env.passThrough` and here.
|
|
538
538
|
*/
|
|
539
|
-
env?: string[]
|
|
539
|
+
env?: readonly string[]
|
|
540
540
|
/**
|
|
541
541
|
* Which upstream tasks' cache keys participate in this task's key.
|
|
542
542
|
* Uses the same Turbo/Nx micro-syntax as `dependsOn`, plus
|
|
@@ -565,7 +565,7 @@ export interface CacheInputs {
|
|
|
565
565
|
* lock, the OUTPUT is resolved live every run — correct under
|
|
566
566
|
* `--frozen`. Use for project-specific runtime probes.
|
|
567
567
|
*/
|
|
568
|
-
runtime?: string[]
|
|
568
|
+
runtime?: readonly string[]
|
|
569
569
|
/**
|
|
570
570
|
* Like `runtime`, but commands run at the WORKSPACE ROOT and are
|
|
571
571
|
* deduped GLOBALLY per command across the whole run — a `node -v`
|
|
@@ -573,7 +573,7 @@ export interface CacheInputs {
|
|
|
573
573
|
* analog of `workspaceFiles`: per-task, root-anchored. Use for global
|
|
574
574
|
* tool versions, OS info, etc.
|
|
575
575
|
*/
|
|
576
|
-
workspaceRuntime?: string[]
|
|
576
|
+
workspaceRuntime?: readonly string[]
|
|
577
577
|
}
|
|
578
578
|
|
|
579
579
|
/**
|
package/src/exec/proc-sample.ts
CHANGED
|
@@ -26,7 +26,7 @@ interface Proc {
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Linux's `/proc/<pid>/stat` counts CPU in USER_HZ ticks, fixed at 100 by
|
|
29
|
-
* the ABI on every architecture; `tests/proc-sample.test.ts` pins it by
|
|
29
|
+
* the ABI on every architecture; `tests/proc-sample.unsafe.test.ts` pins it by
|
|
30
30
|
* burning a known span and reading it back.
|
|
31
31
|
*/
|
|
32
32
|
const TICK_MS = 10
|
|
@@ -82,7 +82,7 @@ export function localBindingOn(c: { localBinding?: boolean | readonly number[] }
|
|
|
82
82
|
* rest of the host is mounted read-only), or under a read grant, a write
|
|
83
83
|
* grant or a baseline read, by its canonical path. A path no grant holds
|
|
84
84
|
* under a denial is not there inside the sandbox, which the shell reports
|
|
85
|
-
* as "not found"
|
|
85
|
+
* as "not found": a PATH search is a stat, which the trace does not stop on.
|
|
86
86
|
*/
|
|
87
87
|
export function sandboxReads(
|
|
88
88
|
sandbox: {
|