@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.
Files changed (55) hide show
  1. package/.claude-plugin/plugin.json +4 -1
  2. package/README.md +121 -21
  3. package/dist/commands/assert-not-visible.js +5 -0
  4. package/dist/commands/assert-visible.js +15 -0
  5. package/dist/commands/back.js +6 -1
  6. package/dist/commands/cheat-sheet.js +4 -0
  7. package/dist/commands/clear-state.js +27 -0
  8. package/dist/commands/copy-app.js +55 -0
  9. package/dist/commands/daemon.js +4 -0
  10. package/dist/commands/device-pool.js +4 -0
  11. package/dist/commands/erase-text.js +2 -0
  12. package/dist/commands/focused.js +233 -0
  13. package/dist/commands/foreground-app.js +2 -0
  14. package/dist/commands/hide-keyboard.js +2 -0
  15. package/dist/commands/inspect.js +23 -1
  16. package/dist/commands/install.js +69 -4
  17. package/dist/commands/launch-app.js +6 -0
  18. package/dist/commands/list-apps.js +3 -1
  19. package/dist/commands/list-devices.js +70 -7
  20. package/dist/commands/open-link.js +2 -0
  21. package/dist/commands/press-key.js +73 -7
  22. package/dist/commands/run-flow-inline.js +3 -0
  23. package/dist/commands/run-flow.js +4 -0
  24. package/dist/commands/run-parallel.js +2 -0
  25. package/dist/commands/screenshot.js +2 -0
  26. package/dist/commands/scroll-until-visible.js +6 -0
  27. package/dist/commands/scroll.js +6 -0
  28. package/dist/commands/session.js +2 -0
  29. package/dist/commands/set-location.js +2 -0
  30. package/dist/commands/set-orientation.js +2 -0
  31. package/dist/commands/start-device.js +311 -9
  32. package/dist/commands/stop-app.js +2 -0
  33. package/dist/commands/swipe.js +10 -0
  34. package/dist/commands/tap.js +20 -0
  35. package/dist/commands/type.js +2 -0
  36. package/dist/commands/uninstall-app.js +31 -0
  37. package/dist/daemon/server.js +64 -22
  38. package/dist/device-picker.js +65 -0
  39. package/dist/drivers/android.js +18 -13
  40. package/dist/drivers/bootstrap.js +179 -6
  41. package/dist/drivers/element-resolver.js +10 -0
  42. package/dist/drivers/flow-runner.js +21 -5
  43. package/dist/drivers/ios.js +7 -1
  44. package/dist/index.js +134 -97
  45. package/dist/postinstall.js +2 -2
  46. package/dist/runner.js +40 -6
  47. package/drivers/ios/conductor-driver-ios.zip +0 -0
  48. package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
  49. package/drivers/tvos/conductor-driver-tvos-config.xctestrun +121 -0
  50. package/drivers/tvos/conductor-driver-tvos.zip +0 -0
  51. package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
  52. package/package.json +1 -1
  53. package/skills/conductor/SKILL.md +66 -6
  54. package/skills/conductor/references/flow-syntax.md +3 -0
  55. 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).flat().some((d) => d.udid === udid);
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 startIOS(osVersion, opts) {
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
- (0, output_js_1.printSuccess)(`Simulator already booted: ${sim.name} (${sim.udid})`, opts);
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
- (0, output_js_1.printSuccess)(`Booted: ${device.name} (${device.udid})`, opts);
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.split('\n').map((l) => l.trim()).filter(Boolean);
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', ['-s', id, 'shell', 'getprop', 'sys.boot_completed']);
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 android.`, opts);
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");
@@ -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();
@@ -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);
@@ -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
+ }
@@ -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
- const alive = await (0, bootstrap_js_1.isPortOpen)(driverPort);
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' && !(await (0, bootstrap_js_1.isSimulatorBooted)(sessionName))) {
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 (sessionName !== 'default') {
120
- dlog(`Stopping driver on port ${driverPort}`);
121
- try {
122
- if (driverPlatform === 'ios') {
123
- await (0, bootstrap_js_1.stopIOSDriver)(sessionName);
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
- else {
126
- await (0, bootstrap_js_1.stopAndroidDriver)(sessionName);
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
- if (await (0, bootstrap_js_1.isPortOpen)(driverPort)) {
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: startIOSDriver uses xcodebuild which installs silently via DependentProductPaths.
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) {