@flxapps/adbax 0.0.0-stage → 0.1.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.
@@ -0,0 +1,163 @@
1
+ # Commands
2
+
3
+ `adbax <command> [arguments] [flags]`. Flags go anywhere on the line,
4
+ `--flag value` and `--flag=value` both work, and `--` ends flag parsing.
5
+ The exception is `shell`: everything after it goes to the device as it is, so
6
+ `-s` and the other global flags go before it.
7
+
8
+ Exit status is 0 on success, 2 when something was not there (no match, an
9
+ ambiguous selector, a `wait` or `scroll-to` that timed out) and 1 for any other
10
+ failure. Errors go to stderr as `adbax: <message>`.
11
+
12
+ ## Global flags
13
+
14
+ | Flag | |
15
+ | --- | --- |
16
+ | `-s <serial>` | the device to talk to; otherwise `$ANDROID_SERIAL`, then the pinned device, then the only one online |
17
+ | `--ui` | after an action, print the screen it led to (saves a separate `adbax ui`) |
18
+ | `--settle <ms>` | how long to give the screen after an action before reading it. Default: 150 with the server, which then also waits for events to stop; 700 without |
19
+ | `--nth N` / `--first` | pick one of several matches instead of exiting 2 |
20
+ | `--re` | the selector is a regular expression |
21
+ | `--shot` | `x,y` are pixels of the last screenshot, not of the screen |
22
+ | `--no-server` | plain `adb shell` and `uiautomator dump`, as on devices the server cannot run on |
23
+
24
+ ## Selectors
25
+
26
+ Anything that takes a selector accepts:
27
+
28
+ - **text**: matched against text, content-desc, hint and resource-id,
29
+ case-insensitively. Exact beats prefix beats substring; text beats
30
+ content-desc beats ids. A tap prefers clickable nodes, and a match that is not
31
+ clickable itself resolves to its nearest clickable ancestor.
32
+ - **a row number**, `5` or `#5`: row 5 of the last `adbax ui`, `find`, `wait` or
33
+ `scroll-to`, without reading the screen again. Valid until the screen changes.
34
+ - **coordinates**, `540,1200`: taken as they are, or scaled from the last
35
+ screenshot with `--shot`.
36
+
37
+ ## Devices
38
+
39
+ | Command | |
40
+ | --- | --- |
41
+ | `devices` | attached devices; `*` marks the pinned one |
42
+ | `use <serial>` | pin a device for later calls; `use -` clears the pin |
43
+ | `avds` | the emulator's AVDs |
44
+ | `boot <avd> [--cold] [--wipe] [--no-window]` | start an AVD, wait for `sys.boot_completed`, unlock and pin it. Emulator output goes to `~/.cache/adbax/emulator-<avd>.log`; gives up after six minutes |
45
+ | `kill [--all]` | shut down the current emulator, or every running one |
46
+
47
+ ## Seeing the screen
48
+
49
+ | Command | |
50
+ | --- | --- |
51
+ | `ui` | the screen as rows: index, kind, label, short resource-id, centre, size, state. The first line names the top activity, the screen size and the row count |
52
+ | `ui --all` | every node, including labels already folded into a clickable parent |
53
+ | `ui --full` | adds each node's class |
54
+ | `ui --json` | the rows as JSON, for scripts |
55
+ | `ui --raw` | the XML as it came off the device |
56
+ | `ui --windows` | every window, not just the active one: status bar, navigation bar, the keyboard, overlays. Needs the server |
57
+ | `find <text>` | only the nodes matching a selector, with their row numbers |
58
+ | `inspect <text>` | every attribute of the best match, and its ancestor chain |
59
+ | `current` | the focused window and the resumed activity |
60
+ | `screenshot [path]` | writes a PNG (a `.jpg` path writes JPEG) and prints the path. Defaults to `~/.cache/adbax/shot.png` |
61
+ | `screenshot --small` | 540 px wide JPEG. For Claude that is about 690 image tokens, where a full-size 1080x1920 screenshot is about 1,840 |
62
+ | `screenshot --width N` | any width, scaled on the device |
63
+ | `screenshot --quality N` | JPEG quality, 80 by default |
64
+ | `screenshot --grid [step]` | a labelled coordinate grid over the image, in the image's own pixels (default: a sixth of its width). Needs the server |
65
+ | `record [secs] [path]` | a screen recording, 15 s by default, 180 at most |
66
+
67
+ Row kinds: `TAP` clickable, `EDIT` a text field, `CHK[x]`/`CHK[ ]` checkable,
68
+ `SCRL` a scroll container, `TXT` text you can read but not act on. State marks:
69
+ `*focused`, `*selected`, `*password`, `*scrollable`, `!disabled`.
70
+
71
+ ## Acting
72
+
73
+ | Command | |
74
+ | --- | --- |
75
+ | `tap <selector>` | a tap at the centre of the match |
76
+ | `longpress <selector> [--dur ms]` | press and hold, 900 ms by default |
77
+ | `type <text>` | types into the focused field |
78
+ | `type <text> --into <selector>` | taps the field first and waits until it has input focus |
79
+ | `type --clear` | empties the field before typing (or on its own) |
80
+ | `type <text> --set` | replaces the whole field in one step (ACTION_SET_TEXT), no keystrokes. For long values, where typing over the old text can leave part of it behind |
81
+ | `type ... --enter` | presses Enter afterwards |
82
+ | `type ... --no-verify` | skips reading the field back |
83
+ | `key <key>...` | key events by name (`back home enter recents del tab esc up down left right menu search power volup voldown space paste`...), by number, or as `KEYCODE_*` |
84
+ | `key <key> --long` | a long press of the key |
85
+ | `back`, `home`, `recents` | shorthands |
86
+ | `hide-keyboard` | presses Back only if the soft keyboard is showing; a plain `back` would leave the screen when it isn't |
87
+ | `swipe <up\|down\|left\|right>` | a swipe across the screen. "up" is the finger moving up, which scrolls the content down |
88
+ | `swipe <dir> --in <selector>` | the same inside one node, a carousel or a list |
89
+ | `swipe x1,y1,x2,y2 [--dur ms]` | between two points, 300 ms by default |
90
+ | `scroll-to <text> [--max 12] [--dir up\|down] [--tap]` | swipes the largest scroll container until the text shows up, then optionally taps it |
91
+ | `wait <text> [--timeout 15] [--gone]` | blocks until the text appears (or disappears) |
92
+
93
+ `type` sends key events where the virtual keyboard map has the characters and
94
+ sets the field's text through `ACTION_SET_TEXT` where it does not, so `Grüße`
95
+ and `日本` arrive intact with the server. Without it, `input text` drops
96
+ non-ASCII on most builds and adbax warns about it. After typing, `type` reads the
97
+ field back and says so when it holds something else, a sign that an IME
98
+ autocorrected it or that characters went missing.
99
+
100
+ ## Apps
101
+
102
+ | Command | |
103
+ | --- | --- |
104
+ | `apps [filter] [--system]` | installed packages, third-party only unless `--system` |
105
+ | `launch <package>` | starts the launcher activity and returns once it is drawn |
106
+ | `start <package/.Activity>` | starts one activity |
107
+ | `stop <package>` | force-stop |
108
+ | `clear <package>` | wipe the app's data |
109
+ | `install <apk> [split.apk ...]` | `-r -g -t`: replace, grant runtime permissions, allow test builds. Several files are installed as one split APK |
110
+ | `uninstall <package>` | |
111
+ | `deeplink <url> [package]` | an `ACTION_VIEW` intent |
112
+ | `perm <grant\|revoke> <package> <PERMISSION>...` | `CAMERA` is short for `android.permission.CAMERA` |
113
+ | `perms <package>` | runtime permissions and their state |
114
+
115
+ ## Debugging
116
+
117
+ | Command | |
118
+ | --- | --- |
119
+ | `log [package] [--lines 200] [--grep re] [--level W]` | logcat; with a package, only its current process |
120
+ | `crash` | the crash buffer, which survives the process dying |
121
+ | `clearlog` | empties the main and crash buffers |
122
+
123
+ ## Device state
124
+
125
+ | Command | |
126
+ | --- | --- |
127
+ | `dark <on\|off>` | night mode |
128
+ | `font <scale>` | system font scale, `1.0` normal, `1.3` large, `2.0` the maximum |
129
+ | `anim <on\|off>` | all three animation scales; off also makes reads after actions settle sooner |
130
+ | `rotate <0\|90\|180\|270\|auto>` | |
131
+ | `demo <on\|off>` | SystemUI demo mode: 10:00, full battery and signal, no notification icons |
132
+
133
+ ## The server
134
+
135
+ | Command | |
136
+ | --- | --- |
137
+ | `server status` | pid, Android version, uptime and the forwarded port |
138
+ | `server start [--idle seconds]` | start it now rather than on first use |
139
+ | `server stop` | stop it and free the accessibility connection |
140
+ | `server restart` | |
141
+ | `server log` | the server's stderr, from `/data/local/tmp/adbax-server.log` |
142
+
143
+ ## Raw adb
144
+
145
+ | Command | |
146
+ | --- | --- |
147
+ | `shell <command>` | runs on the device shell |
148
+ | `pull <remote> [local]`, `push <local> <remote>` | |
149
+ | `version` | |
150
+
151
+ `text`, `press`, `screencap`, `logcat` and `ls` are aliases of `type`,
152
+ `longpress`, `screenshot`, `log` and `apps`.
153
+
154
+ ## Environment
155
+
156
+ | Variable | |
157
+ | --- | --- |
158
+ | `ANDROID_SERIAL` | the device to use; `-s` and `adbax use` do the same |
159
+ | `ANDROID_HOME` | the SDK, if adb or the emulator is not on the PATH |
160
+ | `ADBAX_SERVER=0` | never start the on-device server |
161
+ | `ADBAX_SERVER_IDLE` | seconds before an unused server exits (600) |
162
+ | `ADBAX_CACHE` | where the device pin and the last dump go (`~/.cache/adbax`) |
163
+ | `ADBAX_TRACE=1` | print every adb call and server request with its duration |
package/package.json CHANGED
@@ -1,6 +1,20 @@
1
1
  {
2
2
  "name": "@flxapps/adbax",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Drive an Android device or emulator through its accessibility tree instead of screenshots and pixel coordinates.",
5
+ "keywords": ["android", "adb", "emulator", "uiautomator", "accessibility", "automation", "testing", "cli", "agent"],
6
+ "license": "Apache-2.0",
7
+ "author": "Felix Heller",
8
+ "homepage": "https://github.com/flxapps/adbax#readme",
9
+ "repository": { "type": "git", "url": "git+https://github.com/flxapps/adbax.git" },
10
+ "bugs": { "url": "https://github.com/flxapps/adbax/issues" },
11
+ "type": "module",
12
+ "bin": { "adbax": "bin/adbax.js" },
13
+ "files": ["bin/", "src/", "server/adbax-server.jar", "skill/", "docs/", "NOTICE"],
14
+ "engines": { "node": ">=18" },
15
+ "publishConfig": { "access": "public" },
16
+ "scripts": {
17
+ "test": "node --test",
18
+ "build:server": "sh server/build.sh"
19
+ }
20
+ }
Binary file
package/skill/SKILL.md ADDED
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: android-device
3
+ description: Drive an Android emulator or physical device through its accessibility tree instead of screenshots and pixel guessing. Boot AVDs, read the screen as a labelled node list, tap and type by label, scroll to off-screen elements, wait for state, install APKs, read logcat and crashes, play canvas games through grid screenshots, set dark mode / font scale / demo-mode status bar for store screenshots. Use whenever a task involves an emulator or a connected phone, e.g. "open the app on the emulator", "tap through onboarding", "boot Pixel_7_Pro and check the settings screen", "why does it crash on launch", "take store screenshots".
4
+ ---
5
+
6
+ # Driving an Android device
7
+
8
+ `adbax` must be on the PATH (`npm install -g @flxapps/adbax`, or a link to
9
+ `bin/adbax.js` in a checkout). `adbax help` prints the whole surface.
10
+
11
+ ## Read the tree, do not read pixels
12
+
13
+ A full-size phone screenshot costs about 1,800 image tokens, has to be read
14
+ like an image and gives coordinates you then guess at. The accessibility tree
15
+ costs a few hundred text tokens and gives exact labels, ids and centres:
16
+
17
+ ```
18
+ $ adbax ui
19
+ # com.android.settings/.Settings 1080x1920 11 nodes
20
+ [0] TAP "Search settings" id=search_action_bar @540,147 1038x126
21
+ [5] TAP "Network & Internet / Wi‑Fi, mobile, data usage" id=dashboard_tile @540,903 1080x189
22
+ ```
23
+
24
+ Then act on the label, never on the pixel:
25
+
26
+ ```bash
27
+ adbax tap "Network & Internet"
28
+ ```
29
+
30
+ The first call starts a small server on the device (about a second). After
31
+ that a read takes 0.2-0.3 s end to end, so reading after every step is cheap.
32
+ If adbax says the server is unavailable it has fallen back to `uiautomator dump`
33
+ at about 2 s per read; budget for that and do not poll `ui` in a loop.
34
+
35
+ ## The loop
36
+
37
+ Chain steps in **one** Bash call; each `adbax` is its own process and `&&` costs
38
+ nothing:
39
+
40
+ ```bash
41
+ adbax launch com.example.app && adbax wait "Start" && adbax tap "Settings" --ui
42
+ ```
43
+
44
+ `--ui` on any action prints the screen it led to, after waiting for the
45
+ transition to finish. Use it whenever you need to see the result.
46
+
47
+ | Want | Command |
48
+ | --- | --- |
49
+ | see the screen | `adbax ui` (`--all` includes layout nodes, `--full` adds classes, `--windows` adds system bars and the keyboard) |
50
+ | find one thing | `adbax find "Delete"` (cheaper to read than a full dump) |
51
+ | everything about a node | `adbax inspect "Delete"` (attributes plus ancestor chain) |
52
+ | tap | `adbax tap "Sign in"` / `adbax tap 5` / `adbax tap 540,1200` |
53
+ | type | `adbax type you@example.com --into Email --enter` |
54
+ | replace a field's text | `adbax type "new" --into Name --clear` |
55
+ | off-screen element | `adbax scroll-to "Delete account" --tap` |
56
+ | block until ready | `adbax wait "Welcome" --timeout 20` (`--gone` for the inverse) |
57
+ | hardware keys | `adbax key back home enter recents del` |
58
+
59
+ Selectors match **text, content-desc and resource-id**, case-insensitively,
60
+ exact before prefix before substring. A match that is not itself clickable
61
+ resolves to its nearest clickable ancestor, so `adbax tap "mobile, data usage"`
62
+ hits the whole row. `--re` switches to a regex.
63
+
64
+ **Ambiguity exits 2 and lists the candidates** rather than picking one. Narrow
65
+ the query, or pass `--nth N` / `--first`. Treat that list as the answer, not as
66
+ an error to retry around.
67
+
68
+ `adbax tap 5` reuses the last read's row numbers and skips a fresh one. Row
69
+ numbers are only valid until the screen changes.
70
+
71
+ `type` handles umlauts, CJK and emoji when the server runs (it sets the field's
72
+ text for characters the keyboard map lacks and says so). It reads the field back
73
+ afterwards and reports when it holds something other than what was typed.
74
+
75
+ ## When the tree comes back empty
76
+
77
+ Compose, Views and Flutter all populate it. What does not:
78
+
79
+ - Games and other canvas or GL surfaces. These draw without nodes.
80
+ - `FLAG_SECURE` windows: payment sheets, some password managers. Screenshots of
81
+ them come back black as well.
82
+ - WebViews that draw to a `<canvas>`.
83
+
84
+ For those, read the screen as an image with a coordinate grid, and tap in the
85
+ image's own pixels:
86
+
87
+ ```bash
88
+ adbax screenshot --grid # prints the path of a 540 px JPEG with a labelled grid
89
+ adbax tap 270,480 --shot # 270,480 in that image, scaled to the screen
90
+ adbax swipe 100,800,440,800 --shot --dur 400
91
+ ```
92
+
93
+ Read the labels off the grid rather than estimating. `--shot` always refers to
94
+ the last screenshot taken on that device, so take a fresh one after the screen
95
+ moves. `adbax screenshot --small` is the same image without the grid.
96
+
97
+ ## The server holds the accessibility connection
98
+
99
+ Android allows one UiAutomation client at a time. While the adbax server runs,
100
+ `uiautomator dump`, UiAutomator or screenshot instrumentation tests, Appium and
101
+ Maestro cannot connect. Before running instrumentation tests, `adbax server stop`.
102
+ It also exits on its own after ten minutes without a request.
103
+
104
+ ## Device and app management
105
+
106
+ ```bash
107
+ adbax avds # list AVDs
108
+ adbax boot Pixel_7_Pro # boots, waits for sys.boot_completed, pins it
109
+ adbax devices # * marks the pinned one
110
+ adbax use emulator-5554 # pin when several are attached (or -s per call)
111
+
112
+ adbax apps example # installed third-party packages matching "example"
113
+ adbax install app-debug.apk # -r -g -t, so runtime permissions are pre-granted
114
+ adbax install base.apk split_config.arm64_v8a.apk # split APKs in one go
115
+ adbax launch com.example.app # returns once the activity is drawn
116
+ adbax clear com.example.app # wipe data, back to first run
117
+ adbax deeplink "myapp://order/42"
118
+ adbax perm grant com.example.app POST_NOTIFICATIONS
119
+ ```
120
+
121
+ ## Debugging
122
+
123
+ ```bash
124
+ adbax clearlog && adbax launch com.example.app && adbax log com.example.app --lines 200 --level W
125
+ adbax crash # the crash buffer, which survives the process dying
126
+ adbax log com.example.app --grep "OkHttp|Retrofit"
127
+ ```
128
+
129
+ `log <pkg>` resolves the current pid, so after the app restarts it shows the
130
+ new process only. Without a package it is the whole buffer.
131
+
132
+ ## Screenshots worth keeping
133
+
134
+ ```bash
135
+ adbax demo on # 10:00, full battery, full signal, no notification icons
136
+ adbax dark on
137
+ adbax anim off # also makes reads after actions settle sooner
138
+ adbax font 1.0
139
+ adbax screenshot ~/shots/01.png # full resolution
140
+ adbax demo off
141
+ ```
142
+
143
+ `adbax record 20 walkthrough.mp4` for a clip.
package/src/adb.js ADDED
@@ -0,0 +1,120 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { execFile, execFileSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { AdbaxError, cachePath, fail, trace } from './util.js';
7
+
8
+ function sdkDir() {
9
+ const env = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
10
+ if (env) return env;
11
+ const home = os.homedir();
12
+ const usual = { darwin: 'Library/Android/sdk', win32: 'AppData/Local/Android/Sdk' }[process.platform] || 'Android/Sdk';
13
+ const guesses = [usual, 'Library/Android/sdk', 'Android/Sdk', 'AppData/Local/Android/Sdk'].map((g) => path.join(home, g));
14
+ return guesses.find((g) => fs.existsSync(g)) || guesses[0];
15
+ }
16
+
17
+ const found = {};
18
+ /** Finds `adb` or `emulator` on PATH, then in the SDK. */
19
+ export function binary(name, sub) {
20
+ if (found[name]) return found[name];
21
+ const exe = process.platform === 'win32' ? `${name}.exe` : name;
22
+ for (const dir of (process.env.PATH || '').split(path.delimiter)) {
23
+ const p = path.join(dir, exe);
24
+ if (dir && fs.existsSync(p)) return (found[name] = p);
25
+ }
26
+ const p = path.join(sdkDir(), sub, exe);
27
+ if (fs.existsSync(p)) return (found[name] = p);
28
+ fail(`${name} not found on PATH or in ${path.join(sdkDir(), sub)}. Set ANDROID_HOME.`);
29
+ }
30
+
31
+ /** Runs adb with the given argv. Resolves stdout (a string, or a Buffer with `buffer`). */
32
+ export function adbRaw(args, { buffer = false, timeout = 120_000, soft = false } = {}) {
33
+ const started = performance.now();
34
+ return new Promise((resolve, reject) => {
35
+ execFile(binary('adb', 'platform-tools'), args,
36
+ { encoding: buffer ? 'buffer' : 'utf8', maxBuffer: 256 * 1024 * 1024, timeout },
37
+ (err, stdout, stderr) => {
38
+ trace('adb ' + args.join(' ').slice(0, 120), started);
39
+ if (!err) return resolve(stdout);
40
+ if (soft) return resolve(buffer ? Buffer.alloc(0) : '');
41
+ const detail = (stderr && stderr.toString().trim()) || err.message;
42
+ reject(new AdbaxError(`adb ${args.join(' ')}\n ${detail}`));
43
+ });
44
+ });
45
+ }
46
+
47
+ export async function listDevices() {
48
+ const out = await adbRaw(['devices', '-l']);
49
+ return out.split('\n').slice(1).filter((l) => l.trim()).map((l) => {
50
+ const m = l.match(/^(\S+)\s+(\S+)(.*)$/);
51
+ if (!m) return null;
52
+ const props = {};
53
+ for (const kv of (m[3] || '').trim().split(/\s+/)) {
54
+ const i = kv.indexOf(':');
55
+ if (i > 0) props[kv.slice(0, i)] = kv.slice(i + 1);
56
+ }
57
+ return { serial: m[1], state: m[2], model: props.model || '', device: props.device || '' };
58
+ }).filter(Boolean);
59
+ }
60
+
61
+ const PIN = () => cachePath('device');
62
+
63
+ export function pinnedSerial() {
64
+ try { return fs.readFileSync(PIN(), 'utf8').trim(); } catch { return ''; }
65
+ }
66
+
67
+ export function pinSerial(serial) {
68
+ if (serial) fs.writeFileSync(PIN(), serial);
69
+ else fs.rmSync(PIN(), { force: true });
70
+ }
71
+
72
+ /**
73
+ * Picks the device: -s, then $ANDROID_SERIAL, then the pinned one if it is
74
+ * online, then the only one online. Several and none pinned is an error rather
75
+ * than a guess.
76
+ */
77
+ export async function resolveSerial(flags) {
78
+ if (flags.serial) return flags.serial;
79
+ if (process.env.ANDROID_SERIAL) return process.env.ANDROID_SERIAL;
80
+ const all = await listDevices();
81
+ const online = all.filter((d) => d.state === 'device');
82
+ const pinned = pinnedSerial();
83
+ if (pinned && online.some((d) => d.serial === pinned)) return pinned;
84
+ if (online.length === 1) return online[0].serial;
85
+ if (online.length === 0) {
86
+ const waiting = all.find((d) => d.state === 'unauthorized');
87
+ if (waiting) fail(`${waiting.serial} is unauthorized: accept the USB debugging prompt on the device.`);
88
+ if (all.length) fail(`no device online: ${all.map((d) => `${d.serial} ${d.state}`).join(', ')}.`);
89
+ fail('no device connected. `adbax avds` then `adbax boot <avd>`.');
90
+ }
91
+ fail(`several devices: ${online.map((d) => d.serial).join(', ')}\n` +
92
+ ' pick one with `adbax use <serial>` or -s <serial>.');
93
+ }
94
+
95
+ /** adb bound to one device. */
96
+ export class Adb {
97
+ constructor(serial) {
98
+ this.serial = serial;
99
+ }
100
+
101
+ run(args, opts) {
102
+ return adbRaw(['-s', this.serial, ...args], opts);
103
+ }
104
+
105
+ /** `cmd` is one string for the device shell; quote user input with q(). */
106
+ shell(cmd, opts) {
107
+ return this.run(['shell', cmd], opts);
108
+ }
109
+
110
+ execOut(cmd, opts) {
111
+ return this.run(['exec-out', cmd], { buffer: true, ...opts });
112
+ }
113
+ }
114
+
115
+ /** The emulator binary is only needed for avds/boot, so it is looked up lazily. */
116
+ export const emulatorBinary = () => binary('emulator', 'emulator');
117
+
118
+ export function runEmulatorSync(args) {
119
+ return execFileSync(emulatorBinary(), args, { encoding: 'utf8' });
120
+ }
package/src/args.js ADDED
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ /** Flags that take a value: `--timeout 15` and `--timeout=15` both work. */
4
+ const NEEDS_VALUE = new Set(['serial', 'settle', 'nth', 'timeout', 'max', 'dir', 'dur', 'in', 'into',
5
+ 'scale', 'width', 'lines', 'grep', 'level', 'idle', 'quality']);
6
+ /** Flags that take a value only when one follows that is not another flag. */
7
+ const OPTIONAL_VALUE = new Set(['grid']);
8
+
9
+ /**
10
+ * Splits argv into the command, its positional arguments and its flags.
11
+ * `-s <serial>` is the one short flag, kept for parity with adb. Everything
12
+ * after `shell` belongs to the device command, so `adbax shell logcat -s Tag`
13
+ * keeps its own flags.
14
+ */
15
+ export function parseArgs(argv) {
16
+ const flags = {};
17
+ const pos = [];
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const a = argv[i];
20
+ if (pos[0] === 'shell') { pos.push(...argv.slice(i)); break; }
21
+ if (a === '--') { pos.push(...argv.slice(i + 1)); break; }
22
+ if (a.startsWith('--')) {
23
+ const eq = a.indexOf('=');
24
+ if (eq > -1) flags[a.slice(2, eq)] = a.slice(eq + 1);
25
+ else if (NEEDS_VALUE.has(a.slice(2)) && i + 1 < argv.length) flags[a.slice(2)] = argv[++i];
26
+ else if (OPTIONAL_VALUE.has(a.slice(2)) && /^\d+$/.test(argv[i + 1] || '')) flags[a.slice(2)] = argv[++i];
27
+ else flags[a.slice(2)] = true;
28
+ } else if (a === '-s' && i + 1 < argv.length) {
29
+ flags.serial = argv[++i];
30
+ } else if (a === '-h') {
31
+ flags.help = true;
32
+ } else {
33
+ pos.push(a);
34
+ }
35
+ }
36
+ return { cmd: pos.shift(), pos, flags };
37
+ }