Compare commits

..

3 Commits

Author SHA1 Message Date
Max Goedjen
8826726bc3 Use release config 2025-09-01 20:14:05 -07:00
Max Goedjen
bf78879310 Disable signing on codeql workflow
Updated build command to disable code signing.
2025-09-01 20:12:14 -07:00
Max Goedjen
ca495f4b7f Add codeql workflow 2025-09-01 19:20:27 -07:00
43 changed files with 1068 additions and 1907 deletions

46
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '26 15 * * 3'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
security-events: write
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: swift
build-mode: manual
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- if: matrix.build-mode == 'manual'
name: "Select Xcode"
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
- if: matrix.build-mode == 'manual'
name: "Build"
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@@ -3,16 +3,10 @@ name: Nightly
on: on:
schedule: schedule:
- cron: "0 8 * * *" - cron: "0 8 * * *"
workflow_dispatch:
jobs: jobs:
build: build:
# runs-on: macOS-latest # runs-on: macOS-latest
runs-on: macos-15 runs-on: macos-15
permissions:
id-token: write
contents: write
attestations: write
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
@@ -36,23 +30,22 @@ jobs:
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
- name: Build - name: Build
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
- name: Create ZIP - name: Create ZIPs
run: | run: |
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
- name: Notarize - name: Notarize
env: env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} 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 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 - name: Attest
id: attest id: attest
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v2
with: with:
subject-name: "Secretive.zip" subject-path: 'Secretive.zip'
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }} - name: Upload App to Artifacts
uses: actions/upload-artifact@v4
with:
name: Secretive.zip
path: Secretive.zip

View File

@@ -56,34 +56,39 @@ jobs:
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
- name: Build - name: Build
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
- name: Create ZIP - name: Create ZIPs
run: | run: |
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Xcode_Archive.zip
- name: Notarize - name: Notarize
env: env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} 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 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 - name: Attest
id: attest id: attest
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v2
with: with:
subject-name: "Secretive.zip" subject-path: 'Secretive.zip, Xcode_Archive.zip'
subject-digest: ${{ steps.upload.outputs.artifact-digest }}
- name: Create Release - name: Create Release
run: | run: |
sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md 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 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 create $TAG_NAME -d -F .github/templates/release.md
gh release upload Secretive.zip gh release upload Secretive.zip
gh release upload Xcode_Archive.zip
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ github.ref }} TAG_NAME: ${{ github.ref }}
RUN_ID: ${{ github.run_id }} RUN_ID: ${{ github.run_id }}
ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }} ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }}
- name: Upload App to Artifacts
uses: actions/upload-artifact@v4
with:
name: Secretive.zip
path: Secretive.zip
- name: Upload Archive to Artifacts
uses: actions/upload-artifact@v4
with:
name: Xcode_Archive.zip
path: Xcode_Archive.zip

View File

@@ -57,7 +57,7 @@ let package = Package(
) )
var localization: Resource { var localization: Resource {
.process("../../Resources/Localizable.xcstrings") .process("../../Localizable.xcstrings")
} }
var swiftSettings: [PackageDescription.SwiftSetting] { var swiftSettings: [PackageDescription.SwiftSetting] {

View File

@@ -82,7 +82,7 @@ let package = Package(
) )
var localization: Resource { var localization: Resource {
.process("../../Resources/Localizable.xcstrings") .process("../../Localizable.xcstrings")
} }
var swiftSettings: [PackageDescription.SwiftSetting] { var swiftSettings: [PackageDescription.SwiftSetting] {

View File

@@ -0,0 +1 @@

View File

@@ -4,7 +4,7 @@ import OSLog
/// Manages storage and lookup for OpenSSH certificates. /// Manages storage and lookup for OpenSSH certificates.
public actor OpenSSHCertificateHandler: Sendable { public actor OpenSSHCertificateHandler: Sendable {
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
private let writer = OpenSSHPublicKeyWriter() private let writer = OpenSSHPublicKeyWriter()
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:] private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]

View File

@@ -5,12 +5,12 @@ import OSLog
public final class PublicKeyFileStoreController: Sendable { public final class PublicKeyFileStoreController: Sendable {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController") private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
private let directory: URL private let directory: String
private let keyWriter = OpenSSHPublicKeyWriter() private let keyWriter = OpenSSHPublicKeyWriter()
/// Initializes a PublicKeyFileStoreController. /// Initializes a PublicKeyFileStoreController.
public init(homeDirectory: URL) { public init(homeDirectory: String) {
directory = homeDirectory.appending(component: "PublicKeys") directory = homeDirectory.appending("/PublicKeys")
} }
/// Writes out the keys specified to disk. /// Writes out the keys specified to disk.
@@ -20,17 +20,16 @@ public final class PublicKeyFileStoreController: Sendable {
logger.log("Writing public keys to disk") logger.log("Writing public keys to disk")
if clear { if clear {
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) })) let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? [] let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? []
let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" } let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" }
let untracked = Set(fullPathContents) let untracked = Set(fullPathContents)
.subtracting(validPaths) .subtracting(validPaths)
for path in untracked { for path in untracked {
// string instead of fileURLWithPath since we're already using fileURL format. try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
try? FileManager.default.removeItem(at: URL(string: path)!)
} }
} }
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil) try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
for secret in secrets { for secret in secrets {
let path = publicKeyPath(for: secret) let path = publicKeyPath(for: secret)
let data = Data(keyWriter.openSSHString(secret: secret).utf8) let data = Data(keyWriter.openSSHString(secret: secret).utf8)
@@ -45,14 +44,14 @@ public final class PublicKeyFileStoreController: Sendable {
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to. /// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
public func publicKeyPath<SecretType: Secret>(for secret: SecretType) -> String { public func publicKeyPath<SecretType: Secret>(for secret: SecretType) -> String {
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return directory.appending(component: "\(minimalHex).pub").path() return directory.appending("/").appending("\(minimalHex).pub")
} }
/// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory. /// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory.
public var hasAnyCertificates: Bool { public var hasAnyCertificates: Bool {
do { do {
return try FileManager.default return try FileManager.default
.contentsOfDirectory(atPath: directory.path()) .contentsOfDirectory(atPath: directory)
.filter { $0.hasSuffix("-cert.pub") } .filter { $0.hasSuffix("-cert.pub") }
.isEmpty == false .isEmpty == false
} catch { } catch {
@@ -66,7 +65,7 @@ public final class PublicKeyFileStoreController: Sendable {
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be. /// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String { public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return directory.appending(component: "\(minimalHex)-cert.pub").path() return directory.appending("/").appending("\(minimalHex)-cert.pub")
} }
} }

View File

@@ -30,7 +30,7 @@ extension SecureEnclave {
SecItemCopyMatching(privateAttributes, &privateUntyped) SecItemCopyMatching(privateAttributes, &privateUntyped)
guard let privateTyped = privateUntyped as? [[CFString: Any]] else { return } guard let privateTyped = privateUntyped as? [[CFString: Any]] else { return }
let migratedPublicKeys = Set(store.secrets.map(\.publicKey)) let migratedPublicKeys = Set(store.secrets.map(\.publicKey))
var migratedAny = false var migrated = false
for key in privateTyped { for key in privateTyped {
let name = key[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret) let name = key[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret)
let id = key[kSecAttrApplicationLabel] as! Data let id = key[kSecAttrApplicationLabel] as! Data
@@ -45,24 +45,20 @@ extension SecureEnclave {
// Best guess. // Best guess.
let auth: AuthenticationRequirement = String(describing: accessControl) let auth: AuthenticationRequirement = String(describing: accessControl)
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown .contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
do { let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID) let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth)) guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else { logger.log("Skipping \(name), public key already present. Marking as migrated.")
logger.log("Skipping \(name), public key already present. Marking as migrated.")
try markMigrated(secret: secret, oldID: id)
continue
}
logger.log("Migrating \(name).")
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
logger.log("Migrated \(name).")
try markMigrated(secret: secret, oldID: id) try markMigrated(secret: secret, oldID: id)
migratedAny = true continue
} catch {
logger.error("Failed to migrate \(name): \(error).")
} }
logger.log("Migrating \(name).")
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
logger.log("Migrated \(name).")
try markMigrated(secret: secret, oldID: id)
migrated = true
} }
if migratedAny { if migrated {
store.reloadSecrets() store.reloadSecrets()
} }
} }

