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
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env bash
2
+ # doctor.sh — verify (and cheaply repair) everything the other scripts assume.
3
+ #
4
+ # Run this before the first action of a session, and again whenever any script
5
+ # exits 3. Output is one fixed line per check, always the same checks in the same
6
+ # order, so it can be parsed positionally:
7
+ #
8
+ # <STATUS> <name> — <detail>
9
+ #
10
+ # STATUS is OK, WARN or FAIL. Only FAIL affects the exit code.
11
+ # exit 0 every required check passed
12
+ # exit 2 at least one required check failed
13
+ #
14
+ # Repairs: iproxy is started when missing, always, because it is cheap, local
15
+ # and idempotent. WebDriverAgent is started only with --start-wda, because that
16
+ # is a long-running xcodebuild against the device and starting it implicitly
17
+ # would hide the reason it stopped.
18
+ #
19
+ # --start-wda start the WebDriverAgent runner if it is not answering
20
+ # --recover the same, spelled the way every other script spells it when
21
+ # it tells you to run this
22
+ #
23
+ # The runner does die on its own — twice during a single day of development —
24
+ # so a caller with no human attached needs a way to get back on its feet. This
25
+ # is that way, and it is opt-in rather than automatic.
26
+
27
+ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
28
+
29
+ START_WDA=0
30
+ while [ $# -gt 0 ]; do
31
+ case "$1" in
32
+ --start-wda) START_WDA=1; shift ;;
33
+ # --recover is the platform-neutral spelling, so that any caller can ask any
34
+ # adapter to put itself right without knowing what "right" involves here.
35
+ --recover) START_WDA=1; shift ;;
36
+ -h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
37
+ *) die "unknown argument: $1 (usage: doctor.sh [--start-wda|--recover])" 1 ;;
38
+ esac
39
+ done
40
+
41
+ FAILED=0
42
+
43
+ # How long a freshly started WebDriverAgent runner is given to answer. See the
44
+ # loop that uses it for why it is no longer ninety seconds.
45
+ WDA_START_SECONDS=240
46
+
47
+ # The status word is the whole point of these nine lines, so it is the only
48
+ # thing tinted. The check names stay plain: they are a fixed list in a fixed
49
+ # order and colouring them would suggest one matters more than another.
50
+ say() {
51
+ local col=""
52
+ case "$1" in OK) col="$C_PASS" ;; WARN) col="$C_NOTE" ;; FAIL) col="$C_FAIL" ;; esac
53
+ printf '%s%s%s %s — %s\n' "$col" "$1" "${col:+$C_OFF}" "$2" "$3"
54
+ [ "$1" = FAIL ] && FAILED=1
55
+ return 0
56
+ }
57
+
58
+ # Why the runner started and never answered, read out of xcodebuild's own log.
59
+ # One line, cause first and remedy second, because the caller may be a fleet
60
+ # worker with nobody watching and this line is all it will report.
61
+ #
62
+ # The order is most-specific first: a signing failure also prints a launch
63
+ # failure underneath it, and naming the outer symptom would send someone to the
64
+ # wrong place.
65
+ wda_start_failure() {
66
+ local log; log="$(state_dir)/wda.log"
67
+ [ -f "$log" ] || { echo "started the runner but it never answered"; return; }
68
+
69
+ if grep -q "Timed out while enabling automation mode" "$log"; then
70
+ echo "the device refused automation mode — turn on Settings > Developer > Enable UI Automation on this device, and leave the screen unlocked"
71
+ elif grep -qE "ApplicationVerificationFailed|valid provisioning profile|Failed to codesign|code signature" "$log"; then
72
+ echo "the runner is not signed for this device — register $UDID in team $(cfg '.wda.team_id' '?') and rebuild WebDriverAgent"
73
+ elif grep -q "Missing test product" "$log"; then
74
+ echo "the .xctestrun points at a test bundle that is not beside it — rebuild WebDriverAgent"
75
+ elif grep -qE "device is locked|passcode|Unlock the device" "$log"; then
76
+ echo "the device is locked — unlock it and leave it awake"
77
+ elif grep -qE "Unable to (install|launch)|Failed to install" "$log"; then
78
+ echo "the runner could not be installed on the device"
79
+ else
80
+ # Unknown, so quote what xcodebuild actually said rather than inventing a
81
+ # cause. Truncated because this has to stay one line.
82
+ local raw
83
+ raw="$(grep -m1 -E "^Testing failed:|encountered an error" "$log" 2>/dev/null \
84
+ | tr -s ' \t' ' ' | sed 's/^ *//' | head -c 160)"
85
+ echo "started the runner but it never answered${raw:+ — $raw}"
86
+ fi
87
+ }
88
+
89
+ # --- 1. xcode -----------------------------------------------------------------
90
+ if ! command -v xcodebuild >/dev/null 2>&1; then
91
+ say FAIL xcode "xcodebuild not found — install Xcode and run xcode-select --switch"
92
+ else
93
+ say OK xcode "$(xcodebuild -version 2>/dev/null | tr '\n' ' ' | sed 's/ *$//') at $(xcode-select -p)"
94
+ fi
95
+
96
+ # --- 2. jq --------------------------------------------------------------------
97
+ if command -v jq >/dev/null 2>&1; then
98
+ say OK jq "$(jq --version)"
99
+ else
100
+ say FAIL jq "not found — brew install jq"
101
+ exit 2 # every later check needs jq to read the config
102
+ fi
103
+
104
+ load_config
105
+ UDID="$(cfg '.device.udid')"
106
+ WANT_IOS="$(cfg '.device.ios_version' '')"
107
+
108
+ KIND="$(ios_kind)"
109
+
110
+ # --- 3. device ----------------------------------------------------------------
111
+ #
112
+ # The check names and their order never change between a simulator and a real
113
+ # device, because AGENTS.md promises callers a fixed nine lines. What changes is
114
+ # what each one means, and the detail says which.
115
+ DEVICE_OK=0
116
+ if [ "$KIND" = simulator ]; then
117
+ if ! xcrun simctl list devices -j 2>/dev/null | jq -e --arg u "$UDID" \
118
+ '[.devices[][] | select(.udid == $u)] | length > 0' >/dev/null 2>&1; then
119
+ say FAIL device "simulator $UDID not found — xcrun simctl list devices"
120
+ elif ! sim_booted; then
121
+ say FAIL device "simulator $UDID is not booted — xcrun simctl boot $UDID"
122
+ else
123
+ simname="$(xcrun simctl list devices -j 2>/dev/null | jq -r --arg u "$UDID" \
124
+ '[.devices | to_entries[] | .key as $rt | .value[] | select(.udid == $u) | "\(.name) (\($rt | sub(".*SimRuntime.";"") | gsub("-";" ")))"][0] // "?"')"
125
+ DEVICE_OK=1
126
+ say OK device "simulator $simname ($UDID)"
127
+ fi
128
+ elif ! command -v idevice_id >/dev/null 2>&1; then
129
+ say FAIL device "idevice_id not found — brew install libimobiledevice"
130
+ elif ! idevice_id -l 2>/dev/null | grep -qx "$UDID"; then
131
+ attached="$(idevice_id -l 2>/dev/null | tr '\n' ' ' | sed 's/ *$//')"
132
+ say FAIL device "$UDID not attached — attached: ${attached:-none}"
133
+ else
134
+ got_ios="$(ideviceinfo -u "$UDID" -k ProductVersion 2>/dev/null || echo '?')"
135
+ got_name="$(ideviceinfo -u "$UDID" -k DeviceName 2>/dev/null || echo '?')"
136
+ DEVICE_OK=1
137
+ if [ -n "$WANT_IOS" ] && [ "$got_ios" != "$WANT_IOS" ]; then
138
+ say WARN device "$got_name iOS $got_ios attached, config says $WANT_IOS"
139
+ else
140
+ say OK device "$got_name iOS $got_ios ($UDID)"
141
+ fi
142
+ fi
143
+
144
+ # --- 4. developer disk image --------------------------------------------------
145
+ # Required to launch an XCUITest runner. Unmounts on every device reboot, and
146
+ # Xcode remounts it only when it drives the device itself, so this check catches
147
+ # the single most common "WDA suddenly will not start" cause.
148
+ if [ "$KIND" = simulator ]; then
149
+ say OK ddi "not applicable — a simulator needs no developer disk image"
150
+ elif [ "$DEVICE_OK" -eq 0 ]; then
151
+ say FAIL ddi "skipped — device check failed"
152
+ elif ! command -v ideviceimagemounter >/dev/null 2>&1; then
153
+ say FAIL ddi "ideviceimagemounter not found — brew install libimobiledevice"
154
+ else
155
+ # COUNT THE SIGNATURES, DO NOT READ THE HIGHEST INDEX.
156
+ #
157
+ # The first version took the number out of `ImageSignature[0]:` and required
158
+ # it to be greater than zero. On an iOS 15 device several images are listed,
159
+ # so an index of 1 or more happens to appear and the check passed by luck. An
160
+ # iOS 27 device mounts exactly one, at index 0, and the check read the single
161
+ # mounted image as none — reporting a perfectly healthy phone as broken.
162
+ n="$(ideviceimagemounter -u "$UDID" list 2>/dev/null | grep -c '^ImageSignature\[')"
163
+ if [ "${n:-0}" -gt 0 ] 2>/dev/null; then
164
+ say OK ddi "developer disk image mounted ($n signature(s))"
165
+ else
166
+ say FAIL ddi "no developer disk image mounted — see README.md 'Developer disk image'"
167
+ fi
168
+ fi
169
+
170
+ # --- 5. iproxy ----------------------------------------------------------------
171
+ LPORT="$(cfg '.wda.local_port' '8100')"
172
+ DPORT="$(cfg '.wda.device_port' '8100')"
173
+ IPROXY_OK=0
174
+ if [ "$KIND" = simulator ]; then
175
+ # A simulator shares the host's network stack, so WebDriverAgent is already on
176
+ # localhost and there is nothing to forward.
177
+ say OK iproxy "not needed — a simulator listens on the host directly"
178
+ IPROXY_OK=1
179
+ elif port_open "$LPORT"; then
180
+ say OK iproxy "already forwarding ${LPORT}->${DPORT}"
181
+ IPROXY_OK=1
182
+ elif [ "$DEVICE_OK" -eq 0 ]; then
183
+ say FAIL iproxy "skipped — device check failed"
184
+ elif ! command -v iproxy >/dev/null 2>&1; then
185
+ say FAIL iproxy "iproxy not found — brew install libimobiledevice"
186
+ else
187
+ sd="$(state_dir)"
188
+ nohup iproxy -u "$UDID" "$LPORT:$DPORT" >"$sd/iproxy.log" 2>&1 &
189
+ echo $! >"$sd/iproxy.pid"
190
+ for _ in $(seq 1 25); do
191
+ port_open "$LPORT" && break
192
+ sleep 0.2
193
+ done
194
+ if port_open "$LPORT"; then
195
+ say OK iproxy "started ${LPORT}->${DPORT} (pid $(cat "$sd/iproxy.pid"))"
196
+ IPROXY_OK=1
197
+ else
198
+ say FAIL iproxy "failed to bind $LPORT — see $sd/iproxy.log"
199
+ fi
200
+ fi
201
+
202
+ # --- 6. WebDriverAgent HTTP ---------------------------------------------------
203
+ WDA_OK=0
204
+ if [ "$IPROXY_OK" -eq 0 ]; then
205
+ say FAIL wda "skipped — no port forward"
206
+ else
207
+ status="$(curl -sS -m "$(http_timeout)" "$(wda_base)/status" 2>/dev/null || true)"
208
+
209
+ if { [ -z "$status" ] || ! printf '%s' "$status" | jq -e '.value' >/dev/null 2>&1; } \
210
+ && [ "$START_WDA" -eq 1 ]; then
211
+ xctestrun="$(ls "$(wda_derived)"/Build/Products/*.xctestrun 2>/dev/null | head -1 || true)"
212
+ if [ -z "$xctestrun" ]; then
213
+ say FAIL wda "no .xctestrun in $(wda_derived) — WebDriverAgent has not been built for this target; run: scripts/setup.sh"
214
+ else
215
+ sd="$(state_dir)"
216
+
217
+ # ON A SIMULATOR THE PORT IS WDA'S OWN, NOT A TUNNEL'S.
218
+ #
219
+ # A physical device is reached through iproxy: WebDriverAgent always
220
+ # listens on 8100 on the device, and the host side picks whatever local
221
+ # port it likes. Nothing has to be told anything.
222
+ #
223
+ # A simulator listens on the host directly, so the port in the config is
224
+ # the port WDA itself must bind — and the only way to tell it is USE_PORT
225
+ # in the runner's environment, which for xcodebuild means the .xctestrun
226
+ # rather than this shell. Without this, every simulator would bind 8100
227
+ # and the second one would lose, silently, to a fleet that then drove the
228
+ # first simulator twice.
229
+ #
230
+ # MJPEG_SERVER_PORT gets the same treatment for the same reason; WDA
231
+ # defaults it to 9100 and two of them would collide.
232
+ #
233
+ # The modified copy is written BESIDE THE ORIGINAL, not into the state
234
+ # directory. Every path inside an .xctestrun is relative to __TESTROOT__,
235
+ # which xcodebuild resolves to the directory the file is sitting in — move
236
+ # the file and it looks for the test bundle next to the copy, does not
237
+ # find it, and fails with "Missing test product" naming a directory that
238
+ # was never going to contain one. Measured, once.
239
+ if [ "$KIND" = simulator ]; then
240
+ port_xctestrun="$(dirname "$xctestrun")/devicetools-$LPORT.xctestrun"
241
+ if cp "$xctestrun" "$port_xctestrun" 2>/dev/null \
242
+ && plutil -replace WebDriverAgentRunner.EnvironmentVariables.USE_PORT \
243
+ -string "$LPORT" "$port_xctestrun" 2>/dev/null \
244
+ && plutil -replace WebDriverAgentRunner.EnvironmentVariables.MJPEG_SERVER_PORT \
245
+ -string "$(( LPORT + 1000 ))" "$port_xctestrun" 2>/dev/null; then
246
+ xctestrun="$port_xctestrun"
247
+ else
248
+ rm -f "$port_xctestrun"
249
+ fi
250
+ fi
251
+
252
+ # A RUNNER THAT STOPPED ANSWERING IS STILL A RUNNER.
253
+ #
254
+ # Reaching here means /status did not answer, and --recover is the retry
255
+ # path a caller takes when it loses the connection. It used to start a
256
+ # second xcodebuild and overwrite wda.pid, orphaning the first: two
257
+ # runners driving one simulator, both bound to the same port, answering
258
+ # curl 52 (empty reply) about half the time. Measured — three of them
259
+ # accumulated in one session, and the run they broke looked like an
260
+ # application fault, which is the worst way for this to present.
261
+ #
262
+ # A flaky connection is exactly the condition that calls this repeatedly,
263
+ # so an unbounded supply of runners is what the old code promised.
264
+ if [ -f "$sd/wda.pid" ]; then
265
+ old="$(cat "$sd/wda.pid" 2>/dev/null || true)"
266
+ if [ -n "$old" ] && kill -0 "$old" 2>/dev/null; then
267
+ kill "$old" 2>/dev/null || true
268
+ for _ in 1 2 3 4 5; do kill -0 "$old" 2>/dev/null || break; sleep 1; done
269
+ kill -9 "$old" 2>/dev/null || true
270
+ fi
271
+ fi
272
+
273
+ nohup xcodebuild test-without-building -xctestrun "$xctestrun" \
274
+ -destination "id=$UDID" >"$sd/wda.log" 2>&1 &
275
+ echo $! >"$sd/wda.pid"
276
+ # A COLD MACHINE IS NOT A WARM ONE, AND CI IS ALWAYS COLD.
277
+ #
278
+ # Ninety seconds was measured on a laptop whose simulator had been booted
279
+ # for hours. On a GitHub runner the simulator was minted a minute earlier
280
+ # and the runner app is being launched for the first time: one CI run
281
+ # answered inside the budget and the next one did not, on identical code.
282
+ # Nothing waits this long normally — it polls, so a warm start still
283
+ # returns in a second or two, and the number only decides how patient the
284
+ # failure is. The Android adapter learned the same lesson on the same day.
285
+ for _ in $(seq 1 "$WDA_START_SECONDS"); do
286
+ curl -sS -m 3 "$(wda_base)/status" >/dev/null 2>&1 && break
287
+ sleep 1
288
+ done
289
+ status="$(curl -sS -m "$(http_timeout)" "$(wda_base)/status" 2>/dev/null || true)"
290
+ fi
291
+ fi
292
+
293
+ if [ -z "$status" ] || ! printf '%s' "$status" | jq -e '.value' >/dev/null 2>&1; then
294
+ if [ "$START_WDA" -eq 1 ]; then
295
+ # THE LOG KNOWS WHY. SAY IT.
296
+ #
297
+ # "see the log" is the answer a tool gives when it has not looked. Every
298
+ # cause below is a fixed string xcodebuild prints, and each one has a
299
+ # different remedy — a per-device setting, a signing problem, and a
300
+ # locked screen are not the same failure and must not read as one.
301
+ #
302
+ # Found by putting a second iPhone on the fleet: the runner installed,
303
+ # launched, and timed out enabling automation mode, and the only thing
304
+ # this line said was to go and read a 400-line xcodebuild log.
305
+ say FAIL wda "$(wda_start_failure) after ${WDA_START_SECONDS}s — see $(state_dir)/wda.log"
306
+ else
307
+ say FAIL wda "no answer on $(wda_base)/status — retry with: $(as_cmd doctor --recover)"
308
+ fi
309
+ else
310
+ ios="$(printf '%s' "$status" | jq -r '.value.os.version // "?"')"
311
+ ver="$(printf '%s' "$status" | jq -r '.value.build.version // .value.build.time // "?"')"
312
+ say OK wda "ready, WDA $ver driving iOS $ios"
313
+ WDA_OK=1
314
+ fi
315
+ fi
316
+
317
+ # --- 7. WebDriverAgent session ------------------------------------------------
318
+ # A live HTTP port is not proof that XCUITest can still drive the UI: the runner
319
+ # outlives its test bundle in some failure modes. Creating a real session is.
320
+ if [ "$WDA_OK" -eq 0 ]; then
321
+ say FAIL wda-session "skipped — WDA not answering"
322
+ else
323
+ resp="$(curl -sS -m "$(http_timeout)" -X POST -H 'Content-Type: application/json' \
324
+ -d '{"capabilities":{"alwaysMatch":{}}}' "$(wda_base)/session" 2>/dev/null || true)"
325
+ sid="$(printf '%s' "$resp" | jq -r '.value.sessionId // .sessionId // empty' 2>/dev/null || true)"
326
+ if [ -z "$sid" ]; then
327
+ msg="$(printf '%s' "$resp" | jq -r '.value.error // .value.message // "unknown error"' 2>/dev/null | head -c 120)"
328
+ say FAIL wda-session "could not create a session — $msg"
329
+ else
330
+ # The session capabilities WDA returns carry no bundle id, so ask what is
331
+ # actually on screen. That also proves the runner can introspect a foreign
332
+ # app, which is the capability every other script depends on.
333
+ fg="$(curl -sS -m "$(http_timeout)" "$(wda_base)/wda/activeAppInfo" 2>/dev/null \
334
+ | jq -r '.value.bundleId // empty' 2>/dev/null || true)"
335
+ curl -sS -m "$(http_timeout)" -X DELETE "$(wda_base)/session/$sid" >/dev/null 2>&1 || true
336
+ say OK wda-session "created and released, foreground app ${fg:-unknown}"
337
+ fi
338
+ fi
339
+
340
+ # --- 8. app under test --------------------------------------------------------
341
+ APP_ID="$(cfg '.app.bundle_id' '')"
342
+ if [ -z "$APP_ID" ]; then
343
+ say WARN app "app.bundle_id not set in $DT_CONFIG — install and launch checks unavailable"
344
+ elif [ "$DEVICE_OK" -eq 0 ]; then
345
+ say FAIL app "skipped — device check failed"
346
+ elif [ "$KIND" = simulator ]; then
347
+ if xcrun simctl get_app_container "$UDID" "$APP_ID" app >/dev/null 2>&1; then
348
+ say OK app "$APP_ID installed"
349
+ else
350
+ say FAIL app "$APP_ID not installed on the simulator — $(as_cmd app install '<path>')"
351
+ fi
352
+ elif ! command -v ios-deploy >/dev/null 2>&1; then
353
+ say FAIL app "ios-deploy not found — brew install ios-deploy"
354
+ elif ios-deploy --id "$UDID" --no-wifi --exists --bundle_id "$APP_ID" >/dev/null 2>&1; then
355
+ # The device does not volunteer a version, so what is reported is what
356
+ # DeviceTools put there — a smaller claim, and a true one. Anything installed
357
+ # by other means says so rather than being described from a stale record.
358
+ rec="$(last_install)"
359
+ rec_id="$(printf '%s' "$rec" | cut -f1)"
360
+ if [ -n "$rec" ] && [ "$rec_id" = "$APP_ID" ]; then
361
+ say OK app "$APP_ID installed — $(printf '%s' "$rec" | cut -f2) ($(printf '%s' "$rec" | cut -f3)), put there by DeviceTools at $(printf '%s' "$rec" | cut -f5)"
362
+ else
363
+ say OK app "$APP_ID installed — version unknown, not installed by DeviceTools"
364
+ fi
365
+ else
366
+ say FAIL app "$APP_ID not installed — $(as_cmd app install)"
367
+ fi
368
+
369
+ # THERE IS NO SECTION 9, AND THERE WILL NOT BE ONE.
370
+ #
371
+ # It read the signing identities and provisioning profiles on this machine and
372
+ # tried to say whether a build would install. Deleted at the second complaint,
373
+ # and the complaint was right: this tool owns the phone from "the app is
374
+ # installed" onward. Whose certificate signs the app, which team owns it and
375
+ # which profile Xcode picked are the build's business, and a check standing
376
+ # outside the build can only guess at them — it guessed wrong twice, naming a
377
+ # team the project has nothing to do with, on a machine with fifteen of them.
378
+ #
379
+ # `app install` still refuses an unsigned bundle. That is a different question:
380
+ # it is about whether the verb in front of you will work, not about auditing
381
+ # somebody's account. It also comes from the bundle itself rather than from
382
+ # what happens to be lying in the keychain.
383
+ #
384
+ # profiles_for and project_signing went with it.
385
+
386
+ exit $(( FAILED ? 2 : 0 ))