mirror of
				https://github.com/maxgoedjen/secretive.git
				synced 2025-11-04 01:10:56 +00:00 
			
		
		
		
	Merge branch 'main' into maxgoedjen-patch-1
This commit is contained in:
		
						commit
						b82a52f172
					
				
							
								
								
									
										24
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							@ -12,6 +12,7 @@ jobs:
 | 
			
		||||
      id-token: write
 | 
			
		||||
      contents: write
 | 
			
		||||
      attestations: write
 | 
			
		||||
      actions: read
 | 
			
		||||
    timeout-minutes: 10
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v5
 | 
			
		||||
@ -36,20 +37,27 @@ jobs:
 | 
			
		||||
            sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
 | 
			
		||||
    - name: Build
 | 
			
		||||
      run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
 | 
			
		||||
    - name: Create ZIP
 | 
			
		||||
    - name: Move to Artifact Folder
 | 
			
		||||
      run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
 | 
			
		||||
    - name: Upload App to Artifacts
 | 
			
		||||
      id: upload
 | 
			
		||||
      uses: actions/upload-artifact@v4
 | 
			
		||||
      with:
 | 
			
		||||
        name: Secretive
 | 
			
		||||
        path: Artifact
 | 
			
		||||
    - name: Download Zipped Artifact
 | 
			
		||||
      id: download
 | 
			
		||||
      env: 
 | 
			
		||||
        ZIP_ID: ${{ steps.upload.outputs.artifact-id }}
 | 
			
		||||
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
      run: |
 | 
			
		||||
            ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
 | 
			
		||||
            curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \
 | 
			
		||||
            https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.zip
 | 
			
		||||
    - name: Notarize
 | 
			
		||||
      env: 
 | 
			
		||||
        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
 | 
			
		||||
        APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
 | 
			
		||||
      run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
 | 
			
		||||
    - name: Upload App to Artifacts
 | 
			
		||||
      id: upload
 | 
			
		||||
      uses: actions/upload-artifact@v4
 | 
			
		||||
      with:
 | 
			
		||||
        name: Secretive.zip
 | 
			
		||||
        path: Secretive.zip
 | 
			
		||||
    - name: Attest
 | 
			
		||||
      id: attest
 | 
			
		||||
      uses: actions/attest-build-provenance@v2
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										36
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@ -32,6 +32,7 @@ jobs:
 | 
			
		||||
      id-token: write
 | 
			
		||||
      contents: write
 | 
			
		||||
      attestations: write
 | 
			
		||||
      actions: read
 | 
			
		||||
    runs-on: macos-26
 | 
			
		||||
    timeout-minutes: 10
 | 
			
		||||
    steps:
 | 
			
		||||