View File

@@ -26,7 +26,7 @@ extension SecureEnclave {
for await note in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { for await note in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
guard Constants.notificationToken != (note.object as? String) else { guard Constants.notificationToken != (note.object as? String) else {
// Don't reload if we're the ones triggering this by reloading. // Don't reload if we're the ones triggering this by reloading.
continue return
} }
reloadSecrets() reloadSecrets()
} }

View File

@@ -21,7 +21,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}() }()
private let updater = Updater(checkOnLaunch: true) private let updater = Updater(checkOnLaunch: true)
private let notifier = Notifier() private let notifier = Notifier()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
private lazy var agent: Agent = { private lazy var agent: Agent = {
Agent(storeList: storeList, witness: notifier) Agent(storeList: storeList, witness: notifier)
}() }()

View File

@@ -26,10 +26,6 @@
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; }; 50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
504788EC2E680DC800B4556F /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788EB2E680DC400B4556F /* URLs.swift */; };
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; }; 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
@@ -40,6 +36,7 @@
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; }; 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; }; 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; }; 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; };
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; }; 506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; }; 506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; }; 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; };
@@ -52,12 +49,8 @@
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; }; 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; }; 50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; };
50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; }; 50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; };
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */; };
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 */; };
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */; };
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.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 */
@@ -110,14 +103,10 @@
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; }; 50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; }; 5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Resources/Localizable.xcstrings; sourceTree = SOURCE_ROOT; }; 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Localizable.xcstrings; sourceTree = SOURCE_ROOT; };
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; }; 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.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>"; }; 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; }; 50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -131,6 +120,7 @@
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; }; 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; };
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; }; 5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; }; 5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = "<group>"; };
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; }; 506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; }; 506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; }; 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
@@ -148,12 +138,8 @@
50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; 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>"; }; 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>"; }; 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>"; };
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>"; };
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = "<group>"; };
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.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 */
@@ -193,55 +179,6 @@
path = Helpers; path = Helpers;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
504788ED2E681EB200B4556F /* Styles */ = {
isa = PBXGroup;
children = (
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
);
path = Styles;
sourceTree = "<group>";
};
504788EE2E681EC300B4556F /* Secrets */ = {
isa = PBXGroup;
children = (
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
506772C82425BB8500034DED /* NoStoresView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
50153E21250DECA300525160 /* SecretListItemView.swift */,
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
);
path = Secrets;
sourceTree = "<group>";
};
504788EF2E681ED700B4556F /* Configuration */ = {
isa = PBXGroup;
children = (
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */,
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */,
504788F12E681F3A00B4556F /* Instructions.swift */,
504788F32E681F6900B4556F /* ToolConfigurationView.swift */,
5066A6C12516F303004B5A36 /* SetupView.swift */,
504788F52E68206F00B4556F /* GettingStartedView.swift */,
);
path = Configuration;
sourceTree = "<group>";
};
504788F02E681F0100B4556F /* Views */ = {
isa = PBXGroup;
children = (
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50617D8423FCE48E0099B055 /* ContentView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */,
);
path = Views;
sourceTree = "<group>";
};
50617D7623FCE48D0099B055 = { 50617D7623FCE48D0099B055 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -304,10 +241,20 @@
508A58B0241ED1C40069DC07 /* Views */ = { 508A58B0241ED1C40069DC07 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
504788EF2E681ED700B4556F /* Configuration */, 50617D8423FCE48E0099B055 /* ContentView.swift */,
504788EE2E681EC300B4556F /* Secrets */, 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
504788ED2E681EB200B4556F /* Styles */, 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
504788F02E681F0100B4556F /* Views */, 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
50153E21250DECA300525160 /* SecretListItemView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
506772C82425BB8500034DED /* NoStoresView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */,
5066A6C12516F303004B5A36 /* SetupView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -315,11 +262,11 @@
508A58B1241ED1EA0069DC07 /* Controllers */ = { 508A58B1241ED1EA0069DC07 /* Controllers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
504788EB2E680DC400B4556F /* URLs.swift */,
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
50571E0424393D1500F76F6C /* LaunchAgentController.swift */, 50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */,
); );
path = Controllers; path = Controllers;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -486,33 +433,26 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
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 */,
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */, 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */,
50033AC327813F1700253856 /* BundleIDs.swift in Sources */, 50033AC327813F1700253856 /* BundleIDs.swift in Sources */,
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */,
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */, 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */, 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
50617D8323FCE48E0099B055 /* App.swift in Sources */, 50617D8323FCE48E0099B055 /* App.swift in Sources */,
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
506772C92425BB8500034DED /* NoStoresView.swift in Sources */, 506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */, 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */, 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
@@ -707,18 +647,10 @@
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_USER_SELECTED_FILES = readwrite;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Secretive/Info.plist; INFOPLIST_FILE = Secretive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -747,18 +679,10 @@
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_USER_SELECTED_FILES = readwrite;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Secretive/Info.plist; INFOPLIST_FILE = Secretive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -859,18 +783,10 @@
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_ENHANCED_SECURITY = YES; ENABLE_ENHANCED_SECURITY = YES;
ENABLE_HARDENED_RUNTIME = NO; ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_POINTER_AUTHENTICATION = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_USER_SELECTED_FILES = readwrite;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Secretive/Info.plist; INFOPLIST_FILE = Secretive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -893,17 +809,8 @@
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist; INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -928,17 +835,8 @@
DEVELOPMENT_TEAM = Z72PRUAWF6; DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist; INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -964,17 +862,8 @@
DEVELOPMENT_TEAM = Z72PRUAWF6; DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = SecretAgent/Info.plist; INFOPLIST_FILE = SecretAgent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",

View File

@@ -37,7 +37,6 @@ struct Secretive: App {
@Environment(\.agentStatusChecker) var agentStatusChecker @Environment(\.agentStatusChecker) var agentStatusChecker
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@State private var showingSetup = false @State private var showingSetup = false
@State private var showingIntegrations = false
@State private var showingCreation = false @State private var showingCreation = false
@SceneBuilder var body: some Scene { @SceneBuilder var body: some Scene {
@@ -59,16 +58,8 @@ struct Secretive: App {
forceLaunchAgent() forceLaunchAgent()
} }
} }
.sheet(isPresented: $showingIntegrations) {
IntegrationsView()
}
} }
.commands { .commands {
CommandGroup(before: CommandGroupPlacement.appSettings) {
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
showingIntegrations = true
}
}
CommandGroup(after: CommandGroupPlacement.newItem) { CommandGroup(after: CommandGroupPlacement.newItem) {
Button(.appMenuNewSecretButton) { Button(.appMenuNewSecretButton) {
showingCreation = true showingCreation = true
@@ -80,6 +71,11 @@ struct Secretive: App {
NSWorkspace.shared.open(Constants.helpURL) NSWorkspace.shared.open(Constants.helpURL)
} }
} }
CommandGroup(after: .help) {
Button(.appMenuSetupButton) {
showingSetup = true
}
}
SidebarCommands() SidebarCommands()
} }
} }
@@ -91,7 +87,7 @@ extension Secretive {
private func reinstallAgent() { private func reinstallAgent() {
justUpdatedChecker.check() justUpdatedChecker.check()
Task { Task {
_ = await LaunchAgentController().install() await LaunchAgentController().install()
try? await Task.sleep(for: .seconds(1)) try? await Task.sleep(for: .seconds(1))
agentStatusChecker.check() agentStatusChecker.check()
if !agentStatusChecker.running { if !agentStatusChecker.running {

View File

@@ -6,14 +6,12 @@ import Observation
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable { @MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
var running: Bool { get } var running: Bool { get }
var developmentBuild: Bool { get } var developmentBuild: Bool { get }
var process: NSRunningApplication? { get }
func check() func check()
} }
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol { @Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
var running: Bool = false var running: Bool = false
var process: NSRunningApplication? = nil
nonisolated init() { nonisolated init() {
Task { @MainActor in Task { @MainActor in
@@ -22,39 +20,32 @@ import Observation
} }
func check() { func check() {
process = instanceSecretAgentProcess running = instanceSecretAgentProcess != nil
running = process != nil
} }
// All processes, including ones from older versions, etc // All processes, including ones from older versions, etc
var allSecretAgentProcesses: [NSRunningApplication] { var secretAgentProcesses: [NSRunningApplication] {
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.agentBundleID) NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID)
} }
// The process corresponding to this instance of Secretive // The process corresponding to this instance of Secretive
var instanceSecretAgentProcess: NSRunningApplication? { var instanceSecretAgentProcess: NSRunningApplication? {
// FIXME: CHECK VERSION let agents = secretAgentProcesses
let agents = allSecretAgentProcesses
for agent in agents { for agent in agents {
guard let url = agent.bundleURL else { continue } guard let url = agent.bundleURL else { continue }
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) || (url.isXcodeURL && developmentBuild) { if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) {
return agent return agent
} }
} }
return nil return nil
} }
// Whether Secretive is being run in an Xcode environment. // Whether Secretive is being run in an Xcode environment.
var developmentBuild: Bool { var developmentBuild: Bool {
Bundle.main.bundleURL.isXcodeURL Bundle.main.bundleURL.absoluteString.contains("/Library/Developer/Xcode")
} }
} }
extension URL {
var isXcodeURL: Bool {
absoluteString.contains("/Library/Developer/Xcode")
}
}

View File

@@ -8,28 +8,16 @@ struct LaunchAgentController {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController") private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
func install() async -> Bool { func install() async {
logger.debug("Installing agent") logger.debug("Installing agent")
_ = setEnabled(false) _ = setEnabled(false)
// This is definitely a bit of a "seems to work better" thing but: // 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 // 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))
let result = await MainActor.run { await MainActor.run {
setEnabled(true) _ = 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 { func forceLaunch() async -> Bool {
@@ -40,7 +28,6 @@ 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)")
@@ -49,7 +36,7 @@ struct LaunchAgentController {
} }
private func setEnabled(_ enabled: Bool) -> Bool { private func setEnabled(_ enabled: Bool) -> Bool {
let service = SMAppService.loginItem(identifier: Bundle.agentBundleID) let service = SMAppService.loginItem(identifier: Bundle.main.agentBundleID)
do { do {
if enabled { if enabled {
try service.register() try service.register()

View File

@@ -0,0 +1,63 @@
import Foundation
import Cocoa
import SecretKit
struct ShellConfigurationController {
let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var shellInstructions: [ShellConfigInstruction] {
[
ShellConfigInstruction(shell: "global",
shellConfigDirectory: "~/.ssh/",
shellConfigFilename: "config",
text: "Host *\n\tIdentityAgent \(socketPath)"),
ShellConfigInstruction(shell: "zsh",
shellConfigDirectory: "~/",
shellConfigFilename: ".zshrc",
text: "export SSH_AUTH_SOCK=\(socketPath)"),
ShellConfigInstruction(shell: "bash",
shellConfigDirectory: "~/",
shellConfigFilename: ".bashrc",
text: "export SSH_AUTH_SOCK=\(socketPath)"),
ShellConfigInstruction(shell: "fish",
shellConfigDirectory: "~/.config/fish",
shellConfigFilename: "config.fish",
text: "set -x SSH_AUTH_SOCK \(socketPath)"),
]
}
@MainActor func addToShell(shellInstructions: ShellConfigInstruction) -> Bool {
let openPanel = NSOpenPanel()
// This is sync, so no need to strongly retain
let delegate = Delegate(name: shellInstructions.shellConfigFilename)
openPanel.delegate = delegate
openPanel.message = "Select \(shellInstructions.shellConfigFilename) to let Secretive configure your shell automatically."
openPanel.prompt = "Add to \(shellInstructions.shellConfigFilename)"
openPanel.canChooseFiles = true
openPanel.canChooseDirectories = false
openPanel.showsHiddenFiles = true
openPanel.directoryURL = URL(fileURLWithPath: shellInstructions.shellConfigDirectory)
openPanel.nameFieldStringValue = shellInstructions.shellConfigFilename
openPanel.allowedContentTypes = [.symbolicLink, .data, .plainText]
openPanel.runModal()
guard let fileURL = openPanel.urls.first else { return false }
let handle: FileHandle
do {
handle = try FileHandle(forUpdating: fileURL)
guard let existing = try handle.readToEnd(),
let existingString = String(data: existing, encoding: .utf8) else { return false }
guard !existingString.contains(shellInstructions.text) else {
return true
}
try handle.seekToEnd()
} catch {
return false
}
handle.write(Data("\n# Secretive Config\n\(shellInstructions.text)\n".utf8))
return true
}
}

View File

@@ -1,12 +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()
}
}

View File

@@ -1,11 +1,7 @@
import Foundation import Foundation
extension Bundle {
public static var agentBundleID: String {
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "Host", with: "SecretAgent")
}
public static var hostBundleID: String { extension Bundle {
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "SecretAgent", with: "Host") public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!}
} public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!}
} }

View File

@@ -1,15 +1,12 @@
import Foundation import Foundation
import AppKit
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol { class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
let running: Bool let running: Bool
let process: NSRunningApplication?
let developmentBuild = false let developmentBuild = false
init(running: Bool = true, process: NSRunningApplication? = nil) { init(running: Bool = true) {
self.running = running self.running = running
self.process = process
} }
func check() { func check() {

View File

@@ -0,0 +1,24 @@
import SwiftUI
struct PrimaryButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark {
content.buttonStyle(.glassProminent)
} else {
content.buttonStyle(.borderedProminent)
}
}
}
extension View {
func primary() -> some View {
modifier(PrimaryButtonModifier())
}
}

View File

@@ -1,59 +0,0 @@
import SwiftUI
struct ConfigurationItemView<Content: View>: View {
enum Action: Hashable {
case copy(String)
case revealInFinder(String)
}
let title: LocalizedStringResource
let content: Content
let action: Action?
init(title: LocalizedStringResource, value: String, action: Action? = nil) where Content == Text {
self.title = title
self.content = Text(value)
.font(.subheadline)
.foregroundStyle(.secondary)
self.action = action
}
init(title: LocalizedStringResource, action: Action? = nil, content: () -> Content) {
self.title = title
self.content = content()
self.action = action
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(title)
Spacer()
switch action {
case .copy(let string):
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(string, forType: .string)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case .revealInFinder(let rawPath):
Button(.revealInFinderButton, systemImage: "folder") {
// All foundation-based normalization methods replace this with the container directly.
let processedPath = rawPath.replacingOccurrences(of: "~", with: "/Users/\(NSUserName())")
let url = URL(filePath: processedPath)
let folder = url.deletingLastPathComponent().path()
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case nil:
EmptyView()
}
}
content
}
}
}

View File

@@ -1,49 +0,0 @@
import SwiftUI
struct GettingStartedView: View {
private let instructions = Instructions()
@Binding var selectedInstruction: ConfigurationFileInstructions?
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
_selectedInstruction = selectedInstruction
}
var body: some View {
Form {
Section(.integrationsGettingStartedTitle) {
Text(.integrationsGettingStartedTitleDescription)
}
Section {
Group {
Text(.integrationsGettingStartedSuggestionSsh)
.onTapGesture {
self.selectedInstruction = instructions.ssh
}
VStack(alignment: .leading, spacing: 5) {
Text(.integrationsGettingStartedSuggestionShell)
Text(.integrationsGettingStartedSuggestionShellDefault(shellName: String(localized: instructions.defaultShell.tool)))
.font(.caption2)
}
.onTapGesture {
self.selectedInstruction = instructions.defaultShell
}
Text(.integrationsGettingStartedSuggestionGit)
.onTapGesture {
self.selectedInstruction = instructions.git
}
}
.foregroundStyle(.link)
} header: {
Text(.integrationsGettingStartedWhatShouldIConfigureTitle)
}
footer: {
Text(.integrationsGettingStartedMultipleConfig)
}
}
.formStyle(.grouped)
}
}

View File

@@ -1,179 +0,0 @@
import Foundation
struct Instructions {
enum Constants {
static let publicKeyPathPlaceholder = "_PUBLIC_KEY_PATH_PLACEHOLDER_"
static let publicKeyPlaceholder = "_PUBLIC_KEY_PLACEHOLDER_"
}
var defaultShell: ConfigurationFileInstructions {
zsh
}
var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted)
var ssh: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: LocalizedStringResource.integrationsToolNameSsh,
configPath: "~/.ssh/config",
configText: "Host *\n\tIdentityAgent \(URL.socketPath)",
website: URL(string: "https://man.openbsd.org/ssh_config.5")!,
note: .integrationsSshSpecificKeyNote,
)
}
var git: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: .integrationsToolNameGitSigning,
steps: [
.init(path: "~/.gitconfig", steps: [
.integrationsGitStepGitconfigDescription(publicKeyPathPlaceholder: Constants.publicKeyPathPlaceholder)
],
note: .integrationsGitStepGitconfigSectionNote
),
.init(
path: "~/.gitallowedsigners",
steps: [
LocalizedStringResource(stringLiteral: Constants.publicKeyPlaceholder)
],
note: .integrationsGitStepGitallowedsignersDescription
),
],
website: URL(string: "https://git-scm.com/docs/git-config")!,
)
}
var zsh: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: .integrationsToolNameZsh,
configPath: "~/.zshrc",
configText: "export SSH_AUTH_SOCK=\(URL.socketPath)"
)
}
var instructions: [ConfigurationGroup] {
[
ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [
gettingStarted
]),
ConfigurationGroup(
name: .integrationsSystemSectionTitle,
instructions: [
ssh,
git,
]
),
ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [
zsh,
ConfigurationFileInstructions(
tool: .integrationsToolNameBash,
configPath: "~/.bashrc",
configText: "export SSH_AUTH_SOCK=\(URL.socketPath)"
),
ConfigurationFileInstructions(
tool: .integrationsToolNameFish,
configPath: "~/.config/fish/config.fish",
configText: "set -x SSH_AUTH_SOCK \(URL.socketPath)"
),
ConfigurationFileInstructions(.integrationsOtherShellRowTitle, id: .otherShell),
]),
ConfigurationGroup(name: .integrationsOtherSectionTitle, instructions: [
ConfigurationFileInstructions(.integrationsAppsRowTitle, id: .otherApp),
]),
]
}
}
struct ConfigurationGroup: Identifiable {
let id = UUID()
var name: LocalizedStringResource
var instructions: [ConfigurationFileInstructions] = []
}
struct ConfigurationFileInstructions: Hashable, Identifiable {
struct StepGroup: Hashable, Identifiable {
let path: String
let steps: [LocalizedStringResource]
let note: LocalizedStringResource?
var id: String { path }
init(path: String, steps: [LocalizedStringResource], note: LocalizedStringResource? = nil) {
self.path = path
self.steps = steps
self.note = note
}
func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
}
}
var id: ID
var tool: LocalizedStringResource
var steps: [StepGroup]
var requiresSecret: Bool
var website: URL?
init(
tool: LocalizedStringResource,
configPath: String,
configText: LocalizedStringResource,
requiresSecret: Bool = false,
website: URL? = nil,
note: LocalizedStringResource? = nil
) {
self.id = .tool(String(localized: tool))
self.tool = tool
self.steps = [StepGroup(path: configPath, steps: [configText], note: note)]
self.requiresSecret = requiresSecret
self.website = website
}
init(
tool: LocalizedStringResource,
steps: [StepGroup],
requiresSecret: Bool = false,
website: URL? = nil
) {
self.id = .tool(String(localized: tool))
self.tool = tool
self.steps = steps
self.requiresSecret = true
self.website = website
}
init(_ name: LocalizedStringResource, id: ID) {
self.id = id
tool = name
steps = []
requiresSecret = false
}
func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
}
enum ID: Identifiable, Hashable {
case gettingStarted
case tool(String)
case otherShell
case otherApp
var id: String {
switch self {
case .gettingStarted:
"getting_started"
case .tool(let name):
name
case .otherShell:
"other_shell"
case .otherApp:
"other_app"
}
}
}
}

