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.
- package/AGENTS.md +327 -0
- package/LICENSE +21 -0
- package/README.md +491 -0
- package/VERSION +1 -0
- package/bin/device-devtools-mcp.js +106 -0
- package/bin/devicetools +315 -0
- package/config.example.json +63 -0
- package/integrations/agent-pointer.sh +48 -0
- package/integrations/claude/SKILL.md +8 -0
- package/integrations/cursor/devicetools.mdc +8 -0
- package/integrations/gemini/GEMINI.md +5 -0
- package/integrations/mcp/README.md +184 -0
- package/integrations/mcp/mcp.json +10 -0
- package/integrations/mcp/reference.sh +119 -0
- package/integrations/mcp/selftest.sh +158 -0
- package/integrations/mcp/server.sh +343 -0
- package/package.json +50 -0
- package/scripts/android/app.sh +241 -0
- package/scripts/android/back.sh +73 -0
- package/scripts/android/controls.sh +56 -0
- package/scripts/android/devices.sh +77 -0
- package/scripts/android/doctor.sh +253 -0
- package/scripts/android/lib.sh +699 -0
- package/scripts/android/logs.sh +289 -0
- package/scripts/android/permission.sh +119 -0
- package/scripts/android/settings.sh +63 -0
- package/scripts/android/setup.sh +97 -0
- package/scripts/android/tree.awk +166 -0
- package/scripts/android/tree.sh +127 -0
- package/scripts/android/type.sh +191 -0
- package/scripts/common/find.sh +95 -0
- package/scripts/common/key.sh +65 -0
- package/scripts/common/lib.sh +14 -0
- package/scripts/common/measure.sh +170 -0
- package/scripts/common/open.sh +131 -0
- package/scripts/common/screenshot.sh +102 -0
- package/scripts/common/scroll.sh +177 -0
- package/scripts/common/snapshot.sh +69 -0
- package/scripts/common/swipe.sh +171 -0
- package/scripts/common/tap.sh +226 -0
- package/scripts/common/wait.sh +212 -0
- package/scripts/common/waypoint.sh +141 -0
- package/scripts/dispatch.sh +21 -0
- package/scripts/flow.sh +266 -0
- package/scripts/init.sh +101 -0
- package/scripts/ios/app.sh +404 -0
- package/scripts/ios/back.sh +95 -0
- package/scripts/ios/controls.sh +68 -0
- package/scripts/ios/devices.sh +80 -0
- package/scripts/ios/doctor.sh +386 -0
- package/scripts/ios/lib.sh +864 -0
- package/scripts/ios/logs.sh +272 -0
- package/scripts/ios/permission.sh +108 -0
- package/scripts/ios/settings.sh +76 -0
- package/scripts/ios/setup.sh +175 -0
- package/scripts/ios/tree.sh +128 -0
- package/scripts/ios/type.sh +178 -0
- package/scripts/lib.sh +1032 -0
- package/scripts/links.tsv +44 -0
- package/scripts/relink.sh +121 -0
- package/scripts/run.sh +415 -0
- package/scripts/selftest.sh +1709 -0
- package/scripts/snapshot.awk +362 -0
- package/scripts/verify-npm-package.js +133 -0
- package/tests/fixtures/ios-contacts-list.expected +52 -0
- package/tests/fixtures/ios-contacts-list.rows +140 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# snapshot.awk — normalised rows in, a screen an agent can read out.
|
|
2
|
+
#
|
|
3
|
+
# awk -v ALL=0 -v GREP="" -v MINHIT=44 [-v MODE=store] -f snapshot.awk < rows
|
|
4
|
+
#
|
|
5
|
+
# Everything here is arithmetic over rectangles and tests over strings. There is
|
|
6
|
+
# no estimate anywhere in this file, and that is the rule it is written to keep:
|
|
7
|
+
# a warning that is wrong one time in five is a warning an agent learns to skip,
|
|
8
|
+
# which is worse than not having it. So "the text does not fit" is not computed
|
|
9
|
+
# from a character count — the only truncation reported is the one the operating
|
|
10
|
+
# system already performed and left an ellipsis to prove.
|
|
11
|
+
#
|
|
12
|
+
# ALL=1 show every element, no filter
|
|
13
|
+
# GREP=s case-insensitive substring of type, id, label or value
|
|
14
|
+
# MINHIT=n the minimum comfortable hit area, in points (44 iOS, 48 Android)
|
|
15
|
+
# MODE "display" (default) or "store", which emits the uid table instead
|
|
16
|
+
#
|
|
17
|
+
# Input, one element per line, twelve '|' separated fields, produced by each
|
|
18
|
+
# adapter's snapshot_rows:
|
|
19
|
+
#
|
|
20
|
+
# WINDOW|<w>|<h>|<orientation>
|
|
21
|
+
# <n>|<depth>|<parent-n>|<type>|<id>|<label>|<value>|<x>|<y>|<w>|<h>|<flags>
|
|
22
|
+
#
|
|
23
|
+
# exit 0 a screen was printed
|
|
24
|
+
# exit 4 the rows were empty, or nothing survived the filter
|
|
25
|
+
|
|
26
|
+
BEGIN {
|
|
27
|
+
FS = "|"
|
|
28
|
+
n = 0
|
|
29
|
+
if (MODE == "") MODE = "display"
|
|
30
|
+
|
|
31
|
+
# HOW MANY POINTS COUNT AS NOTHING.
|
|
32
|
+
#
|
|
33
|
+
# Both drivers report rectangles as floating point and this pipeline floors
|
|
34
|
+
# them, so each edge carries up to a point of error and any pair of edges up
|
|
35
|
+
# to two. A "1pt overlap" between two stacked lines of text is therefore not a
|
|
36
|
+
# layout bug, it is the arithmetic.
|
|
37
|
+
#
|
|
38
|
+
# Measured on a real, dense screen before this existed: a list of six
|
|
39
|
+
# contacts produced thirty-one warnings, of which twenty-three were overlaps
|
|
40
|
+
# of one to two points between text lines that are visibly not touching, and
|
|
41
|
+
# four were labels sitting four points outside a table cell that clips
|
|
42
|
+
# nothing. The eight that mattered — a 24x25 button, and the same button
|
|
43
|
+
# hanging eight points off the right edge — were buried in them.
|
|
44
|
+
#
|
|
45
|
+
# So anything at or below this is not reported. It is a floor on what this
|
|
46
|
+
# file will claim to have seen, not a judgement about what matters.
|
|
47
|
+
if (TOL == "") TOL = 2
|
|
48
|
+
# A byte table, because macOS awk has no ord() and its length()/substr() count
|
|
49
|
+
# bytes rather than characters. Every byte value matters: fold the non-ASCII
|
|
50
|
+
# ones together and two Vietnamese labels differing only in their diacritics
|
|
51
|
+
# would hash the same, which would report two screens as one and take change
|
|
52
|
+
# detection quietly out of service.
|
|
53
|
+
for (i = 1; i < 256; i++) ORD[sprintf("%c", i)] = i
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$1 == "WINDOW" {
|
|
57
|
+
WW = $2 + 0; WH = $3 + 0; ORIENT = $4
|
|
58
|
+
next
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$0 == "" { next }
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
n++
|
|
65
|
+
serial[n] = $1 + 0
|
|
66
|
+
depth[n] = $2 + 0
|
|
67
|
+
parent[n] = $3 + 0
|
|
68
|
+
type[n] = $4
|
|
69
|
+
id[n] = $5
|
|
70
|
+
label[n] = $6
|
|
71
|
+
value[n] = $7
|
|
72
|
+
x[n] = $8 + 0; y[n] = $9 + 0; w[n] = $10 + 0; h[n] = $11 + 0
|
|
73
|
+
flags[n] = $12
|
|
74
|
+
byserial[$1 + 0] = n
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function has(f, s) { return index("," f ",", "," s ",") > 0 }
|
|
78
|
+
function best(i) { return label[i] != "" ? label[i] : (value[i] != "" ? value[i] : id[i]) }
|
|
79
|
+
|
|
80
|
+
# Covers the window, give or take the same two points everything else gets.
|
|
81
|
+
function fullscreen(i) {
|
|
82
|
+
if (WW <= 0 || WH <= 0) return 0
|
|
83
|
+
return (x[i] <= TOL && y[i] <= TOL \
|
|
84
|
+
&& w[i] >= WW - TOL && h[i] >= WH - TOL)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Overlap of two intervals, in points. Negative or zero means no overlap.
|
|
88
|
+
function span(a1, a2, b1, b2, lo, hi) {
|
|
89
|
+
lo = (a1 > b1) ? a1 : b1
|
|
90
|
+
hi = (a2 < b2) ? a2 : b2
|
|
91
|
+
return hi - lo
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# An element is shown when it is visible AND it carries something to identify it
|
|
95
|
+
# by, or is something a person can act on. An invisible element with a label is
|
|
96
|
+
# noise; an unlabelled button is not, because a screen whose only three controls
|
|
97
|
+
# are unlabelled would otherwise read as having nothing to tap.
|
|
98
|
+
#
|
|
99
|
+
# --grep searches the whole tree, including what the filter hides, so it can
|
|
100
|
+
# find something that is present but off screen.
|
|
101
|
+
function shown(i, hay) {
|
|
102
|
+
if (GREP != "") {
|
|
103
|
+
hay = tolower(type[i] " " id[i] " " label[i] " " value[i])
|
|
104
|
+
return index(hay, tolower(GREP)) > 0
|
|
105
|
+
}
|
|
106
|
+
if (ALL == 1) return 1
|
|
107
|
+
if (!has(flags[i], "visible")) return 0
|
|
108
|
+
if (id[i] != "" || label[i] != "" || value[i] != "") return 1
|
|
109
|
+
return has(flags[i], "actionable")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# A stable, short identity for the screen: the set of (type,label) pairs of the
|
|
113
|
+
# shown elements, folded into sixteen bits.
|
|
114
|
+
#
|
|
115
|
+
# NOT A CRYPTOGRAPHIC HASH AND NOT CLAIMING TO BE ONE. It answers "is this the
|
|
116
|
+
# same screen as the one before", and a collision costs one unnecessary re-read.
|
|
117
|
+
# XCUITest and UiAutomator publish no view-controller or activity class, so
|
|
118
|
+
# there is no name to use instead; this is the honest substitute and it claims
|
|
119
|
+
# less than a class name would.
|
|
120
|
+
function screenhash( i, s, acc, j) {
|
|
121
|
+
s = ""
|
|
122
|
+
for (i = 1; i <= n; i++) if (shown(i)) s = s type[i] "\037" label[i] "\036"
|
|
123
|
+
acc = 5381
|
|
124
|
+
for (j = 1; j <= length(s); j++)
|
|
125
|
+
acc = (acc * 33 + ORD[substr(s, j, 1)]) % 65536
|
|
126
|
+
return sprintf("%04x", acc)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# The screen's name: a navigation bar's label if there is one, otherwise the
|
|
130
|
+
# topmost static text. Both are guesses about which string a person would call
|
|
131
|
+
# the screen, and neither is load-bearing — the hash is what gets compared.
|
|
132
|
+
function screenname( i, bestY, name) {
|
|
133
|
+
for (i = 1; i <= n; i++)
|
|
134
|
+
if (type[i] ~ /NavigationBar|Toolbar|ActionBar/ && best(i) != "") return best(i)
|
|
135
|
+
bestY = 1000000; name = ""
|
|
136
|
+
for (i = 1; i <= n; i++) {
|
|
137
|
+
if (!shown(i) || best(i) == "") continue
|
|
138
|
+
if (type[i] !~ /StaticText|TextView|Label/) continue
|
|
139
|
+
if (y[i] < bestY) { bestY = y[i]; name = best(i) }
|
|
140
|
+
}
|
|
141
|
+
return name
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
END {
|
|
145
|
+
if (n == 0) {
|
|
146
|
+
print "the driver returned no elements" > "/dev/stderr"
|
|
147
|
+
exit 4
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
uid = 0
|
|
151
|
+
for (i = 1; i <= n; i++) {
|
|
152
|
+
if (!shown(i)) continue
|
|
153
|
+
uid++
|
|
154
|
+
uidof[i] = uid
|
|
155
|
+
order[uid] = i
|
|
156
|
+
}
|
|
157
|
+
if (uid == 0) {
|
|
158
|
+
print "nothing matched" > "/dev/stderr"
|
|
159
|
+
exit 4
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# The uid table, for whoever has to resolve one of these numbers later. Same
|
|
163
|
+
# numbering as the display, by construction rather than by agreement: both
|
|
164
|
+
# come from the loop above.
|
|
165
|
+
#
|
|
166
|
+
# SEPARATED BY '|', NOT BY A TAB, and that is not a style choice. Two of these
|
|
167
|
+
# fields — the identifier and the value — are routinely empty, and bash `read`
|
|
168
|
+
# treats runs of a WHITESPACE delimiter as one separator. A tab is whitespace,
|
|
169
|
+
# so two empty fields in a row collapse and every field after them shifts
|
|
170
|
+
# left: the label ends up holding an x coordinate. A pipe is not whitespace,
|
|
171
|
+
# so empty fields survive, and the adapters have already replaced any pipe
|
|
172
|
+
# inside application copy with a slash.
|
|
173
|
+
if (MODE == "store") {
|
|
174
|
+
for (u = 1; u <= uid; u++) {
|
|
175
|
+
i = order[u]
|
|
176
|
+
printf "%d|%d|%s|%s|%s|%s|%d|%d|%d|%d\n",
|
|
177
|
+
u, serial[i], type[i], id[i], label[i], value[i], x[i], y[i], w[i], h[i]
|
|
178
|
+
}
|
|
179
|
+
exit 0
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# The status bar's height, from the status bar. Zero when the tree has none,
|
|
183
|
+
# which is the honest answer for a full-screen app.
|
|
184
|
+
SBH = 0
|
|
185
|
+
for (i = 1; i <= n; i++)
|
|
186
|
+
if (type[i] ~ /StatusBar/ && y[i] <= TOL && h[i] > SBH) SBH = h[i]
|
|
187
|
+
|
|
188
|
+
nm = screenname()
|
|
189
|
+
if (nm != "") printf "SCREEN \"%s\" #%s %dx%d %s\n\n", nm, screenhash(), WW, WH, ORIENT
|
|
190
|
+
else printf "SCREEN #%s %dx%d %s\n\n", screenhash(), WW, WH, ORIENT
|
|
191
|
+
|
|
192
|
+
for (u = 1; u <= uid; u++) {
|
|
193
|
+
i = order[u]
|
|
194
|
+
|
|
195
|
+
# THE IDENTIFIER IS PRINTED WHEN THERE IS ONE, AND SEPARATELY FROM THE TEXT.
|
|
196
|
+
#
|
|
197
|
+
# --grep has always searched identifiers, so a screen could be found by an
|
|
198
|
+
# identifier the display never showed — the data was here and the line did
|
|
199
|
+
# not carry it. It is also the one selector that survives a change of copy
|
|
200
|
+
# and a change of language, which makes it the one anybody writing a flow
|
|
201
|
+
# should reach for first. Shown as #id, beside the words rather than instead
|
|
202
|
+
# of them, because a name and a caption answer different questions.
|
|
203
|
+
#
|
|
204
|
+
# best() is left alone deliberately: it feeds the screen hash and the screen
|
|
205
|
+
# name, and changing what those fold over would silently invalidate every
|
|
206
|
+
# waypoint recorded before today.
|
|
207
|
+
line = sprintf("[%d] %s", u, type[i])
|
|
208
|
+
if (id[i] != "") line = line sprintf(" #%s", id[i])
|
|
209
|
+
cap = (label[i] != "" ? label[i] : value[i])
|
|
210
|
+
if (cap != "") line = line sprintf(" \"%s\"", cap)
|
|
211
|
+
line = line sprintf(" (%d,%d) %dx%d", x[i], y[i], w[i], h[i])
|
|
212
|
+
if (has(flags[i], "actionable"))
|
|
213
|
+
line = line (has(flags[i], "enabled") ? " enabled" : " disabled")
|
|
214
|
+
if (type[i] ~ /TextField|EditText|SearchField/ && value[i] == "")
|
|
215
|
+
line = line " empty"
|
|
216
|
+
print line
|
|
217
|
+
|
|
218
|
+
# --- the warnings ---------------------------------------------------------
|
|
219
|
+
#
|
|
220
|
+
# Collected, not printed here. See the note above the LAYOUT block below.
|
|
221
|
+
|
|
222
|
+
# Zero size, but carrying text somebody meant to be read.
|
|
223
|
+
if ((w[i] == 0 || h[i] == 0) && best(i) != "")
|
|
224
|
+
warn(u, "zero size, but it carries text")
|
|
225
|
+
|
|
226
|
+
# Off the window, wholly or partly.
|
|
227
|
+
if (WW > 0 && WH > 0) {
|
|
228
|
+
if (x[i] >= WW || y[i] >= WH || x[i] + w[i] <= 0 || y[i] + h[i] <= 0) {
|
|
229
|
+
warn(u, "off screen")
|
|
230
|
+
} else {
|
|
231
|
+
over = 0
|
|
232
|
+
if (x[i] + w[i] > WW) over = x[i] + w[i] - WW
|
|
233
|
+
if (y[i] + h[i] > WH && y[i] + h[i] - WH > over) over = y[i] + h[i] - WH
|
|
234
|
+
if (x[i] < 0 && -x[i] > over) over = -x[i]
|
|
235
|
+
if (y[i] < 0 && -y[i] > over) over = -y[i]
|
|
236
|
+
if (over > TOL) warn(u, sprintf("partly off screen by %dpt", over))
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Outside the parent.
|
|
241
|
+
p = byserial[parent[i]]
|
|
242
|
+
if (p != "" && p > 0 && w[p] > 0 && h[p] > 0) {
|
|
243
|
+
esc = 0
|
|
244
|
+
if (x[i] < x[p]) esc = x[p] - x[i]
|
|
245
|
+
if (x[i] + w[i] > x[p] + w[p] && x[i] + w[i] - (x[p] + w[p]) > esc)
|
|
246
|
+
esc = x[i] + w[i] - (x[p] + w[p])
|
|
247
|
+
if (y[i] < y[p] && y[p] - y[i] > esc) esc = y[p] - y[i]
|
|
248
|
+
if (y[i] + h[i] > y[p] + h[p] && y[i] + h[i] - (y[p] + h[p]) > esc)
|
|
249
|
+
esc = y[i] + h[i] - (y[p] + h[p])
|
|
250
|
+
if (esc > TOL) warn(u, sprintf("outside its parent by %dpt", esc))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Overlapping a shown sibling. Siblings only: a child overlapping its parent
|
|
254
|
+
# is what a child does. The worst overlap is the one reported — which
|
|
255
|
+
# sibling it is comes from `measure`, whose job is exactly that question.
|
|
256
|
+
worst = 0
|
|
257
|
+
for (v = 1; v < u; v++) {
|
|
258
|
+
j = order[v]
|
|
259
|
+
if (parent[j] != parent[i]) continue
|
|
260
|
+
# TWO FULL-SCREEN LAYERS ARE NOT AN OVERLAP, THEY ARE A STACK.
|
|
261
|
+
#
|
|
262
|
+
# --all shows the containers, and a screen is built out of several boxes
|
|
263
|
+
# that each cover the whole window. Reported as "overlaps a sibling by
|
|
264
|
+
# 428pt — [7] [8] [10] and 9 more", which is a true sentence about
|
|
265
|
+
# something no layout has ever got wrong.
|
|
266
|
+
if (fullscreen(i) && fullscreen(j)) continue
|
|
267
|
+
ox = span(x[i], x[i] + w[i], x[j], x[j] + w[j])
|
|
268
|
+
oy = span(y[i], y[i] + h[i], y[j], y[j] + h[j])
|
|
269
|
+
if (ox > TOL && oy > TOL) {
|
|
270
|
+
d = (ox < oy ? ox : oy)
|
|
271
|
+
if (d > worst) worst = d
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (worst > 0) warn(u, sprintf("overlaps a sibling by %dpt", worst))
|
|
275
|
+
|
|
276
|
+
# THE POINT A TAP LANDS ON, NOT THE BOX IT BELONGS TO.
|
|
277
|
+
#
|
|
278
|
+
# Every tap goes to the centre of the rectangle. Spotlight's search field
|
|
279
|
+
# measures (32,4) 364x48, which looks generous — but its centre is at y=28,
|
|
280
|
+
# underneath a status bar 47pt tall, so the tap lands on the clock and the
|
|
281
|
+
# keyboard never appears. The element is not too small and is not off
|
|
282
|
+
# screen; the one number that matters is simply covered.
|
|
283
|
+
#
|
|
284
|
+
# Only reported when the tree actually carries a StatusBar, because its
|
|
285
|
+
# height is measured rather than assumed.
|
|
286
|
+
if (has(flags[i], "actionable") && SBH > 0 && y[i] + h[i] / 2 < SBH)
|
|
287
|
+
warn(u, sprintf("tappable, but its centre is under the %dpt status bar", SBH))
|
|
288
|
+
|
|
289
|
+
# A hit area smaller than a fingertip. The size itself is already on the
|
|
290
|
+
# element's own line, so the warning does not repeat it.
|
|
291
|
+
if (has(flags[i], "actionable") && w[i] > 0 && h[i] > 0 &&
|
|
292
|
+
(w[i] < MINHIT || h[i] < MINHIT))
|
|
293
|
+
warn(u, sprintf("hit area below the %dpt minimum", MINHIT))
|
|
294
|
+
|
|
295
|
+
# A CONTROL WITH NO IDENTIFIER, WHICH IS A FAULT IN THE APP AND NOT ON THE
|
|
296
|
+
# SCREEN.
|
|
297
|
+
#
|
|
298
|
+
# Every other warning here is about geometry. This one is about whether the
|
|
299
|
+
# thing can be named at all: with no identifier the only way to reach it is
|
|
300
|
+
# its label, and a label changes when marketing rewrites the button or the
|
|
301
|
+
# app is read in another language. Nobody else is standing in the right
|
|
302
|
+
# place to say so — the tree is in front of us and it is not in front of the
|
|
303
|
+
# person who wrote the view.
|
|
304
|
+
#
|
|
305
|
+
# The keyboard is excluded. Its keys are the operating system's and telling
|
|
306
|
+
# an application developer to add identifiers to them is advice they cannot
|
|
307
|
+
# take.
|
|
308
|
+
if (has(flags[i], "actionable") && id[i] == "" && type[i] !~ /^(Key|Keyboard)$/)
|
|
309
|
+
warn(u, "no identifier: a selector has to use the label, which changes with the copy")
|
|
310
|
+
|
|
311
|
+
# The OS already truncated the string, and said so in the string.
|
|
312
|
+
s = best(i)
|
|
313
|
+
if (s ~ /…$/ || s ~ /\.\.\.$/)
|
|
314
|
+
warn(u, "the OS truncated this text with an ellipsis")
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# --- what is wrong with this screen -----------------------------------------
|
|
318
|
+
#
|
|
319
|
+
# GROUPED BY MESSAGE, NOT PRINTED BESIDE EACH ELEMENT, AND THAT IS ABOUT COST.
|
|
320
|
+
#
|
|
321
|
+
# Measured on a real screen: a contact list of six identical rows produced the
|
|
322
|
+
# same three warnings once per row. Inline, that is forty-nine lines saying
|
|
323
|
+
# eight things, and an agent that reads forty-nine warnings on every screen
|
|
324
|
+
# stops reading warnings. Grouped, it is four lines and the uids are still
|
|
325
|
+
# there to act on.
|
|
326
|
+
#
|
|
327
|
+
# Nothing is dropped silently. When a group has more members than fit, the
|
|
328
|
+
# line says how many more.
|
|
329
|
+
if (nw > 0) {
|
|
330
|
+
printf "\n"
|
|
331
|
+
for (k = 1; k <= nw; k++) {
|
|
332
|
+
list = ""; shown_n = 0
|
|
333
|
+
cnt = split(wuids[k], parts, " ")
|
|
334
|
+
for (q = 1; q <= cnt && q <= 8; q++) {
|
|
335
|
+
list = list sprintf("%s[%s]", (list == "" ? "" : " "), parts[q])
|
|
336
|
+
shown_n++
|
|
337
|
+
}
|
|
338
|
+
if (cnt > shown_n) list = list sprintf(" and %d more", cnt - shown_n)
|
|
339
|
+
printf "⚠ %s — %s\n", wmsg[k], list
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# warn <uid> <message> — remember it; the END block groups and prints.
|
|
345
|
+
#
|
|
346
|
+
# SILENT UNDER --grep, AND THAT IS THE POINT OF --grep.
|
|
347
|
+
#
|
|
348
|
+
# Without a filter this is an audit of the screen in front of you, and every
|
|
349
|
+
# warning is about something you can see. --grep searches the whole tree
|
|
350
|
+
# including what is off screen, so a home-screen icon sitting on another page
|
|
351
|
+
# comes back "zero size, but it carries text" and "off screen" — both true,
|
|
352
|
+
# neither a layout fault, and both noise in front of the one line the caller
|
|
353
|
+
# asked for. Reported by an agent that grepped for an icon and got two warnings
|
|
354
|
+
# about the phone not currently showing it.
|
|
355
|
+
function warn(u, msg, k) {
|
|
356
|
+
if (GREP != "") return
|
|
357
|
+
for (k = 1; k <= nw; k++)
|
|
358
|
+
if (wmsg[k] == msg) { wuids[k] = wuids[k] " " u; return }
|
|
359
|
+
nw++
|
|
360
|
+
wmsg[nw] = msg
|
|
361
|
+
wuids[nw] = u
|
|
362
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// verify-npm-package.js — pack it, install it somewhere else, and use it.
|
|
3
|
+
//
|
|
4
|
+
// npm run verify-npm-package
|
|
5
|
+
//
|
|
6
|
+
// WHAT THIS IS FOR
|
|
7
|
+
//
|
|
8
|
+
// `npm pack` silently dropped all forty-four symlinks in scripts/ — the entire
|
|
9
|
+
// verb surface — and produced a tarball that installs cleanly, starts cleanly,
|
|
10
|
+
// and answers `unknown tool` to every one of the twenty-one tools. Nothing
|
|
11
|
+
// about that looks like a failure until somebody tries to tap something.
|
|
12
|
+
//
|
|
13
|
+
// A published package is the one artefact in this repository that neither
|
|
14
|
+
// self-test covers: they both run against the working tree, where the symlinks
|
|
15
|
+
// are simply there. So this builds the real thing, unpacks it into a directory
|
|
16
|
+
// with no relation to this one, and drives it: the handshake, the tool list,
|
|
17
|
+
// and a verb — through the same `npx`-shaped entry point a user would get.
|
|
18
|
+
//
|
|
19
|
+
// It does not publish anything and needs no network.
|
|
20
|
+
//
|
|
21
|
+
// exit 0 the package works where it lands
|
|
22
|
+
// exit 1 it does not, and the line above says which part
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
27
|
+
const { mkdtempSync, rmSync, readdirSync, existsSync, writeFileSync, copyFileSync } = require('node:fs');
|
|
28
|
+
const { join } = require('node:path');
|
|
29
|
+
const { tmpdir } = require('node:os');
|
|
30
|
+
|
|
31
|
+
const root = join(__dirname, '..');
|
|
32
|
+
let failures = 0;
|
|
33
|
+
|
|
34
|
+
function ok(what) { console.log(`OK ${what}`); }
|
|
35
|
+
function bad(what, detail) { console.log(`FAIL ${what} — ${detail}`); failures++; }
|
|
36
|
+
function is(what, got, want) { got === want ? ok(what) : bad(what, `expected [${want}], got [${got}]`); }
|
|
37
|
+
|
|
38
|
+
const work = mkdtempSync(join(tmpdir(), 'devicetools-pack-'));
|
|
39
|
+
process.on('exit', () => rmSync(work, { recursive: true, force: true }));
|
|
40
|
+
|
|
41
|
+
// --- build and unpack --------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
execFileSync('npm', ['pack', '--pack-destination', work], { cwd: root, stdio: 'pipe' });
|
|
44
|
+
const tgz = readdirSync(work).find((f) => f.endsWith('.tgz'));
|
|
45
|
+
if (!tgz) { bad('npm pack produces a tarball', 'no .tgz'); process.exit(1); }
|
|
46
|
+
ok(`npm pack produces ${tgz}`);
|
|
47
|
+
|
|
48
|
+
execFileSync('tar', ['xzf', join(work, tgz), '-C', work], { stdio: 'pipe' });
|
|
49
|
+
const pkg = join(work, 'package');
|
|
50
|
+
|
|
51
|
+
// --- the thing that was actually broken --------------------------------------
|
|
52
|
+
|
|
53
|
+
const links = join(pkg, 'scripts', 'links.tsv');
|
|
54
|
+
existsSync(links)
|
|
55
|
+
? ok('the link manifest is in the tarball')
|
|
56
|
+
: bad('the link manifest is in the tarball', 'scripts/links.tsv is missing');
|
|
57
|
+
|
|
58
|
+
// Before anything runs, the verbs are absent — that is npm, not a mistake, and
|
|
59
|
+
// asserting it here means this check still means something on the day npm
|
|
60
|
+
// starts carrying symlinks.
|
|
61
|
+
existsSync(join(pkg, 'scripts', 'tap.sh'))
|
|
62
|
+
? ok('npm now carries symlinks (the restore below is redundant, and harmless)')
|
|
63
|
+
: ok('npm dropped the symlinks, as expected — the restore is what fixes it');
|
|
64
|
+
|
|
65
|
+
// --- drive it, the way npx would ---------------------------------------------
|
|
66
|
+
//
|
|
67
|
+
// A config it can find, in a directory that is not this repository, so nothing
|
|
68
|
+
// can quietly fall back to the checkout being verified.
|
|
69
|
+
const project = join(work, 'project');
|
|
70
|
+
execFileSync('mkdir', ['-p', project]);
|
|
71
|
+
writeFileSync(join(project, '.devicetools'), 'devicetools-project 1\n');
|
|
72
|
+
copyFileSync(join(root, 'config.example.json'), join(project, 'config.json'));
|
|
73
|
+
|
|
74
|
+
const entry = join(pkg, 'bin', 'device-devtools-mcp.js');
|
|
75
|
+
const env = { ...process.env, DEVICETOOLS_CONFIG: join(project, 'config.json') };
|
|
76
|
+
|
|
77
|
+
const rpc = [
|
|
78
|
+
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26"}}',
|
|
79
|
+
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}',
|
|
80
|
+
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"waypoint","arguments":{"action":"list"}}}',
|
|
81
|
+
].join('\n') + '\n';
|
|
82
|
+
|
|
83
|
+
const server = spawnSync('node', [entry], { input: rpc, env, encoding: 'utf8' });
|
|
84
|
+
const replies = server.stdout.trim().split('\n').filter(Boolean).map((l) => {
|
|
85
|
+
try { return JSON.parse(l); } catch { return null; }
|
|
86
|
+
}).filter(Boolean);
|
|
87
|
+
|
|
88
|
+
const listed = replies.find((r) => r.id === 2);
|
|
89
|
+
const names = listed?.result?.tools?.map((t) => t.name).sort() ?? [];
|
|
90
|
+
|
|
91
|
+
is('the server answers the handshake',
|
|
92
|
+
replies.find((r) => r.id === 1)?.result?.protocolVersion, '2025-03-26');
|
|
93
|
+
is('every tool is on the list', String(names.length), '21');
|
|
94
|
+
|
|
95
|
+
// THE FAILURE THIS FILE EXISTS FOR. A tool whose script is missing is still
|
|
96
|
+
// advertised by tools/list — the list is built from a table, the dispatch is a
|
|
97
|
+
// file lookup — so calling one is the only way to find out.
|
|
98
|
+
const called = replies.find((r) => r.id === 3);
|
|
99
|
+
if (called?.error) {
|
|
100
|
+
bad('a verb runs from the unpacked package', `${called.error.message} — the verb surface did not survive`);
|
|
101
|
+
} else if (called?.result?.isError) {
|
|
102
|
+
bad('a verb runs from the unpacked package', called.result.content?.[0]?.text?.split('\n')[0] ?? 'error');
|
|
103
|
+
} else {
|
|
104
|
+
ok('a verb runs from the unpacked package');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The command line, through the same entry point.
|
|
108
|
+
const cli = spawnSync('node', [entry, 'flow', 'list'], { env, encoding: 'utf8' });
|
|
109
|
+
cli.status === 0
|
|
110
|
+
? ok('the CLI runs from the unpacked package')
|
|
111
|
+
: bad('the CLI runs from the unpacked package', (cli.stderr || cli.stdout).trim().split('\n')[0]);
|
|
112
|
+
|
|
113
|
+
// And the restore was not a one-off: the tree it left behind must be complete.
|
|
114
|
+
const check = spawnSync('/bin/bash', [join(pkg, 'scripts', 'relink.sh'), '--check'], { encoding: 'utf8' });
|
|
115
|
+
check.status === 0
|
|
116
|
+
? ok('the restored tree is complete')
|
|
117
|
+
: bad('the restored tree is complete', check.stderr.trim());
|
|
118
|
+
|
|
119
|
+
// --- nothing private, and nothing this machine's -----------------------------
|
|
120
|
+
//
|
|
121
|
+
// The package is the artefact that goes public. A grep over what is actually in
|
|
122
|
+
// the tarball is cheaper than remembering.
|
|
123
|
+
const listing = execFileSync('tar', ['tzf', join(work, tgz)], { encoding: 'utf8' });
|
|
124
|
+
const forbidden = ['config.json\n', '.state/', '.runs/', '.git/'];
|
|
125
|
+
const leaked = forbidden.filter((f) => listing.includes(`package/${f}`));
|
|
126
|
+
leaked.length === 0
|
|
127
|
+
? ok('no config, state or run artefacts are in the tarball')
|
|
128
|
+
: bad('no config, state or run artefacts are in the tarball', leaked.join(' '));
|
|
129
|
+
|
|
130
|
+
console.log(failures === 0
|
|
131
|
+
? 'RESULT PASS npm package'
|
|
132
|
+
: `RESULT FAIL npm package — ${failures} check(s)`);
|
|
133
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
SCREEN "Liên hệ" #9b41 428x926 portrait
|
|
2
|
+
|
|
3
|
+
[1] Application "DemoApp" (0,0) 428x926
|
|
4
|
+
[2] NavigationBar #Liên hệ (0,47) 428x101
|
|
5
|
+
[3] Button (20,51) 36x36 enabled
|
|
6
|
+
[4] StaticText "Liên hệ" (19,108) 116x31
|
|
7
|
+
[5] StaticText "Liên hệ gần đây" (24,224) 121x36
|
|
8
|
+
[6] StaticText "Liên hệ nhóm" (153,224) 122x36
|
|
9
|
+
[7] StaticText "Liên hệ công việc" (283,224) 121x36
|
|
10
|
+
[8] StaticText "Danh sách đã lưu" (23,303) 126x18
|
|
11
|
+
[9] StaticText "Thêm mới" (315,303) 65x18
|
|
12
|
+
[10] TextField "Tìm kiếm" (72,368) 316x25 enabled
|
|
13
|
+
[11] StaticText "Tất cả" (36,432) 39x19
|
|
14
|
+
[12] StaticText "Yêu thích" (107,432) 60x19
|
|
15
|
+
[13] StaticText "Mã số" (199,432) 82x19
|
|
16
|
+
[14] StaticText "Nhãn" (312,432) 44x19
|
|
17
|
+
[15] Other "Vertical scroll bar, 1 pages" (387,424) 30x35
|
|
18
|
+
[16] StaticText "000111222" (68,520) 356x19
|
|
19
|
+
[17] StaticText "NGUOI DUNG A" (68,500) 336x21
|
|
20
|
+
[18] StaticText "Acme Demo" (68,482) 356x19
|
|
21
|
+
[19] Button "icMore" (412,498) 24x25 enabled
|
|
22
|
+
[20] StaticText "000000******0000" (68,600) 356x18
|
|
23
|
+
[21] StaticText "NGUOI DUNG C DEMO BA XX" (68,579) 336x22
|
|
24
|
+
[22] StaticText "Acme Demo" (68,562) 356x18
|
|
25
|
+
[23] Button "icMore" (412,578) 24x25 enabled
|
|
26
|
+
[24] StaticText "000000******0000" (68,679) 356x19
|
|
27
|
+
[25] StaticText "NGUOI DUNG E DEMO HAI TEST" (68,659) 336x21
|
|
28
|
+
[26] StaticText "Epsilon" (68,641) 356x19
|
|
29
|
+
[27] Button "icMore" (412,657) 24x25 enabled
|
|
30
|
+
[28] StaticText "0000000000000" (68,759) 356x19
|
|
31
|
+
[29] StaticText "NGUOI DUNG B DEMO MOT" (68,739) 356x21
|
|
32
|
+
[30] StaticText "Delta" (68,721) 356x19
|
|
33
|
+
[31] Button "icMore" (412,737) 24x25 enabled
|
|
34
|
+
[32] StaticText "0000000000" (68,839) 356x18
|
|
35
|
+
[33] StaticText "NGUOI DUNG H" (68,818) 356x22
|
|
36
|
+
[34] StaticText "Gamma" (68,801) 356x18
|
|
37
|
+
[35] Button "icMore" (412,817) 24x25 enabled
|
|
38
|
+
[36] StaticText "000000******0000" (68,918) 356x19
|
|
39
|
+
[37] StaticText "NGUOI DUNG D DEMO" (68,898) 356x21
|
|
40
|
+
[38] StaticText "Alpha" (68,880) 356x19
|
|
41
|
+
[39] Button "icMore" (412,896) 24x25 enabled
|
|
42
|
+
[40] Other "Vertical scroll bar, 2 pages" (395,148) 30x730
|
|
43
|
+
[41] Other "Horizontal scroll bar, 1 pages" (47,893) 334x30
|
|
44
|
+
|
|
45
|
+
⚠ hit area below the 44pt minimum — [3] [10] [19] [23] [27] [31] [35] [39]
|
|
46
|
+
⚠ no identifier: a selector has to use the label, which changes with the copy — [3] [10] [19] [23] [27] [31] [35] [39]
|
|
47
|
+
⚠ outside its parent by 4pt — [16] [18] [20] [22] [24] [26] [28] [29] and 7 more
|
|
48
|
+
⚠ partly off screen by 8pt — [19] [23] [27] [31] [35] [39]
|
|
49
|
+
⚠ outside its parent by 16pt — [19] [23] [27] [31] [35] [39]
|
|
50
|
+
⚠ overlaps a sibling by 3pt — [19] [23] [27]
|
|
51
|
+
⚠ overlaps a sibling by 12pt — [31] [35] [39]
|
|
52
|
+
⚠ partly off screen by 11pt — [36]
|