Build a Compiled CLI
Bundle and compile a maltty CLI into standalone binaries using @maltty/bundler.
Prerequisites
- An existing maltty CLI project with
maltty - Node.js 24+
- Bun 1.3+ installed (required for the compile step)
@maltty/bundlerinstalled (pnpm add -D @maltty/bundler)
Overview
The bundler package provides three operations you compose into a release pipeline:
build() must run before compile(). The autoload plugin is applied automatically during both build and watch.
Steps
1. Create a maltty config file
The bundler reads from a maltty.config.ts (or .js, .json, .yaml) file in your project root. All fields are optional -- defaults cover the common case.
2. Run a build
Call build() with your config and working directory. It resolves defaults, reads your package.json version, and invokes tsdown.
build() returns a [Error | null, BuildOutput | null] result tuple. BuildOutput contains:
3. Watch for changes
Use watch() during development. The returned promise resolves only when tsdown's watcher terminates (on process exit).
The optional onSuccess callback runs after each successful rebuild.
4. Compile to standalone binaries
After a successful build, call compile() to produce self-contained executables. This uses bun build --compile internally and requires Bun to be installed.
compile() returns a [Error | null, CompileOutput | null] tuple. CompileOutput contains:
Each CompiledBinary has:
5. Use the autoload plugin
When you pass a commands path in your config, the bundler automatically applies the maltty-static-autoloader rolldown plugin during build and watch. It replaces maltty's runtime autoloader with a static version that pre-resolves all command imports at build time.
You only need the plugin directly if you are constructing a custom tsdown config instead of using build() or watch():
Pass plugin into your tsdown plugins array. In most cases you should prefer build() or watch(), which wire this up automatically.
Configuration reference
All fields in maltty.config.ts are optional. The following table lists every supported option with its default.
Top-level
build
compile
Can be true (compile with all defaults), false/omitted (skip compilation), or an object:
Supported targets
Example: release script
A typical release script builds for all default targets and logs each binary path.
Run it with:
Verification
Troubleshooting
bundled entry not found error from compile()
Issue: compile() returns bundled entry not found in ./dist -- run build() first.
Fix: Always call build() before compile(). Confirm build.out and compile.out resolve to the same directory (or that the build output directory contains index.js).
bun build --compile failed for a target
Issue: Compilation fails for a specific target.
Fix: Ensure Bun is installed and up to date (bun --version). Cross-compilation requires Bun 1.3+. The linux-x64-musl target maps to Bun's standard Linux x64 target -- there is no separate musl binary in Bun.
Autoloader finds no commands
Issue: Commands are missing at runtime in the compiled binary.
Fix: Verify commands in your config points to the directory containing your command files. Each file must export a command() call as its default export. The path is resolved relative to the project root (cwd), not the source file.
Compiled binary crashes with illegal hardware instruction (macOS)
Issue: Running a compiled binary on macOS (especially Apple Silicon) exits immediately with illegal hardware instruction (exit code 132 / SIGILL).
Cause: The Mach-O binary's adhoc code signature is invalid. This happens when the binary is modified after bun build --compile produces it — for example, by copying it with cp, appending data, or restoring it from a build cache (e.g. turbo). macOS refuses to execute ARM64 binaries with broken signatures.
Fix:
-
Rebuild from scratch. Clear any build caches (
rm -rf .turbo) and re-run the compile step. A freshly compiled binary should work. -
Re-sign after copying. If your build pipeline copies compiled binaries to another directory, re-sign them afterward:
-
Verify the binary. You can check whether a binary's signature is intact with:
If it reports
invalid signature (code or signature have been modified), the binary needs to be recompiled or re-signed.
Note:
codesign --strictmay still fail on Bun-compiled binaries — this is expected because Bun appends embedded data after the Mach-O segments. Non-strict validation (codesign -v) is the correct check.
Build target does not match runtime
Issue: The binary crashes with a Node API not found error.
Fix: The default build.target is node18. If your code uses APIs from a newer Node version, set target explicitly (e.g., 'node22').