View File

@@ -1,115 +0,0 @@
import SwiftUI
struct IntegrationsView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedInstruction: ConfigurationFileInstructions?
private let instructions = Instructions()
var body: some View {
NavigationSplitView {
List(selection: $selectedInstruction) {
ForEach(instructions.instructions) { group in
Section(group.name) {
ForEach(group.instructions) { instruction in
Text(instruction.tool)
.padding(.vertical, 8)
.tag(instruction)
}
}
}
}
} detail: {
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
.fauxToolbar {
Button(.setupDoneButton) {
dismiss()
}
.normalButton()
}
}
.onAppear {
selectedInstruction = instructions.gettingStarted
}
.frame(minHeight: 500)
}
}
extension View {
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
modifier(FauxToolbarModifier(toolbarContent: content()))
}
}
struct FauxToolbarModifier<ToolbarContent: View>: ViewModifier {
var toolbarContent: ToolbarContent
func body(content: Content) -> some View {
VStack(alignment: .leading, spacing: 0) {
content
Divider()
HStack {
Spacer()
toolbarContent
.padding(.top, 8)
.padding(.trailing, 16)
.padding(.bottom, 16)
}
}
}
}
struct IntegrationsDetailView: View {
@Binding private var selectedInstruction: ConfigurationFileInstructions?
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
_selectedInstruction = selectedInstruction
}
var body: some View {
if let selectedInstruction {
switch selectedInstruction.id {
case .gettingStarted:
GettingStartedView(selectedInstruction: $selectedInstruction)
case .tool:
ToolConfigurationView(selectedInstruction: selectedInstruction)
case .otherShell:
Form {
Section {
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!)
} header: {
Text(.integrationsCommunityShellListDescription)
.font(.body)
}
}
.formStyle(.grouped)
case .otherApp:
Form {
Section {
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!)
} header: {
Text(.integrationsCommunityAppsListDescription)
.font(.body)
}
}
.formStyle(.grouped)
}
}
}
}
#Preview {
IntegrationsView()
.frame(height: 500)
}

