Skip to content

feat(cli): ship a standalone binary built as a Node 26 SEA - #76

Merged
sanchitrk merged 7 commits into
devfrom
feat-cli-bin
Oct 13, 2026
Merged

sanchitrk merged 7 commits into
devfrom
feat-cli-bin

Conversation

@sanchitrk

Copy link
Copy Markdown
Contributor

Ships codework as a standalone binary, so users no longer need Node installed. It is a Node 26 single executable application (SEA): official Node with the CLI bundled into it.

How it works

  • Bundle (vp run bundle): the whole CLI as one ESM file, entry src/sea.ts. Before the CLI starts, the entry unpacks the files that must exist on disk to a per-user cache ($TMPDIR/codework-node-<uid>/<hash>-<os>-<arch>, mode 0700, written atomically). It also gives plugins the CLI's own effect.
  • Plugins share the host's effect: a bundled host publishes its modules on globalThis[Symbol.for("codework.v1.effect")] (and codework.v1.plugin). The plugin loader serves them as virtual modules that load each one lazily. Unbundled installs resolve exactly as before.
  • Unpacked packages: just-bash (its sqlite3 worker, plus zstd and xz) and photon load files from beside their own modules, so they ship as a real node_modules that the harness resolves from CODEWORK_PACKAGES. node-liblzma is rebuilt with xz linked statically, so no system liblzma is needed. just-bash's python and js-exec are not shipped, because the harness doesn't enable them.
  • SEA quirks: a SEA's main module has no import.meta.resolve, and its import() reaches only built-ins. So resolution borrows a resolver (Plugin.lendResolve) and imports go through importModule. The bundle also defines __dirname and __filename for npm's installer.
  • SEA build (vp run sea): Node 26.11.1 checked against a pinned sha256, --build-sea with mainFormat: "module", and ad-hoc signing on macOS.
  • Intel Macs are dropped, both for the binary and the TUI (@codeworksh/tui-darwin-x64 is deprecated on npm).

Releases

publish.yml builds the binary for darwin-arm64, linux-arm64 and linux-x64 on dev and main, each on its own runner. Each row runs the offline binary test, and the release job attaches cli-<os>-<arch>.tar.gz to a GitHub Release:

  • dev: devel, one prerelease replaced on every push → releases/download/devel/cli-<os>-<arch>.tar.gz
  • main: cli-v<version>, marked latest → releases/latest/download/cli-<os>-<arch>.tar.gz
  • canary: stays npm-only.

npm publishing doesn't wait on the binary. The run computes its release id once (scripts/version.mjs), so the binary reports the same version npm publishes.

Testing

packages/codework/test/sea.test.ts runs the real binary with no Node on PATH:

  • offline: it unpacks its assets privately, and a plugin with no dependencies runs through the binary's own effect;
  • live (OpenAI, OpenRouter): a plugin's Schema.Finite tool works, and sqlite3, tar --zstd and tar -J run in the virtual shell;
  • CODEWORK_PLUGIN_LIVE=1: plugins install from the npm registry and from git (codework-tool-spacex v0.5.0 and codework-prompt-musk v0.4.0, both updated to the current SDK), and the spacex tool runs.

All 4 pass locally on darwin-arm64. vp check and typecheck are clean.

The workflow changes can only be proven on GitHub, so the first dev run after merge is their real test. Things to watch:

  • that the Ubuntu runners have no node in /usr/bin, which the offline test asserts;
  • that cmake is present on ubuntu-24.04-arm.

Not covered yet: the codework.sh/install script, and loading third-party sandbox drivers from the plugin store.

🤖 Generated with Claude Code

sanchitrk and others added 7 commits October 13, 2026 12:17
Stop building and publishing @codeworksh/tui-darwin-x64; the package is
deprecated on npm. The launcher now reports darwin-x64 as unsupported
instead of naming a package that no longer ships.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host bundled into one file, like the standalone binary, has no effect
on disk for plugins to resolve to. It now publishes its modules through
Plugin.share on globalThis[Symbol.for("codework.v1.effect")] (and
codework.v1.plugin), and the plugin loader serves them as virtual modules
that load each one on first import. Unbundled hosts resolve as before.

scripts/shared.mjs generates that registry, with every public module's
export names, for the CLI bundle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`vp run bundle` writes dist/sea: the whole CLI as one ESM file, entry
src/sea.ts, beside the assets the binary embeds. Before the CLI starts,
the entry unpacks them to a per-user cache keyed by their content hash,
publishes the CLI's effect for plugins, and names the unpacked paths.

Packages that load files from beside their own modules are unpacked as a
real node_modules instead of bundled: just-bash (its sqlite3 worker and
zstd and xz addons) and photon. The harness resolves them from
CODEWORK_PACKAGES. node-liblzma is rebuilt with xz linked statically, so
the binary needs no system liblzma. The image worker is bundled on its
own, and a managed server re-spawns the binary itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`vp run sea` injects the bundle into the official Node 26.11.1 for this
platform, checked against its pinned sha256, as a single executable
application with every asset embedded, and signs it ad hoc on macOS.

A SEA's main module has no import.meta.resolve and its own import()
reaches only built-ins. The bundled entry now borrows a resolver through
Plugin.lendResolve, and unpacked packages load through importModule.
The bundle also defines __dirname and __filename, which npm's installer
reads, so plugins install from git and the registry inside the binary.

test/sea.test.ts runs the binary with no Node on PATH: it unpacks its
assets, runs a plugin through the shared effect, runs sqlite3, zstd and
xz in the virtual shell against live models, and installs and runs
plugins from git and npm when CODEWORK_PLUGIN_LIVE=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dev and main now build the binary for darwin-arm64, linux-arm64 and
linux-x64, each on its own runner, run the offline binary test, and
attach cli-<os>-<arch>.tar.gz to a GitHub Release: `devel`, one
prerelease replaced on every dev push, or `cli-v<version>`, marked
latest. canary stays npm-only, and npm never waits on the binary.

The run computes its release id once (scripts/version.mjs) and every job
uses it, so the binary reports the version npm publishes. The binary
test's artifact no longer depends on the platform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sanchitrk
sanchitrk merged commit be8ae40 into dev Oct 13, 2026
2 checks passed
@sanchitrk
sanchitrk deleted the feat-cli-bin branch October 13, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant