agent-device 0.12.3 → 0.12.5

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 (35) hide show
  1. package/dist/src/152.js +1 -1
  2. package/dist/src/320.js +1 -1
  3. package/dist/src/57.js +1 -1
  4. package/dist/src/641.js +38 -0
  5. package/dist/src/818.js +1 -1
  6. package/dist/src/974.js +2 -2
  7. package/dist/src/backend.d.ts +205 -0
  8. package/dist/src/backend.js +1 -0
  9. package/dist/src/bin.js +63 -63
  10. package/dist/src/commands/index.d.ts +908 -0
  11. package/dist/src/commands/index.js +1 -0
  12. package/dist/src/contracts.d.ts +1 -1
  13. package/dist/src/daemon.js +15 -15
  14. package/dist/src/index.d.ts +898 -3
  15. package/dist/src/index.js +3 -3
  16. package/dist/src/io.d.ts +85 -0
  17. package/dist/src/io.js +1 -0
  18. package/dist/src/metro-companion.js +1 -1
  19. package/dist/src/metro.d.ts +10 -0
  20. package/dist/src/metro.js +1 -1
  21. package/dist/src/selectors.js +1 -1
  22. package/dist/src/testing/conformance.d.ts +416 -0
  23. package/dist/src/testing/conformance.js +1 -0
  24. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +12 -3
  25. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +1 -0
  26. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +24 -5
  27. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +2 -0
  28. package/ios-runner/AgentDeviceRunner/RecordingScripts/recording-resize.swift +182 -0
  29. package/ios-runner/RUNNER_PROTOCOL.md +1 -1
  30. package/package.json +17 -1
  31. package/skills/agent-device/references/bootstrap-install.md +13 -0
  32. package/skills/agent-device/references/remote-tenancy.md +15 -0
  33. package/skills/agent-device/references/verification.md +1 -0
  34. package/dist/src/155.js +0 -38
  35. package/dist/src/940.js +0 -1
