@ulpi/browse 1.4.4 → 2.3.1

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 (32) hide show
  1. package/LICENSE +191 -21
  2. package/README.md +186 -12
  3. package/bin/browse-android-app.apk +0 -0
  4. package/bin/browse-android.apk +0 -0
  5. package/bin/browse-ax +0 -0
  6. package/bin/browse-ios-runner/BrowseRunnerApp/BrowseRunnerApp.swift +382 -0
  7. package/bin/browse-ios-runner/BrowseRunnerApp/RunnerStatusStore.swift +135 -0
  8. package/bin/browse-ios-runner/BrowseRunnerUITests/ActionHandler.swift +277 -0
  9. package/bin/browse-ios-runner/BrowseRunnerUITests/BrowseRunnerUITests.swift +60 -0
  10. package/bin/browse-ios-runner/BrowseRunnerUITests/Models.swift +78 -0
  11. package/bin/browse-ios-runner/BrowseRunnerUITests/RunnerServer.swift +246 -0
  12. package/bin/browse-ios-runner/BrowseRunnerUITests/ScreenshotHandler.swift +35 -0
  13. package/bin/browse-ios-runner/BrowseRunnerUITests/StateHandler.swift +82 -0
  14. package/bin/browse-ios-runner/BrowseRunnerUITests/TreeBuilder.swift +323 -0
  15. package/bin/browse-ios-runner/build.sh +81 -0
  16. package/bin/browse-ios-runner/project.yml +47 -0
  17. package/browse-ios-runner/BrowseRunnerApp/BrowseRunnerApp.swift +382 -0
  18. package/browse-ios-runner/BrowseRunnerApp/RunnerStatusStore.swift +135 -0
  19. package/browse-ios-runner/BrowseRunnerUITests/ActionHandler.swift +277 -0
  20. package/browse-ios-runner/BrowseRunnerUITests/BrowseRunnerUITests.swift +60 -0
  21. package/browse-ios-runner/BrowseRunnerUITests/Models.swift +78 -0
  22. package/browse-ios-runner/BrowseRunnerUITests/RunnerServer.swift +246 -0
  23. package/browse-ios-runner/BrowseRunnerUITests/ScreenshotHandler.swift +35 -0
  24. package/browse-ios-runner/BrowseRunnerUITests/StateHandler.swift +82 -0
  25. package/browse-ios-runner/BrowseRunnerUITests/TreeBuilder.swift +323 -0
  26. package/browse-ios-runner/README.md +194 -0
  27. package/browse-ios-runner/build.sh +81 -0
  28. package/browse-ios-runner/project.yml +47 -0
  29. package/dist/browse.cjs +26595 -13648
  30. package/package.json +15 -5
  31. package/skill/SKILL.md +33 -0
  32. package/skill/references/commands.md +18 -0
