CLI
pulsar <command>
create <name> scaffold a new app (--template default|js|qml|windows|tray)
install download the native toolchain for this platform (--android for APK)
update update the CLI to the latest release
dev run with hot reload (watch sources)
run run the app in the current directory
build build a single-file executable (--target desktop|android|portable)
doctor check the toolchain (host, bridge, Qt, Bun)
version print the version
help show help
Useful flags and aliases:
| Command | Flags |
|---|---|
create <name> |
--template <t>, --dir <path>, --force |
install |
--version <v>|latest, --slim, --android |
build |
--out <dir> / -o <dir>, --slim, --target, --abi |
run, dev |
env PULSAR_APP (app dir), PULSAR_WATCH (dev sets it) |
init |
legacy alias for create in the current directory |
pulsar build writes dist/<name> — one executable containing the Bun
runtime, your app and (unless --slim) the Qt runtime. Run it directly:
./dist/<name>.
Android
Android runs the QuickJS engine and the QML host in-process, so the app must be
portable: add "targets": ["android"] and "compat": "portable" to
pulsar.json, then:
pulsar build --target android
The first build installs what it needs into PULSAR_HOME (one time, ~2 GB):
JDK 17, the Android SDK/NDK and the Qt for Android bundle with matching host
tools. The result is dist/<name>.apk for arm64-v8a (minSdk 28, targetSdk
35). pulsar install --android performs the same setup without building, and
pulsar check --target android rejects node:/bun: imports before packaging.
net.fetch and streaming net.request work on Android: transport runs in the
native host through QtNetwork. net.websocket needs the Qt WebSockets module,
which the current Android Qt bundle does not ship; calls report
ERR_UNSUPPORTED there (desktop builds are unaffected).
Android dev loop
pulsar dev --android
The first run builds a dev APK (applicationId <id>.dev, label
<name> (dev)) and installs it next to production. The CLI starts a bundle
server, points the app at it through adb reverse, watches the app directory
and restarts the app after an edit (1–2 s). Native inputs (host C++, the
manifest, CMake, the app id) are fingerprinted; when they change the APK is
rebuilt and reinstalled. Flags: --device <serial>, --port <n>,
--no-install, --force-install, --no-keep-awake.
While it runs, another terminal can inspect and drive the app:
pulsar ui # UI tree (JSON)
pulsar state # store snapshot (JSON)
pulsar tap "Add" # press by text, path or id
pulsar type "milk" --into "New task"
pulsar expect "milk" # wait for text
pulsar shot --out shot.png
pulsar tap --at 540,1200 # coordinates (QML apps, system UI)
pulsar swipe 500 1200 500 400 --duration 250
pulsar key back # back, home, enter, ... or a numeric code
pulsar ui --system # uiautomator dump of the foreground screen
pulsar eval "6*7" # evaluate JS in the running QuickJS context
pulsar shot uses adb and captures the whole screen; --window grabs only
the app window over the dev channel. The semantic commands need a JS UI tree,
so they apply to JS-rendered windows, not ui/main.qml apps. pulsar ui --system dumps the foreground window (useful for permission dialogs), and
--at/swipe/key drive the screen by coordinates.
Flow tests can use the host API:
import { connect } from "../../cli/testing.ts"
const app = connect()
await app.tap({ text: "Add" })
await app.expect("count=1")
await app.shot(".pulsar/shots/after.png")
Run it with bun flows/todo.ts while pulsar dev --android is running.
An uncaught error shows a full-screen overlay with a Reload button. While the dev server runs, stack frames are mapped back to your TypeScript sources in the terminal and in the overlay.
Edits reload the app in process (~200 ms on the device): the JS runtime is
recreated and the store is seeded from the previous session. JS windows keep
their view; QML windows are reloaded from ui/main.qml. Because the store is
preserved, changing an initial app.store.set value in code does not take
effect until a full restart (pulsar dev again).