Skip to content

Commit b1773c1

Browse files
committed
Added support to scan for custom project/local path
1 parent eb1e66c commit b1773c1

File tree

12 files changed

+887
-18
lines changed

12 files changed

+887
-18
lines changed

Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ let package = Package(
2323
.unsafeFlags(["-default-isolation", "MainActor"]),
2424
.unsafeFlags(["-strict-concurrency=complete"]),
2525
.unsafeFlags(["-warn-concurrency"]),
26+
]),
27+
.testTarget(
28+
name: "CodexSkillManagerTests",
29+
dependencies: [],
30+
path: "Tests/CodexSkillManagerTests",
31+
swiftSettings: [
32+
.unsafeFlags(["-strict-concurrency=complete"]),
2633
])
2734
]
2835
)

Sources/CodexSkillManager/App/CodexSkillManagerApp.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@ import Sparkle
88
@main
99
struct CodexSkillManagerApp: App {
1010
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
11-
@State private var store = SkillStore()
11+
@State private var customPathStore: CustomPathStore
12+
@State private var store: SkillStore
1213
@State private var remoteStore = RemoteSkillStore(client: .live())
1314

15+
init() {
16+
let pathStore = CustomPathStore()
17+
_customPathStore = State(initialValue: pathStore)
18+
_store = State(initialValue: SkillStore(customPathStore: pathStore))
19+
}
20+
1421
var body: some Scene {
1522
WindowGroup("Codex Skill Manager") {
1623
SkillSplitView()
1724
.environment(store)
1825
.environment(remoteStore)
26+
.environment(customPathStore)
1927
}
2028
.commands {
2129
CommandGroup(after: .appInfo) {

Sources/CodexSkillManager/Skills/Local/Skill.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ struct Skill: Identifiable, Hashable, Sendable {
1818
let name: String
1919
let displayName: String
2020
let description: String
21-
let platform: SkillPlatform
21+
let platform: SkillPlatform?
22+
let customPath: CustomSkillPath?
2223
let folderURL: URL
2324
let skillMarkdownURL: URL
2425
let references: [SkillReference]

Sources/CodexSkillManager/Skills/Local/SkillStore.swift

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,23 @@ import Observation
5050
private let fileWorker = SkillFileWorker()
5151
private let importWorker = SkillImportWorker()
5252
private let cliWorker = ClawdhubCLIWorker()
53+
private let customPathStore: CustomPathStore
54+
55+
init(customPathStore: CustomPathStore = CustomPathStore()) {
56+
self.customPathStore = customPathStore
57+
}
58+
59+
var customPaths: [CustomSkillPath] {
60+
customPathStore.customPaths
61+
}
62+
63+
func addCustomPath(_ url: URL) throws {
64+
try customPathStore.addPath(url)
65+
}
66+
67+
func removeCustomPath(_ path: CustomSkillPath) {
68+
customPathStore.removePath(path)
69+
}
5370

5471
var selectedSkill: Skill? {
5572
skills.first { $0.id == selectedSkillID }
@@ -69,6 +86,8 @@ import Observation
6986
(platform, platform.rootURL, platform.storageKey)
7087
}
7188
var skills: [Skill] = []
89+
90+
// Scan platform paths
7291
for (platform, rootURL, storageKey) in platforms {
7392
let scanned = try await fileWorker.scanSkills(at: rootURL, storageKey: storageKey)
7493
skills.append(contentsOf: scanned.map { scannedSkill in
@@ -78,6 +97,7 @@ import Observation
7897
displayName: scannedSkill.displayName,
7998
description: scannedSkill.description,
8099
platform: platform,
100+
customPath: nil,
81101
folderURL: scannedSkill.folderURL,
82102
skillMarkdownURL: scannedSkill.skillMarkdownURL,
83103
references: scannedSkill.references,
@@ -86,6 +106,32 @@ import Observation
86106
})
87107
}
88108

109+
// Scan custom paths - auto-discover platform subpaths
110+
let fileManager = FileManager.default
111+
for customPath in customPathStore.customPaths {
112+
for platform in SkillPlatform.allCases {
113+
let platformURL = platform.skillsURL(in: customPath.url)
114+
guard fileManager.fileExists(atPath: platformURL.path) else { continue }
115+
116+
let storageKey = "\(customPath.storageKey)-\(platform.storageKey)"
117+
let scanned = try await fileWorker.scanSkills(at: platformURL, storageKey: storageKey)
118+
skills.append(contentsOf: scanned.map { scannedSkill in
119+
Skill(
120+
id: scannedSkill.id,
121+
name: scannedSkill.name,
122+
displayName: scannedSkill.displayName,
123+
description: scannedSkill.description,
124+
platform: platform,
125+
customPath: customPath,
126+
folderURL: scannedSkill.folderURL,
127+
skillMarkdownURL: scannedSkill.skillMarkdownURL,
128+
references: scannedSkill.references,
129+
stats: scannedSkill.stats
130+
)
131+
})
132+
}
133+
}
134+
89135
self.skills = skills.sorted {
90136
$0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending
91137
}
@@ -166,6 +212,10 @@ import Observation
166212
}
167213

168214
func isOwnedSkill(_ skill: Skill) -> Bool {
215+
// Skills from custom paths are always considered "owned"
216+
if skill.customPath != nil {
217+
return true
218+
}
169219
let originURL = skill.folderURL
170220
.appendingPathComponent(".clawdhub")
171221
.appendingPathComponent("origin.json")
@@ -185,7 +235,7 @@ import Observation
185235
}
186236

187237
func installedPlatforms(for slug: String) -> Set<SkillPlatform> {
188-
Set(skills.filter { $0.name == slug }.map(\.platform))
238+
Set(skills.filter { $0.name == slug }.compactMap(\.platform))
189239
}
190240

191241
func groupedLocalSkills(from filteredSkills: [Skill]) -> [LocalSkillGroup] {
@@ -205,10 +255,13 @@ import Observation
205255
.compactMap({ platform in filteredSkills.first(where: { $0.platform == platform }) })
206256
.first ?? filteredSkills.first ?? preferredSelection
207257

258+
// Collect platforms from all skills with the same slug
259+
let installedPlatforms = Set(allSkillsForSlug.compactMap(\.platform))
260+
208261
return LocalSkillGroup(
209262
id: preferredSelection.id,
210263
skill: preferredContent,
211-
installedPlatforms: Set(allSkillsForSlug.map(\.platform)),
264+
installedPlatforms: installedPlatforms,
212265
deleteIDs: allSkillsForSlug.map(\.id)
213266
)
214267
}
@@ -217,6 +270,14 @@ import Observation
217270
}
218271
}
219272

273+
func skillsForCustomPath(_ path: CustomSkillPath) -> [Skill] {
274+
skills.filter { $0.customPath?.id == path.id }
275+
}
276+
277+
func platformSkills() -> [Skill] {
278+
skills.filter { $0.platform != nil }
279+
}
280+
220281
func skillNeedsPublish(_ skill: Skill) async -> Bool {
221282
do {
222283
let hash = try await fileWorker.computeSkillHash(for: skill.folderURL)

0 commit comments

Comments
 (0)