View File

@@ -1,187 +0,0 @@
import SwiftUI
struct SetupView: View {
@Environment(\.dismiss) private var dismiss
@Binding var setupComplete: Bool
@State var showingIntegrations = false
@State var buttonWidth: CGFloat?
@State var installed = false
@State var updates = false
@State var integrations = false
var allDone: Bool {
installed && updates && integrations
}
var body: some View {
VStack {
VStack(alignment: .leading, spacing: 0) {
StepView(
title: .setupAgentTitle,
description: .setupAgentDescription,
systemImage: "lock.laptopcomputer",
) {
setupButton(
.setupAgentInstallButton,
complete: installed,
width: buttonWidth
) {
installed = true
Task {
await LaunchAgentController().install()
}
}
}
Divider()
StepView(
title: .setupUpdatesTitle,
description: .setupUpdatesDescription,
systemImage: "network.badge.shield.half.filled",
) {
setupButton(
.setupUpdatesOkButton,
complete: updates,
width: buttonWidth
) {
updates = true
}
}
Divider()
StepView(
title: .setupIntegrationsTitle,
description: .setupIntegrationsDescription,
systemImage: "firewall",
) {
setupButton(
.setupIntegrationsButton,
complete: integrations,
width: buttonWidth
) {
showingIntegrations = true
}
}
}
.onPreferenceChange(setupButton.WidthKey.self) { width in
buttonWidth = width
}
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
.frame(minWidth: 600, maxWidth: .infinity)
HStack {
Spacer()
Button(.setupDoneButton) {
setupComplete = true
dismiss()
}
.disabled(!allDone)
.primaryButton()
}
}
.interactiveDismissDisabled()
.padding()
.sheet(isPresented: $showingIntegrations, onDismiss: {
integrations = true
}, content: {
IntegrationsView()
})
}
}
struct setupButton: View {
struct WidthKey: @MainActor PreferenceKey {
@MainActor static var defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
if let next = nextValue(), next > (value ?? -1) {
value = next
}
}
}
let label: LocalizedStringResource
let complete: Bool
let action: () -> Void
let width: CGFloat?
@State var currentWidth: CGFloat?
init(_ label: LocalizedStringResource, complete: Bool, width: CGFloat? = nil, action: @escaping () -> Void) {
self.label = label
self.complete = complete
self.action = action
self.width = width
}
var body: some View {
Button(action: action) {
HStack(spacing: 6) {
if complete {
Text(.setupStepCompleteButton)
Image(systemName: "checkmark.circle.fill")
} else {
Text(label)
}
}
.frame(width: width)
.padding(.vertical, 2)
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { newValue in
currentWidth = newValue
}
}
.preference(key: WidthKey.self, value: currentWidth)
.primaryButton()
.disabled(complete)
.tint(complete ? .green : nil)
}
}
struct StepView<Content: View>: View {
let title: LocalizedStringResource
let icon: Image
let description: LocalizedStringResource
let actions: Content
init(title: LocalizedStringResource, description: LocalizedStringResource, systemImage: String, actions: () -> Content) {
self.title = title
self.icon = Image(systemName: systemImage)
self.description = description
self.actions = actions()
}
var body: some View {
HStack(spacing: 0) {
icon
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 24)
Spacer()
.frame(width: 20)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.bold()
Text(description)
}
Spacer(minLength: 20)
actions
}
.padding(20)
}
}
extension SetupView {
enum Constants {
static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")!
}
}
#Preview {
SetupView(setupComplete: .constant(false))
}

