@houwert/conductor 0.2.0 → 0.4.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/.claude-plugin/plugin.json +4 -1
- package/README.md +121 -21
- package/dist/commands/assert-not-visible.js +5 -0
- package/dist/commands/assert-visible.js +15 -0
- package/dist/commands/back.js +6 -1
- package/dist/commands/cheat-sheet.js +4 -0
- package/dist/commands/clear-state.js +27 -0
- package/dist/commands/copy-app.js +55 -0
- package/dist/commands/daemon.js +4 -0
- package/dist/commands/device-pool.js +4 -0
- package/dist/commands/erase-text.js +2 -0
- package/dist/commands/focused.js +233 -0
- package/dist/commands/foreground-app.js +2 -0
- package/dist/commands/hide-keyboard.js +2 -0
- package/dist/commands/inspect.js +23 -1
- package/dist/commands/install.js +69 -4
- package/dist/commands/launch-app.js +6 -0
- package/dist/commands/list-apps.js +3 -1
- package/dist/commands/list-devices.js +70 -7
- package/dist/commands/open-link.js +2 -0
- package/dist/commands/press-key.js +73 -7
- package/dist/commands/run-flow-inline.js +3 -0
- package/dist/commands/run-flow.js +4 -0
- package/dist/commands/run-parallel.js +2 -0
- package/dist/commands/screenshot.js +2 -0
- package/dist/commands/scroll-until-visible.js +6 -0
- package/dist/commands/scroll.js +6 -0
- package/dist/commands/session.js +2 -0
- package/dist/commands/set-location.js +2 -0
- package/dist/commands/set-orientation.js +2 -0
- package/dist/commands/start-device.js +311 -9
- package/dist/commands/stop-app.js +2 -0
- package/dist/commands/swipe.js +10 -0
- package/dist/commands/tap.js +20 -0
- package/dist/commands/type.js +2 -0
- package/dist/commands/uninstall-app.js +31 -0
- package/dist/daemon/server.js +64 -22
- package/dist/device-picker.js +65 -0
- package/dist/drivers/android.js +18 -13
- package/dist/drivers/bootstrap.js +179 -6
- package/dist/drivers/element-resolver.js +10 -0
- package/dist/drivers/flow-runner.js +21 -5
- package/dist/drivers/ios.js +7 -1
- package/dist/index.js +134 -97
- package/dist/postinstall.js +2 -2
- package/dist/runner.js +40 -6
- package/drivers/ios/conductor-driver-ios.zip +0 -0
- package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
- package/drivers/tvos/conductor-driver-tvos-config.xctestrun +121 -0
- package/drivers/tvos/conductor-driver-tvos.zip +0 -0
- package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
- package/package.json +1 -1
- package/skills/conductor/SKILL.md +66 -6
- package/skills/conductor/references/flow-syntax.md +3 -0
- package/skills/skills.yaml +8 -0
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
3
4
|
exports.startDevice = startDevice;
|
|
5
|
+
exports.HELP = ` start-device
|
|
6
|
+
--platform <ios|android|tvos> Boot a simulator or emulator
|
|
7
|
+
--os-version <n> iOS/tvOS version (e.g. 18) or Android API level (e.g. 33)
|
|
8
|
+
--avd <name> Android AVD name (default: first available)
|
|
9
|
+
--name <name> Set a custom name on the simulator after boot (iOS/tvOS only)
|
|
10
|
+
--device-type <name> iOS/tvOS device type (e.g. "iPhone 16 Pro", "Apple TV 4K"); creates if needed`;
|
|
4
11
|
const child_process_1 = require("child_process");
|
|
5
12
|
const runner_js_1 = require("../runner.js");
|
|
6
13
|
const output_js_1 = require("../output.js");
|
|
@@ -15,6 +22,68 @@ async function listIOSSimulators() {
|
|
|
15
22
|
const parsed = JSON.parse(result.stdout);
|
|
16
23
|
return parsed.devices;
|
|
17
24
|
}
|
|
25
|
+
async function listDeviceTypes() {
|
|
26
|
+
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'list', 'devicetypes', '--json']);
|
|
27
|
+
if (!result.success)
|
|
28
|
+
throw new Error(`xcrun simctl list devicetypes failed: ${result.stderr}`);
|
|
29
|
+
const parsed = JSON.parse(result.stdout);
|
|
30
|
+
return parsed.devicetypes;
|
|
31
|
+
}
|
|
32
|
+
async function listRuntimes() {
|
|
33
|
+
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'list', 'runtimes', '--json']);
|
|
34
|
+
if (!result.success)
|
|
35
|
+
throw new Error(`xcrun simctl list runtimes failed: ${result.stderr}`);
|
|
36
|
+
const parsed = JSON.parse(result.stdout);
|
|
37
|
+
return parsed.runtimes;
|
|
38
|
+
}
|
|
39
|
+
function runtimeVersionNumber(version) {
|
|
40
|
+
// Convert "18.2" → 180200, "17.0.1" → 170001 for numeric comparison
|
|
41
|
+
const parts = version.split('.').map(Number);
|
|
42
|
+
return (parts[0] ?? 0) * 10000 + (parts[1] ?? 0) * 100 + (parts[2] ?? 0);
|
|
43
|
+
}
|
|
44
|
+
async function createIOSSimulator(deviceType, osVersion) {
|
|
45
|
+
const deviceTypes = await listDeviceTypes();
|
|
46
|
+
const runtimes = await listRuntimes();
|
|
47
|
+
const matchedType = deviceTypes.find((dt) => dt.name.toLowerCase() === deviceType.toLowerCase());
|
|
48
|
+
if (!matchedType) {
|
|
49
|
+
const iPhoneTypes = deviceTypes
|
|
50
|
+
.filter((dt) => dt.name.toLowerCase().includes('iphone'))
|
|
51
|
+
.map((dt) => dt.name);
|
|
52
|
+
throw new Error(`Unknown device type "${deviceType}". Available iPhone types:\n ${iPhoneTypes.join('\n ')}`);
|
|
53
|
+
}
|
|
54
|
+
// Filter to available iOS runtimes
|
|
55
|
+
let candidates = runtimes.filter((r) => r.isAvailable && r.identifier.startsWith('com.apple.CoreSimulator.SimRuntime.iOS'));
|
|
56
|
+
if (osVersion) {
|
|
57
|
+
candidates = candidates.filter((r) => r.version.startsWith(osVersion));
|
|
58
|
+
}
|
|
59
|
+
if (candidates.length === 0) {
|
|
60
|
+
const hint = osVersion ? ` matching version ${osVersion}` : '';
|
|
61
|
+
throw new Error(`No available iOS runtime found${hint}. Install one via Xcode → Settings → Platforms.`);
|
|
62
|
+
}
|
|
63
|
+
// Sort by version descending to pick the latest
|
|
64
|
+
candidates.sort((a, b) => runtimeVersionNumber(b.version) - runtimeVersionNumber(a.version));
|
|
65
|
+
// Filter by device type compatibility if min/max runtime version is specified
|
|
66
|
+
const compatible = candidates.filter((r) => {
|
|
67
|
+
const ver = runtimeVersionNumber(r.version);
|
|
68
|
+
if (matchedType.minRuntimeVersion && ver < matchedType.minRuntimeVersion)
|
|
69
|
+
return false;
|
|
70
|
+
if (matchedType.maxRuntimeVersion && ver > matchedType.maxRuntimeVersion)
|
|
71
|
+
return false;
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
const runtime = compatible.length > 0 ? compatible[0] : candidates[0];
|
|
75
|
+
const createResult = await (0, runner_js_1.spawnCommand)('xcrun', [
|
|
76
|
+
'simctl',
|
|
77
|
+
'create',
|
|
78
|
+
deviceType,
|
|
79
|
+
matchedType.identifier,
|
|
80
|
+
runtime.identifier,
|
|
81
|
+
]);
|
|
82
|
+
if (!createResult.success) {
|
|
83
|
+
throw new Error(`Failed to create simulator: ${createResult.stderr.trim()}`);
|
|
84
|
+
}
|
|
85
|
+
return createResult.stdout.trim(); // UDID
|
|
86
|
+
}
|
|
18
87
|
async function bootIOSSimulator(udid) {
|
|
19
88
|
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'boot', udid]);
|
|
20
89
|
// exit 149 = already booted, that's fine
|
|
@@ -28,7 +97,9 @@ async function waitForIOSBoot(udid) {
|
|
|
28
97
|
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'list', 'devices', 'booted', '--json']);
|
|
29
98
|
if (result.success) {
|
|
30
99
|
const parsed = JSON.parse(result.stdout);
|
|
31
|
-
const booted = Object.values(parsed.devices)
|
|
100
|
+
const booted = Object.values(parsed.devices)
|
|
101
|
+
.flat()
|
|
102
|
+
.some((d) => d.udid === udid);
|
|
32
103
|
if (booted)
|
|
33
104
|
return;
|
|
34
105
|
}
|
|
@@ -36,7 +107,13 @@ async function waitForIOSBoot(udid) {
|
|
|
36
107
|
}
|
|
37
108
|
throw new Error(`Simulator ${udid} did not boot within ${IOS_BOOT_TIMEOUT_MS / 1000}s`);
|
|
38
109
|
}
|
|
39
|
-
async function
|
|
110
|
+
async function renameIOSSimulator(udid, name) {
|
|
111
|
+
const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'rename', udid, name]);
|
|
112
|
+
if (!result.success) {
|
|
113
|
+
throw new Error(`xcrun simctl rename failed: ${result.stderr.trim()}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function startIOS(osVersion, opts, name, deviceType) {
|
|
40
117
|
let devices;
|
|
41
118
|
try {
|
|
42
119
|
devices = await listIOSSimulators();
|
|
@@ -45,25 +122,74 @@ async function startIOS(osVersion, opts) {
|
|
|
45
122
|
(0, output_js_1.printError)(`Failed to list simulators: ${e instanceof Error ? e.message : String(e)}`, opts);
|
|
46
123
|
return 1;
|
|
47
124
|
}
|
|
48
|
-
// Filter to available (installable) simulators, optionally by OS version
|
|
125
|
+
// Filter to available (installable) simulators, optionally by OS version and device type.
|
|
126
|
+
// Note: --device-type matches against sim.name, so renamed simulators won't match their
|
|
127
|
+
// original device type. This is a simctl limitation — device entries don't expose a stable
|
|
128
|
+
// deviceTypeIdentifier. A renamed sim will be skipped, potentially creating a duplicate.
|
|
49
129
|
const candidates = [];
|
|
50
130
|
for (const [runtime, sims] of Object.entries(devices)) {
|
|
51
131
|
if (osVersion && !runtime.includes(osVersion))
|
|
52
132
|
continue;
|
|
53
133
|
for (const sim of sims) {
|
|
134
|
+
if (deviceType && sim.name.toLowerCase() !== deviceType.toLowerCase())
|
|
135
|
+
continue;
|
|
54
136
|
if (sim.isAvailable && sim.state !== 'Booted') {
|
|
55
137
|
candidates.push({ runtime, device: sim });
|
|
56
138
|
}
|
|
57
139
|
// If already booted, just report it
|
|
58
140
|
if (sim.isAvailable && sim.state === 'Booted') {
|
|
59
141
|
if (!osVersion || runtime.includes(osVersion)) {
|
|
60
|
-
|
|
142
|
+
if (name) {
|
|
143
|
+
try {
|
|
144
|
+
await renameIOSSimulator(sim.udid, name);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const displayName = name ?? sim.name;
|
|
152
|
+
(0, output_js_1.printSuccess)(`Simulator already booted: ${displayName} (${sim.udid})`, opts);
|
|
61
153
|
return 0;
|
|
62
154
|
}
|
|
63
155
|
}
|
|
64
156
|
}
|
|
65
157
|
}
|
|
66
158
|
if (candidates.length === 0) {
|
|
159
|
+
// If a device type was requested, try to create the simulator
|
|
160
|
+
if (deviceType) {
|
|
161
|
+
console.log(`No existing simulator found for "${deviceType}". Creating one...`);
|
|
162
|
+
let udid;
|
|
163
|
+
try {
|
|
164
|
+
udid = await createIOSSimulator(deviceType, osVersion);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
console.log(`Created simulator: ${deviceType} (${udid}). Booting...`);
|
|
171
|
+
try {
|
|
172
|
+
await bootIOSSimulator(udid);
|
|
173
|
+
await waitForIOSBoot(udid);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
if (name) {
|
|
180
|
+
try {
|
|
181
|
+
await renameIOSSimulator(udid, name);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
(0, child_process_1.spawn)('open', ['-a', 'Simulator'], { detached: true, stdio: 'ignore' }).unref();
|
|
189
|
+
const displayName = name ?? deviceType;
|
|
190
|
+
(0, output_js_1.printSuccess)(`Booted: ${displayName} (${udid})`, opts);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
67
193
|
const hint = osVersion ? ` for iOS ${osVersion}` : '';
|
|
68
194
|
(0, output_js_1.printError)(`No available iOS simulator found${hint}. Install one via Xcode → Settings → Platforms.`, opts);
|
|
69
195
|
return 1;
|
|
@@ -84,9 +210,174 @@ async function startIOS(osVersion, opts) {
|
|
|
84
210
|
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
85
211
|
return 1;
|
|
86
212
|
}
|
|
213
|
+
if (name) {
|
|
214
|
+
try {
|
|
215
|
+
await renameIOSSimulator(device.udid, name);
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
219
|
+
return 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Open the Simulator.app so the window appears
|
|
223
|
+
(0, child_process_1.spawn)('open', ['-a', 'Simulator'], { detached: true, stdio: 'ignore' }).unref();
|
|
224
|
+
const displayName = name ?? device.name;
|
|
225
|
+
(0, output_js_1.printSuccess)(`Booted: ${displayName} (${device.udid})`, opts);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
// ── tvOS ──────────────────────────────────────────────────────────────────────
|
|
229
|
+
async function createTvOSSimulator(deviceType, osVersion) {
|
|
230
|
+
const deviceTypes = await listDeviceTypes();
|
|
231
|
+
const runtimes = await listRuntimes();
|
|
232
|
+
const matchedType = deviceTypes.find((dt) => dt.name.toLowerCase() === deviceType.toLowerCase());
|
|
233
|
+
if (!matchedType) {
|
|
234
|
+
const tvTypes = deviceTypes
|
|
235
|
+
.filter((dt) => dt.name.toLowerCase().includes('apple tv'))
|
|
236
|
+
.map((dt) => dt.name);
|
|
237
|
+
throw new Error(`Unknown device type "${deviceType}". Available Apple TV types:\n ${tvTypes.join('\n ')}`);
|
|
238
|
+
}
|
|
239
|
+
// Filter to available tvOS runtimes
|
|
240
|
+
let candidates = runtimes.filter((r) => r.isAvailable && r.identifier.startsWith('com.apple.CoreSimulator.SimRuntime.tvOS'));
|
|
241
|
+
if (osVersion) {
|
|
242
|
+
candidates = candidates.filter((r) => r.version.startsWith(osVersion));
|
|
243
|
+
}
|
|
244
|
+
if (candidates.length === 0) {
|
|
245
|
+
const hint = osVersion ? ` matching version ${osVersion}` : '';
|
|
246
|
+
throw new Error(`No available tvOS runtime found${hint}. Install one via Xcode → Settings → Platforms.`);
|
|
247
|
+
}
|
|
248
|
+
// Sort by version descending to pick the latest
|
|
249
|
+
candidates.sort((a, b) => runtimeVersionNumber(b.version) - runtimeVersionNumber(a.version));
|
|
250
|
+
// Filter by device type compatibility if min/max runtime version is specified
|
|
251
|
+
const compatible = candidates.filter((r) => {
|
|
252
|
+
const ver = runtimeVersionNumber(r.version);
|
|
253
|
+
if (matchedType.minRuntimeVersion && ver < matchedType.minRuntimeVersion)
|
|
254
|
+
return false;
|
|
255
|
+
if (matchedType.maxRuntimeVersion && ver > matchedType.maxRuntimeVersion)
|
|
256
|
+
return false;
|
|
257
|
+
return true;
|
|
258
|
+
});
|
|
259
|
+
const runtime = compatible.length > 0 ? compatible[0] : candidates[0];
|
|
260
|
+
const createResult = await (0, runner_js_1.spawnCommand)('xcrun', [
|
|
261
|
+
'simctl',
|
|
262
|
+
'create',
|
|
263
|
+
deviceType,
|
|
264
|
+
matchedType.identifier,
|
|
265
|
+
runtime.identifier,
|
|
266
|
+
]);
|
|
267
|
+
if (!createResult.success) {
|
|
268
|
+
throw new Error(`Failed to create tvOS simulator: ${createResult.stderr.trim()}`);
|
|
269
|
+
}
|
|
270
|
+
return createResult.stdout.trim(); // UDID
|
|
271
|
+
}
|
|
272
|
+
async function startTvOS(osVersion, opts, name, deviceType) {
|
|
273
|
+
let devices;
|
|
274
|
+
try {
|
|
275
|
+
devices = await listIOSSimulators();
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
(0, output_js_1.printError)(`Failed to list simulators: ${e instanceof Error ? e.message : String(e)}`, opts);
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
// Filter to available tvOS simulators, optionally by OS version and device type.
|
|
282
|
+
const candidates = [];
|
|
283
|
+
for (const [runtime, sims] of Object.entries(devices)) {
|
|
284
|
+
if (!runtime.includes('tvOS'))
|
|
285
|
+
continue;
|
|
286
|
+
if (osVersion && !runtime.includes(osVersion))
|
|
287
|
+
continue;
|
|
288
|
+
for (const sim of sims) {
|
|
289
|
+
if (deviceType && sim.name.toLowerCase() !== deviceType.toLowerCase())
|
|
290
|
+
continue;
|
|
291
|
+
if (sim.isAvailable && sim.state !== 'Booted') {
|
|
292
|
+
candidates.push({ runtime, device: sim });
|
|
293
|
+
}
|
|
294
|
+
// If already booted, just report it
|
|
295
|
+
if (sim.isAvailable && sim.state === 'Booted') {
|
|
296
|
+
if (!osVersion || runtime.includes(osVersion)) {
|
|
297
|
+
if (name) {
|
|
298
|
+
try {
|
|
299
|
+
await renameIOSSimulator(sim.udid, name);
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const displayName = name ?? sim.name;
|
|
307
|
+
(0, output_js_1.printSuccess)(`Simulator already booted: ${displayName} (${sim.udid})`, opts);
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (candidates.length === 0) {
|
|
314
|
+
// If a device type was requested, try to create the simulator
|
|
315
|
+
if (deviceType) {
|
|
316
|
+
console.log(`No existing tvOS simulator found for "${deviceType}". Creating one...`);
|
|
317
|
+
let udid;
|
|
318
|
+
try {
|
|
319
|
+
udid = await createTvOSSimulator(deviceType, osVersion);
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
323
|
+
return 1;
|
|
324
|
+
}
|
|
325
|
+
console.log(`Created simulator: ${deviceType} (${udid}). Booting...`);
|
|
326
|
+
try {
|
|
327
|
+
await bootIOSSimulator(udid);
|
|
328
|
+
await waitForIOSBoot(udid);
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
334
|
+
if (name) {
|
|
335
|
+
try {
|
|
336
|
+
await renameIOSSimulator(udid, name);
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
340
|
+
return 1;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
(0, child_process_1.spawn)('open', ['-a', 'Simulator'], { detached: true, stdio: 'ignore' }).unref();
|
|
344
|
+
const displayName = name ?? deviceType;
|
|
345
|
+
(0, output_js_1.printSuccess)(`Booted: ${displayName} (${udid})`, opts);
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
const hint = osVersion ? ` for tvOS ${osVersion}` : '';
|
|
349
|
+
(0, output_js_1.printError)(`No available tvOS simulator found${hint}. Install one via Xcode → Settings → Platforms.`, opts);
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
// Prefer "Apple TV" models
|
|
353
|
+
const sorted = candidates.sort((a, b) => {
|
|
354
|
+
const ai = a.device.name.toLowerCase().includes('apple tv') ? 0 : 1;
|
|
355
|
+
const bi = b.device.name.toLowerCase().includes('apple tv') ? 0 : 1;
|
|
356
|
+
return ai - bi;
|
|
357
|
+
});
|
|
358
|
+
const { device } = sorted[0];
|
|
359
|
+
console.log(`Booting simulator: ${device.name} (${device.udid})...`);
|
|
360
|
+
try {
|
|
361
|
+
await bootIOSSimulator(device.udid);
|
|
362
|
+
await waitForIOSBoot(device.udid);
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
366
|
+
return 1;
|
|
367
|
+
}
|
|
368
|
+
if (name) {
|
|
369
|
+
try {
|
|
370
|
+
await renameIOSSimulator(device.udid, name);
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
(0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
87
377
|
// Open the Simulator.app so the window appears
|
|
88
378
|
(0, child_process_1.spawn)('open', ['-a', 'Simulator'], { detached: true, stdio: 'ignore' }).unref();
|
|
89
|
-
|
|
379
|
+
const displayName = name ?? device.name;
|
|
380
|
+
(0, output_js_1.printSuccess)(`Booted: ${displayName} (${device.udid})`, opts);
|
|
90
381
|
return 0;
|
|
91
382
|
}
|
|
92
383
|
// ── Android ───────────────────────────────────────────────────────────────────
|
|
@@ -94,7 +385,10 @@ async function listAVDs() {
|
|
|
94
385
|
const result = await (0, runner_js_1.spawnCommand)('emulator', ['-list-avds']);
|
|
95
386
|
if (!result.success)
|
|
96
387
|
throw new Error(`emulator -list-avds failed: ${result.stderr}`);
|
|
97
|
-
return result.stdout
|
|
388
|
+
return result.stdout
|
|
389
|
+
.split('\n')
|
|
390
|
+
.map((l) => l.trim())
|
|
391
|
+
.filter(Boolean);
|
|
98
392
|
}
|
|
99
393
|
async function waitForAndroidBoot(avdName) {
|
|
100
394
|
const deadline = Date.now() + ANDROID_BOOT_TIMEOUT_MS;
|
|
@@ -117,7 +411,13 @@ async function waitForAndroidBoot(avdName) {
|
|
|
117
411
|
const status = parts[1];
|
|
118
412
|
if (id && status === 'device' && !connectedBefore.has(id)) {
|
|
119
413
|
// Check boot completed
|
|
120
|
-
const boot = await (0, runner_js_1.spawnCommand)('adb', [
|
|
414
|
+
const boot = await (0, runner_js_1.spawnCommand)('adb', [
|
|
415
|
+
'-s',
|
|
416
|
+
id,
|
|
417
|
+
'shell',
|
|
418
|
+
'getprop',
|
|
419
|
+
'sys.boot_completed',
|
|
420
|
+
]);
|
|
121
421
|
if (boot.stdout.trim() === '1')
|
|
122
422
|
return id;
|
|
123
423
|
}
|
|
@@ -168,11 +468,13 @@ async function startDevice(platform, opts, flags) {
|
|
|
168
468
|
}
|
|
169
469
|
switch (platform.toLowerCase()) {
|
|
170
470
|
case 'ios':
|
|
171
|
-
return startIOS(flags.osVersion, opts);
|
|
471
|
+
return startIOS(flags.osVersion, opts, flags.name, flags.deviceType);
|
|
472
|
+
case 'tvos':
|
|
473
|
+
return startTvOS(flags.osVersion, opts, flags.name, flags.deviceType);
|
|
172
474
|
case 'android':
|
|
173
475
|
return startAndroid(flags.avd, opts);
|
|
174
476
|
default:
|
|
175
|
-
(0, output_js_1.printError)(`Unknown platform "${platform}". Use ios or
|
|
477
|
+
(0, output_js_1.printError)(`Unknown platform "${platform}". Use ios, android, or tvos.`, opts);
|
|
176
478
|
return 1;
|
|
177
479
|
}
|
|
178
480
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
3
4
|
exports.stopApp = stopApp;
|
|
5
|
+
exports.HELP = ` stop-app [<appId>] Stop app`;
|
|
4
6
|
const runner_js_1 = require("../runner.js");
|
|
5
7
|
const session_js_1 = require("../session.js");
|
|
6
8
|
const output_js_1 = require("../output.js");
|
package/dist/commands/swipe.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
3
4
|
exports.swipe = swipe;
|
|
5
|
+
exports.HELP = ` swipe
|
|
6
|
+
--direction <up|down|left|right> Swipe direction (required unless --start/--end are provided)
|
|
7
|
+
--start <x,y> Start coordinate (0–1 normalised or absolute px)
|
|
8
|
+
--end <x,y> End coordinate (0–1 normalised or absolute px)
|
|
9
|
+
--duration <ms> Swipe duration in milliseconds (default: 500)`;
|
|
4
10
|
const runner_js_1 = require("../runner.js");
|
|
5
11
|
const output_js_1 = require("../output.js");
|
|
6
12
|
const ios_js_1 = require("../drivers/ios.js");
|
|
@@ -16,6 +22,10 @@ async function swipe(direction, opts = {}, sessionName = 'default', flags = {})
|
|
|
16
22
|
return 1;
|
|
17
23
|
}
|
|
18
24
|
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
25
|
+
if (driver instanceof ios_js_1.IOSDriver && driver.platform === 'tvos') {
|
|
26
|
+
throw new Error('swipe is not supported on tvOS — Apple TV uses focus-based navigation.\n' +
|
|
27
|
+
'Use press-key to navigate (e.g. conductor press-key left).');
|
|
28
|
+
}
|
|
19
29
|
let startX, startY, endX, endY;
|
|
20
30
|
if (driver instanceof ios_js_1.IOSDriver) {
|
|
21
31
|
const { widthPoints: w, heightPoints: h } = await driver.deviceInfo();
|
package/dist/commands/tap.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
3
4
|
exports.tap = tap;
|
|
5
|
+
exports.HELP = ` tap <element> Tap element by text or id
|
|
6
|
+
--id <id> Match by accessibility id instead of text
|
|
7
|
+
--text <text> Match by text only (not id)
|
|
8
|
+
--index <n> Pick the nth match (0-based)
|
|
9
|
+
--long-press Hold instead of tap
|
|
10
|
+
--double-tap Double-tap the element
|
|
11
|
+
--optional Do not fail if element is not found
|
|
12
|
+
--focused Match only focused elements
|
|
13
|
+
--enabled / --no-enabled Match by enabled state
|
|
14
|
+
--checked / --no-checked Match by checked state
|
|
15
|
+
--selected / --no-selected Match by selected state
|
|
16
|
+
--below <text> Match element below the given reference
|
|
17
|
+
--above <text> Match element above the given reference
|
|
18
|
+
--left-of <text> Match element left of the given reference
|
|
19
|
+
--right-of <text> Match element right of the given reference`;
|
|
4
20
|
const runner_js_1 = require("../runner.js");
|
|
5
21
|
const output_js_1 = require("../output.js");
|
|
6
22
|
const ios_js_1 = require("../drivers/ios.js");
|
|
@@ -26,6 +42,10 @@ async function tap(query, opts = {}, sessionName = 'default', flags = {}) {
|
|
|
26
42
|
};
|
|
27
43
|
const label = flags.text ? `text="${flags.text}"` : flags.id ? `id="${flags.id}"` : `"${query}"`;
|
|
28
44
|
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
45
|
+
if (driver instanceof ios_js_1.IOSDriver && driver.platform === 'tvos') {
|
|
46
|
+
throw new Error('tap is not supported on tvOS — Apple TV uses focus-based navigation.\n' +
|
|
47
|
+
'Use press-key to navigate (e.g. conductor press-key "Remote Dpad Center").');
|
|
48
|
+
}
|
|
29
49
|
let el;
|
|
30
50
|
if (driver instanceof ios_js_1.IOSDriver) {
|
|
31
51
|
el = await (0, wait_js_1.waitForIOSElement)(() => driver.viewHierarchy().then((h) => h.axElement), sel);
|
package/dist/commands/type.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
3
4
|
exports.typeText = typeText;
|
|
5
|
+
exports.HELP = ` type <text> Type text into focused field`;
|
|
4
6
|
const runner_js_1 = require("../runner.js");
|
|
5
7
|
const output_js_1 = require("../output.js");
|
|
6
8
|
async function typeText(text, opts = {}, sessionName = 'default') {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
4
|
+
exports.uninstallApp = uninstallApp;
|
|
5
|
+
exports.HELP = ` uninstall-app <appId> Uninstall app from device`;
|
|
6
|
+
const runner_js_1 = require("../runner.js");
|
|
7
|
+
const output_js_1 = require("../output.js");
|
|
8
|
+
const ios_js_1 = require("../drivers/ios.js");
|
|
9
|
+
const android_js_1 = require("../drivers/android.js");
|
|
10
|
+
async function uninstallApp(appId, opts = {}, sessionName = 'default') {
|
|
11
|
+
if (!appId) {
|
|
12
|
+
(0, output_js_1.printError)('uninstall-app requires <appId>', opts);
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
16
|
+
if (driver instanceof ios_js_1.IOSDriver) {
|
|
17
|
+
await driver.uninstallApp(appId);
|
|
18
|
+
}
|
|
19
|
+
else if (driver instanceof android_js_1.AndroidDriver) {
|
|
20
|
+
await driver.uninstallApp(appId);
|
|
21
|
+
}
|
|
22
|
+
}, sessionName);
|
|
23
|
+
if (result.success) {
|
|
24
|
+
(0, output_js_1.printSuccess)(`uninstall-app "${appId}" — done`, opts);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
(0, output_js_1.printError)(`uninstall-app "${appId}" — failed\n${result.stderr}`, opts);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/daemon/server.js
CHANGED
|
@@ -19,6 +19,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
19
19
|
const path_1 = __importDefault(require("path"));
|
|
20
20
|
const protocol_js_1 = require("./protocol.js");
|
|
21
21
|
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
22
|
+
const android_js_1 = require("../drivers/android.js");
|
|
22
23
|
const sessionName = process.argv[2] ?? 'default';
|
|
23
24
|
const SOCKET_PATH = (0, protocol_js_1.socketPath)(sessionName);
|
|
24
25
|
const PID_FILE = (0, protocol_js_1.pidFile)(sessionName);
|
|
@@ -37,12 +38,24 @@ let driverPort = 1075;
|
|
|
37
38
|
let driverPlatform = 'ios';
|
|
38
39
|
const DRIVER_HEALTH_INTERVAL_MS = 10000; // Check driver health every 10s
|
|
39
40
|
let _restartInProgress = false;
|
|
41
|
+
let _driverStarted = false;
|
|
40
42
|
async function ensureDriverRunning() {
|
|
41
|
-
if (_restartInProgress)
|
|
43
|
+
if (_restartInProgress || !_driverStarted)
|
|
42
44
|
return;
|
|
43
|
-
|
|
45
|
+
let alive;
|
|
46
|
+
if (driverPlatform === 'android') {
|
|
47
|
+
const probe = new android_js_1.AndroidDriver(sessionName, driverPort);
|
|
48
|
+
await probe.connect();
|
|
49
|
+
alive = await probe.isAlive().catch(() => false);
|
|
50
|
+
probe.close();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Both 'ios' and 'tvos' use an HTTP server — port open = alive
|
|
54
|
+
alive = await (0, bootstrap_js_1.isPortOpen)(driverPort);
|
|
55
|
+
}
|
|
44
56
|
if (!alive) {
|
|
45
|
-
if (driverPlatform === 'ios'
|
|
57
|
+
if ((driverPlatform === 'ios' || driverPlatform === 'tvos') &&
|
|
58
|
+
!(await (0, bootstrap_js_1.isSimulatorBooted)(sessionName))) {
|
|
46
59
|
dlog(`Simulator ${sessionName} is not booted — skipping driver restart`);
|
|
47
60
|
return;
|
|
48
61
|
}
|
|
@@ -52,6 +65,10 @@ async function ensureDriverRunning() {
|
|
|
52
65
|
if (driverPlatform === 'ios') {
|
|
53
66
|
await (0, bootstrap_js_1.startIOSDriver)(sessionName, driverPort);
|
|
54
67
|
}
|
|
68
|
+
else if (driverPlatform === 'tvos') {
|
|
69
|
+
// Health-check restart — don't dismiss, to avoid disrupting user's app
|
|
70
|
+
await (0, bootstrap_js_1.startTvOSDriver)(sessionName, driverPort, /* dismissAfterLaunch */ false);
|
|
71
|
+
}
|
|
55
72
|
else {
|
|
56
73
|
await (0, bootstrap_js_1.startAndroidDriver)(sessionName, driverPort);
|
|
57
74
|
}
|
|
@@ -116,25 +133,33 @@ async function main() {
|
|
|
116
133
|
catch {
|
|
117
134
|
/* ok */
|
|
118
135
|
}
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
if (_driverStarted) {
|
|
137
|
+
// tvOS: keep the driver process alive across daemon restarts.
|
|
138
|
+
// Stopping/reinstalling steals foreground focus and destroys
|
|
139
|
+
// the user's navigation state in the target app.
|
|
140
|
+
if (driverPlatform === 'tvos') {
|
|
141
|
+
dlog('tvOS: leaving driver running to preserve app state');
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
dlog(`Stopping driver on port ${driverPort}`);
|
|
145
|
+
try {
|
|
146
|
+
if (driverPlatform === 'ios') {
|
|
147
|
+
await (0, bootstrap_js_1.stopIOSDriver)(sessionName);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
await (0, bootstrap_js_1.stopAndroidDriver)(sessionName, driverPort);
|
|
151
|
+
}
|
|
124
152
|
}
|
|
125
|
-
|
|
126
|
-
|
|
153
|
+
catch (err) {
|
|
154
|
+
dlog(`Stop driver error: ${err instanceof Error ? err.message : String(err)}`);
|
|
155
|
+
}
|
|
156
|
+
dlog(`Uninstalling driver from ${sessionName}`);
|
|
157
|
+
try {
|
|
158
|
+
await (0, bootstrap_js_1.uninstallDriver)(sessionName, driverPlatform);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
dlog(`Uninstall driver error: ${err instanceof Error ? err.message : String(err)}`);
|
|
127
162
|
}
|
|
128
|
-
}
|
|
129
|
-
catch (err) {
|
|
130
|
-
dlog(`Stop driver error: ${err instanceof Error ? err.message : String(err)}`);
|
|
131
|
-
}
|
|
132
|
-
dlog(`Uninstalling driver from ${sessionName}`);
|
|
133
|
-
try {
|
|
134
|
-
await (0, bootstrap_js_1.uninstallDriver)(sessionName, driverPlatform);
|
|
135
|
-
}
|
|
136
|
-
catch (err) {
|
|
137
|
-
dlog(`Uninstall driver error: ${err instanceof Error ? err.message : String(err)}`);
|
|
138
163
|
}
|
|
139
164
|
}
|
|
140
165
|
}
|
|
@@ -171,12 +196,24 @@ async function main() {
|
|
|
171
196
|
driverPlatform = platform;
|
|
172
197
|
driverPort = await (0, bootstrap_js_1.getDriverPort)(platform, sessionName);
|
|
173
198
|
dlog(`Platform: ${platform}, port: ${driverPort}`);
|
|
174
|
-
|
|
199
|
+
let driverAlive;
|
|
200
|
+
if (platform === 'android') {
|
|
201
|
+
const probe = new android_js_1.AndroidDriver(sessionName, driverPort);
|
|
202
|
+
await probe.connect();
|
|
203
|
+
driverAlive = await probe.isAlive().catch(() => false);
|
|
204
|
+
probe.close();
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// Both 'ios' and 'tvos' use an HTTP server — port open = alive
|
|
208
|
+
driverAlive = await (0, bootstrap_js_1.isPortOpen)(driverPort);
|
|
209
|
+
}
|
|
210
|
+
if (driverAlive) {
|
|
211
|
+
_driverStarted = true;
|
|
175
212
|
dlog(`Driver already running on port ${driverPort}`);
|
|
176
213
|
return;
|
|
177
214
|
}
|
|
178
215
|
// Android: install APKs before starting the driver.
|
|
179
|
-
// iOS:
|
|
216
|
+
// iOS/tvOS: xcodebuild installs silently via DependentProductPaths.
|
|
180
217
|
if (platform === 'android') {
|
|
181
218
|
dlog(`Installing Android driver on ${sessionName}`);
|
|
182
219
|
await (0, bootstrap_js_1.installDriver)(sessionName);
|
|
@@ -187,9 +224,14 @@ async function main() {
|
|
|
187
224
|
if (platform === 'ios') {
|
|
188
225
|
await (0, bootstrap_js_1.startIOSDriver)(sessionName, driverPort);
|
|
189
226
|
}
|
|
227
|
+
else if (platform === 'tvos') {
|
|
228
|
+
// First install — dismiss the runner app to return to homescreen
|
|
229
|
+
await (0, bootstrap_js_1.startTvOSDriver)(sessionName, driverPort, /* dismissAfterLaunch */ true);
|
|
230
|
+
}
|
|
190
231
|
else {
|
|
191
232
|
await (0, bootstrap_js_1.startAndroidDriver)(sessionName, driverPort);
|
|
192
233
|
}
|
|
234
|
+
_driverStarted = true;
|
|
193
235
|
dlog(`Driver started successfully`);
|
|
194
236
|
}
|
|
195
237
|
catch (err) {
|