@vzn/vx 0.0.0 → 0.0.14

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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -1
  3. package/launcher.mjs +92 -0
  4. package/package.json +44 -3
  5. package/src/bin.ts +25 -0
  6. package/src/cache/cache.ts +1832 -0
  7. package/src/cache/cas-backend.ts +97 -0
  8. package/src/cache/digest.ts +47 -0
  9. package/src/cache/index.ts +28 -0
  10. package/src/cache/inputs.ts +839 -0
  11. package/src/cache/layered-cache.ts +355 -0
  12. package/src/cache/tar.ts +343 -0
  13. package/src/cli/backend.ts +23 -0
  14. package/src/cli/cache.ts +96 -0
  15. package/src/cli/format.ts +15 -0
  16. package/src/cli/help.ts +129 -0
  17. package/src/cli/index.ts +85 -0
  18. package/src/cli/info.ts +80 -0
  19. package/src/cli/lock.ts +135 -0
  20. package/src/cli/mcp-rpc.ts +292 -0
  21. package/src/cli/mcp.ts +92 -0
  22. package/src/cli/migrate-nx.ts +450 -0
  23. package/src/cli/migrate-turbo.ts +364 -0
  24. package/src/cli/migrate.ts +219 -0
  25. package/src/cli/plan-format.ts +148 -0
  26. package/src/cli/run.ts +683 -0
  27. package/src/cli/show.ts +201 -0
  28. package/src/cli/upgrade.ts +103 -0
  29. package/src/cli/watch.ts +287 -0
  30. package/src/config.ts +460 -0
  31. package/src/exec/env.ts +74 -0
  32. package/src/exec/index.ts +13 -0
  33. package/src/exec/runner.ts +470 -0
  34. package/src/exec/sandbox-runtime.ts +578 -0
  35. package/src/graph/dependency-spec.ts +85 -0
  36. package/src/graph/index.ts +27 -0
  37. package/src/graph/scheduler.ts +677 -0
  38. package/src/graph/task-graph.ts +377 -0
  39. package/src/index.ts +222 -0
  40. package/src/orchestrator/colors.ts +63 -0
  41. package/src/orchestrator/devframe-surface.ts +74 -0
  42. package/src/orchestrator/events.ts +277 -0
  43. package/src/orchestrator/execute-task.ts +939 -0
  44. package/src/orchestrator/framed-output.ts +461 -0
  45. package/src/orchestrator/history.ts +148 -0
  46. package/src/orchestrator/index.ts +174 -0
  47. package/src/orchestrator/local-shortcircuit.ts +147 -0
  48. package/src/orchestrator/logger.ts +526 -0
  49. package/src/orchestrator/metrics.ts +1927 -0
  50. package/src/orchestrator/options.ts +197 -0
  51. package/src/orchestrator/plan.ts +129 -0
  52. package/src/orchestrator/plugin-host.ts +185 -0
  53. package/src/orchestrator/plugin.ts +283 -0
  54. package/src/orchestrator/predict.ts +106 -0
  55. package/src/orchestrator/prepare.ts +306 -0
  56. package/src/orchestrator/protocol.ts +156 -0
  57. package/src/orchestrator/remote-prefetch.ts +81 -0
  58. package/src/orchestrator/resources.ts +59 -0
  59. package/src/orchestrator/run-artifacts.ts +111 -0
  60. package/src/orchestrator/run-context.ts +205 -0
  61. package/src/orchestrator/run-report.ts +132 -0
  62. package/src/orchestrator/run-state.ts +145 -0
  63. package/src/orchestrator/run.ts +887 -0
  64. package/src/orchestrator/stable-keys.ts +177 -0
  65. package/src/orchestrator/status-line.ts +287 -0
  66. package/src/orchestrator/summary.ts +315 -0
  67. package/src/orchestrator/tally.ts +69 -0
  68. package/src/orchestrator/task-hash.ts +253 -0
  69. package/src/orchestrator/telemetry-host.ts +80 -0
  70. package/src/orchestrator/telemetry.ts +358 -0
  71. package/src/orchestrator/upstream.ts +121 -0
  72. package/src/orchestrator/verify.ts +164 -0
  73. package/src/orchestrator/wire-render.ts +92 -0
  74. package/src/orchestrator/wire.ts +212 -0
  75. package/src/util/errors.ts +11 -0
  76. package/src/util/hash.ts +25 -0
  77. package/src/util/index.ts +8 -0
  78. package/src/util/paths.ts +16 -0
  79. package/src/util/size.ts +16 -0
  80. package/src/util/ulid.ts +15 -0
  81. package/src/version.ts +10 -0
  82. package/src/workspace/affected.ts +118 -0
  83. package/src/workspace/filter.ts +155 -0
  84. package/src/workspace/fingerprint.ts +41 -0
  85. package/src/workspace/index.ts +27 -0
  86. package/src/workspace/lockfile.ts +118 -0
  87. package/src/workspace/nested-dirs.ts +34 -0
  88. package/src/workspace/package-graph.ts +148 -0
  89. package/src/workspace/project-loader.ts +512 -0
  90. package/src/workspace/workspace.ts +176 -0