@ -58,33 +59,40 @@ jobs:
 | 
			
		||||
            sed -i '' -e "s/GITHUB_BUILD_URL/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
 | 
			
		||||
    - name: Build
 | 
			
		||||
      run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
 | 
			
		||||
    - name: Create ZIP
 | 
			
		||||
      run: |
 | 
			
		||||
            ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
 | 
			
		||||
    - name: Notarize
 | 
			
		||||
      env: 
 | 
			
		||||
        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
 | 
			
		||||
        APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
 | 
			
		||||
      run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
 | 
			
		||||
    - name: Move to Artifact Folder
 | 
			
		||||
      run: mkdir Artifact; cp -r Archive.xcarchive/Products/Applications/Secretive.app Artifact
 | 
			
		||||
    - name: Upload App to Artifacts
 | 
			
		||||
      id: upload
 | 
			
		||||
      uses: actions/upload-artifact@v4
 | 
			
		||||
      with:
 | 
			
		||||
        name: Secretive.zip
 | 
			
		||||
        path: Secretive.zip
 | 
			
		||||
        path: Artifact
 | 
			
		||||
    - name: Download Zipped Artifact
 | 
			
		||||
      id: download
 | 
			
		||||
      env: 
 | 
			
		||||
        ZIP_ID: ${{ steps.upload.outputs.artifact-id }}
 | 
			
		||||
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
      run: |
 | 
			
		||||
            curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \
 | 
			
		||||
            https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.zip
 | 
			
		||||
    - name: Notarize
 | 
			
		||||
      env: 
 | 
			
		||||
        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
 | 
			
		||||
        APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
 | 
			
		||||
      run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
 | 
			
		||||
    - name: Attest
 | 
			
		||||
      id: attest
 | 
			
		||||
      uses: actions/attest-build-provenance@v2
 | 
			
		||||
      with:
 | 
			
		||||
        subject-path: "Secretive.zip"
 | 
			
		||||
    - name: Create Release
 | 
			
		||||
      run: |
 | 
			
		||||
            sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md
 | 
			
		||||
            sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md
 | 
			
		||||
            gh release create $TAG_NAME -d -F .github/templates/release.md
 | 
			
		||||
            gh release upload $TAG_NAME Secretive.zip
 | 
			
		||||
      env:
 | 
			
		||||
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
        TAG_NAME: ${{ github.ref }}
 | 
			
		||||
        RUN_ID: ${{ github.run_id }}
 | 
			
		||||
        ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }}
 | 
			
		||||
      run: |
 | 
			
		||||
            sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md
 | 
			
		||||
            sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md
 | 
			
		||||
            gh release create $TAG_NAME -d -F .github/templates/release.md
 | 
			
		||||
            gh release upload $TAG_NAME Secretive.zip
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -47,6 +47,7 @@ extension Agent {
 | 
			
		||||
                logger.debug("Agent returned \(SSHAgent.Response.agentSignResponse.debugDescription)")
 | 
			
		||||
            case .unknown(let value):
 | 
			
		||||
                logger.error("Agent received unknown request of type \(value).")
 | 
			
		||||
                throw UnhandledRequestError()
 | 
			
		||||
            default:
 | 
			
		||||
                logger.debug("Agent received valid request of type \(request.debugDescription), but not currently supported.")
 | 
			
		||||
                throw UnhandledRequestError()
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,6 @@
 | 
			
		||||
		504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
 | 
			
		||||
		504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; };
 | 
			
		||||
		50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
 | 
			
		||||
		50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
 | 
			
		||||
		50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
 | 
			
		||||
		50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
 | 
			
		||||
		50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
 | 
			
		||||