View File

@@ -1,110 +0,0 @@
import SwiftUI
import SecretKit
struct ToolConfigurationView: View {
private let instructions = Instructions()
let selectedInstruction: ConfigurationFileInstructions
@Environment(\.secretStoreList) private var secretStoreList
@State var creating = false
@State var selectedSecret: AnySecret?
init(selectedInstruction: ConfigurationFileInstructions) {
self.selectedInstruction = selectedInstruction
}
var body: some View {
Form {
if selectedInstruction.requiresSecret {
if secretStoreList.allSecrets.isEmpty {
Section {
Text(.integrationsConfigureUsingSecretEmptyCreate)
if let store = secretStoreList.modifiableStore {
HStack {
Spacer()
Button(.createSecretTitle) {
creating = true
}
.sheet(isPresented: $creating) {
CreateSecretView(store: store) { created in
selectedSecret = created
}
}
}
}
}
} else {
Section {
Picker(.integrationsConfigureUsingSecretSecretTitle, selection: $selectedSecret) {
if selectedSecret == nil {
Text(.integrationsConfigureUsingSecretNoSecret)
.tag(nil as (AnySecret?))
}
ForEach(secretStoreList.allSecrets) { secret in
Text(secret.name)
.tag(secret)
}
}
} header: {
Text(.integrationsConfigureUsingSecretHeader)
}
.onAppear {
selectedSecret = secretStoreList.allSecrets.first
}
}
}
ForEach(selectedInstruction.steps) { stepGroup in
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))) {
HStack {
Text(placeholdersReplaced(text: String(localized: step)))
.padding(8)
.font(.system(.subheadline, design: .monospaced))
Spacer()
}
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 6)
.fill(.black.opacity(0.05))
.stroke(.separator, lineWidth: 1)
}
}
}
} footer: {
if let note = stepGroup.note {
Text(note)
.font(.caption)
}
}
}
if let url = selectedInstruction.website {
Section {
Link(destination: url) {
VStack(alignment: .leading, spacing: 5) {
Text(.integrationsWebLink)
.font(.headline)
Text(url.absoluteString)
.font(.caption2)
}
}
}
}
}
.formStyle(.grouped)
}
func placeholdersReplaced(text: String) -> String {
guard let selectedSecret else { return text }
let writer = OpenSSHPublicKeyWriter()
let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
return text
.replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: writer.openSSHString(secret: selectedSecret))
.replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret))
}
}

