Skip to content

Real Apple TV (tvOS) automation: dedicated TvosRobot + focus-by-identity tool - #386

Open
setoelkahfi wants to merge 3 commits into
mobile-next:mainfrom
setoelkahfi:feature/tvos-real-device
Open

Real Apple TV (tvOS) automation: dedicated TvosRobot + focus-by-identity tool#386
setoelkahfi wants to merge 3 commits into
mobile-next:mainfrom
setoelkahfi:feature/tvos-real-device

Conversation

@setoelkahfi

@setoelkahfi setoelkahfi commented Jul 15, 2026

Copy link
Copy Markdown

Summary

Adds a dedicated TvosRobot to mobile-mcp so real Apple TV devices are driven through mobilecli/DeviceKit with tvOS-appropriate behavior, while leaving the existing IosRobot/WebDriverAgent path unchanged for iPhone/iPad.

This is part of a three-repo upstream change across mobile-next/mobilecli, mobile-next/devicekit-ios, and mobile-next/mobile-mcp.

What's in this PR

  • New TvosRobot for real Apple TV routing.
  • Siri Remote button support: UP, DOWN, LEFT, RIGHT, SELECT, MENU, PLAY_PAUSE.
  • Explicit actionable unsupported errors for tvOS-inapplicable touch/orientation/text operations.
  • New mobile_focus_by_identifier MCP tool delegating to mobilecli io focus.
  • iPhone/iPad continue to use IosRobot/WDA; Android and simulator routing unchanged.
  • Unit tests for platform routing, button mapping, unsupported errors, and focus.

Validation

  • npm run build
  • npm run lint has 0 source errors
  • New test/tvos.ts unit tests pass

Implements: SPECS/quality/real-tvos-device-support/SPEC.md

Related PRs

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Walkthrough

The change adds CoreDevice discovery and fallback operations for iOS and tvOS devices. Device metadata now includes platform information, and Apple TV devices route to TvosRobot. The new robot supports app management, URL opening, screenshots, UI hierarchy access, Siri Remote buttons, and accessibility focus. Unsupported tvOS interactions return errors. The server adds tvOS device listing and focus tooling. Tests cover CoreDevice discovery, platform detection, routing, button handling, unsupported operations, and focus behavior.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: dedicated tvOS automation for real Apple TV devices with a TvosRobot and focus tool.
Description check ✅ Passed The description directly explains the tvOS support, routing, Siri Remote controls, focus tool, unsupported operations, and validation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

setoelkahfi and others added 3 commits July 15, 2026 15:02
tvOS simulators now surface through the MCP server.

- Accept `tvos` in the platform unions and validation in the mobilecli
  wrapper.
- Drop the hard `ios` filter on simulator discovery so tvOS simulators
  are returned, and report each device's real platform to analytics.
- List the tvOS Siri Remote buttons (UP, DOWN, LEFT, RIGHT, SELECT,
  MENU, PLAY_PAUSE) in the press-button tool description.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Detect physical Apple TV units by product type and report platform tvos in
mobile_list_available_devices instead of hardcoding ios. Route real tvOS
devices through mobilecli (which installs and drives the tvOS runner) while
iPhones and iPads keep using the WebDriverAgent-based IosRobot. Add a unit
test for platform detection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a dedicated TvosRobot (mobilecli/DeviceKit-backed) for real Apple TV,
keeping IosRobot/WebDriverAgent unchanged for iPhone/iPad (D2). Route
real Apple TVs (platform tvos) to TvosRobot via getRobotFromDevice; iOS
real devices continue to IosRobot.

- TvosRobot: Siri Remote buttons (UP/DOWN/LEFT/RIGHT/SELECT/MENU/
  PLAY_PAUSE); app/screenshot/view-tree/screen-size delegated to
  mobilecli; explicit unsupported-on-tvOS ActionableErrors for
  tap/doubleTap/longPress/swipe/orientation/text; best-effort openUrl
