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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b339666f25ca1e000fff3b11d41645fa5bbab46427c90c80c7cae4b12642558e
|
|
4
|
+
data.tar.gz: 72692513935d521f5e478775eacd31a1e3cc79850f2b1381756671345ff0c06e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c4fe6bd713ef790c672b153cf57b4297dbad448d52c2534a6ed50ee685a4d568c451edd66f774b00f1af502a79417d39d6bd1f605901e42ec732630302d85d6d
|
|
7
|
+
data.tar.gz: c53947f7a8f7aaea9ca83b1b382d18dfa238149110d4c93b71aefb3599b96cbebdfd18fdc7dbe6885a508747de3c5433a2da1f035004e946b616a88b8d43f13d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-01-29
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- Renamed `UrlParser::ParseResult#is_skill_file?` to `skill_file?` (follows Ruby naming conventions)
|
|
8
|
+
|
|
9
|
+
### Refactoring
|
|
10
|
+
|
|
11
|
+
- Decomposed `Vault` class into 4 included modules:
|
|
12
|
+
- `Vault::ResourceAdder` - resource addition logic
|
|
13
|
+
- `Vault::ResourceSyncer` - resource sync logic
|
|
14
|
+
- `Vault::ResourceValidator` - resource validation logic
|
|
15
|
+
- `Vault::ManifestOperations` - manifest import/export operations
|
|
16
|
+
- Reduced `Vault` class from 450 to ~150 lines
|
|
17
|
+
- Extracted helper methods to keep all methods under 20 lines
|
|
18
|
+
- Reduced `Resource#initialize` parameter count from 15 to 11 using `**validation_attrs` splat
|
|
19
|
+
- Simplified `Resource#==` method using `equality_attributes` helper
|
|
20
|
+
- Extracted `Resource.from_h` logic into `extract_core_attrs` and `extract_validation_attrs` helpers
|
|
21
|
+
- Refactored `SkillValidator.validate` method by extracting `build_skill_data` and `validate_fields` helpers
|
|
22
|
+
- Split `UrlParser.parse_path_segments` into smaller methods: `parse_tree_segments`, `parse_blob_segments`, `default_path_data`, `parse_skill_file_segments`
|
|
23
|
+
|
|
24
|
+
### Code Quality
|
|
25
|
+
|
|
26
|
+
- All 33 RuboCop offenses resolved (0 remaining)
|
|
27
|
+
- All methods under 20 lines (excl. tests)
|
|
28
|
+
- All classes under 150 lines (excl. tests)
|
|
29
|
+
- Reduced parameter counts to meet style guidelines
|
|
30
|
+
|
|
31
|
+
## [0.1.0] - 2026-01-25
|
|
32
|
+
|
|
3
33
|
## [0.1.0] - 2026-01-25
|
|
4
34
|
|
|
5
35
|
### Features
|
|
@@ -92,7 +92,7 @@ module AgentsSkillVault
|
|
|
92
92
|
# @param resource [Resource] The resource with updated attributes
|
|
93
93
|
# @return [Boolean] true if the resource was found and updated, false otherwise
|
|
94
94
|
#
|
|
95
|
-
def update_resource(resource)
|
|
95
|
+
def update_resource(resource) # rubocop:disable Naming/PredicateMethod
|
|
96
96
|
data = load
|
|
97
97
|
data[:resources] ||= []
|
|
98
98
|
index = data[:resources].find_index { |r| r[:label] == resource.label }
|
|
@@ -78,14 +78,14 @@ module AgentsSkillVault
|
|
|
78
78
|
# @param relative_path [String, nil] Path relative to repository root
|
|
79
79
|
# @param added_at [Time, nil] When added (defaults to now)
|
|
80
80
|
# @param synced_at [Time, nil] When last synced (defaults to now)
|
|
81
|
-
# @param
|
|
82
|
-
# @
|
|
83
|
-
# @
|
|
84
|
-
# @
|
|
81
|
+
# @param validation_attrs [Hash] Validation-related attributes
|
|
82
|
+
# @option validation_attrs [Symbol] :validation_status Validation status (default: :unvalidated)
|
|
83
|
+
# @option validation_attrs [Array] :validation_errors Validation errors (default: [])
|
|
84
|
+
# @option validation_attrs [String] :skill_name The name of the skill (default: nil)
|
|
85
|
+
# @option validation_attrs [Boolean] :is_skill Whether this is a skill (default: derived from skill_name)
|
|
85
86
|
#
|
|
86
87
|
def initialize(label:, url:, username:, repo:, type:, storage_path:, folder: nil, branch: nil,
|
|
87
|
-
relative_path: nil, added_at: nil, synced_at: nil,
|
|
88
|
-
validation_errors: [], skill_name: nil, is_skill: nil)
|
|
88
|
+
relative_path: nil, added_at: nil, synced_at: nil, **validation_attrs)
|
|
89
89
|
@label = label
|
|
90
90
|
@url = url
|
|
91
91
|
@username = username
|
|
@@ -97,10 +97,10 @@ module AgentsSkillVault
|
|
|
97
97
|
@added_at = added_at || Time.now
|
|
98
98
|
@synced_at = synced_at || Time.now
|
|
99
99
|
@storage_path = storage_path
|
|
100
|
-
@validation_status = validation_status
|
|
101
|
-
@validation_errors = validation_errors
|
|
102
|
-
@skill_name = skill_name
|
|
103
|
-
@is_skill = is_skill
|
|
100
|
+
@validation_status = validation_attrs.fetch(:validation_status, :unvalidated)
|
|
101
|
+
@validation_errors = validation_attrs.fetch(:validation_errors, [])
|
|
102
|
+
@skill_name = validation_attrs[:skill_name]
|
|
103
|
+
@is_skill = validation_attrs.fetch(:is_skill, !@skill_name.nil?)
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
# Returns the local filesystem path where the resource is stored.
|
|
@@ -155,18 +155,16 @@ module AgentsSkillVault
|
|
|
155
155
|
def ==(other)
|
|
156
156
|
return false unless other.is_a?(Resource)
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
skill_name == other.skill_name &&
|
|
169
|
-
is_skill == other.is_skill
|
|
158
|
+
equality_attributes.all? { |attr| public_send(attr) == other.public_send(attr) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Returns list of attributes used for equality comparison.
|
|
162
|
+
#
|
|
163
|
+
# @return [Array<Symbol>] Attribute names to compare
|
|
164
|
+
#
|
|
165
|
+
def equality_attributes
|
|
166
|
+
%i[label url username repo folder type branch relative_path
|
|
167
|
+
validation_status validation_errors skill_name is_skill]
|
|
170
168
|
end
|
|
171
169
|
|
|
172
170
|
# Creates a Resource from a hash.
|
|
@@ -182,19 +180,16 @@ module AgentsSkillVault
|
|
|
182
180
|
# Resource.from_h({ label: "user/repo", ... }, storage_path: "/vault")
|
|
183
181
|
#
|
|
184
182
|
def self.from_h(hash, storage_path: nil)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
validation_status = validation_status.to_sym if validation_status.is_a?(String)
|
|
188
|
-
|
|
189
|
-
# Handle backward compatibility: if is_skill is missing, derive from skill_name
|
|
190
|
-
is_skill = hash[:is_skill] || hash["is_skill"]
|
|
191
|
-
skill_name = hash[:skill_name] || hash["skill_name"]
|
|
192
|
-
is_skill ||= !skill_name.nil?
|
|
193
|
-
|
|
194
|
-
# Handle backward compatibility: if validation_errors is missing, default to empty array
|
|
195
|
-
validation_errors = hash[:validation_errors] || hash["validation_errors"] || []
|
|
183
|
+
new(**extract_core_attrs(hash), **extract_validation_attrs(hash), storage_path:)
|
|
184
|
+
end
|
|
196
185
|
|
|
197
|
-
|
|
186
|
+
# Extracts core resource attributes from a hash.
|
|
187
|
+
#
|
|
188
|
+
# @param hash [Hash] Resource attributes as a hash
|
|
189
|
+
# @return [Hash] Core attributes with proper defaults
|
|
190
|
+
#
|
|
191
|
+
private_class_method def self.extract_core_attrs(hash)
|
|
192
|
+
{
|
|
198
193
|
label: hash[:label] || hash["label"],
|
|
199
194
|
url: hash[:url] || hash["url"],
|
|
200
195
|
username: hash[:username] || hash["username"],
|
|
@@ -202,15 +197,37 @@ module AgentsSkillVault
|
|
|
202
197
|
folder: hash[:folder] || hash["folder"],
|
|
203
198
|
type: (hash[:type] || hash["type"]).to_sym,
|
|
204
199
|
branch: hash[:branch] || hash["branch"],
|
|
205
|
-
relative_path: hash
|
|
200
|
+
relative_path: extract_relative_path(hash),
|
|
206
201
|
added_at: parse_time(hash[:added_at] || hash["added_at"]),
|
|
207
|
-
synced_at: parse_time(hash[:synced_at] || hash["synced_at"])
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
202
|
+
synced_at: parse_time(hash[:synced_at] || hash["synced_at"])
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Extracts relative path with backward compatibility.
|
|
207
|
+
#
|
|
208
|
+
# @param hash [Hash] Resource attributes
|
|
209
|
+
# @return [String, nil] Relative path
|
|
210
|
+
#
|
|
211
|
+
private_class_method def self.extract_relative_path(hash)
|
|
212
|
+
hash[:relative_path] || hash["relative_path"] || hash["path_in_repo"] || hash["path_in_repo"]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Extracts validation-related attributes from a hash with backward compatibility.
|
|
216
|
+
#
|
|
217
|
+
# @param hash [Hash] Resource attributes as a hash
|
|
218
|
+
# @return [Hash] Validation attributes with proper defaults
|
|
219
|
+
#
|
|
220
|
+
private_class_method def self.extract_validation_attrs(hash)
|
|
221
|
+
validation_status = hash[:validation_status] || hash["validation_status"] || :unvalidated
|
|
222
|
+
validation_status = validation_status.to_sym if validation_status.is_a?(String)
|
|
223
|
+
|
|
224
|
+
is_skill = hash[:is_skill] || hash["is_skill"]
|
|
225
|
+
skill_name = hash[:skill_name] || hash["skill_name"]
|
|
226
|
+
is_skill ||= !skill_name.nil?
|
|
227
|
+
|
|
228
|
+
validation_errors = hash[:validation_errors] || hash["validation_errors"] || []
|
|
229
|
+
|
|
230
|
+
{ validation_status:, skill_name:, is_skill:, validation_errors: }
|
|
214
231
|
end
|
|
215
232
|
|
|
216
233
|
# Parses a time value from various formats.
|
|
@@ -62,29 +62,9 @@ module AgentsSkillVault
|
|
|
62
62
|
parsed_data = parse_yaml(frontmatter, errors)
|
|
63
63
|
return { valid: false, errors:, skill_data: {} } if parsed_data.nil?
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
validate_required_fields(parsed_data, errors)
|
|
67
|
-
|
|
68
|
-
# Validate name field
|
|
69
|
-
validate_name(parsed_data, errors) if parsed_data["name"]
|
|
70
|
-
|
|
71
|
-
# Validate description field
|
|
72
|
-
validate_description(parsed_data, errors) if parsed_data["description"]
|
|
73
|
-
|
|
74
|
-
# Validate optional fields if present
|
|
75
|
-
validate_optional_fields(parsed_data, errors)
|
|
76
|
-
|
|
77
|
-
# Build skill data hash
|
|
78
|
-
skill_data = {
|
|
79
|
-
name: parsed_data["name"],
|
|
80
|
-
description: parsed_data["description"],
|
|
81
|
-
license: parsed_data["license"],
|
|
82
|
-
compatibility: parsed_data["compatibility"],
|
|
83
|
-
metadata: parsed_data["metadata"],
|
|
84
|
-
allowed_tools: parsed_data["allowed-tools"]
|
|
85
|
-
}
|
|
65
|
+
validate_fields(parsed_data, errors)
|
|
86
66
|
|
|
87
|
-
{ valid: errors.empty?, errors:, skill_data: }
|
|
67
|
+
{ valid: errors.empty?, errors:, skill_data: build_skill_data(parsed_data) }
|
|
88
68
|
end
|
|
89
69
|
|
|
90
70
|
# Extracts YAML frontmatter from content.
|
|
@@ -144,7 +124,8 @@ module AgentsSkillVault
|
|
|
144
124
|
|
|
145
125
|
return if name.match?(NAME_PATTERN)
|
|
146
126
|
|
|
147
|
-
errors << "Field 'name' must contain only lowercase letters, numbers, and hyphens
|
|
127
|
+
errors << "Field 'name' must contain only lowercase letters, numbers, and hyphens " \
|
|
128
|
+
"(no consecutive or leading/trailing hyphens)"
|
|
148
129
|
end
|
|
149
130
|
|
|
150
131
|
# Validates the description field.
|
|
@@ -197,5 +178,33 @@ module AgentsSkillVault
|
|
|
197
178
|
|
|
198
179
|
errors << "Field 'allowed-tools' must be a string"
|
|
199
180
|
end
|
|
181
|
+
|
|
182
|
+
# Validates all fields for the parsed data.
|
|
183
|
+
#
|
|
184
|
+
# @param data [Hash] Parsed YAML data
|
|
185
|
+
# @param errors [Array] Array to append errors to
|
|
186
|
+
#
|
|
187
|
+
def self.validate_fields(data, errors)
|
|
188
|
+
validate_required_fields(data, errors)
|
|
189
|
+
validate_name(data, errors) if data["name"]
|
|
190
|
+
validate_description(data, errors) if data["description"]
|
|
191
|
+
validate_optional_fields(data, errors)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Builds skill data hash from parsed YAML.
|
|
195
|
+
#
|
|
196
|
+
# @param data [Hash] Parsed YAML data
|
|
197
|
+
# @return [Hash] Skill data with symbolized keys
|
|
198
|
+
#
|
|
199
|
+
def self.build_skill_data(data)
|
|
200
|
+
{
|
|
201
|
+
name: data["name"],
|
|
202
|
+
description: data["description"],
|
|
203
|
+
license: data["license"],
|
|
204
|
+
compatibility: data["compatibility"],
|
|
205
|
+
metadata: data["metadata"],
|
|
206
|
+
allowed_tools: data["allowed-tools"]
|
|
207
|
+
}
|
|
208
|
+
end
|
|
200
209
|
end
|
|
201
210
|
end
|
|
@@ -82,7 +82,7 @@ module AgentsSkillVault
|
|
|
82
82
|
def label
|
|
83
83
|
if type == :repo
|
|
84
84
|
"#{username}/#{repo}"
|
|
85
|
-
elsif
|
|
85
|
+
elsif skill_file?
|
|
86
86
|
"#{username}/#{repo}/#{skill_name}"
|
|
87
87
|
else
|
|
88
88
|
parts = relative_path&.split("/") || []
|
|
@@ -95,7 +95,7 @@ module AgentsSkillVault
|
|
|
95
95
|
#
|
|
96
96
|
# @return [Boolean] true if this is a SKILL.md file URL
|
|
97
97
|
#
|
|
98
|
-
def
|
|
98
|
+
def skill_file?
|
|
99
99
|
type == :file && skill_name && !skill_name.empty?
|
|
100
100
|
end
|
|
101
101
|
end
|
|
@@ -156,52 +156,81 @@ module AgentsSkillVault
|
|
|
156
156
|
# @return [PathSegmentsData] Structured data with type, branch, and relative_path
|
|
157
157
|
#
|
|
158
158
|
private_class_method def self.parse_path_segments(segments)
|
|
159
|
-
if segments.length <= 2
|
|
160
|
-
return PathSegmentsData.new(type: :repo, branch: "main", relative_path: nil, skill_name: nil,
|
|
161
|
-
skill_folder_path: nil)
|
|
162
|
-
end
|
|
159
|
+
return default_path_data if segments.length <= 2
|
|
163
160
|
|
|
164
161
|
case segments[2]
|
|
165
|
-
when "tree"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
skill_folder_path = "."
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
PathSegmentsData.new(
|
|
193
|
-
type: :file,
|
|
194
|
-
branch: branch,
|
|
195
|
-
relative_path:,
|
|
196
|
-
skill_name:,
|
|
197
|
-
skill_folder_path:
|
|
198
|
-
)
|
|
199
|
-
else
|
|
200
|
-
PathSegmentsData.new(type: :file, branch: branch, relative_path:, skill_name: nil, skill_folder_path: nil)
|
|
201
|
-
end
|
|
162
|
+
when "tree" then parse_tree_segments(segments)
|
|
163
|
+
when "blob" then parse_blob_segments(segments)
|
|
164
|
+
else default_path_data
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Returns the default PathSegmentsData for a repository URL.
|
|
169
|
+
#
|
|
170
|
+
# @return [PathSegmentsData] Default path data
|
|
171
|
+
#
|
|
172
|
+
private_class_method def self.default_path_data
|
|
173
|
+
PathSegmentsData.new(type: :repo, branch: "main", relative_path: nil, skill_name: nil, skill_folder_path: nil)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Parses tree (folder) URL segments.
|
|
177
|
+
#
|
|
178
|
+
# @param segments [Array<String>] The split path segments
|
|
179
|
+
# @return [PathSegmentsData] Path data for tree URLs
|
|
180
|
+
#
|
|
181
|
+
private_class_method def self.parse_tree_segments(segments)
|
|
182
|
+
branch = segments[3] || "main"
|
|
183
|
+
path_parts = segments[4..] || []
|
|
184
|
+
if path_parts.empty?
|
|
185
|
+
PathSegmentsData.new(type: :repo, branch: branch, relative_path: nil, skill_name: nil, skill_folder_path: nil)
|
|
202
186
|
else
|
|
203
|
-
PathSegmentsData.new(type: :
|
|
187
|
+
PathSegmentsData.new(type: :folder, branch: branch, relative_path: path_parts.join("/"), skill_name: nil,
|
|
188
|
+
skill_folder_path: nil)
|
|
204
189
|
end
|
|
205
190
|
end
|
|
191
|
+
|
|
192
|
+
# Parses blob (file) URL segments.
|
|
193
|
+
#
|
|
194
|
+
# @param segments [Array<String>] The split path segments
|
|
195
|
+
# @return [PathSegmentsData] Path data for blob URLs
|
|
196
|
+
#
|
|
197
|
+
private_class_method def self.parse_blob_segments(segments)
|
|
198
|
+
branch = segments[3] || "main"
|
|
199
|
+
path_parts = segments[4..] || []
|
|
200
|
+
relative_path = path_parts.join("/")
|
|
201
|
+
filename = path_parts.last
|
|
202
|
+
|
|
203
|
+
if filename == "SKILL.md"
|
|
204
|
+
parse_skill_file_segments(segments, branch, path_parts, relative_path)
|
|
205
|
+
else
|
|
206
|
+
PathSegmentsData.new(type: :file, branch: branch, relative_path:, skill_name: nil, skill_folder_path: nil)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Parses SKILL.md file segments to extract skill name and folder path.
|
|
211
|
+
#
|
|
212
|
+
# @param segments [Array<String>] The split path segments
|
|
213
|
+
# @param branch [String] Branch name
|
|
214
|
+
# @param path_parts [Array<String>] Path parts after branch
|
|
215
|
+
# @param relative_path [String] Full relative path
|
|
216
|
+
# @return [PathSegmentsData] Path data for SKILL.md files
|
|
217
|
+
#
|
|
218
|
+
private_class_method def self.parse_skill_file_segments(segments, branch, path_parts, relative_path)
|
|
219
|
+
if path_parts.length >= 2
|
|
220
|
+
skill_name = path_parts[-2]
|
|
221
|
+
skill_folder_path = path_parts[0..-2].join("/")
|
|
222
|
+
else
|
|
223
|
+
skill_name = segments[1]
|
|
224
|
+
skill_folder_path = "."
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
PathSegmentsData.new(
|
|
228
|
+
type: :file,
|
|
229
|
+
branch: branch,
|
|
230
|
+
relative_path:,
|
|
231
|
+
skill_name:,
|
|
232
|
+
skill_folder_path:
|
|
233
|
+
)
|
|
234
|
+
end
|
|
206
235
|
end
|
|
207
236
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentsSkillVault
|
|
4
|
+
class Vault
|
|
5
|
+
# Module for manifest operations in vault.
|
|
6
|
+
#
|
|
7
|
+
module ManifestOperations
|
|
8
|
+
# Exports manifest to a file for backup or sharing.
|
|
9
|
+
#
|
|
10
|
+
# @param export_path [String] Path where manifest should be exported
|
|
11
|
+
# @return [void]
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# vault.export_manifest("/backups/manifest.json")
|
|
15
|
+
#
|
|
16
|
+
def export_manifest(export_path)
|
|
17
|
+
FileUtils.cp(@manifest_path, export_path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Imports and merges a manifest from another vault.
|
|
21
|
+
#
|
|
22
|
+
# Resources from imported manifest are merged with existing resources.
|
|
23
|
+
# If a label exists in both, imported resource overwrites existing one.
|
|
24
|
+
# Note: This only updates manifest; files are not downloaded automatically.
|
|
25
|
+
#
|
|
26
|
+
# @param import_path [String] Path to manifest file to import
|
|
27
|
+
# @return [void]
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# vault.import_manifest("/shared/manifest.json")
|
|
31
|
+
# vault.redownload_all # Download imported resources
|
|
32
|
+
#
|
|
33
|
+
def import_manifest(import_path)
|
|
34
|
+
imported_data = JSON.parse(File.read(import_path), symbolize_names: true)
|
|
35
|
+
current_data = manifest.load
|
|
36
|
+
|
|
37
|
+
merged_resources = merge_resources(current_data[:resources] || [], imported_data[:resources] || [])
|
|
38
|
+
|
|
39
|
+
manifest.save(version: Manifest::VERSION, resources: merged_resources)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Re-downloads all resources from scratch.
|
|
43
|
+
#
|
|
44
|
+
# Deletes local files and performs a fresh clone/checkout for each resource.
|
|
45
|
+
# Useful for recovering from corruption or ensuring a clean state.
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
#
|
|
49
|
+
def redownload_all
|
|
50
|
+
list.each do |resource|
|
|
51
|
+
FileUtils.rm_rf(resource.local_path) if resource.local_path
|
|
52
|
+
download_resource(resource)
|
|
53
|
+
updated_resource = Resource.from_h(resource.to_h.merge(synced_at: Time.now), storage_path: storage_path)
|
|
54
|
+
manifest.update_resource(updated_resource)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Merges current and imported resources.
|
|
61
|
+
#
|
|
62
|
+
# @param current_resources [Array<Hash>] Current resource hashes
|
|
63
|
+
# @param imported_resources [Array<Hash>] Imported resource hashes
|
|
64
|
+
# @return [Array<Hash>] Merged resources
|
|
65
|
+
#
|
|
66
|
+
def merge_resources(current_resources, imported_resources)
|
|
67
|
+
merged = current_resources.dup
|
|
68
|
+
|
|
69
|
+
imported_resources.each do |imported|
|
|
70
|
+
existing_index = merged.find_index { |r| r[:label] == imported[:label] }
|
|
71
|
+
|
|
72
|
+
if existing_index
|
|
73
|
+
merged[existing_index] = imported
|
|
74
|
+
else
|
|
75
|
+
merged << imported
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
merged
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|