View File

@@ -36,7 +36,7 @@ struct ContentView: View {
toolbarItem(newItemView, id: "new") toolbarItem(newItemView, id: "new")
} }
.sheet(isPresented: $runningSetup) { .sheet(isPresented: $runningSetup) {
SetupView(setupComplete: $hasRunSetup) SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
} }
} }
@@ -56,7 +56,7 @@ extension ContentView {
} }
var needsSetup: Bool { var needsSetup: Bool {
runningSetup || !hasRunSetup (runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild
} }
/// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message /// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message
@@ -66,7 +66,7 @@ extension ContentView {
if needsSetup { if needsSetup {
setupNoticeView setupNoticeView
} else { } else {
agentStatusToolbarView runningNoticeView
} }
} }
@@ -94,7 +94,7 @@ extension ContentView {
.foregroundColor(.white) .foregroundColor(.white)
}) })
.buttonStyle(ToolbarButtonStyle(color: color)) .buttonStyle(ToolbarButtonStyle(color: color))
.sheet(item: $selectedUpdate) { update in .popover(item: $selectedUpdate, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { update in
UpdateDetailView(update: update) UpdateDetailView(update: update)
} }
} }
@@ -103,17 +103,18 @@ extension ContentView {
@ViewBuilder @ViewBuilder
var newItemView: some View { var newItemView: some View {
if storeList.modifiableStore?.isAvailable ?? false { if storeList.modifiableStore?.isAvailable ?? false {
Button(.appMenuNewSecretButton, systemImage: "plus") { Button(action: {
showingCreation = true showingCreation = true
} }, label: {
.menuButton() Image(systemName: "plus")
})
.sheet(isPresented: $showingCreation) { .sheet(isPresented: $showingCreation) {
if let modifiable = storeList.modifiableStore { if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable) { created in CreateSecretView(store: modifiable, showing: $showingCreation)
if let created { .onDisappear {
activeSecret = created guard let newest = modifiable.secrets.last else { return }
activeSecret = newest
} }
}
} }
} }
} }
@@ -124,44 +125,43 @@ extension ContentView {
Button(action: { Button(action: {
runningSetup = true runningSetup = true
}, label: { }, label: {
if !hasRunSetup { Group {
Text(.agentSetupNoticeTitle) if hasRunSetup && !agentStatusChecker.running {
.font(.headline) Text(.agentNotRunningNoticeTitle)
} else {
Text(.agentSetupNoticeTitle)
}
} }
.font(.headline)
}) })
.buttonStyle(ToolbarButtonStyle(color: .orange)) .buttonStyle(ToolbarButtonStyle(color: .orange))
} }
@ViewBuilder @ViewBuilder
var agentStatusToolbarView: some View { var runningNoticeView: some View {
Button(action: { Button(action: {
showingAgentInfo = true showingAgentInfo = true
}, label: { }, label: {
HStack { HStack {
if agentStatusChecker.running { Text(.agentRunningNoticeTitle)
Text(.agentRunningNoticeTitle) .font(.headline)
.font(.headline) .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) Circle()
Circle() .frame(width: 10, height: 10)
.frame(width: 10, height: 10) .foregroundColor(Color.green)
.foregroundColor(Color.green)
} else {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.red)
}
} }
}) })
.buttonStyle( .buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05)))
ToolbarButtonStyle(
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) { .popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
AgentStatusView() VStack {
Text(.agentRunningNoticeDetailTitle)
.font(.title)
.padding(5)
Text(.agentRunningNoticeDetailDescription)
.frame(width: 300)
}
.padding()
} }
} }
@@ -193,22 +193,31 @@ extension ContentView {
} }
var attachmentAnchor: PopoverAttachmentAnchor { var attachmentAnchor: PopoverAttachmentAnchor {
// Ideally .point(.bottom), but broken on Sonoma (FB12726503)
.rect(.bounds) .rect(.bounds)
} }
} }
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
// Empty on modifiable and nonmodifiable
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
.environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
.environment(PreviewUpdater())
// 5 items on modifiable and nonmodifiable
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
.environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
.environment(PreviewUpdater())
}
}
}
#endif
//#Preview {
// // Empty on modifiable and nonmodifiable
// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
// .environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
// .environment(PreviewUpdater())
//}
//
//#Preview {
// // 5 items on modifiable and nonmodifiable
// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
// .environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
// .environment(PreviewUpdater())
//}