@@ -0,0 +1,182 @@
1
+ import AVFoundation
2
+ import Foundation
3
+
4
+ enum ResizeError: Error, CustomStringConvertible {
5
+ case invalidArgs(String)
6
+ case missingVideoTrack
7
+ case exportFailed(String)
8
+
9
+ var description: String {
10
+ switch self {
11
+ case .invalidArgs(let message):
12
+ return message
13
+ case .missingVideoTrack:
14
+ return "Input video does not contain a video track."
15
+ case .exportFailed(let message):
16
+ return message
17
+ }
18
+ }
19
+ }
20
+
21
+ do {
22
+ try run()
23
+ } catch {
24
+ fputs("recording-resize: \(error)\n", stderr)
25
+ exit(1)
26
+ }
27
+
28
+ func run() throws {
29
+ let arguments = Array(CommandLine.arguments.dropFirst())
30
+ let parsedArgs = try parseArguments(arguments)
31
+ let inputURL = URL(fileURLWithPath: parsedArgs.inputPath)
32
+ let outputURL = URL(fileURLWithPath: parsedArgs.outputPath)
33
+
34
+ if FileManager.default.fileExists(atPath: outputURL.path) {
35
+ try FileManager.default.removeItem(at: outputURL)
36
+ }
37
+
38
+ let asset = AVURLAsset(url: inputURL)
39
+ guard let sourceVideoTrack = asset.tracks(withMediaType: .video).first else {
40
+ throw ResizeError.missingVideoTrack
41
+ }
42
+
43
+ let renderSize = scaledRenderSize(for: sourceVideoTrack, quality: parsedArgs.quality)
44
+ let composition = AVMutableComposition()
45
+ let fullRange = CMTimeRange(start: .zero, duration: asset.duration)
46
+
47
+ guard let compositionVideoTrack = composition.addMutableTrack(
48
+ withMediaType: .video,
49
+ preferredTrackID: kCMPersistentTrackID_Invalid
50
+ ) else {
51
+ throw ResizeError.exportFailed("Failed to create composition video track.")
52
+ }
53
+ try compositionVideoTrack.insertTimeRange(fullRange, of: sourceVideoTrack, at: .zero)
54
+
55
+ if let sourceAudioTrack = asset.tracks(withMediaType: .audio).first,
56
+ let compositionAudioTrack = composition.addMutableTrack(
57
+ withMediaType: .audio,
58
+ preferredTrackID: kCMPersistentTrackID_Invalid
59
+ ) {
60
+ try? compositionAudioTrack.insertTimeRange(fullRange, of: sourceAudioTrack, at: .zero)
61
+ }
62
+
63
+ let scale = CGFloat(parsedArgs.quality) / 10.0
64
+ let videoComposition = AVMutableVideoComposition()
65
+ videoComposition.renderSize = renderSize
66
+ videoComposition.frameDuration = resolvedFrameDuration(for: sourceVideoTrack)
67
+
68
+ let instruction = AVMutableVideoCompositionInstruction()
69
+ instruction.timeRange = fullRange
70
+ let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack)
71
+ // Scale the full preferred transform (including translation) to match the smaller render canvas.
72
+ let scaledTransform = scaledPreferredTransform(sourceVideoTrack.preferredTransform, scale: scale)
73
+ layerInstruction.setTransform(scaledTransform, at: .zero)
74
+ instruction.layerInstructions = [layerInstruction]
75
+ videoComposition.instructions = [instruction]
76
+
77
+ guard let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) else {
78
+ throw ResizeError.exportFailed("Failed to create export session.")
79
+ }
80
+
81
+ exporter.outputURL = outputURL
82
+ exporter.outputFileType = .mp4
83
+ exporter.videoComposition = videoComposition
84
+ exporter.shouldOptimizeForNetworkUse = true
85
+
86
+ let semaphore = DispatchSemaphore(value: 0)
87
+ exporter.exportAsynchronously {
88
+ semaphore.signal()
89
+ }
90
+ if semaphore.wait(timeout: .now() + 120) == .timedOut {
91
+ exporter.cancelExport()
92
+ throw ResizeError.exportFailed("Resize export timed out.")
93
+ }
94
+
95
+ if exporter.status != .completed {
96
+ throw ResizeError.exportFailed(exporter.error?.localizedDescription ?? "Resize export failed.")
97
+ }
98
+ }
99
+
100
+ func parseArguments(_ arguments: [String]) throws -> (inputPath: String, outputPath: String, quality: Int) {
101
+ var inputPath: String?
102
+ var outputPath: String?
103
+ var quality: Int?
104
+ var index = 0
105
+
106
+ while index < arguments.count {
107
+ let argument = arguments[index]
108
+ let nextIndex = index + 1
109
+ switch argument {
110
+ case "--input":
111
+ guard nextIndex < arguments.count else { throw ResizeError.invalidArgs("--input requires a value") }
112
+ inputPath = arguments[nextIndex]
113
+ index += 2
114
+ case "--output":
115
+ guard nextIndex < arguments.count else { throw ResizeError.invalidArgs("--output requires a value") }
116
+ outputPath = arguments[nextIndex]
117
+ index += 2
118
+ case "--quality":
119
+ guard nextIndex < arguments.count else { throw ResizeError.invalidArgs("--quality requires a value") }
120
+ guard let parsed = Int(arguments[nextIndex]), parsed >= 5, parsed <= 10 else {
121
+ throw ResizeError.invalidArgs("--quality must be an integer between 5 and 10")
122
+ }
123
+ quality = parsed
124
+ index += 2
125
+ default:
126
+ throw ResizeError.invalidArgs("Unknown argument: \(argument)")
127
+ }
128
+ }
129
+
130
+ guard let inputPath, let outputPath, let quality else {
131
+ throw ResizeError.invalidArgs(
132
+ "Usage: recording-resize.swift --input <video> --output <video> --quality <5-10>"
133
+ )
134
+ }
135
+ return (inputPath, outputPath, quality)
136
+ }
137
+
138
+ func resolvedRenderSize(for track: AVAssetTrack) -> CGSize {
139
+ let transformed = track.naturalSize.applying(track.preferredTransform)
140
+ return CGSize(width: abs(transformed.width), height: abs(transformed.height))
141
+ }
142
+
143
+ func scaledRenderSize(for track: AVAssetTrack, quality: Int) -> CGSize {
144
+ let renderSize = resolvedRenderSize(for: track)
145
+ guard quality < 10 else { return renderSize }
146
+ let scale = CGFloat(quality) / 10.0
147
+ return CGSize(
148
+ width: scaledDimension(renderSize.width, scale: scale),
149
+ height: scaledDimension(renderSize.height, scale: scale)
150
+ )
151
+ }
152
+
153
+ func scaledDimension(_ value: CGFloat, scale: CGFloat) -> CGFloat {
154
+ let evenValue = Int((Double(value * scale) / 2.0).rounded()) * 2
155
+ return CGFloat(max(2, evenValue))
156
+ }
157
+
158
+ func resolvedFrameDuration(for track: AVAssetTrack) -> CMTime {
159
+ let minFrameDuration = track.minFrameDuration
160
+ if minFrameDuration.isValid && !minFrameDuration.isIndefinite && minFrameDuration.seconds > 0 {
161
+ return minFrameDuration
162
+ }
163
+
164
+ let nominalFrameRate = track.nominalFrameRate
165
+ if nominalFrameRate > 0 {
166
+ let timescale = Int32(max(1, round(nominalFrameRate)))
167
+ return CMTime(value: 1, timescale: timescale)
168
+ }
169
+
170
+ return CMTime(value: 1, timescale: 60)
171
+ }
172
+
173
+ func scaledPreferredTransform(_ transform: CGAffineTransform, scale: CGFloat) -> CGAffineTransform {
174
+ CGAffineTransform(
175
+ a: transform.a * scale,
176
+ b: transform.b * scale,
177
+ c: transform.c * scale,
178
+ d: transform.d * scale,
179
+ tx: transform.tx * scale,
180
+ ty: transform.ty * scale
181
+ )
182
+ }
@@ -33,7 +33,7 @@ Examples:
33
33
  ```
34
34
 
35
35
  ```json
