import Foundation import SaplingCore public protocol WorkspaceManaging: Sendable { func openWorkspace(at url: URL) async throws -> Workspace func loadItems(in directoryURL: URL) async throws -> [WorkspaceItem] func sampleWorkspace() -> Workspace } public final class LocalWorkspaceManager: WorkspaceManaging, @unchecked Sendable { private let fileManager: FileManager public init(fileManager: FileManager = .default) { self.fileManager = fileManager } public func openWorkspace(at url: URL) async throws -> Workspace { let items = try scanItems(at: url) let workspace = Workspace(name: url.lastPathComponent, rootURL: url, items: items) try SaplingRules.validateWorkspace(workspace) return workspace } public func loadItems(in directoryURL: URL) async throws -> [WorkspaceItem] { try scanItems(at: directoryURL) } public func sampleWorkspace() -> Workspace { SaplingSampleData.workspace } private func scanItems(at url: URL) throws -> [WorkspaceItem] { guard let children = try? fileManager.contentsOfDirectory( at: url, includingPropertiesForKeys: [.isDirectoryKey], options: directoryOptions ) else { return [] } return try children .map { childURL in let values = try childURL.resourceValues(forKeys: [.isDirectoryKey]) if values.isDirectory == true { if isGitRepository(at: childURL) { return .project(project(at: childURL)) } return .folder( WorkspaceFolder( name: childURL.lastPathComponent, url: childURL ) ) } return .file( WorkspaceFile( name: childURL.lastPathComponent, url: childURL, kind: fileKind(for: childURL) ) ) } .sorted(by: workspaceItemSort) } private var directoryOptions: FileManager.DirectoryEnumerationOptions { [.skipsHiddenFiles, .skipsPackageDescendants] } 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 workspaceItemSort(_ lhs: WorkspaceItem, _ rhs: WorkspaceItem) -> Bool { if lhs.sortGroup != rhs.sortGroup { return lhs.sortGroup < rhs.sortGroup } return lhs.displayName.localizedStandardCompare(rhs.displayName) == .orderedAscending } 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 } } } private extension WorkspaceItem { var sortGroup: Int { switch self { case .folder, .project, .subproject: return 0 case .file: return 1 } } }