package/src/config.ts ADDED
@@ -0,0 +1,460 @@
1
+ // Public schema for vx project and workspace configuration.
2
+
3
+ export interface WorkspaceConfig {
4
+ /** Maximum concurrent tasks. Defaults to the number of CPUs. */
5
+ concurrency?: number
6
+ /** Cache directory, relative to the workspace root. Defaults to `.vx/cache`. */
7
+ cacheDir?: string
8
+ /**
9
+ * Default per-task timeout (ms) — the lowest-precedence fallback for a
10
+ * task that declares no `exec.timeout`. Precedence, highest first:
11
+ * per-task `exec.timeout` → `VX_TASK_TIMEOUT` env → this workspace
12
+ * default. Purely a runaway-process guard; never folded into a cache
13
+ * key (a timed-out task fails and is never cached). Omitted → no default.
14
+ */
15
+ timeout?: number
16
+ /**
17
+ * Plugins registered for this workspace. Each plugin is an
18
+ * in-process subscriber on the run event bus, installed once per
19
+ * `vx run`. See `docs/design/extension-protocol-2026-06.md` §5.
20
+ */
21
+ plugins?: readonly Plugin[]
22
+ /**
23
+ * Opt in to history-aware predictive scheduling. When `true` and
24
+ * `cache.db` has prior runs, the scheduler picks the next ready
25
+ * task by expected remaining critical-path duration (HistoryTable
26
+ * p50) instead of the static reverse-deps count.
27
+ */
28
+ predictive?: boolean
29
+ }
30
+
31
+ /**
32
+ * Structural plugin shape — any subset of the run-level capabilities
33
+ * (`backend`/`cache`/`telemetry`/`eventSink`) plus optional
34
+ * `setup`/`teardown`. The fully-typed `VxPlugin` lives in
35
+ * `src/orchestrator/plugin.ts` and is what users import; this is a
36
+ * re-declaration with opaque function types so `config.ts` stays a leaf
37
+ * module (no orchestrator import). The orchestrator casts to `VxPlugin`
38
+ * before consulting capabilities.
39
+ */
40
+ export interface Plugin {
41
+ readonly name: string
42
+ backend?(ctx: unknown): unknown
43
+ cache?(ctx: unknown): unknown
44
+ telemetry?(ctx: unknown): unknown
45
+ eventSink?(ctx: unknown): unknown
46
+ setup?(ctx: unknown): void | Promise<void>
47
+ teardown?(): void | Promise<void>
48
+ }
49
+
50
+ export interface ProjectConfig {
51
+ /** Tasks declared by this project, keyed by task name. */
52
+ tasks?: Record<string, TaskConfig>
53
+ }
54
+
55
+ export interface TaskConfig {
56
+ /**
57
+ * Optional one-line description of what this task does. Surfaced in
58
+ * the interactive task picker and the `--dry` text view so users
59
+ * can grep for what each task does without opening every config.
60
+ * Pure metadata — no effect on caching, scheduling, or execution.
61
+ */
62
+ description?: string
63
+ /**
64
+ * Shell command + optional env. One command per task. Omit `exec` to
65
+ * declare a **group task** — a no-op that exists only to chain
66
+ * `dependsOn`. Running a group is equivalent to running its
67
+ * dependencies; nothing else happens (no spawn, no I/O, no cache
68
+ * read/write). Useful for "install" / "ci" umbrella tasks that fan
69
+ * out to per-package work.
70
+ */
71
+ exec?: ExecConfig
72
+ /**
73
+ * Tasks that must complete successfully before this task runs.
74
+ * Turbo/Nx-style micro-syntax:
75
+ * - `'name'` — same-project task named `name`. When authored
76
+ * through `defineProject`, a bare entry is
77
+ * **type-checked against this config's task keys**
78
+ * (a typo is a compile error).
79
+ * - `'^name'` — task `name` in every transitive workspace dep
80
+ * (e.g. `'^build'`, `'^all'`); not key-checked.
81
+ * - `'pkg#name'` — specific package's `name` task; not key-checked.
82
+ */
83
+ dependsOn?: readonly string[]
84
+ /**
85
+ * Caching configuration. **Caching is opt-in.** If this field is omitted,
86
+ * the task always runs and nothing is read from or written to the cache.
87
+ * Provide a `cache` block (with at least `outputs`) to enable caching.
88
+ * Requires `exec` — a group task can't cache anything because nothing
89
+ * runs.
90
+ */
91
+ cache?: CacheConfig
92
+ /**
93
+ * Sandbox configuration. **Sandbox is opt-in per task.**
94
+ * - omitted entirely → task runs unsandboxed.
95
+ * - `sandbox: {}` → opts in with the minimum baseline (only
96
+ * resolved inputs readable, only static-prefix of outputs
97
+ * writable, network blocked).
98
+ * - `sandbox: { ... }` → opts in with the explicit options below.
99
+ *
100
+ * No inheritance, no workspace defaults, no built-in escapes. The
101
+ * sandbox sees STRICTLY the union of resolved `cache.inputs.files`
102
+ * + this block's `allowRead` (for reads) and the static prefix of
103
+ * `cache.outputs.files` + this block's `allowWrite` (for writes).
104
+ * If a task needs `node_modules` or `/tmp`, declare them here
105
+ * explicitly.
106
+ *
107
+ * **Policy: fail on violation.** If the sandbox detects an
108
+ * undeclared read/write (macOS log monitor) or the child returns
109
+ * non-zero because it couldn't reach a path it expected (Linux
110
+ * structural deny), the task fails. No cache is written for a
111
+ * failed task.
112
+ *
113
+ * Activation requires `exec` (group tasks have no command to wrap)
114
+ * and is silently skipped for persistent tasks (dev servers need
115
+ * unrestricted network + an indefinite process).
116
+ */
117
+ sandbox?: SandboxConfig
118
+ }
119
+
120
+ /**
121
+ * Per-task sandbox config. Mirrors the user-facing surface of
122
+ * `@anthropic-ai/sandbox-runtime`'s `SandboxRuntimeConfig`, minus
123
+ * deployment-only fields (binary paths, proxy ports, ripgrep config).
124
+ *
125
+ * Path-list fields (`allowRead`, `allowWrite`, `network.allowUnixSockets`)
126
+ * accept:
127
+ * - relative paths → resolved against the project dir
128
+ * - absolute paths → used as-is (`/etc/passwd`, `/tmp`)
129
+ * - tilde paths → expanded against the user's home (`~/.npmrc`)
130
+ *
131
+ * No globs in path lists — bwrap on Linux only accepts path prefixes.
132
+ */
133
+ export interface SandboxConfig {
134
+ // === Filesystem ===
135
+ /**
136
+ * Additional read-allowed paths beyond resolved `cache.inputs.files`.
137
+ * These are unioned with the declared inputs to form the complete
138
+ * allowRead set passed to the sandbox.
139
+ */
140
+ allowRead?: string[]
141
+ /**
142
+ * Additional write-allowed paths beyond the static prefix of
143
+ * `cache.outputs.files`. Tasks with no declared outputs and no extra
144
+ * `allowWrite` can write to nowhere — typical for `lint`/`typecheck`.
145
+ */
146
+ allowWrite?: string[]
147
+ /**
148
+ * Permit writes to `.git/config` files. Default `false` — most build
149
+ * tools should not be reconfiguring git.
150
+ */
151
+ allowGitConfig?: boolean
152
+
153
+ // === Network ===
154
+ /**
155
+ * Network policy.
156
+ * - `false` (default) — block all outbound traffic.
157
+ * - `true` — allow all outbound traffic.
158
+ * - object — fine-grained control (domains / sockets / etc.).
159
+ */
160
+ network?: boolean | SandboxNetworkConfig
161
+
162
+ // === Process behavior ===
163
+ /**
164
+ * Allow the task to acquire a pseudo-terminal. Default `false`.
165
+ * Needed by tasks that interact with a TTY (rare in CI).
166
+ */
167
+ allowPty?: boolean
168
+ /**
169
+ * Linux only. Allow nested sandboxes (a sandboxed task spawning
170
+ * another sandboxed task). Default `false`.
171
+ */
172
+ enableWeakerNestedSandbox?: boolean
173
+ /**
174
+ * macOS only. Skip the network namespace; routes traffic via the
175
+ * host proxy instead of unsharing networking. Lower overhead but
176
+ * weaker isolation. Default `false`.
177
+ */
178
+ enableWeakerNetworkIsolation?: boolean
179
+
180
+ // === Violation policy ===
181
+ /**
182
+ * Map of command-substring → list of paths to ignore violations on
183
+ * for that command. Lets you silence known-noisy probes (e.g. a
184
+ * compiler that statx's many candidate header paths). Per SRT's
185
+ * own ignoreViolations field.
186
+ */
187
+ ignoreViolations?: Record<string, string[]>
188
+ }
189
+
190
+ export interface SandboxNetworkConfig {
191
+ /**
192
+ * Domain patterns the task may reach. Wildcards: `*.example.com`,
193
+ * `*` (allow all).
194
+ */
195
+ allowedDomains?: string[]
196
+ /** Domains explicitly blocked, evaluated before allowedDomains. */
197
+ deniedDomains?: string[]
198
+ /** Unix socket paths the task may connect to. */
199
+ allowUnixSockets?: string[]
200
+ /** Allow any Unix socket. Use with care. */
201
+ allowAllUnixSockets?: boolean
202
+ /**
203
+ * Allow binding to local ports (e.g. a test that boots a server on
204
+ * localhost for itself to query).
205
+ */
206
+ allowLocalBinding?: boolean
207
+ /** macOS only. Mach service names the task may look up. */
208
+ allowMachLookup?: string[]
209
+ }
210
+
211
+ export interface ExecConfig {
212
+ /** Shell command to run, from the project's directory. */
213
+ command: string
214
+ /** Environment exposed to the child process. */
215
+ env?: ExecEnv
216
+ /**
217
+ * Upper bound (ms) before vx SIGTERMs the child. A normal task that
218
+ * runs longer is killed and reported `failed` (timed out) — never
219
+ * cached. Omitted → no limit.
220
+ *
221
+ * For a `persistent` task this bounds the READINESS wait instead: if
222
+ * `readyWhen` hasn't matched within the window the child is SIGTERMed
223
+ * and the task fails — without it a server that prints an unexpected
224
+ * banner would hang the run forever. A persistent task that's ready
225
+ * on spawn (no `readyWhen`) becomes ready before the timer can fire,
226
+ * so the timeout is a no-op for it.
227
+ */
228
+ timeout?: number
229
+ /**
230
+ * Max ADDITIONAL attempts after a failed attempt. `retries: 2` means
231
+ * up to 3 executions total. 0 / omitted → no retries (today's
232
+ * behavior: fail on the first non-zero exit). A retry fires after ANY
233
+ * failure, timeout kills included; a Ctrl-C teardown (`aborted`) is
234
+ * never retried. Declared outputs are re-cleaned before each retry so
235
+ * a failed attempt's partial outputs can't leak into the next. The
236
+ * final outcome (and the cached artifact) is the last attempt's.
237
+ * Not allowed with `persistent` — a persistent task has no exit to
238
+ * retry. Explicit config wins over the run-level `--retry <n>`
239
+ * default, including `retries: 0`.
240
+ */
241
+ retries?: number
242
+ /**
243
+ * Resource RESERVATIONS for admission control — NOT enforcement: vx
244
+ * does not cgroup-limit, nice, or kill the task; it only decides what
245
+ * to co-schedule so concurrent reservations never exceed the CPU /
246
+ * memory budget. Each axis defaults to 0 = reserve nothing: the task
247
+ * runs subject only to the concurrency-count limit. A pure scheduling
248
+ * hint — the whole object is stripped from the cache key, so tuning a
249
+ * reservation never invalidates a cached result.
250
+ */
251
+ resources?: ResourcesConfig
252
+ /**
253
+ * Long-running / continuous task (dev server, watcher, daemon).
254
+ * When present, the task is spawned but the runner does NOT wait
255
+ * for it to exit. Instead it considers the task "ready" — either
256
+ * immediately on spawn (no `readyWhen`) or once the readiness
257
+ * pattern matches a line of stdout/stderr. Downstream tasks unblock
258
+ * on ready, not on exit. The orchestrator SIGTERMs every persistent
259
+ * subprocess once the rest of the graph has finished.
260
+ *
261
+ * `cache` is silently ignored for persistent tasks — there's no
262
+ * exit code to cache and the work continues after vx's run notion
263
+ * of "done".
264
+ */
265
+ persistent?: PersistentConfig
266
+ }
267
+
268
+ /**
269
+ * Per-task resource reservation (see `ExecConfig.resources`). Grouped so
270
+ * a future axis slots in without new top-level `exec` fields; the loader
271
+ * rejects unknown keys.
272
+ */
273
+ export interface ResourcesConfig {
274
+ /**
275
+ * CPU units (fractional ok, e.g. `0.5`), or a `"<n>%"` string of the
276
+ * CPU budget (the run's `concurrency`). `cpus: "50%"` on a budget of 8
277
+ * reserves 4 units.
278
+ */
279
+ cpus?: number | string
280
+ /**
281
+ * Bytes, a size string (`"2GB"`, `"512MB"` — K/M/G/T, powers of 1024),
282
+ * or a `"<n>%"` string of the memory budget (`os.totalmem()` unless
283
+ * overridden with `--memory` — pass `--memory` in cgroup-limited
284
+ * containers, where `os.totalmem()` reports the HOST's RAM).
285
+ */
286
+ memory?: number | string
287
+ }
288
+
289
+ export interface PersistentConfig {
290
+ /**
291
+ * Regex pattern (string) matched against stdout/stderr line-by-line.
292
+ * The task is marked "ready" the first time a line matches. Omitted
293
+ * → ready immediately on spawn (fire-and-forget; downstream still
294
+ * runs but doesn't gate on a signal).
295
+ *
296
+ * Example: Vite prints `Local: http://localhost:5173/` when its
297
+ * dev server is up — `readyWhen: 'Local:'` waits for that line.
298
+ */
299
+ readyWhen?: string
300
+ }
301
+
302
+ export interface ExecEnv {
303
+ /**
304
+ * Names of env vars whose values are taken from the host (parent
305
+ * `process.env`) and forwarded to the child. Their values are NOT
306
+ * folded into the cache key — for things like CI flags, regional
307
+ * vars, secrets that change between machines without affecting output.
308
+ */
309
+ passThrough?: string[]
310
+ /**
311
+ * Explicit `name: value` pairs. The values are set on the child AND
312
+ * folded into the cache key (they are literal in your config, captured
313
+ * via the task config hash).
314
+ */
315
+ define?: Record<string, string>
316
+ }
317
+
318
+ export interface CacheConfig {
319
+ /** What participates in the cache key. */
320
+ inputs: CacheInputs
321
+ /** What this task produces. Captured for restore on a cache hit. */
322
+ outputs: CacheOutputs
323
+ }
324
+
325
+ export interface CacheOutputs {
326
+ /**
327
+ * Files produced by this task, as project-relative globs. Pass `[]`
328
+ * when the task has no files to capture (e.g. `lint`, `typecheck`).
329
+ * Outputs are not filtered through gitignore — typical artifact dirs
330
+ * like `dist/` and `coverage/` are captured normally even when ignored.
331
+ */
332
+ files: string[]
333
+ /**
334
+ * Files produced OUTSIDE the project dir, as workspace-root-relative
335
+ * globs (the Turbo `$TURBO_ROOT$` / Nx `{workspaceRoot}` escape
336
+ * hatch). NO project-boundary rule applies: these globs may capture
337
+ * files anywhere in the workspace, including inside other projects'
338
+ * directories. Deliberate escape hatch — prefer project-relative
339
+ * `files` whenever the task can write inside its own dir. Two tasks
340
+ * declaring overlapping workspace outputs is user responsibility;
341
+ * vx does not police it.
342
+ */
343
+ workspaceFiles?: string[]
344
+ }
345
+
346
+ export interface CacheInputs {
347
+ /**
348
+ * Project-relative globs. Each entry is a positive glob, or a negation
349
+ * prefixed with `!`. Required: pass a recursive glob to mean "all
350
+ * project files" (gitignore-aware), or narrow as needed. An empty
351
+ * array means no file inputs at all.
352
+ *
353
+ * Declared outputs and any nested-project files are excluded
354
+ * automatically — a task cannot invalidate itself, and cannot read
355
+ * across project boundaries.
356
+ */
357
+ files: string[]
358
+ /**
359
+ * Workspace-root-relative globs (the Turbo `$TURBO_ROOT$` / Nx
360
+ * `{workspaceRoot}` equivalent), for inputs that live outside the
361
+ * project dir — a root tsconfig, shared codegen output, etc. Same
362
+ * syntax as `files` (`!` negation supported), resolved against the
363
+ * workspace-wide git file set (gitignored files are invisible).
364
+ *
365
+ * NO project-boundary rule applies: these globs may match files
366
+ * inside other projects' directories. Deliberate escape hatch —
367
+ * prefer project-relative `files` declarations where possible.
368
+ * Declared `outputs.workspaceFiles` are excluded automatically (a
369
+ * task cannot invalidate itself).
370
+ */
371
+ workspaceFiles?: string[]
372
+ /**
373
+ * Env var names whose runtime values from parent `process.env` are
374
+ * folded into the cache key. **Independent of `exec.env`** — declaring
375
+ * a name here does not forward it to the child; it only affects cache
376
+ * invalidation. To both forward AND track, list the name in both
377
+ * `exec.env.passThrough` and here.
378
+ */
379
+ env?: string[]
380
+ /**
381
+ * Which upstream tasks' cache keys participate in this task's key.
382
+ * Uses the same Turbo/Nx micro-syntax as `dependsOn`, plus
383
+ * wildcards and negation for filtering:
384
+ *
385
+ * - `'*'` include every same-project upstream
386
+ * - `'^*'` include every dep-workspace upstream
387
+ * - `'name'` include same-project task `name`
388
+ * - `'^name'` include the `name` task in dep workspaces
389
+ * - `'pkg#name'` include the specific package's `name` task
390
+ * - `'!name'`, `'!^name'`, `'!pkg#name'` — exclude
391
+ *
392
+ * Patterns are applied in order, last write wins, so
393
+ * `['*', '^*', '!^noisy']` reads as "all upstream except deps' noisy".
394
+ *
395
+ * Defaults:
396
+ * - omitted entirely → all upstream contribute (same as `['*', '^*']`).
397
+ * - `[]` → fully decoupled; no upstream contributes.
398
+ */
399
+ tasks?: readonly string[]
400
+ /**
401
+ * Shell commands run in the PROJECT dir at hash time; their combined,
402
+ * trimmed (stdout + stderr) output is folded into the cache key.
403
+ * Deduped per (projectDir, command) within a run. A non-zero exit
404
+ * fails the run. Modeled on `env`: the COMMANDS are frozen in the
405
+ * lock, the OUTPUT is resolved live every run — correct under
406
+ * `--frozen`. Use for project-specific runtime probes.
407
+ */
408
+ runtime?: string[]
409
+ /**
410
+ * Like `runtime`, but commands run at the WORKSPACE ROOT and are
411
+ * deduped GLOBALLY per command across the whole run — a `node -v`
412
+ * declared in 500 projects spawns exactly once. The runtime-input
413
+ * analog of `workspaceFiles`: per-task, root-anchored. Use for global
414
+ * tool versions, OS info, etc.
415
+ */
416
+ workspaceRuntime?: string[]
417
+ }
418
+
419
+ /**
420
+ * A valid `dependsOn` entry, given the set of sibling task names `K`:
421
+ * - `K` — a same-project task; type-checked against the
422
+ * keys of THIS config's `tasks` (typo → error).
423
+ * - `` `^${string}` `` — a task in workspace deps (e.g. `'^build'`,
424
+ * `'^all'`); the dep's task names aren't known
425
+ * here, so any `^`-prefixed string is allowed.
426
+ * - `` `${string}#${string}` `` — a specific package's task
427
+ * (`'pkg#build'`); cross-project, not key-checked.
428
+ * - `` `${string}*${string}` `` — a task-name pattern (`'build.*'`);
429
+ * expands at graph build, so it can't be
430
+ * key-checked (a bare `'*'` still fails at
431
+ * runtime — bare wildcards are filter-only).
432
+ */
433
+ type DependsOnEntry<K extends string> =
434
+ | K
435
+ | `^${string}`
436
+ | `${string}#${string}`
437
+ | `${string}*${string}`
438
+
439
+ /**
440
+ * Identity function — exists only so TypeScript narrows literal types
441
+ * and, crucially, **validates `dependsOn` against this project's own
442
+ * task names**. A bare entry that isn't a declared task key is a compile
443
+ * error; `^name` / `pkg#name` forms reference other projects and stay
444
+ * free strings. Runtime behavior is unchanged (it returns its input).
445
+ */
446
+ export function defineProject<const T extends ProjectConfig>(
447
+ config: T & {
448
+ tasks?: {
449
+ [K in keyof NonNullable<T['tasks']>]?: {
450
+ dependsOn?: readonly DependsOnEntry<Extract<keyof NonNullable<T['tasks']>, string>>[]
451
+ }
452
+ }
453
+ },
454
+ ): T {
455
+ return config as T
456
+ }
457
+
458
+ export function defineWorkspace<T extends WorkspaceConfig>(config: T): T {
459
+ return config
460
+ }
@@ -0,0 +1,74 @@
1
+ import path from 'node:path'
2
+
3
+ // Build the environment exposed to a task.
4
+ //
5
+ // Layers, lowest to highest priority:
6
+ // 1. Essentials (hard-coded allowlist for shell tooling).
7
+ // 2. passThrough: parent process.env values for the named vars.
8
+ // 3. define: explicit name=value pairs from the task config.
9
+ // 4. binPaths prepended to PATH (after all the above resolve PATH).
10
+
11
+ const ESSENTIAL_ENV: readonly string[] = [
12
+ 'PATH',
13
+ 'HOME',
14
+ 'SHELL',
15
+ 'USER',
16
+ 'LOGNAME',
17
+ 'TMPDIR',
18
+ 'TEMP',
19
+ 'TMP',
20
+ 'LANG',
21
+ 'LC_ALL',
22
+ 'LC_CTYPE',
23
+ 'TERM',
24
+ 'COLORTERM',
25
+ 'FORCE_COLOR',
26
+ 'NO_COLOR',
27
+ 'CI',
28
+ 'NODE_OPTIONS',
29
+ 'SYSTEMROOT',
30
+ 'APPDATA',
31
+ 'LOCALAPPDATA',
32
+ 'PROGRAMDATA',
33
+ 'PROGRAMFILES',
34
+ 'PROGRAMFILES(X86)',
35
+ 'COMSPEC',
36
+ 'PATHEXT',
37
+ ]
38
+
39
+ export interface BuildEnvOptions {
40
+ passThrough: readonly string[]
41
+ define: Readonly<Record<string, string>>
42
+ source: NodeJS.ProcessEnv
43
+ /**
44
+ * Directories prepended to the child's PATH (highest priority first).
45
+ * Used to expose the project's own `node_modules/.bin` so user commands
46
+ * (`oxlint`, `tsc`, `vitest`, …) resolve without a PM wrapper. Matches
47
+ * vite-task's behavior — explicit per-project bin, no tree walk, so
48
+ * sibling projects' bins stay invisible per the project-isolation rule.
49
+ */
50
+ binPaths?: readonly string[]
51
+ }
52
+
53
+ export function buildIsolatedEnv(opts: BuildEnvOptions): NodeJS.ProcessEnv {
54
+ const out: NodeJS.ProcessEnv = {}
55
+
56
+ for (const name of ESSENTIAL_ENV) {
57
+ const value = opts.source[name]
58
+ if (value !== undefined) out[name] = value
59
+ }
60
+ for (const name of opts.passThrough) {
61
+ const value = opts.source[name]
62
+ if (value !== undefined) out[name] = value
63
+ }
64
+ for (const [name, value] of Object.entries(opts.define)) {
65
+ out[name] = value
66
+ }
67
+
68
+ if (opts.binPaths && opts.binPaths.length > 0) {
69
+ const prefix = opts.binPaths.join(path.delimiter)
70
+ out['PATH'] = out['PATH'] ? `${prefix}${path.delimiter}${out['PATH']}` : prefix
71
+ }
72
+
73
+ return out
74
+ }
@@ -0,0 +1,13 @@
1
+ // Module contract. Cross-module imports must come through here; see
2
+ // docs/design/module-isolation-2026-06.md and tests/module-boundaries.test.ts.
3
+
4
+ export { buildIsolatedEnv } from './env.js'
5
+ export { runCommand, runPersistent, signalExitCode } from './runner.js'
6
+ export {
7
+ initSandbox,
8
+ probeSandbox,
9
+ resetSandbox,
10
+ resolveSandboxConfig,
11
+ runSandboxed,
12
+ type SandboxViolation,
13
+ } from './sandbox-runtime.js'