- New mobile_focus_by_identifier MCP tool + Mobilecli.focusByIdentifier
  shelling to mobilecli io focus (accessibility-identity focus)
- IosManager.listDevices retains platform so routing can distinguish
  Apple TV; extracted and tested robotForIosDevice helper
- Device-free unit tests for routing, button mapping, unsupported ops,
  and focus

Implements: SPECS/quality/real-tvos-device-support/SPEC.md (Milestone 5, 6)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@setoelkahfi
setoelkahfi force-pushed the feature/tvos-real-device branch from c29090a to 7e7c0f2 Compare July 15, 2026 13:03
@setoelkahfi
setoelkahfi marked this pull request as ready for review July 31, 2026 11:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
src/server.ts (1)

185-194: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The tvOS branch repeats the decision that robotForIosDevice already makes.

Lines 188-191 construct TvosRobot directly, and line 194 calls robotForIosDevice, which performs the same platform === "tvos" test. Keep one selection path and derive the telemetry value from the platform.

♻️ Proposed simplification
 		if (iosDevice) {
-			// Real Apple TVs are enumerated over the same connection as iPhones but
-			// require a dedicated Siri Remote / DeviceKit-backed robot (D2). Route
-			// them before the platform-blind iPhone branch.
-			if (iosDevice.platform === "tvos") {
-				posthog("get_robot", { "DevicePlatform": "tvos", "DeviceType": "real" }).then();
-				return new TvosRobot(deviceId);
-			}
-
-			posthog("get_robot", { "DevicePlatform": "ios", "DeviceType": "real" }).then();
+			// Real Apple TVs are enumerated over the same connection as iPhones but
+			// require a dedicated Siri Remote / DeviceKit-backed robot (D2).
+			posthog("get_robot", { "DevicePlatform": iosDevice.platform, "DeviceType": "real" }).then();
 			return robotForIosDevice(deviceId, iosDevice.platform);
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.ts` around lines 185 - 194, Consolidate robot selection in the
iOS-device branch by removing the separate tvOS condition and routing all
platforms through robotForIosDevice. Derive the PostHog DevicePlatform value
from iosDevice.platform while preserving DeviceType "real", then return the
shared robot-selection result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ios.ts`:
- Around line 449-459: Update IosManager.listDevicesWithDetails, which is
reached by listDevices and getRobotFromDevice on every tool call, to avoid
repeating the full iOS and devicectl discovery sequence; add a short-lived cache
for the merged device list and reuse it until its TTL expires, refreshing only
after expiration.
- Around line 194-206: Add a shared helper in src/ios.ts that converts
CoreDeviceDetailsOutput into InfoCommandOutput, deriving DeviceClass from the
hardware product type and accepting a fallback device name. Replace the inline
mapping in IosRobot.getCoreDeviceInfo at src/ios.ts:194-206 and the fallback
mapping in IosManager.getDeviceInfo at src/ios.ts:427-447 with this helper,
passing deviceId as the fallback name at both sites.
- Around line 384-400: Update listGoIosDevicesWithDetails to isolate failures
from execFileSync and JSON.parse: catch errors from the go-ios list command or
response parsing, return an empty device list, and preserve the existing mapping
for successful responses. Ensure listDevicesWithDetails can continue to
listCoreDeviceDevicesWithDetails when go-ios is installed but unavailable or
returns invalid output.
- Around line 265-281: Update the catch fallback in launchApp so a requested
locale is never silently ignored: when locale is provided and the go-ios launch
fails, throw an ActionableError stating that locale-based launching is
unavailable; retain the existing devicectl fallback only when no locale was
requested.

In `@src/tvos.ts`:
- Around line 116-135: Update TvosRobot.launchApp, terminateApp, installApp, and
uninstallApp to validate their package or bundle identifier arguments with the
shared validators from src/utils.ts before constructing the mobilecli command;
also validate launchApp’s optional locale with validateLocale. Reuse the same
validation flow as IosRobot so values beginning with “-” are rejected
consistently.
- Around line 86-93: Validate the response status before dereferencing payload
data in getScreenSize, getInstalledApps, and dumpUI, and surface an actionable
error for non-success responses instead of allowing a TypeError. In
getScreenSize, remove the zero-size fallback and report the failure when
screenSize is absent; preserve the normal successful response behavior.

---

Nitpick comments:
In `@src/server.ts`:
- Around line 185-194: Consolidate robot selection in the iOS-device branch by
removing the separate tvOS condition and routing all platforms through
robotForIosDevice. Derive the PostHog DevicePlatform value from
iosDevice.platform while preserving DeviceType "real", then return the shared
robot-selection result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d5ca8a2-ab71-43cb-94a0-90d0f7199024

📥 Commits

Reviewing files that changed from the base of the PR and between f084553 and 7e7c0f2.

📒 Files selected for processing (7)
  • src/ios.ts
  • src/mobilecli.ts
  • src/server.ts
  • src/tvos.ts
  • test/ios-coredevice.ts
  • test/ios-platform.ts
  • test/tvos.ts

Comment thread src/ios.ts
Comment on lines +194 to +206
private getCoreDeviceInfo(): InfoCommandOutput {
const output = this.devicectl("device", "info", "details", "--device", this.deviceId);
const json = JSON.parse(output) as CoreDeviceDetailsOutput;
return {
DeviceClass: "iPhone",
DeviceName: json.result?.deviceProperties?.name ?? this.deviceId,
ProductName: json.result?.hardwareProperties?.productType ?? "",
ProductType: json.result?.hardwareProperties?.productType ?? "",
ProductVersion: json.result?.deviceProperties?.osVersionNumber ?? "",
PhoneNumber: "",
TimeZone: "",
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicated CoreDevice-to-InfoCommandOutput mapping in src/ios.ts. Both call sites parse the same CoreDeviceDetailsOutput shape and build the same object, and both hardcode DeviceClass: "iPhone", so an Apple TV is reported as an iPhone. The shared root cause is one missing helper.

  • src/ios.ts#L194-L206: replace the inline mapping in IosRobot.getCoreDeviceInfo with a shared helper that derives DeviceClass from the product type.
  • src/ios.ts#L427-L447: replace the inline mapping in the IosManager.getDeviceInfo fallback with the same shared helper, passing deviceId as the fallback name.
📍 Affects 1 file
  • src/ios.ts#L194-L206 (this comment)
  • src/ios.ts#L427-L447
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ios.ts` around lines 194 - 206, Add a shared helper in src/ios.ts that
converts CoreDeviceDetailsOutput into InfoCommandOutput, deriving DeviceClass
from the hardware product type and accepting a fallback device name. Replace the
inline mapping in IosRobot.getCoreDeviceInfo at src/ios.ts:194-206 and the
fallback mapping in IosManager.getDeviceInfo at src/ios.ts:427-447 with this
helper, passing deviceId as the fallback name at both sites.

Comment thread src/ios.ts
Comment on lines 265 to 281
public async launchApp(packageName: string, locale?: string): Promise<void> {
validatePackageName(packageName);
await this.assertTunnelRunning();
const args = ["launch", packageName];
if (locale) {
validateLocale(locale);
const locales = locale.split(",").map(l => l.trim());
args.push("-AppleLanguages", `(${locales.join(", ")})`);
args.push("-AppleLocale", locales[0]);
}
try {
await this.assertTunnelRunning();
const args = ["launch", packageName];
if (locale) {
validateLocale(locale);
const locales = locale.split(",").map(l => l.trim());
args.push("-AppleLanguages", `(${locales.join(", ")})`);
args.push("-AppleLocale", locales[0]);
}

await this.ios(...args);
await this.ios(...args);
} catch {
this.devicectl("device", "process", "launch", "--device", this.deviceId, packageName);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The CoreDevice fallback drops the requested locale.

launchApp validates and applies locale on the go-ios path only. If go-ios fails, the devicectl fallback launches the app without any locale arguments. The caller receives a success result while the locale request is ignored. Pass the locale to devicectl if it supports it, or fail with an ActionableError that states locale launch is unavailable.

🔧 Proposed fix: report the unsupported locale instead of ignoring it
 		} catch {
+			if (locale) {
+				throw new ActionableError("Launching with a locale requires go-ios; the CoreDevice fallback cannot set AppleLanguages/AppleLocale.");
+			}
+
 			this.devicectl("device", "process", "launch", "--device", this.deviceId, packageName);
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async launchApp(packageName: string, locale?: string): Promise<void> {
validatePackageName(packageName);
await this.assertTunnelRunning();
const args = ["launch", packageName];
if (locale) {
validateLocale(locale);
const locales = locale.split(",").map(l => l.trim());
args.push("-AppleLanguages", `(${locales.join(", ")})`);
args.push("-AppleLocale", locales[0]);
}
try {
await this.assertTunnelRunning();
const args = ["launch", packageName];
if (locale) {
validateLocale(locale);
const locales = locale.split(",").map(l => l.trim());
args.push("-AppleLanguages", `(${locales.join(", ")})`);
args.push("-AppleLocale", locales[0]);
}
await this.ios(...args);
await this.ios(...args);
} catch {
this.devicectl("device", "process", "launch", "--device", this.deviceId, packageName);
}
}
public async launchApp(packageName: string, locale?: string): Promise<void> {
validatePackageName(packageName);
try {
await this.assertTunnelRunning();
const args = ["launch", packageName];
if (locale) {
validateLocale(locale);
const locales = locale.split(",").map(l => l.trim());
args.push("-AppleLanguages", `(${locales.join(", ")})`);
args.push("-AppleLocale", locales[0]);
}
await this.ios(...args);
} catch {
if (locale) {
throw new ActionableError("Launching with a locale requires go-ios; the CoreDevice fallback cannot set AppleLanguages/AppleLocale.");
}
this.devicectl("device", "process", "launch", "--device", this.deviceId, packageName);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ios.ts` around lines 265 - 281, Update the catch fallback in launchApp so
a requested locale is never silently ignored: when locale is provided and the
go-ios launch fails, throw an ActionableError stating that locale-based
launching is unavailable; retain the existing devicectl fallback only when no
locale was requested.

Comment thread src/ios.ts
Comment on lines +384 to +400
private listGoIosDevicesWithDetails(): IosDeviceWithDetails[] {
if (!this.isGoIosInstalled()) {
console.error("go-ios is not installed, no physical iOS devices can be detected");
return [];
}

const output = execFileSync(getGoIosPath(), ["list"]).toString();
const json: ListCommandOutput = JSON.parse(output);
const devices = json.deviceList.map(device => {
return json.deviceList.map(device => {
const info = this.getDeviceInfo(device);
return {
deviceId: device,
deviceName: info.DeviceName,
version: info.ProductVersion,
platform: platformFromProductType(info.ProductType),
};
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A go-ios failure discards all CoreDevice devices.

listGoIosDevicesWithDetails runs execFileSync(getGoIosPath(), ["list"]) and JSON.parse without error handling. isGoIosInstalled() only proves the binary answers version. If list fails or returns non-JSON, the exception propagates out of listDevicesWithDetails, so listCoreDeviceDevicesWithDetails results are never used. In getRobotFromDevice (src/server.ts, line 182) that call is not guarded, so the tool fails with a raw error instead of routing the device found by CoreDevice.

🛡️ Proposed fix: isolate the go-ios path
 	private listGoIosDevicesWithDetails(): IosDeviceWithDetails[] {
 		if (!this.isGoIosInstalled()) {
 			return [];
 		}
 
-		const output = execFileSync(getGoIosPath(), ["list"]).toString();
-		const json: ListCommandOutput = JSON.parse(output);
-		return json.deviceList.map(device => {
-			const info = this.getDeviceInfo(device);
-			return {
-				deviceId: device,
-				deviceName: info.DeviceName,
-				version: info.ProductVersion,
-				platform: platformFromProductType(info.ProductType),
-			};
-		});
+		try {
+			const output = execFileSync(getGoIosPath(), ["list"]).toString();
+			const json: ListCommandOutput = JSON.parse(output);
+			return (json.deviceList ?? []).map(device => {
+				const info = this.getDeviceInfo(device);
+				return {
+					deviceId: device,
+					deviceName: info.DeviceName,
+					version: info.ProductVersion,
+					platform: platformFromProductType(info.ProductType),
+				};
+			});
+		} catch (error) {
+			return [];
+		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private listGoIosDevicesWithDetails(): IosDeviceWithDetails[] {
if (!this.isGoIosInstalled()) {
console.error("go-ios is not installed, no physical iOS devices can be detected");
return [];
}
const output = execFileSync(getGoIosPath(), ["list"]).toString();
const json: ListCommandOutput = JSON.parse(output);
const devices = json.deviceList.map(device => {
return json.deviceList.map(device => {
const info = this.getDeviceInfo(device);
return {
deviceId: device,
deviceName: info.DeviceName,
version: info.ProductVersion,
platform: platformFromProductType(info.ProductType),
};
});
}
private listGoIosDevicesWithDetails(): IosDeviceWithDetails[] {
if (!this.isGoIosInstalled()) {
return [];
}
try {
const output = execFileSync(getGoIosPath(), ["list"]).toString();
const json: ListCommandOutput = JSON.parse(output);
return (json.deviceList ?? []).map(device => {
const info = this.getDeviceInfo(device);
return {
deviceId: device,
deviceName: info.DeviceName,
version: info.ProductVersion,
platform: platformFromProductType(info.ProductType),
};
});
} catch (error) {
return [];
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ios.ts` around lines 384 - 400, Update listGoIosDevicesWithDetails to
isolate failures from execFileSync and JSON.parse: catch errors from the go-ios
list command or response parsing, return an empty device list, and preserve the
existing mapping for successful responses. Ensure listDevicesWithDetails can
continue to listCoreDeviceDevicesWithDetails when go-ios is installed but
unavailable or returns invalid output.

Comment thread src/ios.ts
Comment on lines +449 to 459
public listDevices(): IosDeviceWithDetails[] {
return this.listDevicesWithDetails();
}

return devices;
public listDevicesWithDetails(): IosDeviceWithDetails[] {
const devices = [
...this.listGoIosDevicesWithDetails(),
...this.listCoreDeviceDevicesWithDetails(),
];
return this.mergeUniqueDevices(devices);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Device discovery now runs on every tool call and adds a devicectl round trip.

getRobotFromDevice in src/server.ts calls IosManager.listDevices() for every MCP tool invocation. listDevicesWithDetails now spawns ios version, ios list, one ios info (with a possible devicectl fallback) per device, and xcrun devicectl list devices. xcrun devicectl list devices frequently takes several seconds because it waits for network device discovery. Every tool call pays that cost.

Cache the device list for a short time-to-live, or resolve the platform only for the requested deviceId.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ios.ts` around lines 449 - 459, Update IosManager.listDevicesWithDetails,
which is reached by listDevices and getRobotFromDevice on every tool call, to
avoid repeating the full iOS and devicectl discovery sequence; add a short-lived
cache for the merged device list and reuse it until its TTL expires, refreshing
only after expiration.

Comment thread src/tvos.ts
Comment on lines +86 to +93
public async getScreenSize(): Promise<ScreenSize> {
const response = JSON.parse(this.runCommand(["device", "info"])) as DeviceInfoResponse;
if (response.data.device.screenSize) {
return response.data.device.screenSize;
}

return { width: 0, height: 0, scale: 1.0 };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Check status before you dereference data, and do not report a fake screen size.

DeviceInfoResponse, InstalledAppsResponse, and DumpUIResponse declare status: "ok" only. If mobilecli returns an error payload, response.data.device (line 88), response.data.map (line 110), and response.data.elements.map (line 173) throw a TypeError. The MCP client then receives Cannot read properties of undefined instead of an actionable message.

getScreenSize also returns { width: 0, height: 0, scale: 1.0 } when screenSize is absent. The tool then reports Screen size is 0x0 pixels and hides the real failure.

🛡️ Proposed fix: validate the response envelope
+interface MobilecliEnvelope {
+	status: "ok" | "error";
+	error?: string;
+}
+
+const parseResponse = <T>(output: string, context: string): T => {
+	const json = JSON.parse(output) as MobilecliEnvelope & { data?: unknown };
+	if (json.status !== "ok" || json.data === undefined) {
+		throw new ActionableError(json.error ?? `${context} failed on tvOS`);
+	}
+
+	return json as unknown as T;
+};
 	public async getScreenSize(): Promise<ScreenSize> {
-		const response = JSON.parse(this.runCommand(["device", "info"])) as DeviceInfoResponse;
-		if (response.data.device.screenSize) {
-			return response.data.device.screenSize;
-		}
-
-		return { width: 0, height: 0, scale: 1.0 };
+		const response = parseResponse<DeviceInfoResponse>(this.runCommand(["device", "info"]), "device info");
+		const screenSize = response.data.device.screenSize;
+		if (!screenSize) {
+			throw new ActionableError("The tvOS device did not report a screen size.");
+		}
+
+		return screenSize;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tvos.ts` around lines 86 - 93, Validate the response status before
dereferencing payload data in getScreenSize, getInstalledApps, and dumpUI, and
surface an actionable error for non-success responses instead of allowing a
TypeError. In getScreenSize, remove the zero-size fallback and report the
failure when screenSize is absent; preserve the normal successful response
behavior.

Comment thread src/tvos.ts
Comment on lines +116 to +135
public async launchApp(packageName: string, locale?: string): Promise<void> {
const args = ["apps", "launch", packageName];
if (locale) {
args.push("--locale", locale);
}

this.runCommand(args);
}

public async terminateApp(packageName: string): Promise<void> {
this.runCommand(["apps", "terminate", packageName]);
}

public async installApp(path: string): Promise<void> {
this.runCommand(["apps", "install", path]);
}

public async uninstallApp(bundleId: string): Promise<void> {
this.runCommand(["apps", "uninstall", bundleId]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate packageName and locale as IosRobot does.

IosRobot.launchApp calls validatePackageName and validateLocale (src/utils.ts). TvosRobot passes the raw values into the mobilecli argument list. A value that starts with - is then interpreted by mobilecli as an option instead of a bundle identifier. terminateApp, installApp, and uninstallApp have the same gap. Reuse the shared validators to keep both robots consistent.

🛡️ Proposed fix
-import { ActionableError, Button, InstalledApp, Orientation, Robot, ScreenElement, ScreenSize, SwipeDirection } from "./robot";
+import { ActionableError, Button, InstalledApp, Orientation, Robot, ScreenElement, ScreenSize, SwipeDirection } from "./robot";
+import { validateLocale, validatePackageName } from "./utils";
 	public async launchApp(packageName: string, locale?: string): Promise<void> {
+		validatePackageName(packageName);
 		const args = ["apps", "launch", packageName];
 		if (locale) {
+			validateLocale(locale);
 			args.push("--locale", locale);
 		}
 
 		this.runCommand(args);
 	}
 
 	public async terminateApp(packageName: string): Promise<void> {
+		validatePackageName(packageName);
 		this.runCommand(["apps", "terminate", packageName]);
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tvos.ts` around lines 116 - 135, Update TvosRobot.launchApp,
terminateApp, installApp, and uninstallApp to validate their package or bundle
identifier arguments with the shared validators from src/utils.ts before
constructing the mobilecli command; also validate launchApp’s optional locale
with validateLocale. Reuse the same validation flow as IosRobot so values
beginning with “-” are rejected consistently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant