waos-cli 0.2.0

cargo waos: writes a new program, builds a program as a component, runs it in the WaOS on this machine, packages it as a signed .wapk, and takes screenshots of its windows and gives them input while it runs.
waos-cli-0.2.0 is not a library.

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; and while a run of its own runs the program, it takes a screenshot of the program's windows and gives them input, so an agent building the program sees it and uses it (the design's decision 60). 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 for package and its id for run, screenshot, tap, type, key and scroll; the WaOS app, installed on this machine, for run, and running the program, for the rest; the user's signing key, for package.
  • Provides: the cargo-waos binary: cargo waos new, cargo waos build, cargo waos run, cargo waos package, cargo waos screenshot, cargo waos tap, cargo waos type, cargo waos key and cargo waos scroll.
  • Used by: whoever writes a program, and an agent writing one, which sees and uses it through screenshot and the input commands. The WaOS app itself is an Waft app, built with cargo waft.
cargo waos new <name> [--template winui|cupertino]   # a new program at ./<name>, which run runs and package packages as it is
cargo waos build [-p <package>] [--release]   # the component, at target/waos/<profile>/<package>.wasm
cargo waos run [-p <package>] [--release]     # builds it, and runs it in the WaOS on this machine
cargo waos package [-p <package>]             # its release build, signed, at target/waos/<id>-<version>.wapk, listed in target/waos/index.json
cargo waos screenshot [-p <package>]          # a PNG of each of its windows, as its user sees it, at target/waos/screenshots/<package>/window-<n>.png
cargo waos tap --at <x>,<y> [--window <n>] [-p <package>]                 # a press and release at a point
cargo waos type "<text>" [--window <n>] [-p <package>]                    # text, as typing it enters it
cargo waos key <chord> [--window <n>] [-p <package>]                      # a key or a chord: enter, escape, cmd+s
cargo waos scroll --at <x>,<y> --by <dx>,<dy> [--window <n>] [-p <package>]   # a mouse wheel's scroll at a point

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: by default WinUI's, a WidgetsApp in Fluent's look whose page has a button, in a window sized for a desktop, which works on a phone too; and with --template cupertino, for an app meant for phones alone, a CupertinoApp whose page says hello. 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/winui.rs or templates/cupertino.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, so a new program builds from crates.io as it is written. tests/new.rs builds each template against this workspace instead, so that a change to a template, to the SDK or to Waft is tested before it is published rather than after, when building from crates.io would still build the last release: 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. The published crate leaves the test out, since it needs the workspace.

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:

[package.metadata.waos]
id = "dev.example.triangle"   # reverse-DNS; names the program for good, and what `run` runs it as
name = "Triangle"
icon = "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.

screenshot, tap, type, key and scroll are for the program the package's id names while a run of cargo waos run runs it, started in another shell (the design's decision 60): each connects to the WaOS on this machine, never starting one, sends one Screenshot or Input, and waits for the answer. WaOS refuses a program a run of its own does not run, one the library opened among them, so no tool reads or drives an installed program; and a window the program does not have, a point outside the window, text while nothing in the window is edited and a chord naming no key: each is said on stderr, with the exit status 1. The windows are numbered from 1 in the order they opened; --window names one, the first unless it says otherwise. A point is in the window's pixels, the screenshot's, from its top-left, written <x>,<y>; either number may be negative, as a scroll's --by often is.

screenshot writes the last picture of each of the program's windows as window-<n>.png in target/waos/screenshots/<package>, first taking away the window pictures an earlier screenshot left there, and prints one line for each, its path, its window's number, title and size in pixels: …/window-1.png window 1 "Notes" 1600x1200. A picture is what the program drew in the window, as WaOS shows it, without the composition layers beneath it, a player's video among them; a window that has drawn nothing yet has no file, and a window out of sight, minimized or covered all over, shows its last picture and draws nothing new until it is in sight, which stderr says.

tap presses and releases the mouse's button at the point; scroll turns a mouse wheel there by --by, in pixels, a positive dy scrolling down, towards the content's end. The mouse is one of the run's own, which stays where an input left it, so a tap reaches a control a hover reveals as a user's second click does. type enters the text into what the window edits, as an input method enters what it commits, in place of what is selected; with nothing edited, it is refused, and the text field is tapped first. key presses a key, or a chord of modifiers and a key joined by +, on a US keyboard: cmd, ctrl, alt and shift, then a letter, a digit, one of - = [ ] \ ; ' ` , . /, or enter, escape, tab, space, backspace, delete, up, down, left, right, home, end, pageup, pagedown or f1 to f12, in any case. A key the program leaves goes no further, so a chord that only a menu item of the program's menu bar answers does nothing. Each input returns once the window has shown a picture the program drew after it, so a screenshot taken next shows what it did; when none comes within two seconds, as for an input that changed nothing, or at once for a window out of sight, it returns saying so on stderr, with the exit status 0.

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.