View File

@@ -76,10 +76,10 @@ struct CopyableView: View {
switch interactionState { switch interactionState {
case .hovering: case .hovering:
Image(systemName: "document.on.document") Image(systemName: "document.on.document")
.accessibilityLabel(String(localized: .copyableClickToCopyButton)) .accessibilityLabel(String(localized: "copyable_click_to_copy_button"))
case .clicking: case .clicking:
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.accessibilityLabel(String(localized: .copyableCopied)) .accessibilityLabel(String(localized: "copyable_copied"))
case .normal, .dragging: case .normal, .dragging:
EmptyView() EmptyView()
} }
@@ -163,12 +163,17 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
} }
#Preview { #if DEBUG
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
.padding() struct CopyableView_Previews: PreviewProvider {
static var previews: some View {
Group {
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Hello world.")
.padding()
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
.padding()
}
}
} }
#Preview { #endif
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
.padding()
}

View File

@@ -4,15 +4,13 @@ import SecretKit
struct CreateSecretView<StoreType: SecretStoreModifiable>: View { struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@State var store: StoreType @State var store: StoreType
@Environment(\.dismiss) private var dismiss @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 {
@@ -96,24 +94,16 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
} }
} }
} }
if let errorText {
Section {
} footer: {
Text(verbatim: errorText)
.errorStyle()
}
}
} }
HStack { HStack {
Toggle(.createSecretAdvancedLabel, isOn: $advanced) Toggle(.createSecretAdvancedLabel, isOn: $advanced)
.toggleStyle(.button) .toggleStyle(.button)
Spacer() Spacer()
Button(.createSecretCancelButton, role: .cancel) { Button(.createSecretCancelButton, role: .cancel) {
dismiss() showing = false
} }
Button(.createSecretCreateButton, action: save) Button(.createSecretCreateButton, action: save)
.keyboardShortcut(.return) .primary()
.primaryButton()
.disabled(name.isEmpty) .disabled(name.isEmpty)
} }
.padding() .padding()
@@ -127,25 +117,20 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
func save() { func save() {
let attribution = keyAttribution.isEmpty ? nil : keyAttribution let attribution = keyAttribution.isEmpty ? nil : keyAttribution
Task { Task {
do { try! await store.create(
let new = try await store.create( name: name,
name: name, attributes: .init(
attributes: .init( keyType: keyType!,
keyType: keyType!, authentication: authenticationRequirement,
authentication: authenticationRequirement, publicKeyAttribution: attribution
publicKeyAttribution: attribution
)
) )
createdSecret(AnySecret(new)) )
dismiss() showing = false
} catch {
errorText = error.localizedDescription
}
} }
} }
} }
//#Preview { #Preview {
// CreateSecretView(store: Preview.StoreModifiable()) { _ in } CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true))
//} }

View File

@@ -28,7 +28,8 @@ 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)
.errorStyle() .foregroundStyle(.red)
.font(.callout)
} }
Button(.deleteConfirmationDeleteButton, action: delete) Button(.deleteConfirmationDeleteButton, action: delete)
.disabled(confirmedSecretName != secret.name) .disabled(confirmedSecretName != secret.name)

View File

@@ -30,22 +30,21 @@ 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)
.errorStyle() .foregroundStyle(.red)
} .font(.callout)
} }
} }
HStack { HStack {
Button(.editSaveButton, action: rename)
.disabled(name.isEmpty)
.keyboardShortcut(.return)
Button(.editCancelButton) { Button(.editCancelButton) {
dismissalBlock(false) dismissalBlock(false)
} }
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)
Button(.editSaveButton, action: rename)
.disabled(name.isEmpty)
.keyboardShortcut(.return)
.primaryButton()
} }
.padding() .padding()
} }

View File

@@ -57,10 +57,15 @@ struct EmptyStoreModifiableView: View {
} }
} }
#if DEBUG
#Preview { struct EmptyStoreModifiableView_Previews: PreviewProvider {
EmptyStoreImmutableView() static var previews: some View {
} Group {
#Preview { EmptyStoreImmutableView()
EmptyStoreModifiableView() EmptyStoreModifiableView()
}
}
} }
#endif

View File

@@ -13,7 +13,12 @@ struct NoStoresView: View {
} }
#Preview { #if DEBUG
NoStoresView()
struct NoStoresView_Previews: PreviewProvider {
static var previews: some View {
NoStoresView()
}
} }
#endif

View File

@@ -6,7 +6,7 @@ struct SecretDetailView<SecretType: Secret>: View {
let secret: SecretType let secret: SecretType
private let keyWriter = OpenSSHPublicKeyWriter() private let keyWriter = OpenSSHPublicKeyWriter()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID))
var body: some View { var body: some View {
ScrollView { ScrollView {
@@ -37,6 +37,12 @@ struct SecretDetailView<SecretType: Secret>: View {
} }
//#Preview { #if DEBUG
// SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
//} struct SecretDetailView_Previews: PreviewProvider {
static var previews: some View {
SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0])
}
}
#endif

View File

