90 lines
3.5 KiB
Swift
90 lines
3.5 KiB
Swift
import Foundation
|
|
|
|
public enum ClaudeDesktopProjectsLocator {
|
|
private static let sessionDirectoryNames = [
|
|
"local-agent-mode-sessions",
|
|
"claude-code-sessions",
|
|
]
|
|
|
|
private static let skippedDirectoryNames = Set([
|
|
".build",
|
|
".git",
|
|
"build",
|
|
"DerivedData",
|
|
"node_modules",
|
|
"outputs",
|
|
"target",
|
|
])
|
|
|
|
public static func roots(
|
|
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,
|
|
fileManager: FileManager = .default) -> [URL]
|
|
{
|
|
let claudeApplicationSupport = homeDirectory
|
|
.appendingPathComponent("Library", isDirectory: true)
|
|
.appendingPathComponent("Application Support", isDirectory: true)
|
|
.appendingPathComponent("Claude", isDirectory: true)
|
|
let sessionRoots = self.sessionDirectoryNames.map {
|
|
claudeApplicationSupport.appendingPathComponent($0, isDirectory: true)
|
|
}
|
|
|
|
var roots: [URL] = []
|
|
var queue = sessionRoots.map { (url: $0, depth: 0) }
|
|
var visited = Set(sessionRoots.map(\.standardizedFileURL.path))
|
|
var nextIndex = 0
|
|
// Current Desktop entries under claude-code-sessions are metadata whose cliSessionId maps to the
|
|
// shared ~/.claude/projects logs. Seed that root anyway so embedded project stores are found when present.
|
|
// Covers observed Desktop layouts through account/workspace, session, agent, and local_agent
|
|
// without descending into arbitrary checked-out workspaces.
|
|
let maxDepth = 4
|
|
|
|
while nextIndex < queue.count {
|
|
let current = queue[nextIndex]
|
|
nextIndex += 1
|
|
if let projects = self.projectsRoot(under: current.url, fileManager: fileManager) {
|
|
roots.append(projects)
|
|
}
|
|
|
|
guard current.depth < maxDepth else { continue }
|
|
for child in self.childDirectories(at: current.url, fileManager: fileManager) {
|
|
let standardized = child.standardizedFileURL
|
|
guard visited.insert(standardized.path).inserted else { continue }
|
|
queue.append((standardized, current.depth + 1))
|
|
}
|
|
}
|
|
return roots
|
|
}
|
|
|
|
private static func projectsRoot(under base: URL, fileManager: FileManager) -> URL? {
|
|
let projects = base
|
|
.appendingPathComponent(".claude", isDirectory: true)
|
|
.appendingPathComponent("projects", isDirectory: true)
|
|
var isDirectory: ObjCBool = false
|
|
guard fileManager.fileExists(atPath: projects.path, isDirectory: &isDirectory),
|
|
isDirectory.boolValue
|
|
else { return nil }
|
|
return projects.standardizedFileURL
|
|
}
|
|
|
|
private static func childDirectories(at url: URL, fileManager: FileManager) -> [URL] {
|
|
guard let children = try? fileManager.contentsOfDirectory(
|
|
at: url,
|
|
includingPropertiesForKeys: [.isDirectoryKey, .isSymbolicLinkKey],
|
|
options: [.skipsHiddenFiles, .skipsPackageDescendants])
|
|
else {
|
|
return []
|
|
}
|
|
|
|
return children.compactMap { child in
|
|
guard !self.skippedDirectoryNames.contains(child.lastPathComponent) else { return nil }
|
|
guard let values = try? child.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey]),
|
|
values.isSymbolicLink != true,
|
|
values.isDirectory == true
|
|
else {
|
|
return nil
|
|
}
|
|
return child
|
|
}
|
|
}
|
|
}
|