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

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

```sh
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`.

```text
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`:

```toml
[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.