@@ -0,0 +1,297 @@
import SwiftUI
struct SetupView: View {
@State var stepIndex = 0
@Binding var visible: Bool
@Binding var setupComplete: Bool
var body: some View {
GeometryReader { proxy in
VStack {
StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width)
GeometryReader { _ in
HStack(spacing: 0) {
SecretAgentSetupView(buttonAction: advance)
.frame(width: proxy.size.width)
SSHAgentSetupView(buttonAction: advance)
.frame(width: proxy.size.width)
UpdaterExplainerView {
visible = false
setupComplete = true
}
.frame(width: proxy.size.width)
}
.offset(x: -proxy.size.width * Double(stepIndex), y: 0)
}
}
}
.frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500)
}
func advance() {
withAnimation(.spring()) {
stepIndex += 1
}
}
}
struct StepView: View {
let numberOfSteps: Int
let currentStep: Int
// Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7
let width: Double
var body: some View {
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(.blue)
.frame(height: 5)
Rectangle()
.foregroundColor(.green)
.frame(width: max(0, ((width - (Constants.padding * 2)) / Double(numberOfSteps - 1)) * Double(currentStep) - (Constants.circleWidth / 2)), height: 5)
HStack {
ForEach(Array(0..<numberOfSteps), id: \.self) { index in
ZStack {
if currentStep > index {
Circle()
.foregroundColor(.green)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
Text(.setupStepCompleteSymbol)
.foregroundColor(.white)
.bold()
} else {
Circle()
.foregroundColor(.blue)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
if currentStep == index {
Circle()
.strokeBorder(Color.white, lineWidth: 3)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
}
Text(String(describing: index + 1))
.foregroundColor(.white)
.bold()
}
}
if index < numberOfSteps - 1 {
Spacer(minLength: 30)
}
}
}
}.padding(Constants.padding)
}
}
extension StepView {
enum Constants {
static let padding: Double = 15
static let circleWidth: Double = 30
}
}
struct SetupStepView<Content> : View where Content : View {
let title: LocalizedStringResource
let image: Image
let bodyText: LocalizedStringResource
let buttonTitle: LocalizedStringResource
let buttonAction: () -> Void
let content: Content
init(title: LocalizedStringResource, image: Image, bodyText: LocalizedStringResource, buttonTitle: LocalizedStringResource, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) {
self.title = title
self.image = image
self.bodyText = bodyText
self.buttonTitle = buttonTitle
self.buttonAction = buttonAction
self.content = content()
}
var body: some View {
VStack {
Text(title)
.font(.title)
Spacer()
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 64)
Spacer()
Text(bodyText)
.multilineTextAlignment(.center)
Spacer()
content
Spacer()
Button(buttonTitle) {
buttonAction()
}
}.padding()
}
}
struct SecretAgentSetupView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: .setupAgentTitle,
image: Image(nsImage: NSApplication.shared.applicationIconImage),
bodyText: .setupAgentDescription,
buttonTitle: .setupAgentInstallButton,
buttonAction: install) {
Text(.setupAgentActivityMonitorDescription)
.multilineTextAlignment(.center)
}
}
func install() {
Task {
await LaunchAgentController().install()
buttonAction()
}
}
}
struct SSHAgentSetupView: View {
let buttonAction: () -> Void
private static let controller = ShellConfigurationController()
@State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first!
var body: some View {
SetupStepView(title: .setupSshTitle,
image: Image(systemName: "terminal"),
bodyText: .setupSshDescription,
buttonTitle: .setupSshAddedManuallyButton,
buttonAction: buttonAction) {
Link(.setupThirdPartyFaqLink, destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!)
Picker(selection: $selectedShellInstruction, label: EmptyView()) {
ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in
Text(instruction.shell)
.tag(instruction)
.padding()
}
}.pickerStyle(SegmentedPickerStyle())
CopyableView(title: .setupSshAddToConfigButton(configPath: selectedShellInstruction.shellConfigPath), image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
Button(.setupSshAddForMeButton) {
let controller = ShellConfigurationController()
if controller.addToShell(shellInstructions: selectedShellInstruction) {
buttonAction()
}
}
}
}
}
class Delegate: NSObject, NSOpenSavePanelDelegate {
private let name: String
init(name: String) {
self.name = name
}
func panel(_ sender: Any, shouldEnable url: URL) -> Bool {
return url.lastPathComponent == name
}
}
struct UpdaterExplainerView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: .setupUpdatesTitle,
image: Image(systemName: "dot.radiowaves.left.and.right"),
bodyText: .setupUpdatesDescription,
buttonTitle: .setupUpdatesOk,
buttonAction: buttonAction) {
Link(.setupUpdatesReadmore, destination: SetupView.Constants.updaterFAQURL)
}
}
}
extension SetupView {
enum Constants {
static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")!
}
}
struct ShellConfigInstruction: Identifiable, Hashable {
var shell: String
var shellConfigDirectory: String
var shellConfigFilename: String
var text: String
var id: String {
shell
}
var shellConfigPath: String {
return (shellConfigDirectory as NSString).appendingPathComponent(shellConfigFilename)
}
}
#if DEBUG
struct SetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SetupView(visible: .constant(true), setupComplete: .constant(false))
}
}
}
struct SecretAgentSetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SecretAgentSetupView(buttonAction: {})
}
}
}
struct SSHAgentSetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SSHAgentSetupView(buttonAction: {})
}
}
}
struct UpdaterExplainerView_Previews: PreviewProvider {
static var previews: some View {
Group {
UpdaterExplainerView(buttonAction: {})
}
}
}
#endif

View File

@@ -1,94 +0,0 @@
import SwiftUI
struct PrimaryButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
@Environment(\.isEnabled) var isEnabled
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark, isEnabled {
content.buttonStyle(.glassProminent)
} else {
content.buttonStyle(.borderedProminent)
}
}
}
extension View {
func primaryButton() -> some View {
modifier(PrimaryButtonModifier())
}
}
struct MenuButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content
.glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle)
} else {
content
.buttonStyle(.borderless)
}
}
}
extension View {
func menuButton() -> some View {
modifier(MenuButtonModifier())
}
}
struct NormalButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.buttonStyle(.glass)
} else {
content.buttonStyle(.bordered)
}
}
}
extension View {
func normalButton() -> some View {
modifier(NormalButtonModifier())
}
}
struct DangerButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark {
content.buttonStyle(.glassProminent)
.tint(.red)
.foregroundStyle(.white)
} else {
content.buttonStyle(.borderedProminent)
.tint(.red)
.foregroundStyle(.white)
}
}
}
extension View {
func danger() -> some View {
modifier(DangerButtonModifier())
}
}

View File

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

View File

@@ -1,153 +0,0 @@
import SwiftUI
struct AgentStatusView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
var body: some View {
if agentStatusChecker.running {
AgentRunningView()
} else {
AgentNotRunningView()
}
}
}
struct AgentRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
var body: some View {
Form {
Section {
if let process = agentStatusChecker.process {
ConfigurationItemView(
title: .agentDetailsLocationTitle,
value: process.bundleURL!.path(),
action: .revealInFinder(process.bundleURL!.path()),
)
ConfigurationItemView(
title: .agentDetailsSocketPathTitle,
value: URL.socketPath,
action: .copy(URL.socketPath),
)
ConfigurationItemView(
title: .agentDetailsVersionTitle,
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
)
if let launchDate = process.launchDate {
ConfigurationItemView(
title: .agentDetailsRunningSinceTitle,
value: launchDate.formatted()
)
}
}
} header: {
Text(.agentRunningNoticeDetailTitle)
.font(.headline)
.padding(.top)
} footer: {
VStack(alignment: .leading, spacing: 10) {
Text(.agentRunningNoticeDetailDescription)
HStack {
Spacer()
Menu(.agentDetailsRestartAgentButton) {
Button(.agentDetailsDisableAgentButton) {
Task {
_ = await LaunchAgentController()
.uninstall()
agentStatusChecker.check()
}
}
} primaryAction: {
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
}
}
}
}
.padding(.vertical)
}
}
.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(.agentDetailsStartAgentButton)
} else {
HStack {
Text(.agentDetailsStartAgentButtonStarting)
ProgressView()
.controlSize(.mini)
}
}
}
.primaryButton()
} else {
Text(.agentDetailsCouldNotStartError)
.bold()
.foregroundStyle(.red)
}
}
}
.padding(.bottom)
}
}
.formStyle(.grouped)
.frame(width: 400)
}
}
//#Preview {
// AgentStatusView()
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
//}
//#Preview {
// AgentStatusView()
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
//}