@@ -0,0 +1,382 @@
1
+ import SwiftUI
2
+
3
+ @main
4
+ struct BrowseRunnerApp: App {
5
+ var body: some Scene {
6
+ WindowGroup {
7
+ ContentView()
8
+ .preferredColorScheme(.dark)
9
+ }
10
+ }
11
+ }
12
+
13
+ // MARK: - Theme
14
+
15
+ private enum Theme {
16
+ static let background = Color.black
17
+ static let cardFill = Color(white: 0.067)
18
+ static let cardBorder = Color(white: 0.133)
19
+ static let divider = Color(white: 0.118)
20
+ static let primaryText = Color.white
21
+ static let secondaryText = Color.white.opacity(0.4)
22
+ static let tertiaryText = Color.white.opacity(0.25)
23
+ static let connectedGreen = Color(red: 0.133, green: 0.773, blue: 0.369)
24
+ static let disconnectedRed = Color(red: 0.937, green: 0.267, blue: 0.267)
25
+ static let staleOpacity: Double = 0.45
26
+ }
27
+
28
+ // MARK: - Content View
29
+
30
+ struct ContentView: View {
31
+ @StateObject private var store = RunnerStatusStore()
32
+ @Environment(\.scenePhase) private var scenePhase
33
+
34
+ var body: some View {
35
+ ZStack {
36
+ Theme.background.ignoresSafeArea()
37
+
38
+ VStack(spacing: 0) {
39
+ HeaderView(store: store)
40
+
41
+ Theme.divider.frame(height: 1)
42
+ .padding(.horizontal, 24)
43
+ .padding(.top, 12)
44
+
45
+ TargetAppCard(store: store)
46
+ .padding(.top, 16)
47
+
48
+ StateGrid(store: store)
49
+ .padding(.top, 16)
50
+
51
+ Spacer(minLength: 0)
52
+
53
+ FooterView()
54
+ .padding(.top, 24)
55
+ .padding(.bottom, 16)
56
+ }
57
+ .padding(.horizontal, 24)
58
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
59
+ }
60
+ .onChange(of: scenePhase) { newPhase in
61
+ switch newPhase {
62
+ case .active:
63
+ store.start()
64
+ case .background, .inactive:
65
+ store.stop()
66
+ @unknown default:
67
+ break
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ // MARK: - Header
74
+
75
+ private struct HeaderView: View {
76
+ @ObservedObject var store: RunnerStatusStore
77
+
78
+ var body: some View {
79
+ VStack(alignment: .leading, spacing: 12) {
80
+ HStack(alignment: .center, spacing: 0) {
81
+ HStack(spacing: 14) {
82
+ AppIconView(isConnected: store.isConnected)
83
+ VStack(alignment: .leading, spacing: 3) {
84
+ Text("BrowseRunner")
85
+ .font(.system(size: 20, weight: .bold))
86
+ .foregroundStyle(Theme.primaryText)
87
+ .fixedSize()
88
+ Text("v\(store.version)")
89
+ .font(.system(size: 12, design: .monospaced))
90
+ .foregroundStyle(Theme.secondaryText)
91
+ }
92
+ }
93
+ .layoutPriority(1)
94
+
95
+ Spacer(minLength: 8)
96
+
97
+ VStack(alignment: .trailing, spacing: 4) {
98
+ ConnectionBadge(isConnected: store.isConnected)
99
+ PortBadge(port: store.port)
100
+ }
101
+ }
102
+
103
+ Text("iOS automation runner")
104
+ .font(.system(size: 12, weight: .medium))
105
+ .foregroundStyle(Color.white.opacity(0.3))
106
+ }
107
+ }
108
+ }
109
+
110
+ // MARK: - App Icon
111
+
112
+ private struct AppIconView: View {
113
+ let isConnected: Bool
114
+ private var accent: Color { isConnected ? Theme.connectedGreen : Theme.disconnectedRed }
115
+
116
+ var body: some View {
117
+ Text(">_")
118
+ .font(.system(size: 15, weight: .bold, design: .monospaced))
119
+ .foregroundStyle(accent)
120
+ .frame(width: 42, height: 42)
121
+ .background(
122
+ RoundedRectangle(cornerRadius: 11)
123
+ .fill(Theme.cardFill)
124
+ .overlay(
125
+ RoundedRectangle(cornerRadius: 11)
126
+ .strokeBorder(accent.opacity(0.3), lineWidth: 1)
127
+ )
128
+ )
129
+ }
130
+ }
131
+
132
+ // MARK: - Connection Badge
133
+
134
+ private struct ConnectionBadge: View {
135
+ let isConnected: Bool
136
+ @State private var pulse = false
137
+ private var color: Color { isConnected ? Theme.connectedGreen : Theme.disconnectedRed }
138
+
139
+ var body: some View {
140
+ HStack(spacing: 6) {
141
+ Circle()
142
+ .fill(color)
143
+ .frame(width: 6, height: 6)
144
+ .shadow(color: isConnected ? color.opacity(pulse ? 0.5 : 0.1) : .clear,
145
+ radius: isConnected ? (pulse ? 6 : 2) : 0)
146
+ .animation(isConnected ? .easeInOut(duration: 2).repeatForever(autoreverses: true) : .default,
147
+ value: pulse)
148
+ .onAppear { pulse = true }
149
+
150
+ Text(isConnected ? "Connected" : "Disconnected")
151
+ .font(.system(size: 11, weight: .semibold, design: .monospaced))
152
+ .foregroundStyle(color)
153
+ .fixedSize()
154
+ }
155
+ .padding(.horizontal, 10)
156
+ .padding(.vertical, 5)
157
+ .background(
158
+ RoundedRectangle(cornerRadius: 8)
159
+ .fill(color.opacity(0.1))
160
+ .overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(color.opacity(0.25), lineWidth: 1))
161
+ )
162
+ .fixedSize()
163
+ }
164
+ }
165
+
166
+ // MARK: - Port Badge
167
+
168
+ private struct PortBadge: View {
169
+ let port: Int
170
+
171
+ var body: some View {
172
+ Text(":\(String(port))")
173
+ .font(.system(size: 11, weight: .medium, design: .monospaced))
174
+ .foregroundStyle(Color.white.opacity(0.5))
175
+ .padding(.horizontal, 9)
176
+ .padding(.vertical, 4)
177
+ .background(
178
+ RoundedRectangle(cornerRadius: 6)
179
+ .fill(Theme.cardFill)
180
+ .overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(Theme.cardBorder, lineWidth: 1))
181
+ )
182
+ }
183
+ }
184
+
185
+ // MARK: - Target App Card
186
+
187
+ private struct TargetAppCard: View {
188
+ @ObservedObject var store: RunnerStatusStore
189
+ private var stale: Bool { !store.isConnected && store.hasEverConnected }
190
+ private var empty: Bool { !store.isConnected && !store.hasEverConnected }
191
+
192
+ var body: some View {
193
+ VStack(alignment: .leading, spacing: 10) {
194
+ SectionLabel(title: "TARGET APP")
195
+
196
+ VStack(alignment: .leading, spacing: 14) {
197
+ Text(store.bundleId ?? "No target")
198
+ .font(.system(size: 15, weight: .medium, design: .monospaced))
199
+ .foregroundStyle(Theme.primaryText)
200
+
201
+ VStack(spacing: 8) {
202
+ DetailRow(label: "Current Screen", value: store.screenTitle ?? (empty ? "Unavailable" : "--"))
203
+ DetailRow(label: "Orientation", value: displayOrientation)
204
+ DetailRow(label: "Last Sync", value: formattedSync, mono: true)
205
+ }
206
+ }
207
+ .padding(16)
208
+ .background(
209
+ RoundedRectangle(cornerRadius: 14)
210
+ .fill(Theme.cardFill)
211
+ .overlay(RoundedRectangle(cornerRadius: 14).strokeBorder(Theme.cardBorder, lineWidth: 1))
212
+ )
213
+ .opacity(stale ? Theme.staleOpacity : 1)
214
+ }
215
+ }
216
+
217
+ private var displayOrientation: String {
218
+ guard let o = store.orientation else { return "--" }
219
+ return o.prefix(1).uppercased() + o.dropFirst()
220
+ }
221
+
222
+ private var formattedSync: String {
223
+ guard let date = store.lastSync else { return "--" }
224
+ let fmt = DateFormatter()
225
+ fmt.dateFormat = "H:mm:ss"
226
+ return fmt.string(from: date)
227
+ }
228
+ }
229
+
230
+ // MARK: - Detail Row
231
+
232
+ private struct DetailRow: View {
233
+ let label: String
234
+ let value: String
235
+ var mono: Bool = false
236
+
237
+ var body: some View {
238
+ HStack {
239
+ Text(label)
240
+ .font(.system(size: 12, weight: .medium))
241
+ .foregroundStyle(Theme.secondaryText)
242
+ Spacer()
243
+ Text(value)
244
+ .font(.system(size: 12, weight: .medium, design: mono ? .monospaced : .default))
245
+ .foregroundStyle(Theme.primaryText)
246
+ }
247
+ }
248
+ }
249
+
250
+ // MARK: - State Grid
251
+
252
+ private struct StateGrid: View {
253
+ @ObservedObject var store: RunnerStatusStore
254
+ private var stale: Bool { !store.isConnected && store.hasEverConnected }
255
+ private var empty: Bool { !store.isConnected && !store.hasEverConnected }
256
+
257
+ private let columns = [
258
+ GridItem(.flexible(), spacing: 8),
259
+ GridItem(.flexible(), spacing: 8),
260
+ ]
261
+
262
+ var body: some View {
263
+ VStack(alignment: .leading, spacing: 10) {
264
+ SectionLabel(title: "STATE")
265
+
266
+ LazyVGrid(columns: columns, spacing: 8) {
267
+ MetricCard(label: "ELEMENTS") {
268
+ Text(store.elementCount.map(String.init) ?? "--")
269
+ .font(.system(size: 26, weight: .bold, design: .monospaced))
270
+ .foregroundStyle(Theme.primaryText)
271
+ }
272
+
273
+ MetricCard(label: "UPTIME") {
274
+ TimelineView(.periodic(from: .now, by: 1)) { ctx in
275
+ Text(formatUptime(ctx.date.timeIntervalSince(store.launchTime)))
276
+ .font(.system(size: 26, weight: .bold, design: .monospaced))
277
+ .foregroundStyle(Theme.primaryText)
278
+ }
279
+ }
280
+
281
+ MetricCard(label: "KEYBOARD") {
282
+ StatusDot(label: displayKeyboard, active: store.keyboardVisible == true)
283
+ }
284
+
285
+ MetricCard(label: "ALERT") {
286
+ StatusDot(label: displayAlert, active: store.alertPresent == true)
287
+ }
288
+ }
289
+ .opacity(stale ? Theme.staleOpacity : 1)
290
+ }
291
+ }
292
+
293
+ private var displayKeyboard: String {
294
+ guard let v = store.keyboardVisible else { return "--" }
295
+ return v ? "Visible" : "Hidden"
296
+ }
297
+
298
+ private var displayAlert: String {
299
+ guard let v = store.alertPresent else { return "--" }
300
+ return v ? "Present" : "None"
301
+ }
302
+
303
+ private func formatUptime(_ interval: TimeInterval) -> String {
304
+ let total = Int(max(0, interval))
305
+ let h = total / 3600, m = (total % 3600) / 60, s = total % 60
306
+ return h > 0 ? "\(h)h \(m)m" : "\(m)m \(s)s"
307
+ }
308
+ }
309
+
310
+ // MARK: - Metric Card
311
+
312
+ private struct MetricCard<Content: View>: View {
313
+ let label: String
314
+ @ViewBuilder let content: Content
315
+
316
+ var body: some View {
317
+ VStack(alignment: .leading, spacing: 8) {
318
+ Text(label)
319
+ .font(.system(size: 10, weight: .semibold))
320
+ .tracking(0.5)
321
+ .foregroundStyle(Theme.secondaryText)
322
+ .textCase(.uppercase)
323
+ content
324
+ }
325
+ .frame(maxWidth: .infinity, alignment: .leading)
326
+ .padding(14)
327
+ .background(
328
+ RoundedRectangle(cornerRadius: 12)
329
+ .fill(Theme.cardFill)
330
+ .overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(Theme.cardBorder, lineWidth: 1))
331
+ )
332
+ }
333
+ }
334
+
335
+ // MARK: - Status Dot
336
+
337
+ private struct StatusDot: View {
338
+ let label: String
339
+ var active: Bool = false
340
+
341
+ var body: some View {
342
+ HStack(spacing: 8) {
343
+ Circle()
344
+ .fill(Color.white.opacity(active ? 0.6 : 0.25))
345
+ .frame(width: 6, height: 6)
346
+ Text(label)
347
+ .font(.system(size: 14, weight: .medium))
348
+ .foregroundStyle(Color.white.opacity(0.5))
349
+ }
350
+ }
351
+ }
352
+
353
+ // MARK: - Section Label
354
+
355
+ private struct SectionLabel: View {
356
+ let title: String
357
+
358
+ var body: some View {
359
+ Text(title)
360
+ .font(.system(size: 10, weight: .semibold))
361
+ .tracking(1)
362
+ .foregroundStyle(Theme.tertiaryText)
363
+ .textCase(.uppercase)
364
+ }
365
+ }
366
+
367
+ // MARK: - Footer
368
+
369
+ private struct FooterView: View {
370
+ var body: some View {
371
+ VStack(spacing: 4) {
372
+ Text("Commands are executed from browse CLI")
373
+ .font(.system(size: 11))
374
+ .foregroundStyle(Color.white.opacity(0.3))
375
+ Text("This app shows runner health and current target state")
376
+ .font(.system(size: 11))
377
+ .foregroundStyle(Color.white.opacity(0.2))
378
+ }
379
+ .frame(maxWidth: .infinity)
380
+ .multilineTextAlignment(.center)
381
+ }
382
+ }
@@ -0,0 +1,135 @@
1
+ import Foundation
2
+ import SwiftUI
3
+
4
+ // MARK: - Decode Models
5
+
6
+ struct RunnerEnvelope<T: Decodable>: Decodable {
7
+ let success: Bool
8
+ let data: T?
9
+ let error: String?
10
+ }
11
+
12
+ struct RunnerHealthData: Decodable {
13
+ let status: String
14
+ }
15
+
16
+ struct RunnerStateData: Decodable {
17
+ let bundleId: String
18
+ let screenTitle: String
19
+ let elementCount: Int
20
+ let alertPresent: Bool
21
+ let keyboardVisible: Bool
22
+ let orientation: String
23
+ let statusBarTime: String
24
+ }
25
+
26
+ // MARK: - RunnerStatusStore
27
+
28
+ @MainActor
29
+ final class RunnerStatusStore: ObservableObject {
30
+
31
+ // MARK: Connection
32
+
33
+ @Published var isConnected: Bool = false
34
+ @Published var hasEverConnected: Bool = false
35
+ @Published var isLoading: Bool = false
36
+ @Published var lastError: String?
37
+
38
+ // MARK: State fields
39
+
40
+ @Published var bundleId: String? = nil
41
+ @Published var screenTitle: String? = nil
42
+ @Published var elementCount: Int? = nil
43
+ @Published var orientation: String? = nil
44
+ @Published var keyboardVisible: Bool? = nil
45
+ @Published var alertPresent: Bool? = nil
46
+ @Published var lastSync: Date? = nil
47
+
48
+ // MARK: Server info
49
+
50
+ let port: Int = 9820
51
+ let version: String = {
52
+ Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
53
+ }()
54
+ let launchTime: Date = Date()
55
+
56
+ // MARK: Private
57
+
58
+ private var pollingTask: Task<Void, Never>?
59
+ private let session: URLSession = {
60
+ let config = URLSessionConfiguration.ephemeral
61
+ config.timeoutIntervalForRequest = 4
62
+ config.timeoutIntervalForResource = 5
63
+ config.waitsForConnectivity = false
64
+ return URLSession(configuration: config)
65
+ }()
66
+
67
+ private let baseURL = "http://127.0.0.1:9820"
68
+
69
+ // MARK: Lifecycle
70
+
71
+ func start() {
72
+ guard pollingTask == nil else { return }
73
+ pollingTask = Task { [weak self] in
74
+ guard let self else { return }
75
+ await self.refresh()
76
+ while !Task.isCancelled {
77
+ try? await Task.sleep(for: .seconds(2))
78
+ guard !Task.isCancelled else { break }
79
+ await self.refresh()
80
+ }
81
+ }
82
+ }
83
+
84
+ func stop() {
85
+ pollingTask?.cancel()
86
+ pollingTask = nil
87
+ }
88
+
89
+ func refresh() async {
90
+ isLoading = true
91
+ defer { isLoading = false }
92
+
93
+ do {
94
+ let healthURL = URL(string: "\(baseURL)/health")!
95
+ let (healthData, _) = try await session.data(from: healthURL)
96
+ let envelope = try JSONDecoder().decode(RunnerEnvelope<RunnerHealthData>.self, from: healthData)
97
+
98
+ guard envelope.success, envelope.data?.status == "healthy" else {
99
+ isConnected = false
100
+ lastError = envelope.error ?? "Unhealthy"
101
+ return
102
+ }
103
+ } catch {
104
+ isConnected = false
105
+ lastError = error.localizedDescription
106
+ return
107
+ }
108
+
109
+ do {
110
+ let stateURL = URL(string: "\(baseURL)/state")!
111
+ let (stateData, _) = try await session.data(from: stateURL)
112
+ let envelope = try JSONDecoder().decode(RunnerEnvelope<RunnerStateData>.self, from: stateData)
113
+
114
+ guard envelope.success, let state = envelope.data else {
115
+ isConnected = false
116
+ lastError = envelope.error ?? "No state data"
117
+ return
118
+ }
119
+
120
+ isConnected = true
121
+ hasEverConnected = true
122
+ lastError = nil
123
+ bundleId = state.bundleId
124
+ screenTitle = state.screenTitle
125
+ elementCount = state.elementCount
126
+ orientation = state.orientation
127
+ keyboardVisible = state.keyboardVisible
128
+ alertPresent = state.alertPresent
129
+ lastSync = Date()
130
+ } catch {
131
+ isConnected = false
132
+ lastError = error.localizedDescription
133
+ }
134
+ }
135
+ }