Skip to content

Commit 8a98eb7

Browse files
fix(skills): follow symlinks when scanning roots (#20)
Some users symlink platform skill roots to a shared directory. URL-based directory enumeration can fail on directory symlinks, so scans returned empty. Resolve symlinks before enumerating and add a regression test using a symlinked skills root.
1 parent 22e7588 commit 8a98eb7

File tree

3 files changed

+42
-3
lines changed

3 files changed

+42
-3
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ let package = Package(
2626
]),
2727
.testTarget(
2828
name: "CodexSkillManagerTests",
29-
dependencies: [],
29+
dependencies: ["CodexSkillManager"],
3030
path: "Tests/CodexSkillManagerTests",
3131
swiftSettings: [
3232
.unsafeFlags(["-strict-concurrency=complete"]),

Sources/CodexSkillManager/Workers/SkillFileWorker.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,16 @@ actor SkillFileWorker {
8686

8787
func scanSkills(at baseURL: URL, storageKey: String) throws -> [ScannedSkillData] {
8888
let fileManager = FileManager.default
89-
guard fileManager.fileExists(atPath: baseURL.path) else {
89+
90+
// Directory symlinks can fail URL-based enumeration on macOS.
91+
let directoryURL = baseURL.resolvingSymlinksInPath()
92+
93+
guard fileManager.fileExists(atPath: directoryURL.path) else {
9094
return []
9195
}
9296

9397
let items = try fileManager.contentsOfDirectory(
94-
at: baseURL,
98+
at: directoryURL,
9599
includingPropertiesForKeys: [.isDirectoryKey],
96100
options: [.skipsHiddenFiles]
97101
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Foundation
2+
import Testing
3+
4+
@testable import CodexSkillManager
5+
6+
@Suite("Symlink Scan")
7+
struct SymlinkScanTests {
8+
@Test("scanSkills follows directory symlinks")
9+
func scanSkillsFollowsDirectorySymlinks() async throws {
10+
let fileManager = FileManager.default
11+
let tempRoot = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString)
12+
defer { try? fileManager.removeItem(at: tempRoot) }
13+
14+
let realRoot = tempRoot.appendingPathComponent("real")
15+
let symlinkRoot = tempRoot.appendingPathComponent("link")
16+
17+
try fileManager.createDirectory(at: realRoot, withIntermediateDirectories: true)
18+
19+
let skillRoot = realRoot.appendingPathComponent("my-skill")
20+
try fileManager.createDirectory(at: skillRoot, withIntermediateDirectories: true)
21+
try "# My Skill\n".write(
22+
to: skillRoot.appendingPathComponent("SKILL.md"),
23+
atomically: true,
24+
encoding: .utf8
25+
)
26+
27+
try fileManager.createSymbolicLink(at: symlinkRoot, withDestinationURL: realRoot)
28+
29+
let worker = SkillFileWorker()
30+
let scanned = try await worker.scanSkills(at: symlinkRoot, storageKey: "test")
31+
32+
#expect(scanned.count == 1)
33+
#expect(scanned.first?.name == "my-skill")
34+
}
35+
}

0 commit comments

Comments
 (0)