WebAssembly Component composition CLI tool.
wacli is a CLI tool for composing WebAssembly Components using the WAC language. It provides a framework for building CLI applications from modular WASM components.
Key Features:
- Build CLI apps from modular WASM components
- Auto-generates registry component from command plugins
- Core provides a consistent CLI experience (
--help/--version+ validation) from command schemas - Aliases and hidden commands/args are respected (dispatch + help output)
- Command metadata is extracted as data (WASM custom section), no plugin execution required
- Single binary, no external dependencies (wac, wasm-tools, jq)
- Optional registry integration for framework components, plugins, and WIT/index queries (Molt spec)
cargo install wacliOr build from source:
git clone https://github.com/RAKUDEJI/wacli.git
cd wacli
cargo build --release| Feature | Default | Description |
|---|---|---|
runtime |
✓ | Enables wacli run command (requires wasmtime) |
To build without runtime support (smaller binary, faster compile):
cargo install wacli --no-default-featureswacli reads registry settings from environment variables. For local/dev usage
you can put them in a .env file (loaded automatically if present).
This repository includes a sample: .env.example.
wacli init my-cliDownload framework components in one step:
wacli init my-cli --with-components--with-components pulls host.component.wasm and core.component.wasm from
an OCI registry via /v2 (requires MOLT_REGISTRY). By default it uses:
WACLI_HOST_REPO(defaultwacli/host) withWACLI_HOST_REFERENCE(defaultv<cli-version>)WACLI_CORE_REPO(defaultwacli/core) withWACLI_CORE_REFERENCE(defaultv<cli-version>)
This creates the directory structure:
my-cli/
wacli.json
defaults/
commands/
wit/
types.wit
schema.wit
registry-schema.wit
host-*.wit
command.wit
pipe*.wit
Note: wacli.lock is created/updated by wacli build when resolving registry pulls.
cd my-cli
wacli buildIf defaults/host.component.wasm or defaults/core.component.wasm is missing
and MOLT_REGISTRY is set, wacli build will pull the missing framework
components from the registry into .wacli/framework/ and use the cached files.
wacli init creates a wacli.json manifest so you don't need to repeat build flags.
Example wacli.json:
{
"schemaVersion": 1,
"build": {
"name": "example:my-cli",
"version": "0.1.0",
"description": "Example CLI built with wacli",
"output": "my-cli.component.wasm",
"defaultsDir": "defaults",
"commandsDir": "commands"
}
}Optional: resolve command plugins from an OCI registry (instead of requiring
local commands/*.component.wasm files):
{
"schemaVersion": 1,
"build": {
"name": "example:my-cli",
"version": "0.1.0",
"output": "my-cli.component.wasm",
"defaultsDir": "defaults",
"commandsDir": "commands",
"commands": [
{ "name": "greet", "repo": "example/greet", "reference": "1.0.0" }
]
}
}This requires MOLT_REGISTRY and auth via either MOLT_AUTH_HEADER or
USERNAME/PASSWORD (Basic). Pulled
components are cached under .wacli/commands/. Set WACLI_REGISTRY_REFRESH=1
to force re-pull.
When wacli pulls components from the registry, it writes/updates wacli.lock
to pin the resolved manifest digest (sha256:...). This prevents tags from
drifting and makes builds reproducible.
- By default,
wacli buildprefers digests already pinned inwacli.lock. - Use
wacli build --update-lockto resolve tags to the latest digest and updatewacli.lock.
Options:
--manifest: Path to a wacli manifest (defaults to./wacli.jsonif present)--name: Package name (default: "example:my-cli")--version: Package version (default: "0.1.0")--description: Package description (used for global help output)- Package name and version are combined as
name@versionunlessnamealready contains@. -o, --output: Output file path (default: "my-cli.component.wasm")--defaults-dir: Defaults directory (default: "defaults")--commands-dir: Commands directory (default: "commands")--no-validate: Skip validation of the composed component--print-wac: Print generated WAC without composing--use-prebuilt-registry: Usedefaults/registry.component.wasminstead of generating a registry--update-lock: Resolve registry tags to digests and updatewacli.lock
Note: wacli build scans commands/**/*.component.wasm recursively, and
also resolves any registry plugins configured in build.commands.
wacli run my-cli.component.wasm <command> [args...]
wacli run --dir /path/to/data my-cli.component.wasm <command> [args...]
wacli run --dir /path/to/data::/data my-cli.component.wasm <command> [args...]Tip: --dir can appear before or after the component path. Use -- if you
need to pass flags like --dir, --help, or --version through to the composed CLI.
Note: Direct wasmtime run is not supported because the composed CLI imports
wacli:cli/[email protected], which is provided by wacli run.
These are handled by the core component, so they work even if plugins don't call any parsing helper:
wacli run my-cli.component.wasm -- --help
wacli run my-cli.component.wasm -- --version
wacli run my-cli.component.wasm -- help greet
wacli run my-cli.component.wasm -- greet --help
wacli run my-cli.component.wasm -- greet --versionSemantics are documented in docs/cli-semantics.md.
Global --help/--version use app metadata embedded at build time (from wacli.json build.name / build.version / build.description).
wacli compose app.wac -o app.wasm -d "pkg:name=path.wasm"wacli plug socket.wasm --plug a.wasm --plug b.wasm -o out.wasmwacli self-updateThese commands call the registry's /wasm/v1 endpoints to fetch WIT and query
the WASM index.
Set MOLT_REGISTRY or pass --registry on each command:
export MOLT_REGISTRY="https://registry.example.com"
# Optional auth
# export USERNAME="..."
# export PASSWORD="..."
# export MOLT_AUTH_HEADER="Authorization: Bearer $TOKEN"
# wacli wasm wit ... --header "Authorization: Bearer $TOKEN" # per-command overrideFetch WIT for a repo + tag (prints WIT text to stdout):
wacli wasm wit example/repo 1.0.0 > component.witBy default wacli wasm wit uses --artifact-type application/vnd.wasm.wit.v1+text.
Fetch indexed imports/exports:
wacli wasm interfaces example/repo 1.0.0Search by imports/exports (AND semantics):
wacli wasm search --export "wacli:cli/[email protected]" --os wasip2my-cli/
wacli.json # Build manifest (created by `wacli init`)
wacli.lock # Registry digest lock (created/updated by `wacli`)
defaults/
host.component.wasm # Required: WASI to wacli bridge
core.component.wasm # Required: Command router
registry.component.wasm # Optional: Used only with --use-prebuilt-registry
commands/
greet.component.wasm # Command plugins (*.component.wasm)
show.component.wasm
.wacli/
registry.component.wasm # Auto-generated build cache (do not edit)
framework/ # Cached host/core pulls (optional)
commands/ # Cached registry plugin pulls (optional)
wit/
*.wit # Installed by `wacli init` (types/host/command/pipe, etc.)
Note: .wacli/ contains build cache artifacts. It's safe to add it to .gitignore.
Runtime layout (for wacli run):
my-cli.component.wasm
plugins/
show/
format/
table.component.wasm
The wacli build command:
- Scans
defaults/for framework components (host, core) - If host/core is missing and
MOLT_REGISTRYis set, pulls them into.wacli/framework/ - Scans
commands/for command plugins (*.component.wasm) - If
build.commandsis set, pulls those plugin components into.wacli/commands/ - If registry pulls occur, resolves tags to digests and updates
wacli.lock - Extracts command metadata from plugins and generates a registry component into
.wacli/registry.component.wasm(or usesdefaults/registry.component.wasmwith--use-prebuilt-registry) - Composes all components into the final CLI
The wacli run command:
- Runs a composed CLI component
- Loads pipes from
./plugins/<command>/...relative to the current working directory - Preopens the current directory and any
--dir HOST[::GUEST]entries
┌─────────────────────────────────────────────────────────────┐
│ Final CLI (my-cli.component.wasm) │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ host │ │ core │──▶│ registry │──▶ plugins │
│ └─────────┘ └─────────┘ └──────────┘ │
│ Host APIs Router Dispatch + schemas │
└─────────────────────────────────────────────────────────────┘
- host: Bridges WASI interfaces to
wacli:cli/host-* - core: Routes commands and exports
wasi:cli/run - registry: Manages command registration
- plugins: Implement commands via
wacli:cli/command
Plugins are built using wacli-cdk:
use wacli_cdk::{Command, CommandMeta, CommandResult};
// Embed command metadata (including a richer per-command schema) into a WASM custom section.
// `wacli build` extracts this data without executing the plugin.
wacli_cdk::declare_command_metadata!(greet_meta, {
name: "greet",
summary: "Greet someone",
usage: "greet [NAME]",
version: "0.1.0",
hidden: false,
args: [
{ name: "name", value_name: "NAME", help: "Person to greet" },
],
});
struct Greet;
impl Command for Greet {
fn meta() -> CommandMeta {
greet_meta()
}
fn run(argv: Vec<String>) -> CommandResult {
let name = argv.first().map(|s| s.as_str()).unwrap_or("World");
wacli_cdk::io::println(format!("Hello, {name}!"));
Ok(0)
}
}
wacli_cdk::export!(Greet);Tip: The command name is derived from the component filename (e.g. greet.component.wasm
becomes greet). Keep it in sync with name: "greet" in the embedded metadata to avoid confusion.
Note: wacli build extracts metadata from the wacli:cli/command-metadata@1 WASM custom
section. Plugins without embedded metadata are rejected. For consistency, implement meta()
by returning the same metadata function used for the custom section.
For the clap-like semantics that core provides (help/version/validation, aliases/hidden, env/default
precedence, etc.), see docs/cli-semantics.md.
For pipe plugins (the pipe-plugin world), see the “Building a Pipe Plugin” section in
crates/wacli-cdk/README.md.
Plugins do not import WASI directly. All host interactions go through the
wacli:cli/host-* interfaces (host-env, host-io, host-fs, host-process, host-pipes).
Framework components are published to an OCI registry (Molt spec):
host.component.wasm- WASI to wacli bridgecore.component.wasm- Command router
Configure MOLT_REGISTRY and use wacli init --with-components / wacli build
to pull them from the registry.
| Interface | Description |
|---|---|
wacli:cli/types |
Shared types (exit-code, command-meta, command-error) |
wacli:cli/schema |
Command/arg schema used for help/version/validation |
wacli:cli/host-env |
Host environment (args, env) |
wacli:cli/host-io |
Host I/O (stdout-write, stderr-write, flush) |
wacli:cli/host-fs |
Host filesystem (read-file, write-file, create-dir, list-dir) |
wacli:cli/host-process |
Host process (exit) |
wacli:cli/host-pipes |
Pipe loader (list-pipes, load-pipe) |
wacli:cli/command |
Plugin export interface (meta, run) |
wacli:cli/registry |
Command management (list-commands, run) |
wacli:cli/registry-schema |
Registry/app schema access (get-app-meta, list-schemas) |
wacli:cli/pipe |
Pipe export interface (meta, process) |
world plugin {
import host-env;
import host-io;
import host-fs;
import host-process;
import host-pipes;
export command;
}Note: These unqualified imports are shorthand for the same-package interfaces.
When embedded into a component they resolve to fully-qualified names like
wacli:cli/[email protected]. This is expected and matches what wacli provides.
Apache-2.0