From b95faa68951596a19bbb218c68d9fa9de84ae1db Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Tue, 17 Feb 2026 17:03:44 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20automatic=20fix=20during=20st?= =?UTF-8?q?artup=20(sudoers=20step=20only=20for=20now)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 4 +- .../xcschemes/PHP Monitor EAP.xcscheme | 2 +- .../PHP Monitor Self-Updater.xcscheme | 2 +- .../xcschemes/PHP Monitor.xcscheme | 2 +- .../xcschemes/Unit Tests.xcscheme | 2 +- phpmon/Domain/App/EnvironmentCheck.swift | 3 ++ phpmon/Domain/App/Startup.swift | 53 +++++++++++++++++-- .../Packagist/ValetUpgrader.swift | 16 +++--- phpmon/en.lproj/Localizable.strings | 5 ++ 9 files changed, 68 insertions(+), 21 deletions(-) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index adf86923..df7c0f33 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -2621,7 +2621,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 2620; + LastUpgradeCheck = 2630; ORGANIZATIONNAME = "Nico Verbruggen"; TargetAttributes = { C406A5EF298AD2CE00B5B85A = { @@ -4696,7 +4696,7 @@ repositoryURL = "https://github.com/nicoverbruggen/NVAlert"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.0.0; + minimumVersion = 2.1.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme index 147ae0be..f0894b86 100644 --- a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme @@ -1,6 +1,6 @@ Bool + let fixCommand: ((_ container: Container) async throws -> Void)? let name: String let titleText: String let subtitleText: String @@ -23,6 +24,7 @@ struct EnvironmentCheck { init( command: @escaping (_ container: Container) async -> Bool, + fix: ((_ container: Container) async throws -> Void)? = nil, name: String, titleText: String, subtitleText: String, @@ -31,6 +33,7 @@ struct EnvironmentCheck { requiresAppRestart: Bool = false, ) { self.command = command + self.fixCommand = fix self.name = name self.titleText = titleText self.subtitleText = subtitleText diff --git a/phpmon/Domain/App/Startup.swift b/phpmon/Domain/App/Startup.swift index 65614294..e357a79d 100644 --- a/phpmon/Domain/App/Startup.swift +++ b/phpmon/Domain/App/Startup.swift @@ -44,7 +44,26 @@ class Startup { // If we get here, something's gone wrong and the check has failed... Log.info("[FAIL] \(check.name) (\(start.milliseconds) ms)") - await showAlert(for: check) + + // We will present the user with an option (potentially) + let outcome = await showAlert(for: check) + + if outcome == .shouldRunFix { + // First, validate there's a fix + guard let command = check.fixCommand else { + return false + } + + // We will try to run the fix, it may fail! + do { + try await command(App.shared.container) + return await check.succeeds() + } catch { + // our fix failed :( + return false + } + } + return false } } else { @@ -59,12 +78,17 @@ class Startup { return true } + enum EnvironmentAlertOutcome { + case shouldRunFix + case shouldRetryStartup + } + /** Displays an alert for a particular check. There are two types of alerts: - ones that require an app restart, which prompt the user to exit the app - ones that allow the app to continue, which allow the user to retry */ - @MainActor private func showAlert(for check: EnvironmentCheck) { + @MainActor private func showAlert(for check: EnvironmentCheck) -> EnvironmentAlertOutcome { // Ensure that the timeout does not fire until we restart Self.startupTimer?.invalidate() @@ -80,14 +104,30 @@ class Startup { }).show(urgency: .bringToFront) } - NVAlert() + // Verify if an automatic fix is available + let hasAutomaticFix = check.fixCommand != nil + + // Present an alert with one or two buttons (depending on fix) + let outcome = NVAlert() .withInformation( title: check.titleText, subtitle: check.subtitleText, description: check.descriptionText ) - .withPrimary(text: "generic.ok".localized) - .show(urgency: .bringToFront) + .withPrimary(text: hasAutomaticFix ? "startup.fix_for_me".localized : "startup.fix_manually".localized) + .withSecondary(if: hasAutomaticFix, text: "startup.fix_manually".localized) + .withTertiary(if: hasAutomaticFix, text: "", action: { _ in + NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions) + }) + .runModal(urgency: .bringToFront) + + // If there's an automatic fix and we chose to fix it, return outcome + if hasAutomaticFix && outcome == .alertFirstButtonReturn { + return .shouldRunFix + } + + // In any other situation, we will require a retry of the startup + return .shouldRetryStartup } // MARK: - Check (List) @@ -212,6 +252,9 @@ class Startup { .pipe("cat /private/etc/sudoers.d/brew") .out.contains(container.paths.brew) }, + fix: { container in + try sudo("export USER=\(container.paths.whoami) && export PATH=/bin:/usr/bin:\(container.paths.binPath) && valet trust") + }, name: "`/private/etc/sudoers.d/brew` contains brew", titleText: "startup.errors.sudoers_brew.title".localized, subtitleText: "startup.errors.sudoers_brew.subtitle".localized, diff --git a/phpmon/Domain/Integrations/Packagist/ValetUpgrader.swift b/phpmon/Domain/Integrations/Packagist/ValetUpgrader.swift index 00664f66..3bef26f8 100644 --- a/phpmon/Domain/Integrations/Packagist/ValetUpgrader.swift +++ b/phpmon/Domain/Integrations/Packagist/ValetUpgrader.swift @@ -73,7 +73,7 @@ class ValetUpgrader { } @MainActor private static func notifyAboutCompletion() { - return NVAlert().withInformation( + NVAlert().withInformation( title: "valet_upgraded.title".localized, subtitle: "valet_upgraded.subtitle".localized, description: "valet_upgraded.description".localized, @@ -85,7 +85,7 @@ class ValetUpgrader { } @MainActor private static func notifyAboutUpgrade(latest: String, constraint: String, passing: Bool) { - let alert = NVAlert().withInformation( + return NVAlert().withInformation( title: "valet_upgrade_available.title".localized, subtitle: "valet_upgrade_available.subtitle".localized(latest), description: passing @@ -97,13 +97,9 @@ class ValetUpgrader { ValetUpgrader.upgradeValet() }) .withSecondary(text: "valet_upgrade_available.cancel".localized) - - if !passing { - _ = alert.withTertiary(text: "valet_upgrade_available.open_composer".localized, action: { _ in - MainMenu.shared.openGlobalComposerFolder() - }) - } - - alert.show(urgency: .bringToFront) + .withTertiary(if: !passing, text: "valet_upgrade_available.open_composer".localized, action: { _ in + MainMenu.shared.openGlobalComposerFolder() + }) + .show(urgency: .bringToFront) } } diff --git a/phpmon/en.lproj/Localizable.strings b/phpmon/en.lproj/Localizable.strings index 043b1ffc..406efb2a 100644 --- a/phpmon/en.lproj/Localizable.strings +++ b/phpmon/en.lproj/Localizable.strings @@ -958,3 +958,8 @@ PHP Monitor will tell Valet to unsecure and re-secure all expired domains for yo "alert.enable_integrations.desc" = "If you did not trigger this via Alfred or Raycast, there may be another application trying to control PHP Monitor.\n\nIn such a case, I recommend keeping this integration turned off, unless you are fine with another third party app controlling PHP Monitor for you, which could present a potential security risk."; "alert.enable_integrations.ok" = "Allow Integrations"; "alert.enable_integrations.cancel" = "Don't Allow"; + +// AUTOMATIC FIXES AT STARTUP + +"startup.fix_for_me" = "Fix For Me"; +"startup.fix_manually" = "I Fixed It";