waos-cli
cargo waos: what someone writing a program runs. It writes a new program, builds a program as a component, runs it in the WaOS on this machine, and packages it as a signed .wapk listed in a package source's index.json. It gains a command when a step needs one, and has no doctor and no device commands: a phone is reached through the WaOS app already on it, found on the network, never by installing anything on the phone (the design's decision 21).
- Takes: a name and a widget kit, for
new; a program's crate, in a cargo project, with its[package.metadata.waos], whole forpackageand its id forrun; the WaOS app, installed on this machine, forrun; the user's signing key, forpackage. - Provides: the
cargo-waosbinary:cargo waos new,cargo waos build,cargo waos runandcargo waos package. - Used by: whoever writes a program. The WaOS app itself is an Waft app, built with
cargo waft.
new writes a new program in a folder of its name in this directory, a Waft app over waft-embedder-wasdk in one widget kit's look, as cargo waft new writes a Waft app: with --template cupertino, the default, a CupertinoApp whose page says hello, and with --template winui a WidgetsApp in Fluent's look whose page has a button, in a window sized for a desktop. cargo waos run runs it and cargo waos package packages it as it is written. It prints the command that runs it, cd <name> && cargo waos run.
hello/
Cargo.toml wasdk, waft-embedder-wasdk, waft and the kit by their crates.io versions; [package.metadata.waos]; [workspace]
rust-toolchain.toml WaOS's toolchain, nightly-2026-09-02 with wasm32-wasip2, which rustup installs by itself
icon.png a blue tile with a white disc, 1024 pixels on a side
src/main.rs the app: templates/cupertino.rs or templates/winui.rs
.gitignore /target
The program's id is dev.example. and its name without - and _, which an id cannot hold, so my-app is dev.example.myapp; the name people see is the package's, spaced and capitalized, My app; its version is the package's, 0.1.0. A name is refused unless it is lowercase ASCII letters, digits, - and _, starting with a letter, and neither a Rust keyword nor a name cargo keeps for a package with a binary (test, build, deps, examples, incremental); so is a folder that is there already. Nothing is written then.
The manifest's empty [workspace] makes the program a workspace of its own, so it builds by itself in any folder, inside another workspace too, where cargo new would add the new crate to that workspace's members instead. A workspace whose members take the program's folder in fails with cargo's "multiple workspace roots found" until it lists the folder in its exclude, or the program's [workspace] is removed to make it a member.
The SDK's crates are named at this release's version, since the workspace versions its crates together, and Waft's at the version waft-embedder-wasdk is built on. The SDK is not on crates.io yet, so tests/new.rs builds each template against this workspace: it runs cargo waos new in a folder of its own, patches crates.io there, in a .cargo/config.toml above the program, with the workspace's path of each crate the program's manifest names, gives the program the workspace's Cargo.lock, and runs cargo waos build, into a target directory under CARGO_TARGET_TMPDIR kept between runs.
build runs cargo for wasm32-wasip2, debug unless --release (a debug build carries function names and line tables, debug = "line-tables-only", which is what WaOS reads of debug info: trap backtraces with file and line), and puts the component cargo made at target/waos/<profile>/<package>.wasm, named after the package; without -p the package is the one of the current directory. It prints the component's path.
run reads the program's id from its [package.metadata.waos], the one field of the table a run needs, and refuses a package without the table, with package's message, before it builds anything. It builds the program, connects to the WaOS on this machine at waos_dev_protocol::socket_path(), starts the installed WaOS.app when none answers and waits up to ten seconds for it, and sends the program with its id (the design's decision 29). WaOS runs it as the program the id names (decision 11 of docs/proposals/2026-10-23-permissions.md): with that program's settings and its own /data and /cache, installed or not, as Xcode's and Android Studio's runs share the app's container and permissions. It starts WaOS as open -b dev.waos.app --stderr ~/Library/Logs/WaOS.log --args --panel-closed, with --env RUST_LOG=… before --args when RUST_LOG is set, so what WaOS logs is appended to that file, picked by the same RUST_LOG, and says so once with the file's path; and with --env WAOS_NATIVE_UNWIND_INFO=… when that is set, which at 1 has WaOS compile programs with native unwind information: set it to profile or debug a program natively, for a native profiler or debugger to walk through its frames, and not otherwise, since macOS then takes seconds to drop a large program. --panel-closed, WaOS's own argument, keeps its library's panel closed at launch, so the run shows only the program until the user clicks WaOS's Dock icon. These take effect only when the system starts the app, and a WaOS running already keeps its own. Only macOS's WaOS.app is started: elsewhere, a run with no WaOS answering ends saying to start WaOS by hand. The connection is the run: what the program writes to stdout and stderr comes to this process's own, bytes as the program wrote them, until the run ends; then one line on stderr says how the program ended, and the exit status is 0 for a main that returned success and 1 otherwise, for a run WaOS refused, and for one WaOS stopped or replaced with a later run of the same id. Ctrl-C closes the connection, which stops the program in WaOS, and exits with 130. A WaOS of another build answers with another protocol number, and the run ends with a line naming both before anything is sent.
package reads the program's id, the name people see and its icon from its Cargo.toml, and takes the version from the package's own version:
[]
= "dev.example.triangle" # reverse-DNS; names the program for good, and what `run` runs it as
= "Triangle"
= "icon.png" # relative to Cargo.toml: a square PNG, at least 256 pixels on a side
It builds the release component, signs it with the user's signing key, writes the package at target/waos/<id>-<version>.wapk, prints its path, and lists it in target/waos/index.json, making the index when there is none: a listing of the same id at the same version is replaced, and every other is kept. target/waos then holds what a package source serves, the index with the packages beside it, each listed by its file name with its SHA-256; uploading the index and the packages as assets of a GitHub release makes the repository a source (the design's decision 45).
The signing key, Android's name for the private key that signs a publisher's packages, is the text of WAOS_SIGNING_KEY when that is set, as on CI, and otherwise the file signing.key in the user's configuration folder: $XDG_CONFIG_HOME/waos/signing.key, or ~/.config/waos/signing.key when that is not set, on macOS as on Linux, apart from the WaOS app's own folder so removing the app never takes the key with it. Its public half, which each package carries as publisher.key, is the publisher's key. The first package with neither makes the signing key, in a file only the user may read in a folder only the user may open, and says where it is and the fingerprint of the publisher key its packages carry (the design's decision 47). Back it up: every update of a program must be signed with the key that signed it first, so a lost key means publishing the program again under a new id. Both hold the same text, the key's PKCS#8 PEM (-----BEGIN PRIVATE KEY-----), so a CI secret is set to the file's content. A key that cannot be read is an error, never a reason to make another, and so is a WAOS_SIGNING_KEY set but empty, which is what CI gives a secret it does not have. On CI, where CI is set, a key is read but never made: with no WAOS_SIGNING_KEY and no key file, package fails saying to set WAOS_SIGNING_KEY, rather than sign with a key thrown away with the machine.
Nothing here runs a program: it runs in the WaOS app, on a thread of its own there, and this process holds only the connection.