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.
@@ -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 the specified user
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 the skill name
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 the resource
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 the resource
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 the vault.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentsSkillVault
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -21,23 +21,23 @@ def section(title)
21
21
  puts "=" * 70
22
22
  end
23
23
 
24
- def print_resource(r)
25
- status_sym = r.validation_status.to_s.upcase
26
- status_color = case r.validation_status
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} #{r.label}"
34
- puts " Type: #{r.type}, URL: #{r.url}"
35
- puts " Local: #{r.local_path}"
36
- puts " Skill: #{r.skill_name || "N/A"}"
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 r.validation_errors.any?
38
+ return unless resource.validation_errors.any?
39
39
 
40
- puts " Errors: #{r.validation_errors.empty? ? "None" : r.validation_errors.join(", ")}"
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.1.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