Publishes the ZCode Agent CLI as an npm package with a
zcode bin, so it installs with npm i -g instead of a hand-written shell wrapper.
The upstream repo is vendored as a git submodule at vendor/ZCode; the CLI bundle is built
from it and shipped inside this package.
Requires git, pnpm and Node >= 22. The bundled CLI works on any Node 22+; building it
does not need Node 24.
git submodule update --init --recursive
npm run build # or: node scripts/prepare.mjsnpm run build does four things:
pnpm install --frozen-lockfileandpnpm --filter @zcode/cli... buildinsidevendor/ZCode- copies the artifacts into this package (see layout below)
- applies two guarded rewrites to its own copy of the bundle (see below)
- syncs
versionfromapps/zcode-cli/package.json
prepack runs the same script, so npm pack / npm publish never ship a stale bundle.
pnpm i at this repo root is optional: it installs the runtime dependencies so
node bin/zcode.js works locally, but they are only needed by consumers of the published
package. Build dependencies live in the submodule.
lib/zcode.cjs CLI bundle (the real entry point)
lib/provider/zcode-builtin.json built-in provider catalog
lib/tui/index.js TUI runtime (ESM bundle built from packages/tui)
lib/zcode.cjs keeps @zcode/tui, koffi and playwright-core external, and reads
provider/zcode-builtin.json relative to its own directory.
@zcode/tui is private and not on npm, so it is not a dependency at all. The bundle's
non-SEA TUI import is rewritten to a relative path (see below) and the runtime ships inside
lib/. Its own non-@zcode imports (@mbears/opentui-core, @mbears/opentui-react,
react, shiki) are declared as regular dependencies. scripts/prepare.mjs re-reads the
emitted TUI bundle on every build and fails if upstream starts importing a package that is
not declared.
The submodule stays a pristine checkout: upstream source is never edited, and everything this
package changes lives in scripts/prepare.mjs as a guarded string replacement on lib/.
Each one asserts that its literal occurs exactly once, so an upstream reshuffle fails the
build loudly instead of silently shipping a different result. Both comment blocks document how
to re-derive the literals after updating the submodule.
Upstream defaults dynamic workflows to disabled for standalone CLI runs
(DEFAULT_CLI_WORKFLOW_MODE in apps/zcode-cli/packages/cli/src/workflow-mode.ts). This
package flips it to alwaysOn so zcode and zcode tui match the desktop app:
const WORKFLOW_DEFAULT_PATTERN = 'DEFAULT_CLI_WORKFLOW_MODE = "disabled"';
const WORKFLOW_DEFAULT_REPLACEMENT = 'DEFAULT_CLI_WORKFLOW_MODE = "alwaysOn"';The shipped diff against upstream is a single line, and --workflow-mode disabled still
turns workflows off. Note this also applies to headless --prompt runs, which is why upstream
keeps it off by default.
Without SEA the CLI loads the TUI with await import("@zcode/tui")
(apps/zcode-cli/packages/cli/src/tui-runtime-loader.ts), and the bundle keeps that bare
specifier because @zcode/tui is a build external. Since the package does not exist in any
registry, the specifier is rewritten to point into lib/:
const TUI_IMPORT_PATTERN = 'import("@zcode/tui")';
const TUI_IMPORT_REPLACEMENT = 'import("./tui/index.js")';The SEA branch of the same file uses its own node_modules/@zcode/tui/dist/index.js asset
path and is intentionally left alone.
Both rewrites happen at build time rather than through a postinstall/patch-package step:
install-time scripts are subject to npm's allow-scripts policy and may not run, and patching
files inside a globally installed package would have to mutate npm's own tree.
npm i -g @toy-forge/zcode # once published
npm i -g ./toy-forge-zcode-0.17.1.tgz # from a local tarballbin/zcode.js is a thin launcher:
- re-execs Node with
--no-experimental-ffiwhen the running Node hasnode:ffi(Node 26+). The bundled opentui would otherwise pick that backend, which does not implementusize, and TUI init throwsFailed to initialize OpenTUI render library. Older Node rejects the flag, so it is probed first. zcodewith no arguments opens the TUI. Dynamic workflows are already on by default (see above), so no flags are injected; pass--workflow-mode disabledto opt out.- everything else is passed through unchanged:
zcode doctor --json,zcode plugins list,zcode --prompt ..., etc.
- Official plugins and skills are not seeded into this package.
zcode plugins installfetches them from the marketplace;plugins listshows whatever is already in~/.zcode/cli/plugins. playwright-coreis installed for--browser-use headless; a Chrome/Chromium is still required separately.- Data, credentials and configuration stay in
~/.zcode, shared with the desktop app.