import Foundation import SaplingCore import SaplingGit import SaplingStorage public protocol WorkspaceManaging: Sendable { func openWorkspace(at url: URL) async throws -> Workspace func sampleWorkspace() -> Workspace } public final class LocalWorkspaceManager: WorkspaceManaging, @unchecked Sendable { private let gitProvider: any GitProvider private let metadataStore: any WorkspaceMetadataStore private let fileManager: FileManager public init( gitProvider: any GitProvider, metadataStore: any WorkspaceMetadataStore, fileManager: FileManager = .default ) { self.gitProvider = gitProvider self.metadataStore = metadataStore self.fileManager = fileManager } public func openWorkspace(at url: URL) async throws -> Workspace { let items = try scanItems(at: url, relativeTo: url) let workspace = Workspace(name: url.lastPathComponent, rootURL: url, items: items) try SaplingRules.validateWorkspace(workspace) try metadataStore.saveMetadata(WorkspaceMetadata(workspaceID: workspace.id)) return workspace } public func sampleWorkspace() -> Workspace { SaplingSampleData.workspace } private func scanItems(at url: URL, relativeTo rootURL: URL) throws -> [WorkspaceItem] { guard let children = try? fileManager.contentsOfDirectory( at: url, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles] ) else { return [] } return try children .sorted { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending } .map { childURL in if isGitRepository(at: childURL) { return .project(project(at: childURL)) } let values = try childURL.resourceValues(forKeys: [.isDirectoryKey]) if values.isDirectory == true { return .folder( WorkspaceFolder( name: childURL.lastPathComponent, url: childURL, children: try scanItems(at: childURL, relativeTo: rootURL) ) ) } return .file( WorkspaceFile( name: childURL.lastPathComponent, url: childURL, kind: fileKind(for: childURL) ) ) } } private func isGitRepository(at url: URL) -> Bool { fileManager.fileExists(atPath: url.appendingPathComponent(".git").path) } private func project(at url: URL) -> Project { let repository = GitRepository( name: url.lastPathComponent, rootURL: url, statusSummary: .unknown ) return Project( name: url.lastPathComponent, repositoryURL: url, gitRepository: repository ) } private func fileKind(for url: URL) -> WorkspaceFileKind { switch url.pathExtension.lowercased() { case "md", "markdown": return .markdown case "png", "jpg", "jpeg", "gif", "webp", "pdf", "mp3", "mp4": return .attachment default: return .other } } }