mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-05-13 02:38:59 +02:00
Merge
This commit is contained in:
@@ -8,7 +8,7 @@ import CertificateKit
|
||||
@main
|
||||
struct Secretive: App {
|
||||
|
||||
@Environment(\.agentStatusChecker) var agentStatusChecker
|
||||
@Environment(\.agentLaunchController) var agentLaunchController
|
||||
@Environment(\.justUpdatedChecker) var justUpdatedChecker
|
||||
|
||||
@SceneBuilder var body: some Scene {
|
||||
@@ -17,14 +17,16 @@ struct Secretive: App {
|
||||
.environment(EnvironmentValues._secretStoreList)
|
||||
.environment(EnvironmentValues._certificateStore)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,30 +83,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")!
|
||||
}
|
||||
@@ -124,9 +102,10 @@ extension EnvironmentValues {
|
||||
}()
|
||||
|
||||
@MainActor fileprivate static let _certificateStore: CertificateStore = CertificateStore()
|
||||
|
||||
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,18 +2,26 @@ import Foundation
|
||||
import AppKit
|
||||
import SecretKit
|
||||
import Observation
|
||||
import OSLog
|
||||
import ServiceManagement
|
||||
import Common
|
||||
|
||||
@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 +41,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 +57,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,25 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
|
||||
static var agentHomeURL: URL {
|
||||
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
|
||||
}
|
||||
|
||||
static var socketPath: String {
|
||||
URL.agentHomeURL.appendingPathComponent("socket.ssh").path()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
var normalizedPathAndFolder: (String, String) {
|
||||
// All foundation-based normalization methods replace this with the container directly.
|
||||
let processedPath = replacingOccurrences(of: "~", with: "/Users/\(NSUserName())")
|
||||
let url = URL(filePath: processedPath)
|
||||
let folder = url.deletingLastPathComponent().path()
|
||||
return (processedPath, folder)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Bundle {
|
||||
public static var agentBundleID: String {
|
||||
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "Host", with: "SecretAgent")
|
||||
}
|
||||
|
||||
public static var hostBundleID: String {
|
||||
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "SecretAgent", with: "Host")
|
||||
}
|
||||
}
|
||||
@@ -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 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ struct ToolConfigurationView: View {
|
||||
|
||||
@State var creating = false
|
||||
@State var selectedSecret: AnySecret?
|
||||
@State var email = ""
|
||||
|
||||
init(selectedInstruction: ConfigurationFileInstructions) {
|
||||
self.selectedInstruction = selectedInstruction
|
||||
@@ -49,6 +50,12 @@ struct ToolConfigurationView: View {
|
||||
.tag(secret)
|
||||
}
|
||||
}
|
||||
TextField(text: $email, prompt: Text(.integrationsConfigureUsingEmailPlaceholder)) {
|
||||
Text(.integrationsConfigureUsingEmailTitle)
|
||||
Text(.integrationsConfigureUsingEmailSubtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text(.integrationsConfigureUsingSecretHeader)
|
||||
}
|
||||
@@ -61,7 +68,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)
|
||||
@@ -103,9 +110,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))
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,9 @@ 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))
|
||||
|
||||
@@ -24,6 +24,18 @@ struct SecretListItemView: View {
|
||||
Text(secret.name)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isRenaming, onDismiss: {
|
||||
renamedSecret(secret)
|
||||
}, content: {
|
||||
if let modifiable = store as? AnySecretStoreModifiable {
|
||||
EditSecretView(store: modifiable, secret: secret)
|
||||
}
|
||||
})
|
||||
.showingDeleteConfirmation(isPresented: $isDeleting, secret, store as? AnySecretStoreModifiable) { deleted in
|
||||
if deleted {
|
||||
deletedSecret(secret)
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
if store is AnySecretStoreModifiable {
|
||||
Button(action: { isRenaming = true }) {
|
||||
@@ -36,17 +48,5 @@ struct SecretListItemView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.showingDeleteConfirmation(isPresented: $isDeleting, secret, store as? AnySecretStoreModifiable) { deleted in
|
||||
if deleted {
|
||||
deletedSecret(secret)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isRenaming, onDismiss: {
|
||||
renamedSecret(secret)
|
||||
}, content: {
|
||||
if let modifiable = store as? AnySecretStoreModifiable {
|
||||
EditSecretView(store: modifiable, secret: secret)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
//}
|
||||
|
||||
@@ -16,7 +16,7 @@ struct ContentView: View {
|
||||
@Environment(\.secretStoreList) private var storeList
|
||||
@Environment(\.certificateStore) private var certificateStore
|
||||
@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
|
||||
@@ -145,7 +145,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)
|
||||
@@ -163,8 +163,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) {
|
||||
|
||||
Reference in New Issue
Block a user