36
- { "command": "recordStart", "outPath": "/tmp/demo.mp4", "fps": 30 }
36
+ { "command": "recordStart", "outPath": "/tmp/demo.mp4", "fps": 30, "quality": 7 }
37
37
  ```
38
38
 
39
39
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Callstack",
@@ -12,6 +12,22 @@
12
12
  "import": "./dist/src/index.js",
13
13
  "types": "./dist/src/index.d.ts"
14
14
  },
15
+ "./commands": {
16
+ "import": "./dist/src/commands/index.js",
17
+ "types": "./dist/src/commands/index.d.ts"
18
+ },
19
+ "./backend": {
20
+ "import": "./dist/src/backend.js",
21
+ "types": "./dist/src/backend.d.ts"
22
+ },
23
+ "./io": {
24
+ "import": "./dist/src/io.js",
25
+ "types": "./dist/src/io.d.ts"
26
+ },
27
+ "./testing/conformance": {
28
+ "import": "./dist/src/testing/conformance.js",
29
+ "types": "./dist/src/testing/conformance.d.ts"
30
+ },
15
31
  "./artifacts": {
16
32
  "import": "./dist/src/artifacts.js",
17
33
  "types": "./dist/src/artifacts.d.ts"
@@ -17,6 +17,7 @@ Use this exact order when you are not sure about the installed app identifier. O
17
17
  ## Install path
18
18
 
19
19
  - `install` or `reinstall`
20
+ - `install-from-source` when the artifact already exists at a URL the daemon can reach
20
21
 
21
22
  ## Most common mistake to avoid
22
23
 
@@ -60,14 +61,26 @@ agent-device install com.example.app ./build/app.apk --platform android --serial
60
61
  agent-device install com.example.app ./build/MyApp.app --platform ios --device "iPhone 17 Pro"
61
62
  ```
62
63
 
64
+ ```bash
65
+ agent-device install-from-source https://example.com/builds/app.aab --platform android
66
+ agent-device install-from-source https://api.github.com/repos/acme/app/actions/artifacts/123/zip --platform ios --header "authorization: Bearer TOKEN"
67
+ ```
68
+
63
69
  ## Install guidance
64
70
 
65
71
  - Use `install <app> <path>` when the app may already be installed and you do not need a fresh-state reset.
66
72
  - Use `reinstall <app> <path>` when you explicitly need uninstall plus install as one deterministic step.
