diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9d701c70..2ea1d115 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -39,6 +39,7 @@
+
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index ceb052e0..37298737 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -32,6 +32,10 @@
https://github.com/dotnet/dotnet
73b3b5ac0e4a5658c7a0555b67d91a22ad39de4b
+
+ https://github.com/dotnet/macios-devtools
+ 14efcb9735fab369066fa3c99130d076aa0dfdc9
+
diff --git a/eng/Versions.props b/eng/Versions.props
index cb0f2a20..9cc80e44 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -44,6 +44,10 @@
1.0.143-preview.8
+
+
+ 1.0.0-preview.1.26201.1
+
0.3.0
$(PlatformMauiMacOSVersion)
diff --git a/src/Cli/Microsoft.Maui.Cli.UnitTests/DeviceManagerTests.cs b/src/Cli/Microsoft.Maui.Cli.UnitTests/DeviceManagerTests.cs
index ad6c5125..082948ca 100644
--- a/src/Cli/Microsoft.Maui.Cli.UnitTests/DeviceManagerTests.cs
+++ b/src/Cli/Microsoft.Maui.Cli.UnitTests/DeviceManagerTests.cs
@@ -33,6 +33,70 @@ public async Task GetAllDevicesAsync_ReturnsAndroidDevices()
Assert.Contains(devices, d => d.Platforms.Contains("android"));
}
+ [Fact]
+ public async Task GetAllDevicesAsync_ReturnsAppleSimulators()
+ {
+ // Arrange
+ var fakeApple = new FakeAppleProvider
+ {
+ Devices = new List
+ {
+ new Device
+ {
+ Id = "sim-udid-1234",
+ Name = "iPhone 15 Pro",
+ Platforms = new[] { "ios" },
+ Type = DeviceType.Simulator,
+ State = DeviceState.Booted,
+ IsEmulator = true,
+ IsRunning = true,
+ EmulatorId = "sim-udid-1234",
+ Version = "18.0"
+ }
+ }
+ };
+
+ var manager = new DeviceManager(appleProvider: fakeApple);
+
+ // Act
+ var devices = await manager.GetAllDevicesAsync();
+
+ // Assert
+ Assert.Single(devices);
+ Assert.Contains(devices, d => d.Platforms.Contains("ios"));
+ Assert.Equal(DeviceType.Simulator, devices[0].Type);
+ }
+
+ [Fact]
+ public async Task GetAllDevicesAsync_ReturnsBothAndroidAndApple()
+ {
+ // Arrange
+ var fakeAndroid = new FakeAndroidProvider
+ {
+ Devices = new List
+ {
+ new Device { Id = "emulator-5554", Name = "Pixel 6", Platforms = new[] { "android" }, Type = DeviceType.Emulator, State = DeviceState.Booted, IsEmulator = true, IsRunning = true }
+ }
+ };
+ var fakeApple = new FakeAppleProvider
+ {
+ Devices = new List
+ {
+ new Device { Id = "sim-udid", Name = "iPhone 15", Platforms = new[] { "ios" }, Type = DeviceType.Simulator, State = DeviceState.Booted, IsEmulator = true, IsRunning = true }
+ }
+ };
+
+ var manager = new DeviceManager(fakeAndroid, fakeApple);
+
+ // Act
+ var devices = await manager.GetAllDevicesAsync();
+
+ // Assert
+ Assert.Equal(2, devices.Count);
+ Assert.Contains(devices, d => d.Platforms.Contains("android"));
+ Assert.Contains(devices, d => d.Platforms.Contains("ios"));
+ }
+
[Fact]
public async Task GetDevicesByPlatformAsync_FiltersCorrectly()
{
@@ -55,6 +119,35 @@ public async Task GetDevicesByPlatformAsync_FiltersCorrectly()
Assert.All(androidOnly, d => Assert.Contains("android", d.Platforms));
}
+ [Fact]
+ public async Task GetDevicesByPlatformAsync_FiltersIosDevices()
+ {
+ // Arrange
+ var fakeAndroid = new FakeAndroidProvider
+ {
+ Devices = new List
+ {
+ new Device { Id = "emulator-5554", Name = "Pixel 6", Platforms = new[] { "android" }, Type = DeviceType.Emulator, State = DeviceState.Booted, IsEmulator = true, IsRunning = true }
+ }
+ };
+ var fakeApple = new FakeAppleProvider
+ {
+ Devices = new List
+ {
+ new Device { Id = "sim-udid", Name = "iPhone 15", Platforms = new[] { "ios" }, Type = DeviceType.Simulator, State = DeviceState.Booted, IsEmulator = true, IsRunning = true }
+ }
+ };
+
+ var manager = new DeviceManager(fakeAndroid, fakeApple);
+
+ // Act
+ var iosOnly = await manager.GetDevicesByPlatformAsync("ios");
+
+ // Assert
+ Assert.Single(iosOnly);
+ Assert.All(iosOnly, d => Assert.Contains("ios", d.Platforms));
+ }
+
[Fact]
public async Task GetDeviceByIdAsync_FindsCorrectDevice()
{
diff --git a/src/Cli/Microsoft.Maui.Cli.UnitTests/DoctorServiceTests.cs b/src/Cli/Microsoft.Maui.Cli.UnitTests/DoctorServiceTests.cs
index bd9553d7..55dc4d7f 100644
--- a/src/Cli/Microsoft.Maui.Cli.UnitTests/DoctorServiceTests.cs
+++ b/src/Cli/Microsoft.Maui.Cli.UnitTests/DoctorServiceTests.cs
@@ -59,6 +59,41 @@ public async Task RunAllChecksAsync_IncludesAndroidChecks_WhenProviderReturnsChe
Assert.Contains(report.Checks, c => c.Category == "android" && c.Name == "Android SDK");
}
+ [Fact]
+ public async Task RunAllChecksAsync_IncludesAppleChecks_WhenProviderReturnsChecks()
+ {
+ // Arrange
+ var fakeApple = new FakeAppleProvider
+ {
+ HealthChecks = new List
+ {
+ new HealthCheck
+ {
+ Category = "apple",
+ Name = "Xcode",
+ Status = CheckStatus.Ok,
+ Message = "Xcode 16.0"
+ },
+ new HealthCheck
+ {
+ Category = "apple",
+ Name = "Command Line Tools",
+ Status = CheckStatus.Ok,
+ Message = "CLT installed"
+ }
+ }
+ };
+
+ var service = new DoctorService(appleProvider: fakeApple);
+
+ // Act
+ var report = await service.RunAllChecksAsync();
+
+ // Assert
+ Assert.Contains(report.Checks, c => c.Category == "apple" && c.Name == "Xcode");
+ Assert.Contains(report.Checks, c => c.Category == "apple" && c.Name == "Command Line Tools");
+ }
+
[Fact]
public async Task RunAllChecksAsync_CalculatesCorrectSummary()
{
@@ -105,6 +140,27 @@ public async Task RunCategoryChecksAsync_SetsStatusBasedOnChecks()
Assert.NotEqual(HealthStatus.Unhealthy, report.Status);
}
+ [Fact]
+ public async Task RunCategoryChecksAsync_AppleCategory_ReturnsAppleChecks()
+ {
+ // Arrange
+ var fakeApple = new FakeAppleProvider
+ {
+ HealthChecks = new List
+ {
+ new HealthCheck { Category = "apple", Name = "Xcode", Status = CheckStatus.Ok, Message = "Xcode 16.0" }
+ }
+ };
+
+ var service = new DoctorService(appleProvider: fakeApple);
+
+ // Act
+ var report = await service.RunCategoryChecksAsync("apple");
+
+ // Assert
+ Assert.Contains(report.Checks, c => c.Category == "apple" && c.Name == "Xcode");
+ }
+
[Fact]
public async Task RunAllChecksAsync_IncludesAndroidChecks_WhenProviderReturnsAndroidOnly()
{
@@ -125,4 +181,33 @@ public async Task RunAllChecksAsync_IncludesAndroidChecks_WhenProviderReturnsAnd
// Assert - android checks should be present
Assert.Contains(report.Checks, c => c.Category == "android" && c.Name == "JDK");
}
+
+ [Fact]
+ public async Task RunAllChecksAsync_BothProviders_IncludesBothChecks()
+ {
+ // Arrange
+ var fakeAndroid = new FakeAndroidProvider
+ {
+ HealthChecks = new List
+ {
+ new HealthCheck { Category = "android", Name = "JDK", Status = CheckStatus.Ok }
+ }
+ };
+ var fakeApple = new FakeAppleProvider
+ {
+ HealthChecks = new List
+ {
+ new HealthCheck { Category = "apple", Name = "Xcode", Status = CheckStatus.Ok }
+ }
+ };
+
+ var service = new DoctorService(fakeAndroid, fakeApple);
+
+ // Act
+ var report = await service.RunAllChecksAsync();
+
+ // Assert
+ Assert.Contains(report.Checks, c => c.Category == "android");
+ Assert.Contains(report.Checks, c => c.Category == "apple");
+ }
}
diff --git a/src/Cli/Microsoft.Maui.Cli.UnitTests/Fakes/FakeAppleProvider.cs b/src/Cli/Microsoft.Maui.Cli.UnitTests/Fakes/FakeAppleProvider.cs
new file mode 100644
index 00000000..6c863176
--- /dev/null
+++ b/src/Cli/Microsoft.Maui.Cli.UnitTests/Fakes/FakeAppleProvider.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Maui.Cli.Models;
+using Microsoft.Maui.Cli.Providers.Apple;
+
+namespace Microsoft.Maui.Cli.UnitTests.Fakes;
+
+///
+/// Hand-written fake for used in unit tests.
+/// Set the public properties to control return values; inspect the tracking
+/// lists to verify which methods were called and with what arguments.
+///
+public class FakeAppleProvider : IAppleProvider
+{
+ // --- Configurable return values ---
+
+ public List XcodeInstallations { get; set; } = new();
+ public XcodeInstallation? SelectedXcode { get; set; }
+ public CommandLineToolsStatus CltStatus { get; set; } = new();
+ public List Runtimes { get; set; } = new();
+ public List Simulators { get; set; } = new();
+ public List HealthChecks { get; set; } = new();
+ public List Devices { get; set; } = new();
+
+ public bool SelectXcodeResult { get; set; } = true;
+ public bool BootSimulatorResult { get; set; } = true;
+ public bool ShutdownSimulatorResult { get; set; } = true;
+ public bool DeleteSimulatorResult { get; set; } = true;
+ public string? CreateSimulatorResult { get; set; } = "new-udid";
+
+ // --- Call tracking ---
+
+ public List SelectedXcodePaths { get; } = new();
+ public List BootedSimulators { get; } = new();
+ public List ShutdownSimulators { get; } = new();
+ public List DeletedSimulators { get; } = new();
+ public List<(string Name, string DeviceType, string? Runtime)> CreatedSimulators { get; } = new();
+
+ // --- IAppleProvider implementation ---
+
+ public List GetXcodeInstallations() => XcodeInstallations;
+
+ public XcodeInstallation? GetSelectedXcode() => SelectedXcode;
+
+ public bool SelectXcode(string path)
+ {
+ SelectedXcodePaths.Add(path);
+ return SelectXcodeResult;
+ }
+
+ public CommandLineToolsStatus GetCommandLineToolsStatus() => CltStatus;
+
+ public List GetRuntimes(string? platform = null, bool availableOnly = false)
+ {
+ var result = Runtimes;
+ if (platform is not null)
+ result = result.Where(r => string.Equals(r.Platform, platform, StringComparison.OrdinalIgnoreCase)).ToList();
+ if (availableOnly)
+ result = result.Where(r => r.IsAvailable).ToList();
+ return result;
+ }
+
+ public List GetSimulators(bool availableOnly = false)
+ {
+ return availableOnly ? Simulators.Where(s => s.IsAvailable).ToList() : Simulators;
+ }
+
+ public bool BootSimulator(string udidOrName)
+ {
+ BootedSimulators.Add(udidOrName);
+ return BootSimulatorResult;
+ }
+
+ public bool ShutdownSimulator(string udidOrName)
+ {
+ ShutdownSimulators.Add(udidOrName);
+ return ShutdownSimulatorResult;
+ }
+
+ public bool DeleteSimulator(string udidOrName)
+ {
+ DeletedSimulators.Add(udidOrName);
+ return DeleteSimulatorResult;
+ }
+
+ public string? CreateSimulator(string name, string deviceTypeIdentifier, string? runtimeIdentifier = null)
+ {
+ CreatedSimulators.Add((name, deviceTypeIdentifier, runtimeIdentifier));
+ return CreateSimulatorResult;
+ }
+
+ public List CheckHealth() => HealthChecks;
+
+ public List GetDevices() => Devices;
+}
diff --git a/src/Cli/Microsoft.Maui.Cli.UnitTests/ServiceConfigurationTests.cs b/src/Cli/Microsoft.Maui.Cli.UnitTests/ServiceConfigurationTests.cs
index 0d45df3d..53a57f07 100644
--- a/src/Cli/Microsoft.Maui.Cli.UnitTests/ServiceConfigurationTests.cs
+++ b/src/Cli/Microsoft.Maui.Cli.UnitTests/ServiceConfigurationTests.cs
@@ -4,6 +4,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Cli.DevFlow;
using Microsoft.Maui.Cli.Providers.Android;
+using Microsoft.Maui.Cli.Providers.Apple;
using Microsoft.Maui.Cli.Services;
using Microsoft.Maui.Cli.UnitTests.Fakes;
using Xunit;
@@ -21,6 +22,7 @@ public void CreateServiceProvider_RegistersAllServices()
// Assert
Assert.NotNull(provider.GetService());
Assert.NotNull(provider.GetService());
+ Assert.NotNull(provider.GetService());
Assert.NotNull(provider.GetService());
Assert.NotNull(provider.GetService());
Assert.NotNull(provider.GetService());
@@ -33,9 +35,12 @@ public void CreateServiceProvider_ReturnsSingletonForProviders()
var provider = ServiceConfiguration.CreateServiceProvider();
var android1 = provider.GetService();
var android2 = provider.GetService();
+ var apple1 = provider.GetService();
+ var apple2 = provider.GetService();
// Assert
Assert.Same(android1, android2);
+ Assert.Same(apple1, apple2);
}
[Fact]
@@ -43,13 +48,16 @@ public void CreateTestServiceProvider_UsesProvidedFakes()
{
// Arrange
var fakeAndroid = new FakeAndroidProvider();
+ var fakeApple = new FakeAppleProvider();
// Act
var provider = ServiceConfiguration.CreateTestServiceProvider(
- androidProvider: fakeAndroid);
+ androidProvider: fakeAndroid,
+ appleProvider: fakeApple);
// Assert
Assert.Same(fakeAndroid, provider.GetService());
+ Assert.Same(fakeApple, provider.GetService());
}
[Fact]
@@ -64,6 +72,7 @@ public void CreateTestServiceProvider_CreatesMissingServices()
// Assert - should create real services for everything else
Assert.Same(fakeAndroid, provider.GetService());
+ Assert.NotNull(provider.GetService());
Assert.NotNull(provider.GetService());
}
@@ -74,14 +83,17 @@ public void Program_Services_CanBeOverridden()
{
// Arrange
var fakeAndroid = new FakeAndroidProvider();
+ var fakeApple = new FakeAppleProvider();
var testProvider = ServiceConfiguration.CreateTestServiceProvider(
- androidProvider: fakeAndroid);
+ androidProvider: fakeAndroid,
+ appleProvider: fakeApple);
// Act
Program.Services = testProvider;
// Assert
Assert.Same(fakeAndroid, Program.AndroidProvider);
+ Assert.Same(fakeApple, Program.AppleProvider);
}
finally
{
diff --git a/src/Cli/Microsoft.Maui.Cli/Commands/AppleCommands.cs b/src/Cli/Microsoft.Maui.Cli/Commands/AppleCommands.cs
new file mode 100644
index 00000000..fc015533
--- /dev/null
+++ b/src/Cli/Microsoft.Maui.Cli/Commands/AppleCommands.cs
@@ -0,0 +1,258 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.CommandLine;
+using System.CommandLine.Parsing;
+using Microsoft.Maui.Cli.Output;
+using Microsoft.Maui.Cli.Providers.Apple;
+using Microsoft.Maui.Cli.Utils;
+
+namespace Microsoft.Maui.Cli.Commands;
+
+///
+/// Implementation of 'maui apple' command group.
+/// Sub-commands: xcode, runtime, simulator.
+///
+public static class AppleCommands
+{
+ public static Command Create()
+ {
+ var command = new Command("apple", "Apple platform management (Xcode, simulators, runtimes)");
+
+ command.Add(CreateXcodeCommand());
+ command.Add(CreateRuntimeCommand());
+ command.Add(CreateSimulatorCommand());
+
+ return command;
+ }
+
+ static Command CreateXcodeCommand()
+ {
+ var xcodeCommand = new Command("xcode", "Manage Xcode installations");
+
+ // maui apple xcode list
+ var listCommand = new Command("list", "List installed Xcode versions");
+ listCommand.SetAction((ParseResult parseResult) =>
+ {
+ var formatter = Program.GetFormatter(parseResult);
+
+ if (!PlatformDetector.IsMacOS)
+ {
+ formatter.WriteWarning("Xcode is only available on macOS.");
+ return 1;
+ }
+
+ var appleProvider = Program.AppleProvider;
+ var useJson = parseResult.GetValue(GlobalOptions.JsonOption);
+
+ var installations = appleProvider.GetXcodeInstallations();
+ if (useJson)
+ {
+ formatter.Write(installations);
+ }
+ else
+ {
+ if (!installations.Any())
+ {
+ formatter.WriteWarning("No Xcode installations found.");
+ return 0;
+ }
+
+ if (formatter is SpectreOutputFormatter spectre)
+ {
+ spectre.WriteTable(installations,
+ ("Version", x => x.Version ?? "?"),
+ ("Build", x => x.Build ?? "?"),
+ ("Path", x => x.Path),
+ ("Selected", x => x.IsSelected ? "✓" : ""));
+ }
+ }
+ return 0;
+ });
+
+ xcodeCommand.Add(listCommand);
+ return xcodeCommand;
+ }
+
+ static Command CreateRuntimeCommand()
+ {
+ var runtimeCommand = new Command("runtime", "Manage simulator runtimes");
+
+ // maui apple runtime list [--platform ios]
+ var platformOption = new Option("--platform") { Description = "Filter by platform (iOS, tvOS, watchOS, visionOS)" };
+ var listCommand = new Command("list", "List installed simulator runtimes")
+ {
+ platformOption
+ };
+
+ listCommand.SetAction((ParseResult parseResult) =>
+ {
+ var formatter = Program.GetFormatter(parseResult);
+
+ if (!PlatformDetector.IsMacOS)
+ {
+ formatter.WriteWarning("Runtimes are only available on macOS.");
+ return 1;
+ }
+
+ var appleProvider = Program.AppleProvider;
+ var useJson = parseResult.GetValue(GlobalOptions.JsonOption);
+ var platform = parseResult.GetValue(platformOption);
+
+ var runtimes = appleProvider.GetRuntimes(platform, availableOnly: false);
+ if (useJson)
+ {
+ formatter.Write(runtimes);
+ }
+ else
+ {
+ if (!runtimes.Any())
+ {
+ formatter.WriteWarning("No simulator runtimes found.");
+ return 0;
+ }
+
+ if (formatter is SpectreOutputFormatter spectre)
+ {
+ spectre.WriteTable(runtimes,
+ ("Name", r => r.Name),
+ ("Platform", r => r.Platform ?? "?"),
+ ("Version", r => r.Version ?? "?"),
+ ("Available", r => r.IsAvailable ? "✓" : "✗"),
+ ("Bundled", r => r.IsBundled ? "Yes" : "No"));
+ }
+ }
+ return 0;
+ });
+
+ runtimeCommand.Add(listCommand);
+ return runtimeCommand;
+ }
+
+ static Command CreateSimulatorCommand()
+ {
+ var simCommand = new Command("simulator", "Manage iOS simulators");
+
+ // maui apple simulator list
+ var listCommand = new Command("list", "List simulator devices");
+ listCommand.SetAction((ParseResult parseResult) =>
+ {
+ var formatter = Program.GetFormatter(parseResult);
+
+ if (!PlatformDetector.IsMacOS)
+ {
+ formatter.WriteWarning("Simulators are only available on macOS.");
+ return 1;
+ }
+
+ var appleProvider = Program.AppleProvider;
+ var useJson = parseResult.GetValue(GlobalOptions.JsonOption);
+
+ var simulators = appleProvider.GetSimulators(availableOnly: false);
+ if (useJson)
+ {
+ formatter.Write(simulators);
+ }
+ else
+ {
+ if (!simulators.Any())
+ {
+ formatter.WriteWarning("No simulators found.");
+ return 0;
+ }
+
+ if (formatter is SpectreOutputFormatter spectre)
+ {
+ spectre.WriteTable(simulators,
+ ("Name", s => s.Name),
+ ("UDID", s => s.Udid),
+ ("OS", s => $"{s.Platform} {s.OSVersion}"),
+ ("State", s => s.IsBooted ? "Booted" : s.State ?? "Shutdown"),
+ ("Available", s => s.IsAvailable ? "✓" : "✗"));
+ }
+ }
+ return 0;
+ });
+
+ // maui apple simulator start
+ var startNameArg = new Argument("name-or-udid") { Description = "Simulator name or UDID to boot" };
+ var startCommand = new Command("start", "Boot a simulator") { startNameArg };
+ startCommand.SetAction((ParseResult parseResult) =>
+ {
+ var formatter = Program.GetFormatter(parseResult);
+
+ if (!PlatformDetector.IsMacOS)
+ {
+ formatter.WriteWarning("Simulators are only available on macOS.");
+ return 1;
+ }
+
+ var appleProvider = Program.AppleProvider;
+ var target = parseResult.GetValue(startNameArg);
+
+ var success = appleProvider.BootSimulator(target!);
+ if (success)
+ formatter.WriteSuccess($"Simulator '{target}' booted.");
+ else
+ formatter.WriteWarning($"Failed to boot simulator '{target}'.");
+
+ return success ? 0 : 1;
+ });
+
+ // maui apple simulator stop
+ var stopNameArg = new Argument("name-or-udid") { Description = "Simulator name or UDID to shut down (or 'all')" };
+ var stopCommand = new Command("stop", "Shut down a simulator") { stopNameArg };
+ stopCommand.SetAction((ParseResult parseResult) =>
+ {
+ var formatter = Program.GetFormatter(parseResult);
+
+ if (!PlatformDetector.IsMacOS)
+ {
+ formatter.WriteWarning("Simulators are only available on macOS.");
+ return 1;
+ }
+
+ var appleProvider = Program.AppleProvider;
+ var target = parseResult.GetValue(stopNameArg);
+
+ var success = appleProvider.ShutdownSimulator(target!);
+ if (success)
+ formatter.WriteSuccess($"Simulator '{target}' shut down.");
+ else
+ formatter.WriteWarning($"Failed to shut down simulator '{target}'.");
+
+ return success ? 0 : 1;
+ });
+
+ // maui apple simulator delete
+ var deleteNameArg = new Argument("name-or-udid") { Description = "Simulator name or UDID to delete" };
+ var deleteCommand = new Command("delete", "Delete a simulator") { deleteNameArg };
+ deleteCommand.SetAction((ParseResult parseResult) =>
+ {
+ var formatter = Program.GetFormatter(parseResult);
+
+ if (!PlatformDetector.IsMacOS)
+ {
+ formatter.WriteWarning("Simulators are only available on macOS.");
+ return 1;
+ }
+
+ var appleProvider = Program.AppleProvider;
+ var target = parseResult.GetValue(deleteNameArg);
+
+ var success = appleProvider.DeleteSimulator(target!);
+ if (success)
+ formatter.WriteSuccess($"Simulator '{target}' deleted.");
+ else
+ formatter.WriteWarning($"Failed to delete simulator '{target}'.");
+
+ return success ? 0 : 1;
+ });
+
+ simCommand.Add(listCommand);
+ simCommand.Add(startCommand);
+ simCommand.Add(stopCommand);
+ simCommand.Add(deleteCommand);
+ return simCommand;
+ }
+}
diff --git a/src/Cli/Microsoft.Maui.Cli/Errors/ErrorCodes.cs b/src/Cli/Microsoft.Maui.Cli/Errors/ErrorCodes.cs
index 1c463442..db9a20e5 100644
--- a/src/Cli/Microsoft.Maui.Cli/Errors/ErrorCodes.cs
+++ b/src/Cli/Microsoft.Maui.Cli/Errors/ErrorCodes.cs
@@ -37,6 +37,12 @@ public static class ErrorCodes
public const string AndroidDeviceNotFound = "E2111";
public const string AndroidAvdDeleteFailed = "E2112";
+ // Platform/SDK errors - Apple (E22xx)
+ public const string AppleXcodeNotFound = "E2201";
+ public const string AppleCltNotFound = "E2202";
+ public const string AppleSimctlFailed = "E2203";
+ public const string AppleSimulatorNotFound = "E2204";
+
// Platform/SDK errors - Windows (E23xx)
public const string WindowsSdkNotFound = "E2301";
diff --git a/src/Cli/Microsoft.Maui.Cli/Microsoft.Maui.Cli.csproj b/src/Cli/Microsoft.Maui.Cli/Microsoft.Maui.Cli.csproj
index c366bc37..65a1477b 100644
--- a/src/Cli/Microsoft.Maui.Cli/Microsoft.Maui.Cli.csproj
+++ b/src/Cli/Microsoft.Maui.Cli/Microsoft.Maui.Cli.csproj
@@ -27,6 +27,7 @@
+
diff --git a/src/Cli/Microsoft.Maui.Cli/Program.cs b/src/Cli/Microsoft.Maui.Cli/Program.cs
index cbe2c699..8ce3956f 100644
--- a/src/Cli/Microsoft.Maui.Cli/Program.cs
+++ b/src/Cli/Microsoft.Maui.Cli/Program.cs
@@ -8,6 +8,7 @@
using Microsoft.Maui.Cli.Commands;
using Microsoft.Maui.Cli.Output;
using Microsoft.Maui.Cli.Providers.Android;
+using Microsoft.Maui.Cli.Providers.Apple;
using Microsoft.Maui.Cli.Services;
using Microsoft.Maui.Cli.Utils;
@@ -37,6 +38,7 @@ public static IServiceProvider Services
// Convenience accessors for services
internal static IAndroidProvider AndroidProvider => Services.GetRequiredService();
+ internal static IAppleProvider AppleProvider => Services.GetRequiredService();
internal static IDoctorService DoctorService => Services.GetRequiredService();
internal static IDeviceManager DeviceManager => Services.GetRequiredService();
internal static IJdkManager JdkManager => Services.GetRequiredService();
@@ -89,6 +91,7 @@ internal static RootCommand BuildRootCommand()
// Platform-specific command groups
rootCommand.Add(AndroidCommands.Create());
+ rootCommand.Add(AppleCommands.Create());
// DevFlow automation commands (maui devflow ...)
rootCommand.Add(DevFlow.DevFlowCommands.CreateDevFlowCommand(GlobalOptions.JsonOption));
diff --git a/src/Cli/Microsoft.Maui.Cli/Providers/Apple/AppleProvider.cs b/src/Cli/Microsoft.Maui.Cli/Providers/Apple/AppleProvider.cs
new file mode 100644
index 00000000..c23e7262
--- /dev/null
+++ b/src/Cli/Microsoft.Maui.Cli/Providers/Apple/AppleProvider.cs
@@ -0,0 +1,316 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Maui.Cli.Errors;
+using Microsoft.Maui.Cli.Models;
+using Microsoft.Maui.Cli.Utils;
+using Xamarin.MacDev;
+
+namespace Microsoft.Maui.Cli.Providers.Apple;
+
+///
+/// Apple platform provider backed by Xamarin.Apple.Tools.MaciOS.
+/// Only functional on macOS; returns empty results on other platforms.
+///
+public class AppleProvider : IAppleProvider
+{
+ readonly XcodeManager? _xcodeManager;
+ readonly SimulatorService? _simulatorService;
+ readonly RuntimeService? _runtimeService;
+ readonly CommandLineTools? _commandLineTools;
+
+ public AppleProvider()
+ {
+ if (!PlatformDetector.IsMacOS)
+ return;
+
+ var logger = ConsoleLogger.Instance;
+ _xcodeManager = new XcodeManager(logger);
+ _simulatorService = new SimulatorService(logger);
+ _runtimeService = new RuntimeService(logger);
+ _commandLineTools = new CommandLineTools(logger);
+ }
+
+ public List GetXcodeInstallations()
+ {
+ if (_xcodeManager is null)
+ return new List();
+
+ return _xcodeManager.List().Select(x => new XcodeInstallation
+ {
+ Path = x.Path,
+ Version = x.Version.ToString(),
+ Build = x.Build,
+ IsSelected = x.IsSelected
+ }).ToList();
+ }
+
+ public XcodeInstallation? GetSelectedXcode()
+ {
+ if (_xcodeManager is null)
+ return null;
+
+ var selected = _xcodeManager.GetSelected();
+ if (selected is null)
+ return null;
+
+ return new XcodeInstallation
+ {
+ Path = selected.Path,
+ Version = selected.Version.ToString(),
+ Build = selected.Build,
+ IsSelected = true
+ };
+ }
+
+ public bool SelectXcode(string path)
+ {
+ if (_xcodeManager is null)
+ return false;
+
+ return _xcodeManager.Select(path);
+ }
+
+ public CommandLineToolsStatus GetCommandLineToolsStatus()
+ {
+ if (_commandLineTools is null)
+ return new CommandLineToolsStatus();
+
+ var info = _commandLineTools.Check();
+ return new CommandLineToolsStatus
+ {
+ IsInstalled = info.IsInstalled,
+ Version = info.Version,
+ Path = info.Path
+ };
+ }
+
+ public List GetRuntimes(string? platform = null, bool availableOnly = false)
+ {
+ if (_runtimeService is null)
+ return new List();
+
+ var runtimes = string.IsNullOrEmpty(platform)
+ ? _runtimeService.List(availableOnly)
+ : _runtimeService.ListByPlatform(platform!, availableOnly);
+
+ return runtimes.Select(r => new RuntimeInfo
+ {
+ Name = r.Name,
+ Identifier = r.Identifier,
+ Platform = r.Platform,
+ Version = r.Version,
+ BuildVersion = r.BuildVersion,
+ IsAvailable = r.IsAvailable,
+ IsBundled = r.IsBundled
+ }).ToList();
+ }
+
+ public List GetSimulators(bool availableOnly = false)
+ {
+ if (_simulatorService is null)
+ return new List();
+
+ return _simulatorService.List(availableOnly).Select(s => new SimulatorInfo
+ {
+ Name = s.Name,
+ Udid = s.Udid,
+ State = s.State,
+ Platform = s.Platform,
+ OSVersion = s.OSVersion,
+ RuntimeIdentifier = s.RuntimeIdentifier,
+ DeviceTypeIdentifier = s.DeviceTypeIdentifier,
+ IsAvailable = s.IsAvailable,
+ IsBooted = s.IsBooted
+ }).ToList();
+ }
+
+ public bool BootSimulator(string udidOrName)
+ {
+ return _simulatorService?.Boot(udidOrName) ?? false;
+ }
+
+ public bool ShutdownSimulator(string udidOrName)
+ {
+ return _simulatorService?.Shutdown(udidOrName) ?? false;
+ }
+
+ public bool DeleteSimulator(string udidOrName)
+ {
+ return _simulatorService?.Delete(udidOrName) ?? false;
+ }
+
+ public string? CreateSimulator(string name, string deviceTypeIdentifier, string? runtimeIdentifier = null)
+ {
+ return _simulatorService?.Create(name, deviceTypeIdentifier, runtimeIdentifier);
+ }
+
+ public List CheckHealth()
+ {
+ var checks = new List();
+
+ if (!PlatformDetector.IsMacOS)
+ {
+ checks.Add(new HealthCheck
+ {
+ Category = "apple",
+ Name = "Platform",
+ Status = CheckStatus.Skipped,
+ Message = "Apple checks only available on macOS"
+ });
+ return checks;
+ }
+
+ // Xcode check
+ var xcode = _xcodeManager?.GetBest();
+ if (xcode is not null)
+ {
+ checks.Add(new HealthCheck
+ {
+ Category = "apple",
+ Name = "Xcode",
+ Status = CheckStatus.Ok,
+ Message = $"Xcode {xcode.Version} ({xcode.Build}) at {xcode.Path}",
+ Details = new Dictionary
+ {
+ ["version"] = xcode.Version.ToString(),
+ ["build"] = xcode.Build,
+ ["path"] = xcode.Path,
+ ["selected"] = xcode.IsSelected
+ }
+ });
+ }
+ else
+ {
+ checks.Add(new HealthCheck
+ {
+ Category = "apple",
+ Name = "Xcode",
+ Status = CheckStatus.Error,
+ Message = "Xcode not found. Install Xcode from the App Store.",
+ Fix = new FixInfo
+ {
+ IssueId = ErrorCodes.AppleXcodeNotFound,
+ Description = "Install Xcode from the Mac App Store",
+ AutoFixable = false,
+ ManualSteps = new[] { "Open the Mac App Store and install Xcode" }
+ }
+ });
+ }
+
+ // Command Line Tools check
+ var clt = _commandLineTools?.Check();
+ if (clt is not null && clt.IsInstalled)
+ {
+ checks.Add(new HealthCheck
+ {
+ Category = "apple",
+ Name = "Command Line Tools",
+ Status = CheckStatus.Ok,
+ Message = $"CLT {clt.Version ?? "installed"} at {clt.Path}"
+ });
+ }
+ else
+ {
+ checks.Add(new HealthCheck
+ {
+ Category = "apple",
+ Name = "Command Line Tools",
+ Status = CheckStatus.Warning,
+ Message = "Xcode Command Line Tools not found",
+ Fix = new FixInfo
+ {
+ IssueId = ErrorCodes.AppleCltNotFound,
+ Description = "Install Command Line Tools",
+ AutoFixable = true,
+ Command = "xcode-select --install"
+ }
+ });
+ }
+
+ // Simulator runtimes check
+ HealthCheck iosRuntimesCheck;
+ try
+ {
+ var runtimes = _runtimeService?.List(availableOnly: true);
+ if (runtimes is { Count: > 0 })
+ {
+ var iosRuntimes = runtimes.Where(r => r.Platform == "iOS").ToList();
+ iosRuntimesCheck = new HealthCheck
+ {
+ Category = "apple",
+ Name = "iOS Runtimes",
+ Status = iosRuntimes.Count > 0 ? CheckStatus.Ok : CheckStatus.Warning,
+ Message = iosRuntimes.Count > 0
+ ? $"{iosRuntimes.Count} iOS runtime(s) available (latest: {iosRuntimes.OrderByDescending(r => r.Version).First().Name})"
+ : "No iOS runtimes found. Install one via Xcode."
+ };
+ }
+ else
+ {
+ iosRuntimesCheck = new HealthCheck
+ {
+ Category = "apple",
+ Name = "iOS Runtimes",
+ Status = CheckStatus.Warning,
+ Message = "No simulator runtimes found. Install simulator runtimes via Xcode."
+ };
+ }
+ }
+ catch
+ {
+ iosRuntimesCheck = new HealthCheck
+ {
+ Category = "apple",
+ Name = "iOS Runtimes",
+ Status = CheckStatus.Warning,
+ Message = "Unable to determine installed iOS simulator runtimes."
+ };
+ }
+
+ checks.Add(iosRuntimesCheck);
+
+ return checks;
+ }
+
+ public List GetDevices()
+ {
+ if (_simulatorService is null)
+ return new List();
+
+ var sims = _simulatorService.List(availableOnly: true);
+ return sims.Select(s =>
+ {
+ var platform = s.Platform?.ToLowerInvariant() switch
+ {
+ "ios" => Platforms.iOS,
+ "tvos" or "watchos" or "visionos" => s.Platform!.ToLowerInvariant(),
+ _ => Platforms.iOS
+ };
+
+ return new Device
+ {
+ Id = s.Udid,
+ Name = s.Name,
+ Platforms = new[] { platform },
+ Type = DeviceType.Simulator,
+ State = s.IsBooted ? DeviceState.Booted : DeviceState.Shutdown,
+ IsEmulator = true,
+ IsRunning = s.IsBooted,
+ ConnectionType = Models.ConnectionType.Local,
+ EmulatorId = s.Udid,
+ Model = s.DeviceTypeIdentifier,
+ Manufacturer = "Apple",
+ Version = s.OSVersion,
+ VersionName = s.Platform != null ? $"{s.Platform} {s.OSVersion}" : s.OSVersion,
+ Idiom = s.DeviceTypeIdentifier.Contains("iPad") ? DeviceIdiom.Tablet : DeviceIdiom.Phone,
+ Details = new Dictionary
+ {
+ ["runtime"] = s.RuntimeIdentifier,
+ ["device_type"] = s.DeviceTypeIdentifier,
+ ["platform"] = s.Platform ?? ""
+ }
+ };
+ }).ToList();
+ }
+}
diff --git a/src/Cli/Microsoft.Maui.Cli/Providers/Apple/IAppleProvider.cs b/src/Cli/Microsoft.Maui.Cli/Providers/Apple/IAppleProvider.cs
new file mode 100644
index 00000000..b3d1413e
--- /dev/null
+++ b/src/Cli/Microsoft.Maui.Cli/Providers/Apple/IAppleProvider.cs
@@ -0,0 +1,124 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Maui.Cli.Models;
+
+namespace Microsoft.Maui.Cli.Providers.Apple;
+
+///
+/// Interface for Apple platform operations (Xcode, simulators, runtimes).
+/// Wraps the Xamarin.Apple.Tools.MaciOS package APIs.
+///
+public interface IAppleProvider
+{
+ ///
+ /// Lists installed Xcode installations.
+ ///
+ List GetXcodeInstallations();
+
+ ///
+ /// Gets the currently selected Xcode installation.
+ ///
+ XcodeInstallation? GetSelectedXcode();
+
+ ///
+ /// Selects an Xcode installation by path.
+ ///
+ bool SelectXcode(string path);
+
+ ///
+ /// Gets the Command Line Tools installation info.
+ ///
+ CommandLineToolsStatus GetCommandLineToolsStatus();
+
+ ///
+ /// Lists simulator runtimes. Optionally filter by platform (e.g., "iOS").
+ ///
+ List GetRuntimes(string? platform = null, bool availableOnly = false);
+
+ ///
+ /// Lists simulator devices. Optionally filters by availability.
+ ///
+ List GetSimulators(bool availableOnly = false);
+
+ ///
+ /// Boots a simulator device.
+ ///
+ bool BootSimulator(string udidOrName);
+
+ ///
+ /// Shuts down a simulator device. Pass "all" to shut down all.
+ ///
+ bool ShutdownSimulator(string udidOrName);
+
+ ///
+ /// Deletes a simulator device.
+ ///
+ bool DeleteSimulator(string udidOrName);
+
+ ///
+ /// Creates a new simulator device.
+ ///
+ string? CreateSimulator(string name, string deviceTypeIdentifier, string? runtimeIdentifier = null);
+
+ ///
+ /// Gets the health status of Apple tooling (Xcode, CLT, simulators).
+ ///
+ List CheckHealth();
+
+ ///
+ /// Lists simulator devices as models for device manager integration.
+ ///
+ List GetDevices();
+}
+
+///
+/// Information about an Xcode installation.
+///
+public record XcodeInstallation
+{
+ public required string Path { get; init; }
+ public string? Version { get; init; }
+ public string? Build { get; init; }
+ public bool IsSelected { get; init; }
+}
+
+///
+/// Status of the Xcode Command Line Tools installation.
+///
+public record CommandLineToolsStatus
+{
+ public bool IsInstalled { get; init; }
+ public string? Version { get; init; }
+ public string? Path { get; init; }
+}
+
+///
+/// Information about a simulator runtime.
+///
+public record RuntimeInfo
+{
+ public required string Name { get; init; }
+ public required string Identifier { get; init; }
+ public string? Platform { get; init; }
+ public string? Version { get; init; }
+ public string? BuildVersion { get; init; }
+ public bool IsAvailable { get; init; }
+ public bool IsBundled { get; init; }
+}
+
+///
+/// Information about a simulator device.
+///
+public record SimulatorInfo
+{
+ public required string Name { get; init; }
+ public required string Udid { get; init; }
+ public string? State { get; init; }
+ public string? Platform { get; init; }
+ public string? OSVersion { get; init; }
+ public string? RuntimeIdentifier { get; init; }
+ public string? DeviceTypeIdentifier { get; init; }
+ public bool IsAvailable { get; init; }
+ public bool IsBooted { get; init; }
+}
diff --git a/src/Cli/Microsoft.Maui.Cli/ServiceConfiguration.cs b/src/Cli/Microsoft.Maui.Cli/ServiceConfiguration.cs
index 4b916519..93407694 100644
--- a/src/Cli/Microsoft.Maui.Cli/ServiceConfiguration.cs
+++ b/src/Cli/Microsoft.Maui.Cli/ServiceConfiguration.cs
@@ -5,6 +5,7 @@
using Microsoft.Maui.Cli.DevFlow;
using Microsoft.Maui.Cli.Output;
using Microsoft.Maui.Cli.Providers.Android;
+using Microsoft.Maui.Cli.Providers.Apple;
using Microsoft.Maui.Cli.Services;
using Microsoft.Maui.Cli.Utils;
@@ -34,6 +35,9 @@ public static void ConfigureServices(IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
+ // Apple providers
+ services.AddSingleton();
+
// Core services
services.AddSingleton();
services.AddSingleton();
@@ -51,6 +55,7 @@ public static void ConfigureServices(IServiceCollection services)
///
public static IServiceProvider CreateTestServiceProvider(
IAndroidProvider? androidProvider = null,
+ IAppleProvider? appleProvider = null,
IJdkManager? jdkManager = null,
IDoctorService? doctorService = null,
IDeviceManager? deviceManager = null,
@@ -69,6 +74,11 @@ public static IServiceProvider CreateTestServiceProvider(
else
services.AddSingleton();
+ if (appleProvider != null)
+ services.AddSingleton(appleProvider);
+ else
+ services.AddSingleton();
+
if (doctorService != null)
services.AddSingleton(doctorService);
else
diff --git a/src/Cli/Microsoft.Maui.Cli/Services/DeviceManager.cs b/src/Cli/Microsoft.Maui.Cli/Services/DeviceManager.cs
index bdbb2c02..e310e4e5 100644
--- a/src/Cli/Microsoft.Maui.Cli/Services/DeviceManager.cs
+++ b/src/Cli/Microsoft.Maui.Cli/Services/DeviceManager.cs
@@ -4,6 +4,7 @@
using Microsoft.Maui.Cli.Errors;
using Microsoft.Maui.Cli.Models;
using Microsoft.Maui.Cli.Providers.Android;
+using Microsoft.Maui.Cli.Providers.Apple;
using Microsoft.Maui.Cli.Utils;
namespace Microsoft.Maui.Cli.Services;
@@ -14,10 +15,12 @@ namespace Microsoft.Maui.Cli.Services;
public class DeviceManager : IDeviceManager
{
readonly IAndroidProvider? _androidProvider;
+ readonly IAppleProvider? _appleProvider;
- public DeviceManager(IAndroidProvider? androidProvider = null)
+ public DeviceManager(IAndroidProvider? androidProvider = null, IAppleProvider? appleProvider = null)
{
_androidProvider = androidProvider;
+ _appleProvider = appleProvider;
}
public async Task> GetAllDevicesAsync(CancellationToken cancellationToken = default)
@@ -108,8 +111,12 @@ public async Task> GetAllDevicesAsync(CancellationToken ca
}
}
- // TODO: Get Apple devices when AppleProvider is implemented
- // TODO: Get Windows devices when WindowsProvider is implemented
+ // Get Apple devices (simulators) when on macOS
+ if (_appleProvider != null)
+ {
+ var appleDevices = _appleProvider.GetDevices();
+ devices.AddRange(appleDevices);
+ }
return devices;
}
diff --git a/src/Cli/Microsoft.Maui.Cli/Services/DoctorService.cs b/src/Cli/Microsoft.Maui.Cli/Services/DoctorService.cs
index 8aa7569b..a462d528 100644
--- a/src/Cli/Microsoft.Maui.Cli/Services/DoctorService.cs
+++ b/src/Cli/Microsoft.Maui.Cli/Services/DoctorService.cs
@@ -4,6 +4,7 @@
using Microsoft.Maui.Cli.Errors;
using Microsoft.Maui.Cli.Models;
using Microsoft.Maui.Cli.Providers.Android;
+using Microsoft.Maui.Cli.Providers.Apple;
using Microsoft.Maui.Cli.Utils;
namespace Microsoft.Maui.Cli.Services;
@@ -14,10 +15,12 @@ namespace Microsoft.Maui.Cli.Services;
public class DoctorService : IDoctorService
{
readonly IAndroidProvider? _androidProvider;
+ readonly IAppleProvider? _appleProvider;
- public DoctorService(IAndroidProvider? androidProvider = null)
+ public DoctorService(IAndroidProvider? androidProvider = null, IAppleProvider? appleProvider = null)
{
_androidProvider = androidProvider;
+ _appleProvider = appleProvider;
}
public async Task RunAllChecksAsync(CancellationToken cancellationToken = default)
@@ -37,6 +40,13 @@ public async Task RunAllChecksAsync(CancellationToken cancellation
checks.AddRange(androidChecks);
}
+ // Apple checks (macOS only)
+ if (_appleProvider != null)
+ {
+ var appleChecks = _appleProvider.CheckHealth();
+ checks.AddRange(appleChecks);
+ }
+
// Windows checks (Windows only)
if (PlatformDetector.IsWindows)
{
@@ -65,6 +75,23 @@ public async Task RunCategoryChecksAsync(string category, Cancella
}
break;
+ case "apple":
+ if (_appleProvider != null)
+ {
+ checks.AddRange(_appleProvider.CheckHealth());
+ }
+ else if (!PlatformDetector.IsMacOS)
+ {
+ checks.Add(new HealthCheck
+ {
+ Category = "apple",
+ Name = "Platform",
+ Status = CheckStatus.Skipped,
+ Message = "Apple checks only available on macOS"
+ });
+ }
+ break;
+
case "windows":
if (PlatformDetector.IsWindows)
{
@@ -85,7 +112,7 @@ public async Task RunCategoryChecksAsync(string category, Cancella
default:
throw new MauiToolException(
ErrorCodes.InvalidArgument,
- $"Unknown category: {category}. Valid categories: dotnet, android, windows");
+ $"Unknown category: {category}. Valid categories: dotnet, android, apple, windows");
}
return CreateReport(checks);