pulsar
EN

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