agent-device 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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/bin/agent-device.mjs +14 -0
  4. package/bin/axsnapshot +0 -0
  5. package/dist/src/861.js +1 -0
  6. package/dist/src/bin.js +50 -0
  7. package/dist/src/daemon.js +5 -0
  8. package/ios-runner/AXSnapshot/Package.swift +18 -0
  9. package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +167 -0
  10. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.swift +17 -0
  11. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
  12. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner/Assets.xcassets/AppIcon.appiconset/Contents.json +36 -0
  13. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner/Assets.xcassets/AppIcon.appiconset/logo.jpg +0 -0
  14. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner/Assets.xcassets/Contents.json +6 -0
  15. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner/Assets.xcassets/Logo.imageset/Contents.json +21 -0
  16. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner/Assets.xcassets/Logo.imageset/logo.jpg +0 -0
  17. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner/Assets.xcassets/PoweredBy.imageset/Contents.json +21 -0
  18. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner/Assets.xcassets/PoweredBy.imageset/powered-by.png +0 -0
  19. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner/ContentView.swift +34 -0
  20. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +461 -0
  21. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  22. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +102 -0
  23. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +696 -0
  24. package/ios-runner/README.md +11 -0
  25. package/package.json +66 -0
  26. package/src/bin.ts +3 -0
  27. package/src/cli.ts +160 -0
  28. package/src/core/dispatch.ts +259 -0
  29. package/src/daemon-client.ts +166 -0
  30. package/src/daemon.ts +842 -0
  31. package/src/platforms/android/devices.ts +59 -0
  32. package/src/platforms/android/index.ts +442 -0
  33. package/src/platforms/ios/ax-snapshot.ts +154 -0
  34. package/src/platforms/ios/devices.ts +65 -0
  35. package/src/platforms/ios/index.ts +218 -0
  36. package/src/platforms/ios/runner-client.ts +534 -0
  37. package/src/utils/args.ts +175 -0
  38. package/src/utils/device.ts +84 -0
  39. package/src/utils/errors.ts +35 -0
  40. package/src/utils/exec.ts +229 -0
  41. package/src/utils/interactive.ts +4 -0
  42. package/src/utils/interactors.ts +72 -0
  43. package/src/utils/output.ts +146 -0
  44. package/src/utils/snapshot.ts +63 -0
