Skip to content

Commit 3ead2ce

Browse files
committed
feat: Add ios-emulators resource (auto-generated from issue #72)
1 parent 484de42 commit 3ead2ce

6 files changed

Lines changed: 407 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
title: ios-simulator
3+
description: A reference page for the ios-simulator resource
4+
---
5+
6+
The ios-simulator resource manages iOS (and iPadOS/watchOS/tvOS/visionOS) simulator instances on macOS
7+
using `xcrun simctl`. Each resource declaration represents one simulator. You can declare multiple
8+
`ios-simulator` resources to create a full testing matrix across device types and OS versions.
9+
10+
Simulators are created with the specified device type and runtime, and optionally booted. Removing a
11+
resource deletes the simulator from the system. Xcode Command Line Tools must be installed — add an
12+
`xcode-tools` resource as a dependency if you are not sure they are present.
13+
14+
## Parameters:
15+
16+
- **name** *(string, required)* — Human-readable name for the simulator instance (e.g. `"iPhone 15 Dev"`). Must be unique across your declared simulators.
17+
18+
- **deviceType** *(string, required)* — CoreSimulator device type identifier. Use the format `com.apple.CoreSimulator.SimDeviceType.<Device>`. Run `xcrun simctl list devicetypes` to see identifiers available on your machine.
19+
20+
- **runtime** *(string, required)* — CoreSimulator runtime identifier. Use the format `com.apple.CoreSimulator.SimRuntime.<Platform>-<Version>`. Run `xcrun simctl list runtimes` to see installed runtimes.
21+
22+
- **state** *(string, optional)* — Desired runtime state of the simulator. One of `"Booted"` or `"Shutdown"`. Defaults to `"Shutdown"`. Can be modified after creation.
23+
24+
## Example usage:
25+
26+
```json title="codify.jsonc"
27+
[
28+
{
29+
"type": "ios-simulator",
30+
"name": "iPhone 15 Dev",
31+
"deviceType": "com.apple.CoreSimulator.SimDeviceType.iPhone-15",
32+
"runtime": "com.apple.CoreSimulator.SimRuntime.iOS-18-0",
33+
"state": "Shutdown",
34+
"os": ["macOS"]
35+
}
36+
]
37+
```
38+
39+
```json title="codify.jsonc"
40+
[
41+
{
42+
"type": "xcode-tools",
43+
"os": ["macOS"]
44+
},
45+
{
46+
"type": "ios-simulator",
47+
"name": "iPhone 15 Pro",
48+
"deviceType": "com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro",
49+
"runtime": "com.apple.CoreSimulator.SimRuntime.iOS-18-0",
50+
"state": "Shutdown",
51+
"os": ["macOS"]
52+
},
53+
{
54+
"type": "ios-simulator",
55+
"name": "iPad Pro 11-inch",
56+
"deviceType": "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M4",
57+
"runtime": "com.apple.CoreSimulator.SimRuntime.iOS-18-0",
58+
"state": "Shutdown",
59+
"os": ["macOS"]
60+
}
61+
]
62+
```

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import { TerraformResource } from './resources/terraform/terraform.js';
6767
import { CursorResource } from './resources/cursor/cursor.js';
6868
import { VscodeResource } from './resources/vscode/vscode.js';
6969
import { WebStormResource } from './resources/webstorm/webstorm.js';
70+
import { IosSimulatorResource } from './resources/ios/ios-simulator/ios-simulator.js';
7071
import { XcodeToolsResource } from './resources/xcode-tools/xcode-tools.js';
7172
import { YumResource } from './resources/yum/yum.js';
7273
import { PyCharmResource } from './resources/jetbrains/pycharm/pycharm.js';
@@ -86,6 +87,7 @@ runPlugin(Plugin.create(
8687
[
8788
new GitResource(),
8889
new XcodeToolsResource(),
90+
new IosSimulatorResource(),
8991
new PathResource(),
9092
new AliasResource(),
9193
new AliasesResource(),
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Known CoreSimulator device type identifiers shipped with Xcode.
2+
// These identifiers are stable across Xcode versions for each device family.
3+
export default async function loadIosSimulatorDeviceTypes(): Promise<string[]> {
4+
return [
5+
// iPhone SE
6+
'com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation',
7+
8+
// iPhone 14 family
9+
'com.apple.CoreSimulator.SimDeviceType.iPhone-14',
10+
'com.apple.CoreSimulator.SimDeviceType.iPhone-14-Plus',
11+
'com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro',
12+
'com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro-Max',
13+
14+
// iPhone 15 family
15+
'com.apple.CoreSimulator.SimDeviceType.iPhone-15',
16+
'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Plus',
17+
'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro',
18+
'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro-Max',
19+
20+
// iPhone 16 family
21+
'com.apple.CoreSimulator.SimDeviceType.iPhone-16',
22+
'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Plus',
23+
'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro',
24+
'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro-Max',
25+
26+
// iPad mini
27+
'com.apple.CoreSimulator.SimDeviceType.iPad-mini-6th-generation',
28+
'com.apple.CoreSimulator.SimDeviceType.iPad-mini-A17-Pro',
29+
30+
// iPad Air
31+
'com.apple.CoreSimulator.SimDeviceType.iPad-Air-5th-generation',
32+
'com.apple.CoreSimulator.SimDeviceType.iPad-Air-11-inch-M2',
33+
'com.apple.CoreSimulator.SimDeviceType.iPad-Air-13-inch-M2',
34+
35+
// iPad Pro 11-inch
36+
'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-4th-generation',
37+
'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M4',
38+
39+
// iPad Pro 12.9 / 13-inch
40+
'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-12-9-inch-6th-generation',
41+
'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M4',
42+
43+
// Apple Watch
44+
'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-9-41mm',
45+
'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-9-45mm',
46+
'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Ultra-2-49mm',
47+
48+
// Apple TV
49+
'com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-4K',
50+
'com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-1080p',
51+
52+
// Apple Vision Pro
53+
'com.apple.CoreSimulator.SimDeviceType.Apple-Vision-Pro',
54+
];
55+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Known CoreSimulator runtime identifiers. Each entry corresponds to an iOS
2+
// (or watchOS/tvOS/visionOS) runtime that can be installed via Xcode.
3+
export default async function loadIosSimulatorRuntimes(): Promise<string[]> {
4+
return [
5+
// iOS
6+
'com.apple.CoreSimulator.SimRuntime.iOS-16-0',
7+
'com.apple.CoreSimulator.SimRuntime.iOS-16-1',
8+
'com.apple.CoreSimulator.SimRuntime.iOS-16-2',
9+
'com.apple.CoreSimulator.SimRuntime.iOS-16-4',
10+
'com.apple.CoreSimulator.SimRuntime.iOS-17-0',
11+
'com.apple.CoreSimulator.SimRuntime.iOS-17-2',
12+
'com.apple.CoreSimulator.SimRuntime.iOS-17-4',
13+
'com.apple.CoreSimulator.SimRuntime.iOS-17-5',
14+
'com.apple.CoreSimulator.SimRuntime.iOS-18-0',
15+
'com.apple.CoreSimulator.SimRuntime.iOS-18-1',
16+
'com.apple.CoreSimulator.SimRuntime.iOS-18-2',
17+
'com.apple.CoreSimulator.SimRuntime.iOS-18-3',
18+
'com.apple.CoreSimulator.SimRuntime.iOS-18-4',
19+
20+
// watchOS
21+
'com.apple.CoreSimulator.SimRuntime.watchOS-10-0',
22+
'com.apple.CoreSimulator.SimRuntime.watchOS-10-4',
23+
'com.apple.CoreSimulator.SimRuntime.watchOS-11-0',
24+
'com.apple.CoreSimulator.SimRuntime.watchOS-11-2',
25+
26+
// tvOS
27+
'com.apple.CoreSimulator.SimRuntime.tvOS-17-0',
28+
'com.apple.CoreSimulator.SimRuntime.tvOS-17-4',
29+
'com.apple.CoreSimulator.SimRuntime.tvOS-18-0',
30+
'com.apple.CoreSimulator.SimRuntime.tvOS-18-2',
31+
32+
// visionOS
33+
'com.apple.CoreSimulator.SimRuntime.xrOS-1-0',
34+
'com.apple.CoreSimulator.SimRuntime.xrOS-1-2',
35+
'com.apple.CoreSimulator.SimRuntime.xrOS-2-0',
36+
'com.apple.CoreSimulator.SimRuntime.xrOS-2-2',
37+
];
38+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import {
2+
CreatePlan,
3+
DestroyPlan,
4+
ExampleConfig,
5+
ModifyPlan,
6+
ParameterChange,
7+
Resource,
8+
ResourceSettings,
9+
SpawnStatus,
10+
getPty,
11+
z,
12+
} from '@codifycli/plugin-core';
13+
import { OS } from '@codifycli/schemas';
14+
15+
const schema = z.object({
16+
name: z
17+
.string()
18+
.describe('Name for the iOS simulator instance (e.g. "iPhone 15 Dev")'),
19+
deviceType: z
20+
.string()
21+
.describe('Device type identifier (e.g. "com.apple.CoreSimulator.SimDeviceType.iPhone-15")'),
22+
runtime: z
23+
.string()
24+
.describe('Runtime identifier (e.g. "com.apple.CoreSimulator.SimRuntime.iOS-18-0")'),
25+
state: z
26+
.enum(['Booted', 'Shutdown'])
27+
.optional()
28+
.describe('Desired runtime state of the simulator. Defaults to Shutdown.'),
29+
});
30+
31+
export type IosSimulatorConfig = z.infer<typeof schema>;
32+
33+
interface SimDevice {
34+
udid: string;
35+
name: string;
36+
state: string;
37+
deviceTypeIdentifier: string;
38+
}
39+
40+
interface SimctlDevicesOutput {
41+
devices: Record<string, SimDevice[]>;
42+
}
43+
44+
const defaultConfig: Partial<IosSimulatorConfig> & { os: any } = {
45+
name: '<Replace me here!>',
46+
deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15',
47+
runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0',
48+
state: 'Shutdown',
49+
os: ['macOS'],
50+
};
51+
52+
const exampleBasic: ExampleConfig = {
53+
title: 'iPhone 15 simulator for development',
54+
description: 'Create an iPhone 15 simulator running iOS 18 for use in development and UI testing.',
55+
configs: [{
56+
type: 'ios-simulator',
57+
name: 'iPhone 15 Dev',
58+
deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15',
59+
runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0',
60+
state: 'Shutdown',
61+
os: ['macOS'],
62+
}],
63+
};
64+
65+
const exampleMultiDevice: ExampleConfig = {
66+
title: 'iPhone and iPad simulator setup',
67+
description: 'Install Xcode Command Line Tools and create an iPhone and iPad simulator for cross-device testing.',
68+
configs: [
69+
{ type: 'xcode-tools', os: ['macOS'] },
70+
{
71+
type: 'ios-simulator',
72+
name: 'iPhone 15 Pro',
73+
deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro',
74+
runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0',
75+
state: 'Shutdown',
76+
os: ['macOS'],
77+
},
78+
{
79+
type: 'ios-simulator',
80+
name: 'iPad Pro 11-inch',
81+
deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M4',
82+
runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0',
83+
state: 'Shutdown',
84+
os: ['macOS'],
85+
},
86+
],
87+
};
88+
89+
export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
90+
getSettings(): ResourceSettings<IosSimulatorConfig> {
91+
return {
92+
id: 'ios-simulator',
93+
defaultConfig,
94+
exampleConfigs: {
95+
example1: exampleBasic,
96+
example2: exampleMultiDevice,
97+
},
98+
operatingSystems: [OS.Darwin],
99+
dependencies: ['xcode-tools'],
100+
schema,
101+
parameterSettings: {
102+
state: { type: 'string', canModify: true },
103+
},
104+
allowMultiple: {
105+
identifyingParameters: ['name'],
106+
},
107+
};
108+
}
109+
110+
async refresh(parameters: Partial<IosSimulatorConfig>): Promise<Partial<IosSimulatorConfig> | null> {
111+
const $ = getPty();
112+
113+
const { status, data } = await $.spawnSafe('xcrun simctl list devices --json');
114+
if (status !== SpawnStatus.SUCCESS) {
115+
return null;
116+
}
117+
118+
let parsed: SimctlDevicesOutput;
119+
try {
120+
parsed = JSON.parse(data);
121+
} catch {
122+
return null;
123+
}
124+
125+
for (const [runtimeId, devices] of Object.entries(parsed.devices)) {
126+
const match = devices.find((d) => d.name === parameters.name);
127+
if (match) {
128+
return {
129+
name: match.name,
130+
deviceType: match.deviceTypeIdentifier,
131+
runtime: runtimeId,
132+
state: match.state === 'Booted' ? 'Booted' : 'Shutdown',
133+
};
134+
}
135+
}
136+
137+
return null;
138+
}
139+
140+
async create(plan: CreatePlan<IosSimulatorConfig>): Promise<void> {
141+
const $ = getPty();
142+
const { name, deviceType, runtime, state } = plan.desiredConfig;
143+
144+
// xcrun simctl create prints the new simulator's UDID to stdout
145+
const { data: udid } = await $.spawn(
146+
`xcrun simctl create "${name}" "${deviceType}" "${runtime}"`,
147+
{ interactive: true }
148+
);
149+
150+
if (state === 'Booted') {
151+
await $.spawn(`xcrun simctl boot "${udid.trim()}"`, { interactive: true });
152+
}
153+
}
154+
155+
async modify(pc: ParameterChange<IosSimulatorConfig>, plan: ModifyPlan<IosSimulatorConfig>): Promise<void> {
156+
if (pc.name !== 'state') return;
157+
158+
const $ = getPty();
159+
const udid = await this.getUdidByName(plan.desiredConfig.name);
160+
if (!udid) return;
161+
162+
if (plan.desiredConfig.state === 'Booted') {
163+
await $.spawn(`xcrun simctl boot "${udid}"`, { interactive: true });
164+
} else {
165+
await $.spawn(`xcrun simctl shutdown "${udid}"`, { interactive: true });
166+
}
167+
}
168+
169+
async destroy(plan: DestroyPlan<IosSimulatorConfig>): Promise<void> {
170+
const $ = getPty();
171+
const udid = await this.getUdidByName(plan.currentConfig.name);
172+
if (!udid) return;
173+
174+
await $.spawn(`xcrun simctl delete "${udid}"`, { interactive: true });
175+
}
176+
177+
private async getUdidByName(name: string | undefined): Promise<string | null> {
178+
if (!name) return null;
179+
180+
const $ = getPty();
181+
const { status, data } = await $.spawnSafe('xcrun simctl list devices --json');
182+
if (status !== SpawnStatus.SUCCESS) return null;
183+
184+
try {
185+
const parsed: SimctlDevicesOutput = JSON.parse(data);
186+
for (const devices of Object.values(parsed.devices)) {
187+
const match = devices.find((d) => d.name === name);
188+
if (match) return match.udid;
189+
}
190+
} catch {
191+
// ignore parse errors
192+
}
193+
194+
return null;
195+
}
196+
}

0 commit comments

Comments
 (0)