This commit is contained in:
Max Goedjen 2025-08-31 00:58:16 -07:00
parent b949d846c1
commit cd76bb95ec
No known key found for this signature in database
8 changed files with 177 additions and 91 deletions

View File

@ -52,6 +52,7 @@
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; }; 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; };
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; }; 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; }; 50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; };
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */; };
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; }; 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -142,6 +143,7 @@
50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; }; 50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; };
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; }; 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = "<group>"; }; 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = "<group>"; };
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = "<group>"; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; }; 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -259,6 +261,7 @@
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */, 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */, 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */, 5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -436,6 +439,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */, 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,

View File

@ -15,17 +15,21 @@ struct LaunchAgentController {
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old // Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new? // and start new?
try? await Task.sleep(for: .seconds(1)) try? await Task.sleep(for: .seconds(1))
return await MainActor.run { let result = await MainActor.run {
setEnabled(true) setEnabled(true)
} }
try? await Task.sleep(for: .seconds(1))
return result
} }
func uninstall() async -> Bool { func uninstall() async -> Bool {
logger.debug("Uninstalling agent") logger.debug("Uninstalling agent")
try? await Task.sleep(for: .seconds(1)) try? await Task.sleep(for: .seconds(1))
return await MainActor.run { let result = await MainActor.run {
setEnabled(false) setEnabled(false)
} }
try? await Task.sleep(for: .seconds(1))
return result
} }
func forceLaunch() async -> Bool { func forceLaunch() async -> Bool {
@ -36,6 +40,7 @@ struct LaunchAgentController {
do { do {
try await NSWorkspace.shared.openApplication(at: url, configuration: config) try await NSWorkspace.shared.openApplication(at: url, configuration: config)
logger.debug("Agent force launched") logger.debug("Agent force launched")
try? await Task.sleep(for: .seconds(1))
return true return true
} catch { } catch {
logger.error("Error force launching \(error.localizedDescription)") logger.error("Error force launching \(error.localizedDescription)")

View File

@ -3,81 +3,63 @@ import SwiftUI
struct AgentStatusView: View { struct AgentStatusView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var body: some View { var body: some View {
if agentStatusChecker.running { if agentStatusChecker.running {
Form { AgentRunningView()
Section { } else {
if let process = agentStatusChecker.process { AgentNotRunningView()
}
}
}
struct AgentRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var body: some View {
Form {
Section {
if let process = agentStatusChecker.process {
AgentInformationView(
title: "Secret Agent Location",
value: process.bundleURL!.path(),
actions: [.revealInFinder],
)
AgentInformationView(
title: "Socket Path",
value: socketPath,
actions: [.copy],
)
AgentInformationView(
title: "Version",
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
)
if let launchDate = process.launchDate {
AgentInformationView( AgentInformationView(
title: "Secret Agent Location", title: "Running Since",
value: process.bundleURL!.path(), value: launchDate.formatted()
actions: [.revealInFinder],
) )
AgentInformationView(
title: "Socket Path",
value: socketPath,
actions: [.copy],
)
AgentInformationView(
title: "Version",
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
)
if let launchDate = process.launchDate {
AgentInformationView(
title: "Running Since",
value: launchDate.formatted()
)
}
} }
} header: { }
Text(.agentRunningNoticeDetailTitle) } header: {
.font(.headline) Text(.agentRunningNoticeDetailTitle)
.padding(.top) .font(.headline)
} footer: { .padding(.top)
VStack(alignment: .leading) { } footer: {
Text(.agentRunningNoticeDetailDescription) VStack(alignment: .leading, spacing: 10) {
HStack { Text(.agentRunningNoticeDetailDescription)
Spacer() HStack {
Menu("Restart Agent") { Spacer()
Button("Disable Agent") { Menu("Restart Agent") {
Task { Button("Disable Agent") {
await LaunchAgentController()
.uninstall()
}
}
} primaryAction: {
Task { Task {
let controller = LaunchAgentController() _ = await LaunchAgentController()
let installed = await controller.install() .uninstall()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check() agentStatusChecker.check()
} }
} }
} } primaryAction: {
}
.padding(.vertical)
}
}
.formStyle(.grouped)
.frame(width: 400)
} else {
Form {
Section {
} header: {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
.padding(.top)
} footer: {
Text(.agentNotRunningNoticeDetailDescription)
Spacer()
HStack {
Spacer()
Button("Start Agent") {
Task { Task {
let controller = LaunchAgentController() let controller = LaunchAgentController()
let installed = await controller.install() let installed = await controller.install()
@ -87,14 +69,77 @@ struct AgentStatusView: View {
agentStatusChecker.check() agentStatusChecker.check()
} }
} }
.primary()
} }
.padding(.vertical)
} }
.padding(.vertical)
} }
.formStyle(.grouped)
.frame(width: 400)
} }
.formStyle(.grouped)
.frame(width: 400)
}
}
struct AgentNotRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@State var triedRestart = false
@State var loading = false
var body: some View {
Form {
Section {
} header: {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
.padding(.top)
} footer: {
VStack(alignment: .leading, spacing: 10) {
Text(.agentNotRunningNoticeDetailDescription)
HStack {
if !triedRestart {
Spacer()
Button {
guard !loading else { return }
loading = true
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
loading = false
if !agentStatusChecker.running {
triedRestart = true
}
}
} label: {
if !loading {
Text("Start Agent")
} else {
HStack {
Text("Starting Agent")
ProgressView()
.controlSize(.mini)
}
}
}
.primary()
} else {
Text("Secretive was unable to get SecretAgent to launch. Please try restarting your Mac, and if that doesn't work, file an issue on GitHub.")
.bold()
.foregroundStyle(.red)
}
}
}
.padding(.bottom)
}
}
.formStyle(.grouped)
.frame(width: 400)
} }
} }

View File

@ -110,11 +110,11 @@ extension ContentView {
}) })
.sheet(isPresented: $showingCreation) { .sheet(isPresented: $showingCreation) {
if let modifiable = storeList.modifiableStore { if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable, showing: $showingCreation) CreateSecretView(store: modifiable, showing: $showingCreation) { created in
.onDisappear { if let created {
guard let newest = modifiable.secrets.last else { return } activeSecret = created
activeSecret = newest
} }
}
} }
} }
} }

