namespace NucleicApp.Services; public enum OnboardingPhase { Idle, StartingHost, Connecting, Ready, Failed, } /// First-run coordinator for the item-11 core. Component installation and OAuth screens plug /// into the same phase surface once their host intents land; host launch + authenticated local /// sync are real today. public sealed class OnboardingService { private readonly HostdLauncher launcher; private readonly HostConnection connection; public OnboardingPhase Phase { get; private set; } = OnboardingPhase.Idle; public string? Failure { get; private set; } public event Action? PhaseChanged; public OnboardingService(HostdLauncher launcher, HostConnection connection) { this.launcher = launcher; this.connection = connection; } public async Task StartAsync(string rendezvousPath, CancellationToken cancellationToken = default) { try { SetPhase(OnboardingPhase.StartingHost); _ = await launcher.EnsureRunningAsync(cancellationToken).ConfigureAwait(false); SetPhase(OnboardingPhase.Connecting); var ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); void Changed(HostConnectionState state) { if (state == HostConnectionState.Connected) ready.TrySetResult(); else if (state == HostConnectionState.Failed) ready.TrySetException(new InvalidOperationException( connection.LastProtocolState?.Message ?? "local host connection failed")); } connection.StateChanged += Changed; try { await connection.StartLocalAsync(rendezvousPath, cancellationToken).ConfigureAwait(false); await ready.Task.WaitAsync(TimeSpan.FromSeconds(30), cancellationToken).ConfigureAwait(false); } finally { connection.StateChanged -= Changed; } SetPhase(OnboardingPhase.Ready); } catch (Exception error) { Failure = error.Message; SetPhase(OnboardingPhase.Failed); throw; } } private void SetPhase(OnboardingPhase phase) { if (Phase == phase) return; Phase = phase; PhaseChanged?.Invoke(phase); } }