Skip to content

RAKUDEJI/wacli

Repository files navigation

wacli

WebAssembly Component composition CLI tool.

CI crates.io License

Overview

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)

Installation

cargo install wacli

Or build from source:

git clone https://github.com/RAKUDEJI/wacli.git
cd wacli
cargo build --release

Features

Feature Default Description
runtime Enables wacli run command (requires wasmtime)

To build without runtime support (smaller binary, faster compile):

cargo install wacli --no-default-features

Usage

wacli 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.

Initialize a new project

wacli init my-cli

Download 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 (default wacli/host) with WACLI_HOST_REFERENCE (default v<cli-version>)
  • WACLI_CORE_REPO (default wacli/core) with WACLI_CORE_REFERENCE (default v<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.

Build from defaults/ and commands/

cd my-cli
wacli build

If 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.

Reproducible Builds (wacli.lock)

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 build prefers digests already pinned in wacli.lock.
  • Use wacli build --update-lock to resolve tags to the latest digest and update wacli.lock.

Options:

  • --manifest: Path to a wacli manifest (defaults to ./wacli.json if 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@version unless name already 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: Use defaults/registry.component.wasm instead of generating a registry
  • --update-lock: Resolve registry tags to digests and update wacli.lock

Note: wacli build scans commands/**/*.component.wasm recursively, and also resolves any registry plugins configured in build.commands.

Run the composed CLI (native host)

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.

Core-provided help/version/validation ("clap-like")

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 --version

Semantics 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).

Compose components directly

wacli compose app.wac -o app.wasm -d "pkg:name=path.wasm"

Plug components together

wacli plug socket.wasm --plug a.wasm --plug b.wasm -o out.wasm

Self update

wacli self-update

Molt WASM-aware registry helper commands (/wasm/v1)

These 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 override

Fetch WIT for a repo + tag (prints WIT text to stdout):

wacli wasm wit example/repo 1.0.0 > component.wit

By default wacli wasm wit uses --artifact-type application/vnd.wasm.wit.v1+text.

Fetch indexed imports/exports:

wacli wasm interfaces example/repo 1.0.0

Search by imports/exports (AND semantics):

wacli wasm search --export "wacli:cli/[email protected]" --os wasip2

Project Structure

my-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:

  1. Scans defaults/ for framework components (host, core)
  2. If host/core is missing and MOLT_REGISTRY is set, pulls them into .wacli/framework/
  3. Scans commands/ for command plugins (*.component.wasm)
  4. If build.commands is set, pulls those plugin components into .wacli/commands/
  5. If registry pulls occur, resolves tags to digests and updates wacli.lock
  6. Extracts command metadata from plugins and generates a registry component into .wacli/registry.component.wasm (or uses defaults/registry.component.wasm with --use-prebuilt-registry)
  7. 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

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Final CLI (my-cli.component.wasm)        │
│  ┌─────────┐   ┌─────────┐   ┌──────────┐                 │
│  │  host   │   │  core   │──▶│ registry  │──▶ plugins      │
│  └─────────┘   └─────────┘   └──────────┘                 │
│   Host APIs      Router        Dispatch + schemas          │
└─────────────────────────────────────────────────────────────┘

Components

  • 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

Plugin Development

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.

Host Access

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

Framework components are published to an OCI registry (Molt spec):

  • host.component.wasm - WASI to wacli bridge
  • core.component.wasm - Command router

Configure MOLT_REGISTRY and use wacli init --with-components / wacli build to pull them from the registry.

WIT Interfaces

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)

Plugin World

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.

License

Apache-2.0

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages