agent-device 0.1.1 → 0.1.3

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.
@@ -19,17 +19,13 @@ struct AXNode: Codable {
19
19
  let children: [AXNode]
20
20
  }
21
21
 
22
- struct AXSnapshot: Codable {
23
- let windowFrame: AXNode.Frame?
24
- let root: AXNode
25
- }
26
-
27
22
  struct AXSnapshotError: Error, CustomStringConvertible {
28
23
  let message: String
29
24
  var description: String { message }
30
25
  }
31
26
 
32
27
  let simulatorBundleId = "com.apple.iphonesimulator"
28
+ let defaultMaxDepth = 40
33
29
 
34
30
  func hasAccessibilityPermission() -> Bool {
35
31
  AXIsProcessTrusted()
@@ -51,15 +47,19 @@ func getAttribute<T>(_ element: AXUIElement, _ attribute: CFString) -> T? {
51
47
  }
52
48
 
53
49
  func getChildren(_ element: AXUIElement) -> [AXUIElement] {
54
- getAttribute(element, kAXChildrenAttribute as CFString) ?? []
55
- }
56
-
57
- func getRole(_ element: AXUIElement) -> String? {
58
- getAttribute(element, kAXRoleAttribute as CFString)
59
- }
60
-
61
- func getSubrole(_ element: AXUIElement) -> String? {
62
- getAttribute(element, kAXSubroleAttribute as CFString)
50
+ if let children: [AXUIElement] = getAttribute(element, kAXChildrenAttribute as CFString),
51
+ !children.isEmpty {
52
+ return children
53
+ }
54
+ if let children: [AXUIElement] = getAttribute(element, kAXVisibleChildrenAttribute as CFString),
55
+ !children.isEmpty {
56
+ return children
57
+ }
58
+ if let children: [AXUIElement] = getAttribute(element, kAXContentsAttribute as CFString),
59
+ !children.isEmpty {
60
+ return children
61
+ }
62
+ return []
63
63
  }
64
64
 
65
65
  func getLabel(_ element: AXUIElement) -> String? {
@@ -72,6 +72,10 @@ func getLabel(_ element: AXUIElement) -> String? {
72
72
  return nil
73
73
  }
74
74
 
75
+ func getDescription(_ element: AXUIElement) -> String? {
76
+ getAttribute(element, kAXDescriptionAttribute as CFString)
77
+ }
78
+
75
79
  func getValue(_ element: AXUIElement) -> String? {
76
80
  if let value: String = getAttribute(element, kAXValueAttribute as CFString) {
77
81
  return value
@@ -111,11 +115,13 @@ func getFrame(_ element: AXUIElement) -> AXNode.Frame? {
111
115
  )
112
116
  }
113
117
 
114
- func buildTree(_ element: AXUIElement, depth: Int = 0, maxDepth: Int = 40) -> AXNode {
115
- let children = depth < maxDepth ? getChildren(element).map { buildTree($0, depth: depth + 1, maxDepth: maxDepth) } : []
118
+ func buildTree(_ element: AXUIElement, depth: Int = 0, maxDepth: Int = defaultMaxDepth) -> AXNode {
119
+ let children = depth < maxDepth
120
+ ? getChildren(element).map { buildTree($0, depth: depth + 1, maxDepth: maxDepth) }
121
+ : []
116
122
  return AXNode(
117
- role: getRole(element),
118
- subrole: getSubrole(element),
123
+ role: getAttribute(element, kAXRoleAttribute as CFString),
124
+ subrole: getAttribute(element, kAXSubroleAttribute as CFString),
119
125
  label: getLabel(element),
120
126
  value: getValue(element),
121
127
  identifier: getIdentifier(element),
@@ -124,17 +130,250 @@ func buildTree(_ element: AXUIElement, depth: Int = 0, maxDepth: Int = 40) -> AX
124
130
  )
125
131
  }
126
132
 
127
- func findIOSAppRoot(in simulator: NSRunningApplication) -> (AXUIElement, AXNode.Frame?)? {
133
+ func findIOSAppSnapshot(in simulator: NSRunningApplication) -> (AXUIElement, AXNode.Frame?, AXUIElement, [AXUIElement], [AXUIElement])? {
128
134
  let appElement = axElement(for: simulator)
129
- let windows = getChildren(appElement).filter { getRole($0) == "AXWindow" }
135
+ let windows = getChildren(appElement).filter {
136
+ (getAttribute($0, kAXRoleAttribute as CFString) as String?) == (kAXWindowRole as String)
137
+ }
138
+ if windows.isEmpty { return nil }
139
+
140
+ if let focused: AXUIElement = getAttribute(appElement, kAXFocusedWindowAttribute as CFString),
141
+ let root = chooseRoot(in: focused) {
142
+ let extras = dedupeElements(findToolbarExtras(in: focused, root: root) + findTabBarExtras(in: focused, root: root))
143
+ let modalRoots = findAdditionalWindowRoots(windows: windows, excluding: focused, windowFrame: getFrame(focused))
144
+ return (root, getFrame(focused), focused, extras, modalRoots)
145
+ }
146
+
147
+ let sorted = windows.sorted { lhs, rhs in
148
+ let l = getFrame(lhs)
149
+ let r = getFrame(rhs)
150
+ let la = (l?.width ?? 0) * (l?.height ?? 0)
151
+ let ra = (r?.width ?? 0) * (r?.height ?? 0)
152
+ return la > ra
153
+ }
154
+ for window in sorted {
155
+ if let root = chooseRoot(in: window) {
156
+ let extras = dedupeElements(findToolbarExtras(in: window, root: root) + findTabBarExtras(in: window, root: root))
157
+ let modalRoots = findAdditionalWindowRoots(windows: windows, excluding: window, windowFrame: getFrame(window))
158
+ return (root, getFrame(window), window, extras, modalRoots)
159
+ }
160
+ }
161
+ return nil
162
+ }
163
+
164
+ private func findAdditionalWindowRoots(
165
+ windows: [AXUIElement],
166
+ excluding mainWindow: AXUIElement,
167
+ windowFrame: AXNode.Frame?
168
+ ) -> [AXUIElement] {
169
+ var roots: [AXUIElement] = []
130
170
  for window in windows {
131
- for child in getChildren(window) {
132
- if getRole(child) == "AXGroup" {
133
- return (child, getFrame(window))
171
+ if CFEqual(window, mainWindow) { continue }
172
+ let frame = getFrame(window)
173
+ if let windowFrame = windowFrame, !frameIntersects(frame, windowFrame) {
174
+ continue
175
+ }
176
+ if let root = chooseRoot(in: window) {
177
+ roots.append(root)
178
+ }
179
+ }
180
+ return dedupeElements(roots)
181
+ }
182
+
183
+ private func dedupeElements(_ elements: [AXUIElement]) -> [AXUIElement] {
184
+ var seen: Set<AXWrapper> = []
185
+ var result: [AXUIElement] = []
186
+ for element in elements {
187
+ let wrapper = AXWrapper(element)
188
+ if seen.contains(wrapper) { continue }
189
+ seen.insert(wrapper)
190
+ result.append(element)
191
+ }
192
+ return result
193
+ }
194
+
195
+ func chooseRoot(in window: AXUIElement) -> AXUIElement? {
196
+ let windowFrame = getFrame(window)
197
+ let candidates = findGroupCandidates(in: window, windowFrame: windowFrame)
198
+ if let best = candidates.first?.element { return best }
199
+ return findLargestChildCandidate(in: window, windowFrame: windowFrame)
200
+ }
201
+
202
+ private func findLargestChildCandidate(in window: AXUIElement, windowFrame: AXNode.Frame?) -> AXUIElement? {
203
+ var best: (element: AXUIElement, area: Double)? = nil
204
+ for child in getChildren(window) {
205
+ let children = getChildren(child)
206
+ if children.isEmpty { continue }
207
+ let area = frameArea(getFrame(child), windowFrame: windowFrame)
208
+ if area <= 0 { continue }
209
+ if best == nil || area > best!.area {
210
+ best = (child, area)
211
+ }
212
+ }
213
+ return best?.element
214
+ }
215
+
216
+ private func frameIntersects(_ frame: AXNode.Frame?, _ target: AXNode.Frame?) -> Bool {
217
+ guard let frame = frame, let target = target else { return false }
218
+ let fx1 = frame.x
219
+ let fy1 = frame.y
220
+ let fx2 = frame.x + frame.width
221
+ let fy2 = frame.y + frame.height
222
+ let tx1 = target.x
223
+ let ty1 = target.y
224
+ let tx2 = target.x + target.width
225
+ let ty2 = target.y + target.height
226
+ return fx1 < tx2 && fx2 > tx1 && fy1 < ty2 && fy2 > ty1
227
+ }
228
+
229
+ private func isToolbarLike(_ element: AXUIElement) -> Bool {
230
+ let role = (getAttribute(element, kAXRoleAttribute as CFString) as String?) ?? ""
231
+ let subrole = (getAttribute(element, kAXSubroleAttribute as CFString) as String?) ?? ""
232
+ if role == (kAXToolbarRole as String) ||
233
+ role == (kAXTabGroupRole as String) ||
234
+ role == "AXTabBar" {
235
+ return true
236
+ }
237
+ if subrole == "AXTabBar" {
238
+ return true
239
+ }
240
+ return false
241
+ }
242
+
243
+ private func isTabBarLike(_ element: AXUIElement) -> Bool {
244
+ let role = (getAttribute(element, kAXRoleAttribute as CFString) as String?) ?? ""
245
+ let subrole = (getAttribute(element, kAXSubroleAttribute as CFString) as String?) ?? ""
246
+ if role == (kAXTabGroupRole as String) || role == "AXTabBar" { return true }
247
+ if subrole == "AXTabBar" { return true }
248
+ let desc = (getDescription(element) ?? "").lowercased()
249
+ if desc.contains("tab bar") { return true }
250
+ let label = (getLabel(element) ?? "").lowercased()
251
+ if label.contains("tab bar") { return true }
252
+ return false
253
+ }
254
+
255
+ private func findToolbarExtras(in window: AXUIElement, root: AXUIElement) -> [AXUIElement] {
256
+ let rootFrame = getFrame(root)
257
+ let rootIds = collectDescendantWrappers(from: root)
258
+ var extras: [AXUIElement] = []
259
+ var stack = getChildren(window)
260
+ while !stack.isEmpty {
261
+ let current = stack.removeLast()
262
+ if isToolbarLike(current) && !rootIds.contains(AXWrapper(current)) {
263
+ let frame = getFrame(current)
264
+ if frameIntersects(frame, rootFrame) {
265
+ extras.append(current)
134
266
  }
135
267
  }
268
+ stack.append(contentsOf: getChildren(current))
136
269
  }
137
- return nil
270
+ return extras
271
+ }
272
+
273
+ private func findTabBarExtras(in window: AXUIElement, root: AXUIElement) -> [AXUIElement] {
274
+ let rootFrame = getFrame(root)
275
+ let rootIds = collectDescendantWrappers(from: root)
276
+ var extras: [AXUIElement] = []
277
+ var stack = getChildren(window)
278
+ while !stack.isEmpty {
279
+ let current = stack.removeLast()
280
+ if isTabBarLike(current) && !rootIds.contains(AXWrapper(current)) {
281
+ let frame = getFrame(current)
282
+ if frameIntersects(frame, rootFrame) {
283
+ extras.append(current)
284
+ }
285
+ }
286
+ stack.append(contentsOf: getChildren(current))
287
+ }
288
+ return extras
289
+ }
290
+
291
+ private struct GroupCandidate {
292
+ let element: AXUIElement
293
+ let area: Double
294
+ let childCount: Int
295
+ }
296
+
297
+ private func findGroupCandidates(in root: AXUIElement, windowFrame: AXNode.Frame?) -> [GroupCandidate] {
298
+ var candidates: [GroupCandidate] = []
299
+ var visited: Set<AXWrapper> = []
300
+ func walk(_ element: AXUIElement) {
301
+ let wrapper = AXWrapper(element)
302
+ if visited.contains(wrapper) { return }
303
+ visited.insert(wrapper)
304
+ let children = getChildren(element)
305
+ let role = (getAttribute(element, kAXRoleAttribute as CFString) as String?) ?? ""
306
+ let isContainer = role == (kAXGroupRole as String) ||
307
+ role == (kAXScrollAreaRole as String) ||
308
+ role == (kAXUnknownRole as String)
309
+ if isContainer {
310
+ let hasNonToolbarChild = children.contains {
311
+ ((getAttribute($0, kAXRoleAttribute as CFString) as String?) ?? "") != (kAXToolbarRole as String)
312
+ }
313
+ if hasNonToolbarChild {
314
+ let frame = getFrame(element)
315
+ let area = frameArea(frame, windowFrame: windowFrame)
316
+ if area > 0 {
317
+ let childCount = children.count
318
+ candidates.append(
319
+ GroupCandidate(
320
+ element: element,
321
+ area: area,
322
+ childCount: childCount
323
+ )
324
+ )
325
+ }
326
+ }
327
+ }
328
+ for child in children {
329
+ walk(child)
330
+ }
331
+ }
332
+ walk(root)
333
+ candidates.sort { lhs, rhs in
334
+ if lhs.area == rhs.area { return lhs.childCount > rhs.childCount }
335
+ return lhs.area > rhs.area
336
+ }
337
+ return candidates
338
+ }
339
+
340
+ private func frameArea(_ frame: AXNode.Frame?, windowFrame: AXNode.Frame?) -> Double {
341
+ guard let frame = frame else { return 0 }
342
+ if let windowFrame = windowFrame {
343
+ let windowArea = max(1.0, windowFrame.width * windowFrame.height)
344
+ let area = frame.width * frame.height
345
+ if area > windowArea { return 0 }
346
+ return area
347
+ }
348
+ return frame.width * frame.height
349
+ }
350
+
351
+ private final class AXWrapper: Hashable {
352
+ let element: AXUIElement
353
+ init(_ element: AXUIElement) { self.element = element }
354
+ func hash(into hasher: inout Hasher) { hasher.combine(CFHash(element)) }
355
+ static func == (lhs: AXWrapper, rhs: AXWrapper) -> Bool {
356
+ return CFEqual(lhs.element, rhs.element)
357
+ }
358
+ }
359
+
360
+ private func collectDescendantWrappers(from root: AXUIElement) -> Set<AXWrapper> {
361
+ var seen: Set<AXWrapper> = []
362
+ var stack = [root]
363
+ while !stack.isEmpty {
364
+ let current = stack.removeLast()
365
+ let wrapper = AXWrapper(current)
366
+ if seen.contains(wrapper) { continue }
367
+ seen.insert(wrapper)
368
+ stack.append(contentsOf: getChildren(current))
369
+ }
370
+ return seen
371
+ }
372
+
373
+
374
+ private struct SnapshotPayload: Codable {
375
+ let windowFrame: AXNode.Frame?
376
+ let root: AXNode
138
377
  }
139
378
 
140
379
  func main() throws {
@@ -144,14 +383,52 @@ func main() throws {
144
383
  guard let simulator = findSimulatorApp() else {
145
384
  throw AXSnapshotError(message: "iOS Simulator is not running.")
146
385
  }
147
- guard let (root, windowFrame) = findIOSAppRoot(in: simulator) else {
386
+ let maxAttempts = 5
387
+ var snapshot: (AXUIElement, AXNode.Frame?, AXUIElement, [AXUIElement], [AXUIElement])? = nil
388
+ for attempt in 0..<maxAttempts {
389
+ if let candidate = findIOSAppSnapshot(in: simulator) {
390
+ let (root, _, _, _, modalRoots) = candidate
391
+ if !getChildren(root).isEmpty || !modalRoots.isEmpty {
392
+ snapshot = candidate
393
+ break
394
+ }
395
+ }
396
+ if attempt < maxAttempts - 1 {
397
+ usleep(300_000)
398
+ }
399
+ }
400
+ guard let (root, windowFrame, _, extras, modalRoots) = snapshot else {
148
401
  throw AXSnapshotError(message: "Could not find iOS app content in Simulator.")
149
402
  }
150
- let tree = buildTree(root)
151
- let snapshot = AXSnapshot(windowFrame: windowFrame, root: tree)
403
+ var tree = buildTree(root)
404
+ if !extras.isEmpty {
405
+ let extraNodes = extras.map { buildTree($0) }
406
+ tree = AXNode(
407
+ role: tree.role,
408
+ subrole: tree.subrole,
409
+ label: tree.label,
410
+ value: tree.value,
411
+ identifier: tree.identifier,
412
+ frame: tree.frame,
413
+ children: tree.children + extraNodes
414
+ )
415
+ }
416
+ if !modalRoots.isEmpty {
417
+ let modalNodes = modalRoots.map { buildTree($0) }
418
+ tree = AXNode(
419
+ role: tree.role,
420
+ subrole: tree.subrole,
421
+ label: tree.label,
422
+ value: tree.value,
423
+ identifier: tree.identifier,
424
+ frame: tree.frame,
425
+ children: tree.children + modalNodes
426
+ )
427
+ }
428
+ let payload = SnapshotPayload(windowFrame: windowFrame, root: tree)
152
429
  let encoder = JSONEncoder()
153
430
  encoder.outputFormatting = [.sortedKeys]
154
- let data = try encoder.encode(snapshot)
431
+ let data = try encoder.encode(payload)
155
432
  if let json = String(data: data, encoding: .utf8) {
156
433
  print(json)
157
434
  } else {
@@ -249,21 +249,6 @@ final class RunnerTests: XCTestCase {
249
249
  }
250
250
  let found = findElement(app: activeApp, text: text) != nil
251
251
  return Response(ok: true, data: DataPayload(found: found))
252
- case .rect:
253
- guard let text = command.text else {
254
- return Response(ok: false, error: ErrorPayload(message: "rect requires text"))
255
- }
256
- guard let element = findElement(app: activeApp, text: text) else {
257
- return Response(ok: false, error: ErrorPayload(message: "element not found"))
258
- }
259
- let frame = element.frame
260
- let rect = SnapshotRect(
261
- x: Double(frame.origin.x),
262
- y: Double(frame.origin.y),
263
- width: Double(frame.size.width),
264
- height: Double(frame.size.height)
265
- )
266
- return Response(ok: true, data: DataPayload(rect: rect))
267
252
  case .listTappables:
268
253
  let elements = activeApp.descendants(matching: .any).allElementsBoundByIndex
269
254
  let labels = elements.compactMap { element -> String? in
@@ -287,9 +272,62 @@ final class RunnerTests: XCTestCase {
287
272
  return Response(ok: true, data: snapshotRaw(app: activeApp, options: options))
288
273
  }
289
274
  return Response(ok: true, data: snapshotFast(app: activeApp, options: options))
275
+ case .back:
276
+ if tapNavigationBack(app: activeApp) {
277
+ return Response(ok: true, data: DataPayload(message: "back"))
278
+ }
279
+ performBackGesture(app: activeApp)
280
+ return Response(ok: true, data: DataPayload(message: "back"))
281
+ case .home:
282
+ XCUIDevice.shared.press(.home)
283
+ return Response(ok: true, data: DataPayload(message: "home"))
284
+ case .appSwitcher:
285
+ performAppSwitcherGesture(app: activeApp)
286
+ return Response(ok: true, data: DataPayload(message: "appSwitcher"))
287
+ case .alert:
288
+ let action = (command.action ?? "get").lowercased()
289
+ let alert = activeApp.alerts.firstMatch
290
+ if !alert.exists {
291
+ return Response(ok: false, error: ErrorPayload(message: "alert not found"))
292
+ }
293
+ if action == "accept" {
294
+ let button = alert.buttons.allElementsBoundByIndex.first
295
+ button?.tap()
296
+ return Response(ok: true, data: DataPayload(message: "accepted"))
297
+ }
298
+ if action == "dismiss" {
299
+ let button = alert.buttons.allElementsBoundByIndex.last
300
+ button?.tap()
301
+ return Response(ok: true, data: DataPayload(message: "dismissed"))
302
+ }
303
+ let buttonLabels = alert.buttons.allElementsBoundByIndex.map { $0.label }
304
+ return Response(ok: true, data: DataPayload(message: alert.label, items: buttonLabels))
290
305
  }
291
306
  }
292
307
 
308
+ private func tapNavigationBack(app: XCUIApplication) -> Bool {
309
+ let buttons = app.navigationBars.buttons.allElementsBoundByIndex
310
+ if let back = buttons.first(where: { $0.isHittable }) {
311
+ back.tap()
312
+ return true
313
+ }
314
+ return false
315
+ }
316
+
317
+ private func performBackGesture(app: XCUIApplication) {
318
+ let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
319
+ let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.05, dy: 0.5))
320
+ let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
321
+ start.press(forDuration: 0.05, thenDragTo: end)
322
+ }
323
+
324
+ private func performAppSwitcherGesture(app: XCUIApplication) {
325
+ let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
326
+ let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.99))
327
+ let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.7))
328
+ start.press(forDuration: 0.6, thenDragTo: end)
329
+ }
330
+
293
331
  private func findElement(app: XCUIApplication, text: String) -> XCUIElement? {
294
332
  let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR identifier CONTAINS[c] %@ OR value CONTAINS[c] %@", text, text, text)
295
333
  let element = app.descendants(matching: .any).matching(predicate).firstMatch
@@ -602,7 +640,10 @@ enum CommandType: String, Codable {
602
640
  case findText
603
641
  case listTappables
604
642
  case snapshot
605
- case rect
643
+ case back
644
+ case home
645
+ case appSwitcher
646
+ case alert
606
647
  case shutdown
607
648
  }
608
649
 
@@ -617,6 +658,7 @@ struct Command: Codable {
617
658
  let command: CommandType
618
659
  let appBundleId: String?
619
660
  let text: String?
661
+ let action: String?
620
662
  let x: Double?
621
663
  let y: Double?
622
664
  let direction: SwipeDirection?
@@ -645,22 +687,19 @@ struct DataPayload: Codable {
645
687
  let items: [String]?
646
688
  let nodes: [SnapshotNode]?
647
689
  let truncated: Bool?
648
- let rect: SnapshotRect?
649
690
 
650
691
  init(
651
692
  message: String? = nil,
652
693
  found: Bool? = nil,
653
694
  items: [String]? = nil,
654
695
  nodes: [SnapshotNode]? = nil,
655
- truncated: Bool? = nil,
656
- rect: SnapshotRect? = nil
696
+ truncated: Bool? = nil
657
697
  ) {
658
698
  self.message = message
659
699
  self.found = found
660
700
  self.items = items
661
701
  self.nodes = nodes
662
702
  self.truncated = truncated
663
- self.rect = rect
664
703
  }
665
704
  }
666
705
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Callstack",
@@ -16,8 +16,10 @@
16
16
  "build": "rslib build",
17
17
  "build:node": "pnpm build && rm -f ~/.agent-device/daemon.json",
18
18
  "build:swift": "swift build -c release --package-path ios-runner/AXSnapshot",
19
- "build:axsnapshot": "pnpm build:swift && mkdir -p bin && cp -f ios-runner/AXSnapshot/.build/release/axsnapshot bin/axsnapshot && chmod +x bin/axsnapshot",
19
+ "build:axsnapshot": "pnpm build:swift && mkdir -p dist/bin && cp -f ios-runner/AXSnapshot/.build/release/axsnapshot dist/bin/axsnapshot && chmod +x dist/bin/axsnapshot",
20
+ "build:xcuitest": "AGENT_DEVICE_IOS_CLEAN_DERIVED=1 xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"generic/platform=iOS Simulator\" -derivedDataPath ~/.agent-device/ios-runner/derived",
20
21
  "build:clis": "pnpm build:node && pnpm build:axsnapshot",
22
+ "build:all": "pnpm build:node && pnpm build:axsnapshot && pnpm build:xcuitest",
21
23
  "format": "prettier --write .",
22
24
  "prepublishOnly": "pnpm build:node && pnpm build:axsnapshot",
23
25
  "prepack": "pnpm build:node && pnpm build:axsnapshot",
@@ -29,7 +31,6 @@
29
31
  "files": [
30
32
  "bin",
31
33
  "dist",
32
- "bin/axsnapshot",
33
34
  "ios-runner",
34
35
  "!ios-runner/**/.build",
35
36
  "!ios-runner/**/.swiftpm",
package/src/cli.ts CHANGED
@@ -84,6 +84,28 @@ export async function runCli(argv: string[]): Promise<void> {
84
84
  if (logTailStopper) logTailStopper();
85
85
  return;
86
86
  }
87
+ if (response.data && typeof response.data === 'object') {
88
+ const data = response.data as Record<string, unknown>;
89
+ if (command === 'devices') {
90
+ const devices = Array.isArray((data as any).devices) ? (data as any).devices : [];
91
+ const lines = devices.map((d: any) => {
92
+ const name = d?.name ?? d?.id ?? 'unknown';
93
+ const platform = d?.platform ?? 'unknown';
94
+ const kind = d?.kind ? ` ${d.kind}` : '';
95
+ const booted = typeof d?.booted === 'boolean' ? ` booted=${d.booted}` : '';
96
+ return `${name} (${platform}${kind})${booted}`;
97
+ });
98
+ process.stdout.write(`${lines.join('\n')}\n`);
99
+ if (logTailStopper) logTailStopper();
100
+ return;
101
+ }
102
+ if (command === 'apps') {
103
+ const apps = Array.isArray((data as any).apps) ? (data as any).apps : [];
104
+ process.stdout.write(`${apps.join('\n')}\n`);
105
+ if (logTailStopper) logTailStopper();
106
+ return;
107
+ }
108
+ }
87
109
  if (logTailStopper) logTailStopper();
88
110
  return;
89
111
  }