agents_skill_vault 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/lib/agents_skill_vault/manifest.rb +1 -1
- data/lib/agents_skill_vault/resource.rb +59 -42
- data/lib/agents_skill_vault/skill_validator.rb +32 -23
- data/lib/agents_skill_vault/url_parser.rb +73 -44
- data/lib/agents_skill_vault/vault/manifest_operations.rb +83 -0
- data/lib/agents_skill_vault/vault/resource_adder.rb +410 -0
- data/lib/agents_skill_vault/vault/resource_syncer.rb +199 -0
- data/lib/agents_skill_vault/vault/resource_validator.rb +102 -0
- data/lib/agents_skill_vault/vault.rb +10 -535
- data/lib/agents_skill_vault/version.rb +1 -1
- data/scripts/manual_test.rb +9 -9
- metadata +5 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentsSkillVault
|
|
4
|
+
class Vault
|
|
5
|
+
# Module for adding resources to the vault.
|
|
6
|
+
#
|
|
7
|
+
module ResourceAdder
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Adds a repository resource, scanning for all skills.
|
|
11
|
+
#
|
|
12
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
13
|
+
# @param label [String, nil] Custom label
|
|
14
|
+
# @return [Array<Resource>] Created resources (one per skill found)
|
|
15
|
+
#
|
|
16
|
+
def add_repository_resource(parsed_url, label:)
|
|
17
|
+
clone_repository(parsed_url)
|
|
18
|
+
skills = SkillScanner.scan_directory(local_repo_path(parsed_url))
|
|
19
|
+
|
|
20
|
+
if skills.empty?
|
|
21
|
+
add_non_skill_repo(parsed_url, label:)
|
|
22
|
+
else
|
|
23
|
+
add_skill_repo_resources(parsed_url, skills, label:)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Adds a folder resource.
|
|
28
|
+
#
|
|
29
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
30
|
+
# @param label [String, nil] Custom label
|
|
31
|
+
# @return [Resource, Array<Resource>] Created resource(s)
|
|
32
|
+
#
|
|
33
|
+
def add_folder_resource(parsed_url, label:)
|
|
34
|
+
setup_repo_folder(parsed_url)
|
|
35
|
+
target_path = File.join(local_repo_path(parsed_url), parsed_url.relative_path)
|
|
36
|
+
skills = SkillScanner.scan_directory(target_path)
|
|
37
|
+
|
|
38
|
+
case skills.length
|
|
39
|
+
when 0 then add_non_skill_folder(parsed_url, label:)
|
|
40
|
+
when 1 then add_single_skill_folder(parsed_url, skills.first, label:, target_path:)
|
|
41
|
+
else add_multi_skill_folder(parsed_url, skills, label:, target_path:)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Adds a file resource.
|
|
46
|
+
#
|
|
47
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
48
|
+
# @param label [String, nil] Custom label
|
|
49
|
+
# @return [Resource] Created resource
|
|
50
|
+
#
|
|
51
|
+
def add_file_resource(parsed_url, label:)
|
|
52
|
+
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
53
|
+
|
|
54
|
+
if parsed_url.skill_file?
|
|
55
|
+
add_skill_file(parsed_url, repo_url, label:)
|
|
56
|
+
else
|
|
57
|
+
add_non_skill_file(parsed_url, repo_url, label:)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Creates a resource from parsed URL with optional label.
|
|
62
|
+
#
|
|
63
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
64
|
+
# @param label [String, nil] Custom label
|
|
65
|
+
# @return [Resource] Created resource
|
|
66
|
+
#
|
|
67
|
+
def create_resource(parsed_url, label:)
|
|
68
|
+
label ||= parsed_url.label
|
|
69
|
+
|
|
70
|
+
Resource.new(
|
|
71
|
+
label: label,
|
|
72
|
+
url: "https://github.com/#{parsed_url.username}/#{parsed_url.repo}",
|
|
73
|
+
username: parsed_url.username,
|
|
74
|
+
repo: parsed_url.repo,
|
|
75
|
+
folder: parsed_url.type == :repo ? nil : File.basename(parsed_url.relative_path || ""),
|
|
76
|
+
type: parsed_url.type,
|
|
77
|
+
branch: parsed_url.branch,
|
|
78
|
+
relative_path: parsed_url.relative_path,
|
|
79
|
+
storage_path: storage_path
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Clones a repository to the vault.
|
|
84
|
+
#
|
|
85
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
86
|
+
# @return [void]
|
|
87
|
+
#
|
|
88
|
+
def clone_repository(parsed_url)
|
|
89
|
+
target_path = local_repo_path(parsed_url)
|
|
90
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
|
91
|
+
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
92
|
+
GitOperations.clone_repo(repo_url, target_path, branch: parsed_url.branch)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Adds a non-skill repository resource.
|
|
96
|
+
#
|
|
97
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
98
|
+
# @param label [String, nil] Custom label
|
|
99
|
+
# @return [Array<Resource>] Created resources
|
|
100
|
+
#
|
|
101
|
+
def add_non_skill_repo(parsed_url, label:)
|
|
102
|
+
resource = create_resource(parsed_url, label: label || parsed_url.label)
|
|
103
|
+
resource = Resource.from_h(
|
|
104
|
+
resource.to_h.merge(
|
|
105
|
+
validation_status: :not_a_skill,
|
|
106
|
+
is_skill: false
|
|
107
|
+
),
|
|
108
|
+
storage_path: storage_path
|
|
109
|
+
)
|
|
110
|
+
manifest.add_resource(resource)
|
|
111
|
+
[resource]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Adds skill resources for a repository.
|
|
115
|
+
#
|
|
116
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
117
|
+
# @param skills [Array<Hash>] List of skills found
|
|
118
|
+
# @param label [String, nil] Custom label
|
|
119
|
+
# @return [Array<Resource>] Created resources
|
|
120
|
+
#
|
|
121
|
+
def add_skill_repo_resources(parsed_url, skills, label:)
|
|
122
|
+
target_path = local_repo_path(parsed_url)
|
|
123
|
+
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
124
|
+
|
|
125
|
+
skills.map do |skill|
|
|
126
|
+
skill_label = if label
|
|
127
|
+
"#{label}-#{skill[:skill_name]}"
|
|
128
|
+
else
|
|
129
|
+
"#{parsed_url.username}/#{parsed_url.repo}/#{skill[:skill_name]}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
resource = build_skill_resource(
|
|
133
|
+
skill_label, repo_url, parsed_url, skill[:folder_path],
|
|
134
|
+
skill[:folder_path], target_path
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
validate_and_update_resource(resource, skill[:folder_path], target_path)
|
|
138
|
+
manifest.add_resource(resource)
|
|
139
|
+
resource
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Adds a non-skill folder resource.
|
|
144
|
+
#
|
|
145
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
146
|
+
# @param label [String, nil] Custom label
|
|
147
|
+
# @return [Resource] Created resource
|
|
148
|
+
#
|
|
149
|
+
def add_non_skill_folder(parsed_url, label:)
|
|
150
|
+
skill_name = File.basename(parsed_url.relative_path)
|
|
151
|
+
resource_label = label || "#{parsed_url.username}/#{parsed_url.repo}/#{skill_name}"
|
|
152
|
+
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
153
|
+
|
|
154
|
+
resource = Resource.new(
|
|
155
|
+
label: resource_label,
|
|
156
|
+
url: repo_url,
|
|
157
|
+
username: parsed_url.username,
|
|
158
|
+
repo: parsed_url.repo,
|
|
159
|
+
folder: parsed_url.relative_path,
|
|
160
|
+
type: :folder,
|
|
161
|
+
branch: parsed_url.branch,
|
|
162
|
+
relative_path: parsed_url.relative_path,
|
|
163
|
+
storage_path: storage_path,
|
|
164
|
+
validation_status: :not_a_skill,
|
|
165
|
+
validation_errors: [],
|
|
166
|
+
skill_name: nil,
|
|
167
|
+
is_skill: false
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
manifest.add_resource(resource)
|
|
171
|
+
resource
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Adds a single skill folder resource.
|
|
175
|
+
#
|
|
176
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
177
|
+
# @param skill [Hash] Skill data
|
|
178
|
+
# @param label [String, nil] Custom label
|
|
179
|
+
# @param target_path [String] Target path for validation
|
|
180
|
+
# @return [Resource] Created resource
|
|
181
|
+
#
|
|
182
|
+
def add_single_skill_folder(parsed_url, skill, label:, target_path:)
|
|
183
|
+
skill_name = skill[:skill_name]
|
|
184
|
+
resource_label = label || "#{parsed_url.username}/#{parsed_url.repo}/#{skill_name}"
|
|
185
|
+
|
|
186
|
+
skill_relative_path = if skill[:relative_path] == "."
|
|
187
|
+
parsed_url.relative_path
|
|
188
|
+
else
|
|
189
|
+
File.join(parsed_url.relative_path, skill[:relative_path])
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
193
|
+
|
|
194
|
+
resource = build_skill_resource(
|
|
195
|
+
resource_label, repo_url, parsed_url, parsed_url.relative_path,
|
|
196
|
+
skill_relative_path, target_path, skill[:relative_path]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
validate_and_update_resource(resource, skill[:relative_path], target_path)
|
|
200
|
+
manifest.add_resource(resource)
|
|
201
|
+
resource
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Adds multiple skill folder resources.
|
|
205
|
+
#
|
|
206
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
207
|
+
# @param skills [Array<Hash>] List of skills found
|
|
208
|
+
# @param label [String, nil] Custom label
|
|
209
|
+
# @param target_path [String] Target path for validation
|
|
210
|
+
# @return [Array<Resource>] Created resources
|
|
211
|
+
#
|
|
212
|
+
def add_multi_skill_folder(parsed_url, skills, label:, target_path:)
|
|
213
|
+
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
214
|
+
|
|
215
|
+
skills.map do |skill|
|
|
216
|
+
skill_name = skill[:skill_name]
|
|
217
|
+
skill_label = label ? "#{label}/#{skill_name}" : "#{parsed_url.username}/#{parsed_url.repo}/#{skill_name}"
|
|
218
|
+
|
|
219
|
+
skill_relative_path = if skill[:relative_path] == "."
|
|
220
|
+
parsed_url.relative_path
|
|
221
|
+
else
|
|
222
|
+
File.join(parsed_url.relative_path, skill[:relative_path])
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
resource = build_skill_resource(
|
|
226
|
+
skill_label, repo_url, parsed_url, parsed_url.relative_path,
|
|
227
|
+
skill_relative_path, target_path, skill[:relative_path]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
validate_and_update_resource(resource, skill[:relative_path], target_path)
|
|
231
|
+
manifest.add_resource(resource)
|
|
232
|
+
resource
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Adds a skill file resource.
|
|
237
|
+
#
|
|
238
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
239
|
+
# @param repo_url [String] Repository URL
|
|
240
|
+
# @param label [String, nil] Custom label
|
|
241
|
+
# @return [Resource] Created resource
|
|
242
|
+
#
|
|
243
|
+
def add_skill_file(parsed_url, repo_url, label:)
|
|
244
|
+
setup_skill_file_path(parsed_url)
|
|
245
|
+
target_path = File.join(local_repo_path(parsed_url), parsed_url.skill_folder_path)
|
|
246
|
+
result = SkillValidator.validate(File.join(target_path, "SKILL.md"))
|
|
247
|
+
|
|
248
|
+
resource = create_skill_file_resource(parsed_url, repo_url, label, result)
|
|
249
|
+
manifest.add_resource(resource)
|
|
250
|
+
resource
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Creates a skill file resource.
|
|
254
|
+
#
|
|
255
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
256
|
+
# @param repo_url [String] Repository URL
|
|
257
|
+
# @param label [String, nil] Custom label
|
|
258
|
+
# @param result [Hash] Validation result
|
|
259
|
+
# @return [Resource] Created resource
|
|
260
|
+
#
|
|
261
|
+
def create_skill_file_resource(parsed_url, repo_url, label, result)
|
|
262
|
+
resource_label = label || "#{parsed_url.username}/#{parsed_url.repo}/#{parsed_url.skill_name}"
|
|
263
|
+
|
|
264
|
+
Resource.new(
|
|
265
|
+
label: resource_label,
|
|
266
|
+
url: repo_url,
|
|
267
|
+
username: parsed_url.username,
|
|
268
|
+
repo: parsed_url.repo,
|
|
269
|
+
folder: parsed_url.skill_folder_path,
|
|
270
|
+
type: :file,
|
|
271
|
+
branch: parsed_url.branch,
|
|
272
|
+
relative_path: parsed_url.skill_folder_path,
|
|
273
|
+
storage_path: storage_path,
|
|
274
|
+
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
275
|
+
validation_errors: result[:errors],
|
|
276
|
+
skill_name: parsed_url.skill_name,
|
|
277
|
+
is_skill: true
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Adds a non-skill file resource.
|
|
282
|
+
#
|
|
283
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
284
|
+
# @param repo_url [String] Repository URL
|
|
285
|
+
# @param label [String, nil] Custom label
|
|
286
|
+
# @return [Resource] Created resource
|
|
287
|
+
#
|
|
288
|
+
def add_non_skill_file(parsed_url, _repo_url, label:)
|
|
289
|
+
parent_path = File.dirname(parsed_url.relative_path)
|
|
290
|
+
setup_non_skill_file_path(parsed_url, parent_path)
|
|
291
|
+
|
|
292
|
+
resource = Resource.new(
|
|
293
|
+
label: label || parsed_url.label,
|
|
294
|
+
url: parsed_url.url,
|
|
295
|
+
username: parsed_url.username,
|
|
296
|
+
repo: parsed_url.repo,
|
|
297
|
+
folder: parent_path,
|
|
298
|
+
type: :file,
|
|
299
|
+
branch: parsed_url.branch,
|
|
300
|
+
relative_path: parsed_url.relative_path,
|
|
301
|
+
storage_path: storage_path,
|
|
302
|
+
validation_status: :not_a_skill,
|
|
303
|
+
validation_errors: [],
|
|
304
|
+
skill_name: nil,
|
|
305
|
+
is_skill: false
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
manifest.add_resource(resource)
|
|
309
|
+
resource
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Builds a skill resource from parsed data.
|
|
313
|
+
#
|
|
314
|
+
# @param label [String] Resource label
|
|
315
|
+
# @param repo_url [String] Repository URL
|
|
316
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
317
|
+
# @param folder [String] Folder path
|
|
318
|
+
# @param relative_path [String] Relative path
|
|
319
|
+
# @param target_path [String] Target path for type detection
|
|
320
|
+
# @param skill_relative_path [String, nil] Relative path for skill
|
|
321
|
+
# @return [Resource] Created resource
|
|
322
|
+
#
|
|
323
|
+
def build_skill_resource(label, repo_url, parsed_url, folder, relative_path, _target_path,
|
|
324
|
+
skill_relative_path = nil)
|
|
325
|
+
Resource.new(
|
|
326
|
+
label: label,
|
|
327
|
+
url: repo_url,
|
|
328
|
+
username: parsed_url.username,
|
|
329
|
+
repo: parsed_url.repo,
|
|
330
|
+
folder: folder,
|
|
331
|
+
type: parsed_url.type,
|
|
332
|
+
branch: parsed_url.branch,
|
|
333
|
+
relative_path: relative_path,
|
|
334
|
+
storage_path: storage_path,
|
|
335
|
+
skill_name: skill_relative_path ? label.split("/").last : nil,
|
|
336
|
+
is_skill: true
|
|
337
|
+
)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Validates and updates a resource.
|
|
341
|
+
#
|
|
342
|
+
# @param resource [Resource] Resource to validate
|
|
343
|
+
# @param relative_path [String] Relative path for validation
|
|
344
|
+
# @param target_path [String] Target path
|
|
345
|
+
# @return [Resource] Updated resource
|
|
346
|
+
#
|
|
347
|
+
def validate_and_update_resource(resource, relative_path, target_path)
|
|
348
|
+
skill_file = File.join(target_path, relative_path, "SKILL.md")
|
|
349
|
+
result = SkillValidator.validate(skill_file)
|
|
350
|
+
|
|
351
|
+
Resource.from_h(
|
|
352
|
+
resource.to_h.merge(
|
|
353
|
+
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
354
|
+
validation_errors: result[:errors]
|
|
355
|
+
),
|
|
356
|
+
storage_path: storage_path
|
|
357
|
+
)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Returns the local path for a repository.
|
|
361
|
+
#
|
|
362
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
363
|
+
# @return [String] Local path
|
|
364
|
+
#
|
|
365
|
+
def local_repo_path(parsed_url)
|
|
366
|
+
File.join(storage_path, parsed_url.username, parsed_url.repo)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Sets up repository folder for sparse checkout.
|
|
370
|
+
#
|
|
371
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
372
|
+
# @return [void]
|
|
373
|
+
#
|
|
374
|
+
def setup_repo_folder(parsed_url)
|
|
375
|
+
repo_path = local_repo_path(parsed_url)
|
|
376
|
+
FileUtils.mkdir_p(repo_path)
|
|
377
|
+
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
378
|
+
paths = [parsed_url.relative_path]
|
|
379
|
+
GitOperations.sparse_checkout(repo_url, repo_path, branch: parsed_url.branch, paths: paths)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Sets up skill file path for download.
|
|
383
|
+
#
|
|
384
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
385
|
+
# @return [void]
|
|
386
|
+
#
|
|
387
|
+
def setup_skill_file_path(parsed_url)
|
|
388
|
+
repo_path = local_repo_path(parsed_url)
|
|
389
|
+
FileUtils.mkdir_p(repo_path)
|
|
390
|
+
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
391
|
+
paths = [parsed_url.skill_folder_path]
|
|
392
|
+
GitOperations.sparse_checkout(repo_url, repo_path, branch: parsed_url.branch, paths: paths)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Sets up non-skill file path for download.
|
|
396
|
+
#
|
|
397
|
+
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
398
|
+
# @param parent_path [String] Parent path
|
|
399
|
+
# @return [void]
|
|
400
|
+
#
|
|
401
|
+
def setup_non_skill_file_path(parsed_url, parent_path)
|
|
402
|
+
repo_path = local_repo_path(parsed_url)
|
|
403
|
+
FileUtils.mkdir_p(repo_path)
|
|
404
|
+
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
405
|
+
paths = [parent_path]
|
|
406
|
+
GitOperations.sparse_checkout(repo_url, repo_path, branch: parsed_url.branch, paths: paths)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentsSkillVault
|
|
4
|
+
class Vault
|
|
5
|
+
# Module for syncing resources in vault.
|
|
6
|
+
#
|
|
7
|
+
module ResourceSyncer
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Syncs a resource by pulling latest changes.
|
|
11
|
+
#
|
|
12
|
+
# @param resource [Resource] Resource to sync
|
|
13
|
+
# @return [SyncResult] Sync result
|
|
14
|
+
#
|
|
15
|
+
def sync_resource(resource)
|
|
16
|
+
return SyncResult.new(success: false, error: "Resource has no local path") unless resource.local_path
|
|
17
|
+
|
|
18
|
+
unless Dir.exist?(resource.local_path)
|
|
19
|
+
return SyncResult.new(success: false, error: "Path does not exist: #{resource.local_path}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sync_by_type(resource)
|
|
23
|
+
update_synced_at(resource)
|
|
24
|
+
|
|
25
|
+
SyncResult.new(success: true, changes: true)
|
|
26
|
+
rescue Errors::Error => e
|
|
27
|
+
SyncResult.new(success: false, error: e.message)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Syncs a repository-type resource, re-scanning for skills.
|
|
31
|
+
#
|
|
32
|
+
# @param resource [Resource] The resource to sync
|
|
33
|
+
# @return [void]
|
|
34
|
+
#
|
|
35
|
+
def sync_repository_resource(resource)
|
|
36
|
+
skills = SkillScanner.scan_directory(resource.local_path)
|
|
37
|
+
existing_resources = repo_resources(resource)
|
|
38
|
+
|
|
39
|
+
skills.each do |skill|
|
|
40
|
+
process_skill_sync(resource, skill, existing_resources)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Downloads a resource from GitHub.
|
|
45
|
+
#
|
|
46
|
+
# @param resource [Resource] Resource to download
|
|
47
|
+
# @return [void]
|
|
48
|
+
#
|
|
49
|
+
def download_resource(resource)
|
|
50
|
+
target_path = resource.local_path
|
|
51
|
+
parent_dir = File.dirname(target_path)
|
|
52
|
+
FileUtils.mkdir_p(parent_dir)
|
|
53
|
+
|
|
54
|
+
if resource.type == :repo
|
|
55
|
+
GitOperations.clone_repo(resource.url, target_path, branch: resource.branch)
|
|
56
|
+
else
|
|
57
|
+
paths = [resource.relative_path]
|
|
58
|
+
GitOperations.sparse_checkout(resource.url, target_path, branch: resource.branch, paths: paths)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Syncs resource based on its type.
|
|
63
|
+
#
|
|
64
|
+
# @param resource [Resource] Resource to sync
|
|
65
|
+
# @return [void]
|
|
66
|
+
#
|
|
67
|
+
def sync_by_type(resource)
|
|
68
|
+
GitOperations.pull(resource.local_path)
|
|
69
|
+
|
|
70
|
+
if resource.type == :repo
|
|
71
|
+
sync_repository_resource(resource)
|
|
72
|
+
else
|
|
73
|
+
validate_resource(resource.label)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Updates synced_at timestamp for all repo resources.
|
|
78
|
+
#
|
|
79
|
+
# @param resource [Resource] Resource to update
|
|
80
|
+
# @return [void]
|
|
81
|
+
#
|
|
82
|
+
def update_synced_at(resource)
|
|
83
|
+
repo_resources = list.select { |r| r.repo == resource.repo && r.username == resource.username }
|
|
84
|
+
repo_resources.each do |r|
|
|
85
|
+
updated = Resource.from_h(r.to_h.merge(synced_at: Time.now), storage_path: storage_path)
|
|
86
|
+
manifest.update_resource(updated)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns all resources for a repository.
|
|
91
|
+
#
|
|
92
|
+
# @param resource [Resource] Resource to match
|
|
93
|
+
# @return [Array<Resource>] Matching resources
|
|
94
|
+
#
|
|
95
|
+
def repo_resources(resource)
|
|
96
|
+
list.select do |r|
|
|
97
|
+
r.repo == resource.repo && r.username == resource.username && r.type == :repo
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Processes skill during sync (add or update).
|
|
102
|
+
#
|
|
103
|
+
# @param resource [Resource] Parent resource
|
|
104
|
+
# @param skill [Hash] Skill data
|
|
105
|
+
# @param existing_resources [Array<Resource>] Existing resources
|
|
106
|
+
# @return [void]
|
|
107
|
+
#
|
|
108
|
+
def process_skill_sync(resource, skill, existing_resources)
|
|
109
|
+
skill_label = "#{resource.username}/#{resource.repo}/#{skill[:skill_name]}"
|
|
110
|
+
existing = existing_resources.find { |r| r.skill_name == skill[:skill_name] }
|
|
111
|
+
|
|
112
|
+
if existing
|
|
113
|
+
revalidate_existing_skill(resource, existing, skill)
|
|
114
|
+
else
|
|
115
|
+
add_new_skill_from_sync(resource, skill, skill_label)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Re-validates an existing skill.
|
|
120
|
+
#
|
|
121
|
+
# @param resource [Resource] Parent resource
|
|
122
|
+
# @param existing [Resource] Existing skill resource
|
|
123
|
+
# @param skill [Hash] Skill data
|
|
124
|
+
# @return [void]
|
|
125
|
+
#
|
|
126
|
+
def revalidate_existing_skill(resource, existing, skill)
|
|
127
|
+
skill_file = File.join(resource.local_path, skill[:folder_path], "SKILL.md")
|
|
128
|
+
result = SkillValidator.validate(skill_file)
|
|
129
|
+
|
|
130
|
+
updated = Resource.from_h(
|
|
131
|
+
existing.to_h.merge(
|
|
132
|
+
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
133
|
+
validation_errors: result[:errors]
|
|
134
|
+
),
|
|
135
|
+
storage_path: storage_path
|
|
136
|
+
)
|
|
137
|
+
manifest.update_resource(updated)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Adds a new skill found during sync.
|
|
141
|
+
#
|
|
142
|
+
# @param resource [Resource] Parent resource
|
|
143
|
+
# @param skill [Hash] Skill data
|
|
144
|
+
# @param skill_label [String] Skill label
|
|
145
|
+
# @return [void]
|
|
146
|
+
#
|
|
147
|
+
def add_new_skill_from_sync(resource, skill, skill_label)
|
|
148
|
+
new_resource = create_new_skill_resource(resource, skill, skill_label)
|
|
149
|
+
new_resource = validate_and_update_skill(resource, new_resource, skill)
|
|
150
|
+
manifest.add_resource(new_resource)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Creates a new skill resource.
|
|
154
|
+
#
|
|
155
|
+
# @param resource [Resource] Parent resource
|
|
156
|
+
# @param skill [Hash] Skill data
|
|
157
|
+
# @param skill_label [String] Skill label
|
|
158
|
+
# @return [Resource] Created resource
|
|
159
|
+
#
|
|
160
|
+
def create_new_skill_resource(resource, skill, skill_label)
|
|
161
|
+
Resource.new(
|
|
162
|
+
label: skill_label,
|
|
163
|
+
url: resource.url,
|
|
164
|
+
username: resource.username,
|
|
165
|
+
repo: resource.repo,
|
|
166
|
+
folder: skill[:folder_path],
|
|
167
|
+
type: :repo,
|
|
168
|
+
branch: resource.branch,
|
|
169
|
+
relative_path: skill[:folder_path],
|
|
170
|
+
storage_path: storage_path,
|
|
171
|
+
validation_status: :unvalidated,
|
|
172
|
+
validation_errors: [],
|
|
173
|
+
skill_name: skill[:skill_name],
|
|
174
|
+
is_skill: true
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Validates and updates a skill resource.
|
|
179
|
+
#
|
|
180
|
+
# @param resource [Resource] Parent resource
|
|
181
|
+
# @param new_resource [Resource] Skill resource to validate
|
|
182
|
+
# @param skill [Hash] Skill data
|
|
183
|
+
# @return [Resource] Updated resource
|
|
184
|
+
#
|
|
185
|
+
def validate_and_update_skill(resource, new_resource, skill)
|
|
186
|
+
skill_file = File.join(resource.local_path, skill[:folder_path], "SKILL.md")
|
|
187
|
+
result = SkillValidator.validate(skill_file)
|
|
188
|
+
|
|
189
|
+
Resource.from_h(
|
|
190
|
+
new_resource.to_h.merge(
|
|
191
|
+
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
192
|
+
validation_errors: result[:errors]
|
|
193
|
+
),
|
|
194
|
+
storage_path: storage_path
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentsSkillVault
|
|
4
|
+
class Vault
|
|
5
|
+
# Module for validating resources in vault.
|
|
6
|
+
#
|
|
7
|
+
module ResourceValidator
|
|
8
|
+
# Validates a specific resource.
|
|
9
|
+
#
|
|
10
|
+
# Re-validates the skill file and updates the validation status.
|
|
11
|
+
#
|
|
12
|
+
# @param label [String] The label of the resource to validate
|
|
13
|
+
# @return [Resource] The updated resource
|
|
14
|
+
# @raise [Errors::NotFound] if the resource doesn't exist
|
|
15
|
+
#
|
|
16
|
+
def validate_resource(label)
|
|
17
|
+
resource = fetch(label)
|
|
18
|
+
|
|
19
|
+
updated_resource = if resource.is_skill
|
|
20
|
+
validate_skill_resource(resource)
|
|
21
|
+
else
|
|
22
|
+
Resource.from_h(
|
|
23
|
+
resource.to_h.merge(validation_status: :not_a_skill),
|
|
24
|
+
storage_path: storage_path
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
manifest.update_resource(updated_resource)
|
|
29
|
+
updated_resource
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Validates all resources in the vault.
|
|
33
|
+
#
|
|
34
|
+
# Re-validates all resources and updates their validation status.
|
|
35
|
+
#
|
|
36
|
+
# @return [Hash] Summary of validation results
|
|
37
|
+
# - :valid [Integer] Count of valid skills
|
|
38
|
+
# - :invalid [Integer] Count of invalid skills
|
|
39
|
+
# - :not_a_skill [Integer] Count of non-skill resources
|
|
40
|
+
# - :unvalidated [Integer] Count of resources that couldn't be validated
|
|
41
|
+
#
|
|
42
|
+
def validate_all
|
|
43
|
+
results = { valid: 0, invalid: 0, not_a_skill: 0, unvalidated: 0 }
|
|
44
|
+
|
|
45
|
+
list.each do |resource|
|
|
46
|
+
validate_resource(resource.label)
|
|
47
|
+
|
|
48
|
+
case resource.validation_status
|
|
49
|
+
when :valid_skill
|
|
50
|
+
results[:valid] += 1
|
|
51
|
+
when :invalid_skill
|
|
52
|
+
results[:invalid] += 1
|
|
53
|
+
when :not_a_skill
|
|
54
|
+
results[:not_a_skill] += 1
|
|
55
|
+
else
|
|
56
|
+
results[:unvalidated] += 1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
results
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Removes all invalid skills from the vault.
|
|
64
|
+
#
|
|
65
|
+
# Deletes local files and removes from manifest for all resources with
|
|
66
|
+
# validation_status == :invalid_skill.
|
|
67
|
+
#
|
|
68
|
+
# @return [Integer] Number of skills removed
|
|
69
|
+
#
|
|
70
|
+
def cleanup_invalid_skills
|
|
71
|
+
invalid_resources = list_invalid_skills
|
|
72
|
+
|
|
73
|
+
invalid_resources.each do |resource|
|
|
74
|
+
FileUtils.rm_rf(resource.local_path) if resource.local_path
|
|
75
|
+
manifest.remove_resource(resource.label)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
invalid_resources.size
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Validates a skill resource.
|
|
84
|
+
#
|
|
85
|
+
# @param resource [Resource] Skill resource to validate
|
|
86
|
+
# @return [Resource] Updated resource
|
|
87
|
+
#
|
|
88
|
+
def validate_skill_resource(resource)
|
|
89
|
+
skill_file = File.join(resource.local_path, "SKILL.md")
|
|
90
|
+
result = SkillValidator.validate(skill_file)
|
|
91
|
+
|
|
92
|
+
Resource.from_h(
|
|
93
|
+
resource.to_h.merge(
|
|
94
|
+
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
95
|
+
validation_errors: result[:errors]
|
|
96
|
+
),
|
|
97
|
+
storage_path: storage_path
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|