Compare commits
No commits in common. "main" and "v3.0.0" have entirely different histories.
BIN
.github/readme/app-dark.png
vendored
Before Width: | Height: | Size: 668 KiB After Width: | Height: | Size: 520 KiB |
BIN
.github/readme/app-light.png
vendored
Before Width: | Height: | Size: 618 KiB After Width: | Height: | Size: 519 KiB |
BIN
.github/readme/localize_add.png
vendored
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
.github/readme/localize_sidebar.png
vendored
Normal file
After Width: | Height: | Size: 162 KiB |
BIN
.github/readme/localize_translate.png
vendored
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
.github/readme/notification.png
vendored
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 1.4 MiB |
BIN
.github/readme/touchid.png
vendored
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 259 KiB |
26
.github/workflows/nightly.yml
vendored
@ -12,7 +12,6 @@ jobs:
|
||||
id-token: write
|
||||
contents: write
|
||||
attestations: write
|
||||
actions: read
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@ -34,30 +33,23 @@ jobs:
|
||||
DATE=$(date "+%Y-%m-%d")
|
||||
sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0_nightly-$DATE/g" Sources/Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
||||
- name: Build
|
||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||
- 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 }}
|
||||
- name: Create ZIP
|
||||
run: |
|
||||
curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \
|
||||
https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.zip
|
||||
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: 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
|
||||
|
39
.github/workflows/release.yml
vendored
@ -32,7 +32,6 @@ jobs:
|
||||
id-token: write
|
||||
contents: write
|
||||
attestations: write
|
||||
actions: read
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
@ -56,43 +55,37 @@ jobs:
|
||||
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
|
||||
sed -i '' -e "s/GITHUB_CI_VERSION/$CLEAN_TAG/g" Sources/Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_BUILD_URL/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Config/Config.xcconfig
|
||||
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: 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: Artifact
|
||||
- name: Download Zipped Artifact
|
||||
id: download
|
||||
env:
|
||||
ZIP_ID: ${{ steps.upload.outputs.artifact-id }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create ZIP
|
||||
run: |
|
||||
curl -L -H "Authorization: Bearer $GITHUB_TOKEN" -L \
|
||||
https://api.github.com/repos/maxgoedjen/secretive/actions/artifacts/$ZIP_ID/zip > Secretive.zip
|
||||
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: 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
|
||||
with:
|
||||
subject-path: "Secretive.zip"
|
||||
subject-name: "Secretive.zip"
|
||||
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
||||
- name: Create Release
|
||||
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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }}
|
||||
|
@ -4,8 +4,7 @@
|
||||
Secretive is an app for protecting and managing SSH keys with the Secure Enclave.
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="/.github/readme/app-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="/.github/readme/app-light.png">
|
||||
<img src="/.github/readme/app-dark.png" alt="Screenshot of Secretive" width="600">
|
||||
<img src="/.github/readme/app-light.png" alt="Screenshot of Secretive" width="600">
|
||||
</picture>
|
||||
|
||||
|
||||
@ -13,7 +12,7 @@ Secretive is an app for protecting and managing SSH keys with the Secure Enclave
|
||||
|
||||
### Safer Storage
|
||||
|
||||
The most common setup for SSH keys is just keeping them on disk, guarded by proper permissions. This is fine in most cases, but it's not super hard for malicious users or malware to copy your private key. If you protect your keys with the Secure Enclave, it's impossible to export them, by design.
|
||||
The most common setup for SSH keys is just keeping them on disk, guarded by proper permissions. This is fine in most cases, but it's not super hard for malicious users or malware to copy your private key. If you store your keys in the Secure Enclave, it's impossible to export them, by design.
|
||||
|
||||
### Access Control
|
||||
|
||||
@ -53,7 +52,7 @@ Builds are produced by GitHub Actions with an auditable build and release genera
|
||||
|
||||
### A Note Around Code Signing and Keychains
|
||||
|
||||
While Secretive uses the Secure Enclave to protect keys, it still relies on Keychain APIs to store and access them. Keychain restricts reads of keys to the app (and specifically, the bundle ID) that created them. If you build Secretive from source, make sure you are consistent in which bundle ID you use so that the Keychain is able to locate your keys.
|
||||
While Secretive uses the Secure Enclave for key storage, it still relies on Keychain APIs to access them. Keychain restricts reads of keys to the app (and specifically, the bundle ID) that created them. If you build Secretive from source, make sure you are consistent in which bundle ID you use so that the Keychain is able to locate your keys.
|
||||
|
||||
### Backups and Transfers to New Machines
|
||||
|
||||
|
@ -32,6 +32,7 @@
|
||||
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 */; };
|
||||
@ -193,7 +194,7 @@
|
||||
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>"; };
|
||||
5059933F2E7A3B5B0092CFFA /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; 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>"; };
|
||||
50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
@ -226,6 +227,7 @@
|
||||
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; };
|
||||
50A3B78A24026B7500D209EA /* SecretAgent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecretAgent.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
50A3B79324026B7600D209EA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = "<group>"; };
|
||||
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = "<group>"; };
|
||||
@ -446,6 +448,7 @@
|
||||
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
|
||||
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
|
||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
|
||||
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
|
||||
);
|
||||
path = Controllers;
|
||||
sourceTree = "<group>";
|
||||
@ -613,6 +616,7 @@
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
it,
|
||||
fr,
|
||||
de,
|
||||
@ -704,6 +708,7 @@
|
||||
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 */,
|
||||
@ -783,7 +788,7 @@
|
||||
50A3B79524026B7600D209EA /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
5059933F2E7A3B5B0092CFFA /* en */,
|
||||
50A3B79624026B7600D209EA /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
|
@ -7,7 +7,7 @@ import Brief
|
||||
@main
|
||||
struct Secretive: App {
|
||||
|
||||
@Environment(\.agentLaunchController) var agentLaunchController
|
||||
@Environment(\.agentStatusChecker) var agentStatusChecker
|
||||
@Environment(\.justUpdatedChecker) var justUpdatedChecker
|
||||
|
||||
@SceneBuilder var body: some Scene {
|
||||
@ -15,15 +15,14 @@ struct Secretive: App {
|
||||
ContentView()
|
||||
.environment(EnvironmentValues._secretStoreList)
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||
Task {
|
||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||
guard hasRunSetup else { return }
|
||||
agentLaunchController.check()
|
||||
guard !agentLaunchController.developmentBuild else { return }
|
||||
if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running {
|
||||
agentStatusChecker.check()
|
||||
if agentStatusChecker.running && justUpdatedChecker.justUpdatedBuild {
|
||||
// Relaunch the agent, since it'll be running from earlier update still
|
||||
try await agentLaunchController.forceLaunch()
|
||||
}
|
||||
reinstallAgent()
|
||||
} else if !agentStatusChecker.running && !agentStatusChecker.developmentBuild {
|
||||
forceLaunchAgent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,6 +79,30 @@ 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")!
|
||||
}
|
||||
@ -98,8 +121,8 @@ extension EnvironmentValues {
|
||||
return list
|
||||
}()
|
||||
|
||||
private static let _agentLaunchController = AgentLaunchController()
|
||||
@Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
|
||||
private static let _agentStatusChecker = AgentStatusChecker()
|
||||
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
|
||||
private static let _updater: any UpdaterProtocol = {
|
||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||
return Updater(checkOnLaunch: hasRunSetup)
|
||||
|
@ -2,25 +2,18 @@ import Foundation
|
||||
import AppKit
|
||||
import SecretKit
|
||||
import Observation
|
||||
import OSLog
|
||||
import ServiceManagement
|
||||
|
||||
@MainActor protocol AgentLaunchControllerProtocol: Observable, Sendable {
|
||||
@MainActor protocol AgentStatusCheckerProtocol: 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 AgentLaunchController: AgentLaunchControllerProtocol {
|
||||
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
|
||||
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
|
||||
@ -56,47 +49,6 @@ import ServiceManagement
|
||||
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 {
|
||||
|
65
Sources/Secretive/Controllers/LaunchAgentController.swift
Normal file
@ -0,0 +1,65 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -20,12 +20,12 @@
|
||||
<string>$(CI_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CI_BUILD_NUMBER)</string>
|
||||
<key>GitHubBuildLog</key>
|
||||
<string>https://$(CI_BUILD_LINK)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||
<key>GitHubBuildLog</key>
|
||||
<string>$(CI_BUILD_LINK)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticTermination</key>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
class PreviewAgentLaunchController: AgentLaunchControllerProtocol {
|
||||
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
|
||||
let running: Bool
|
||||
let process: NSRunningApplication?
|
||||
@ -15,13 +15,4 @@ class PreviewAgentLaunchController: AgentLaunchControllerProtocol {
|
||||
func check() {
|
||||
}
|
||||
|
||||
func install() async throws {
|
||||
}
|
||||
|
||||
func uninstall() async throws {
|
||||
}
|
||||
|
||||
func forceLaunch() async throws {
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import SwiftUI
|
||||
struct SetupView: View {
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.agentLaunchController) private var agentLaunchController
|
||||
@Binding var setupComplete: Bool
|
||||
|
||||
@State var showingIntegrations = false
|
||||
@ -32,7 +31,7 @@ struct SetupView: View {
|
||||
) {
|
||||
installed = true
|
||||
Task {
|
||||
try? await agentLaunchController.install()
|
||||
await LaunchAgentController().install()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,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(placeholdersReplaced(text: String(localized: step)))) {
|
||||
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) {
|
||||
HStack {
|
||||
Text(placeholdersReplaced(text: String(localized: step)))
|
||||
.padding(8)
|
||||
|
@ -68,9 +68,6 @@ struct ToolbarButtonStyle: PrimitiveButtonStyle {
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 12)
|
||||
.glassEffect(.regular.interactive().tint(tint))
|
||||
.onTapGesture {
|
||||
configuration.trigger()
|
||||
}
|
||||
} else {
|
||||
BorderedButtonStyle().makeBody(configuration: configuration)
|
||||
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
|
||||
|
@ -2,10 +2,10 @@ import SwiftUI
|
||||
|
||||
struct AgentStatusView: View {
|
||||
|
||||
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
|
||||
var body: some View {
|
||||
if agentLaunchController.running {
|
||||
if agentStatusChecker.running {
|
||||
AgentRunningView()
|
||||
} else {
|
||||
AgentNotRunningView()
|
||||
@ -14,12 +14,12 @@ struct AgentStatusView: View {
|
||||
}
|
||||
struct AgentRunningView: View {
|
||||
|
||||
@Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
if let process = agentLaunchController.process {
|
||||
if let process = agentStatusChecker.process {
|
||||
ConfigurationItemView(
|
||||
title: .agentDetailsLocationTitle,
|
||||
value: process.bundleURL!.path(),
|
||||
@ -53,13 +53,19 @@ struct AgentRunningView: View {
|
||||
Menu(.agentDetailsRestartAgentButton) {
|
||||
Button(.agentDetailsDisableAgentButton) {
|
||||
Task {
|
||||
try? await agentLaunchController
|
||||
_ = await LaunchAgentController()
|
||||
.uninstall()
|
||||
agentStatusChecker.check()
|
||||
}
|
||||
}
|
||||
} primaryAction: {
|
||||
Task {
|
||||
try? await agentLaunchController.forceLaunch()
|
||||
let controller = LaunchAgentController()
|
||||
let installed = await controller.install()
|
||||
if !installed {
|
||||
_ = await controller.forceLaunch()
|
||||
}
|
||||
agentStatusChecker.check()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,7 +82,7 @@ struct AgentRunningView: View {
|
||||
|
||||
struct AgentNotRunningView: View {
|
||||
|
||||
@Environment(\.agentLaunchController) private var agentLaunchController
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
@State var triedRestart = false
|
||||
@State var loading = false
|
||||
|
||||
@ -97,10 +103,15 @@ struct AgentNotRunningView: View {
|
||||
guard !loading else { return }
|
||||
loading = true
|
||||
Task {
|
||||
try await agentLaunchController.forceLaunch()
|
||||
let controller = LaunchAgentController()
|
||||
let installed = await controller.install()
|
||||
if !installed {
|
||||
_ = await controller.forceLaunch()
|
||||
}
|
||||
agentStatusChecker.check()
|
||||
loading = false
|
||||
|
||||
if !agentLaunchController.running {
|
||||
if !agentStatusChecker.running {
|
||||
triedRestart = true
|
||||
}
|
||||
}
|
||||
@ -134,9 +145,9 @@ struct AgentNotRunningView: View {
|
||||
|
||||
//#Preview {
|
||||
// AgentStatusView()
|
||||
// .environment(\.agentLaunchController, PreviewAgentLaunchController(running: false))
|
||||
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
|
||||
//}
|
||||
//#Preview {
|
||||
// AgentStatusView()
|
||||
// .environment(\.agentLaunchController, PreviewAgentLaunchController(running: true, process: .current))
|
||||
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(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(\.agentLaunchController) private var agentLaunchController
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker
|
||||
|
||||
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
|
||||
@State private var showingCreation = false
|
||||
@ -127,7 +127,7 @@ extension ContentView {
|
||||
showingAgentInfo = true
|
||||
}, label: {
|
||||
HStack {
|
||||
if agentLaunchController.running {
|
||||
if agentStatusChecker.running {
|
||||
Text(.agentRunningNoticeTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
||||
@ -145,8 +145,8 @@ extension ContentView {
|
||||
})
|
||||
.buttonStyle(
|
||||
ToolbarStatusButtonStyle(
|
||||
lightColor: agentLaunchController.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
||||
darkColor: agentLaunchController.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
||||
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
||||
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
||||
)
|
||||
)
|
||||
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
||||
|