73
+ - Use `install-from-source <url>` when an existing artifact URL is already reachable by the daemon.
74
+ - Local `.apk`, `.aab`, `.app`, and `.ipa` paths go through `install` or `reinstall`; existing reachable URLs go through `install-from-source`.
75
+ - Do not download, re-zip, publish temporary GitHub releases, or move CI artifacts elsewhere just to make an install command work.
67
76
  - Keep install and open as separate phases. Do not turn them into one default command flow.
68
77
  - Supported binary formats:
69
78
  - Android: `.apk` and `.aab`
70
79
  - iOS: `.app` and `.ipa`
80
+ - Android URL sources can be direct `.apk` or `.aab` files.
81
+ - Trusted artifact service URLs, currently GitHub Actions and EAS, may point at archive-backed downloads that contain one installable artifact. This includes GitHub Actions artifact ZIPs whose URL path does not end in `.zip` and ZIPs containing one nested `.apk`, `.aab`, `.ipa`, or iOS `.app` tar archive.
82
+ - If a trusted artifact archive contains multiple installables, stop and ask for the intended artifact instead of guessing.
83
+ - `.aab` still requires `bundletool` in `PATH`, or `AGENT_DEVICE_BUNDLETOOL_JAR=<absolute-path-to-bundletool-all.jar>` with `java` in `PATH`, when the daemon installs the materialized artifact.
71
84
  - For iOS `.ipa` files, `<app>` is used as the bundle id or bundle name hint when the archive contains multiple app bundles.
72
85
  - After install or reinstall, later use `open <app>` with the exact discovered or known package/bundle identifier, not the artifact path.
73
86
 
@@ -27,6 +27,7 @@ agent-device connect \
27
27
  --remote-config ./remote-config.json
28
28
 
29
29
  agent-device install com.example.app ./app.apk
30
+ agent-device install-from-source https://example.com/builds/app.apk --platform android
30
31
  agent-device open com.example.app --relaunch
31
32
  agent-device snapshot -i
32
33
  agent-device fill @e3 "test@example.com"
@@ -35,6 +36,20 @@ agent-device disconnect
35
36
 
36
37
  `connect` resolves the remote profile, verifies daemon reachability through the normal client path, allocates or refreshes the tenant lease, prepares local Metro when the profile has Metro fields, starts the local Metro companion when the bridge needs it, and writes local non-secret connection state for later commands. `disconnect` closes the session when possible, stops the Metro companion owned by that connection, releases the lease, and removes local connection state.
37
38
 
39
+ After `connect`, normal `agent-device` commands use the active remote connection. Do not repeat `--remote-config` on every command.
40
+
41
+ Remote install examples:
42
+
43
+ ```bash
44
+ agent-device install com.example.app ./app.apk
45
+ agent-device install-from-source https://example.com/builds/app.aab --platform android
46
+ agent-device install-from-source https://api.github.com/repos/acme/app/actions/artifacts/123/zip --platform ios --header "authorization: Bearer TOKEN"
47
+ ```
48
+
49
+ - Use `install` or `reinstall` for local paths; remote daemons upload local artifacts automatically.
50
+ - Use `install-from-source` for artifact URLs the remote daemon can reach.
51
+ - For local-path versus URL artifact rules, follow [bootstrap-install.md](bootstrap-install.md).
52
+
38
53
  Use `agent-device connection status --session adc-android` to inspect the active connection without reading JSON state manually. Status output must not include auth tokens.
39
54
 
40
55
  ## Remote config shape
@@ -82,6 +82,7 @@ agent-device record stop
82
82
  - On iOS, recording is a wrapper around `simctl` for simulators and the corresponding device capture path for physical devices.
83
83
  - On Android, recording is a wrapper around `adb`.
84
84
  - Recording writes a video artifact and a gesture-telemetry sidecar JSON.
85
+ - Use `record start <path> --quality 5` when a smaller video is easier to inspect or share. The scale is 5-10, where 10 is native resolution; omit it to preserve native/current resolution.
85
86
  - On macOS hosts, touch overlay burn-in is available for supported recordings.
86
87
  - On non-macOS hosts, recording still succeeds but the video stays raw and `record stop` can return an `overlayWarning`.
87
88
  - If the agent already knows the interaction sequence and wants a more lifelike, uninterrupted recording, drive the flow with `batch` while recording instead of replanning between each step.