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
|
@@ -20,6 +20,11 @@ module AgentsSkillVault
|
|
|
20
20
|
# vault.sync("user/repo") # => SyncResult
|
|
21
21
|
#
|
|
22
22
|
class Vault
|
|
23
|
+
include ResourceAdder
|
|
24
|
+
include ResourceSyncer
|
|
25
|
+
include ResourceValidator
|
|
26
|
+
include ManifestOperations
|
|
27
|
+
|
|
23
28
|
# @return [String] The absolute path to the vault's storage directory
|
|
24
29
|
attr_reader :storage_path
|
|
25
30
|
|
|
@@ -46,7 +51,6 @@ module AgentsSkillVault
|
|
|
46
51
|
GitOperations.check_git_available!
|
|
47
52
|
GitOperations.check_git_version!
|
|
48
53
|
|
|
49
|
-
# Auto-validate unvalidated resources if manifest exists
|
|
50
54
|
validate_all if File.exist?(@manifest_path) && !list_unvalidated.empty?
|
|
51
55
|
end
|
|
52
56
|
|
|
@@ -102,7 +106,7 @@ module AgentsSkillVault
|
|
|
102
106
|
# Filters resources by GitHub username.
|
|
103
107
|
#
|
|
104
108
|
# @param username [String] The GitHub username to filter by
|
|
105
|
-
# @return [Array<Resource>] Resources owned by
|
|
109
|
+
# @return [Array<Resource>] Resources owned by specified user
|
|
106
110
|
#
|
|
107
111
|
# @example
|
|
108
112
|
# vault.filter_by_username("octocat")
|
|
@@ -159,7 +163,7 @@ module AgentsSkillVault
|
|
|
159
163
|
# Filters resources by skill name.
|
|
160
164
|
#
|
|
161
165
|
# @param name [String] The skill name to filter by
|
|
162
|
-
# @return [Array<Resource>] Resources matching
|
|
166
|
+
# @return [Array<Resource>] Resources matching skill name
|
|
163
167
|
#
|
|
164
168
|
def filter_by_skill_name(name)
|
|
165
169
|
list.select { |r| r.skill_name == name }
|
|
@@ -167,7 +171,7 @@ module AgentsSkillVault
|
|
|
167
171
|
|
|
168
172
|
# Finds a resource by its label without raising an error.
|
|
169
173
|
#
|
|
170
|
-
# @param label [String] The unique label of
|
|
174
|
+
# @param label [String] The unique label of resource
|
|
171
175
|
# @return [Resource, nil] The resource if found, nil otherwise
|
|
172
176
|
#
|
|
173
177
|
# @example
|
|
@@ -180,7 +184,7 @@ module AgentsSkillVault
|
|
|
180
184
|
|
|
181
185
|
# Retrieves a resource by its label, raising an error if not found.
|
|
182
186
|
#
|
|
183
|
-
# @param label [String] The unique label of
|
|
187
|
+
# @param label [String] The unique label of resource
|
|
184
188
|
# @return [Resource] The resource with the specified label
|
|
185
189
|
# @raise [Errors::NotFound] if no resource with the label exists
|
|
186
190
|
#
|
|
@@ -231,7 +235,7 @@ module AgentsSkillVault
|
|
|
231
235
|
results
|
|
232
236
|
end
|
|
233
237
|
|
|
234
|
-
# Removes a resource from
|
|
238
|
+
# Removes a resource from vault.
|
|
235
239
|
#
|
|
236
240
|
# @param label [String] The label of the resource to remove
|
|
237
241
|
# @param delete_files [Boolean] Whether to also delete the local files (default: false)
|
|
@@ -251,539 +255,10 @@ module AgentsSkillVault
|
|
|
251
255
|
manifest.remove_resource(label)
|
|
252
256
|
end
|
|
253
257
|
|
|
254
|
-
# Validates a specific resource.
|
|
255
|
-
#
|
|
256
|
-
# Re-validates the skill file and updates the validation status.
|
|
257
|
-
#
|
|
258
|
-
# @param label [String] The label of the resource to validate
|
|
259
|
-
# @return [Resource] The updated resource
|
|
260
|
-
# @raise [Errors::NotFound] if the resource doesn't exist
|
|
261
|
-
#
|
|
262
|
-
def validate_resource(label)
|
|
263
|
-
resource = fetch(label)
|
|
264
|
-
|
|
265
|
-
if resource.is_skill
|
|
266
|
-
skill_file = File.join(resource.local_path, "SKILL.md")
|
|
267
|
-
result = SkillValidator.validate(skill_file)
|
|
268
|
-
|
|
269
|
-
updated_resource = Resource.from_h(
|
|
270
|
-
resource.to_h.merge(
|
|
271
|
-
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
272
|
-
validation_errors: result[:errors]
|
|
273
|
-
),
|
|
274
|
-
storage_path: storage_path
|
|
275
|
-
)
|
|
276
|
-
else
|
|
277
|
-
updated_resource = Resource.from_h(
|
|
278
|
-
resource.to_h.merge(validation_status: :not_a_skill),
|
|
279
|
-
storage_path: storage_path
|
|
280
|
-
)
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
manifest.update_resource(updated_resource)
|
|
284
|
-
updated_resource
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Validates all resources in the vault.
|
|
288
|
-
#
|
|
289
|
-
# Re-validates all resources and updates their validation status.
|
|
290
|
-
#
|
|
291
|
-
# @return [Hash] Summary of validation results
|
|
292
|
-
# - :valid [Integer] Count of valid skills
|
|
293
|
-
# - :invalid [Integer] Count of invalid skills
|
|
294
|
-
# - :not_a_skill [Integer] Count of non-skill resources
|
|
295
|
-
# - :unvalidated [Integer] Count of resources that couldn't be validated
|
|
296
|
-
#
|
|
297
|
-
def validate_all
|
|
298
|
-
results = { valid: 0, invalid: 0, not_a_skill: 0, unvalidated: 0 }
|
|
299
|
-
|
|
300
|
-
list.each do |resource|
|
|
301
|
-
validate_resource(resource.label)
|
|
302
|
-
|
|
303
|
-
case resource.validation_status
|
|
304
|
-
when :valid_skill
|
|
305
|
-
results[:valid] += 1
|
|
306
|
-
when :invalid_skill
|
|
307
|
-
results[:invalid] += 1
|
|
308
|
-
when :not_a_skill
|
|
309
|
-
results[:not_a_skill] += 1
|
|
310
|
-
else
|
|
311
|
-
results[:unvalidated] += 1
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
results
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
# Removes all invalid skills from the vault.
|
|
319
|
-
#
|
|
320
|
-
# Deletes local files and removes from manifest for all resources with
|
|
321
|
-
# validation_status == :invalid_skill.
|
|
322
|
-
#
|
|
323
|
-
# @return [Integer] Number of skills removed
|
|
324
|
-
#
|
|
325
|
-
def cleanup_invalid_skills
|
|
326
|
-
invalid_resources = list_invalid_skills
|
|
327
|
-
|
|
328
|
-
invalid_resources.each do |resource|
|
|
329
|
-
FileUtils.rm_rf(resource.local_path) if resource.local_path
|
|
330
|
-
manifest.remove_resource(resource.label)
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
invalid_resources.size
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
# Re-downloads all resources from scratch.
|
|
337
|
-
#
|
|
338
|
-
# Deletes local files and performs a fresh clone/checkout for each resource.
|
|
339
|
-
# Useful for recovering from corruption or ensuring a clean state.
|
|
340
|
-
#
|
|
341
|
-
# @return [void]
|
|
342
|
-
#
|
|
343
|
-
def redownload_all
|
|
344
|
-
list.each do |resource|
|
|
345
|
-
FileUtils.rm_rf(resource.local_path) if resource.local_path
|
|
346
|
-
download_resource(resource)
|
|
347
|
-
updated_resource = Resource.from_h(resource.to_h.merge(synced_at: Time.now), storage_path: storage_path)
|
|
348
|
-
manifest.update_resource(updated_resource)
|
|
349
|
-
end
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
# Exports the manifest to a file for backup or sharing.
|
|
353
|
-
#
|
|
354
|
-
# @param export_path [String] Path where the manifest should be exported
|
|
355
|
-
# @return [void]
|
|
356
|
-
#
|
|
357
|
-
# @example
|
|
358
|
-
# vault.export_manifest("/backups/manifest.json")
|
|
359
|
-
#
|
|
360
|
-
def export_manifest(export_path)
|
|
361
|
-
FileUtils.cp(@manifest_path, export_path)
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
# Imports and merges a manifest from another vault.
|
|
365
|
-
#
|
|
366
|
-
# Resources from the imported manifest are merged with existing resources.
|
|
367
|
-
# If a label exists in both, the imported resource overwrites the existing one.
|
|
368
|
-
# Note: This only updates the manifest; files are not downloaded automatically.
|
|
369
|
-
#
|
|
370
|
-
# @param import_path [String] Path to the manifest file to import
|
|
371
|
-
# @return [void]
|
|
372
|
-
#
|
|
373
|
-
# @example
|
|
374
|
-
# vault.import_manifest("/shared/manifest.json")
|
|
375
|
-
# vault.redownload_all # Download the imported resources
|
|
376
|
-
#
|
|
377
|
-
def import_manifest(import_path)
|
|
378
|
-
imported_data = JSON.parse(File.read(import_path), symbolize_names: true)
|
|
379
|
-
current_data = manifest.load
|
|
380
|
-
|
|
381
|
-
merged_resources = merge_resources(current_data[:resources] || [], imported_data[:resources] || [])
|
|
382
|
-
|
|
383
|
-
manifest.save(version: Manifest::VERSION, resources: merged_resources)
|
|
384
|
-
end
|
|
385
|
-
|
|
386
258
|
private
|
|
387
259
|
|
|
388
260
|
def default_manifest_data
|
|
389
261
|
{ version: Manifest::VERSION, resources: [] }
|
|
390
262
|
end
|
|
391
|
-
|
|
392
|
-
# Adds a repository resource, scanning for all skills.
|
|
393
|
-
#
|
|
394
|
-
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
395
|
-
# @param label [String, nil] Custom label
|
|
396
|
-
# @return [Array<Resource>] Created resources (one per skill found)
|
|
397
|
-
#
|
|
398
|
-
def add_repository_resource(parsed_url, label:)
|
|
399
|
-
target_path = File.join(storage_path, parsed_url.username, parsed_url.repo)
|
|
400
|
-
FileUtils.mkdir_p(File.dirname(target_path))
|
|
401
|
-
|
|
402
|
-
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
403
|
-
GitOperations.clone_repo(repo_url, target_path, branch: parsed_url.branch)
|
|
404
|
-
|
|
405
|
-
skills = SkillScanner.scan_directory(target_path)
|
|
406
|
-
|
|
407
|
-
if skills.empty?
|
|
408
|
-
# No skills found, create single resource for repo
|
|
409
|
-
resource = create_resource(parsed_url, label: label || parsed_url.label)
|
|
410
|
-
resource = Resource.from_h(
|
|
411
|
-
resource.to_h.merge(
|
|
412
|
-
validation_status: :not_a_skill,
|
|
413
|
-
is_skill: false
|
|
414
|
-
),
|
|
415
|
-
storage_path: storage_path
|
|
416
|
-
)
|
|
417
|
-
manifest.add_resource(resource)
|
|
418
|
-
return [resource]
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
# Create resource for each skill found
|
|
422
|
-
skills.map do |skill|
|
|
423
|
-
skill_label = label ? "#{label}-#{skill[:skill_name]}" : "#{parsed_url.username}/#{parsed_url.repo}/#{skill[:skill_name]}"
|
|
424
|
-
|
|
425
|
-
resource = Resource.new(
|
|
426
|
-
label: skill_label,
|
|
427
|
-
url: repo_url,
|
|
428
|
-
username: parsed_url.username,
|
|
429
|
-
repo: parsed_url.repo,
|
|
430
|
-
folder: skill[:folder_path],
|
|
431
|
-
type: :repo,
|
|
432
|
-
branch: parsed_url.branch,
|
|
433
|
-
relative_path: skill[:folder_path],
|
|
434
|
-
storage_path: storage_path,
|
|
435
|
-
validation_status: :unvalidated,
|
|
436
|
-
validation_errors: [],
|
|
437
|
-
skill_name: skill[:skill_name],
|
|
438
|
-
is_skill: true
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
skill_file = File.join(target_path, skill[:folder_path], "SKILL.md")
|
|
442
|
-
result = SkillValidator.validate(skill_file)
|
|
443
|
-
|
|
444
|
-
resource = Resource.from_h(
|
|
445
|
-
resource.to_h.merge(
|
|
446
|
-
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
447
|
-
validation_errors: result[:errors]
|
|
448
|
-
),
|
|
449
|
-
storage_path: storage_path
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
manifest.add_resource(resource)
|
|
453
|
-
resource
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
# Adds a folder resource.
|
|
458
|
-
#
|
|
459
|
-
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
460
|
-
# @param label [String, nil] Custom label
|
|
461
|
-
# @return [Resource] Created resource
|
|
462
|
-
#
|
|
463
|
-
def add_folder_resource(parsed_url, label:)
|
|
464
|
-
repo_path = File.join(storage_path, parsed_url.username, parsed_url.repo)
|
|
465
|
-
target_path = File.join(repo_path, parsed_url.relative_path)
|
|
466
|
-
FileUtils.mkdir_p(repo_path)
|
|
467
|
-
|
|
468
|
-
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
469
|
-
paths = [parsed_url.relative_path]
|
|
470
|
-
GitOperations.sparse_checkout(repo_url, repo_path, branch: parsed_url.branch, paths: paths)
|
|
471
|
-
|
|
472
|
-
skills = SkillScanner.scan_directory(target_path)
|
|
473
|
-
|
|
474
|
-
if skills.empty?
|
|
475
|
-
skill_name = File.basename(parsed_url.relative_path)
|
|
476
|
-
resource_label = label || "#{parsed_url.username}/#{parsed_url.repo}/#{skill_name}"
|
|
477
|
-
|
|
478
|
-
resource = Resource.new(
|
|
479
|
-
label: resource_label,
|
|
480
|
-
url: repo_url,
|
|
481
|
-
username: parsed_url.username,
|
|
482
|
-
repo: parsed_url.repo,
|
|
483
|
-
folder: parsed_url.relative_path,
|
|
484
|
-
type: :folder,
|
|
485
|
-
branch: parsed_url.branch,
|
|
486
|
-
relative_path: parsed_url.relative_path,
|
|
487
|
-
storage_path: storage_path,
|
|
488
|
-
validation_status: :not_a_skill,
|
|
489
|
-
validation_errors: [],
|
|
490
|
-
skill_name: nil,
|
|
491
|
-
is_skill: false
|
|
492
|
-
)
|
|
493
|
-
|
|
494
|
-
manifest.add_resource(resource)
|
|
495
|
-
resource
|
|
496
|
-
elsif skills.length == 1
|
|
497
|
-
skill = skills.first
|
|
498
|
-
skill_name = skill[:skill_name]
|
|
499
|
-
resource_label = label || "#{parsed_url.username}/#{parsed_url.repo}/#{skill_name}"
|
|
500
|
-
|
|
501
|
-
skill_relative_path = if skill[:relative_path] == "."
|
|
502
|
-
parsed_url.relative_path
|
|
503
|
-
else
|
|
504
|
-
File.join(
|
|
505
|
-
parsed_url.relative_path, skill[:relative_path]
|
|
506
|
-
)
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
resource = Resource.new(
|
|
510
|
-
label: resource_label,
|
|
511
|
-
url: repo_url,
|
|
512
|
-
username: parsed_url.username,
|
|
513
|
-
repo: parsed_url.repo,
|
|
514
|
-
folder: parsed_url.relative_path,
|
|
515
|
-
type: :folder,
|
|
516
|
-
branch: parsed_url.branch,
|
|
517
|
-
relative_path: skill_relative_path,
|
|
518
|
-
storage_path: storage_path,
|
|
519
|
-
skill_name: skill_name,
|
|
520
|
-
is_skill: true
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
skill_file = File.join(target_path, skill[:relative_path], "SKILL.md")
|
|
524
|
-
result = SkillValidator.validate(skill_file)
|
|
525
|
-
|
|
526
|
-
resource = Resource.from_h(
|
|
527
|
-
resource.to_h.merge(
|
|
528
|
-
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
529
|
-
validation_errors: result[:errors]
|
|
530
|
-
),
|
|
531
|
-
storage_path: storage_path
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
manifest.add_resource(resource)
|
|
535
|
-
resource
|
|
536
|
-
else
|
|
537
|
-
skills.map do |skill|
|
|
538
|
-
skill_name = skill[:skill_name]
|
|
539
|
-
skill_label = label ? "#{label}/#{skill_name}" : "#{parsed_url.username}/#{parsed_url.repo}/#{skill_name}"
|
|
540
|
-
|
|
541
|
-
skill_relative_path = if skill[:relative_path] == "."
|
|
542
|
-
parsed_url.relative_path
|
|
543
|
-
else
|
|
544
|
-
File.join(
|
|
545
|
-
parsed_url.relative_path, skill[:relative_path]
|
|
546
|
-
)
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
resource = Resource.new(
|
|
550
|
-
label: skill_label,
|
|
551
|
-
url: repo_url,
|
|
552
|
-
username: parsed_url.username,
|
|
553
|
-
repo: parsed_url.repo,
|
|
554
|
-
folder: parsed_url.relative_path,
|
|
555
|
-
type: :folder,
|
|
556
|
-
branch: parsed_url.branch,
|
|
557
|
-
relative_path: skill_relative_path,
|
|
558
|
-
storage_path: storage_path,
|
|
559
|
-
skill_name: skill_name,
|
|
560
|
-
is_skill: true
|
|
561
|
-
)
|
|
562
|
-
|
|
563
|
-
skill_file = File.join(target_path, skill[:relative_path], "SKILL.md")
|
|
564
|
-
result = SkillValidator.validate(skill_file)
|
|
565
|
-
|
|
566
|
-
resource = Resource.from_h(
|
|
567
|
-
resource.to_h.merge(
|
|
568
|
-
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
569
|
-
validation_errors: result[:errors]
|
|
570
|
-
),
|
|
571
|
-
storage_path: storage_path
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
manifest.add_resource(resource)
|
|
575
|
-
resource
|
|
576
|
-
end
|
|
577
|
-
end
|
|
578
|
-
end
|
|
579
|
-
|
|
580
|
-
# Adds a file resource.
|
|
581
|
-
#
|
|
582
|
-
# @param parsed_url [UrlParser::ParseResult] Parsed URL
|
|
583
|
-
# @param label [String, nil] Custom label
|
|
584
|
-
# @return [Resource] Created resource
|
|
585
|
-
#
|
|
586
|
-
def add_file_resource(parsed_url, label:)
|
|
587
|
-
repo_url = "https://github.com/#{parsed_url.username}/#{parsed_url.repo}"
|
|
588
|
-
|
|
589
|
-
if parsed_url.is_skill_file?
|
|
590
|
-
# This is a SKILL.md file, download the parent folder
|
|
591
|
-
repo_path = File.join(storage_path, parsed_url.username, parsed_url.repo)
|
|
592
|
-
target_path = File.join(repo_path, parsed_url.skill_folder_path)
|
|
593
|
-
FileUtils.mkdir_p(repo_path)
|
|
594
|
-
|
|
595
|
-
paths = [parsed_url.skill_folder_path]
|
|
596
|
-
GitOperations.sparse_checkout(repo_url, repo_path, branch: parsed_url.branch, paths: paths)
|
|
597
|
-
|
|
598
|
-
skill_file = File.join(target_path, "SKILL.md")
|
|
599
|
-
result = SkillValidator.validate(skill_file)
|
|
600
|
-
|
|
601
|
-
resource_label = label || "#{parsed_url.username}/#{parsed_url.repo}/#{parsed_url.skill_name}"
|
|
602
|
-
resource = Resource.new(
|
|
603
|
-
label: resource_label,
|
|
604
|
-
url: repo_url,
|
|
605
|
-
username: parsed_url.username,
|
|
606
|
-
repo: parsed_url.repo,
|
|
607
|
-
folder: parsed_url.skill_folder_path,
|
|
608
|
-
type: :file,
|
|
609
|
-
branch: parsed_url.branch,
|
|
610
|
-
relative_path: parsed_url.skill_folder_path,
|
|
611
|
-
storage_path: storage_path,
|
|
612
|
-
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
613
|
-
validation_errors: result[:errors],
|
|
614
|
-
skill_name: parsed_url.skill_name,
|
|
615
|
-
is_skill: true
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
manifest.add_resource(resource)
|
|
619
|
-
resource
|
|
620
|
-
else
|
|
621
|
-
# Non-SKILL.md file, download the parent folder
|
|
622
|
-
parent_path = File.dirname(parsed_url.relative_path)
|
|
623
|
-
repo_path = File.join(storage_path, parsed_url.username, parsed_url.repo)
|
|
624
|
-
FileUtils.mkdir_p(repo_path)
|
|
625
|
-
|
|
626
|
-
paths = [parent_path]
|
|
627
|
-
GitOperations.sparse_checkout(repo_url, repo_path, branch: parsed_url.branch, paths: paths)
|
|
628
|
-
|
|
629
|
-
resource = Resource.new(
|
|
630
|
-
label: label || parsed_url.label,
|
|
631
|
-
url: parsed_url.url,
|
|
632
|
-
username: parsed_url.username,
|
|
633
|
-
repo: parsed_url.repo,
|
|
634
|
-
folder: parent_path,
|
|
635
|
-
type: :file,
|
|
636
|
-
branch: parsed_url.branch,
|
|
637
|
-
relative_path: parsed_url.relative_path,
|
|
638
|
-
storage_path: storage_path,
|
|
639
|
-
validation_status: :not_a_skill,
|
|
640
|
-
validation_errors: [],
|
|
641
|
-
skill_name: nil,
|
|
642
|
-
is_skill: false
|
|
643
|
-
)
|
|
644
|
-
|
|
645
|
-
manifest.add_resource(resource)
|
|
646
|
-
resource
|
|
647
|
-
end
|
|
648
|
-
end
|
|
649
|
-
|
|
650
|
-
def create_resource(parsed_url, label:)
|
|
651
|
-
label ||= parsed_url.label
|
|
652
|
-
|
|
653
|
-
Resource.new(
|
|
654
|
-
label: label,
|
|
655
|
-
url: "https://github.com/#{parsed_url.username}/#{parsed_url.repo}",
|
|
656
|
-
username: parsed_url.username,
|
|
657
|
-
repo: parsed_url.repo,
|
|
658
|
-
folder: parsed_url.type == :repo ? nil : File.basename(parsed_url.relative_path || ""),
|
|
659
|
-
type: parsed_url.type,
|
|
660
|
-
branch: parsed_url.branch,
|
|
661
|
-
relative_path: parsed_url.relative_path,
|
|
662
|
-
storage_path: storage_path
|
|
663
|
-
)
|
|
664
|
-
end
|
|
665
|
-
|
|
666
|
-
def download_resource(resource)
|
|
667
|
-
target_path = resource.local_path
|
|
668
|
-
parent_dir = File.dirname(target_path)
|
|
669
|
-
FileUtils.mkdir_p(parent_dir)
|
|
670
|
-
|
|
671
|
-
if resource.type == :repo
|
|
672
|
-
GitOperations.clone_repo(resource.url, target_path, branch: resource.branch)
|
|
673
|
-
else
|
|
674
|
-
paths = [resource.relative_path]
|
|
675
|
-
GitOperations.sparse_checkout(resource.url, target_path, branch: resource.branch, paths: paths)
|
|
676
|
-
end
|
|
677
|
-
end
|
|
678
|
-
|
|
679
|
-
def sync_resource(resource)
|
|
680
|
-
return SyncResult.new(success: false, error: "Resource has no local path") unless resource.local_path
|
|
681
|
-
|
|
682
|
-
unless Dir.exist?(resource.local_path)
|
|
683
|
-
return SyncResult.new(success: false,
|
|
684
|
-
error: "Path does not exist: #{resource.local_path}")
|
|
685
|
-
end
|
|
686
|
-
|
|
687
|
-
GitOperations.pull(resource.local_path)
|
|
688
|
-
|
|
689
|
-
# For repo type resources, re-scan for skills and re-validate
|
|
690
|
-
if resource.type == :repo
|
|
691
|
-
sync_repository_resource(resource)
|
|
692
|
-
else
|
|
693
|
-
# For folder/file type resources, just re-validate
|
|
694
|
-
validate_resource(resource.label)
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
# Update synced_at for all resources from this repo
|
|
698
|
-
repo_resources = list.select { |r| r.repo == resource.repo && r.username == resource.username }
|
|
699
|
-
repo_resources.each do |r|
|
|
700
|
-
updated = Resource.from_h(r.to_h.merge(synced_at: Time.now), storage_path: storage_path)
|
|
701
|
-
manifest.update_resource(updated)
|
|
702
|
-
end
|
|
703
|
-
|
|
704
|
-
SyncResult.new(success: true, changes: true)
|
|
705
|
-
rescue Errors::Error => e
|
|
706
|
-
SyncResult.new(success: false, error: e.message)
|
|
707
|
-
end
|
|
708
|
-
|
|
709
|
-
# Syncs a repository-type resource, re-scanning for skills.
|
|
710
|
-
#
|
|
711
|
-
# @param resource [Resource] The resource to sync
|
|
712
|
-
#
|
|
713
|
-
def sync_repository_resource(resource)
|
|
714
|
-
skills = SkillScanner.scan_directory(resource.local_path)
|
|
715
|
-
|
|
716
|
-
# Get existing skills for this repo
|
|
717
|
-
existing_resources = list.select do |r|
|
|
718
|
-
r.repo == resource.repo && r.username == resource.username && r.type == :repo
|
|
719
|
-
end
|
|
720
|
-
|
|
721
|
-
# Process each found skill
|
|
722
|
-
skills.each do |skill|
|
|
723
|
-
skill_label = "#{resource.username}/#{resource.repo}/#{skill[:skill_name]}"
|
|
724
|
-
existing = existing_resources.find { |r| r.skill_name == skill[:skill_name] }
|
|
725
|
-
|
|
726
|
-
if existing
|
|
727
|
-
# Re-validate existing skill
|
|
728
|
-
skill_file = File.join(resource.local_path, skill[:folder_path], "SKILL.md")
|
|
729
|
-
result = SkillValidator.validate(skill_file)
|
|
730
|
-
|
|
731
|
-
updated = Resource.from_h(
|
|
732
|
-
existing.to_h.merge(
|
|
733
|
-
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
734
|
-
validation_errors: result[:errors]
|
|
735
|
-
),
|
|
736
|
-
storage_path: storage_path
|
|
737
|
-
)
|
|
738
|
-
manifest.update_resource(updated)
|
|
739
|
-
else
|
|
740
|
-
# Add new skill
|
|
741
|
-
new_resource = Resource.new(
|
|
742
|
-
label: skill_label,
|
|
743
|
-
url: resource.url,
|
|
744
|
-
username: resource.username,
|
|
745
|
-
repo: resource.repo,
|
|
746
|
-
folder: skill[:folder_path],
|
|
747
|
-
type: :repo,
|
|
748
|
-
branch: resource.branch,
|
|
749
|
-
relative_path: skill[:folder_path],
|
|
750
|
-
storage_path: storage_path,
|
|
751
|
-
validation_status: :unvalidated,
|
|
752
|
-
validation_errors: [],
|
|
753
|
-
skill_name: skill[:skill_name],
|
|
754
|
-
is_skill: true
|
|
755
|
-
)
|
|
756
|
-
|
|
757
|
-
skill_file = File.join(resource.local_path, skill[:folder_path], "SKILL.md")
|
|
758
|
-
result = SkillValidator.validate(skill_file)
|
|
759
|
-
|
|
760
|
-
new_resource = Resource.from_h(
|
|
761
|
-
new_resource.to_h.merge(
|
|
762
|
-
validation_status: result[:valid] ? :valid_skill : :invalid_skill,
|
|
763
|
-
validation_errors: result[:errors]
|
|
764
|
-
),
|
|
765
|
-
storage_path: storage_path
|
|
766
|
-
)
|
|
767
|
-
|
|
768
|
-
manifest.add_resource(new_resource)
|
|
769
|
-
end
|
|
770
|
-
end
|
|
771
|
-
end
|
|
772
|
-
|
|
773
|
-
def merge_resources(current_resources, imported_resources)
|
|
774
|
-
merged = current_resources.dup
|
|
775
|
-
|
|
776
|
-
imported_resources.each do |imported|
|
|
777
|
-
existing_index = merged.find_index { |r| r[:label] == imported[:label] }
|
|
778
|
-
|
|
779
|
-
if existing_index
|
|
780
|
-
merged[existing_index] = imported
|
|
781
|
-
else
|
|
782
|
-
merged << imported
|
|
783
|
-
end
|
|
784
|
-
end
|
|
785
|
-
|
|
786
|
-
merged
|
|
787
|
-
end
|
|
788
263
|
end
|
|
789
264
|
end
|
data/scripts/manual_test.rb
CHANGED
|
@@ -21,23 +21,23 @@ def section(title)
|
|
|
21
21
|
puts "=" * 70
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def print_resource(
|
|
25
|
-
status_sym =
|
|
26
|
-
status_color = case
|
|
24
|
+
def print_resource(resource)
|
|
25
|
+
status_sym = resource.validation_status.to_s.upcase
|
|
26
|
+
status_color = case resource.validation_status
|
|
27
27
|
when :valid_skill then "✓"
|
|
28
28
|
when :invalid_skill then "✗"
|
|
29
29
|
when :not_a_skill then "○"
|
|
30
30
|
else "?"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
puts " #{status_color} #{
|
|
34
|
-
puts " Type: #{
|
|
35
|
-
puts " Local: #{
|
|
36
|
-
puts " Skill: #{
|
|
33
|
+
puts " #{status_color} #{resource.label}"
|
|
34
|
+
puts " Type: #{resource.type}, URL: #{resource.url}"
|
|
35
|
+
puts " Local: #{resource.local_path}"
|
|
36
|
+
puts " Skill: #{resource.skill_name || "N/A"}"
|
|
37
37
|
puts " Status: #{status_sym}"
|
|
38
|
-
return unless
|
|
38
|
+
return unless resource.validation_errors.any?
|
|
39
39
|
|
|
40
|
-
puts " Errors: #{
|
|
40
|
+
puts " Errors: #{resource.validation_errors.empty? ? "None" : resource.validation_errors.join(", ")}"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def print_sync_result(label, result)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agents_skill_vault
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucian Ghinda
|
|
@@ -84,6 +84,10 @@ files:
|
|
|
84
84
|
- lib/agents_skill_vault/sync_result.rb
|
|
85
85
|
- lib/agents_skill_vault/url_parser.rb
|
|
86
86
|
- lib/agents_skill_vault/vault.rb
|
|
87
|
+
- lib/agents_skill_vault/vault/manifest_operations.rb
|
|
88
|
+
- lib/agents_skill_vault/vault/resource_adder.rb
|
|
89
|
+
- lib/agents_skill_vault/vault/resource_syncer.rb
|
|
90
|
+
- lib/agents_skill_vault/vault/resource_validator.rb
|
|
87
91
|
- lib/agents_skill_vault/version.rb
|
|
88
92
|
- scripts/manual_test.rb
|
|
89
93
|
- sig/agents_skill_vault.rbs
|