This guide is for engineers and architects who need to understand how zx is built, how to configure and operate it reliably, how to secure scripts that use it, and how to extend or embed it. For end-user task documentation (installing, writing scripts, the helper API), see the companion User Guide.
1. Executive Summary
zx is a Node.js library and CLI for writing shell scripts in JavaScript/TypeScript. It wraps child_process in a tagged-template API ($`cmd`) that automatically escapes interpolated arguments, captures output, and provides ergonomic helpers for piping, retries, parallelism, HTTP, and file operations.
| Attribute | Value |
|---|---|
| Project | zx |
| Version | 8.9.0 |
| License | Apache-2.0 |
| Author | Anton Medvedev |
| Publisher | Google (not an officially supported Google product) |
| Language | TypeScript 5 (ESM-first, hybrid CJS/ESM output) |
| Runtimes | Node.js >= 12.17.0, Bun >= 1.0.0, Deno 1.x/2.x, GraalVM Node.js |
| Platforms | Linux, macOS, Windows |
| Binary | zx → build/cli.js |
| Homepage | https://google.github.io/zx/ |
| Repository | https://github.com/google/zx |
| Execution engine | zurk |
Design philosophy. zx does not replace bash — it augments it with JavaScript's control flow, data handling, exception handling, and parallelism, while delegating actual command execution to a real shell (bash by default). The library is deliberately small and bundled, with a strong emphasis on supply-chain auditability and broad runtime compatibility.
Note
zxis distributed in multiple channels (latest,lite,dev,legacy) and through many registries (npm, JSR@webpod/zx, GitHub Container Registry, Homebrew). Thelitechannel is the core engine without the CLI or extra helpers.
2. Architecture
2.1 Component overview
┌──────────────────────────────────────┐
zx CLI (bin) │ src/cli.ts │
zx script.mjs ──────► │ parseArgv → main() → readScript() │
│ → runScript() (import()) │
└───────────────┬──────────────────────┘
│ injects globals, presets
▼
import {$} from 'zx' ┌──────────────────────────────────────┐
import 'zx/globals' ─► │ src/index.ts (public barrel) │
│ ├── core.ts ($, ProcessPromise, │
│ │ ProcessOutput, cd, │
│ │ kill, Options) │
│ ├── goods.ts (echo, fetch, question, │
│ │ retry, spinner, glob…) │
│ ├── error.ts (Fail, exit/errno maps) │
│ ├── log.ts (LogEntry, formatters) │
│ ├── md.ts (markdown → script) │
│ └── vendor* (chalk, which, ps, fs, │
│ yaml, globby, minimist)│
└───────────────┬──────────────────────┘
│ exec()
▼
┌──────────────────────────────────────┐
│ zurk/spawn (execution engine) │
│ → child_process.spawn / spawnSync │
└──────────────────────────────────────┘
2.2 Source module map (src/)
| Module | Responsibility |
|---|---|
core.ts |
The heart: the $ factory, Options, defaults, Snapshot, ProcessPromise, ProcessOutput, cd(), kill(), within(), resolveDefaults(), shell selectors (useBash/usePwsh/usePowerShell), syncProcessCwd() |
goods.ts |
High-level helpers: echo, fetch, question, stdin, sleep, retry, expBackoff, spinner, tempdir/tempfile, parseArgv/updateArgv/argv |
cli.ts |
CLI entry point and script preprocessor: argv parsing, main(), readScript(), runScript(), printUsage(), injectGlobalRequire(), isMain() |
deps.ts |
Dependency analysis and installation (parseDeps, installDeps) for the --install flag |
error.ts |
Fail error class; EXIT_CODES and ERRNO_CODES maps; stack/caller-location parsing; message formatting |
log.ts |
LogEntry union type, the default log function, and per-kind formatters (including the formatCmd syntax highlighter) |
md.ts |
transformMarkdown() — converts a Markdown file into a runnable script by extracting js/ts/bash code fences |
repl.ts |
startRepl() — interactive REPL with history |
util.ts |
Generic helpers: quote/quotePowerShell, parseDuration, toCamelCase, parseBool, getLines, preferLocalBin, once, proxyOverride, etc. |
internals.ts |
The internal bus (a lockable proxy registry used to override bundled dependencies) |
globals.ts / globals-jsr.ts |
Populate globalThis with all exports and declare global types |
index.ts |
Public API barrel; re-exports core + goods + selected vendor packages; defines VERSION |
vendor-core.ts / vendor-extra.ts / vendor.ts |
Wrapped re-exports of third-party packages (chalk, which, ps, zurk/spawn, globby, yaml, maml, fs-extra, minimist, envapi, node-fetch-native, depseek, create-require) |
versions.ts |
Pinned version strings for zx and its bundled dependencies |
2.3 The $ factory and async context
$ is a Proxy-wrapped function backed by an AsyncLocalStorage<Options> store. This is what lets configuration be both global and locally scoped:
const storage = new AsyncLocalStorage<Options>()
const getStore = () => storage.getStore() || defaults
export function within<R>(callback: () => R): R {
return storage.run({ ...getStore() }, callback)
}
- Calling
$`cmd`reads the current store, builds aSnapshotof the effective options, and constructs aProcessPromise. - Calling
$({...})returns a new$whose store is merged with the supplied options — this is how presets and chaining work. within()runs a callback inside a forked store, so mutations to$(andcd()) inside it do not leak out.
2.4 ProcessPromise lifecycle
ProcessPromise extends Promise<ProcessOutput>. It moves through five stages:
| Stage | Meaning |
|---|---|
initial |
Constructed but not yet armed |
halted |
Created with {halt: true}; awaits an explicit .run() |
running |
Process spawned; output streaming |
fulfilled |
Completed with exit code 0 (or nothrow) |
rejected |
Completed with a non-zero exit code or spawn error |
Three internal gears drive it:
| Method | Role |
|---|---|
build() |
Builds the final cmd string from the template pieces and args, applying the quote function; validates shell/quote availability and sync constraints |
run() |
Calls zurk's exec() to spawn the process, wires up start/stdout/stderr/end event listeners, and arms the timeout |
finalize() |
Constructs the ProcessOutput, resolves or rejects the promise based on exit status and nothrow |
Important
ProcessPromiseinstances must be created through$, not vianew ProcessPromise(). Direct construction is disarmed (disarm()) and accessing its methods throws "Inappropriate usage. Apply $ instead of direct instantiation."
2.5 The pipe bus
Piping is implemented with a static bus (a Map of source→destinations plus a WeakMap of promisified streams). Two properties make zx piping unusual:
- Stream-kind selection —
pipe.stdout,pipe.stderr, andpipe.stdalllet you pipe a specific stream. - Replay ("time machine") — output chunks are buffered, so you can attach a consumer to a process after it has already produced output or even settled. The bus replays buffered chunks in order, then forwards live data.
This also enables fan-out (one source → many consumers) and merge (many sources → one consumer via halted processes).
2.6 The dependency bus and bundling
internals.ts exposes a small bus that wraps each bundled third-party package in a Proxy. Wrapped packages can be overridden at runtime before the bus is locked (index.ts calls bus.lock() on import). This indirection lets zx ship a single static bundle while still allowing the lite build and tests to swap implementations.
3. Build & Packaging
zx uses a bespoke esbuild-based toolchain (the authors documented why). The package is a hybrid distribution: it provides both CJS and ESM entry points and bundles its dependencies for reproducibility and auditability.
3.1 Entry points
package.json exports define four public subpaths:
| Subpath | Purpose |
|---|---|
zx |
Main entry — all features |
zx/globals |
Populate globalThis with zx functions |
zx/cli |
The CLI runner |
zx/core |
The template spawner core, for building 3rd-party tools on top of zx |
The binary is declared as "bin": { "zx": "build/cli.js" } and the man page as "man": "./man/zx.1".
3.2 Build scripts
| npm script | Action |
|---|---|
build |
Runs versions → js → dts → tests sub-builds |
build:js |
esbuild hybrid CJS bundle for cli, core, deps, globals, index, internals, util, vendor* |
build:vendor |
Bundles vendor-*.ts with all deps inlined (--bundle=all) |
build:dts |
tsc then build-dts.mjs (merges 3rd-party types via dts-bundle-generator) |
build:tests |
Generates export-consistency autotests |
build:dcr |
Builds the Docker image (dcr/Dockerfile, base node:24-alpine) |
build:jsr |
Builds JSR publishing assets |
build:lite / build:pkgjson / build:manifest |
Produce the lite channel and package manifests |
Note
Bundling solves four goals at once: smaller install/footprint, reproducible
npx/dlx/bunxinvocations, support for Node 12–25, and easier auditing (all code in one place).
3.3 Runtime dependencies (bundled)
These versions are pinned in src/versions.ts and bundled into the published artifacts:
| Package | Version | Role |
|---|---|---|
zurk |
0.11.10 | Process execution engine (exec, buildCmd, VoidStream) |
chalk |
5.6.2 | Terminal colors |
which |
7.0.0 | Locate binaries |
@webpod/ps |
1.2.1 | Cross-platform process listing/tree |
globby |
16.2.0 | Glob matching |
yaml |
2.8.4 | YAML parsing |
maml |
^1.0.0 | MAML parsing |
fs-extra |
11.3.5 | Extended fs API |
create-require |
1.1.1 | require() shim for ESM |
node-fetch-native |
1.6.7 | fetch implementation |
depseek |
0.4.6 | Import scanning for --install |
minimist |
1.2.8 | Argument parsing |
envapi |
0.2.3 | .env parsing/loading (dotenv) |
Tip
versionsis exported at runtime, so an operator can assert the exact bundled dependency versions in a health check:import { versions } from 'zx'; assert(versions.zx === '8.9.0').
4. Configuration Reference
All configuration lives on the $ object (and its defaults). Options can be set globally ($.option = …), per-invocation via a preset ($({option})), through CLI flags, or via ZX_-prefixed environment variables.
4.1 The Options interface
| Option | Type | Default | Description |
|---|---|---|---|
cwd |
string |
process.cwd() |
Working directory for spawned processes |
ac |
AbortController |
auto | Abort controller for the process |
signal |
AbortSignal |
from ac |
Abort signal |
input |
string | Buffer | Readable | ProcessOutput | ProcessPromise |
— | stdin to feed the command |
timeout |
Duration |
— | Auto-kill after this duration |
timeoutSignal |
NodeJS.Signals |
SIGTERM |
Signal used on timeout |
stdio |
StdioOptions |
'pipe' |
Standard stream configuration |
verbose |
boolean |
false |
Print commands and their output |
sync |
boolean |
false |
Synchronous execution |
env |
NodeJS.ProcessEnv |
process.env |
Environment variables |
shell |
string | true |
which bash |
Shell binary (or true for default) |
nothrow |
boolean |
false |
Do not throw on non-zero exit |
prefix |
string |
set -euo pipefail; (bash) |
Prepended to every command |
postfix |
string |
; exit $LastExitCode (pwsh) |
Appended to every command |
quote |
typeof quote |
bash quote |
Argument-escaping function |
quiet |
boolean |
false |
Suppress all output |
detached |
boolean |
false |
Spawn detached |
preferLocal |
boolean | string | string[] |
false |
Prefer node_modules/.bin binaries |
spawn |
typeof child_process.spawn |
native | Custom spawn implementation |
spawnSync |
typeof child_process.spawnSync |
native | Custom sync spawn implementation |
store |
TSpawnStore |
— | Custom output store |
log |
typeof log |
built-in | Logging function |
kill |
typeof kill |
built-in | Process killer |
killSignal |
NodeJS.Signals |
SIGTERM |
Default kill signal |
halt |
boolean |
false |
Create without auto-running |
delimiter |
string | RegExp |
/\r?\n/ |
Line split delimiter for lines()/iteration |
4.2 Key configuration knobs
$.shell — Path to the shell binary. Default is which bash. CLI: --shell=/bin/bash.
$.prefix / $.postfix — Wrap every command. Default prefix is set -euo pipefail; (fail-fast bash). The postfix ; exit $LastExitCode is used for PowerShell compatibility.
$.preferLocal — Prepend node_modules/.bin to PATH so local binaries win over global ones. Accepts a boolean or one/many directories.
$.kill — The default implements a half-graceful shutdown by walking the process tree via ps.tree(). Override for custom logic (e.g. tree-kill).
$.log — A function receiving LogEntry objects (cmd, stdout, stderr, end, cd, fetch, retry, kill, custom). Override $.log.output to change the stream and $.log.formatters to customize rendering. Useful for masking secrets in logged commands.
$.delimiter — Controls how lines() and async iteration split output. Set to /\0/ for null-delimited tools (find -print0).
import { LogEntry, log } from 'zx/core'
$.log = (entry: LogEntry) => {
if (entry.kind === 'cmd') process.stderr.write(masker(entry.cmd))
else log(entry)
}
4.3 Environment-variable configuration
resolveDefaults() reads process.env for ZX_-prefixed keys and maps the allowed subset onto $. The allow-list (ENV_OPTS) is:
cwd, preferLocal, detached, verbose, quiet, timeout,
timeoutSignal, killSignal, prefix, postfix, shell
Keys are camel-cased after stripping the ZX_ prefix, and values are coerced with parseBool. The CLI additionally honors ZX_-prefixed forms of all its flags.
| Variable | Effect |
|---|---|
ZX_VERBOSE, ZX_QUIET, ZX_SHELL, ZX_PREFIX, ZX_POSTFIX, ZX_CWD, ZX_PREFER_LOCAL, ZX_TIMEOUT, … |
Set the corresponding $ option |
ZX_REPL_HISTORY |
Path to REPL history (default ~/.zx_repl_history) |
FORCE_COLOR |
Force subprocess colors |
CI |
Disables the spinner() animation |
5. Deployment & Operations
5.1 Running in containers
The official image is published to the GitHub Container Registry, built FROM node:24-alpine (see dcr/Dockerfile):
docker pull ghcr.io/google/zx:8.5.0
# Inline eval
docker run -t ghcr.io/google/zx:8.5.0 -e="await \$({verbose: true})\`echo foo\`"
# Mount a script directory
docker run -t -i -v ./:/script ghcr.io/google/zx:8.5.0 script/t.js
5.2 CI usage
The default GitHub Actions runner ships npx, so no install step is needed:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build with zx
env:
FORCE_COLOR: 3
run: |
npx zx <<'EOF'
await $`...`
EOF
Tip
Prefer
ZX_-prefixed env vars in CI (e.g.ZX_VERBOSE: true) over editing each script. They apply across every command the runner executes.
5.3 Operational checklist
- [ ] Bash (or PowerShell) is available on the target host; on Windows, decide between WSL/Git Bash and
usePowerShell()/usePwsh(). - [ ] The chosen runtime meets the version floor (Node
>= 12.17.0, Bun>= 1.0.0, Deno1.x/2.x). - [ ] For Deno, grant
--allow-read --allow-sys --allow-env --allow-run. - [ ]
FORCE_COLORset if colored subprocess output is expected. - [ ] Scripts avoid
process.exit()immediately afterconsole.log()(see Troubleshooting in the User Guide). - [ ] Timeouts (
$.timeout/.timeout()) set for any network or long-running command. - [ ] For embedding inside another tool, evaluate
zx@liteorzx/core.
5.4 Behavioral defaults to know
$.verboseis false by default — successful command output is captured but not echoed; onlystderris shown.- Process cwd synchronization is off by default; enable
syncProcessCwd()ifcd()must affectprocess.cwd()globally. - The
spinner()is disabled underCI. - Sync mode cannot resolve async-interpolated values inside a template.
6. Security Analysis
zx executes shell commands, so it is inherently powerful and must be treated as a privileged tool. The following analysis reflects the actual code paths.
6.1 Argument escaping (defense)
The primary safety mechanism is automatic escaping of interpolated values. quote() (bash) emits C-style $'...' quoting; quotePowerShell() emits single-quote-doubled strings. This neutralizes shell metacharacters in user-supplied data:
const name = 'foo & bar'
await $`mkdir ${name}` // safe; '&' is not interpreted
Warning
Do not defeat the escaper. Wrapping an interpolation in your own quotes can reintroduce injection. The documented anti-pattern:
const args = ['param && echo bar'] const p = $`echo --foo=$'${args}'` (await p).stdout // '--foo=$param\nbar\n' ← command injectionAlways interpolate a bare
${value}(or an array of values) and letzxquote it.
6.2 Command-injection surface via env-driven prefix/postfix
$.prefix and $.postfix are prepended/appended to every command without escaping (they are intended to carry shell directives). Because ZX_PREFIX and ZX_POSTFIX environment variables map onto these options via resolveDefaults(), an attacker who controls the environment can inject arbitrary commands into every invocation (tracked upstream as issue #1435, "ZX_PREFIX / ZX_POSTFIX allow implicit global command injection via environment variables").
Caution
Treat the process environment as a trust boundary. In multi-tenant or untrusted-input contexts, sanitize or strip
ZX_*variables before runningzx, and never derive$.prefix/$.postfixfrom user input.
6.3 Remote and dynamic code execution
zx <url>downloads and executes a remote script. Only run trusted URLs.--eval/-eand stdin scripts execute arbitrary code by design.--installparses imports (viadepseek) and runsnpm installfor missing packages. The dependency parser inheritsdepseek's limitations; note the upstream bug wherenode:-prefixed built-in imports were mis-resolved tonode@latest(issue #1462, fixed).
Important
--installshells out to the package manager with--no-save --no-audit --no-fund. In hardened environments, prefer committing dependencies explicitly over relying on--install, and pin the--registry.
6.4 Secrets handling
Commands are logged through $.log when verbose. To avoid leaking secrets into logs, override the cmd formatter to mask sensitive substrings (see §4.2). Avoid passing secrets as command arguments where a child process or ps listing could expose them; prefer $.env injection.
6.5 Supply-chain posture
The project bundles its dependencies into a single auditable artifact, runs npm audit checks (test:audit), enforces a license check (test:license), and runs the zizmor GitHub Actions security linter (test:workflow, configured by zizmor.yml). The lite channel reduces the audited surface further.
6.6 Known security-relevant issues
| Issue | Status | Note |
|---|---|---|
| #1435 ZX_PREFIX/ZX_POSTFIX injection | Open | Env-driven prefix/postfix is unescaped by design; sanitize env |
#1462 --install treats node: imports as node@latest |
Closed | Dependency-parse edge case |
#1443 Unhandled rejection in fetch().pipe() on stream error |
Open | Wrap piped fetches in error handling |
#1461 $ throws synchronously on build failure |
Closed | Build-time errors now surface as rejected promises |
7. Error Handling Internals
The Fail class (src/error.ts) centralizes error handling:
EXIT_CODES— maps shell exit codes to human-readable meanings (e.g.2→ "Misuse of shell builtins",127→ "Command not found",137→ "Kill (terminate immediately)").ERRNO_CODES— maps libc errno values to descriptions (e.g.-2→ "No such file or directory").formatExitMessage()/formatErrorMessage()/formatErrorDetails()— build the human-readableProcessOutput.message, including the caller location, exit code annotation, signal, and a sampled extract of error lines.getCallerLocation()— parses the stack (supporting both V8 and JSC stack formats) to attribute the failing command to its source line.
ProcessOutput extends Error, so a rejected command is a throwable that also carries exitCode, signal, stdout, stderr, and ok.
8. Logging & Observability
The log.ts module defines a discriminated-union LogEntry and a pluggable log function. Each entry has a kind:
| Kind | Emitted when | Gated by |
|---|---|---|
cmd |
A command starts | verbose |
stdout |
stdout data (unless piped) | verbose |
stderr |
stderr data | not quiet |
end |
A command finishes | verbose |
cd |
cd() is called |
verbose, not quiet |
fetch |
fetch() is called |
verbose, not quiet |
retry |
A retry() attempt fails |
verbose, not quiet |
kill |
kill() is called |
verbose, not quiet |
custom |
User-defined | verbose |
The verbose/quiet matrix (only stderr is shown by default):
| Event | Verbose | Quiet |
|---|---|---|
| stdout | shown | hidden |
| stderr | shown | hidden |
| cmd | shown | hidden |
| fetch / cd / retry / custom | shown | hidden |
formatCmd() provides syntax highlighting for logged commands (greens for the command, yellow for quotes/$, red for operators, cyan for reserved words). Output defaults to process.stderr; redirect via $.log.output.
9. Testing Strategy
zx invests heavily in correctness across runtimes. The pretest step builds the bundles first, because tests run against production artifacts, not raw source.
| Category | Scripts |
|---|---|
| Unit | test:unit (node --experimental-transform-types ./test/all.test.js) |
| Coverage | test:coverage (c8, 98% threshold per the contribution guide) |
| Artifact integrity | test:npm, test:jsr, test:dcr (verify built npm/JSR/Docker artifacts) |
| Size | test:size (size-limit, catches bundle duplication) |
| Static analysis | fmt:check (prettier), test:circular (madge, no circular deps) |
| Types | test:types (tsd) |
| Smoke / runtime matrix | test:smoke:* for strip-types, tsx, tsc, ts-node, bun, win32, deno, cjs, mjs |
| Supply chain | test:license (test/extra.test.js), test:audit, test:workflow (zizmor) |
The full runtime × OS matrix is exercised in CI (.github/workflows/test.yml), covering Node 12–25, Bun, Deno, and Windows.
10. Extending & Embedding
10.1 Custom shells
Use useBash(), usePowerShell(), or usePwsh(), or set $.shell + $.quote manually. These helpers set both the binary and the matching quote function (and bash sets the set -euo pipefail; prefix).
10.2 Custom spawn / kill
Override $.spawn, $.spawnSync, and $.kill to integrate with alternative process managers or sandboxes. Example: replacing kill with tree-kill:
import treekill from 'tree-kill'
$.kill = (pid, signal = 'SIGTERM') =>
new Promise((resolve, reject) =>
treekill(pid, signal, (err) => (err ? reject(err) : resolve())))
10.3 Building tools on zx/core
Import from zx/core to reuse the template spawner without the extra helpers — ideal for libraries that want the $ ergonomics with their own utility set. The lite channel is the packaged form of this approach.
10.4 Custom output store and delimiters
Provide $.store (a TSpawnStore) to capture output differently, and $.delimiter to control line splitting for binary-safe tools.
11. Migration & Compatibility
Breaking changes from v7 → v8, with restoration paths:
| Change | Restore v7 behavior |
|---|---|
$.verbose defaults to false |
$.verbose = true (or $.quiet = true to fully silence) |
ssh API removed |
import { ssh } from 'webpod' |
| No PowerShell auto-detect on Windows | usePowerShell() or usePwsh() |
| cwd sync disabled by default | syncProcessCwd() |
Note
v7 is in maintenance mode (bugfixes only). v8 is substantially smaller, faster, and safer. The
--experimentalCLI flag is deprecated and no longer required.
12. Repository Structure
zx/
├── src/ Core library (TypeScript)
│ ├── core.ts $, ProcessPromise, ProcessOutput, Options, cd, kill
│ ├── goods.ts echo, fetch, question, retry, spinner, glob, temp*
│ ├── cli.ts CLI entry + script preprocessor
│ ├── deps.ts --install dependency analysis
│ ├── error.ts Fail, EXIT_CODES, ERRNO_CODES
│ ├── log.ts LogEntry, log, formatters
│ ├── md.ts Markdown → script transform
│ ├── repl.ts REPL
│ ├── util.ts quote, parseDuration, helpers
│ ├── internals.ts dependency bus
│ ├── globals*.ts global injection + type decls
│ ├── vendor*.ts wrapped 3rd-party deps
│ ├── versions.ts pinned dep versions
│ └── index.ts public API barrel
├── scripts/ esbuild-based build toolchain
├── test/ unit, smoke (bun/deno/win32/cjs/mjs), it (artifact)
├── test-d/ type tests (tsd)
├── examples/ runnable sample scripts
├── man/zx.1 man page
├── dcr/Dockerfile container image (node:24-alpine)
├── docs/ VitePress documentation site
├── package.json hybrid exports, bin, build scripts
└── tsconfig.json
13. Quick Reference
13.1 Defaults at a glance
| Setting | Default |
|---|---|
| Shell | which bash |
| Prefix (bash) | set -euo pipefail; |
verbose |
false |
quiet |
false |
nothrow |
false |
sync |
false |
detached |
false |
preferLocal |
false |
stdio |
'pipe' |
killSignal / timeoutSignal |
SIGTERM |
delimiter |
/\r?\n/ |
| cwd sync | disabled |
13.2 Entry points
| Import | Use |
|---|---|
zx |
Everything |
zx/globals |
Inject globals |
zx/cli |
CLI runner |
zx/core |
Spawner core for embedding |
13.3 Operational commands
# Run a script
zx script.mjs
# Verbose via env (CI-friendly)
ZX_VERBOSE=true zx script.mjs
# Pin registry, auto-install deps
zx --install --registry=https://registry.npmjs.org/ script.mjs
# Container
docker run -t -i -v ./:/script ghcr.io/google/zx:8.5.0 script/t.js