From 30b73cf481f720fad4f4a48c7c50d2fd17d71e3c Mon Sep 17 00:00:00 2001 From: Agent Krystof Woldrich <296461163+krystofwoldrich-agent@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:25:05 +0200 Subject: [PATCH] [eas-cli] Add simulator profiles command Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Co-authored-by: Codex --- CHANGELOG.md | 2 + .../simulator/__tests__/profiles.test.ts | 125 ++++++++++++++++++ .../src/commands/simulator/profiles.ts | 93 +++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 packages/eas-cli/src/commands/simulator/__tests__/profiles.test.ts create mode 100644 packages/eas-cli/src/commands/simulator/profiles.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 423d34f2ac..fc54c4d49e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐ŸŽ‰ New features +- [eas-cli] Add `eas simulator:profiles` to list available EAS Simulator device and system image profiles. ([#4323](https://github.com/expo/eas-cli/pull/4323) by [@krystofwoldrich-agent](https://github.com/krystofwoldrich-agent)) + ### ๐Ÿ› Bug fixes ### ๐Ÿงน Chores diff --git a/packages/eas-cli/src/commands/simulator/__tests__/profiles.test.ts b/packages/eas-cli/src/commands/simulator/__tests__/profiles.test.ts new file mode 100644 index 0000000000..92f3722f8a --- /dev/null +++ b/packages/eas-cli/src/commands/simulator/__tests__/profiles.test.ts @@ -0,0 +1,125 @@ +import { Config } from '@oclif/core'; +import chalk from 'chalk'; + +import Log from '../../../log'; +import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json'; +import SimulatorProfiles from '../profiles'; + +jest.mock('../../../log'); +jest.mock('../../../utils/json'); + +const DEVICES = [ + 'pixel_6', + 'pixel_6_pro', + 'pixel_6a', + 'pixel_7', + 'pixel_7_pro', + 'pixel_7a', + 'pixel_8', + 'pixel_8_pro', + 'pixel_8a', + 'pixel_9', + 'pixel_9_pro', + 'pixel_9_pro_xl', + 'pixel_9a', +]; +const IMAGES = [ + 'system-images;android-30;default;x86_64', + 'system-images;android-30;google_apis;x86_64', + 'system-images;android-32;default;x86_64', + 'system-images;android-32;google_apis;x86_64', + 'system-images;android-34;default;x86_64', + 'system-images;android-34;google_apis;x86_64', + 'system-images;android-35;default;x86_64', + 'system-images;android-35;google_apis;x86_64', + 'system-images;android-36;default;x86_64', + 'system-images;android-36;google_apis;x86_64', +]; +const NO_IOS_PROFILES_MESSAGE = 'No simulator profiles are available for iOS yet.'; + +const mockEnableJsonOutput = jest.mocked(enableJsonOutput); +const mockPrintJsonOnlyOutput = jest.mocked(printJsonOnlyOutput); +const mockLog = jest.mocked(Log.log); + +function getMockOclifConfig(): Config { + const config = new Config({ root: __dirname }); + config.runHook = async () => ({ + failures: [], + successes: [], + }); + return config; +} + +describe(SimulatorProfiles, () => { + const mockConfig = getMockOclifConfig(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('emits Android profiles as JSON', async () => { + const command = new SimulatorProfiles(['--platform', 'android', '--json'], mockConfig); + + await command.runAsync(); + + expect(mockEnableJsonOutput).toHaveBeenCalledTimes(1); + expect(mockPrintJsonOnlyOutput).toHaveBeenCalledWith({ devices: DEVICES, images: IMAGES }); + expect(mockLog).not.toHaveBeenCalled(); + }); + + it('formats Android profiles and marks the default device', async () => { + const command = new SimulatorProfiles(['--platform', 'android'], mockConfig); + + await command.runAsync(); + + expect(mockEnableJsonOutput).not.toHaveBeenCalled(); + expect(mockPrintJsonOnlyOutput).not.toHaveBeenCalled(); + expect(mockLog).toHaveBeenCalledWith( + [ + chalk.bold('Devices:'), + ...DEVICES.map( + device => + ` ${device === 'pixel_9' ? chalk.green(device) : device}${ + device === 'pixel_9' ? ` ${chalk.green('(default)')}` : '' + }` + ), + '', + chalk.bold('System images:'), + ...IMAGES.map(image => ` ${image}`), + ].join('\n') + ); + }); + + it('reports that iOS profiles are unavailable', async () => { + const command = new SimulatorProfiles(['--platform', 'ios'], mockConfig); + + await command.runAsync(); + + expect(mockLog).toHaveBeenCalledWith(NO_IOS_PROFILES_MESSAGE); + expect(mockPrintJsonOnlyOutput).not.toHaveBeenCalled(); + }); + + it('reports unavailable iOS profiles as a JSON error', async () => { + const command = new SimulatorProfiles(['--platform', 'ios', '--json'], mockConfig); + + await command.runAsync(); + + expect(mockEnableJsonOutput).toHaveBeenCalledTimes(1); + expect(mockPrintJsonOnlyOutput).toHaveBeenCalledWith({ error: NO_IOS_PROFILES_MESSAGE }); + expect(mockLog).not.toHaveBeenCalled(); + }); + + it('requires --platform', async () => { + const command = new SimulatorProfiles([], mockConfig); + + await expect(command.runAsync()).rejects.toThrow('Missing required flag platform'); + }); + + it('rejects unsupported platforms', async () => { + const command = new SimulatorProfiles(['--platform', 'web'], mockConfig); + + await expect(command.runAsync()).rejects.toThrow( + 'Expected --platform=web to be one of: android, ios' + ); + }); +}); diff --git a/packages/eas-cli/src/commands/simulator/profiles.ts b/packages/eas-cli/src/commands/simulator/profiles.ts new file mode 100644 index 0000000000..97826d0a74 --- /dev/null +++ b/packages/eas-cli/src/commands/simulator/profiles.ts @@ -0,0 +1,93 @@ +import { Flags } from '@oclif/core'; +import chalk from 'chalk'; + +import EasCommand from '../../commandUtils/EasCommand'; +import { EasJsonOnlyFlag } from '../../commandUtils/flags'; +import Log from '../../log'; +import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; + +const PLATFORM_FLAG_VALUES = ['android', 'ios'] as const; +const DEVICES = [ + 'pixel_6', + 'pixel_6_pro', + 'pixel_6a', + 'pixel_7', + 'pixel_7_pro', + 'pixel_7a', + 'pixel_8', + 'pixel_8_pro', + 'pixel_8a', + 'pixel_9', + 'pixel_9_pro', + 'pixel_9_pro_xl', + 'pixel_9a', +] as const; +const IMAGES = [ + 'system-images;android-30;default;x86_64', + 'system-images;android-30;google_apis;x86_64', + 'system-images;android-32;default;x86_64', + 'system-images;android-32;google_apis;x86_64', + 'system-images;android-34;default;x86_64', + 'system-images;android-34;google_apis;x86_64', + 'system-images;android-35;default;x86_64', + 'system-images;android-35;google_apis;x86_64', + 'system-images;android-36;default;x86_64', + 'system-images;android-36;google_apis;x86_64', +] as const; +const DEFAULT_DEVICE: (typeof DEVICES)[number] = 'pixel_9'; +const NO_IOS_PROFILES_MESSAGE = 'No simulator profiles are available for iOS yet.'; + +export default class SimulatorProfiles extends EasCommand { + static override hidden = true; + static override aliases = ['sim:profiles']; + static override description = + '[EXPERIMENTAL] list available device and system image profiles for EAS Simulator'; + + static override flags = { + platform: Flags.option({ + char: 'p', + description: 'Device platform', + options: PLATFORM_FLAG_VALUES, + required: true, + })(), + ...EasJsonOnlyFlag, + }; + + async runAsync(): Promise { + const { flags } = await this.parse(SimulatorProfiles); + + if (flags.json) { + enableJsonOutput(); + } + + if (flags.platform === 'ios') { + if (flags.json) { + printJsonOnlyOutput({ error: NO_IOS_PROFILES_MESSAGE }); + } else { + Log.log(NO_IOS_PROFILES_MESSAGE); + } + return; + } + + if (flags.json) { + printJsonOnlyOutput({ devices: DEVICES, images: IMAGES }); + return; + } + + Log.log( + [ + chalk.bold('Devices:'), + ...DEVICES.map(formatDevice), + '', + chalk.bold('System images:'), + ...IMAGES.map(image => ` ${image}`), + ].join('\n') + ); + } +} + +function formatDevice(device: (typeof DEVICES)[number]): string { + const formattedDevice = device === DEFAULT_DEVICE ? chalk.green(device) : device; + const defaultLabel = device === DEFAULT_DEVICE ? ` ${chalk.green('(default)')}` : ''; + return ` ${formattedDevice}${defaultLabel}`; +}