@hasna/recordings 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +349 -69
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1919 -98
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1973 -236
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
|
@@ -1,43 +1,136 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import Darwin
|
|
3
|
+
|
|
4
|
+
public enum PostProcessingMode: String, CaseIterable, Identifiable, Codable, Sendable {
|
|
5
|
+
case off
|
|
6
|
+
case auto
|
|
7
|
+
case always
|
|
8
|
+
|
|
9
|
+
public var id: String { rawValue }
|
|
10
|
+
|
|
11
|
+
public var label: String {
|
|
12
|
+
switch self {
|
|
13
|
+
case .off: return "Raw"
|
|
14
|
+
case .auto: return "Auto"
|
|
15
|
+
case .always: return "Always"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public enum ProjectStoreError: Error, LocalizedError {
|
|
21
|
+
case synchronizationInProgress
|
|
22
|
+
case persistenceFailure(String)
|
|
23
|
+
|
|
24
|
+
public var errorDescription: String? {
|
|
25
|
+
switch self {
|
|
26
|
+
case .synchronizationInProgress: return "Project synchronization is in progress"
|
|
27
|
+
case .persistenceFailure(let message): return message
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
2
31
|
|
|
3
32
|
public struct RecProject: Codable, Identifiable, Sendable {
|
|
4
|
-
public
|
|
33
|
+
public var id: String
|
|
5
34
|
public var name: String
|
|
6
35
|
var path: String?
|
|
7
36
|
var systemPrompt: String?
|
|
8
37
|
var appBundleIds: [String]?
|
|
38
|
+
var canonicalPath: String?
|
|
39
|
+
var canonicalStoreId: String?
|
|
40
|
+
var color: String?
|
|
9
41
|
|
|
10
|
-
public init(
|
|
11
|
-
|
|
42
|
+
public init(
|
|
43
|
+
id: String = UUID().uuidString,
|
|
44
|
+
name: String,
|
|
45
|
+
path: String? = nil,
|
|
46
|
+
systemPrompt: String? = nil,
|
|
47
|
+
appBundleIds: [String]? = nil,
|
|
48
|
+
canonicalPath: String? = nil,
|
|
49
|
+
canonicalStoreId: String? = nil,
|
|
50
|
+
color: String? = nil
|
|
51
|
+
) {
|
|
52
|
+
self.id = id
|
|
12
53
|
self.name = name
|
|
13
54
|
self.path = path
|
|
14
55
|
self.systemPrompt = systemPrompt
|
|
15
56
|
self.appBundleIds = appBundleIds
|
|
57
|
+
self.canonicalPath = canonicalPath
|
|
58
|
+
self.canonicalStoreId = canonicalStoreId
|
|
59
|
+
self.color = color
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var registrationPath: String {
|
|
63
|
+
let stored = canonicalPath?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
64
|
+
if !stored.isEmpty { return stored }
|
|
65
|
+
let configured = path?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
66
|
+
if !configured.isEmpty { return configured }
|
|
67
|
+
return "recordings-app://projects/\(id)"
|
|
16
68
|
}
|
|
17
69
|
}
|
|
18
70
|
|
|
19
71
|
public struct ProjectSettings: Codable, Sendable {
|
|
20
|
-
var globalSystemPrompt: String
|
|
21
|
-
var
|
|
22
|
-
var
|
|
72
|
+
public var globalSystemPrompt: String
|
|
73
|
+
public var postProcessingMode: String
|
|
74
|
+
public var projects: [RecProject]
|
|
75
|
+
public var activeProjectId: String?
|
|
23
76
|
|
|
24
77
|
public init() {
|
|
25
78
|
globalSystemPrompt = ""
|
|
79
|
+
postProcessingMode = PostProcessingMode.auto.rawValue
|
|
26
80
|
projects = []
|
|
27
81
|
activeProjectId = nil
|
|
28
82
|
}
|
|
83
|
+
|
|
84
|
+
enum CodingKeys: String, CodingKey {
|
|
85
|
+
case globalSystemPrompt
|
|
86
|
+
case postProcessingMode
|
|
87
|
+
case projects
|
|
88
|
+
case activeProjectId
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public init(from decoder: Decoder) throws {
|
|
92
|
+
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
93
|
+
globalSystemPrompt = try container.decodeIfPresent(String.self, forKey: .globalSystemPrompt) ?? ""
|
|
94
|
+
postProcessingMode = try container.decodeIfPresent(String.self, forKey: .postProcessingMode) ?? PostProcessingMode.auto.rawValue
|
|
95
|
+
projects = try container.decodeIfPresent([RecProject].self, forKey: .projects) ?? []
|
|
96
|
+
activeProjectId = try container.decodeIfPresent(String.self, forKey: .activeProjectId)
|
|
97
|
+
}
|
|
29
98
|
}
|
|
30
99
|
|
|
31
100
|
@MainActor
|
|
32
101
|
public final class ProjectStore: ObservableObject {
|
|
102
|
+
typealias SettingsDataLoader = (String) throws -> Data?
|
|
103
|
+
|
|
33
104
|
@Published public var settings = ProjectSettings()
|
|
105
|
+
@Published public private(set) var isReadyForRecording = false
|
|
106
|
+
@Published public private(set) var isSynchronizingProjects = false
|
|
107
|
+
@Published public private(set) var synchronizationError: String?
|
|
108
|
+
@Published public private(set) var persistenceError: String?
|
|
34
109
|
|
|
35
110
|
private let filePath: String
|
|
111
|
+
private let dataLoader: SettingsDataLoader
|
|
112
|
+
private var loadSucceeded = true
|
|
113
|
+
private var loadFailureMessage: String?
|
|
114
|
+
private var persistedData: Data?
|
|
115
|
+
|
|
116
|
+
public var canMutateProjects: Bool {
|
|
117
|
+
loadSucceeded && !isSynchronizingProjects
|
|
118
|
+
}
|
|
36
119
|
|
|
37
120
|
public var activeProject: RecProject? {
|
|
38
121
|
settings.projects.first { $0.id == settings.activeProjectId }
|
|
39
122
|
}
|
|
40
123
|
|
|
124
|
+
/// Only canonical Store ids are safe to pass to the CLI's `--project` option.
|
|
125
|
+
/// Legacy local ids remain available for display and prompting while synchronization
|
|
126
|
+
/// is degraded, but omitting them here prevents a recordings foreign-key failure.
|
|
127
|
+
public var activeCanonicalProjectIdForRecording: String? {
|
|
128
|
+
guard let activeProject else { return nil }
|
|
129
|
+
let canonicalStoreId = activeProject.canonicalStoreId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
130
|
+
if !canonicalStoreId.isEmpty { return canonicalStoreId }
|
|
131
|
+
return isReadyForRecording ? activeProject.id : nil
|
|
132
|
+
}
|
|
133
|
+
|
|
41
134
|
public var effectiveSystemPrompt: String {
|
|
42
135
|
let global = settings.globalSystemPrompt.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
43
136
|
let project = activeProject?.systemPrompt?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
@@ -47,54 +140,257 @@ public final class ProjectStore: ObservableObject {
|
|
|
47
140
|
return "\(global)\n\n\(project)"
|
|
48
141
|
}
|
|
49
142
|
|
|
50
|
-
public
|
|
143
|
+
public var effectivePostProcessingMode: String {
|
|
144
|
+
let mode = PostProcessingMode(rawValue: settings.postProcessingMode) ?? .auto
|
|
145
|
+
return mode.rawValue
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public convenience init(filePath: String? = nil) {
|
|
51
149
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
|
52
|
-
|
|
53
|
-
|
|
150
|
+
self.init(
|
|
151
|
+
filePath: filePath ?? "\(home)/.hasna/recordings/projects.json",
|
|
152
|
+
dataLoader: Self.readSettingsData
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
init(filePath: String, dataLoader: @escaping SettingsDataLoader) {
|
|
157
|
+
self.filePath = filePath
|
|
158
|
+
self.dataLoader = dataLoader
|
|
159
|
+
let loaded = load()
|
|
160
|
+
loadSucceeded = loaded
|
|
161
|
+
isReadyForRecording = loaded && settings.projects.isEmpty
|
|
54
162
|
}
|
|
55
163
|
|
|
56
|
-
func
|
|
57
|
-
guard FileManager.default.fileExists(atPath: filePath),
|
|
58
|
-
let data = FileManager.default.contents(atPath: filePath) else { return }
|
|
164
|
+
nonisolated private static func readSettingsData(atPath path: String) throws -> Data? {
|
|
59
165
|
do {
|
|
166
|
+
return try Data(contentsOf: URL(fileURLWithPath: path))
|
|
167
|
+
} catch {
|
|
168
|
+
let cocoaError = error as NSError
|
|
169
|
+
if cocoaError.domain == NSCocoaErrorDomain,
|
|
170
|
+
cocoaError.code == NSFileReadNoSuchFileError || cocoaError.code == NSFileNoSuchFileError {
|
|
171
|
+
return nil
|
|
172
|
+
}
|
|
173
|
+
throw error
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@discardableResult
|
|
178
|
+
func load() -> Bool {
|
|
179
|
+
do {
|
|
180
|
+
guard let data = try dataLoader(filePath) else {
|
|
181
|
+
persistedData = nil
|
|
182
|
+
loadFailureMessage = nil
|
|
183
|
+
persistenceError = nil
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
60
186
|
settings = try JSONDecoder().decode(ProjectSettings.self, from: data)
|
|
187
|
+
persistedData = data
|
|
188
|
+
loadFailureMessage = nil
|
|
189
|
+
persistenceError = nil
|
|
190
|
+
return true
|
|
61
191
|
} catch {
|
|
62
|
-
|
|
192
|
+
let message = "Failed to load projects: \(error.localizedDescription)"
|
|
193
|
+
blockMutations(message: message)
|
|
194
|
+
return false
|
|
63
195
|
}
|
|
64
196
|
}
|
|
65
197
|
|
|
66
|
-
func save() {
|
|
198
|
+
public func save() throws {
|
|
199
|
+
try requireWritableState()
|
|
200
|
+
try persistSettings()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private func requireWritableState() throws {
|
|
204
|
+
guard loadSucceeded else {
|
|
205
|
+
throw ProjectStoreError.persistenceFailure(loadFailureMessage ?? "Failed to load projects")
|
|
206
|
+
}
|
|
207
|
+
guard !isSynchronizingProjects else { throw ProjectStoreError.synchronizationInProgress }
|
|
208
|
+
try validatePersistedData()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private func validatePersistedData() throws {
|
|
212
|
+
guard loadSucceeded else {
|
|
213
|
+
throw ProjectStoreError.persistenceFailure(loadFailureMessage ?? "Failed to load projects")
|
|
214
|
+
}
|
|
215
|
+
do {
|
|
216
|
+
guard try dataLoader(filePath) == persistedData else {
|
|
217
|
+
let message = "Project settings changed on disk. Restart Recordings before making changes."
|
|
218
|
+
blockMutations(message: message)
|
|
219
|
+
throw ProjectStoreError.persistenceFailure(message)
|
|
220
|
+
}
|
|
221
|
+
} catch let error as ProjectStoreError {
|
|
222
|
+
throw error
|
|
223
|
+
} catch {
|
|
224
|
+
let message = "Failed to read projects before saving: \(error.localizedDescription)"
|
|
225
|
+
blockMutations(message: message)
|
|
226
|
+
throw ProjectStoreError.persistenceFailure(message)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private func blockMutations(message: String) {
|
|
231
|
+
loadSucceeded = false
|
|
232
|
+
isReadyForRecording = false
|
|
233
|
+
loadFailureMessage = message
|
|
234
|
+
persistenceError = message
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private func persistSettings() throws {
|
|
67
238
|
do {
|
|
68
239
|
let data = try JSONEncoder().encode(settings)
|
|
69
240
|
let dir = (filePath as NSString).deletingLastPathComponent
|
|
70
241
|
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
|
|
71
|
-
try
|
|
242
|
+
try withExclusivePersistenceLock {
|
|
243
|
+
try validatePersistedData()
|
|
244
|
+
try data.write(to: URL(fileURLWithPath: filePath), options: .atomic)
|
|
245
|
+
persistedData = data
|
|
246
|
+
}
|
|
247
|
+
persistenceError = nil
|
|
72
248
|
} catch {
|
|
73
|
-
|
|
249
|
+
if error is ProjectStoreError { throw error }
|
|
250
|
+
persistenceError = "Failed to save projects: \(error.localizedDescription)"
|
|
251
|
+
throw error
|
|
74
252
|
}
|
|
75
253
|
}
|
|
76
254
|
|
|
77
|
-
func
|
|
78
|
-
let
|
|
255
|
+
private func withExclusivePersistenceLock<T>(_ operation: () throws -> T) throws -> T {
|
|
256
|
+
let lockPath = "\(filePath).lock"
|
|
257
|
+
let descriptor = Darwin.open(lockPath, O_CREAT | O_RDWR, mode_t(S_IRUSR | S_IWUSR))
|
|
258
|
+
guard descriptor >= 0 else { throw posixError() }
|
|
259
|
+
defer { Darwin.close(descriptor) }
|
|
260
|
+
guard Darwin.lockf(descriptor, F_LOCK, 0) == 0 else { throw posixError() }
|
|
261
|
+
defer { Darwin.lockf(descriptor, F_ULOCK, 0) }
|
|
262
|
+
return try operation()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private func posixError() -> NSError {
|
|
266
|
+
let code = errno
|
|
267
|
+
return NSError(
|
|
268
|
+
domain: NSPOSIXErrorDomain,
|
|
269
|
+
code: Int(code),
|
|
270
|
+
userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(code))]
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public func clearPersistenceError() {
|
|
275
|
+
persistenceError = nil
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public func reconcileWithCanonicalStore(home: String = RecordingsCLI.defaultHome) async throws {
|
|
279
|
+
try requireWritableState()
|
|
280
|
+
isSynchronizingProjects = true
|
|
281
|
+
defer { isSynchronizingProjects = false }
|
|
282
|
+
let original = settings
|
|
283
|
+
guard !original.projects.isEmpty else {
|
|
284
|
+
isReadyForRecording = true
|
|
285
|
+
synchronizationError = nil
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
isReadyForRecording = false
|
|
289
|
+
let registrations: [(String, RecordingsCLI.CanonicalProject)]
|
|
290
|
+
do {
|
|
291
|
+
registrations = try await Task.detached(priority: .utility) {
|
|
292
|
+
try original.projects.map { project in
|
|
293
|
+
let canonical = try RecordingsCLI.registerProject(
|
|
294
|
+
name: project.name,
|
|
295
|
+
path: project.registrationPath,
|
|
296
|
+
home: home
|
|
297
|
+
)
|
|
298
|
+
return (project.id, canonical)
|
|
299
|
+
}
|
|
300
|
+
}.value
|
|
301
|
+
} catch {
|
|
302
|
+
let message = "Failed to register projects: \((error as? RecordingsCLI.Failure)?.message ?? error.localizedDescription)"
|
|
303
|
+
synchronizationError = message
|
|
304
|
+
persistenceError = message
|
|
305
|
+
throw error
|
|
306
|
+
}
|
|
307
|
+
var migrated = original
|
|
308
|
+
var canonicalIDByLocalID: [String: String] = [:]
|
|
309
|
+
var seenCanonicalIDs = Set<String>()
|
|
310
|
+
migrated.projects = zip(original.projects, registrations).compactMap { pair -> RecProject? in
|
|
311
|
+
let (project, registration) = pair
|
|
312
|
+
let canonical = registration.1
|
|
313
|
+
if canonicalIDByLocalID[project.id] == nil {
|
|
314
|
+
canonicalIDByLocalID[project.id] = canonical.id
|
|
315
|
+
}
|
|
316
|
+
guard seenCanonicalIDs.insert(canonical.id).inserted else { return nil }
|
|
317
|
+
return RecProject(
|
|
318
|
+
id: canonical.id,
|
|
319
|
+
name: project.name,
|
|
320
|
+
path: project.path,
|
|
321
|
+
systemPrompt: project.systemPrompt,
|
|
322
|
+
appBundleIds: project.appBundleIds,
|
|
323
|
+
canonicalPath: canonical.path,
|
|
324
|
+
canonicalStoreId: canonical.id,
|
|
325
|
+
color: project.color
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
if let active = original.activeProjectId {
|
|
329
|
+
migrated.activeProjectId = canonicalIDByLocalID[active]
|
|
330
|
+
}
|
|
331
|
+
settings = migrated
|
|
332
|
+
do {
|
|
333
|
+
try persistSettings()
|
|
334
|
+
isReadyForRecording = true
|
|
335
|
+
synchronizationError = nil
|
|
336
|
+
} catch {
|
|
337
|
+
settings = original
|
|
338
|
+
synchronizationError = persistenceError ?? "Failed to persist synchronized projects"
|
|
339
|
+
throw error
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public func addProject(name: String, path: String? = nil, systemPrompt: String? = nil, color: String? = nil, home: String = RecordingsCLI.defaultHome) async throws {
|
|
344
|
+
try requireWritableState()
|
|
345
|
+
isSynchronizingProjects = true
|
|
346
|
+
defer { isSynchronizingProjects = false }
|
|
347
|
+
let local = RecProject(name: name, path: path, systemPrompt: systemPrompt, color: color)
|
|
348
|
+
let canonical: RecordingsCLI.CanonicalProject
|
|
349
|
+
do {
|
|
350
|
+
canonical = try await Task.detached(priority: .userInitiated) {
|
|
351
|
+
try RecordingsCLI.registerProject(name: local.name, path: local.registrationPath, home: home)
|
|
352
|
+
}.value
|
|
353
|
+
} catch {
|
|
354
|
+
persistenceError = "Failed to register project: \((error as? RecordingsCLI.Failure)?.message ?? error.localizedDescription)"
|
|
355
|
+
throw error
|
|
356
|
+
}
|
|
357
|
+
let project = RecProject(
|
|
358
|
+
id: canonical.id,
|
|
359
|
+
name: local.name,
|
|
360
|
+
path: local.path,
|
|
361
|
+
systemPrompt: local.systemPrompt,
|
|
362
|
+
appBundleIds: local.appBundleIds,
|
|
363
|
+
canonicalPath: canonical.path,
|
|
364
|
+
canonicalStoreId: canonical.id,
|
|
365
|
+
color: local.color
|
|
366
|
+
)
|
|
367
|
+
let original = settings
|
|
79
368
|
settings.projects.append(project)
|
|
80
|
-
|
|
369
|
+
do { try persistSettings() } catch { settings = original; throw error }
|
|
370
|
+
isReadyForRecording = true
|
|
81
371
|
}
|
|
82
372
|
|
|
83
|
-
func updateProject(_ project: RecProject) {
|
|
373
|
+
public func updateProject(_ project: RecProject) throws {
|
|
374
|
+
try requireWritableState()
|
|
84
375
|
guard let idx = settings.projects.firstIndex(where: { $0.id == project.id }) else { return }
|
|
376
|
+
let original = settings
|
|
85
377
|
settings.projects[idx] = project
|
|
86
|
-
save()
|
|
378
|
+
do { try save() } catch { settings = original; throw error }
|
|
87
379
|
}
|
|
88
380
|
|
|
89
|
-
func removeProject(id: String) {
|
|
381
|
+
public func removeProject(id: String) throws {
|
|
382
|
+
try requireWritableState()
|
|
383
|
+
let original = settings
|
|
90
384
|
settings.projects.removeAll { $0.id == id }
|
|
91
385
|
if settings.activeProjectId == id { settings.activeProjectId = nil }
|
|
92
|
-
save()
|
|
386
|
+
do { try save() } catch { settings = original; throw error }
|
|
93
387
|
}
|
|
94
388
|
|
|
95
|
-
func setActive(_ id: String?) {
|
|
389
|
+
public func setActive(_ id: String?) throws {
|
|
390
|
+
try requireWritableState()
|
|
391
|
+
let original = settings
|
|
96
392
|
settings.activeProjectId = id
|
|
97
|
-
save()
|
|
393
|
+
do { try save() } catch { settings = original; throw error }
|
|
98
394
|
}
|
|
99
395
|
|
|
100
396
|
// MARK: - Auto-detection
|