device-devtools-mcp 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.
Files changed (66) hide show
  1. package/AGENTS.md +327 -0
  2. package/LICENSE +21 -0
  3. package/README.md +491 -0
  4. package/VERSION +1 -0
  5. package/bin/device-devtools-mcp.js +106 -0
  6. package/bin/devicetools +315 -0
  7. package/config.example.json +63 -0
  8. package/integrations/agent-pointer.sh +48 -0
  9. package/integrations/claude/SKILL.md +8 -0
  10. package/integrations/cursor/devicetools.mdc +8 -0
  11. package/integrations/gemini/GEMINI.md +5 -0
  12. package/integrations/mcp/README.md +184 -0
  13. package/integrations/mcp/mcp.json +10 -0
  14. package/integrations/mcp/reference.sh +119 -0
  15. package/integrations/mcp/selftest.sh +158 -0
  16. package/integrations/mcp/server.sh +343 -0
  17. package/package.json +50 -0
  18. package/scripts/android/app.sh +241 -0
  19. package/scripts/android/back.sh +73 -0
  20. package/scripts/android/controls.sh +56 -0
  21. package/scripts/android/devices.sh +77 -0
  22. package/scripts/android/doctor.sh +253 -0
  23. package/scripts/android/lib.sh +699 -0
  24. package/scripts/android/logs.sh +289 -0
  25. package/scripts/android/permission.sh +119 -0
  26. package/scripts/android/settings.sh +63 -0
  27. package/scripts/android/setup.sh +97 -0
  28. package/scripts/android/tree.awk +166 -0
  29. package/scripts/android/tree.sh +127 -0
  30. package/scripts/android/type.sh +191 -0
  31. package/scripts/common/find.sh +95 -0
  32. package/scripts/common/key.sh +65 -0
  33. package/scripts/common/lib.sh +14 -0
  34. package/scripts/common/measure.sh +170 -0
  35. package/scripts/common/open.sh +131 -0
  36. package/scripts/common/screenshot.sh +102 -0
  37. package/scripts/common/scroll.sh +177 -0
  38. package/scripts/common/snapshot.sh +69 -0
  39. package/scripts/common/swipe.sh +171 -0
  40. package/scripts/common/tap.sh +226 -0
  41. package/scripts/common/wait.sh +212 -0
  42. package/scripts/common/waypoint.sh +141 -0
  43. package/scripts/dispatch.sh +21 -0
  44. package/scripts/flow.sh +266 -0
  45. package/scripts/init.sh +101 -0
  46. package/scripts/ios/app.sh +404 -0
  47. package/scripts/ios/back.sh +95 -0
  48. package/scripts/ios/controls.sh +68 -0
  49. package/scripts/ios/devices.sh +80 -0
  50. package/scripts/ios/doctor.sh +386 -0
  51. package/scripts/ios/lib.sh +864 -0
  52. package/scripts/ios/logs.sh +272 -0
  53. package/scripts/ios/permission.sh +108 -0
  54. package/scripts/ios/settings.sh +76 -0
  55. package/scripts/ios/setup.sh +175 -0
  56. package/scripts/ios/tree.sh +128 -0
  57. package/scripts/ios/type.sh +178 -0
  58. package/scripts/lib.sh +1032 -0
  59. package/scripts/links.tsv +44 -0
  60. package/scripts/relink.sh +121 -0
  61. package/scripts/run.sh +415 -0
  62. package/scripts/selftest.sh +1709 -0
  63. package/scripts/snapshot.awk +362 -0
  64. package/scripts/verify-npm-package.js +133 -0
  65. package/tests/fixtures/ios-contacts-list.expected +52 -0
  66. package/tests/fixtures/ios-contacts-list.rows +140 -0
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "device-devtools-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for a phone: read a native app's screen as measurable elements, act on them, and refuse rather than guess",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "ios",
9
+ "android",
10
+ "xcuitest",
11
+ "webdriveragent",
12
+ "uiautomator2",
13
+ "accessibility",
14
+ "devtools"
15
+ ],
16
+ "repository": "hung977/device-devtools-mcp",
17
+ "homepage": "https://github.com/hung977/device-devtools-mcp#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/hung977/device-devtools-mcp/issues"
20
+ },
21
+ "license": "MIT",
22
+ "mcpName": "io.github.hung977/device-devtools-mcp",
23
+ "type": "commonjs",
24
+ "bin": {
25
+ "device-devtools-mcp": "bin/device-devtools-mcp.js"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "os": [
31
+ "darwin",
32
+ "linux"
33
+ ],
34
+ "files": [
35
+ "bin/",
36
+ "scripts/",
37
+ "integrations/",
38
+ "tests/",
39
+ "config.example.json",
40
+ "AGENTS.md",
41
+ "VERSION",
42
+ "LICENSE",
43
+ "README.md"
44
+ ],
45
+ "scripts": {
46
+ "test": "scripts/selftest.sh && integrations/mcp/selftest.sh",
47
+ "prepack": "scripts/relink.sh --write",
48
+ "verify-npm-package": "node scripts/verify-npm-package.js"
49
+ }
50
+ }
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env bash
2
+ # app.sh — the app under test's lifecycle.
3
+ #
4
+ # app.sh installed <id> is some other app present?
5
+ # app.sh state
6
+ # app.sh launch | kill | relaunch
7
+ # app.sh reset
8
+ # app.sh install [apk]
9
+ # app.sh build
10
+ #
11
+ # `reset` means something stronger here than it does on iOS. Android has
12
+ # `pm clear`, which discards the app's data and cache in place, so a reset is
13
+ # one command and does not need a copy of the package to reinstall from. It is
14
+ # still destructive: accounts are logged out and stored state is gone.
15
+ #
16
+ # `relaunch` restarts the process and keeps stored data. A test that navigates
17
+ # somewhere and stops there passes once and fails on the second run, which reads
18
+ # like flakiness and is not. Either end where you started, or use `reset`.
19
+ #
20
+ # --snapshot prints the screen this left behind and renumbers the uids, so the
21
+ # read that would have been the next call is folded into this one. In a measured
22
+ # session — log in, open Settings — twelve calls did the work and six of them
23
+ # were snapshots taken only to find out what the previous action produced.
24
+ #
25
+ # exit 0 done
26
+ # exit 1 usage error
27
+ # exit 2 environment or configuration problem
28
+ # exit 3 device unreachable — run doctor.sh
29
+ # exit 4 the command ran but the app did not end up in the expected state
30
+
31
+ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
32
+
33
+ CMD="${1-}"
34
+ case "$CMD" in
35
+ -h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
36
+ esac
37
+ [ -n "$CMD" ] || die "missing verb (usage: app.sh state|launch|kill|relaunch|reset|install|build)" 1
38
+ shift
39
+
40
+ # --snapshot IS ANSWERED ONCE, HERE, AND NOT BY EVERY VERB SEPARATELY.
41
+ #
42
+ # Each verb below counts its own arguments and refuses a stray one, which is
43
+ # right and is also why a flag that applies to all of them cannot be added to
44
+ # all of them: it would have to be subtracted from eight separate `[ $# -eq 0 ]`
45
+ # guards. Taken out of the list up front, every verb sees exactly what it saw
46
+ # before, and the screen is printed at the end of whichever one ran.
47
+ DT_ARGV=()
48
+ for a in ${@+"$@"}; do
49
+ case "$a" in
50
+ --snapshot) act_snapshot_on ;;
51
+ *) DT_ARGV+=("$a") ;;
52
+ esac
53
+ done
54
+ set -- ${DT_ARGV[@]+"${DT_ARGV[@]}"}
55
+
56
+ load_config
57
+
58
+ PKG="$(cfg '.app.package' '')"
59
+ [ -n "$PKG" ] || die "app.package is not set in $DT_CONFIG — every verb here needs it" 2
60
+
61
+ installed() {
62
+ pkg_installed "$PKG"
63
+ }
64
+
65
+ running() {
66
+ [ -n "$(adb_dev shell pidof "$PKG" 2>/dev/null | tr -d '\r ')" ]
67
+ }
68
+
69
+ # The focused window, as one line, tested afterwards rather than piped into a
70
+ # second grep.
71
+ #
72
+ # `dumpsys | grep -m1 X | grep -q Y` looks equivalent and is not: `grep -q`
73
+ # exits the moment it matches, `grep -m1` likewise, and the SIGPIPE that gives
74
+ # the upstream command becomes a pipeline failure under `set -o pipefail`. The
75
+ # function then reports "not in the foreground" for an app that is — measured
76
+ # here as a launch that worked being reported as a launch that timed out.
77
+ #
78
+ # TWO QUESTIONS, NOT ONE. The focused window is the fast answer and it is wrong
79
+ # for popups: open the overflow menu and the focus moves to a PopupWindow whose
80
+ # name does not carry the package, so an app plainly on screen with its own menu
81
+ # open reports as backgrounded. Found while walking the app, which recorded two taps as
82
+ # "left the app" when they had opened a menu.
83
+ #
84
+ # So the resumed activity is asked as well. That is the activity Android has
85
+ # actually resumed, which a popup belonging to the app does not change.
86
+ foreground() {
87
+ local line
88
+ line="$(adb_dev shell dumpsys window 2>/dev/null | tr -d '\r' | awk '/mCurrentFocus/ { print; exit }')"
89
+ case "$line" in
90
+ *"$PKG"*) return 0 ;;
91
+ esac
92
+ line="$(adb_dev shell dumpsys activity activities 2>/dev/null | tr -d '\r' \
93
+ | awk '/topResumedActivity|mResumedActivity/ { print; exit }')"
94
+ case "$line" in
95
+ *"$PKG"*) return 0 ;;
96
+ *) return 1 ;;
97
+ esac
98
+ }
99
+
100
+ # state_name — one of the four words every message here uses, so that "what is
101
+ # the app doing" always reads the same way whichever verb reports it.
102
+ state_name() {
103
+ if ! installed; then printf 'not installed'
104
+ elif foreground; then printf 'foreground'
105
+ elif running; then printf 'running in background'
106
+ else printf 'not running'
107
+ fi
108
+ }
109
+
110
+ # The launcher activity is resolved rather than configured. Hardcoding it in the
111
+ # config means every launch breaks the day the app renames its entry point, and
112
+ # the package manager already knows the answer.
113
+ launcher_activity() {
114
+ local a
115
+ a="$(cfg '.app.activity' '')"
116
+ if [ -n "$a" ]; then printf '%s' "$a"; return 0; fi
117
+ a="$(adb_dev shell cmd package resolve-activity --brief "$PKG" 2>/dev/null \
118
+ | tr -d '\r' | grep -m1 "^$PKG/" || true)"
119
+ [ -n "$a" ] || die "cannot find a launcher activity for $PKG — set app.activity in $DT_CONFIG" 2
120
+ printf '%s' "$a"
121
+ }
122
+
123
+ # Waiting for a state rather than sleeping through it. Launch was measured
124
+ # taking anywhere from under a second to several, and a fixed sleep is either
125
+ # wasted time or a race.
126
+ wait_for() {
127
+ local want="$1" limit="${2:-15}" i=0
128
+ while [ "$i" -lt "$((limit * 4))" ]; do
129
+ case "$want" in
130
+ up) foreground && return 0 ;;
131
+ down) running || return 0 ;;
132
+ esac
133
+ sleep 0.25
134
+ i=$((i + 1))
135
+ done
136
+ return 1
137
+ }
138
+
139
+ do_launch() {
140
+ local act
141
+ act="$(launcher_activity)"
142
+ adb_dev shell am start -W -n "$act" >/dev/null 2>&1 \
143
+ || die "am start $act failed — run $(as_cmd doctor)" 3
144
+ wait_for up || die "launched $PKG but it did not reach the foreground within 15s" 4
145
+ # The driver's session refers to the process that has just been replaced, so
146
+ # it is dropped rather than left to fail on the next verb with a message about
147
+ # sessions that says nothing about what actually happened.
148
+ ua2_drop_session
149
+ }
150
+
151
+ case "$CMD" in
152
+ # installed <package> — is some OTHER app on this device?
153
+ #
154
+ # Every other verb here acts on the app under test. This one answers about a
155
+ # package the test merely depends on: a browser for a deep link, a maps app
156
+ # for a hand-off. It exists because a CI image is not a phone, and a test that
157
+ # needs Chrome on an emulator that ships none should say so and be skipped
158
+ # rather than fail as though the product were broken.
159
+ #
160
+ # exit 4 for absent rather than exit 1, because "not there" is an answer.
161
+ installed)
162
+ [ $# -eq 1 ] || die "app.sh installed needs exactly one package name" 1
163
+ if pkg_installed "$1"; then
164
+ printf 'OK %s — installed\n' "$1"
165
+ else
166
+ die "$1 is not installed on $(device_serial)" 4
167
+ fi ;;
168
+
169
+ state)
170
+ [ $# -eq 0 ] || die "app.sh state takes no arguments" 1
171
+ printf 'OK %s — %s\n' "$PKG" "$(state_name)" ;;
172
+
173
+ launch)
174
+ [ $# -eq 0 ] || die "app.sh launch takes no arguments" 1
175
+ installed || die "$PKG is not installed — run $(as_cmd app install)" 4
176
+ do_launch
177
+ printf 'OK launched %s — %s\n' "$PKG" "$(state_name)" ;;
178
+
179
+ kill)
180
+ [ $# -eq 0 ] || die "app.sh kill takes no arguments" 1
181
+ adb_dev shell am force-stop "$PKG" >/dev/null 2>&1 \
182
+ || die "force-stop failed — run $(as_cmd doctor)" 3
183
+ ua2_drop_session
184
+ wait_for down 10 || die "force-stop returned but $PKG is still running" 4
185
+ printf 'OK killed %s — %s\n' "$PKG" "$(state_name)" ;;
186
+
187
+ relaunch)
188
+ [ $# -eq 0 ] || die "app.sh relaunch takes no arguments" 1
189
+ installed || die "$PKG is not installed — run $(as_cmd app install)" 4
190
+ adb_dev shell am force-stop "$PKG" >/dev/null 2>&1 || true
191
+ ua2_drop_session
192
+ wait_for down 10 || true
193
+ do_launch
194
+ printf 'OK relaunched %s — %s (stored data kept; use reset to discard it)\n' "$PKG" "$(state_name)" ;;
195
+
196
+ reset)
197
+ [ $# -eq 0 ] || die "app.sh reset takes no arguments" 1
198
+ installed || die "$PKG is not installed — run $(as_cmd app install)" 4
199
+ out="$(adb_dev shell pm clear "$PKG" 2>&1 | tr -d '\r')"
200
+ case "$out" in
201
+ *Success*) ;;
202
+ *) die "pm clear $PKG did not report success — $(first_line "$out")" 4 ;;
203
+ esac
204
+ ua2_drop_session
205
+ printf 'OK reset %s — data and cache cleared, app is %s\n' "$PKG" "$(state_name)" ;;
206
+
207
+ install)
208
+ [ $# -le 1 ] || die "app.sh install takes at most a path to an apk" 1
209
+ APK="${1-}"
210
+ [ -n "$APK" ] || APK="$(cfg '.app.apk' '')"
211
+ [ -n "$APK" ] || die "install needs an apk — pass a path or set app.apk in $DT_CONFIG" 2
212
+ APK="$(expand_path "$APK")"
213
+ [ -f "$APK" ] || die "no such apk: $APK" 2
214
+ # -r reinstalls over an existing copy and keeps data; -g grants the runtime
215
+ # permissions declared in the manifest, so a first run does not stop on a
216
+ # permission dialog that has nothing to do with the test.
217
+ out="$(adb_dev install -r -g "$APK" 2>&1 | tr -d '\r' | tail -3)"
218
+ case "$out" in
219
+ *Success*) ;;
220
+ *) die "install failed — $(printf '%s' "$out" | grep -m1 -i 'failure\|error' || printf '%s' "$out" | tail -1)" 2 ;;
221
+ esac
222
+ installed || die "install reported success but $PKG is not present" 4
223
+ ua2_drop_session
224
+ printf 'OK installed %s from %s\n' "$PKG" "$APK" ;;
225
+
226
+ build)
227
+ [ $# -eq 0 ] || die "app.sh build takes no arguments" 1
228
+ BUILD_CMD="$(cfg '.app.build_command' '')"
229
+ [ -n "$BUILD_CMD" ] || die "app.build_command is not set in $DT_CONFIG" 2
230
+ # The build's own output is the one place a wall of text is the right answer:
231
+ # a failing Gradle build is diagnosed from its log and nothing else. It goes
232
+ # to stderr so that stdout stays the one-line contract.
233
+ ( eval "$BUILD_CMD" ) >&2 || die "build failed — see the output above" 2
234
+ printf 'OK built %s\n' "$PKG" ;;
235
+
236
+ *)
237
+ die "unknown verb '$CMD' (usage: app.sh state|launch|kill|relaunch|reset|install|build)" 1 ;;
238
+ esac
239
+
240
+ # The screen the verb left behind, when it was asked for.
241
+ act_snapshot_now
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # back.sh — go back one screen.
3
+ #
4
+ # back.sh [--method nav|swipe]
5
+ #
6
+ # Android has a real system back action, so unlike iOS this is not a heuristic.
7
+ # `nav` sends the system back event and is the default. `swipe` performs a
8
+ # left-edge swipe, which is what gesture navigation sends and is worth having
9
+ # for the apps that handle the gesture but swallow the key event.
10
+ #
11
+ # The flag exists on both platforms so a test case does not have to know which
12
+ # one it is running on. What it selects differs, because the platforms differ,
13
+ # and pretending otherwise is how a cross-platform abstraction starts lying.
14
+ #
15
+ # It then verifies that the screen actually changed. A back that does nothing is
16
+ # worse than a back that fails: the test carries on believing it moved.
17
+ #
18
+ # --snapshot prints the screen this left behind and renumbers the uids, so the
19
+ # read that would have been the next call is folded into this one. In a measured
20
+ # session — log in, open Settings — twelve calls did the work and six of them
21
+ # were snapshots taken only to find out what the previous action produced.
22
+ #
23
+ # exit 0 went back, and the screen changed
24
+ # exit 1 usage error
25
+ # exit 3 UiAutomator2 unreachable — run doctor.sh
26
+ # exit 4 nothing to go back to, or the screen did not change
27
+
28
+ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
29
+
30
+ METHOD=nav
31
+ while [ $# -gt 0 ]; do
32
+ case "$1" in
33
+ --method)
34
+ [ $# -ge 2 ] || die "--method needs nav or swipe" 1
35
+ case "$2" in nav|swipe) METHOD="$2" ;; *) die "--method must be nav or swipe, got: $2" 1 ;; esac
36
+ shift 2 ;;
37
+ --snapshot) act_snapshot_on; shift ;;
38
+ -h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
39
+ *) die "unknown argument: $1 (usage: back.sh [--method nav|swipe])" 1 ;;
40
+ esac
41
+ done
42
+
43
+ load_config
44
+
45
+ src="$(fetch_source)"
46
+ before="$(drv_fingerprint "$src")"
47
+ act_before
48
+ read -r WIN_W WIN_H <<< "$(window_size "$src")"
49
+
50
+ if [ "$METHOD" = nav ]; then
51
+ resp="$(session_post '/back' '{}')"
52
+ err="$(session_error "$resp")"
53
+ [ -z "$err" ] || die "system back failed — $err" 3
54
+ used="system back"
55
+ else
56
+ # Kept off x=0 exactly: some devices treat the very edge as a bezel touch and
57
+ # discard it. The travel ends past the middle so the gesture is unambiguous.
58
+ FY=$(( WIN_H / 2 ))
59
+ TX=$(( WIN_W * 60 / 100 ))
60
+ err="$(drv_drag 2 "$FY" "$TX" "$FY" 50 200 || true)"
61
+ [ -z "$err" ] || die "edge swipe failed — $err" 3
62
+ used="edge swipe from 2,$FY"
63
+ fi
64
+
65
+ after="$(drv_fingerprint "$(fetch_source)")"
66
+ [ "$before" != "$after" ] \
67
+ || die "went back using the $used but the screen did not change — there may be nothing to go back to" 4
68
+
69
+ # back already refuses when the screen did not move, so the SCREEN line here
70
+ # is not the verdict — it is which screen you have arrived at, and what the app
71
+ # said on the way.
72
+ act_report BACK "$used"
73
+ journal_append back "" "" "" "" "" 0 0 0 0 "$DT_ACT_HASH"
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # controls.sh — the things on this screen you can actually act on.
3
+ #
4
+ # controls.sh [--all]
5
+ #
6
+ # One line per interactive element, in find.sh's columns:
7
+ #
8
+ # type | id | label | text | x,y,w,h
9
+ #
10
+ # This is usually a better first question than `tree`. A screen with 150
11
+ # elements has perhaps a dozen you can tap, and those dozen are what a test is
12
+ # written against; the rest is layout.
13
+ #
14
+ # Android answers this exactly: `clickable` is published on the element. iOS has
15
+ # to infer it from the element's type, so the two adapters disagree slightly in
16
+ # what they can promise, and each says which.
17
+ #
18
+ # --all also includes elements that are focusable or long-clickable but not
19
+ # clickable — scroll containers, and controls that only respond to a hold.
20
+ #
21
+ # exit 0 at least one control
22
+ # exit 1 usage error
23
+ # exit 3 UiAutomator2 unreachable — run doctor.sh
24
+ # exit 4 nothing interactive on screen
25
+
26
+ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
27
+
28
+ ALL=0
29
+ while [ $# -gt 0 ]; do
30
+ case "$1" in
31
+ --all) ALL=1; shift ;;
32
+ -h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
33
+ *) die "unknown argument: $1 (usage: controls.sh [--all])" 1 ;;
34
+ esac
35
+ done
36
+
37
+ load_config
38
+ src="$(fetch_source)"
39
+
40
+ # Columns of the table: depth|class|id|label|text|x|y|w|h|vis|en|pw|click|hint
41
+ rows="$(printf '%s' "$src" | awk -F'|' -v all="$ALL" '
42
+ $1 == "WINDOW" || $1 == "" { next }
43
+ $10 != 1 || $8 <= 0 || $9 <= 0 { next }
44
+ {
45
+ interactive = ($13 == 1)
46
+ # Text inputs are acted on by typing rather than by tapping, and a field
47
+ # that is not marked clickable is still very much a control.
48
+ if (!interactive && $2 ~ /Edit/) interactive = 1
49
+ if (!interactive && all == 1 && $11 == 1) interactive = 1
50
+ if (!interactive) next
51
+ printf "%s|%s|%s|%s|%s,%s,%s,%s\n", $2, $3, $4, $5, $6, $7, $8, $9
52
+ }')"
53
+
54
+ [ -n "$rows" ] || die "nothing interactive on screen — is the app in the foreground?" 4
55
+
56
+ printf '%s\n' "$rows" | awk -F'|' '{ printf "%s | %s | %s | %s | %s\n", $1, $2, $3, $4, $5 }'
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # devices.sh — what is attached, one per line.
3
+ #
4
+ # devices.sh [--ready]
5
+ #
6
+ # Output, pipe-separated so it parses without a JSON reader:
7
+ #
8
+ # <serial> | <kind> | <name> | <state>
9
+ #
10
+ # kind is emulator or device. state is ready, unauthorized, offline or
11
+ # no-permissions — anything other than ready means the device is attached but
12
+ # cannot be driven, and saying which is the difference between a five-second fix
13
+ # and an afternoon.
14
+ #
15
+ # --ready lists only the ones that can be driven.
16
+ #
17
+ # It reads `adb devices -l` and nothing else: no `adb shell` per device, because
18
+ # this runs before anything is known to be healthy and a shell into a wedged
19
+ # device is exactly where this would hang.
20
+ #
21
+ # exit 0 at least one device listed
22
+ # exit 1 usage error
23
+ # exit 2 adb is not installed
24
+ # exit 4 nothing attached (or nothing ready, with --ready)
25
+
26
+ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
27
+
28
+ READY_ONLY=0
29
+ while [ $# -gt 0 ]; do
30
+ case "$1" in
31
+ --ready) READY_ONLY=1; shift ;;
32
+ -h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
33
+ *) die "unknown argument: $1 (usage: devices.sh [--ready])" 1 ;;
34
+ esac
35
+ done
36
+
37
+ load_config
38
+ command -v adb >/dev/null 2>&1 || die "adb is not on PATH — see README.md" 2
39
+
40
+ n=0
41
+ while IFS= read -r line; do
42
+ [ -n "$line" ] || continue
43
+ case "$line" in "List of devices"*|"* daemon"*) continue ;; esac
44
+
45
+ serial="${line%% *}"
46
+ rest="${line#"$serial"}"
47
+ raw_state="$(printf '%s' "$rest" | awk '{print $1}')"
48
+
49
+ case "$raw_state" in
50
+ device) state=ready ;;
51
+ unauthorized) state=unauthorized ;;
52
+ offline) state=offline ;;
53
+ no) state=no-permissions ;; # "no permissions; see [http://…]"
54
+ *) state="$raw_state" ;;
55
+ esac
56
+
57
+ # The serial prefix is enough to tell an emulator apart here. Elsewhere the
58
+ # adapter also checks ro.kernel.qemu, because a renamed serial could mislead
59
+ # something that matters; a listing is not that.
60
+ case "$serial" in emulator-*) kind=emulator ;; *) kind=device ;; esac
61
+
62
+ name="$(printf '%s' "$rest" | tr ' ' '\n' | awk -F: '$1=="model" { print $2; exit }')"
63
+ [ -n "$name" ] || name="$serial"
64
+ name="$(printf '%s' "$name" | tr '_' ' ')"
65
+
66
+ [ "$READY_ONLY" -eq 0 ] || [ "$state" = ready ] || continue
67
+
68
+ printf '%s | %s | %s | %s\n' "$serial" "$kind" "$name" "$state"
69
+ n=$(( n + 1 ))
70
+ done <<< "$(adb devices -l 2>/dev/null | tr -d '\r')"
71
+
72
+ if [ "$n" -eq 0 ]; then
73
+ if [ "$READY_ONLY" -eq 1 ]; then
74
+ die "no device is ready — run $(as_cmd devices) without --ready to see what is attached" 4
75
+ fi
76
+ die "nothing attached — connect a device or start an emulator" 4
77
+ fi