MauiSherpa is a .NET 10 MAUI Blazor Hybrid desktop application for managing developer tools:
- Android SDK (packages, emulators, devices)
- Android Keystores (creation, signatures, PEPK export, cloud sync)
- Apple Developer Tools (certificates, profiles, devices, bundle IDs)
- .NET MAUI Doctor (dependency checking and workload management)
- .NET SDK management via
dotnetup(install/update SDKs & runtimes — seedocs/dotnet-sdk-management.md) - GitHub Copilot integration
Platforms: macOS (AppKit), Windows, Linux (GTK)
Bundle Identifier: codes.redth.mauisherpa
MAUI.Sherpa/
├── src/
│ ├── MauiSherpa.Core/ # Business logic, ViewModels, interfaces, services
│ │ ├── Handlers/ # Shiny.Mediator request handlers
│ │ ├── Requests/ # Mediator request records
│ │ ├── Services/ # Service implementations
│ │ ├── ViewModels/ # MVVM ViewModels
│ │ └── Interfaces.cs # All interface definitions
│ ├── MauiSherpa/ # Shared MAUI app with Blazor UI (Windows head)
│ │ ├── Components/ # Reusable Blazor components
│ │ ├── Pages/ # Blazor page components
│ │ ├── Services/ # Platform-specific service implementations
│ │ ├── Platforms/ # Platform-specific code (Windows)
│ │ └── wwwroot/ # Static assets (CSS, JS, index.html)
│ ├── MauiSherpa.MacOS/ # macOS AppKit app head (net10.0-macos)
│ ├── MauiSherpa.LinuxGtk/ # Linux GTK app head
│ └── MauiSherpa.Workloads/ # .NET SDK workload querying library
│ ├── Models/ # Workload data models
│ ├── Services/ # Workload services
│ └── NuGet/ # NuGet client for package queries
├── tests/
│ ├── MauiSherpa.Core.Tests/ # Unit tests for Core library
│ └── MauiSherpa.Workloads.Tests/ # Unit tests for Workloads library
└── docs/ # Documentation
# Build for macOS (AppKit head)
dotnet build src/MauiSherpa.MacOS -f net10.0-macos
# Build for Windows (on Windows only)
dotnet build src/MauiSherpa -f net10.0-windows10.0.19041.0
# Build entire solution (uses default TFM for each project)
dotnet build MauiSherpa.sln
# Run all tests
dotnet test MauiSherpa.sln
# Publish macOS app
dotnet publish src/MauiSherpa.MacOS -f net10.0-macos -c Release
# Publish Windows app
dotnet publish src/MauiSherpa -f net10.0-windows10.0.19041.0 -c Releasesrc/MauiSherpa is the shared UI project and the Windows head. It only builds on Windows — on
macOS and Linux its Build/Rebuild/Publish targets are no-ops so solution builds still work.
IMPORTANT: dotnet run does NOT work for .NET MAUI apps (until .NET 11). Use one of:
# Option 1: Build with -t:Run target (keeps process alive until app exits)
dotnet build src/MauiSherpa.MacOS -f net10.0-macos -t:Run
# Option 2: Build then manually open the .app bundle
dotnet build src/MauiSherpa.MacOS -f net10.0-macos
open "src/MauiSherpa.MacOS/bin/Debug/net10.0-macos/osx-arm64/MAUI Sherpa.app"Always launch from bin/ path, NOT artifacts/. The artifacts/ copy may be stale and missing DLLs.
- Core contains no platform dependencies
- Platform implements interfaces defined in Core
- Services are injected via constructor DI
ViewModels inherit from ViewModelBase → ObservableObject:
public class MyViewModel : ViewModelBase
{
public MyViewModel(IAlertService alertService, ILoggingService logger)
: base(alertService, logger) { }
}Request/Handler pattern with automatic caching:
// Request (in Core/Requests/)
[Cache(AbsoluteExpirationSeconds = 300)]
public record GetDataRequest() : IRequest<IReadOnlyList<Data>>;
// Handler (in Core/Handlers/)
public class GetDataHandler : IRequestHandler<GetDataRequest, IReadOnlyList<Data>>
{
public async Task<IReadOnlyList<Data>> Handle(GetDataRequest request, IMediatorContext context, CancellationToken ct)
{
// Implementation
}
}Handlers must be registered in MauiProgram.cs:
builder.Services.AddSingletonAsImplementedInterfaces<GetDataHandler>();IMPORTANT: Mediator.Request() returns a tuple (IMediatorContext Context, TResult Result) — use .Result to get the value.
In MauiProgram.cs:
builder.Services.AddSingleton<IMyService, MyService>();
builder.Services.AddSingleton<MyViewModel>();| Interface | Purpose |
|---|---|
IAlertService |
Native dialogs and toasts (ShowConfirmAsync, NOT ShowConfirmationAsync) |
ILoggingService |
Structured logging to ~/Library/Application Support/MauiSherpa/logs/ |
INavigationService |
Page navigation |
IDialogService |
Loading indicators, input dialogs, file pickers |
IAndroidSdkService |
Android SDK operations |
IOpenJdkSettingsService |
OpenJDK location detection and override |
IKeystoreService |
Android keystore creation, signatures, PEPK export |
IKeystoreSyncService |
Cloud sync for Android keystores |
IAppleConnectService |
App Store Connect API |
IAppleIdentityService |
Apple credential management |
IDoctorService |
MAUI dependency checking |
IDotnetUpService |
Bootstraps & drives dotnetup to install/update .NET SDKs & runtimes |
ICloudSecretsService |
Cloud secret storage (uses byte[] for values) |
ISecureStorageService |
Local secure storage (Keychain on macOS) |
- Located in
src/MauiSherpa/Pages/ - Use
@injectfor services - Use mediator for cached data:
await Mediator.Request(new GetDataRequest())
OperationModal— Single long-running operation with progressMultiOperationModal— Batch operations with per-item progressProcessExecutionModal— CLI process with terminal output
OperationModalService.RunAsync signature:
RunAsync(string title, string description, Func<IOperationContext, Task<bool>> operation, bool canCancel = true)Both title AND description are required. Callback returns Task<bool>.
Per-page modal CSS: Each Blazor page defines its own .modal-overlay, .modal, .modal-header, .modal-body, .modal-footer CSS in a <style> block. These are NOT global styles. New pages MUST include modal CSS or modals will render inline without overlay/positioning.
All modals use modalInterop.js (wwwroot/js/modalInterop.js) for focus trapping:
- Tab/Shift+Tab cycles through focusable elements within the modal
- Escape closes the modal
- Auto-focuses
.btn-primary:not([disabled])on open
CRITICAL: In Blazor WebView (macOS), browser default Tab navigation does NOT work. All Tab keypresses must be intercepted with preventDefault() and explicit .focus() calls via JS interop.
Global user-select: none is applied on * to prevent accidental text selection in the hybrid app. Selectively re-enabled on: input, textarea, select, code, pre, .mono, .terminal-output, .log-entry, .error-message, .chat-message, and .text-selectable.
Using Font Awesome. Example:
<i class="fa-solid fa-check text-success"></i>- CSS variables defined in
app.csswith.theme-lightand.theme-darkoverrides - Always validate UI changes in BOTH light and dark mode
- Use
var(--text-primary),var(--bg-tertiary),var(--card-bg), etc. — never hardcode colors - Service-specific tags (Google, Firebase, Facebook) need explicit dark mode overrides with
rgba()backgrounds
When making or reviewing UI changes, always verify:
- Padding around elements is consistent and looks polished
- Button alignment is consistent across the app
- Icon usage is appropriate (correct icon, right size)
- Text and button sizes are proportional and readable
- Both light AND dark mode look good
- No XAML — All UI is Blazor (.razor) or C# code
- Nullable enabled — All projects use
<Nullable>enable</Nullable> - Property notification — Use
SetProperty(ref field, value)in ViewModels - Async naming — Suffix async methods with
Async - Records for DTOs — Use record types for data transfer objects
- NuGet packages for APIs — Use official NuGet packages (Octokit, AppStoreConnectClient, etc.) instead of raw HttpClient for external API integrations
App data path: Use AppDataPath.GetAppDataDirectory() which returns ~/Library/Application Support/MauiSherpa/. Do NOT use SpecialFolder.ApplicationData (resolves to ~/Documents/.config/ which is TCC-protected).
Secure storage in Debug: Ad-hoc Debug builds have different code signatures each rebuild, making macOS Keychain entries inaccessible. Debug builds always use fallback file storage (#if DEBUG _usesFallback = true; in SecureStorageService).
File save dialogs: PickSaveFileAsync (native NSSavePanel) creates an empty file at the chosen path. Tools like keytool that refuse to overwrite existing files need the empty file deleted first.
Duplicate native libraries: The app heads reference both MauiSherpa.AppInspector and MauiSherpa.Core, and AppInspector also references Core, so GitHub.Copilot.SDK stages libcopilot_runtime.dylib several times over. The Apple SDK's InstallNameTool task runs its items in parallel against a shared .tmp path and crashes on duplicates, so build/DedupeNativeReferences.targets collapses each destination to one entry. Import it from any new Apple app head.
Logging: Logs saved to ~/Library/Application Support/MauiSherpa/logs/maui-sherpa-{yyyy-MM-dd}.log.
@bind vs JS DOM manipulation: Setting element.value = 'x' via JS does NOT update Blazor two-way binding. Must use the native value setter pattern:
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));Use @bind:event="oninput" on inputs for this to work (requires input events, not change).
The project uses MAUI DevFlow from the unified Microsoft.Maui.Cli tool for AI-assisted debugging. Broker discovery is preferred; no .mauidevflow file is committed in this repo.
# Always run CLI commands from src/MauiSherpa/ for auto port detection
cd src/MauiSherpa
# Check agent connectivity
dotnet maui devflow ui status
# Take screenshots
dotnet maui devflow ui screenshot --output screen.png
# Blazor DOM snapshot (best for AI)
dotnet maui devflow webview snapshot
# Inject dark/light mode for testing (avoids navigating to Settings)
dotnet maui devflow webview Runtime evaluate "document.body.classList.remove('theme-light'); document.body.classList.add('theme-dark'); document.querySelector('.main-layout').classList.remove('theme-light'); document.querySelector('.main-layout').classList.add('theme-dark');"- GitHub Actions runner:
macos-26(NOTmacos-26-arm64) - Xcode: Select
Xcode_26.2.app(NOTXcode_26.2.0.app— the.0variant has broken SDK lookups) - Workflow file:
.github/workflows/build.yml
| Package | Purpose |
|---|---|
| Microsoft.Maui.Controls | MAUI framework |
| Microsoft.AspNetCore.Components.WebView.Maui | Blazor Hybrid |
| Shiny.Mediator | Request/Response with caching |
| AndroidSdk | Android SDK management |
| AppStoreConnectClient | Apple API client |
| FluentValidation | Input validation |
- xUnit for test framework
- Moq for mocking
- FluentAssertions for assertions
Test projects target net10.0 (not platform-specific) for portability.
When debugging UI issues:
- Launch the app, then ask the user to navigate/reproduce the issue before inspecting — the app may require user interaction to reach the desired state
- When using logs for debugging, wait for the user to confirm they've performed the action before checking log files
- Use screenshots (
screencapturefor macOS,xcrun simctl io screenshotfor iOS simulators) to visually verify the app - Always back up modified files before git branching/splitting operations to enable restoration if changes are lost