Skip to content

Commit d192370

Browse files
committed
fix: refresh not working properly after deletion
1 parent f83f8b7 commit d192370

2 files changed

Lines changed: 77 additions & 44 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "default",
3-
"version": "1.15.1-beta.2",
3+
"version": "1.15.1-beta.8",
44
"description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux",
55
"main": "dist/index.js",
66
"scripts": {

src/resources/ios/ios-simulator/ios-simulator.ts

Lines changed: 76 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ interface SimDevice {
5454
udid: string;
5555
name: string;
5656
state: string;
57+
isAvailable: boolean;
5758
deviceTypeIdentifier: string;
5859
}
5960

@@ -71,6 +72,7 @@ interface SimctlRuntimesOutput {
7172
runtimes: SimctlRuntime[];
7273
}
7374

75+
7476
const defaultConfig: Partial<IosSimulatorConfig> & { os: any } = {
7577
simulators: [],
7678
os: ['macOS'],
@@ -122,6 +124,20 @@ function runtimeToXcodebuildPlatform(runtimeId: string): string {
122124
return match ? match[1] : runtimeId;
123125
}
124126

127+
// com.apple.CoreSimulator.SimRuntime.iOS-26-0 → "com.apple.CoreSimulator.SimRuntime.iOS-26"
128+
function runtimeMajorPrefix(runtimeId: string): string {
129+
// Strip the patch version component (last -N segment) to get a major-match prefix.
130+
// iOS-26-0 and iOS-26-3 both share prefix "...iOS-26".
131+
return runtimeId.replace(/-\d+$/, '');
132+
}
133+
134+
// Two runtime IDs match if they are equal or share the same major-version prefix.
135+
// Allows iOS-26-0 (declared) to match iOS-26-3 (installed).
136+
function runtimesMatch(a: string, b: string): boolean {
137+
if (a === b) return true;
138+
return runtimeMajorPrefix(a) === runtimeMajorPrefix(b);
139+
}
140+
125141
export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
126142
getSettings(): ResourceSettings<IosSimulatorConfig> {
127143
return {
@@ -141,7 +157,7 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
141157
isElementEqual: (a, b) =>
142158
a.name === b.name &&
143159
a.deviceType === b.deviceType &&
144-
a.runtime === b.runtime,
160+
runtimesMatch(a.runtime, b.runtime),
145161
filterInStatelessMode: (desired, current) =>
146162
current.filter((c) => desired.some((d) => d.name === c.name)),
147163
canModify: true,
@@ -154,21 +170,7 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
154170
}
155171

156172
async refresh(): Promise<Partial<IosSimulatorConfig> | null> {
157-
const allDevices = await this.listAllDevices();
158-
if (!allDevices) return null;
159-
160-
const simulators: SimulatorDeclaration[] = [];
161-
for (const [runtimeId, devices] of Object.entries(allDevices)) {
162-
for (const device of devices) {
163-
simulators.push({
164-
name: device.name,
165-
deviceType: device.deviceTypeIdentifier,
166-
runtime: runtimeId,
167-
});
168-
}
169-
}
170-
171-
return simulators.length > 0 ? { simulators } : null;
173+
return null;
172174
}
173175

174176
async create(plan: CreatePlan<IosSimulatorConfig>): Promise<void> {
@@ -182,10 +184,17 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
182184
} else {
183185
await this.assertRuntimesAvailable(simulators);
184186
}
187+
const available = await this.listAvailableRuntimes();
188+
const existingDevices = await this.listAllDevices() ?? {};
189+
const existingNames = new Set(
190+
Object.values(existingDevices).flat().map((d) => d.name),
191+
);
185192
const $ = getPty();
186193
for (const sim of simulators) {
194+
if (existingNames.has(sim.name)) continue;
195+
const runtimeId = this.resolveRuntimeId(sim.runtime, available);
187196
const { status, data } = await $.spawnSafe(
188-
`xcrun simctl create "${sim.name}" "${sim.deviceType}" "${sim.runtime}"`,
197+
`xcrun simctl create "${sim.name}" "${sim.deviceType}" "${runtimeId}"`,
189198
{ interactive: true },
190199
);
191200
if (status !== SpawnStatus.SUCCESS) {
@@ -230,9 +239,11 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
230239
} else if (toAdd.length > 0) {
231240
await this.assertRuntimesAvailable(toAdd);
232241
}
242+
const available = await this.listAvailableRuntimes();
233243
for (const sim of toAdd) {
244+
const runtimeId = this.resolveRuntimeId(sim.runtime, available);
234245
await $.spawn(
235-
`xcrun simctl create "${sim.name}" "${sim.deviceType}" "${sim.runtime}"`,
246+
`xcrun simctl create "${sim.name}" "${sim.deviceType}" "${runtimeId}"`,
236247
{ interactive: true },
237248
);
238249
}
@@ -273,41 +284,44 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
273284
}
274285
}
275286

276-
private async deleteOrphanedRuntimes(candidateRuntimes: Set<string>): Promise<void> {
277-
if (candidateRuntimes.size === 0) return;
278-
279-
const allDevices = await this.listAllDevices();
280-
const stillInUse = new Set<string>();
281-
if (allDevices) {
282-
for (const [runtimeId, devices] of Object.entries(allDevices)) {
283-
if (devices.length > 0) stillInUse.add(runtimeId);
284-
}
285-
}
286-
287-
const $ = getPty();
288-
for (const runtimeId of candidateRuntimes) {
289-
if (!stillInUse.has(runtimeId)) {
290-
await $.spawnSafe(`xcrun simctl runtime delete "${runtimeId}"`);
291-
}
292-
}
293-
}
294-
295-
private async getMissingRuntimes(simulators: SimulatorDeclaration[]): Promise<string[]> {
287+
private async listAvailableRuntimes(): Promise<SimctlRuntime[]> {
296288
const $ = getPty();
297289
const { status, data } = await $.spawnSafe('xcrun simctl list runtimes --json');
298290
if (status !== SpawnStatus.SUCCESS) return [];
299-
300-
let allRuntimes: SimctlRuntime[];
301291
try {
302292
const parsed: SimctlRuntimesOutput = JSON.parse(data);
303-
allRuntimes = parsed.runtimes;
293+
return parsed.runtimes.filter((r) => r.isAvailable);
304294
} catch {
305295
return [];
306296
}
297+
}
298+
299+
// Resolve a declared runtime ID to the actual available one.
300+
// If the exact ID is available, return it unchanged.
301+
// Otherwise fall back to the highest-versioned available runtime sharing the same major prefix
302+
// (e.g. iOS-26-0 → iOS-26-3 when only iOS 26.3 is installed).
303+
private resolveRuntimeId(declared: string, available: SimctlRuntime[]): string {
304+
if (available.some((r) => r.identifier === declared)) return declared;
305+
306+
const prefix = runtimeMajorPrefix(declared);
307+
const candidates = available.filter((r) => r.identifier.startsWith(prefix));
308+
if (candidates.length === 0) return declared;
309+
310+
// Pick the lexicographically highest patch version
311+
candidates.sort((a, b) => b.identifier.localeCompare(a.identifier));
312+
return candidates[0].identifier;
313+
}
307314

308-
const availableIds = new Set(allRuntimes.filter((r) => r.isAvailable).map((r) => r.identifier));
315+
private async getMissingRuntimes(simulators: SimulatorDeclaration[]): Promise<string[]> {
316+
const available = await this.listAvailableRuntimes();
317+
const availableIds = new Set(available.map((r) => r.identifier));
309318
const requiredRuntimes = [...new Set(simulators.map((s) => s.runtime))];
310-
return requiredRuntimes.filter((r) => !availableIds.has(r));
319+
return requiredRuntimes.filter((declared) => {
320+
if (availableIds.has(declared)) return false;
321+
// Also consider it present if a same-major-version runtime is available
322+
const prefix = runtimeMajorPrefix(declared);
323+
return !available.some((r) => r.identifier.startsWith(prefix));
324+
});
311325
}
312326

313327
private async downloadMissingRuntimes(simulators: SimulatorDeclaration[]): Promise<void> {
@@ -334,6 +348,25 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
334348
throw new Error(lines.join('\n'));
335349
}
336350

351+
private async deleteOrphanedRuntimes(candidateRuntimes: Set<string>): Promise<void> {
352+
if (candidateRuntimes.size === 0) return;
353+
354+
const allDevices = await this.listAllDevices();
355+
const stillInUse = new Set<string>();
356+
if (allDevices) {
357+
for (const [runtimeId, devices] of Object.entries(allDevices)) {
358+
if (devices.length > 0) stillInUse.add(runtimeId);
359+
}
360+
}
361+
362+
const $ = getPty();
363+
for (const runtimeId of candidateRuntimes) {
364+
if (!stillInUse.has(runtimeId)) {
365+
await $.spawnSafe(`xcrun simctl runtime delete "${runtimeId}"`);
366+
}
367+
}
368+
}
369+
337370
private async acceptLicenseIfNeeded(): Promise<void> {
338371
const $ = getPty();
339372
const { status } = await $.spawnSafe('xcodebuild -license status');

0 commit comments

Comments
 (0)