@ -194,7 +193,6 @@
 | 
			
		||||
		504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
 | 
			
		||||
		504789222E697DD300B4556F /* BoxBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxBackgroundStyle.swift; sourceTree = "<group>"; };
 | 
			
		||||
		50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
 | 
			
		||||
		50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
 | 
			
		||||
		5059933F2E7A3B5B0092CFFA /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = "<group>"; };
 | 
			
		||||
		50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
 | 
			
		||||
		50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
 | 
			
		||||
@ -448,7 +446,6 @@
 | 
			
		||||
				508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
 | 
			
		||||
				5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
 | 
			
		||||
				50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
 | 
			
		||||
				50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
 | 
			
		||||
			);
 | 
			
		||||
			path = Controllers;
 | 
			
		||||
			sourceTree = "<group>";
 | 
			
		||||
@ -707,7 +704,6 @@
 | 
			
		||||
				5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
 | 
			
		||||
				50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
 | 
			
		||||
				50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
 | 
			
		||||
				50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
 | 
			
		||||
				5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
 | 
			
		||||
				50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
 | 
			
		||||
				50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import Brief
 | 
			
		||||
@main
 | 
			
		||||
struct Secretive: App {
 | 
			
		||||
    
 | 
			
		||||
    @Environment(\.agentStatusChecker) var agentStatusChecker
 | 
			
		||||
    @Environment(\.agentLaunchController) var agentLaunchController
 | 
			
		||||
    @Environment(\.justUpdatedChecker) var justUpdatedChecker
 | 
			
		||||
 | 
			
		||||
    @SceneBuilder var body: some Scene {
 | 
			
		||||
@ -15,14 +15,16 @@ struct Secretive: App {
 | 
			
		||||
            ContentView()
 | 
			
		||||
                .environment(EnvironmentValues._secretStoreList)
 | 
			
		||||
                .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
 | 
			
		||||
                    @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
 | 
			
		||||
                    guard hasRunSetup else { return }
 | 
			
		||||
                    agentStatusChecker.check()
 | 
			
		||||
                    if agentStatusChecker.running && justUpdatedChecker.justUpdatedBuild {
 | 
			
		||||
                        // Relaunch the agent, since it'll be running from earlier update still
 | 
			
		||||
                        reinstallAgent()
 | 
			
		||||
                    } else if !agentStatusChecker.running && !agentStatusChecker.developmentBuild {
 | 
			
		||||
                        forceLaunchAgent()
 | 
			
		||||
                    Task {
 | 
			
		||||
                        @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
 | 
			
		||||
                        @AppStorage("explicitlyDisabled") var explicitlyDisabled = false
 | 
			
		||||
                        guard hasRunSetup && !explicitlyDisabled else { return }
 | 
			
		||||
                        agentLaunchController.check()
 | 
			
		||||
                        guard !agentLaunchController.developmentBuild else { return }
 | 
			
		||||
                        if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running {
 | 
			
		||||
                            // Relaunch the agent, since it'll be running from earlier update still
 | 
			
		||||
                            try await agentLaunchController.forceLaunch()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
@ -79,30 +81,6 @@ extension Secretive {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension Secretive {
 | 
			
		||||
 | 
			
		||||
    private func reinstallAgent() {
 | 
			
		||||
        Task {
 | 
			
		||||
            _ = await LaunchAgentController().install()
 | 
			
		||||
            try? await Task.sleep(for: .seconds(1))
 | 
			
		||||
            agentStatusChecker.check()
 | 
			
		||||
            if !agentStatusChecker.running {
 | 
			
		||||
                forceLaunchAgent()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func forceLaunchAgent() {
 | 
			
		||||
        // We've run setup, we didn't just update, launchd is just not doing it's thing.
 | 
			
		||||
        // Force a launch directly.
 | 
			
		||||
        Task {
 | 
			
		||||
            _ = await LaunchAgentController().forceLaunch()
 | 
			
		||||
            agentStatusChecker.check()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private enum Constants {
 | 
			
		||||
    static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")!
 | 
			
		||||
}
 | 
			
		||||
@ -121,8 +99,8 @@ extension EnvironmentValues {
 | 
			
		||||
        return list
 | 
			
		||||
    }()
 | 
			
		||||
 | 
			
		||||
    private static let _agentStatusChecker = AgentStatusChecker()
 | 
			
		||||
    @Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
 | 
			
		||||
    private static let _agentLaunchController = AgentLaunchController()
 | 
			
		||||
    @Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
 | 
			
		||||
    private static let _updater: any UpdaterProtocol = {
 | 
			
		||||
        @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
 | 
			
		||||
        return Updater(checkOnLaunch: hasRunSetup)
 | 
			
		||||
 | 
			
		||||
@ -2,18 +2,25 @@ import Foundation
 | 
			
		||||
import AppKit
 | 
			
		||||
import SecretKit
 | 
			
		||||
import Observation
 | 
			
		||||
import OSLog
 | 
			
		||||
import ServiceManagement
 | 
			
		||||
 | 
			
		||||
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
 | 
			
		||||
@MainActor protocol AgentLaunchControllerProtocol: Observable, Sendable {
 | 
			
		||||
    var running: Bool { get }
 | 
			
		||||
    var developmentBuild: Bool { get }
 | 
			
		||||
    var process: NSRunningApplication? { get }
 | 
			
		||||
    func check()
 | 
			
		||||
    func install() async throws
 | 
			
		||||
    func uninstall() async throws
 | 
			
		||||
    func forceLaunch() async throws
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
 | 
			
		||||
@Observable @MainActor final class AgentLaunchController: AgentLaunchControllerProtocol {
 | 
			
		||||
 | 
			
		||||
    var running: Bool = false
 | 
			
		||||
    var process: NSRunningApplication? = nil
 | 
			
		||||
    private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
 | 
			
		||||
    private let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
 | 
			
		||||
 | 
			
		||||
    nonisolated init() {
 | 
			
		||||
        Task { @MainActor in
 | 
			
		||||
@ -33,7 +40,7 @@ import Observation
 | 
			
		||||
 | 
			
		||||
    // The process corresponding to this instance of Secretive
 | 
			
		||||
    var instanceSecretAgentProcess: NSRunningApplication? {
 | 
			
		||||
        // FIXME: CHECK VERSION
 | 
			
		||||
        // TODO: CHECK VERSION
 | 
			
		||||
        let agents = allSecretAgentProcesses
 | 
			
		||||
        for agent in agents {
 | 
			
		||||
            guard let url = agent.bundleURL else { continue }
 | 
			
		||||
@ -49,6 +56,47 @@ import Observation
 | 
			
		||||
        Bundle.main.bundleURL.isXcodeURL
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func install() async throws {
 | 
			
		||||
        logger.debug("Installing agent")
 | 
			
		||||
        try? await service.unregister()
 | 
			
		||||
        // This is definitely a bit of a "seems to work better" thing but:
 | 
			
		||||
        // Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
 | 
			
		||||
        // and start new?
 | 
			
		||||
        try await Task.sleep(for: .seconds(1))
 | 
			
		||||
        try service.register()
 | 
			
		||||
        try await Task.sleep(for: .seconds(1))
 | 
			
		||||
        check()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func uninstall() async throws {
 | 
			
		||||
        logger.debug("Uninstalling agent")
 | 
			
		||||
        try await Task.sleep(for: .seconds(1))
 | 
			
		||||
        try await service.unregister()
 | 
			
		||||
        try await Task.sleep(for: .seconds(1))
 | 
			
		||||
        check()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func forceLaunch() async throws {
 | 
			
		||||
        logger.debug("Agent is not running, attempting to force launch by reinstalling")
 | 
			
		||||
        try await install()
 | 
			
		||||
        if running {
 | 
			
		||||
            logger.debug("Agent successfully force launched by reinstalling")
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        logger.debug("Agent is not running, attempting to force launch by launching directly")
 | 
			
		||||
        let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
 | 
			
		||||
        let config = NSWorkspace.OpenConfiguration()
 | 
			
		||||
        config.activates = false
 | 
			
		||||
        do {
 | 
			
		||||
            try await NSWorkspace.shared.openApplication(at: url, configuration: config)
 | 
			
		||||
            logger.debug("Agent force launched")
 | 
			
		||||
            try await Task.sleep(for: .seconds(1))
 | 
			
		||||
        } catch {
 | 
			
		||||
            logger.error("Error force launching \(error.localizedDescription)")
 | 
			
		||||
        }
 | 
			
		||||
        check()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension URL {
 | 
			
		||||
 | 
			
		||||
@ -1,65 +0,0 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import ServiceManagement
 | 
			
		||||
import AppKit
 | 
			
		||||
import OSLog
 | 
			
		||||
import SecretKit
 | 
			
		||||
 | 
			
		||||
struct LaunchAgentController {
 | 
			
		||||
    
 | 
			
		||||
    private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
 | 
			
		||||
 | 
			
		||||
    func install() async -> Bool {
 | 
			
		||||
        logger.debug("Installing agent")
 | 
			
		||||
        _ = setEnabled(false)
 | 
			
		||||
        // This is definitely a bit of a "seems to work better" thing but:
 | 
			
		||||
        // Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
 | 
			
		||||
        // and start new?
 | 
			
		||||
        try? await Task.sleep(for: .seconds(1))
 | 
			
		||||
        let result = await MainActor.run {
 | 
			
		||||
            setEnabled(true)
 | 
			
		||||
        }
 | 
			
		||||
        try? await Task.sleep(for: .seconds(1))
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func uninstall() async -> Bool {
 | 
			
		||||
        logger.debug("Uninstalling agent")
 | 
			
		||||
        try? await Task.sleep(for: .seconds(1))
 | 
			
		||||
        let result = await MainActor.run {
 | 
			
		||||
            setEnabled(false)
 | 
			
		||||
        }
 | 
			
		||||
        try? await Task.sleep(for: .seconds(1))
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func forceLaunch() async -> Bool {
 | 
			
		||||
        logger.debug("Agent is not running, attempting to force launch")
 | 
			
		||||
        let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
 | 
			
		||||
        let config = NSWorkspace.OpenConfiguration()
 | 
			
		||||
        config.activates = false
 | 
			
		||||
        do {
 | 
			
		||||
            try await NSWorkspace.shared.openApplication(at: url, configuration: config)
 | 
			
		||||
            logger.debug("Agent force launched")
 | 
			
		||||
            try? await Task.sleep(for: .seconds(1))
 | 
			
		||||
            return true
 | 
			
		||||
        } catch {
 | 
			
		||||
            logger.error("Error force launching \(error.localizedDescription)")
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func setEnabled(_ enabled: Bool) -> Bool {
 | 
			
		||||
        let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
 | 
			
		||||
        do {
 | 
			
		||||
            if enabled {
 | 
			
		||||
                try service.register()
 | 
			
		||||
            } else {
 | 
			
		||||
                try service.unregister()
 | 
			
		||||
            }
 | 
			
		||||
            return true
 | 
			
		||||
        } catch {
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
import AppKit
 | 
			
		||||
 | 
			
		||||
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
 | 
			
		||||
class PreviewAgentLaunchController: AgentLaunchControllerProtocol {
 | 
			
		||||
 | 
			
		||||
    let running: Bool
 | 
			
		||||
    let process: NSRunningApplication?
 | 
			
		||||
@ -15,4 +15,13 @@ class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
 | 
			
		||||
    func check() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func install() async throws {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func uninstall() async throws {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func forceLaunch() async throws {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import SwiftUI
 | 
			
		||||
struct SetupView: View {
 | 
			
		||||
 | 
			
		||||
    @Environment(\.dismiss) private var dismiss
 | 
			
		||||
    @Environment(\.agentLaunchController) private var agentLaunchController
 | 
			
		||||
    @Binding var setupComplete: Bool
 | 
			
		||||
 | 
			
		||||
    @State var showingIntegrations = false
 | 
			
		||||
@ -31,7 +32,7 @@ struct SetupView: View {
 | 
			
		||||
                    ) {
 | 
			
		||||
                        installed = true
 | 
			
		||||
                        Task {
 | 
			
		||||
                            await LaunchAgentController().install()
 | 
			
		||||
                            try? await agentLaunchController.install()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ struct ToolConfigurationView: View {
 | 
			
		||||
 | 
			
		||||
    @State var creating = false
 | 
			
		||||
    @State var selectedSecret: AnySecret?
 | 
			
		||||
    @State var email = ""
 | 
			
		||||
 | 
			
		||||
    init(selectedInstruction: ConfigurationFileInstructions) {
 | 
			
		||||
        self.selectedInstruction = selectedInstruction
 | 
			
		||||
@ -48,6 +49,12 @@ struct ToolConfigurationView: View {
 | 
			
		||||
                                    .tag(secret)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        TextField(text: $email, prompt: Text(.integrationsConfigureUsingEmailPlaceholder)) {
 | 
			
		||||
                            Text(.integrationsConfigureUsingEmailTitle)
 | 
			
		||||
                            Text(.integrationsConfigureUsingEmailSubtitle)
 | 
			
		||||
                                .font(.subheadline)
 | 
			
		||||
                                .foregroundStyle(.secondary)
 | 
			
		||||
                        }
 | 
			
		||||
                    } header: {
 | 
			
		||||
                        Text(.integrationsConfigureUsingSecretHeader)
 | 
			
		||||
                    }
 | 
			
		||||
@ -60,7 +67,7 @@ struct ToolConfigurationView: View {
 | 
			
		||||
                Section {
 | 
			
		||||
                    ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
 | 
			
		||||
                    ForEach(stepGroup.steps, id: \.self.key) { step in
 | 
			
		||||
                        ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) {
 | 
			
		||||
                        ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(placeholdersReplaced(text: String(localized: step)))) {
 | 
			
		||||
                            HStack {
 | 
			
		||||
                                Text(placeholdersReplaced(text: String(localized: step)))
 | 
			
		||||
                                    .padding(8)
 | 
			
		||||
@ -102,9 +109,11 @@ struct ToolConfigurationView: View {
 | 
			
		||||
    func placeholdersReplaced(text: String) -> String {
 | 
			
		||||
        guard let selectedSecret else { return text }
 | 
			
		||||
        let writer = OpenSSHPublicKeyWriter()
 | 
			
		||||
        let gitAllowedSignersString = [email.isEmpty ? String(localized: .integrationsConfigureUsingEmailPlaceholder) : email, writer.openSSHString(secret: selectedSecret)]
 | 
			
		||||
            .joined(separator: " ")
 | 
			
		||||
        let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
 | 
			
		||||
        return text
 | 
			
		||||
            .replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: writer.openSSHString(secret: selectedSecret))
 | 
			
		||||
            .replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: gitAllowedSignersString)
 | 
			
		||||
            .replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,10 @@ import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct AgentStatusView: View {
 | 
			
		||||
 | 
			
		||||
    @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
 | 
			
		||||
    @Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        if agentStatusChecker.running {
 | 
			
		||||
        if agentLaunchController.running {
 | 
			
		||||
            AgentRunningView()
 | 
			
		||||
        } else {
 | 
			
		||||
            AgentNotRunningView()
 | 
			
		||||
@ -14,12 +14,13 @@ struct AgentStatusView: View {
 | 
			
		||||
}
 | 
			
		||||
struct AgentRunningView: View {
 | 
			
		||||
 | 
			
		||||
    @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
 | 
			
		||||
    @Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
 | 
			
		||||
    @AppStorage("explicitlyDisabled") var explicitlyDisabled = false
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        Form {
 | 
			
		||||
            Section {
 | 
			
		||||
                if let process = agentStatusChecker.process {
 | 
			
		||||
                if let process = agentLaunchController.process {
 | 
			
		||||
                    ConfigurationItemView(
 | 
			
		||||
                        title: .agentDetailsLocationTitle,
 | 
			
		||||
                        value: process.bundleURL!.path(),
 | 
			
		||||
@ -53,19 +54,14 @@ struct AgentRunningView: View {
 | 
			
		||||
                        Menu(.agentDetailsRestartAgentButton) {
 | 
			
		||||
                            Button(.agentDetailsDisableAgentButton) {
 | 
			
		||||
                                Task {
 | 
			
		||||
                                    _ = await LaunchAgentController()
 | 
			
		||||
                                    explicitlyDisabled = true
 | 
			
		||||
                                    try? await agentLaunchController
 | 
			
		||||
                                        .uninstall()
 | 
			
		||||
                                    agentStatusChecker.check()
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        } primaryAction: {
 | 
			
		||||
                            Task {
 | 
			
		||||
                                let controller = LaunchAgentController()
 | 
			
		||||
                                let installed = await controller.install()
 | 
			
		||||
                                if !installed {
 | 
			
		||||
                                    _ = await controller.forceLaunch()
 | 
			
		||||
                                }
 | 
			
		||||
                                agentStatusChecker.check()
 | 
			
		||||
                                try? await agentLaunchController.forceLaunch()
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@ -82,9 +78,10 @@ struct AgentRunningView: View {
 | 
			
		||||
 | 
			
		||||
struct AgentNotRunningView: View {
 | 
			
		||||
 | 
			
		||||
    @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
 | 
			
		||||
    @Environment(\.agentLaunchController) private var agentLaunchController
 | 
			
		||||
    @State var triedRestart = false
 | 
			
		||||
    @State var loading = false
 | 
			
		||||
    @AppStorage("explicitlyDisabled") var explicitlyDisabled = false
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        Form {
 | 
			
		||||
@ -100,18 +97,14 @@ struct AgentNotRunningView: View {
 | 
			
		||||
                        if !triedRestart {
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                            Button {
 | 
			
		||||
                                explicitlyDisabled = false
 | 
			
		||||
                                guard !loading else { return }
 | 
			
		||||
                                loading = true
 | 
			
		||||
                                Task {
 | 
			
		||||
                                    let controller = LaunchAgentController()
 | 
			
		||||
                                    let installed = await controller.install()
 | 
			
		||||
                                    if !installed {
 | 
			
		||||
                                        _ = await controller.forceLaunch()
 | 
			
		||||
                                    }
 | 
			
		||||
                                    agentStatusChecker.check()
 | 
			
		||||
                                    try await agentLaunchController.forceLaunch()
 | 
			
		||||
                                    loading = false
 | 
			
		||||
 | 
			
		||||
                                    if !agentStatusChecker.running {
 | 
			
		||||
                                    if !agentLaunchController.running {
 | 
			
		||||
                                        triedRestart = true
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
@ -145,9 +138,9 @@ struct AgentNotRunningView: View {
 | 
			
		||||
 | 
			
		||||
//#Preview {
 | 
			
		||||
//    AgentStatusView()
 | 
			
		||||
//        .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
 | 
			
		||||
//        .environment(\.agentLaunchController, PreviewAgentLaunchController(running: false))
 | 
			
		||||
//}
 | 
			
		||||
//#Preview {
 | 
			
		||||
//    AgentStatusView()
 | 
			
		||||
//        .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
 | 
			
		||||
//        .environment(\.agentLaunchController, PreviewAgentLaunchController(running: true, process: .current))
 | 
			
		||||
//}
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ struct ContentView: View {
 | 
			
		||||
    @Environment(\.openWindow) private var openWindow
 | 
			
		||||
    @Environment(\.secretStoreList) private var storeList
 | 
			
		||||
    @Environment(\.updater) private var updater
 | 
			
		||||
    @Environment(\.agentStatusChecker) private var agentStatusChecker
 | 
			
		||||
    @Environment(\.agentLaunchController) private var agentLaunchController
 | 
			
		||||
 | 
			
		||||
    @AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
 | 
			
		||||
    @State private var showingCreation = false
 | 
			
		||||
@ -127,7 +127,7 @@ extension ContentView {
 | 
			
		||||
            showingAgentInfo = true
 | 
			
		||||
        }, label: {
 | 
			
		||||
            HStack {
 | 
			
		||||
                if agentStatusChecker.running {
 | 
			
		||||
                if agentLaunchController.running {
 | 
			
		||||
                    Text(.agentRunningNoticeTitle)
 | 
			
		||||
                        .font(.headline)
 | 
			
		||||
                        .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
 | 
			
		||||
@ -145,8 +145,8 @@ extension ContentView {
 | 
			
		||||
        })
 | 
			
		||||
        .buttonStyle(
 | 
			
		||||
            ToolbarStatusButtonStyle(
 | 
			
		||||
                lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
 | 
			
		||||
                darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
 | 
			
		||||
                lightColor: agentLaunchController.running ? .black.opacity(0.05) : .red.opacity(0.75),
 | 
			
		||||
                darkColor: agentLaunchController.running ? .white.opacity(0.05) : .red.opacity(0.5),
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        .popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user