@@ -0,0 +1,65 @@
1
+ import { runCmd, whichCmd } from '../../utils/exec.ts';
2
+ import { AppError } from '../../utils/errors.ts';
3
+ import type { DeviceInfo } from '../../utils/device.ts';
4
+
5
+ export async function listIosDevices(): Promise<DeviceInfo[]> {
6
+ if (process.platform !== 'darwin') {
7
+ throw new AppError('UNSUPPORTED_PLATFORM', 'iOS tools are only available on macOS');
8
+ }
9
+
10
+ const simctlAvailable = await whichCmd('xcrun');
11
+ if (!simctlAvailable) {
12
+ throw new AppError('TOOL_MISSING', 'xcrun not found in PATH');
13
+ }
14
+
15
+ const devices: DeviceInfo[] = [];
16
+
17
+ const simResult = await runCmd('xcrun', ['simctl', 'list', 'devices', '-j']);
18
+ try {
19
+ const payload = JSON.parse(simResult.stdout as string) as {
20
+ devices: Record<
21
+ string,
22
+ { name: string; udid: string; state: string; isAvailable: boolean }[]
23
+ >;
24
+ };
25
+ for (const runtimes of Object.values(payload.devices)) {
26
+ for (const device of runtimes) {
27
+ if (!device.isAvailable) continue;
28
+ devices.push({
29
+ platform: 'ios',
30
+ id: device.udid,
31
+ name: device.name,
32
+ kind: 'simulator',
33
+ booted: device.state === 'Booted',
34
+ });
35
+ }
36
+ }
37
+ } catch (err) {
38
+ throw new AppError('COMMAND_FAILED', 'Failed to parse simctl devices JSON', undefined, err);
39
+ }
40
+
41
+ const devicectlAvailable = await whichCmd('xcrun');
42
+ if (devicectlAvailable) {
43
+ try {
44
+ const result = await runCmd('xcrun', ['devicectl', 'list', 'devices', '--json']);
45
+ const payload = JSON.parse(result.stdout as string) as {
46
+ devices: { identifier: string; name: string; platform: string }[];
47
+ };
48
+ for (const device of payload.devices ?? []) {
49
+ if (device.platform?.toLowerCase().includes('ios')) {
50
+ devices.push({
51
+ platform: 'ios',
52
+ id: device.identifier,
53
+ name: device.name,
54
+ kind: 'device',
55
+ booted: true,
56
+ });
57
+ }
58
+ }
59
+ } catch {
60
+ // Ignore devicectl failures; simulators are still supported.
61
+ }
62
+ }
63
+
64
+ return devices;
65
+ }
@@ -0,0 +1,218 @@
1
+ import { runCmd } from '../../utils/exec.ts';
2
+ import { AppError } from '../../utils/errors.ts';
3
+ import type { DeviceInfo } from '../../utils/device.ts';
4
+
5
+ const ALIASES: Record<string, string> = {
6
+ settings: 'com.apple.Preferences',
7
+ };
8
+
9
+ export async function resolveIosApp(device: DeviceInfo, app: string): Promise<string> {
10
+ const trimmed = app.trim();
11
+ if (trimmed.includes('.')) return trimmed;
12
+
13
+ const alias = ALIASES[trimmed.toLowerCase()];
14
+ if (alias) return alias;
15
+
16
+ if (device.kind === 'simulator') {
17
+ const list = await listSimulatorApps(device);
18
+ const matches = list.filter((entry) => entry.name.toLowerCase() === trimmed.toLowerCase());
19
+ if (matches.length === 1) return matches[0].bundleId;
20
+ if (matches.length > 1) {
21
+ throw new AppError('INVALID_ARGS', `Multiple apps matched "${app}"`, { matches });
22
+ }
23
+ }
24
+
25
+ throw new AppError('APP_NOT_INSTALLED', `No app found matching "${app}"`);
26
+ }
27
+
28
+ export async function openIosApp(device: DeviceInfo, app: string): Promise<void> {
29
+ const bundleId = await resolveIosApp(device, app);
30
+ if (device.kind === 'simulator') {
31
+ await ensureBootedSimulator(device);
32
+ await runCmd('xcrun', ['simctl', 'launch', device.id, bundleId]);
33
+ return;
34
+ }
35
+ await runCmd('xcrun', [
36
+ 'devicectl',
37
+ 'device',
38
+ 'process',
39
+ 'launch',
40
+ '--device',
41
+ device.id,
42
+ bundleId,
43
+ ]);
44
+ }
45
+
46
+ export async function closeIosApp(device: DeviceInfo, app: string): Promise<void> {
47
+ const bundleId = await resolveIosApp(device, app);
48
+ if (device.kind === 'simulator') {
49
+ await ensureBootedSimulator(device);
50
+ const result = await runCmd('xcrun', ['simctl', 'terminate', device.id, bundleId], {
51
+ allowFailure: true,
52
+ });
53
+ if (result.exitCode !== 0) {
54
+ const stderr = result.stderr.toLowerCase();
55
+ if (stderr.includes('found nothing to terminate')) return;
56
+ throw new AppError('COMMAND_FAILED', `xcrun exited with code ${result.exitCode}`, {
57
+ cmd: 'xcrun',
58
+ args: ['simctl', 'terminate', device.id, bundleId],
59
+ stdout: result.stdout,
60
+ stderr: result.stderr,
61
+ exitCode: result.exitCode,
62
+ });
63
+ }
64
+ return;
65
+ }
66
+ await runCmd('xcrun', [
67
+ 'devicectl',
68
+ 'device',
69
+ 'process',
70
+ 'terminate',
71
+ '--device',
72
+ device.id,
73
+ bundleId,
74
+ ]);
75
+ }
76
+
77
+ export async function pressIos(device: DeviceInfo, x: number, y: number): Promise<void> {
78
+ ensureSimulator(device, 'press');
79
+ await ensureBootedSimulator(device);
80
+ throw new AppError(
81
+ 'UNSUPPORTED_OPERATION',
82
+ 'simctl io tap is not available; use the XCTest runner for input',
83
+ );
84
+ }
85
+
86
+ export async function longPressIos(
87
+ device: DeviceInfo,
88
+ x: number,
89
+ y: number,
90
+ durationMs = 800,
91
+ ): Promise<void> {
92
+ ensureSimulator(device, 'long-press');
93
+ await ensureBootedSimulator(device);
94
+ throw new AppError(
95
+ 'UNSUPPORTED_OPERATION',
96
+ 'long-press is not supported on iOS simulators without XCTest runner support',
97
+ );
98
+ }
99
+
100
+ export async function focusIos(device: DeviceInfo, x: number, y: number): Promise<void> {
101
+ await pressIos(device, x, y);
102
+ }
103
+
104
+ export async function typeIos(device: DeviceInfo, text: string): Promise<void> {
105
+ ensureSimulator(device, 'type');
106
+ await ensureBootedSimulator(device);
107
+ throw new AppError(
108
+ 'UNSUPPORTED_OPERATION',
109
+ 'simctl io keyboard is not available; use the XCTest runner for input',
110
+ );
111
+ }
112
+
113
+ export async function fillIos(
114
+ device: DeviceInfo,
115
+ x: number,
116
+ y: number,
117
+ text: string,
118
+ ): Promise<void> {
119
+ await focusIos(device, x, y);
120
+ await typeIos(device, text);
121
+ }
122
+
123
+ export async function scrollIos(
124
+ device: DeviceInfo,
125
+ direction: string,
126
+ amount = 0.6,
127
+ ): Promise<void> {
128
+ ensureSimulator(device, 'scroll');
129
+ await ensureBootedSimulator(device);
130
+ throw new AppError(
131
+ 'UNSUPPORTED_OPERATION',
132
+ 'simctl io swipe is not available; use the XCTest runner for input',
133
+ );
134
+ }
135
+
136
+ export async function scrollIntoViewIos(text: string): Promise<void> {
137
+ throw new AppError(
138
+ 'UNSUPPORTED_OPERATION',
139
+ `scrollintoview is not supported on iOS without UI automation (${text})`,
140
+ );
141
+ }
142
+
143
+ export async function screenshotIos(device: DeviceInfo, outPath: string): Promise<void> {
144
+ if (device.kind === 'simulator') {
145
+ await ensureBootedSimulator(device);
146
+ await runCmd('xcrun', ['simctl', 'io', device.id, 'screenshot', outPath]);
147
+ return;
148
+ }
149
+ await runCmd('xcrun', ['devicectl', 'device', 'screenshot', '--device', device.id, outPath]);
150
+ }
151
+
152
+ function ensureSimulator(device: DeviceInfo, command: string): void {
153
+ if (device.kind !== 'simulator') {
154
+ throw new AppError(
155
+ 'UNSUPPORTED_OPERATION',
156
+ `${command} is only supported on iOS simulators in v1`,
157
+ );
158
+ }
159
+ }
160
+
161
+ async function listSimulatorApps(
162
+ device: DeviceInfo,
163
+ ): Promise<{ bundleId: string; name: string }[]> {
164
+ const result = await runCmd('xcrun', ['simctl', 'listapps', device.id], { allowFailure: true });
165
+ const stdout = result.stdout as string;
166
+ if (!stdout.trim().startsWith('{')) return [];
167
+ try {
168
+ const payload = JSON.parse(stdout) as Record<
169
+ string,
170
+ { CFBundleDisplayName?: string; CFBundleName?: string }
171
+ >;
172
+ return Object.entries(payload).map(([bundleId, info]) => ({
173
+ bundleId,
174
+ name: info.CFBundleDisplayName ?? info.CFBundleName ?? bundleId,
175
+ }));
176
+ } catch {
177
+ return [];
178
+ }
179
+ }
180
+
181
+ export async function getSimulatorScreenSize(
182
+ device: DeviceInfo,
183
+ ): Promise<{ width: number; height: number }> {
184
+ await ensureBootedSimulator(device);
185
+ const result = await runCmd('xcrun', ['simctl', 'io', device.id, 'status-bar', '--list'], {
186
+ allowFailure: true,
187
+ });
188
+ const match = (result.stdout as string).match(/(\d+)x(\d+)/);
189
+ if (match) return { width: Number(match[1]), height: Number(match[2]) };
190
+ return { width: 1170, height: 2532 };
191
+ }
192
+
193
+ async function ensureBootedSimulator(device: DeviceInfo): Promise<void> {
194
+ if (device.kind !== 'simulator') return;
195
+ const state = await getSimulatorState(device.id);
196
+ if (state === 'Booted') return;
197
+ await runCmd('xcrun', ['simctl', 'boot', device.id], { allowFailure: true });
198
+ await runCmd('xcrun', ['simctl', 'bootstatus', device.id, '-b'], { allowFailure: true });
199
+ }
200
+
201
+ async function getSimulatorState(udid: string): Promise<string | null> {
202
+ const result = await runCmd('xcrun', ['simctl', 'list', 'devices', '-j'], {
203
+ allowFailure: true,
204
+ });
205
+ if (result.exitCode !== 0) return null;
206
+ try {
207
+ const payload = JSON.parse(result.stdout as string) as {
208
+ devices: Record<string, { udid: string; state: string }[]>;
209
+ };
210
+ for (const runtime of Object.values(payload.devices ?? {})) {
211
+ const match = runtime.find((d) => d.udid === udid);
212
+ if (match) return match.state;
213
+ }
214
+ } catch {
215
+ return null;
216
+ }
217
+ return null;
218
+ }