@vzn/vx 0.0.634 → 0.0.635
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 +51 -52
- package/docs/caching.md +7 -1
- package/docs/cli.md +102 -35
- package/docs/features.md +2 -1
- package/docs/patterns.md +8 -5
- package/docs/schema.md +34 -2
- package/docs/security.md +6 -0
- package/launcher.cjs +0 -0
- package/launcher.sh +50 -0
- package/package.json +17 -8
- package/schemas/last.json +5 -1
- package/schemas/show.json +2 -1
- package/schemas/why.json +4 -1
- package/src/cache/layered-cache.ts +3 -1
- package/src/cli/last.ts +9 -1
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +9 -6
- package/src/cli/select.ts +17 -11
- package/src/cli/show.ts +2 -2
- package/src/config.ts +14 -2
- package/src/graph/scheduler.ts +14 -3
- package/src/graph/task-graph.ts +22 -5
- package/src/index.ts +6 -1
- package/src/orchestrator/execute-task.ts +30 -1
- package/src/orchestrator/framed-output.ts +27 -1
- package/src/orchestrator/history.ts +75 -1
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/path-links.ts +5 -3
- package/src/orchestrator/plugin.ts +3 -1
- package/src/orchestrator/prepare.ts +15 -12
- package/src/orchestrator/run-failures.ts +20 -3
- package/src/orchestrator/run.ts +11 -1
- package/src/orchestrator/sandbox-fix.ts +123 -0
- package/src/orchestrator/task-hash.ts +7 -1
- package/src/workspace/affected.ts +16 -4
- package/src/workspace/config-schema.ts +14 -2
- package/src/workspace/migration.ts +51 -4
- package/src/workspace/project-loader.ts +3 -1
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
<h1 align="center">vx</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>
|
|
9
|
-
|
|
8
|
+
<strong>Task orchestration you own.</strong><br>
|
|
9
|
+
Correct, sandboxed, extensible. No cloud, no account, no paywall.<br>
|
|
10
|
+
<a href="https://vznjs.github.io/vx/"><strong>vznjs.github.io/vx</strong></a>
|
|
10
11
|
</p>
|
|
11
12
|
|
|
12
13
|
<p align="center">
|
|
@@ -16,29 +17,9 @@
|
|
|
16
17
|
<a href="https://vznjs.github.io/vx/"><img src="https://img.shields.io/badge/docs-vznjs.github.io%2Fvx-c6f84e" alt="Documentation"></a>
|
|
17
18
|
</p>
|
|
18
19
|
|
|
19
|
-
<!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
|
|
20
|
-
|
|
21
|
-
| 1,090 packages, 3,270 tasks | vx | Turborepo | Nx | Vite Task |
|
|
22
|
-
| ----------------------------------------------- | -------------- | -------------------------- | ------------------------- | -------------------------- |
|
|
23
|
-
| Cold build: total time | **3 min 40 s** | 4 min 59 s (vx 36% faster) | 3 min 49 s (vx 4% faster) | 4 min 49 s (vx 31% faster) |
|
|
24
|
-
| Cold build: CPU burned | **17 s** | 21 s (vx 22% faster) | 52 s (vx 3× faster) | 12 s (vx 39% slower) |
|
|
25
|
-
| Fully cached run (restored) | **650 ms** | 997 ms (vx 53% faster) | 6.25 s (vx 9.6× faster) | 2.64 s (vx 4.1× faster) |
|
|
26
|
-
| Fully cached run (up-to-date) | **393 ms** | 463 ms (vx 18% faster) | 6.45 s (vx 16× faster) | 2.49 s (vx 6.3× faster) |
|
|
27
|
-
| Secondary: time the runner adds to a cold build | **2 s** | 1 min 21 s | 11 s | 1 min 11 s |
|
|
28
|
-
|
|
29
|
-
vx N% or N× faster: that tool takes N% longer or N times as long as vx.
|
|
30
|
-
|
|
31
|
-
Benchmark workload: a synthetic monorepo of 1,090 packages and 3,270 tasks in 100 dependency layers, every build and test taking 1 s; real repos with uneven task times will differ.
|
|
32
|
-
Run 2026-10-04 on linux x64, 4 cores: vx from source, Turborepo 2.11.7, Nx 23.2.1, Vite Task (vite-plus) 1.0.0.
|
|
33
|
-
|
|
34
|
-
The secondary row is the wall time over the tasks' own ideal schedule (3 min 38 s).
|
|
35
|
-
Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
|
|
36
|
-
|
|
37
|
-
<!-- bench:end -->
|
|
38
|
-
|
|
39
20
|
## What vx is
|
|
40
21
|
|
|
41
|
-
|
|
22
|
+
Task orchestration with a cache you can trust, on a core you can extend.
|
|
42
23
|
Each task is one shell command with declared inputs and outputs; vx runs
|
|
43
24
|
them in dependency order and replays results from a content-addressed cache.
|
|
44
25
|
|
|
@@ -95,22 +76,7 @@ More: [Quickstart](https://vznjs.github.io/vx/quickstart/) ·
|
|
|
95
76
|
[CLI](packages/vx/docs/cli.md) · [every config field](packages/vx/docs/schema.md) ·
|
|
96
77
|
[playground](https://vznjs.github.io/vx/playground/) (the planner, in your browser).
|
|
97
78
|
|
|
98
|
-
##
|
|
99
|
-
|
|
100
|
-
Beside `turbo.json` or `nx.json`, `npx vx init` runs `@vzn/vx-migrate`:
|
|
101
|
-
it writes a `vx.config.ts` per package from `turbo.json` or the Nx graph,
|
|
102
|
-
and you keep those and delete the old config. `vx init --keep` writes only
|
|
103
|
-
a `vx.workspace.ts` that maps the old config instead: a temporary start,
|
|
104
|
-
not a way to run the repo. The numbers above are native vx config.
|
|
105
|
-
[Migration guide](https://vznjs.github.io/vx/guides/migrate/).
|
|
106
|
-
|
|
107
|
-
## Why it is faster
|
|
108
|
-
|
|
109
|
-
- **Nothing boots per task:** no daemon, no Node process; vx spawns your command.
|
|
110
|
-
- **No source-file reads to hash:** on a clean tree, keys come from git's index; each project's `package.json` and the lockfile are still read.
|
|
111
|
-
- **One pass:** one git walk per run; each stat and lookup happens once.
|
|
112
|
-
|
|
113
|
-
## More than speed
|
|
79
|
+
## A foundation you own
|
|
114
80
|
|
|
115
81
|
- **A cache you can trust:** TypeScript configs are evaluated before
|
|
116
82
|
hashing, so imports and computed values are in the key. Outputs are
|
|
@@ -122,6 +88,32 @@ not a way to run the repo. The numbers above are native vx config.
|
|
|
122
88
|
CLI verbs. Core ships none and needs none.
|
|
123
89
|
- **Free:** MIT. No paywall, no cloud, no account.
|
|
124
90
|
|
|
91
|
+
## Plugins
|
|
92
|
+
|
|
93
|
+
What vx can do, not what it is limited to. Declared in `vx.workspace.ts`; without any, vx runs and caches locally.
|
|
94
|
+
|
|
95
|
+
| Package | What it adds |
|
|
96
|
+
| ---------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
97
|
+
| [`@vzn/vx-migrate`](packages/vx-migrate) | Migrate from Turbo or Nx: write native configs, keep its remote cache |
|
|
98
|
+
| [`@vzn/vx-reapi`](packages/vx-reapi) | Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy) |
|
|
99
|
+
| [`@vzn/vx-lockfile`](packages/vx-lockfile) | Key each project on its own lockfile slice: pnpm, bun, npm, yarn |
|
|
100
|
+
| [`@vzn/vx-otel`](packages/vx-otel) | OpenTelemetry traces, metrics and logs, no SDK |
|
|
101
|
+
| [`@vzn/vx-ci`](packages/vx-ci) | GitHub Actions job summary and Checks API |
|
|
102
|
+
| [`@vzn/vx-mcp`](packages/vx-mcp) | `vx mcp`: an MCP server for AI coding agents that also runs tasks |
|
|
103
|
+
| [`@vzn/vx-schedule-history`](packages/vx-schedule-history) | Schedule by the critical path learned from past runs |
|
|
104
|
+
|
|
105
|
+
Write your own: [plugin guide](https://vznjs.github.io/vx/guides/plugins/) ·
|
|
106
|
+
[architecture](packages/vx/docs/architecture.md).
|
|
107
|
+
|
|
108
|
+
## Coming from Turbo or Nx
|
|
109
|
+
|
|
110
|
+
Beside `turbo.json` or `nx.json`, `npx vx init` runs `@vzn/vx-migrate`:
|
|
111
|
+
it writes a `vx.config.ts` per package from `turbo.json` or the Nx graph,
|
|
112
|
+
and you keep those and delete the old config. `vx init --keep` writes only
|
|
113
|
+
a `vx.workspace.ts` that maps the old config instead: a temporary start,
|
|
114
|
+
not a way to run the repo. The benchmark below uses native vx config.
|
|
115
|
+
[Migration guide](https://vznjs.github.io/vx/guides/migrate/).
|
|
116
|
+
|
|
125
117
|
## How it compares
|
|
126
118
|
|
|
127
119
|
| | vx | Turborepo | Nx |
|
|
@@ -138,22 +130,29 @@ Every gap, and when another tool is the better pick:
|
|
|
138
130
|
[comparison](packages/vx/docs/comparison.md) ·
|
|
139
131
|
[vx, Turborepo, Nx, Bazel](https://vznjs.github.io/vx/compare/).
|
|
140
132
|
|
|
141
|
-
##
|
|
133
|
+
## Fast, without trading correctness
|
|
142
134
|
|
|
143
|
-
|
|
135
|
+
- **Nothing boots per task:** no daemon, no Node process; vx spawns your command.
|
|
136
|
+
- **No source-file reads to hash:** on a clean tree, keys come from git's index; each project's `package.json` and the lockfile are still read.
|
|
137
|
+
- **One pass:** one git walk per run; each stat and lookup happens once.
|
|
144
138
|
|
|
145
|
-
|
|
146
|
-
| ---------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
147
|
-
| [`@vzn/vx-migrate`](packages/vx-migrate) | Migrate from Turbo or Nx: write native configs, keep its remote cache |
|
|
148
|
-
| [`@vzn/vx-reapi`](packages/vx-reapi) | Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy) |
|
|
149
|
-
| [`@vzn/vx-lockfile`](packages/vx-lockfile) | Key each project on its own lockfile slice: pnpm, bun, npm, yarn |
|
|
150
|
-
| [`@vzn/vx-otel`](packages/vx-otel) | OpenTelemetry traces, metrics and logs, no SDK |
|
|
151
|
-
| [`@vzn/vx-ci`](packages/vx-ci) | GitHub Actions job summary and Checks API |
|
|
152
|
-
| [`@vzn/vx-mcp`](packages/vx-mcp) | `vx mcp`: an MCP server for AI coding agents that also runs tasks |
|
|
153
|
-
| [`@vzn/vx-schedule-history`](packages/vx-schedule-history) | Schedule by the critical path learned from past runs |
|
|
139
|
+
<!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
|
|
154
140
|
|
|
155
|
-
|
|
156
|
-
|
|
141
|
+
| Time added over the ideal run, 1,601 projects, 9,603 tasks | vx | Turborepo | Nx | Vite Task |
|
|
142
|
+
| ---------------------------------------------------------- | ----------- | ------------------------- | -------------------------- | ------------------------ |
|
|
143
|
+
| Nothing changed | **1.07 s** | 898 ms (vx 19% slower) | 25.45 s (vx 24× faster) | 11.75 s (vx 11× faster) |
|
|
144
|
+
| Nothing changed, outputs restored | **1.27 s** | 844 ms (vx 51% slower) | 23.82 s (vx 19× faster) | 11.83 s (vx 9.3× faster) |
|
|
145
|
+
| Cold build | **7.07 s** | 9.15 s (vx 29% faster) | 3 min 48 s (vx 32× faster) | 8.16 s (vx 15% faster) |
|
|
146
|
+
| Cold build: CPU the runner burns | **32.28 s** | 1 min 3 s (vx 94% faster) | 6 min 6 s (vx 11× faster) | 24.82 s (vx 30% slower) |
|
|
147
|
+
|
|
148
|
+
Time each tool adds over the ideal run; vx N% or N× faster means that tool adds N% more or N times as much as vx.
|
|
149
|
+
|
|
150
|
+
Benchmark workload: a synthetic monorepo of 1,601 projects and 9,603 tasks in 30 dependency levels, five core libraries a quarter of the projects use; build 300 ms, test and typecheck 150 ms, lint 75 ms, publish 30 ms; real repos with uneven task times will differ.
|
|
151
|
+
Run 2026-10-09 on linux x64, 4 cores: vx at commit 3d48a981d, Turborepo 2.11.7, Nx 23.3.0, Vite Task (vite-plus) 1.1.0.
|
|
152
|
+
|
|
153
|
+
Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
|
|
154
|
+
|
|
155
|
+
<!-- bench:end -->
|
|
157
156
|
|
|
158
157
|
## Status
|
|
159
158
|
|
package/docs/caching.md
CHANGED
|
@@ -704,7 +704,13 @@ is not saved either: its write cannot be told from an edit reverted
|
|
|
704
704
|
mid-run, so it pays a re-run each time rather than risk a stale entry;
|
|
705
705
|
declare what it writes as an output, which the status line says when it
|
|
706
706
|
names a file other than `package.json` (TanStack Router's committed
|
|
707
|
-
`routeTree.gen.ts`, rewritten by every build).
|
|
707
|
+
`routeTree.gen.ts`, rewritten by every build). That holds on CI too: a
|
|
708
|
+
task of the same project running alongside can change an input and put
|
|
709
|
+
it back. A task that rewrites its own input to identical bytes (tsdown's
|
|
710
|
+
`package.json`) sets `cache.rewrittenInputs: 'compare'`, and a write
|
|
711
|
+
during its command is then judged by its bytes, as an earlier write is;
|
|
712
|
+
different bytes still withhold the entry. `VX_REWRITTEN_INPUTS=compare`
|
|
713
|
+
makes that the default (owner, 2026-10-12). A file ADDED under an
|
|
708
714
|
input glob since the listing the key filtered (`addedInput`) withholds
|
|
709
715
|
the save the same way, named on the same line: the listing's
|
|
710
716
|
directories a glob reaches are `lstat`ed, one whose ctime moved since
|
package/docs/cli.md
CHANGED
|
@@ -137,9 +137,12 @@ Exit codes:
|
|
|
137
137
|
A task runs in its own session, so a terminal's Ctrl-C reaches vx alone,
|
|
138
138
|
and each task hears it once: from vx, as SIGINT.
|
|
139
139
|
|
|
140
|
-
Installed from npm, `vx` is a
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
Installed from npm, `vx` is a sh launcher that execs the platform
|
|
141
|
+
binary, so every signal reaches vx directly and no Node starts first
|
|
142
|
+
(a `vx --version` through npm: 14 ms, was 56 ms with the Node launcher, 2026-10-12). Where it
|
|
143
|
+
finds no binary beside it (musl, Yarn PnP, a platform with none) it
|
|
144
|
+
hands off to a Node launcher. On Node 22.15 or later that one
|
|
145
|
+
replaces itself with the binary (`process.execve`) too. On older Node it runs the binary and waits for it:
|
|
143
146
|
a signal sent to the launcher alone (`kill`, a process manager) is
|
|
144
147
|
passed to the binary, and a terminal's Ctrl-C already reaches both, so
|
|
145
148
|
the launcher does not send it twice.
|
|
@@ -957,8 +960,10 @@ readable. Its `criticalPath` names the would-run tasks with history on
|
|
|
957
960
|
the chain `wallMs` measures, dependencies first: the tasks to speed up
|
|
958
961
|
to shorten the run.
|
|
959
962
|
|
|
960
|
-
|
|
961
|
-
|
|
963
|
+
### Why a task is affected (`affected.kind`)
|
|
964
|
+
|
|
965
|
+
Under `--affected`, each requested task the diff kept gets a line, and
|
|
966
|
+
its JSON object an `affected` reason:
|
|
962
967
|
|
|
963
968
|
```
|
|
964
969
|
$ vx run build lint --affected=main --dry
|
|
@@ -981,6 +986,8 @@ against: a bare `--affected` guesses it (`origin/HEAD`, a trunk branch,
|
|
|
981
986
|
else `HEAD~1`), so a reason that names no file may come from a commit
|
|
982
987
|
the base reaches, a root `package.json` edit among them.
|
|
983
988
|
|
|
989
|
+
### Task graph (`--graph`)
|
|
990
|
+
|
|
984
991
|
`--graph` prints Graphviz DOT (stdout by default; `--graph=path`
|
|
985
992
|
writes a file):
|
|
986
993
|
|
|
@@ -1283,7 +1290,19 @@ package, are reported.
|
|
|
1283
1290
|
Policy: **fail on violation.** Any task that touches a path it didn't
|
|
1284
1291
|
declare either fails naturally (Linux: `ENOENT` from bwrap's
|
|
1285
1292
|
mount-namespace hide) or is flagged via the macOS violation store
|
|
1286
|
-
and forced to exit non-zero. No cache is written for a failed task.
|
|
1293
|
+
and forced to exit non-zero. No cache is written for a failed task. The
|
|
1294
|
+
task's `SANDBOX VIOLATIONS` section ends with a dimmed `fix:` line,
|
|
1295
|
+
the grants that would have allowed each denial, project-relative:
|
|
1296
|
+
`fix: exec.sandbox.allow: { read: ['tsconfig.json'], write: ['dist/'] }`.
|
|
1297
|
+
A write grants the top directory it landed in, so the next file there
|
|
1298
|
+
passes too, unless `cache.inputs.files` reads that directory: then it
|
|
1299
|
+
grants the file alone, so the task cannot write its own inputs; a read the output depends on is a key input as well, and
|
|
1300
|
+
the line says so, since a grant never adds one. A credential path
|
|
1301
|
+
(`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.kube`, `~/.config/gh`,
|
|
1302
|
+
`~/.config/gcloud`, `~/.netrc`, `~/.npmrc`, `~/.pypirc`,
|
|
1303
|
+
`~/.git-credentials`, `~/.docker/config.json`, a `.env` file) is never put in the
|
|
1304
|
+
line: an agent applies it as given, so it is named apart as one to
|
|
1305
|
+
grant only on purpose.
|
|
1287
1306
|
"Didn't declare" means the grants, which on Linux are wider than they
|
|
1288
1307
|
read: a file-shaped write grant is bound as its whole directory, so
|
|
1289
1308
|
every file beside it is readable without a violation — `schema.md`
|
|
@@ -1961,13 +1980,17 @@ install its dependencies. `--mjs` writes the same objects as `vx.config.mjs` and
|
|
|
1961
1980
|
own `tsconfig` includes every `.ts` under it compiles a `vx.config.ts`
|
|
1962
1981
|
into its dist (TanStack/query's `tsc --build`, 2026-09-11), and an
|
|
1963
1982
|
`.mjs` is outside that include. `@vzn/vx-migrate` takes the same flag.
|
|
1983
|
+
A file it writes that `.gitignore` drops (vueuse ignores
|
|
1984
|
+
`packages/**/*.mjs`) gets a `note:` naming it and the `!vx.config.mjs`
|
|
1985
|
+
line that keeps it, since a commit would leave it behind unnoticed.
|
|
1964
1986
|
|
|
1965
1987
|
Each script becomes a task with its command verbatim. `build` gets
|
|
1966
1988
|
`dependsOn: ['^build']` (a `build` that only delegates, `pnpm run
|
|
1967
1989
|
compile`, is a group, and the edge goes on the script that does the
|
|
1968
1990
|
work) and **no cache block** — under a
|
|
1969
1991
|
`TODO(vx-migrate)` showing the block to add with the package's real
|
|
1970
|
-
inputs and outputs
|
|
1992
|
+
inputs and outputs, and the same block commented as the task's first
|
|
1993
|
+
line: set the real globs and delete the `// `. A task without a cache block always runs; a block
|
|
1971
1994
|
with EMPTY outputs is not "uncached" but a no-output task that hits on
|
|
1972
1995
|
unchanged inputs and skips the build with nothing to restore (a deleted
|
|
1973
1996
|
`dist` stays deleted under a green `up-to-date` run — what `init`
|
|
@@ -2113,8 +2136,8 @@ order may matter, so it is not made a group.
|
|
|
2113
2136
|
|
|
2114
2137
|
The report lists each TODO once per reason: tasks that share one are
|
|
2115
2138
|
named together (the first five, then a count; the files carry each),
|
|
2116
|
-
and when no task caches it says
|
|
2117
|
-
second run a hit. Its `next:` line is a command the user can type: the
|
|
2139
|
+
and when no task caches it says uncommenting a TODO task's cache line
|
|
2140
|
+
makes the second run a hit. Its `next:` line is a command the user can type: the
|
|
2118
2141
|
runner that started vx (`npx`, `pnpm`, `yarn`, `bunx`, read from
|
|
2119
2142
|
`npm_config_user_agent`) with the installed `vx` bin, else the
|
|
2120
2143
|
`@vzn/vx` package (`npx` under Yarn 1, which has no `dlx`); with no
|
|
@@ -2370,6 +2393,42 @@ cache) or grant the write, then re-run.
|
|
|
2370
2393
|
Out of file descriptors. Raise the limit (`ulimit -n 4096`), or run fewer
|
|
2371
2394
|
tasks at once (`--concurrency`), then re-run.
|
|
2372
2395
|
|
|
2396
|
+
#### `VX_E_AFFECTED_BASE`
|
|
2397
|
+
|
|
2398
|
+
`--affected` (or `--affected=<ref>`) has no base to diff against: the
|
|
2399
|
+
ref does not resolve, is a range (`A..B`) or an exclusion (`^A`), names
|
|
2400
|
+
a path inside a commit, or the repository has no history to compare
|
|
2401
|
+
with. Pass a branch or commit you have locally, alone:
|
|
2402
|
+
`--affected=origin/main`. On CI, fetch the history the base needs
|
|
2403
|
+
(`fetch-depth: 0` on actions/checkout, or `git fetch --unshallow`).
|
|
2404
|
+
|
|
2405
|
+
#### `VX_E_GIT`
|
|
2406
|
+
|
|
2407
|
+
A git command vx runs to read the diff failed; the message carries
|
|
2408
|
+
git's exit and its own words. Run that command in the workspace to see
|
|
2409
|
+
why (a broken index, a missing object in a partial clone).
|
|
2410
|
+
|
|
2411
|
+
#### `VX_E_DEPENDS_ON`
|
|
2412
|
+
|
|
2413
|
+
A task's `dependsOn` names a task or project that is not declared, or uses
|
|
2414
|
+
a form vx does not take (a bare wildcard, a negation, a pattern in
|
|
2415
|
+
`pkg#task`). The message names the task and the entry: declare the
|
|
2416
|
+
target, fix its spelling, or drop the entry.
|
|
2417
|
+
|
|
2418
|
+
#### `VX_E_OUTPUT_OVERLAP`
|
|
2419
|
+
|
|
2420
|
+
Two tasks declare overlapping outputs with no `dependsOn` edge between
|
|
2421
|
+
them, so whichever runs second deletes the other's files. Give each task
|
|
2422
|
+
its own output path, or make one depend on the other.
|
|
2423
|
+
|
|
2424
|
+
#### `VX_E_INPUT_IS_OUTPUT`
|
|
2425
|
+
|
|
2426
|
+
A task's `cache.inputs` match outputs: its own (vx cleans outputs before
|
|
2427
|
+
a run, so it would delete its sources) or another task's (its key must
|
|
2428
|
+
not read them; `dependsOn` already cascades). Exclude the outputs from
|
|
2429
|
+
the inputs with a `!` glob, as the message shows, or declare no outputs
|
|
2430
|
+
for a task that rewrites files in place.
|
|
2431
|
+
|
|
2373
2432
|
#### `VX_E_INTERNAL`
|
|
2374
2433
|
|
|
2375
2434
|
A defect in vx; stderr carries the stack. Report it with that stack at
|
|
@@ -2807,8 +2866,14 @@ failed runs. `--format json`
|
|
|
2807
2866
|
emits `{ invocation, tasks }` for scripting; a failed task's row adds
|
|
2808
2867
|
`output` (its output as plain text, the first 8 KiB and last 56 KiB,
|
|
2809
2868
|
secrets masked) and `locations` (the existing files it names,
|
|
2810
|
-
`{ file, line?, col? }`, file absolute
|
|
2811
|
-
|
|
2869
|
+
`{ file, line?, col? }`, file absolute; a file also named with a line
|
|
2870
|
+
drops its bare mention, such as bun test's `app.test.js:` header), so an
|
|
2871
|
+
agent reads why without the terminal. `fixedIn` names the first later run where that task
|
|
2872
|
+
passed, so a replayed failure that is already fixed says so. A
|
|
2873
|
+
sandboxed task's row adds `sandboxFix`, the `exec.sandbox.allow`
|
|
2874
|
+
entries (`read`, `write`, `network`, `systemInfo`) that would have
|
|
2875
|
+
granted its denials, the same grants the terminal prints as `fix:`;
|
|
2876
|
+
`withheld` lists the credential paths it denied and left out. They live in `<cacheDir>/failures/<runId>.json`, the
|
|
2812
2877
|
newest 50 failed runs; past those, or when the write was refused, a
|
|
2813
2878
|
failed row reads `output: ''` and `locations: []`. `--log <task>`
|
|
2814
2879
|
prints one task's output instead of the summary: in the latest run that
|
|
@@ -2958,30 +3023,32 @@ REAPI endpoints, the GitHub token) are in its README; what a TASK sees
|
|
|
2958
3023
|
is `exec.env` in `docs/schema.md`, and the two markers vx sets on every
|
|
2959
3024
|
task are the last row.
|
|
2960
3025
|
|
|
2961
|
-
| Var | Value | Default
|
|
2962
|
-
| --------------------------------- | --------------------- |
|
|
2963
|
-
| `VX_TIMING` | any non-empty | off
|
|
2964
|
-
| `VX_TASK_TIMEOUT` | positive integer (ms) | none
|
|
2965
|
-
| `VX_CACHE_DIR` | a path | none
|
|
2966
|
-
| `VX_CACHE_SCOPE` | a `cacheScope` value | none
|
|
2967
|
-
| `
|
|
2968
|
-
| `
|
|
2969
|
-
| `
|
|
2970
|
-
| `
|
|
2971
|
-
| `
|
|
2972
|
-
| `
|
|
2973
|
-
|
|
|
2974
|
-
|
|
|
2975
|
-
|
|
|
2976
|
-
| `
|
|
2977
|
-
| `
|
|
2978
|
-
| `
|
|
2979
|
-
| `
|
|
2980
|
-
| `
|
|
2981
|
-
| `
|
|
2982
|
-
| `
|
|
2983
|
-
| `
|
|
2984
|
-
| `
|
|
3026
|
+
| Var | Value | Default | Effect |
|
|
3027
|
+
| --------------------------------- | --------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3028
|
+
| `VX_TIMING` | any non-empty | off | Print the stage table to stderr after a run, a `--dry`, or each `vx watch` cycle (`docs/modules/timing.md`). |
|
|
3029
|
+
| `VX_TASK_TIMEOUT` | positive integer (ms) | none | The default timeout for tasks without their own `exec.timeout`, one rung below `--timeout` / `RunOptions.timeout` and one above the workspace `timeout`. Empty, non-integer or non-positive is ignored; a value past the largest timer (~24.8 days) is clamped to it, never refused. |
|
|
3030
|
+
| `VX_CACHE_DIR` | a path | none | The workspace's cache dir, relative to the workspace root, below `--cache-dir` and `vx.workspace`'s `cacheDir`. A named cache dir holds the whole cache and shares nothing; unset, entries live in the user's shared store (`docs/caching.md`). Empty is ignored; whitespace alone is refused, as `cacheDir` is. |
|
|
3031
|
+
| `VX_CACHE_SCOPE` | a `cacheScope` value | none | The run's `cacheScope` (`trusted`, `read-only`, `pr-123`), above `vx.workspace`'s and `github()`'s. Empty is ignored; a value `cacheScope` would refuse is refused before any task. |
|
|
3032
|
+
| `VX_REWRITTEN_INPUTS` | `withhold`, `compare` | `withhold` | What a write to an input during the command means for a task that does not set `cache.rewrittenInputs`: `compare` saves the entry when the file holds the bytes the key folded, anything else withholds it (`docs/schema.md`). |
|
|
3033
|
+
| `VX_KILL_GRACE_MS` | positive integer (ms) | 2000 | The SIGTERM → SIGKILL grace a child that ignores SIGTERM gets: on a timeout, on a signal, and at the end-of-run shutdown of persistent tasks. Out of range falls back to the default. |
|
|
3034
|
+
| `VX_READY_NOTICE_MS` | positive integer (ms) | 10000 | How long a persistent task may take to match `readyWhen` before vx says once what it waits for. Out of range falls back to the default. |
|
|
3035
|
+
| `VX_TEARDOWN_TIMEOUT_MS` | integer (ms) | 3000 | The bound on one plugin's end-of-run flush or teardown, and on its `telemetry()` setup, so a third party's I/O cannot hold the run. Out of range falls back to the default, never clamps: a bound of 24.8 days is no bound. |
|
|
3036
|
+
| `VX_CONFIG_WORKER_TIMEOUT_MS` | integer (ms) | 30000 | How long one `vx.config.ts` evaluation may take, in process or in its worker, before the load fails naming the config (a real evaluation is ~10 ms). A synchronous loop on a first load holds the thread the deadline needs, so it hangs until killed. Out of range falls back to the default. |
|
|
3037
|
+
| `VX_WATCH_POLL` | any non-empty | off | `vx watch` polls every 250 ms from the start instead of probing the OS watcher (§ `vx watch` › How changes are seen). |
|
|
3038
|
+
| `VX_RUN_WORKSPACE`, `VX_RUN_TASK` | set by vx | — | Set on every task's environment (the workspace root; `project#task`). Read back by a `vx run` a task starts: one in the same workspace is refused, since a nested run is invisible to the outer graph and a loop back to its own task forks without bound (`docs/schema.md` § `env`). |
|
|
3039
|
+
| Var | Value | Default | Effect |
|
|
3040
|
+
| --------------------------------- | --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3041
|
+
| `VX_TIMING` | any non-empty | off | Print the stage table to stderr after a run, a `--dry`, or each `vx watch` cycle (`docs/modules/timing.md`). |
|
|
3042
|
+
| `VX_TASK_TIMEOUT` | positive integer (ms) | none | The default timeout for tasks without their own `exec.timeout`, one rung below `--timeout` / `RunOptions.timeout` and one above the workspace `timeout`. Empty, non-integer or non-positive is ignored; a value past the largest timer (~24.8 days) is clamped to it, never refused. |
|
|
3043
|
+
| `VX_CACHE_DIR` | a path | none | The workspace's cache dir, relative to the workspace root, below `--cache-dir` and `vx.workspace`'s `cacheDir`. A named cache dir holds the whole cache and shares nothing; unset, entries live in the user's shared store (`docs/caching.md`). Empty is ignored. |
|
|
3044
|
+
| `VX_CACHE_SCOPE` | a `cacheScope` value | none | The run's `cacheScope` (`trusted`, `read-only`, `pr-123`), above `vx.workspace`'s and `github()`'s. Empty is ignored; a value `cacheScope` would refuse is refused before any task. |
|
|
3045
|
+
| `VX_REWRITTEN_INPUTS` | `withhold`, `compare` | `withhold` | What a write to an input during the command means for a task that does not set `cache.rewrittenInputs`: `compare` saves the entry when the file holds the bytes the key folded, anything else withholds it (`docs/schema.md`). |
|
|
3046
|
+
| `VX_KILL_GRACE_MS` | positive integer (ms) | 2000 | The SIGTERM → SIGKILL grace a child that ignores SIGTERM gets: on a timeout, on a signal, and at the end-of-run shutdown of persistent tasks. Out of range falls back to the default. |
|
|
3047
|
+
| `VX_READY_NOTICE_MS` | positive integer (ms) | 10000 | How long a persistent task may take to match `readyWhen` before vx says once what it waits for. Out of range falls back to the default. |
|
|
3048
|
+
| `VX_TEARDOWN_TIMEOUT_MS` | integer (ms) | 3000 | The bound on one plugin's end-of-run flush or teardown, and on its `telemetry()` setup, so a third party's I/O cannot hold the run. Out of range falls back to the default, never clamps: a bound of 24.8 days is no bound. |
|
|
3049
|
+
| `VX_CONFIG_WORKER_TIMEOUT_MS` | integer (ms) | 30000 | How long one `vx.config.ts` evaluation may take, in process or in its worker, before the load fails naming the config (a real evaluation is ~10 ms). A synchronous loop on a first load holds the thread the deadline needs, so it hangs until killed. Out of range (0, or past 2³¹−1) falls back to the default. |
|
|
3050
|
+
| `VX_WATCH_POLL` | any non-empty | off | `vx watch` polls every 250 ms from the start instead of probing the OS watcher (§ `vx watch` › How changes are seen). |
|
|
3051
|
+
| `VX_RUN_WORKSPACE`, `VX_RUN_TASK` | set by vx | — | Set on every task's environment (the workspace root; `project#task`). Read back by a `vx run` a task starts: one in the same workspace is refused, since a nested run is invisible to the outer graph and a loop back to its own task forks without bound (`docs/schema.md` § `env`). |
|
|
2985
3052
|
|
|
2986
3053
|
Colors are the two conventions in § Output format › Colors (`NO_COLOR`,
|
|
2987
3054
|
`FORCE_COLOR`). A truthy `CI` picks the CI output flow and
|
package/docs/features.md
CHANGED
|
@@ -81,6 +81,7 @@ config key or an environment variable is missing here.
|
|
|
81
81
|
- **Opt-in, explicit inputs** (`cache.inputs.files`) — a task caches only when it names its inputs. [page](https://vznjs.github.io/vx/features/explicit-inputs/) · post: [Explicit over magical](https://vznjs.github.io/vx/blog/explicit-over-magical/)
|
|
82
82
|
- **Inputs** (`cache.inputs.env`, `cache.inputs.runtime`, `cache.inputs.tasks`, `cache.inputs.workspaceFiles`, `cache.inputs.workspaceRuntime`) — env vars, tool versions, upstream tasks and workspace files in the key. [page](https://vznjs.github.io/vx/features/key-inputs/) · post: [What goes into a key, and what comes back](https://vznjs.github.io/vx/blog/inside-a-cache-hit/)
|
|
83
83
|
- **Outputs** (`cache.outputs.files`, `cache.outputs.workspaceFiles`) — what a hit restores. [page](https://vznjs.github.io/vx/features/cache-outputs/) · post: [What goes into a key, and what comes back](https://vznjs.github.io/vx/blog/inside-a-cache-hit/)
|
|
84
|
+
- **Rewritten inputs** (`cache.rewrittenInputs`, `VX_REWRITTEN_INPUTS`) — an input written while the command runs withholds the save, on CI too; `compare` accepts a task's identical rewrite of its own input. no post
|
|
84
85
|
- **Outputs are exactly the snapshot** (`exclusiveOutputs`) — a hit leaves the tree as the run did; two tasks may not own one output without an edge. [page](https://vznjs.github.io/vx/features/strict-outputs/) · post: [The tree is exactly the snapshot](https://vznjs.github.io/vx/blog/strict-output-ownership/)
|
|
85
86
|
- **Keys from git's index** — tracked clean files hash by their blob id, no read. [page](https://vznjs.github.io/vx/features/keys-from-git/) · post: [Your cache key is already in git's index](https://vznjs.github.io/vx/blog/keys-from-git/)
|
|
86
87
|
- **Config keyed as evaluated** — the key sees the resolved config object. [page](https://vznjs.github.io/vx/features/typescript-config/) · post: [Configs are programs](https://vznjs.github.io/vx/blog/resolved-config-hashing/)
|
|
@@ -156,7 +157,7 @@ config key or an environment variable is missing here.
|
|
|
156
157
|
|
|
157
158
|
## Plugins
|
|
158
159
|
|
|
159
|
-
- **A pipeline with seams** (`plugins`, `definePlugin`; stages `config` `discover` `project` `graph` `key` `fingerprint` `schedule` `admit` `executor` `cache` `telemetry` `commands`) — every stage is a plugin seam. [page](https://vznjs.github.io/vx/features/plugins/) · post: [A pipeline with seams](https://vznjs.github.io/vx/blog/pipeline-with-seams/)
|
|
160
|
+
- **A pipeline with seams** (`plugins`, `definePlugin`; stages `config` `discover` `project` `graph` `key` `fingerprint` `schedule` `admit` `executor` `cache` `telemetry` `commands`) — every stage is a plugin seam; an `undefined`, `null` or `false` entry is skipped. [page](https://vznjs.github.io/vx/features/plugins/) · post: [A pipeline with seams](https://vznjs.github.io/vx/blog/pipeline-with-seams/)
|
|
160
161
|
- **Write a plugin** (`vx init --plugin`) — scaffold a runnable plugin for a seam. [page](https://vznjs.github.io/vx/features/plugins/) · post: [Extend vx in an afternoon](https://vznjs.github.io/vx/blog/extend-vx/)
|
|
161
162
|
- **The local floor** — running and caching here are core's, not plugins. [page](https://vznjs.github.io/vx/features/local-floor/) · post: [The local floor](https://vznjs.github.io/vx/blog/the-local-floor/)
|
|
162
163
|
- **Remote cache and execution** (`@vzn/vx-reapi`, `exec.remote`) — Bazel REAPI cache and workers; the scheduler stays here. [page](https://vznjs.github.io/vx/features/remote-execution/) · post: [Remote execution without moving the scheduler](https://vznjs.github.io/vx/blog/remote-execution/)
|
package/docs/patterns.md
CHANGED
|
@@ -183,11 +183,14 @@ Sharing the patterns doesn't mean sharing the overhead. On the
|
|
|
183
183
|
476-package / 1,428-node synthetic workspace
|
|
184
184
|
(`packages/vx-bench/compare.ts 20 25 1`, 2026-09-02):
|
|
185
185
|
|
|
186
|
-
| Runner |
|
|
187
|
-
| ------ |
|
|
188
|
-
| **vx** |
|
|
189
|
-
| Turbo |
|
|
190
|
-
| Nx |
|
|
186
|
+
| Runner | Warm (no restore) | Warm (restore) |
|
|
187
|
+
| ------ | ----------------------- | ----------------------- |
|
|
188
|
+
| **vx** | **297 ms** | **416 ms** |
|
|
189
|
+
| Turbo | 342 ms (vx 15% faster) | 612 ms (vx 47% faster) |
|
|
190
|
+
| Nx | 1.38 s (vx 4.6× faster) | 1.33 s (vx 3.2× faster) |
|
|
191
|
+
|
|
192
|
+
A cached run is all runner overhead. Cold builds are compared by the
|
|
193
|
+
time the runner adds, on [`benchmarks.md`](./benchmarks.md).
|
|
191
194
|
|
|
192
195
|
vx N% faster: that tool takes N% longer than vx; N× faster: N times as long.
|
|
193
196
|
|
package/docs/schema.md
CHANGED
|
@@ -704,9 +704,38 @@ Semantics:
|
|
|
704
704
|
interface CacheConfig {
|
|
705
705
|
inputs: CacheInputs // required when `cache` is provided
|
|
706
706
|
outputs: CacheOutputs // required when `cache` is provided
|
|
707
|
+
rewrittenInputs?: 'withhold' | 'compare' // optional, default 'withhold'
|
|
707
708
|
}
|
|
708
709
|
```
|
|
709
710
|
|
|
711
|
+
`rewrittenInputs` says what a write to an input while the command runs
|
|
712
|
+
means. `'withhold'` (the default, on CI too): the result stands but the
|
|
713
|
+
entry is not saved, since an edit undone mid-run, or another task
|
|
714
|
+
changing the file and putting it back, cannot be told from no edit.
|
|
715
|
+
`'compare'`: a file rewritten to the bytes the key folded counts as
|
|
716
|
+
unchanged and the entry is saved, for a task that rewrites its own input
|
|
717
|
+
identically (tsdown's `package.json`); different bytes still withhold
|
|
718
|
+
it. `VX_REWRITTEN_INPUTS=compare` sets the default for every task that
|
|
719
|
+
does not say (owner, 2026-10-12).
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
import { defineProject } from '@vzn/vx/config'
|
|
723
|
+
|
|
724
|
+
export default defineProject({
|
|
725
|
+
tasks: {
|
|
726
|
+
build: {
|
|
727
|
+
// tsdown rewrites package.json with the same bytes on every build.
|
|
728
|
+
exec: { command: 'tsdown' },
|
|
729
|
+
cache: {
|
|
730
|
+
inputs: { files: ['src/**', 'package.json'] },
|
|
731
|
+
outputs: { files: ['dist/**'] },
|
|
732
|
+
rewrittenInputs: 'compare',
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
})
|
|
737
|
+
```
|
|
738
|
+
|
|
710
739
|
**Caching is opt-in.** Omit `cache` and the task always runs (no read,
|
|
711
740
|
no write). Provide it and caching is active for that task. The
|
|
712
741
|
project loader requires _both_ `inputs` and `outputs` when `cache` is
|
|
@@ -1558,7 +1587,7 @@ interface WorkspaceConfig {
|
|
|
1558
1587
|
/** Which ready task starts first with no timings: 'most-work' (default), 'critical-path', 'direct-dependents', 'ready-order'. */
|
|
1559
1588
|
schedule?: 'most-work' | 'critical-path' | 'direct-dependents' | 'ready-order'
|
|
1560
1589
|
/** Run-level plugins (cache / executor / telemetry capabilities). */
|
|
1561
|
-
plugins?: readonly Plugin[]
|
|
1590
|
+
plugins?: readonly (Plugin | undefined | null | false)[]
|
|
1562
1591
|
}
|
|
1563
1592
|
|
|
1564
1593
|
interface WorkspaceRules {
|
|
@@ -1682,7 +1711,9 @@ false } in vx.workspace.ts to let it wait for its producer.` Such a key
|
|
|
1682
1711
|
Declaration order is precedence: every `executor` is consulted in
|
|
1683
1712
|
order per task (first to accept runs it; what all decline runs
|
|
1684
1713
|
locally); every `cache` layer is chained (lookup walks, save reaches
|
|
1685
|
-
all, the local store last).
|
|
1714
|
+
all, the local store last). An `undefined`, `null` or `false` entry
|
|
1715
|
+
is skipped, as Vite skips one: a factory that declines (`github()` off
|
|
1716
|
+
Actions) or `cond && plugin()` needs no filter. Each entry is a `VxPlugin` contributing
|
|
1686
1717
|
any subset of the fourteen hooks, in pipeline order: `config` (edit
|
|
1687
1718
|
the workspace config before anything derives from it), `discover`
|
|
1688
1719
|
(name directories to make projects beyond the members), `project`
|
|
@@ -2057,6 +2088,7 @@ a user meets most:
|
|
|
2057
2088
|
| `cache.inputs.tasks must be an array of non-empty strings` | Non-string / empty entry, or a bare string. |
|
|
2058
2089
|
| `cache.inputs.tasks: "<name>" names no task in <task>.dependsOn` | An exact entry no `dependsOn` entry of its form names. |
|
|
2059
2090
|
| `cache.outputs is required when cache is set` | Forgot `outputs`. |
|
|
2091
|
+
| `cache.rewrittenInputs must be 'withhold' or 'compare'` | Any other value (`'bytes'`, `true`). |
|
|
2060
2092
|
| `cache.outputs must be an object` | Present but not an object: a string (`outputs: 'dist'`). An array is the row above. |
|
|
2061
2093
|
| `cache.outputs.files must be an array` | Wrong shape. |
|
|
2062
2094
|
| `cache.inputs.files: every entry is a negation, which selects NOTHING` | Only `!` globs — nothing to subtract from. |
|
package/docs/security.md
CHANGED
|
@@ -89,6 +89,12 @@ writes whatever its config says. Never give the write secret to a
|
|
|
89
89
|
out a pull request's code: it runs with main's ref and secrets, and
|
|
90
90
|
`github()` scopes it `pr-<n>` only on the client side.
|
|
91
91
|
|
|
92
|
+
A boundary that holds whoever can write the store is a signing cache
|
|
93
|
+
plugin: main's CI signs each upload, and a reader refuses, as it streams,
|
|
94
|
+
a download no trusted key signed, so the hit is a miss. Core needs no
|
|
95
|
+
change for it; the plugins guide (§ Your own cache › Signed artifacts) has a
|
|
96
|
+
complete one, run by `tests/signed-cache-guide.unsafe.test.ts`.
|
|
97
|
+
|
|
92
98
|
## What vx checks on bytes it did not write
|
|
93
99
|
|
|
94
100
|
Every artifact that arrives from a remote, and every one read back from
|
package/launcher.cjs
CHANGED
|
File without changes
|
package/launcher.sh
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# npm launcher for a vx command: the published package's `bin`. It execs the
|
|
3
|
+
# platform package's prebuilt binary straight from the shell, since Node's
|
|
4
|
+
# own start was ~40 ms of every `vx` call made through node_modules/.bin
|
|
5
|
+
# (45-53 ms against 12-14 ms for `vx --version`, 2026-10-12). Anything it
|
|
6
|
+
# cannot settle (no sibling platform package, musl, Yarn PnP, an unknown
|
|
7
|
+
# platform) goes to launcher.cjs beside it, which resolves through Node and
|
|
8
|
+
# owns the fallbacks and the error messages.
|
|
9
|
+
|
|
10
|
+
# The real file: npm's .bin entry is a symlink to it. Parameter expansion,
|
|
11
|
+
# not dirname/basename, and no `cd`: each fork is a millisecond, and the
|
|
12
|
+
# kernel resolves `<dir>/..` through a symlinked directory (pnpm's
|
|
13
|
+
# node_modules/@vzn/vx) to its real parent on its own.
|
|
14
|
+
self=$0
|
|
15
|
+
hops=0
|
|
16
|
+
while [ -L "$self" ] && [ "$hops" -lt 40 ]; do
|
|
17
|
+
hops=$((hops + 1))
|
|
18
|
+
target=$(readlink "$self")
|
|
19
|
+
case $target in
|
|
20
|
+
/*) self=$target ;;
|
|
21
|
+
*) self=${self%/*}/$target ;;
|
|
22
|
+
esac
|
|
23
|
+
done
|
|
24
|
+
case $self in
|
|
25
|
+
*/*) here=${self%/*} ;;
|
|
26
|
+
*) here=. ;;
|
|
27
|
+
esac
|
|
28
|
+
base=${here##*/}
|
|
29
|
+
scope=${here%/*}
|
|
30
|
+
scope=${scope##*/}
|
|
31
|
+
|
|
32
|
+
key=
|
|
33
|
+
loader=
|
|
34
|
+
case $(uname -sm) in
|
|
35
|
+
'Linux x86_64') key=linux-x64 loader=/lib64/ld-linux-x86-64.so.2 ;;
|
|
36
|
+
'Linux aarch64' | 'Linux arm64') key=linux-arm64 loader=/lib/ld-linux-aarch64.so.1 ;;
|
|
37
|
+
'Darwin x86_64') key=darwin-x64 ;;
|
|
38
|
+
'Darwin arm64') key=darwin-arm64 ;;
|
|
39
|
+
esac
|
|
40
|
+
|
|
41
|
+
# The platform package sits beside this one under npm, pnpm and Bun
|
|
42
|
+
# (node_modules/@vzn/vx-<key>), or under this package's own node_modules
|
|
43
|
+
# when npm could not hoist it. The glibc binary needs its loader (musl
|
|
44
|
+
# has none); launcher.cjs names that cause.
|
|
45
|
+
if [ -n "$key" ] && { [ -z "$loader" ] || [ -e "$loader" ]; }; then
|
|
46
|
+
for bin in "$here/../$base-$key/$base" "$here/node_modules/$scope/$base-$key/$base"; do
|
|
47
|
+
if [ -x "$bin" ]; then exec "$bin" "$@"; fi
|
|
48
|
+
done
|
|
49
|
+
fi
|
|
50
|
+
exec node "$here/launcher.cjs" "$@"
|