@oniroproject/oniro-app 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,11 +58,13 @@ Install commands are idempotent (they skip when already present); pass `--force`
58
58
 
59
59
  #### `sdk` — manage OpenHarmony SDK installs
60
60
 
61
+ HarmonyOS SDKs are not managed here: they ship inside the HarmonyOS command-line tools (see `cmdtools` below) or DevEco Studio.
62
+
61
63
  | Command | Description |
62
64
  | --- | --- |
63
65
  | `sdk install <version> [--force] [--tmp-dir <path>]` | Download and install an OpenHarmony SDK (e.g. `6.1`). `--tmp-dir` overrides where download/extract temporaries go (see [Temporary space](#temporary-space)). |
64
66
  | `sdk install --from-archive <path> [--force] [--tmp-dir <path>]` | Install an SDK from a local `.tar.gz` instead, such as the [full SDK](#full-sdk). Only the current OS's components are unpacked, and the API level is read from them; an SDK already installed at that level is replaced only with `--force`. |
65
- | `sdk list [--json]` | List known SDK versions; an `*` marks installed ones. |
67
+ | `sdk list [--json]` | List known OpenHarmony SDK versions; an `*` marks installed ones. Then it says HarmonyOS SDKs come with the command-line tools and lists the HarmonyOS SDK a HarmonyOS build would use and where it came from, or names `oniro-app cmdtools install` when there is none. `--json` adds `flavour: "OpenHarmony"` to each release and appends the HarmonyOS SDK as `{ flavour: "HarmonyOS", displayName, version, api, installed, path, source }`, `source` being `cmdtools`, `deveco-studio` or `configured` (ONIRO_HARMONYOS_SDK_PATH). |
66
68
  | `sdk remove <api>` | Remove an installed SDK by API level (e.g. `18`, `20`). |
67
69
 
68
70
  ##### Full SDK
@@ -75,12 +77,12 @@ oniro-app sdk install --from-archive ~/Downloads/version-Master_Version-OpenHarm
75
77
 
76
78
  It lands in `<ONIRO_SDK_ROOT_DIR>/<os>/<api>` like a downloaded release, so projects with that `compileSdkVersion` build against it. Pass `--force` to replace a public SDK of the same API level, or set `ONIRO_SDK_ROOT_DIR` to a separate root to keep both.
77
79
 
78
- #### `cmdtools` — manage the OpenHarmony command-line tools (provides `hvigorw`, `ohpm`, `hdc`, `codelinter`)
80
+ #### `cmdtools` — manage the command-line tools (provides `hvigorw`, `ohpm`, `hdc`, `codelinter` and, in the HarmonyOS edition, the HarmonyOS SDK)
79
81
 
80
82
  | Command | Description |
81
83
  | --- | --- |
82
- | `cmdtools install [--from-zip <path>] [--force] [--tmp-dir <path>]` | Install the tools. `--from-zip` installs from a local archive instead of downloading (required on Windows/macOS — see [Configuration](#configuration)); `--tmp-dir` overrides where download/extract temporaries go (see [Temporary space](#temporary-space)). |
83
- | `cmdtools status [--json]` | Report whether the tools are installed, their version, their flavour (`HarmonyOS` when they bundle `sdk/default/hms`, as the Huawei developer-portal download does, else `OpenHarmony`), and the `hdc` binary every device command uses. Exits non-zero when not installed. |
84
+ | `cmdtools install [--from-zip <path>] [--force] [--tmp-dir <path>]` | Install the tools. `--from-zip` installs from a local archive instead of downloading (required on Windows/macOS — see [Configuration](#configuration)); `--tmp-dir` overrides where download/extract temporaries go (see [Temporary space](#temporary-space)). The new tools are staged and swapped in only once complete: an install that fails (a full disk, say) leaves the existing tools as they were, and a successful one replaces the tools' own entries outright (anything else in the directory stays). Without `--force`, existing tools are left alone and named (`Command-line tools 6.1.1.280 (HarmonyOS 6.1.1, API 24, hvigor 6.24.2) already installed at …`). With it, before the old tools are deleted, it prints what replaces what, e.g. `Replacing command-line tools 5.1.0.840 (HarmonyOS 5.1.0, API 18, hvigor 5.18.5) with 6.1.1.280 (HarmonyOS 6.1.1, API 24, hvigor 6.24.2).`, and says when hvigor changes major version or the new release is older. These tools serve every project on the machine, and newer releases are only on the Huawei developer portal, so keep the old ZIP to go back. |
85
+ | `cmdtools status [--json]` | Report whether the tools are installed and what they are, e.g. `Installed 6.1.1.280 (HarmonyOS 6.1.1, API 24, hvigor 6.24.2)`, then their path, their flavour (`HarmonyOS` when they bundle `sdk/default/hms`, as the Huawei developer-portal download does, else `OpenHarmony`), the SDK they bundle (name, API level, build and path; in the HarmonyOS edition, the SDK HarmonyOS builds use), the hvigor version, and the `hdc` binary every device command uses. `--json` carries the same as `version`, `flavour`, `sdk` (`path`, `displayName`, `platformVersion`, `apiVersion`, `build`) and `tools` (`hvigor`, `ohpm`, `codelinter`, `hstack`, as `version.txt` lists them), with `installed`, `status`, `path` and `hdc`. Exits non-zero when not installed. |
84
86
  | `cmdtools remove` | Delete the configured command-line-tools directory. |
85
87
 
86
88
  #### `emulator` — manage the Oniro emulator (QEMU-based)
@@ -88,20 +90,90 @@ It lands in `<ONIRO_SDK_ROOT_DIR>/<os>/<api>` like a downloaded release, so proj
88
90
  | Command | Description |
89
91
  | --- | --- |
90
92
  | `emulator install [--force] [--tmp-dir <path>]` | Download and install the emulator. `--tmp-dir` overrides where the downloaded ZIP goes (see [Temporary space](#temporary-space)). |
91
- | `emulator start [options]` | Start the emulator via the bundled launcher and detach, so the CLI can exit while it keeps running. |
92
- | `emulator status [--address <host:port>] [--json]` | Report whether the emulator is installed, running, and connected to `hdc`, without starting it. Exits non-zero when not installed. DevEco Studio's HarmonyOS emulator is not covered; it shows up in `devices`. |
93
- | `emulator stop` | Kill running emulator processes. |
94
- | `emulator connect [--address <host:port>]` | Attempt an `hdc` connection (default `127.0.0.1:55555`). |
95
- | `emulator remove` | Delete the configured emulator directory. |
93
+ | `emulator start [options]` | Start the emulator via the bundled launcher and detach, so the CLI can exit while it keeps running. With `--instance <n>` (1..99), start pool instance `n` instead (see [Emulator pool](#emulator-pool)). |
94
+ | `emulator status [--address <host:port>] [--instance <n>] [--json]` | Report whether the emulator is installed, running, and connected to `hdc`, without starting it, then (when pool instances exist) one line per instance, the default emulator as `0`, with its [health](#emulator-pool). `--json` adds an `instances` array to the existing fields; with `--instance`, only that instance's object is printed. Exits non-zero when not installed. DevEco Studio's HarmonyOS emulator is not covered; it shows up in `devices`. |
95
+ | `emulator stop [--instance <n> [--discard]]` | Without `--instance`: kill running emulator processes (every `qemu-system-x86_64`, pool instances included). With `--instance <n>`: stop only that pool instance's QEMU; `--discard` also deletes its overlays, log and state. |
96
+ | `emulator connect [--address <host:port>]` | Attempt an `hdc` connection (default `127.0.0.1:55555`). Restarts the `hdc` server first. |
97
+ | `emulator remove` | Delete the configured emulator directory (pool instances' overlays included). |
96
98
 
97
99
  `emulator start` options:
98
100
 
99
101
  | Flag | Description |
100
102
  | --- | --- |
101
- | `--headless` | Launch headless (VNC + telnet serial, no local window). Required in CI. |
102
- | `--log <path>` | Redirect launcher stdout/stderr to a file (default: discard). |
103
- | `--wait-for-hdc <seconds>` | Block until `hdc` connects, up to N seconds. `0` (default) returns as soon as the launcher spawns. |
104
- | `--connect <host:port>` | Override the launcher's hdc port-forward bind address. Pass `0.0.0.0:55555` on hosts where QEMU refuses to bind `127.0.0.1` (some CI runners). |
103
+ | `--instance <n>` | Pool instance `1..99`. `0` or unset: the default emulator, through `run.sh`/`run.bat`, exactly as before. |
104
+ | `--headless` | Launch headless (VNC + telnet serial, no local window). Required in CI. Pool instances are headless unless `--window`. |
105
+ | `--log <path>` | Redirect launcher stdout/stderr to a file (default: discard; pool instances: `<instance dir>/qemu.log`). |
106
+ | `--wait-for-hdc <seconds>` | Default emulator: block until `hdc` connects, up to N seconds (restarts the `hdc` server). `0` (default) returns as soon as the launcher spawns. |
107
+ | `--wait-ready <seconds>` | Block until the guest reports `bootevent.boot.completed=true`, up to N seconds. Connects with `hdc tconn` only and never restarts the `hdc` server, so other devices stay connected. Exits non-zero on timeout or if QEMU dies (with the log tail). |
108
+ | `--connect <host:port>` | Default emulator: override the launcher's hdc port-forward bind address. Pass `0.0.0.0:55555` on hosts where QEMU refuses to bind `127.0.0.1` (some CI runners). |
109
+ | `--memory <size>`, `--smp <n>` | Pool: guest RAM (default `2048M`) and virtual CPUs (default `2`), smaller than `run.sh`'s 4 GB / 6 CPUs so several instances fit. |
110
+ | `--resolution <WxH>` | Pool: display resolution (default `360x720`). |
111
+ | `--window` | Pool: an SDL window instead of VNC. |
112
+ | `--fresh` | Pool: discard the instance's overlays first (factory reset). Refused while the instance runs. |
113
+ | `--boot-timeout <seconds>` | Pool: a boot that has not completed this long after launch is reported `stuck` (default 300). |
114
+ | `--qemu <path>`, `--accel <kind>` | Pool: the `qemu-system-x86_64` binary (default `QEMU_BIN`, else `PATH`; `qemu-img` is looked up next to it), and the accelerator (`auto`, `kvm`, `hvf`, `whpx`, `tcg`; `auto` picks as `run.sh` does). |
115
+ | `--json` | Pool: print the instance's status as JSON when done. |
116
+
117
+ ##### Emulator pool
118
+
119
+ For parallel work, each item on its own emulator. `emulator start --instance <n>` runs its own QEMU, detached, with the command line `run.sh` uses (q35, KVM, the bundled kernel, ramdisk and kernel arguments, the four virtio disks in the same order, virtio-gpu, user networking), except that:
120
+
121
+ - the disks are qcow2 copy-on-write overlays in `<emulator dir>/instances/<n>/` (`qemu-img create -f qcow2 -F raw -b images/<disk>.img`), so each instance has its own `/data` and the bundled images are only ever read. Overlays persist across `stop`/`start`; `--fresh` or `stop --discard` drops them. A booted instance's overlays take about 30 MB.
122
+ - every port is bound to `127.0.0.1` at the instance's number:
123
+
124
+ | Port | Default emulator (`0`) | Pool instance `n` |
125
+ | --- | --- | --- |
126
+ | hdc (`hdc tconn`, `--device`) | `55555` | `55555+n` |
127
+ | VNC | display `:0` (`5900`) | display `:n` (`5900+n`) |
128
+ | serial console (telnet) | `4444` | `4444+n` |
129
+
130
+ A port that is already taken fails the start, naming it.
131
+
132
+ Address an instance like any TCP device: `oniro-app --device 127.0.0.1:55556 …` or `ONIRO_DEVICE_SERIAL=127.0.0.1:55556`. `emulator start`, `status` and `--wait-ready` connect it with `hdc tconn`; nothing in the pool restarts or kills the `hdc` server, so a USB device and the other instances stay connected. A stopped instance stays listed by `hdc` as `Offline` until the `hdc` server next restarts.
133
+
134
+ Health codes (`state` in `emulator status --json`):
135
+
136
+ | State | Meaning |
137
+ | --- | --- |
138
+ | `stopped` | No QEMU process. |
139
+ | `booting` | QEMU is alive; `hdc` cannot reach the guest yet, or `bootevent.boot.completed` is not `true`. |
140
+ | `ready` | `hdc -t 127.0.0.1:<port> shell param get bootevent.boot.completed` answers `true`. `readyAt` and `bootSeconds` record when. |
141
+ | `hdc-lost` | The instance was `ready` during this boot and `hdc` can no longer reach it (frozen or wedged guest). |
142
+ | `stuck` | QEMU is alive but the boot has not completed `--boot-timeout` seconds after launch. |
143
+
144
+ ```bash
145
+ oniro-app emulator start --instance 1 --wait-ready 300
146
+ oniro-app emulator start --instance 2 --wait-ready 300 --memory 3G --smp 4
147
+ oniro-app emulator status --json # instances: [{instance: 0, state: "stopped"}, {instance: 1, state: "ready", ...}, ...]
148
+ oniro-app --device 127.0.0.1:55557 hdc shell param get const.ohos.apiversion
149
+ oniro-app emulator stop --instance 1 # instance 2 keeps running
150
+ oniro-app emulator stop --instance 2 --discard
151
+ ```
152
+
153
+ On this machine's KVM host (Linux), two instances at the defaults booted side by side to `ready` in about 15 s, each using about 2.3 GB of RAM and under 1 % of a CPU when idle. The pool shares the bundled images with the default emulator, which opens them read-write, so the two cannot run at once: `emulator start --instance <n>` refuses while the default emulator runs, and `emulator start` refuses while a pool instance does. If the default emulator has written to the images since an instance's overlays were made, starting that instance fails and asks for `--fresh`.
154
+
155
+ #### `doctor` — check the machine can build, check, sign and test
156
+
157
+ `doctor [project-dir]` (defaults to the project in the current directory, if any). One line per check, `ok`, `warn`, `fail` or `skip`, with the fix under anything that is not ok; exits 1 when a check fails.
158
+
159
+ | Check | Fails when |
160
+ | --- | --- |
161
+ | `node`, `java` | Node.js is older than 20; no Java on `JAVA_HOME` or `PATH` (its `-version` is recorded). |
162
+ | `sdk` | The SDK the project's `compileSdkVersion` names is not installed (the HarmonyOS SDK for a HarmonyOS project, and its release must match). `--fix` installs it. |
163
+ | `cmdtools` | The command-line tools are missing, or a partial install (`bin/ohpm`, `bin/hvigorw`, `bin/codelinter`, `hvigor/`, `ohpm/`, `codelinter/`, `tool/node`, the SDK toolchains or `version.txt` missing). The version is recorded. |
164
+ | `cmdtools-flavour` | The tools are the other flavour than the project's `runtimeOS`, so `lint` skips its checks. |
165
+ | `arkts-check` | `check arkts` does not run, or runs but misses an error planted in a generated one-file project. |
166
+ | `hdc`, `device` | hdc does not run; no device or emulator is reachable; the device's API is below the project's `compatibleSdkVersion`. A device of the other OS family is a warning. |
167
+ | `signing` | The signingConfig names material that is missing (no signingConfig at all is a warning). |
168
+ | `disk` | Under 1 GB free where SDKs install or the project builds (under 5 GB is a warning). |
169
+
170
+ | Flag | Description |
171
+ | --- | --- |
172
+ | `--json` | Print the report: `ok`, `checks` (`id`, `status`, `detail`, `fix`, `fixed`), `project` (`runtimeOS`, `compileSdkVersion`, `compatibleSdkVersion`, `minApi`), `versions` (`node`, `java`, `cmdTools`, `cmdToolsFlavour`, `sdk`, `hdc`, `deviceApi`), for comparing two machines or two runs. |
173
+ | `--fix` | Install the project's missing OpenHarmony SDK. |
174
+ | `--no-device` | Skip the device check, on a machine that only builds. |
175
+ | `--serial <serial>` | The device to check when several are connected. |
176
+ | `--no-arkts-check` | Skip the `check arkts` self-test. |
105
177
 
106
178
  ### Project lifecycle
107
179
 
@@ -131,7 +203,7 @@ Only needed for HarmonyOS signing; see [HarmonyOS apps](#harmonyos-apps).
131
203
 
132
204
  | Command | Description |
133
205
  | --- | --- |
134
- | `auth login [--timeout <s>]` | Sign in via the browser (default timeout 600s). |
206
+ | `auth login [--timeout <s>] [--port <n>] [--no-browser\|--manual] [--print-url] [--json]` | Sign in via the browser (default timeout 600s). With the browser on another machine: `--manual` and paste the address it ends up on, or `--port` and an `ssh -L` tunnel. See [Signing in from a headless host](#signing-in-from-a-headless-host). |
135
207
  | `auth logout` | Delete the locally stored token. |
136
208
  | `auth status [--json]` | Show the signed-in account; exits non-zero when signed out. |
137
209
  | `auth team list [--json]` | List AGC teams. |
@@ -140,6 +212,8 @@ Only needed for HarmonyOS signing; see [HarmonyOS apps](#harmonyos-apps).
140
212
 
141
213
  `sign [project-dir]` (defaults to the current directory). Requires `java` on `PATH`. Rewrites the `signingConfigs` block of `build-profile.json5`, and points any product that names no `signingConfig` at the generated one (other keys are preserved). The generated profile uses the SDK's built-in development cert (`issuer=pki_internal`) and its validity window — **dev/local builds only, not distribution.** See [Signing apps that need system permissions](#signing-apps-that-need-system-permissions) for `--apl` / `--acls`.
142
214
 
215
+ The last line names what was made: `Signing configs generated (release profile, apl normal; certificate valid until 2049-12-31 12:21 UTC).` With `--harmonyos`, it names the profile's type and expiry, which for AGC's debug profiles is 14 days out: `Signing configs ready for com.example.myapp (profile names 6 device(s), debug, valid until 2026-10-01 11:50 UTC).` `oniro-app app verify <hap>` checks what actually signed a built HAP.
216
+
143
217
  | Flag | Description |
144
218
  | --- | --- |
145
219
  | `--apl <level>` | Ability Privilege Level: `normal` (default), `system_basic`, or `system_core`. |
@@ -179,7 +253,37 @@ for HarmonyOS is refused with a pointer to `--harmonyos`.
179
253
 
180
254
  #### `build` — build the app via `hvigorw`
181
255
 
182
- `build [project-dir]` (defaults to the current directory). Modules build in parallel by default. An OpenHarmony project whose `compileSdkVersion` SDK is not installed fails before anything runs, naming the release to install. When the compiler output shows system-API errors (`'…' is not exported from Kit '…'`), the error adds a hint about the full SDK.
256
+ `build [project-dir]` (defaults to the current directory). Modules build in parallel by default. An OpenHarmony project whose `compileSdkVersion` SDK is not installed fails before anything runs, naming the release to install. A failed build lists its errors on stderr, one per line (`file:line:col error source code: message (rule)`), after hvigor's own output. When the project uses kit symbols that its SDK lacks but a newer installed SDK declares, the error names that API level; when no installed SDK has them (`'…' is not exported from Kit '…'`), it adds a hint about the full SDK, which carries the system APIs. A failed build names the SDK it compiled against (`Built against HarmonyOS 5.1.0 (API 18) at ~/command-line-tools/sdk.`). When the errors are missing properties or names (`Property '…' does not exist on type …`, `Cannot find name '…'`) in code under `oh_modules/`, it adds a hint naming the dependency and that SDK: the dependency may need a newer API level than the SDK has. The hint reads only the compiler messages, not the dependencies' metadata, whose declared `compatibleSdkVersion` can be wrong.
257
+
258
+ A build that signs with material for the other runtime still passes, but warns on stderr (and with a `signing` warning diagnostic in `--json`). hvigor's SignHap signs with whatever the product's signingConfig holds, and the `-signed.hap` fails only at install time. The two cases are: a product with `runtimeOS: "HarmonyOS"` whose signingConfig lacks `type: "HarmonyOS"` (the OpenHarmony material a plain `sign` writes; run `oniro-app sign --harmonyos`), and an OpenHarmony product whose signingConfig has it (run `oniro-app sign`).
259
+
260
+ With `--json`, stdout gets one object whether the build passes or fails, and a failed build still exits 1:
261
+
262
+ ```json
263
+ {
264
+ "ok": false, "exitCode": 255, "durationMs": 7221,
265
+ "failedTask": ":entry:default@CompileArkTS",
266
+ "diagnostics": [
267
+ { "severity": "error", "source": "arkts", "code": "10605008", "rule": "arkts-no-any-unknown",
268
+ "message": "Use explicit types instead of \"any\", \"unknown\"",
269
+ "file": "entry/src/main/ets/pages/Index.ets", "line": 5, "column": 24,
270
+ "task": ":entry:default@CompileArkTS" }
271
+ ],
272
+ "summary": { "errors": 1, "warnings": 0, "reportedErrors": 2, "reportedWarnings": 1 },
273
+ "discoveredHaps": {}, "warnings": [],
274
+ "error": "hvigorw exited with code 255\nBuilt against OpenHarmony 6.0 (API 20) at /home/user/setup-ohos-sdk/linux/20.",
275
+ "sdk": { "runtimeOS": "OpenHarmony", "displayName": "OpenHarmony 6.0", "version": "6.0.0.47", "api": "20",
276
+ "path": "/home/user/setup-ohos-sdk/linux/20" },
277
+ "rawTail": "last 200 lines of build output, colour codes removed"
278
+ }
279
+ ```
280
+
281
+ - `source` is `arkts` (the ArkTS compiler), `hvigor` (configuration and task errors such as `00303060`), `native` (clang and lld, one diagnostic per undefined symbol), `ohpm` (the dependency install before the build; `failedTask` is then `ohpm install --all`), `signing` (a signingConfig for the other runtime, see above; `file` is `build-profile.json5`) or `sdk`. An `sdk` diagnostic carries `api`, the API level the build needs: the SDK the project compiles against is not installed (the object also has `missingSdk: { api, version, expectedPath }`), or a newer installed SDK declares the symbols the build could not find. A `warning` `sdk` diagnostic without `api` is the dependency hint above.
282
+ - `sdk` is the SDK the build compiled against: `runtimeOS`, `displayName`, `version` (`6.1.1(24)` for HarmonyOS, the SDK build for OpenHarmony), `api` and `path` (`DEVECO_SDK_HOME` for HarmonyOS). It is there for a build that passed too, and absent when it cannot be told (an ohpm failure, a missing SDK, no numeric `compileSdkVersion`).
283
+ - `file` is relative to the project when it lies inside it. hvigor prints the ArkTS block twice; each diagnostic is listed once.
284
+ - `summary.reportedErrors` and `reportedWarnings` are hvigor's own `COMPILE RESULT` counts. They can exceed the distinct diagnostics (hvigor counts 3 errors for the two above), so use them as a cross-check, not as the length of the list.
285
+ - A build that passes has `ok: true`, `discoveredHaps` (HAPs grouped by module) and its compiler warnings in `diagnostics`.
286
+ - When the build cannot run at all (no command-line tools, not a project), the object is `{ ok: false, exitCode: 1, error, diagnostics: [] }`.
183
287
 
184
288
  | Flag | Description |
185
289
  | --- | --- |
@@ -190,16 +294,138 @@ for HarmonyOS is refused with a pointer to `--harmonyos`.
190
294
  | `--no-deps` | Skip the automatic `ohpm install --all` when `oh_modules/` is missing. |
191
295
  | `--no-parallel` | Build modules serially. |
192
296
  | `--install-sdk` | Install the missing OpenHarmony SDK the project compiles against, then build. |
193
- | `--json` | Emit the result (discovered HAPs grouped by module, warnings) as JSON. |
297
+ | `--json` | Print the result as one JSON object, described above, whether the build passes or fails. |
298
+
299
+ #### `test` — run the module's ohosTest suite on the device
300
+
301
+ `test [project-dir]` (defaults to the current directory) builds the module's main HAP and its test HAP (`hvigorw … -p module=entry@ohosTest`), installs the main HAP the way `app apply` does and the test HAP after it, runs the suite with `aa test -b <bundle> -m <test module> -s unittest OpenHarmonyTestRunner`, and prints one line per case:
302
+
303
+ ```text
304
+ FAIL ActsAbilityTest > listHasItems (2 ms)
305
+ expect 0 equals 3
306
+ at entry/src/ohosTest/ets/test/Ability.test.ets:26:17
307
+ PASS IndexTest > tapShowsWelcome (656 ms)
308
+ 4 run: 3 passed, 1 failed, 0 errors, 0 ignored.
309
+ ```
310
+
311
+ Exits **0** when every case passed, **1** when a case failed (an assertion, runner code -2) or threw (-1), and **2** when the run itself did not complete: the build failed (its diagnostics are listed, as for `build`), a HAP is missing, the install failed, the runner did not find the test module, no case ran, the run timed out, or there is no device. The test module (`entry_test` in the templates, read from `src/ohosTest/module.json5`) stays installed. Before the run the screen is turned on, a lock screen without a credential is swiped away, and the screen is kept on until the run ends; a lock screen that asks for a credential is reported as a warning, since UI tests cannot get past it.
312
+
313
+ | Flag | Description |
314
+ | --- | --- |
315
+ | `--module <module>` | Module whose `src/ohosTest` suite runs (default `entry`). |
316
+ | `--product <product>` | hvigor product (default `default`). |
317
+ | `--class <filter>` | Run only `Suite`, `Suite#case`, or several of them joined by commas. |
318
+ | `--timeout <ms>` | Per-case timeout passed to the runner (default 15000). |
319
+ | `--run-timeout <ms>` | Wall-clock limit for the whole run (default 600000); `aa test` can hang when the app dies. |
320
+ | `--no-build` | Use the HAPs already built (`<module>/build/<product>/outputs/{default,ohosTest}`). |
321
+ | `--no-wake` | Leave the screen as it is. |
322
+ | `--mode <mode>` / `--no-deps` | As for `build`. |
323
+ | `--device <serial>` | Target device serial. |
324
+ | `--json` | Print the report: `ok`, `outcome` (`passed`, `failed`, `runner-error`), `tests` (`suite`, `name`, `status`, the runner's raw `statusCode`, `durationMs`, `message`, `stack`), `summary` (`run`, `pass`, `fail`, `error`, `ignore`), `resultCode`, `resultMessage`, `reportCode`, `bundle`, `testModule`, `haps`, `log` (the app's hilog lines during the run), `warnings`, `error`, `diagnostics` and `failedTask` (for a failed build), `rawTail`. |
325
+
326
+ `build` and `app install`/`app apply` leave test HAPs (`*-ohosTest-*.hap`) out of the HAPs they pick for a module.
327
+
328
+ #### `check arkts` — ArkTS type check and lint without a build
329
+
330
+ `check arkts [project-dir]` (defaults to the current directory) runs the SDK's own ArkTS checker (ets-loader's `etsStandaloneChecker`: the type check and ArkTS linter that the build's `CompileArkTS` task runs) without hvigor, plus the rules hvigor enforces later in a build that the checker does not see: `@Component`/`@ComponentV2` member decorators, `@ObservedV2` types under V1 state, the `@Entry` root node, statements in `build()`, struct names that collide with built-in components, `Navigation` route builders and route maps, `$r('app.*')` and `$r('sys.*')` resource names, resource directory names, `main_pages.json` pages, and `module.json5` permissions. It checks against the SDK the project compiles with: `<sdk home>/<compileSdkVersion>` for OpenHarmony, the located HarmonyOS SDK (with its `hms/ets` APIs) for a `runtimeOS: "HarmonyOS"` project. It needs no build, no device and no codelinter, so it works whichever flavour the command-line tools are.
331
+
332
+ Prints each diagnostic as `file:line:col severity arkts-check: message (rule)`, then the totals. Exits 0 when clean, 1 on an error, 2 when the checker could not run (the SDK is missing, or the checker crashed): then nothing is known about the code, so do not read it as a pass. `lint --engine arkts` runs the same check.
333
+
334
+ | Flag | Description |
335
+ | --- | --- |
336
+ | `--files <files...>` | Check only these `.ets` files. The project-wide rules (`main_pages.json`, route maps, resource directories, `module.json5`) are then skipped. |
337
+ | `--fix` | Apply the fixes the checker can verify: the compiler's `Did you mean` suggestions, an import path ending in `.ets` or with the wrong depth, a default import of a module with only named exports, a missing `export` (in the declaring file, listed in `alsoModified`), misplaced imports, a `@Component`/`@ComponentV2` decorator that contradicts every member. It re-checks, reverts any file whose error count rose, and lists what it fixed in `fixed`. |
338
+ | `--sdk <dir>` | Check against this SDK directory (the one holding `ets/`) instead of the one `compileSdkVersion` names. |
339
+ | `--json` | Print the report: `ok`, `completed`, `diagnostics` (`severity`, `source: "arkts-check"`, `rule`, `message`, `file`, `line`, `column`, the shape `build --json` uses), `fixed`, `alsoModified`, `summary` (`errors`, `warnings`, `fixed`, `files`, `checkerDiagnostics`), `sdk`, `durationMs`, `error`. |
340
+
341
+ The checker is deveco-cli's `arkts-check.cjs` (MIT, see `NOTICE`), run in a child `node` process.
342
+
343
+ #### `screens` — static page and route inventory
344
+
345
+ `screens [project-dir]` (defaults to the current directory) lists the pages of a project, the routes between them and the placeholder content left in them, read from the sources of every module in `build-profile.json5`. It needs no build, no SDK and no device.
346
+
347
+ - **Pages.** `@Entry` pages listed in `main_pages.json` (named `pages/Index`), `@Entry({ routeName })` named routes, `NavDestination` pages registered in a route map (`module.json5` `"routerMap": "$profile:route_map"`, named by the map's `name`) or in a `.navDestination(builder)` builder that dispatches on the route name (`if (name === 'Detail') { DetailPage() }`, with `==`, `||` and string constants such as `RouteNames.DETAIL`). An `@Entry` struct that `main_pages.json` does not list, and a struct whose `build()` root is `NavDestination` but that nothing registers, are listed as unregistered.
348
+ - **Content nodes.** The component calls a page renders, following its custom components and `@Builder`s (in the same file or elsewhere in the project), counting each `if` branch and each `ForEach`/`LazyForEach`/`Repeat` item template once, and leaving out layout containers (`Column`, `Row`, `Stack`, `Flex`, `RelativeContainer`, `List`/`ListItem`, `Grid`/`GridItem`, `Scroll`, `Tabs`/`TabContent`, `Swiper`, `Navigation`, `NavDestination`, ...) and `Blank`/`Divider`. Builders passed as attributes (`.navDestination(...)`, `.bindSheet(...)`) are not part of the page.
349
+ - **Routes.** Call sites of `pushUrl`, `replaceUrl`, `pushNamedRoute`, `replaceNamedRoute` (`router` or `getUIContext().getRouter()`), `pushPath`, `pushPathByName`, `replacePath`, `replacePathByName`, `pushDestination`, `pushDestinationByName` (`NavPathStack`), and the ability's `loadContent`. The target is read from a literal, a constant, a `{ url }`/`{ name }` object or `new NavPathInfo(...)`, also through a local `const options: RouterOptions = { url: ... }`; any other target is reported with its source text and marked dynamic. `from` is the page whose component holds the call site, else the enclosing struct, class or function; a page's routes out include those of the components it renders.
350
+ - **Placeholders.** `TODO`/`FIXME` comments, string literals and `string.json` values that read like placeholders (`placeholder`, `lorem ipsum`, `coming soon`, `TBD`, `not implemented`, `under construction`, `Hello World`), an empty `build()`, and a page that renders no content node.
351
+
352
+ Prints three tables (pages, routes, placeholders) and any warnings (a page `main_pages.json` lists whose file is missing, a route-map entry whose file or builder is not found, a builder whose route names cannot be read). Exits 0, since it is an inventory, not a gate, and 1 when the project cannot be read. The G2 gate ("every spec screen has a page with at least k content nodes and every spec route has a push call site") is `checkScreensAgainstSpec` in `@oniroproject/core`, a pure function of the `--json` output.
353
+
354
+ | Flag | Description |
355
+ | --- | --- |
356
+ | `--json` | Print the inventory: `pages` (`name`, `module`, `file`, `line`, `component`, `entry`, `kind` (`entry` or `navDestination`), `registered`, `contentNodes`, `routesOut`), `routes` (`from`, `to`, `file`, `line`, `via`, `dynamic`), `placeholders` (`file`, `line`, `text`, `kind`: `comment`, `text`, `resource`, `empty-build`, `no-content`), `warnings`, `modules`, `projectDir`; `{ "error": ... }` when the project cannot be read. |
357
+
358
+ #### `api search` / `api show` — look up the SDK's declarations
359
+
360
+ What the SDK at one API level declares, read from its own typings: the ArkTS API (`ets/api`, `ets/arkts`), the global ArkUI components and decorators (`ets/component`), with the kit that re-exports each symbol (`ets/kits/@kit.*.d.ts`), and the Node-API C headers (`native/sysroot/usr/include/{node_api*,js_native_api*}.h`, `napi/*.h`). Use it to check that an API exists, at the API level the project compiles with, before writing code against it.
361
+
362
+ - `api search <query...>` ranks symbols: every word must match a name (exact name, then prefix, then a camelCase or snake_case part such as `Path` in `pushPathByName`, then a substring), the class or namespace it belongs to, or its doc line. A dotted word matches qualified names (`router.pushUrl`); a leading `@` is ignored (`@Local`). Overloads are one hit. Exits 1 when nothing matches.
363
+ - `api show <symbol>` is an exact lookup by qualified name (`NavPathStack.pushPathByName`, `router.pushUrl`, `Column`, `@Local`, `napi_create_threadsafe_function`), else by dotted suffix (`Preferences.get`). It prints every declaration and exits 0 when the SDK declares it; otherwise it exits 1 and names the other installed SDKs that declare it (with their `@since`) and the closest names this SDK does declare.
364
+ - `api index` builds or refreshes the index and prints its size (for warming a cache in CI).
365
+
366
+ Each hit prints the qualified name, kind (`namespace`, `class`, `interface`, `enum`, `enumMember`, `type`, `function`, `method`, `constructor`, `property`, `variable`, `component`, `decorator`, and for C `macro`, `typedef`, `struct`), kit, `since` (the lowest `@since` of its doc comments, counted from when it became public for a former system API), `@deprecated` with `@useinstead`, `@systemapi`, the declaration text, the first doc line, `file:line`, the module, and how to import it (`import { router } from '@kit.ArkUI'`, or `global, no import` for the ArkUI component declarations).
367
+
368
+ ```
369
+ $ oniro-app api show NavPathStack.getPathStack --api 18
370
+ NavPathStack.getPathStack is not declared in API 18 (5.1.0.107).
371
+ declared in the installed API 23 SDK (@since 19)
372
+ ```
373
+
374
+ The SDK is `--sdk <dir>`, else `<sdk home>/<--api>`, else the one the project's `compileSdkVersion` names (the project in the current directory or `--project`, looked up the way hvigor does), else the newest installed. The index is built on the first use of an SDK (about 3 s for API 23, parsing with the SDK's own TypeScript) and cached under `~/.oniro/cache/api-index/` (`ONIRO_CACHE_DIR`), keyed by the SDK directory and its `oh-uni-package.json` mtimes, so a reinstalled SDK is re-indexed.
375
+
376
+ | Flag | Description |
377
+ | --- | --- |
378
+ | `--api <level>` | The SDK API level to read. |
379
+ | `--sdk <dir>` | An SDK API directory (holding `ets/`) to read instead. |
380
+ | `--project <dir>` | The project whose `compileSdkVersion` picks the SDK (default: the current directory). |
381
+ | `--rebuild` | Rebuild the index even when the cached one is current. |
382
+ | `--kit <name>` | Only symbols this kit exports (`ArkUI` or `@kit.ArkUI`). |
383
+ | `--kind <kinds...>` | `search` only: only these kinds. |
384
+ | `--lang <arkts\|c>` | `search` only: only ArkTS declarations, or only the Node-API headers. |
385
+ | `--limit <n>` | `search` only: at most n hits (default 20). |
386
+ | `--no-deprecated` / `--no-systemapi` | `search` only: leave out `@deprecated` / `@systemapi` symbols. |
387
+ | `--no-other-sdks` | `show` only: on a miss, do not look in the other installed SDKs. |
388
+ | `--json` | `search`: `sdk` (`dir`, `api`, `version`, `source`), `query`, `total`, `hits` (the fields above as `name`, `qualifiedName`, `kind`, `kit`, `import`, `global`, `module`, `since`, `sinceText`, `deprecated`, `deprecatedSince`, `useInstead`, `syscap`, `systemApi`, `file`, `path`, `line`, `signature`, `doc`, `overloads`, `score`). `show`: `sdk`, `symbol`, `found`, `entries`, and on a miss `alsoIn` (`[{ api, since }]`) and `suggestions`. |
389
+
390
+ #### `docs sync` / `docs search` / `docs show` — read the OpenHarmony documentation
391
+
392
+ The application-development documentation of the OpenHarmony release matching the SDK (`en/application-dev` of [gitcode.com/openharmony/docs](https://gitcode.com/openharmony/docs)): usage guides (ArkUI components, navigation, state management, data storage, abilities), kit API prose, the business error codes each kit throws and platform behaviour (lifecycle, permissions, threads). `api search` says whether a symbol exists and with what signature; this says how it is used.
393
+
394
+ - `docs sync` clones the release's tree once: a partial, sparse clone without figures, about 100 MB (a plain clone of the tree is 2 GB), under `<sdkRootDir>/docs/<branch>` (`ONIRO_DOCS_DIR`). The branch comes from the SDK's version (`6.1.0.31` → `OpenHarmony-6.1-Release`, `5.1.0.107` → `OpenHarmony-5.1.0-Release`) or `--ref`. A second `sync` brings the clone up to date. Needs `git`.
395
+ - `docs search <query...>` ranks documents that contain every word (any word when none does) by where the words occur: title, headings, path, body; a camelCase or dotted identifier also matches by its parts (`NavPathStack` → nav, path, stack, weighing less). A document whose title is exactly a query word, or that holds the whole query in a title or heading, ranks higher; the generated API reference a little below the guides; system-API pages and tables of contents below everything. Each hit prints the path, the title, the matching sections with their line numbers and the first line. Exits 1 when nothing matches. The index is built in memory on first use (about 3 s for 5,000 documents) and answers in milliseconds.
396
+ - `docs show <path>` prints one document (a path `search` printed, relative to `en/application-dev`), or one section of it with `--heading` (a heading or a distinctive part of it, case-insensitive: from that heading to the next of the same or a higher level). Image lines and inline HTML are dropped; a long document is cut at `--lines` (default 250) with the list of its sections, so ask for a heading rather than the whole file. Exits 1 when the document or heading does not exist, suggesting close paths or the document's headings.
397
+
398
+ ```
399
+ $ oniro-app docs search --api 23 relationalStore insert
400
+ reference/apis-arkdata/arkts-apis-data-relationalStore-RdbStore.md — Interface (RdbStore)
401
+ sections: "insert" (line 94), "insert10+" (line 178), "insertSync12+" (line 443), …
402
+ Provides APIs for managing data in an RDB store.
403
+ …
404
+ $ oniro-app docs show --api 23 reference/apis-arkdata/errorcode-data-rdb.md --heading 14800011
405
+ ## 14800011 Database File Corrupted
406
+ …
407
+ ```
408
+
409
+ | Flag | Description |
410
+ | --- | --- |
411
+ | `--api <level>` / `--sdk <dir>` / `--project <dir>` | The SDK whose release picks the docs, as for `api search`. |
412
+ | `--ref <branch>` | A docs branch or tag (`OpenHarmony-6.1-Release`, `master`) instead of the SDK-chosen one. |
413
+ | `--limit <n>` | `search` only: at most n hits (default 8). |
414
+ | `--section <dir>` | `search` only: only documents under this subtree (`ui`, `database`, `reference/errorcodes`, `application-models`). |
415
+ | `--heading <text>` | `show` only: only that section. |
416
+ | `--lines <n>` | `show` only: at most n lines (default 250). |
417
+ | `--json` | `sync`: `docs` (`ref`, `dir`, `tree`, `sdk`), `files`, `updated`, `commit`. `search`: `docs`, `query`, `terms`, `total`, `hits` (`path`, `title`, `score`, `headings` `[{ line, text }]`, `snippet`). `show`: `docs`, `found`, `path`, `text`, `truncated`, `lines`, `headings`; on a miss `error` and `suggestions`. |
194
418
 
195
419
  #### `lint` — run the OpenHarmony codelinter
196
420
 
197
- `lint [project-dir]` (defaults to the current directory). Prints each finding as `file:line:col severity message [rule]`, then the totals (`N defects: E errors, W warnings, S suggestions.`). Exits 1 on error-severity findings or when codelinter fails to produce a report; warnings alone exit 0, although codelinter itself exits 1 for them. When codelinter says its report is incomplete, for example because the command-line tools' flavour (see `cmdtools status`) does not match the project's `runtimeOS`, a warning on stderr explains it: treat the counts as a lower bound.
421
+ `lint [project-dir]` (defaults to the current directory). Prints each finding as `file:line:col severity message [rule]`, then the totals (`N defects: E errors, W warnings, S suggestions.`). Exits 1 on error-severity findings or when codelinter fails to produce a report; warnings alone exit 0, although codelinter itself exits 1 for them. When codelinter says its report is incomplete, for example because the command-line tools' flavour (see `cmdtools status`) does not match the project's `runtimeOS`, it exits 2: it skipped checks, so a clean report is not evidence of clean code. Its notices, and a hint when the flavour is the cause, go to stderr.
198
422
 
199
423
  | Flag | Description |
200
424
  | --- | --- |
201
425
  | `--files <globs...>` | Specific files/globs to lint (default: the whole project). |
202
426
  | `--json` | Emit the report as JSON: `findings`, `summary` (codelinter's totals), `notices`, `incomplete`, `code`, and the cleaned `raw` output. |
427
+ | `--allow-incomplete` | Exit 0 rather than 2 for an incomplete report without errors. |
428
+ | `--engine <engine>` | `codelinter` (default), or `arkts` to run `check arkts` instead, with its report and exit codes. |
203
429
 
204
430
  ### Device & app
205
431
 
@@ -216,10 +442,11 @@ These commands target a connected device or emulator over `hdc`. Most accept `--
216
442
  | Command | Description |
217
443
  | --- | --- |
218
444
  | `app install [project-dir \| file.hap] [--hap <path>] [--device <serial>]` | Install a `.hap` via `hdc`: a given file, else the project's newest signed HAP, else its unsigned one (a `*-signed.hap` path that does not exist falls back to its `*-unsigned.hap`). Fails when the bundle manager rejects the HAP; when a target refuses an unsigned HAP, the error gives the bundle manager's reason and the `sign` command to run. |
219
- | `app launch [project-dir] [--module <m>] [--ability <name>] [--device <serial>]` | Launch the app. `--ability` defaults to the module's `mainElement` / first visible ability. |
445
+ | `app launch [project-dir] [--module <m>] [--ability <name>] [--device <serial>] [--smoke]` | Launch the app. `--ability` defaults to the module's `mainElement` / first visible ability. `--bundle <b>` launches an installed app without a project. With `--smoke`, also judge the launch (below). |
220
446
  | `app apply [project-dir] --bundle <b> [options]` | Install a HAP **and verify it took**: fails when the bundle manager rejects it or `bm dump` does not then list the bundle, and handles sign-info mismatch, asset-cache invalidation, and persistent-bundle restart. Uses the newest signed HAP, else the unsigned one, and explains a target's refusal of an unsigned HAP like `app install`. |
221
447
  | `app uninstall <bundle> [--device <serial>]` | Uninstall an app by bundle name. |
222
448
  | `app stop <bundle> [--device <serial>]` | Force-stop an app by bundle name. |
449
+ | `app verify <hap> [--json]` | Verify a HAP's signature and report what signed it, profile expiry included. See [Verifying a HAP's signature](#verifying-a-haps-signature). |
223
450
 
224
451
  `app apply` options:
225
452
 
@@ -232,7 +459,58 @@ These commands target a connected device or emulator over `hdc`. Most accept `--
232
459
  | `--system` | Treat as a persistent/system bundle (refuses to uninstall on a sign-info mismatch). |
233
460
  | `--allow-uninstall` | Permit uninstalling a system bundle on sign-info mismatch (dangerous). |
234
461
  | `--device <serial>` | Target device serial. |
235
- | `--json` | Emit the result (`method`, pre/post-install pid, `cacheCleared`) as JSON. |
462
+ | `--json` | Emit the result (`method`, pre/post-install pid, `cacheCleared`) as JSON; with `--smoke`, plus a `smoke` object. |
463
+ | `--smoke` | After installing, launch the app and judge the launch (below). |
464
+
465
+ **Launch smoke check** (`app launch --smoke`, `app apply --smoke`): wakes the screen and swipes away a lock screen without a credential, launches, waits for the app's process (up to 10 s), waits `--smoke-delay`, takes a screenshot, and checks the process is still the same pid. The verdict fails, and the command exits 1, when the process never started or died, a crash was recorded since the launch (read with `logs crash`'s faultlog reader, which waits up to 15 s for the faultlog of an app that died), or the screen is blank: the app area (status and navigation bars cropped off) is a flat frame by perceptual hash with little variance, or 99.8 % one colour — the deveco-cli smoke-inspector pattern. `--json` prints it (`app launch` as `{ bundle, smoke }`):
466
+
467
+ ```json
468
+ { "smoke": { "passed": false, "failures": ["the screen is blank"], "pid": 15446, "alive": true,
469
+ "blankScreen": true, "blankScore": 1, "blank": { "phashDistance": 0, "stdDev": 0, "background": 255, … },
470
+ "screenshot": "/tmp/oniro-smoke-com.example.app-1762342512000.jpeg", "crash": null, "crashChecked": true, "delayMs": 2000 } }
471
+ ```
472
+
473
+ `blankScore` is the share (0–1) of the app area in its dominant colour: a "Hello World" page scores about 0.977, an empty page 1. `crash` is the `logs crash --json` verdict of the newest fault since the launch (`js_crash`, `cpp_crash`, `app_freeze`, with its reason and project frame), or `null`; the text line names it, e.g. `js_crash (RangeError) at entry/src/main/ets/pages/Index.ets:8`.
474
+
475
+ | Flag | Description |
476
+ | --- | --- |
477
+ | `--smoke-delay <ms>` | Wait between the process appearing and the screenshot (default `2000`). |
478
+ | `--smoke-screenshot <file>` | Where to write the screenshot (default: `oniro-smoke-<bundle>-<time>.jpeg` in the system temp dir). |
479
+
480
+ #### Verifying a HAP's signature
481
+
482
+ `app verify <hap>` runs the SDK's `hap-sign-tool.jar verify-app` on the HAP (from an installed
483
+ OpenHarmony SDK, else the HarmonyOS SDK, else the command-line tools; it needs `java`), then reads
484
+ the certificate chain and provisioning profile the HAP carries. No device is needed.
485
+
486
+ ```console
487
+ $ oniro-app app verify entry/build/default/outputs/default/entry-default-signed.hap
488
+ …/entry-default-signed.hap: valid
489
+ signature: valid
490
+ signer: HarmonyOS, issued by AppGallery Connect (chains to Huawei CBG Root CA G2)
491
+ chain: Jane Dev(1234),Development [leaf, until 2026-10-01 11:50 UTC]
492
+ Huawei CBG Developer Relations CA G2 DRE [intermediate, until 2030-08-16 16:23 UTC]
493
+ Huawei CBG Root CA G2 [root, until 2049-03-16 03:04 UTC]
494
+ profile: debug, issuer app_gallery
495
+ bundle: com.example.myapp
496
+ apl: normal
497
+ app-feature: hos_normal_app
498
+ devices: 6
499
+ validity: 2026-09-17 12:00 UTC to 2026-10-01 11:50 UTC, 6 days left
500
+ note: a debug profile installs only on the devices it names
501
+ ```
502
+
503
+ It reports whether the signature is valid; the chain, leaf first, and its root; whether the
504
+ material is OpenHarmony development material (the SDK's certificate, profile issuer `pki_internal`),
505
+ which only OpenHarmony devices accept, or a HarmonyOS certificate from AppGallery Connect; and the
506
+ profile's type, bundle name, apl, app-feature, device count and validity. A HAP is **invalid** when
507
+ it is unsigned, hap-sign-tool rejects its signature, a certificate or (for AGC material) the
508
+ profile has expired, the profile names another certificate or bundle than the HAP's, or a HAP
509
+ built for HarmonyOS carries OpenHarmony development material. The SDK's development profile
510
+ carries a fixed validity window that ended in 2023, which OpenHarmony devices do not enforce, so
511
+ that is only a note. `--json` emits every field (dates as ISO strings).
512
+
513
+ Exit codes: `0` valid, `1` invalid, `2` could not verify (no HAP, no `java`, no `hap-sign-tool.jar`).
236
514
 
237
515
  #### `file` — transfer files to/from the device
238
516
 
@@ -295,6 +573,39 @@ owner-only permissions where the host OS supports POSIX modes.
295
573
  | `--redact` | Redact text evidence; create the archive only from the redacted copy. |
296
574
  | `--json` | Emit bundle paths and completion status as JSON. |
297
575
 
576
+ #### `logs crash` — what the last crash was, and where
577
+
578
+ ```bash
579
+ oniro-app app launch && oniro-app logs crash --since 30 --wait 15000 --json
580
+ ```
581
+
582
+ Fetches the newest faultlog of an app and classifies it: `cpp_crash` (a native signal), `js_crash` (an uncaught
583
+ ArkTS/JS error), `app_freeze` (`THREAD_BLOCK_6S`, `APP_INPUT_BLOCK`, `LIFECYCLE_TIMEOUT`, or a service's
584
+ `SERVICE_BLOCK`), `sample_stack` or `unknown`, each with a `confidence`. The bundle defaults to the project in the
585
+ current directory, else the newest fault of any process. Faultlogs are listed with
586
+ `hidumper -s 1201 -a '-p Faultlogger -LogSuffixWithMs'` and read with `-f <name>`. Some builds never turn an app's
587
+ native crash into a faultlog (hiview logs `Fail to add fault log … is not processdump`); the command then reads
588
+ faultloggerd's record in `/data/log/faultlog/temp/cppcrash-<pid>-<ms>.json` instead and says so in `notes`. The
589
+ whole log is saved under `--output`.
590
+
591
+ The verdict (`--json`) has `crashed`, `kind`, `subtype` (the freeze event), `confidence`, `bundle`, `pid`, `tid`,
592
+ `time` (device local), `reason`, `topFrame`, `projectFrame` (the first frame in the project's own code), `file`, `line`
593
+ and `column` of the first project ArkTS frame (for a native crash, the ArkTS code that called into it),
594
+ `jsError {name, message, stack}`, `stack` (the faulting thread's frames as printed), `nativeFrames` (`index`, `pc`,
595
+ `library`, `symbol`, `buildId`, `area`), `jsFrames`, `binderChain` and `deadlocks` (freeze), `notes`, `name`, `path`
596
+ (on the device) and `rawPath` (the saved copy). Without a fault it prints `{ "crashed": false, "kind": null, … }`.
597
+ The command exits 0 either way: a gate reads `crashed`.
598
+
599
+ | Flag | Description |
600
+ | --- | --- |
601
+ | `--bundle <bundle>` | App to look for (default: the current project's `bundleName`, else any process). |
602
+ | `--since <s\|ISO>` | Only faults from the last N seconds by the device clock, or since an ISO 8601 time. |
603
+ | `--wait <ms>` | Keep looking this long when there is no fault yet (hiview writes it a few seconds after the process dies). |
604
+ | `--path <file>` | Analyse a local faultlog or `.json` crash record instead of fetching one. |
605
+ | `-o, --output <directory>` | Where to save the fetched log (default `~/.oniro/faultlogs`). |
606
+ | `--device <serial>` | Target device serial. |
607
+ | `--json` | Print the verdict as JSON. |
608
+
298
609
  #### `hdc` — run the bundled hdc
299
610
 
300
611
  `hdc [args...]` runs the `hdc` binary oniro-app itself uses (the one `cmdtools status` shows), with stdio passed through, so it needs nothing on `PATH`. Device commands are addressed at `--device` or `ONIRO_DEVICE_SERIAL` unless the arguments start with `-t`. `hdc --path` prints the binary's location. The exit code is hdc's own, which is 0 even for a `[Fail]` line.
@@ -315,7 +626,7 @@ oniro-app hdc --device 127.0.0.1:5555 shell aa dump -l
315
626
 
316
627
  #### `wait` — wait for a device condition
317
628
 
318
- Specify exactly one of `--log`, `--boot`, `--bundle`, or an element selector (`--target` / `--target-id` / `--target-type`).
629
+ Specify exactly one of `--log`, `--boot`, `--bundle`, or an element selector (`--target` / `--target-id` / `--target-type`, or `--selector`).
319
630
 
320
631
  | Flag | Description |
321
632
  | --- | --- |
@@ -326,6 +637,8 @@ Specify exactly one of `--log`, `--boot`, `--bundle`, or an element selector (`-
326
637
  | `--domain <domain>` | With `--log`: hilog domain filter (e.g. `0xD003900`). |
327
638
  | `--target <text>` | Resolve when an uncovered element whose text, description, or placeholder matches is on screen (case-insensitive; exact matches win, else substring). |
328
639
  | `--target-id <id>` / `--target-type <type>` | Match the element by id/key, or restrict it to a component type (e.g. `Button`). |
640
+ | `--selector <expr>` | Instead of `--target*`: an attribute selector or `xpath=` expression (see [Selectors and windows](#selectors-and-windows)). With a `[n]` suffix, waits for that match. |
641
+ | `--window <id>` | With a selector: only look in this window; while it is not on screen nothing matches. |
329
642
  | `--gone` | With a selector: resolve when no uncovered element matches any more. |
330
643
  | `--interval <ms>` | With a selector: poll interval (default `500`; each poll is a layout dump). |
331
644
  | `--timeout <ms>` | Max wait (default `30000`). |
@@ -346,7 +659,7 @@ Specify exactly one of `--log`, `--boot`, `--bundle`, or an element selector (`-
346
659
 
347
660
  #### `screenshot` — capture the screen as a JPEG
348
661
 
349
- Default: writes the full-resolution raw JPEG. `--grid` reproduces the old MCP screenshot (downscaled + grid overlay). `--crop` / `--target` write a full-resolution crop of one region or element — detail where it is needed without the cost of a full-resolution screenshot. `--contact-sheet` captures a burst (default 8 frames) into one tiled sheet and prints per-frame diffs (0..1) on stdout — one image instead of N, pointing you at the frame where something changed.
662
+ Default: writes the full-resolution raw JPEG. `--grid` reproduces the old MCP screenshot (downscaled + grid overlay). `--crop` / `--target` / `--selector` / `--window` write a full-resolution crop of one region, element or window — detail where it is needed without the cost of a full-resolution screenshot. `--contact-sheet` captures a burst (default 8 frames) into one tiled sheet and prints per-frame diffs (0..1) on stdout — one image instead of N, pointing you at the frame where something changed. `--diff <baseline.jpeg>` captures the screen (to `--output`) and compares it with a baseline.
350
663
 
351
664
  | Flag | Description |
352
665
  | --- | --- |
@@ -355,40 +668,114 @@ Default: writes the full-resolution raw JPEG. `--grid` reproduces the old MCP sc
355
668
  | `--max-dim <px>` | Longest-side cap for `--grid` / `--crop` / `--target` / `--contact-sheet` (256–4096, default `1024`). On its own, downscales without a grid. |
356
669
  | `--crop <x1,y1,x2,y2>` | Crop to this region, given as 0–1 fractions of the screen (as in `dump layout` and the grid). |
357
670
  | `--target <text>` | Crop to the element whose text, description, or placeholder matches, plus a small margin. Also `--target-id`, `--target-type`, `--target-index`. |
671
+ | `--selector <expr>` | Crop to the element this selector picks, like `--target` (see [Selectors and windows](#selectors-and-windows)). |
672
+ | `--window <id>` | Crop to this window; with `--target*` / `--selector`, only look for the element in it. |
358
673
  | `--contact-sheet` | Capture a burst and composite it into one tiled image with per-frame change diffs. |
359
674
  | `--burst <count>` | Capture N frames; writes `<base>-<i><ext>` (or sets the `--contact-sheet` frame count). |
360
675
  | `--interval <ms>` | Delay between burst frames (default `50`). |
361
- | `--json` | With `--contact-sheet`, emit the full result object as JSON on stdout. |
676
+ | `--diff <baseline.jpeg>` | Capture and compare with this screenshot: pHash and dHash distance (0–64 bits), `pixelChangeRatio` (share of pixels that moved more than 24 of 255 grey levels), `meanDiff` (the contact sheet's change metric), and `same` (both hashes within 4 bits, under 1 % of pixels changed). Images of different sizes are scaled to a common one (`sizeMismatch` flags a different aspect). Exits 0 either way. |
677
+ | `--json` | With `--contact-sheet` or `--diff`, emit the full result object as JSON on stdout. |
362
678
  | `--device <serial>` | Target device serial. |
363
679
 
680
+ ```text
681
+ $ oniro-app screenshot --diff before.jpeg -o after.jpeg
682
+ different: pHash 9/64, dHash 5/64, 2.47% of pixels changed, mean change 0.0072
683
+ ```
684
+
364
685
  #### `dump` — dump device state
365
686
 
366
687
  | Command | Description |
367
688
  | --- | --- |
368
- | `dump [layout]` | Dump the on-screen UI layout (the default and currently only target) to stdout: a pruned, 0–1-normalized JSON tree, or one line per element with `--flat`. |
689
+ | `dump [layout]` | Dump the on-screen UI layout (the default target) to stdout: a pruned, 0–1-normalized JSON tree, or one line per element with `--flat`. |
690
+ | `dump windows` | List every window from `hidumper -s WindowManagerService`: id (what `--window` takes), name, pid, type (1 = an app's main window), z-order (-1 = not shown), rectangle, and which one has focus. `--json` for JSON. |
369
691
 
370
692
  | Flag | Description |
371
693
  | --- | --- |
372
694
  | `--bundle <bundle>` | Filter the layout to a single window/bundle. |
373
695
  | `--flat` | One line per labelled or interactive element, grouped by window: type, label, `#id`, center `c=x,y` (0–1 fractions), and flags (`click`, `focus`, `disabled`, `covered`, …). The soft keyboard collapses to one `[keyboard …]` line. |
374
- | `--raw` | Return the unpruned tree (no keyboard detection; not combinable with `--flat`). |
696
+ | `--raw` | Return the unpruned tree (no keyboard detection; not combinable with `--flat`, `--summary`, `--diff` or `--selector`). |
697
+ | `--summary` | Per window, and in total: `elements`, `content` (text, description, placeholder or image), `clickable` and `text` node counts (JSON). |
698
+ | `--diff <baseline.json>` | Compare the live layout with one saved by `dump layout > baseline.json` (JSON). Elements pair within a window by id, then key, then label and type, then type with overlapping boxes; each pair reports `by`, its `iou` (boxes are 0–1 fractions, so layouts from different resolutions compare) and `changed` fields. Unpaired ones are listed as `missing` and `extra`, and `similarity` (0–1) is the sum of pair IoUs (halved when a field changed) over pairs plus missing plus extra. Exits 0 either way. |
699
+ | `--selector <expr>` | List only the elements matching this expression (see [Selectors and windows](#selectors-and-windows)), numbered with the index its `[n]` suffix picks; JSON `{ display, selector, matches }`, or one line each with `--flat`. Soft-keyboard keys are left out, as `input` and `wait` leave them out. |
700
+ | `--window <id>` | Only this window (`[window <id> …]` in `--flat`, or `dump windows`). The whole screen is still dumped, so an overlay in another window still marks elements `covered`. |
375
701
  | `--device <serial>` | Target device serial. |
376
702
 
377
- The pruned output marks elements as `covered` when a later layer with a visible background (a dialog scrim, an overlay, the keyboard) paints over their center, so a tap there would land on that layer instead. A fully transparent layer that only intercepts touches is not detected. Placeholder text is reported as `hint`, and disabled components as `disabled`. The JSON result also carries `keyboard: { shown, bundle, b }`.
703
+ The pruned output marks elements as `covered` when a later layer with a visible background (a dialog scrim, an overlay, the keyboard) paints over their center, so a tap there would land on that layer instead. A fully transparent layer that only intercepts touches is not detected. Placeholder text is reported as `hint`, disabled components as `disabled`, and components with `visible: false` as `hidden`. The node where a window starts carries its `windowId`. The JSON result also carries `keyboard: { shown, bundle, b }`.
704
+
705
+ ```text
706
+ $ oniro-app dump layout > before.json
707
+ $ oniro-app input --type click --target "Hello World" # the text becomes "Welcome"
708
+ $ oniro-app dump layout --diff before.json
709
+ layout similarity 0.978: 28 matched (1 moved, 1 changed), 0 missing, 0 extra. # stderr; the JSON follows on stdout
710
+ { "similarity": 0.978, "matched": [ …, { "by": "id", "baseline": { "id": "HelloWorld", "text": "Hello World", … },
711
+ "current": { "id": "HelloWorld", "text": "Welcome", … }, "iou": 0.766, "changed": ["text"] } ], "missing": [], "extra": [], … }
712
+ ```
378
713
 
379
714
  ```text
380
715
  $ oniro-app dump layout --flat
381
716
  # display 1080x2400; c = element center as 0-1 fractions (input --fx/--fy), or act by label with input --target "<text>"
382
- [window com.example.app]
717
+ [window 92 com.example.app]
383
718
  Text "Sign in" c=0.500,0.180
384
719
  TextInput hint="you@example.com" c=0.500,0.320 click focus
385
720
  Button "Sign in" c=0.500,0.520 click
386
- [keyboard dev.patrickgold.florisboard] up — dismiss with input --type hideKeyboard
721
+ [keyboard 41 dev.patrickgold.florisboard] up — dismiss with input --type hideKeyboard
722
+
723
+ $ oniro-app dump layout --selector 'type=Button,text*=Sign' --flat
724
+ # display 1080x2400; 2 element(s) match "type=Button,text*=Sign"; append [n] to the selector to act on one
725
+ [0] Button "Sign in" #signIn c=0.361,0.214 click (window 92 com.example.app)
726
+ [1] Button "Sign up" #signUp c=0.631,0.214 click (window 92 com.example.app)
727
+ ```
728
+
729
+ #### Selectors and windows
730
+
731
+ `input`, `wait`, `screenshot` and `dump layout` take `--selector <expr>` as an alternative to `--target` / `--target-id` / `--target-type`, which stay the default and keep their behaviour. A selector is exact where `--target` is forgiving: values compare case-sensitively, and every clause must hold. It is evaluated over the same pruned tree `dump layout` prints, so what you see there is what you can select.
732
+
733
+ **Attribute selectors** are comma-separated `key<op>value` clauses, optionally followed by an index:
734
+
735
+ | Keys | |
736
+ | --- | --- |
737
+ | `text`, `desc` (or `description`), `hint`, `id`, `key`, `type` | Strings as `dump layout` shows them. `key` falls back to `id`; `type` is the component type (`Button`, `TextInput`, `Column`, …). |
738
+ | `enabled`, `focused`, `checked`, `selected`, `visible`, `clickable`, `longClickable`, `checkable`, `scrollable` | `true` or `false`. `visible` means drawn and not `covered` by an overlay. |
739
+
740
+ | Operator | Matches when the value… |
741
+ | --- | --- |
742
+ | `=` | equals |
743
+ | `*=` | contains |
744
+ | `^=` | starts with |
745
+ | `$=` | ends with |
746
+ | `/=` | matches a JavaScript regular expression; flags after a slash (`text/=^sign/i`) |
747
+
748
+ Spaces inside a bare value are kept (`text=Sign in`); quote a value that holds `,`, `[`, a quote or edge spaces (`text="a, b"`, `id/="^item[0-9]$"`), with `\` escaping inside quotes. A key may appear once. An unknown key is an error, not an empty match.
749
+
750
+ The `[n]` suffix picks the n-th match, 0-based, in paint order: the list `dump layout --selector` prints. `[-1]` or `[last]` is the last one. The picked element must be on screen and not covered. Without an index, the selector picks the way `--target` does: covered elements are skipped, clickable matches preferred, a label inside a clickable match counts as that match, and more than one candidate is an error listing them. `--target-index` does not combine with `--selector`.
751
+
752
+ **XPath** (`xpath=<expr>`) runs over the same tree, each element named after its component type (`node` when it has none) and carrying the keys above as attributes. The subset is deliberately small, and anything outside it is a parse error rather than a wrong answer:
753
+
754
+ | Supported | Example |
755
+ | --- | --- |
756
+ | `//` (any depth) and `/` (child) steps, from the top | `//Row/Button`, `/node/root/Text` |
757
+ | `*`, `.` and `..` | `//Column[@id="items"]/*`, `//Text[@id="b"]/..` |
758
+ | `[@attr="v"]`, `[@attr!="v"]`, `[@attr]` (has it) | `//Button[@enabled="false"]` |
759
+ | `contains`, `starts-with`, `ends-with`, `not`, `text()` (same as `@text`) | `//Button[contains(@text,"Sign")]` |
760
+ | `and`, `or`, parentheses | `//*[@id="ok" or @id="cancel"]` |
761
+ | `[n]` (1-based), `[last()]`, `[position()<3]` | `//Column/Text[2]` |
762
+ | `(path)[n]`: the n-th match of the whole path | `(//Button)[1]` |
763
+
764
+ As in XPath, `[n]` counts among the step's matches under one parent: `//Button[1]` is every Button that is its parent's first, while `(//Button)[1]` is the first Button on screen. Not supported: axis names (`child::`), unions (`|`), attribute steps (`/@text`), paths inside predicates, and other functions.
765
+
766
+ **`--window <id>`** restricts dump, selection and screenshot to one window. Window ids are the `WinId` column of `hidumper -s WindowManagerService -a -a`: `dump windows` lists all of them, `dump layout --flat` shows the on-screen ones in its `[window <id> <bundle>]` headers, and the JSON tree carries `windowId` on each window's root. The layout is still dumped whole and then filtered, so overlays from other windows still count; `dump layout` and `input` fail with the list of on-screen windows when the id is not among them. Window ids come from uitest's `hostWindowId` attribute, which this was verified with on an API 26 device; deveco-cli notes that uitest's own `dumpLayout -w` / `-d` flags need API 20 or newer, and `--window` does not use them.
767
+
768
+ ```bash
769
+ oniro-app input --type click --selector 'type=Button,text*=Sign[1]' --window 92
770
+ oniro-app wait --selector 'xpath=//Text[@id="status" and contains(@text,"Signed")]' --timeout 5000
771
+ oniro-app wait --selector 'id=spinner' --gone
772
+ oniro-app screenshot --selector 'xpath=//Column[@id="items"]' -o items.jpeg
773
+ oniro-app dump layout --window 92 --flat
387
774
  ```
388
775
 
389
776
  #### `input` / `gesture` — inject UI input
390
777
 
391
- `input` injects a single event via `uitest uiInput`. Give the start point one way: an on-screen element (`--target`), 0–1 fractions of the display (`--fx`/`--fy`, as read from `dump layout` or the screenshot grid), or pixels (`--x`/`--y`).
778
+ `input` injects a single event via `uitest uiInput`. Give the start point one way: an on-screen element (`--target`, or `--selector`), 0–1 fractions of the display (`--fx`/`--fy`, as read from `dump layout` or the screenshot grid), or pixels (`--x`/`--y`).
392
779
 
393
780
  | Flag | Description |
394
781
  | --- | --- |
@@ -396,6 +783,8 @@ Button "Sign in" c=0.500,0.520 click
396
783
  | `--target <text>` | Act on the center of the element whose text, description, or placeholder matches (case-insensitive; exact matches win, else substring). Covered elements and soft-keyboard keys are skipped and clickable ones preferred; if the keyboard hides the element, it is dismissed first. |
397
784
  | `--target-id <id>` / `--target-type <type>` | Match by id/key, or restrict to a component type (e.g. `Button`, `TextInput`). |
398
785
  | `--target-index <n>` | Pick the n-th (0-based) of several matching elements; without it, an ambiguous match fails and lists the candidates. |
786
+ | `--selector <expr>` | Instead of `--target*`: act on the element an attribute selector or `xpath=` expression picks (see [Selectors and windows](#selectors-and-windows)). |
787
+ | `--window <id>` | Only look for the element in this window; fails when the window is not on screen. |
399
788
  | `--fx <fraction>` / `--fy <fraction>` | Start point as 0–1 fractions of the display. |
400
789
  | `--fx2 <fraction>` / `--fy2 <fraction>` | End point as fractions (swipe / drag / fling). |
401
790
  | `--x <px>` / `--y <px>` | Start point in pixels. |
@@ -413,6 +802,7 @@ Button "Sign in" c=0.500,0.520 click
413
802
  oniro-app input --type inputText --target "you@example.com" --text 'me@example.com'
414
803
  oniro-app input --type inputText --target-id password --clear --text 'secret' --hide-keyboard
415
804
  oniro-app input --type click --target "Sign in" --target-type Button
805
+ oniro-app input --type click --selector 'type=Button,id=signIn'
416
806
  oniro-app input --type swipe --fx 0.5 --fy 0.8 --fx2 0.5 --fy2 0.3
417
807
  ```
418
808
 
@@ -429,11 +819,235 @@ oniro-app input --type swipe --fx 0.5 --fy 0.8 --fx2 0.5 --fy2 0.3
429
819
  oniro-app gesture --waypoints '[{"x":200,"y":600,"t":0},{"x":200,"y":200,"t":400}]'
430
820
  ```
431
821
 
822
+ #### `replay` — run a UI flow and report where it diverged
823
+
824
+ `replay <flow.json>` runs a [flow file](#flow-files), written by hand or by `record`, on the device. For each step it waits for the step's `selector` (as `wait --selector` does), acts on that element (as `input --selector` does: covered elements are skipped, the soft keyboard is dismissed when it hides the target), pauses `waitMs`, then waits for the step's `expect`. It stops at the first step that fails and reports why:
825
+
826
+ | `reason` | Meaning |
827
+ | --- | --- |
828
+ | `not-found` | The step's element did not appear within the step timeout (for a `wait` step with `gone`, it did not go away). |
829
+ | `expect-not-found` | The step ran, but its `expect` element did not appear. |
830
+ | `wrong-screen` | The flow's `bundle` has no window on screen (it crashed, another app or the lock screen is in front). Overrides the two above. |
831
+ | `ambiguous` | Several elements match the selector; make it more specific or append `[n]`. |
832
+ | `covered` | The element is there but under an overlay. |
833
+ | `action-failed` | The input could not be sent. |
834
+
835
+ The report adds a screenshot of the screen at that moment, the app's elements as `dump layout --flat` would list them (or every element when the app is not on screen), and the elements that match all but one clause of the selector (`near match: Button "Next" #continueBtn (differs in text=Continue)`), which is usually the renamed element.
836
+
837
+ | Flag | Description |
838
+ | --- | --- |
839
+ | `--from <i>` / `--to <i>` | Run only steps `i` to `j` (0-based, inclusive). |
840
+ | `--step-timeout <ms>` | How long to wait for a step's selector and its `expect` (default `10000`); a step's `timeoutMs` overrides it. |
841
+ | `--screenshots <dir>` | Save a screenshot after every step (`step-<i>.jpeg`) and the divergence screenshot (`failed-step-<i>.jpeg`) here. Without it the divergence screenshot goes to a new directory under the system temp dir. |
842
+ | `--fallback` | When a step's selector is not found, act on its `fallback` point instead of stopping (the step is reported with `usedFallback`). |
843
+ | `--launch` | Force-stop and start the flow's `bundle` (its `ability`, else the main ability) before the first step. |
844
+ | `--no-wake` | Leave the screen as it is. By default it is turned on, a lock screen without a credential is swiped away, and it is kept on during the run (`power-shell timeout`, restored afterwards). |
845
+ | `--device <serial>` | Target device serial. |
846
+ | `--json` | Print the report: `ok`, `outcome` (`passed`, `diverged`, `error`), `range`, `steps` (`index`, `action`, `selector`, `ok`, `durationMs`, `target`, `usedFallback`, `screenshot`), `failedStep`, `reason`, `message`, `selector`, `screenshot`, `layout` (`windows`, `appOnScreen`, `lockScreen`, `keyboard`, `elements`, `elementCount`), `nearMatches`, `error`, `warnings`. |
847
+
848
+ Exit codes: `0` every step ran, `1` the flow diverged, `2` it could not run (the flow file is invalid, a step range is out of bounds, no device, the lock screen needs a credential, `--launch` failed).
849
+
850
+ ```bash
851
+ oniro-app replay login.json --launch
852
+ oniro-app replay login.json --from 3 --step-timeout 5000 --screenshots shots/ --json
853
+ ```
854
+
855
+ ```text
856
+ [0] wait type=Text,text=Flow probe ok 0.9 s
857
+ [1] inputText type=TextInput,hint=Your name ok 2.6 s
858
+ [2] click type=Button,text=Continue FAILED 4.2 s
859
+ Diverged at step 2 (not-found): No element matching type=Button,text=Continue appeared within 4000 ms.
860
+ screenshot: /tmp/oniro-replay-N0vlTn/failed-step-02.jpeg
861
+ near match: Button "Next" #continueBtn at 0.500,0.305 (differs in text=Continue)
862
+ on screen in com.example.flowprobe (3 element(s), keyboard up):
863
+ Text "Flow probe" #title at 0.500,0.169
864
+ TextInput "Ann" #nameInput at 0.500,0.235
865
+ Button "Next" #continueBtn at 0.500,0.305
866
+ ```
867
+
868
+ #### `record` — record a flow from what is done on the device
869
+
870
+ `record --out flow.json` runs uitest's own recorder (`uitest uiRecord record -l`) until Ctrl+C or `--for <ms>`, and turns what it logged into a flow file for `replay`. Act on the device by hand, or with `input` by coordinates (uitest records injected events too). Each touch becomes a step on the element under the finger in the layout on screen at that moment, with the most specific stable selector for it (`type` plus a unique `id`, `key`, `text`, `desc` or `hint`, `[n]` only when needed; exact `=` clauses), the touch point as `fallback`, and as `expect` an element that appeared after it (ids first, then new texts). Taps on the soft keyboard and typed keys collapse into one `inputText` step on the focused field, with the text read off the field afterwards; other keys, Back and Home become `keyEvent` steps.
871
+
872
+ | Flag | Description |
873
+ | --- | --- |
874
+ | `--out <file>` | **Required.** Where to write the flow. |
875
+ | `--for <ms>` | Stop after this long instead of waiting for Ctrl+C. |
876
+ | `--bundle <bundle>` | The app being recorded (default: the foreground app when recording starts, as the recorder reports it). |
877
+ | `--raw <dir>` | Also keep uitest's raw recording here: `record.csv`, its layout dumps, the recorder console, and `recording.json` (display, touch and layout times, the layouts before and after). For bug reports. |
878
+ | `--no-wake` | Leave the screen as it is (by default it is woken, unlocked when there is no credential, and kept on while recording). |
879
+ | `--device <serial>` | Target device serial. |
880
+ | `--json` | Print `{ out, steps, events, warnings }`. |
881
+
882
+ What uitest's recorder does on OpenHarmony (checked on API 26), and how `record` deals with it:
883
+
884
+ - `uitest uiRecord record [-W true|false] [-l] [-c true|false]` writes one JSON object per line (despite the name) to `/data/local/tmp/record.csv`; `uitest uiRecord read` prints it. Pointer events carry `OP_TYPE` (`click`, `longClick`, `doubleClick`, `swipe`, `fling`, `drag`, `back`, `home`, `recent`), the touch points in pixels, the foreground bundle and ability, and the widget under the finger (`W1_ID`, `W1_Text`, `W1_Type`, `W1_BOUNDS`); key events carry KeyCode ids. With `-l` it dumps the layout to `/data/local/tmp/layout_<n>_<i>.json` for each event. It runs until SIGINT; `record` starts it in the background and stops it that way.
885
+ - uitest only writes an event, hit-tests its widget and dumps its layout once the next touch has started (or 1.5 to 3 s later), so with a quick next action uitest's widget and layout already show the next screen (a tap on Continue is reported on the next page's list). `record` therefore follows the multimodal input service's hilog domain while recording for the time of every touch-down, and matches each touch to the last layout uitest wrote before it, by the files' modification times. When the touches and events do not line up it warns and falls back to event order; pausing about 3 s between actions then gives correct selectors.
886
+ - While the recorder runs, `uitest dumpLayout` blocks (for every user of the device), so `dump layout`, `input --selector` and `screenshot --target` do not work until it stops; `input` by coordinates does.
887
+ - The recorder has no replay of its own; `replay` is that.
888
+
889
+ #### Flow files
890
+
891
+ A flow is JSON, meant to be written and edited by hand:
892
+
893
+ ```json
894
+ {
895
+ "version": 1,
896
+ "bundle": "com.example.flowprobe",
897
+ "ability": "EntryAbility",
898
+ "device": { "api": 26, "display": { "width": 1080, "height": 2400 } },
899
+ "steps": [
900
+ { "action": "wait", "selector": "type=Text,text=Flow probe", "note": "the start page is up" },
901
+ { "action": "inputText", "selector": "type=TextInput,hint=Your name", "text": "Ann" },
902
+ { "action": "click", "selector": "type=Button,text=Continue", "fallback": { "fx": 0.5, "fy": 0.305 }, "expect": "type=Text,text=Hello Ann" },
903
+ { "action": "swipe", "selector": "type=List,id=fruitList", "to": { "fx": 0.5, "fy": 0.3 } },
904
+ { "action": "click", "selector": "type=Text,text=Banana", "expect": "type=Text,text=Picked: Banana", "timeoutMs": 5000 },
905
+ { "action": "keyEvent", "key": "Back", "expect": "id=title" }
906
+ ]
907
+ }
908
+ ```
909
+
910
+ | Field | Description |
911
+ | --- | --- |
912
+ | `version` | **Required.** `1`. |
913
+ | `bundle` | The app the flow drives: `replay --launch` starts it, and a divergence where it is not on screen is `wrong-screen`. |
914
+ | `ability` | The ability `--launch` starts (default: the bundle's main ability). |
915
+ | `device` | Informational: the `api` level and `display` size the flow was recorded on. |
916
+ | `steps[].action` | **Required.** `click`, `longClick`, `doubleClick`, `swipe`, `inputText`, `keyEvent`, or `wait`. |
917
+ | `steps[].selector` | The element to wait for and act on, in the [selector syntax](#selectors-and-windows) (`type=Button,id=ok`, `text*=Sign`, `xpath=//Button[@text="OK"]`). Click-like actions and `swipe` need it or `fallback`; `wait` needs it; for `keyEvent` it is only waited for; `inputText` without it types into the focused field. |
918
+ | `steps[].fallback` | `{ "fx", "fy" }` (0–1 fractions of the display): where to act when there is no selector, or with `replay --fallback` when it is not found. |
919
+ | `steps[].text` / `steps[].clear` | `inputText`: the text to type (the field is tapped first); `clear` empties it first. |
920
+ | `steps[].key` | `keyEvent`: as `input --key` (`Back`, `Home`, `Enter`, `Delete`, a KeyCode id, `Ctrl+A`, …). |
921
+ | `steps[].to` / `steps[].speed` | `swipe`: the end point (`{ "fx", "fy" }`) from the element's center, and the velocity in px/s. |
922
+ | `steps[].gone` | `wait`: wait until no element matches, instead of until one does. |
923
+ | `steps[].expect` | A selector for an element that must be on screen after the step. |
924
+ | `steps[].waitMs` | Pause after the action, before `expect` and the next step. |
925
+ | `steps[].timeoutMs` | This step's timeout for its selector and `expect` (default `--step-timeout`). |
926
+ | `steps[].note` | Free text, ignored. |
927
+
928
+ Unknown fields are errors (so a typo such as `selecter` does not silently drop a selector), and every selector is parsed when the file is loaded; `replay` lists every problem with its path (`steps[2].selector: …`) and exits 2. Step indices are 0-based everywhere. Prefer `id` selectors where the app sets ids (a recorded flow uses them): they survive text changes that `text=` selectors flag as divergences.
929
+
930
+ #### `perf trace` — frames, jank and startup from a hitrace capture
931
+
932
+ `perf trace` captures a `hitrace` trace on the device while the app cold-launches (`--launch`), a [flow](#flow-files) is replayed (`--flow`), or for a fixed time (`--for`), pulls it, and summarises it for the app. It is the measurement behind a performance gate: `--max-jank-rate` and `--max-startup-ms` turn it into a pass/fail check.
933
+
934
+ What it reads from the trace (the markers an OpenHarmony API 26 device writes with the default categories `ace,app,graphic,ability,window`):
935
+
936
+ | Measure | How |
937
+ | --- | --- |
938
+ | Frames | Each UI frame of the app is one `ReceiveVsync … now:<vsync>` slice on its main thread (ArkUI asks for a vsync only when something changed, so an idle app has no frames). A frame's time runs from its vsync to the end of that slice, so a frame held up by other work on the main thread counts the wait. |
939
+ | Janky frames | Frames longer than one refresh interval (they missed the next vsync). `jankRate` = janky / total; `missedVsyncs` adds up the intervals missed. |
940
+ | Refresh rate | The vsync period ArkUI logs (`VSyncReceiver:period:<ns>`), else `hidumper -s RenderService -a screen`, else 60 Hz (with a note). |
941
+ | Cold start | With `--launch`: from the launch request (`AbilityManagerClient::StartAbility` in `aa`) to RenderService compositing the app's first frame (`RSJankStats::ReportEventFirstFrame app pid <pid>`); `processMs` is when the process was forked, `firstFrameMs` the main-thread time of the first frame. That first frame is left out of the frame stats. |
942
+ | Main thread | `busyMs`: time in top-level traced slices; `cpuMs`: CPU time, with the `sched` category only. |
943
+ | Scenes | RenderService's own reports for the app (`APP_LIST_FLING`, `ABILITY_OR_PAGE_SWITCH`, …): max frame and hitch time, hitch ratio (ms/s), response latency. |
944
+
945
+ The app's process is found by its main thread's name, by launch markers that name the bundle, by `pidof` after the capture, or by its RenderService surface (`<last part of the bundle><n>`) when the kernel did not keep thread names. `trace_streamer` is not needed (and not in the SDKs).
946
+
947
+ | Flag | Description |
948
+ | --- | --- |
949
+ | `--launch` | Force-stop the app, start tracing, cold-launch it (its `--ability`, else the main ability) and report `startup`. |
950
+ | `--flow <file>` | Replay this flow while tracing (after the launch with `--launch`), through the same engine as `replay`; `--step-timeout` and `--fallback` as there. Its `bundle` is the default `--bundle`. |
951
+ | `--for <ms>` | Without `--flow`: how long to capture (after the launch with `--launch`), default `5000`. With `--flow`: how long to keep capturing after it, default `1000`. |
952
+ | `--bundle <bundle>` | The app to measure (default: the flow's bundle, else the project in the current directory). |
953
+ | `--out <file>` | Where to save the trace (default: a new file under the system temp dir). It is ftrace text; `perf summary` reads it again. |
954
+ | `--categories <list>` | hitrace categories (`hitrace -l` on the device lists them). Add `sched` for the main thread's CPU time, at a large cost in buffer. |
955
+ | `--buffer-kb <kb>` | hitrace buffer (default `65536`). When it overflows the oldest events are lost, and a note says so. |
956
+ | `--hiperf` | Also sample the app's process with `hiperf record -p <pid>` during the capture and list its top functions (`hiperf report --sort dso,func`); the `perf.data` is pulled next to the trace. The app must be running when sampling starts (right after the launch with `--launch`). |
957
+ | `--max-jank-rate <r>` | Exit 1 when `frames.jankRate` is above `r` (0..1), or when there were no frames to judge. |
958
+ | `--max-startup-ms <ms>` | Exit 1 when `startup.coldMs` is above it, or when no startup was measured. |
959
+ | `--no-wake` | Leave the screen as it is. By default, with `--launch` or `--flow`, it is woken, a lock screen without a credential is swiped away, and it is kept on. |
960
+ | `--device <serial>` | Target device serial. |
961
+ | `--json` | Print the report: `ok`, `bundle`, `durationMs`, `trace`, `refreshRateHz`, `frames` (`total`, `janky`, `jankRate`, `p50Ms`, `p95Ms`, `maxMs`, `missedVsyncs`, `longest`), `startup` (`coldMs`, `firstFrameMs`, `processMs`, `from`, `to`) or `null`, `flow` (the `replay` report) or `null`, `pid`, `mainThread`, `scenes`, `hiperf`, `categories`, `notes`, `gate` (`passed`, `failures`), `error`. |
962
+
963
+ Exit codes: `0` the capture completed (and every threshold held), `1` a threshold was exceeded or the flow diverged (the trace does not cover it), `2` it could not capture (no device, no bundle, hitrace refused the categories, the flow file is invalid or could not run, the lock screen needs a credential).
964
+
965
+ `perf summary <trace>` summarises a trace taken earlier (`--bundle`, `--launched` when it holds a cold launch, `--pid`, `--refresh-rate`, the same thresholds and `--json`), with the same exit codes.
966
+
967
+ ```bash
968
+ oniro-app perf trace --launch --bundle com.example.app --max-startup-ms 1500
969
+ oniro-app perf trace --flow scroll.json --max-jank-rate 0.05 --out scroll.ftrace --json
970
+ oniro-app perf summary scroll.ftrace --bundle com.example.app --max-jank-rate 0.02
971
+ ```
972
+
973
+ ```text
974
+ com.example.perfprobe (pid 22334): 11.8 s captured, display 60.6 Hz (16.5 ms per frame)
975
+ frames: 200, janky 28 (14.0%), p50 4.7 ms, p95 20.5 ms, max 592.6 ms, 68 vsync(s) missed
976
+ longest: 592.6 ms at +6.50 s, 121.5 ms at +0.77 s, 27.6 ms at +0.98 s, 27.2 ms at +8.80 s, 24.8 ms at +0.90 s
977
+ startup: cold 778 ms (aa StartAbility → first frame composited (RSJankStats::ReportEventFirstFrame)), process up after 30.5 ms, first frame 151.4 ms
978
+ main thread: 2651.5 ms in traced work
979
+ RenderService APP_LIST_FLING /ets/pages/Index: max frame 14 ms, max hitch 15 ms, hitch 12 ms/s, response 62 ms
980
+ flow: passed (3 step(s))
981
+ trace: /tmp/oniro-perf-x1Y2z3/com.example.perfprobe.ftrace
982
+ note: The first frame after launch is reported under startup, not in the frame stats.
983
+ GATE FAILED: Jank rate 0.14 (28/200 frames) is above 0.02.
984
+ ```
985
+
986
+ ### Agents
987
+
988
+ #### `serve mcp` — the gate commands as MCP tools
989
+
990
+ A [Model Context Protocol](https://modelcontextprotocol.io) server on stdio (newline-delimited JSON-RPC 2.0, protocol versions 2024-11-05 to 2025-11-25) that exposes the gate commands as typed tools, so another agent (a Claude Code session, any MCP client) can build, check, deploy and inspect without shelling out and parsing text. Each tool calls the same `@oniroproject/core` function as its command and returns the JSON the command's `--json` prints, as `structuredContent` and as JSON text. stdout carries only the protocol; logs go to stderr.
991
+
992
+ | Flag | Description |
993
+ | --- | --- |
994
+ | `--project <dir>` | Project the tools work on unless a call passes `project` (default: the current directory). Relative paths in tool arguments resolve against it. |
995
+ | `--device <serial>` | Device the tools target unless a call passes `device` (default: `ONIRO_DEVICE_SERIAL`, else the only connected one). |
996
+ | `--tools <list>` | Expose only these tools, comma-separated, e.g. `--tools check_arkts,screens,api_show`. |
997
+ | `--read-only` | Leave out the tools that change the device: `apply`, `launch`, `input`, `test`, `perf_trace` (it force-stops and launches the app, and a flow taps and swipes). |
998
+
999
+ Register it with Claude Code:
1000
+
1001
+ ```bash
1002
+ claude mcp add oniro-app -- oniro-app serve mcp --project /path/to/app
1003
+ claude mcp add oniro-app-ro -- oniro-app serve mcp --project /path/to/app --read-only # look, don't touch
1004
+ ```
1005
+
1006
+ or in any client's `mcpServers` JSON (`.mcp.json`, Claude Desktop, ...):
1007
+
1008
+ ```json
1009
+ {
1010
+ "mcpServers": {
1011
+ "oniro-app": {
1012
+ "command": "oniro-app",
1013
+ "args": ["serve", "mcp", "--project", "/path/to/app"]
1014
+ }
1015
+ }
1016
+ }
1017
+ ```
1018
+
1019
+ Tools (the inputs are the command's flags in camelCase, with the same defaults; `project` and `device` default to the server's):
1020
+
1021
+ | Tool | Command | Inputs |
1022
+ | --- | --- | --- |
1023
+ | `build` | `build --json` | `project`, `product`, `module`, `mode`, `task`, `deps`, `parallel`, `installSdk` |
1024
+ | `check_arkts` | `check arkts --json` | `project`, `files[]`, `fix`, `sdk` |
1025
+ | `test` | `test --json` | `project`, `module`, `product`, `class`, `timeout`, `runTimeout`, `build`, `wake`, `mode`, `deps`, `device` |
1026
+ | `apply` | `app apply --json` | `project`, `hap`, `bundle`, `module`, `installedHap`, `system`, `allowUninstall`, `smoke`, `smokeDelay`, `smokeScreenshot`, `device` |
1027
+ | `launch` | `app launch --smoke --json` | `project`, `bundle`, `module`, `ability`, `smoke`, `smokeDelay`, `smokeScreenshot`, `device` |
1028
+ | `dump_layout` | `dump layout` | `bundle`, `raw`, `flat`, `summary`, `diff`, `output`, `selector`, `window`, `project`, `device` |
1029
+ | `input` | `input` | `type` (required), `target`, `targetId`, `targetType`, `targetIndex`, `selector`, `window`, `fx`, `fy`, `fx2`, `fy2`, `x`, `y`, `x2`, `y2`, `speed`, `key`, `text`, `clear`, `hideKeyboard`, `device` |
1030
+ | `screenshot` | `screenshot --max-dim` | `output`, `grid`, `maxDim` (default 1024), `crop`, `target`, `targetId`, `targetType`, `targetIndex`, `selector`, `window`, `contactSheet`, `burst`, `interval`, `diff`, `project`, `device` |
1031
+ | `wait` | `wait --json` | `log`, `since`, `boot`, `bundle`, `pidOf`, `domain`, `target`, `targetId`, `targetType`, `selector`, `window`, `gone`, `interval`, `timeout`, `device` |
1032
+ | `logs_crash` | `logs crash --json` | `bundle`, `since`, `wait`, `path`, `output`, `project`, `device` |
1033
+ | `perf_trace` | `perf trace --json` | `bundle`, `launch`, `flow`, `for`, `ability`, `out`, `categories[]`, `bufferKb`, `hiperf`, `maxJankRate`, `maxStartupMs`, `stepTimeout`, `fallback`, `wake`, `project`, `device` |
1034
+ | `screens` | `screens --json` | `project` |
1035
+ | `api_search` | `api search --json` | `query` (required), `kit`, `kind[]`, `lang`, `limit`, `deprecated`, `systemapi`, `api`, `sdk`, `project`, `rebuild` |
1036
+ | `api_show` | `api show --json` | `symbol` (required), `kit`, `otherSdks`, `api`, `sdk`, `project`, `rebuild` |
1037
+ | `docs_search` | `docs search --json` | `query` (required), `limit`, `section`, `api`, `sdk`, `ref`, `project` |
1038
+ | `docs_show` | `docs show --json` | `path` (required), `heading`, `maxLines`, `api`, `sdk`, `ref`, `project` |
1039
+ | `doctor` | `doctor --json` | `project`, `fix`, `device` (boolean, as `--no-device`), `serial`, `arktsCheck` |
1040
+ | `devices` | `devices --json` | `long`; the array comes wrapped as `{ "devices": [...] }` |
1041
+
1042
+ - **Errors.** A tool result has `isError: true` when the command would exit non-zero: a failed build, `check_arkts` with an error or one that could not run, a failed or incomplete test run, a failed smoke check, a `perf_trace` threshold exceeded or capture failed, an `api_show` miss or an `api_search` without hits, a `docs_search` without hits, a `docs_show` of a missing document or heading, or either when the release's docs are not synced (the text says to run `docs sync`), a failing `doctor` check, or a tool that threw (no device, a timeout, bad arguments). The JSON is still there to read. `logs_crash` is not an error when there was no crash; check `crashed`. JSON-RPC errors are kept for protocol problems (an unknown tool or method).
1043
+ - **Images.** `screenshot` returns the capture as an `image/jpeg` content block downscaled to `maxDim` (with `grid`, the 10x10 coordinate grid), plus its dimensions as JSON; `crop` and `target*` return a full-resolution crop, `contactSheet` the tiled burst, `diff` the comparison and the current screen. `output` also writes the image to a file (with `diff`, the full-resolution capture, usable as a later baseline).
1044
+ - **Layouts.** `dump_layout` without `flat` or `summary` returns the whole tree; `flat` (text) is the cheap way to read a screen. `output` saves what is returned, so `{ "output": "before.json" }` then `{ "diff": "before.json" }` compares two moments.
1045
+
432
1046
  ## Output & exit codes
433
1047
 
434
- - **Results → stdout.** Plain text by default; `--json` (where supported) writes `JSON.stringify(..., null, 2)`. Read/observe commands (`sdk list`, `cmdtools status`, `templates list`, `devices`, `wait --log` / `--target`, `watch`, `lint`, `app apply`, `build`) accept `--json` (and `screenshot` with `--contact-sheet`); `dump` emits JSON unless `--flat`; action-only commands do not.
1048
+ - **Results → stdout.** Plain text by default; `--json` (where supported) writes `JSON.stringify(..., null, 2)`. Read/observe commands (`sdk list`, `cmdtools status`, `templates list`, `devices`, `wait --log` / `--target` / `--selector`, `dump windows`, `watch`, `lint`, `check arkts`, `screens`, `api search`, `api show`, `app apply`, `app verify`, `build`, `test`, `record`, `replay`, `perf trace`, `perf summary`) accept `--json` (and `screenshot` with `--contact-sheet` or `--diff`, `app launch` with `--smoke`); `dump` emits JSON unless `--flat`; action-only commands do not.
435
1049
  - **Progress & logs → stderr.** stdout stays clean for machine consumers, so `oniro-app create … > path.txt` or `oniro-app devices --json | jq` work cleanly.
436
- - **Exit code.** `0` on success, non-zero on failure (the error message is written to stderr). Set `ONIRO_DEBUG=1` to also print full stack traces.
1050
+ - **Exit code.** `0` on success, non-zero on failure (the error message is written to stderr). `app launch --smoke` and `app apply --smoke` exit 1 when the smoke verdict fails. `test` and `replay` exit 1 when a case fails or the flow diverges, and 2 when they could not run; `perf trace` exits 1 when a threshold is exceeded and 2 when it could not capture. Set `ONIRO_DEBUG=1` to also print full stack traces.
437
1051
  - **Global options go before the command** (`oniro-app --version`), so `hdc` can pass its arguments through untouched.
438
1052
 
439
1053
  ## Configuration
@@ -458,6 +1072,7 @@ The CLI reads paths and URLs from environment variables. All are optional; defau
458
1072
  | `ONIRO_HARMONYOS_SDK_PATH` | autodetected | HarmonyOS SDK: a DevEco Studio or HarmonyOS command-line-tools install, or its `sdk` directory |
459
1073
  | `ONIRO_HARMONYOS_AUTH_DIR` | `~/.oniro/harmonyos` | Where the encrypted Huawei account token is stored |
460
1074
  | `ONIRO_HARMONYOS_SIGNING_DIR` | `~/.ohos/config` | Where generated HarmonyOS signing material (p12/csr/cer/p7b) is written |
1075
+ | `ONIRO_CACHE_DIR` | `~/.oniro/cache` | Where caches go: the SDK declaration index of `api search` / `api show` |
461
1076
  | `ONIRO_DEBUG` | (unset) | Set to `1` for full stack traces on error |
462
1077
 
463
1078
  ### Temporary space
@@ -488,6 +1103,10 @@ so. When space runs short, the install fails with a message naming the directory
488
1103
  it needed, whether that directory is RAM-backed, and this flag — rather than crashing part-way
489
1104
  through.
490
1105
 
1106
+ `cmdtools install` stages the new tools inside the install directory and swaps them in only once
1107
+ they are complete, so a failure leaves the existing tools as they were. The old tools are deleted
1108
+ only after the swap, so the install directory's filesystem needs room for both versions at once.
1109
+
491
1110
  **Command-line tools on Windows / macOS.** The Huawei mirror only publishes a Linux build of the command-line tools. On Windows and macOS you must either download the ZIP manually from the Huawei developer portal and install it with `oniro-app cmdtools install --from-zip path/to/commandline-tools-<platform>.zip`, or host the archive yourself and set `ONIRO_CMD_TOOLS_URL_WINDOWS` / `ONIRO_CMD_TOOLS_URL_MAC` so `cmdtools install` can fetch it.
492
1111
 
493
1112
  ## HarmonyOS apps
@@ -527,14 +1146,46 @@ installed SDK's release, e.g. `6.1.1(24)`, and warns when `--sdk` asks for anoth
527
1146
 
528
1147
  | Command | What it does |
529
1148
  | --- | --- |
530
- | `auth login [--timeout <s>]` | Opens the Huawei sign-in page and waits for the browser to redirect back to a loopback port. The URL is also printed, so a headless host can be signed in from another machine. |
1149
+ | `auth login [--timeout <s>]` | Opens the Huawei sign-in page and waits for the browser to redirect back to a loopback port (`http://127.0.0.1:<port>/callback`). The URL is also printed to stderr. |
531
1150
  | `auth logout` | Deletes the locally stored token. |
532
1151
  | `auth status [--json]` | Prints the signed-in account, and warns when it is not allowed to sign. Exits non-zero when signed out. |
533
1152
  | `auth team list [--json]` | Lists AGC teams; pass an id to `sign --harmonyos --team-id`. |
534
1153
 
1154
+ | `auth login` flag | Meaning |
1155
+ | --- | --- |
1156
+ | `--timeout <s>` | How long to wait for the sign-in (default 600). |
1157
+ | `--port <n>` | Listen for the redirect on this port instead of a random one, so a tunnel can be set up before the login starts. |
1158
+ | `--no-browser`, `--manual` | Do not open a browser. Print the URL, then finish the sign-in from the callback address pasted on stdin — or from a request to the loopback port, whichever comes first. |
1159
+ | `--print-url` | Also write the sign-in URL to stdout, alone on its line, before waiting. |
1160
+ | `--json` | Write JSON lines to stdout: `{"loginUrl","port","callbackUrl"}` before waiting, then `{"userId","userName","site","tokenPath"}` once signed in. |
1161
+
535
1162
  The token is stored encrypted under `ONIRO_HARMONYOS_AUTH_DIR`. It is a credential: the
536
1163
  directory is created `0700` and the files `0600`.
537
1164
 
1165
+ #### Signing in from a headless host
1166
+
1167
+ After the sign-in, Huawei sends the browser to `http://127.0.0.1:<port>/callback?...` — which,
1168
+ when the browser runs on another machine, is that machine, not the build host waiting for it.
1169
+ Either of these completes the sign-in:
1170
+
1171
+ - **Paste the address.** Run `oniro-app auth login --manual`, open the printed URL anywhere and
1172
+ sign in. The browser then fails to load the `127.0.0.1` page; copy the whole address from its
1173
+ address bar (or just the part after `?`) and paste it into the waiting terminal. A paste that
1174
+ is not this sign-in's callback (another attempt's, or a truncated one) is reported, and the
1175
+ next line is read.
1176
+ - **Tunnel the port.** Pick a port, open the tunnel from the machine with the browser, then sign in:
1177
+
1178
+ ```bash
1179
+ ssh -L 8765:127.0.0.1:8765 build-host # on the machine with the browser
1180
+ oniro-app auth login --no-browser --port 8765 # on the build host
1181
+ ```
1182
+
1183
+ Use the same port on both ends: the page redirects to the port in its URL.
1184
+
1185
+ Scripts can read the URL without scraping stderr: `--print-url` writes it alone to stdout, and
1186
+ `--json` writes `{"loginUrl","port","callbackUrl"}` as the first line, then the account once
1187
+ signed in.
1188
+
538
1189
  ### `oniro-app sign --harmonyos`
539
1190
 
540
1191
  Mirrors DevEco Studio's automatic signing:
@@ -551,6 +1202,11 @@ still valid: same bundle name and team, unexpired, naming the attached devices a
551
1202
  permissions, and wired into this project's `build-profile.json5`. Certificates are a small
552
1203
  per-team quota; `--force` regenerates anyway.
553
1204
 
1205
+ AGC's debug certificate and profile last 14 days, after which a device refuses to install the
1206
+ HAP. `sign --harmonyos` therefore ends by naming the profile's type and expiry, e.g.
1207
+ `(profile names 6 device(s), debug, valid until 2026-10-01 11:50 UTC)`, and
1208
+ `oniro-app app verify <hap>` reports both, with the days left, for a built HAP.
1209
+
554
1210
  The AGC certificate is named `oniro_debug_<team>.cer`, distinct from DevEco Studio's
555
1211
  `auto_debug_<team>.cer`: each tool replaces only its own certificate when regenerating, so
556
1212
  using both on one team does not revoke the other's signing material.