View File

@ -5,12 +5,14 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@State var store: StoreType @State var store: StoreType
@Binding var showing: Bool @Binding var showing: Bool
var createdSecret: (AnySecret?) -> Void
@State private var name = "" @State private var name = ""
@State private var keyAttribution = "" @State private var keyAttribution = ""
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired @State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
@State private var keyType: KeyType? @State private var keyType: KeyType?
@State var advanced = false @State var advanced = false
@State var errorText: String?
private var authenticationOptions: [AuthenticationRequirement] { private var authenticationOptions: [AuthenticationRequirement] {
if advanced || authenticationRequirement == .biometryCurrent { if advanced || authenticationRequirement == .biometryCurrent {
@ -94,6 +96,13 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
} }
} }
} }
if let errorText {
Section {
} footer: {
Text(verbatim: errorText)
.errorStyle()
}
}
} }
HStack { HStack {
Toggle(.createSecretAdvancedLabel, isOn: $advanced) Toggle(.createSecretAdvancedLabel, isOn: $advanced)
@ -118,20 +127,25 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
func save() { func save() {
let attribution = keyAttribution.isEmpty ? nil : keyAttribution let attribution = keyAttribution.isEmpty ? nil : keyAttribution
Task { Task {
try! await store.create( do {
name: name, let new = try await store.create(
attributes: .init( name: name,
keyType: keyType!, attributes: .init(
authentication: authenticationRequirement, keyType: keyType!,
publicKeyAttribution: attribution authentication: authenticationRequirement,
publicKeyAttribution: attribution
)
) )
) createdSecret(AnySecret(new))
showing = false showing = false
} catch {
errorText = error.localizedDescription
}
} }
} }
} }
#Preview { #Preview {
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true)) CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true)) { _ in }
} }

View File

@ -28,8 +28,7 @@ struct DeleteSecretConfirmationModifier: ViewModifier {
TextField(secret.name, text: $confirmedSecretName) TextField(secret.name, text: $confirmedSecretName)
if let errorText { if let errorText {
Text(verbatim: errorText) Text(verbatim: errorText)
.foregroundStyle(.red) .errorStyle()
.font(.callout)
} }
Button(.deleteConfirmationDeleteButton, action: delete) Button(.deleteConfirmationDeleteButton, action: delete)
.disabled(confirmedSecretName != secret.name) .disabled(confirmedSecretName != secret.name)

View File

@ -30,11 +30,11 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} } footer: {
if let errorText { if let errorText {
Text(verbatim: errorText) Text(verbatim: errorText)
.foregroundStyle(.red) .errorStyle()
.font(.callout) }
} }
} }
HStack { HStack {

View File

@ -0,0 +1,19 @@
import SwiftUI
struct ErrorStyleModifier: ViewModifier {
func body(content: Content) -> some View {
content
.foregroundStyle(.red)
.font(.callout)
}
}
extension View {
func errorStyle() -> some View {
modifier(ErrorStyleModifier())
}
}