sxn 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 +7 -0
- data/.gem_rbs_collection/addressable/2.8/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/addressable/2.8/addressable.rbs +62 -0
- data/.gem_rbs_collection/async/2.12/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/async/2.12/async.rbs +119 -0
- data/.gem_rbs_collection/async/2.12/kernel.rbs +5 -0
- data/.gem_rbs_collection/async/2.12/manifest.yaml +7 -0
- data/.gem_rbs_collection/bcrypt/3.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/bcrypt/3.1/bcrypt.rbs +47 -0
- data/.gem_rbs_collection/bcrypt/3.1/manifest.yaml +2 -0
- data/.gem_rbs_collection/bigdecimal/3.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/bigdecimal/3.1/bigdecimal-math.rbs +119 -0
- data/.gem_rbs_collection/bigdecimal/3.1/bigdecimal.rbs +1630 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +26 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +65 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
- data/.gem_rbs_collection/diff-lcs/1.5/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/diff-lcs/1.5/diff-lcs.rbs +11 -0
- data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
- data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
- data/.gem_rbs_collection/mini_mime/0.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/mini_mime/0.1/mini_mime.rbs +14 -0
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +822 -0
- data/.gem_rbs_collection/sqlite3/2.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/sqlite3/2.0/database.rbs +20 -0
- data/.gem_rbs_collection/sqlite3/2.0/pragmas.rbs +5 -0
- data/.rspec +4 -0
- data/.rubocop.yml +121 -0
- data/.simplecov +51 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +329 -0
- data/LICENSE.txt +21 -0
- data/README.md +225 -0
- data/Rakefile +54 -0
- data/Steepfile +50 -0
- data/bin/sxn +6 -0
- data/lib/sxn/CLI.rb +275 -0
- data/lib/sxn/commands/init.rb +137 -0
- data/lib/sxn/commands/projects.rb +350 -0
- data/lib/sxn/commands/rules.rb +435 -0
- data/lib/sxn/commands/sessions.rb +300 -0
- data/lib/sxn/commands/worktrees.rb +416 -0
- data/lib/sxn/commands.rb +13 -0
- data/lib/sxn/config/config_cache.rb +295 -0
- data/lib/sxn/config/config_discovery.rb +242 -0
- data/lib/sxn/config/config_validator.rb +562 -0
- data/lib/sxn/config.rb +259 -0
- data/lib/sxn/core/config_manager.rb +290 -0
- data/lib/sxn/core/project_manager.rb +307 -0
- data/lib/sxn/core/rules_manager.rb +306 -0
- data/lib/sxn/core/session_manager.rb +336 -0
- data/lib/sxn/core/worktree_manager.rb +281 -0
- data/lib/sxn/core.rb +13 -0
- data/lib/sxn/database/errors.rb +29 -0
- data/lib/sxn/database/session_database.rb +691 -0
- data/lib/sxn/database.rb +24 -0
- data/lib/sxn/errors.rb +76 -0
- data/lib/sxn/rules/base_rule.rb +367 -0
- data/lib/sxn/rules/copy_files_rule.rb +346 -0
- data/lib/sxn/rules/errors.rb +28 -0
- data/lib/sxn/rules/project_detector.rb +871 -0
- data/lib/sxn/rules/rules_engine.rb +485 -0
- data/lib/sxn/rules/setup_commands_rule.rb +307 -0
- data/lib/sxn/rules/template_rule.rb +262 -0
- data/lib/sxn/rules.rb +148 -0
- data/lib/sxn/runtime_validations.rb +96 -0
- data/lib/sxn/security/secure_command_executor.rb +364 -0
- data/lib/sxn/security/secure_file_copier.rb +478 -0
- data/lib/sxn/security/secure_path_validator.rb +258 -0
- data/lib/sxn/security.rb +15 -0
- data/lib/sxn/templates/common/gitignore.liquid +99 -0
- data/lib/sxn/templates/common/session-info.md.liquid +58 -0
- data/lib/sxn/templates/errors.rb +36 -0
- data/lib/sxn/templates/javascript/README.md.liquid +59 -0
- data/lib/sxn/templates/javascript/session-info.md.liquid +206 -0
- data/lib/sxn/templates/rails/CLAUDE.md.liquid +78 -0
- data/lib/sxn/templates/rails/database.yml.liquid +31 -0
- data/lib/sxn/templates/rails/session-info.md.liquid +144 -0
- data/lib/sxn/templates/template_engine.rb +346 -0
- data/lib/sxn/templates/template_processor.rb +279 -0
- data/lib/sxn/templates/template_security.rb +410 -0
- data/lib/sxn/templates/template_variables.rb +713 -0
- data/lib/sxn/templates.rb +28 -0
- data/lib/sxn/ui/output.rb +103 -0
- data/lib/sxn/ui/progress_bar.rb +91 -0
- data/lib/sxn/ui/prompt.rb +116 -0
- data/lib/sxn/ui/table.rb +183 -0
- data/lib/sxn/ui.rb +12 -0
- data/lib/sxn/version.rb +5 -0
- data/lib/sxn.rb +63 -0
- data/rbs_collection.lock.yaml +180 -0
- data/rbs_collection.yaml +39 -0
- data/scripts/test.sh +31 -0
- data/sig/external/liquid.rbs +116 -0
- data/sig/external/thor.rbs +99 -0
- data/sig/external/tty.rbs +71 -0
- data/sig/sxn/cli.rbs +46 -0
- data/sig/sxn/commands/init.rbs +38 -0
- data/sig/sxn/commands/projects.rbs +72 -0
- data/sig/sxn/commands/rules.rbs +95 -0
- data/sig/sxn/commands/sessions.rbs +62 -0
- data/sig/sxn/commands/worktrees.rbs +82 -0
- data/sig/sxn/commands.rbs +6 -0
- data/sig/sxn/config/config_cache.rbs +67 -0
- data/sig/sxn/config/config_discovery.rbs +64 -0
- data/sig/sxn/config/config_validator.rbs +64 -0
- data/sig/sxn/config.rbs +74 -0
- data/sig/sxn/core/config_manager.rbs +67 -0
- data/sig/sxn/core/project_manager.rbs +52 -0
- data/sig/sxn/core/rules_manager.rbs +54 -0
- data/sig/sxn/core/session_manager.rbs +59 -0
- data/sig/sxn/core/worktree_manager.rbs +50 -0
- data/sig/sxn/core.rbs +87 -0
- data/sig/sxn/database/errors.rbs +37 -0
- data/sig/sxn/database/session_database.rbs +151 -0
- data/sig/sxn/database.rbs +83 -0
- data/sig/sxn/errors.rbs +89 -0
- data/sig/sxn/rules/base_rule.rbs +137 -0
- data/sig/sxn/rules/copy_files_rule.rbs +65 -0
- data/sig/sxn/rules/errors.rbs +33 -0
- data/sig/sxn/rules/project_detector.rbs +115 -0
- data/sig/sxn/rules/rules_engine.rbs +118 -0
- data/sig/sxn/rules/setup_commands_rule.rbs +60 -0
- data/sig/sxn/rules/template_rule.rbs +44 -0
- data/sig/sxn/rules.rbs +287 -0
- data/sig/sxn/runtime_validations.rbs +16 -0
- data/sig/sxn/security/secure_command_executor.rbs +63 -0
- data/sig/sxn/security/secure_file_copier.rbs +79 -0
- data/sig/sxn/security/secure_path_validator.rbs +30 -0
- data/sig/sxn/security.rbs +128 -0
- data/sig/sxn/templates/errors.rbs +43 -0
- data/sig/sxn/templates/template_engine.rbs +50 -0
- data/sig/sxn/templates/template_processor.rbs +44 -0
- data/sig/sxn/templates/template_security.rbs +62 -0
- data/sig/sxn/templates/template_variables.rbs +103 -0
- data/sig/sxn/templates.rbs +104 -0
- data/sig/sxn/ui/output.rbs +50 -0
- data/sig/sxn/ui/progress_bar.rbs +39 -0
- data/sig/sxn/ui/prompt.rbs +38 -0
- data/sig/sxn/ui/table.rbs +43 -0
- data/sig/sxn/ui.rbs +63 -0
- data/sig/sxn/version.rbs +5 -0
- data/sig/sxn.rbs +29 -0
- metadata +635 -0
@@ -0,0 +1,562 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require_relative "../errors"
|
5
|
+
|
6
|
+
module Sxn
|
7
|
+
module Config
|
8
|
+
# Validates configuration structure and values
|
9
|
+
#
|
10
|
+
# Features:
|
11
|
+
# - Schema validation for configuration structure
|
12
|
+
# - Value validation with helpful error messages
|
13
|
+
# - Support for configuration migrations from older versions
|
14
|
+
# - Type checking and constraint validation
|
15
|
+
class ConfigValidator
|
16
|
+
# Current schema version
|
17
|
+
CURRENT_VERSION = 1
|
18
|
+
|
19
|
+
# Configuration schema definition
|
20
|
+
SCHEMA = {
|
21
|
+
"version" => {
|
22
|
+
type: :integer,
|
23
|
+
required: true,
|
24
|
+
min: 1,
|
25
|
+
max: CURRENT_VERSION
|
26
|
+
},
|
27
|
+
"sessions_folder" => {
|
28
|
+
type: :string,
|
29
|
+
required: true,
|
30
|
+
min_length: 1
|
31
|
+
},
|
32
|
+
"current_session" => {
|
33
|
+
type: :string,
|
34
|
+
required: false
|
35
|
+
},
|
36
|
+
"projects" => {
|
37
|
+
type: :hash,
|
38
|
+
required: true,
|
39
|
+
default: {},
|
40
|
+
value_schema: {
|
41
|
+
"path" => {
|
42
|
+
type: :string,
|
43
|
+
required: true,
|
44
|
+
min_length: 1
|
45
|
+
},
|
46
|
+
"type" => {
|
47
|
+
type: :string,
|
48
|
+
required: false,
|
49
|
+
allowed_values: %w[rails ruby javascript typescript react nextjs vue angular unknown]
|
50
|
+
},
|
51
|
+
"default_branch" => {
|
52
|
+
type: :string,
|
53
|
+
required: false,
|
54
|
+
default: "main"
|
55
|
+
},
|
56
|
+
"package_manager" => {
|
57
|
+
type: :string,
|
58
|
+
required: false,
|
59
|
+
allowed_values: %w[npm yarn pnpm]
|
60
|
+
},
|
61
|
+
"rules" => {
|
62
|
+
type: :hash,
|
63
|
+
required: false,
|
64
|
+
default: {},
|
65
|
+
value_schema: {
|
66
|
+
"copy_files" => {
|
67
|
+
type: :array,
|
68
|
+
required: false,
|
69
|
+
item_schema: {
|
70
|
+
"source" => {
|
71
|
+
type: :string,
|
72
|
+
required: true,
|
73
|
+
min_length: 1
|
74
|
+
},
|
75
|
+
"strategy" => {
|
76
|
+
type: :string,
|
77
|
+
required: false,
|
78
|
+
default: "copy",
|
79
|
+
allowed_values: %w[copy symlink]
|
80
|
+
},
|
81
|
+
"permissions" => {
|
82
|
+
type: :integer,
|
83
|
+
required: false,
|
84
|
+
min: 0,
|
85
|
+
max: 0o777
|
86
|
+
},
|
87
|
+
"encrypt" => {
|
88
|
+
type: :boolean,
|
89
|
+
required: false,
|
90
|
+
default: false
|
91
|
+
}
|
92
|
+
}
|
93
|
+
},
|
94
|
+
"setup_commands" => {
|
95
|
+
type: :array,
|
96
|
+
required: false,
|
97
|
+
item_schema: {
|
98
|
+
"command" => {
|
99
|
+
type: :array,
|
100
|
+
required: true,
|
101
|
+
min_length: 1,
|
102
|
+
item_type: :string
|
103
|
+
},
|
104
|
+
"environment" => {
|
105
|
+
type: :hash,
|
106
|
+
required: false,
|
107
|
+
value_type: :string
|
108
|
+
},
|
109
|
+
"condition" => {
|
110
|
+
type: :string,
|
111
|
+
required: false,
|
112
|
+
allowed_values: %w[always db_not_exists file_not_exists]
|
113
|
+
}
|
114
|
+
}
|
115
|
+
},
|
116
|
+
"templates" => {
|
117
|
+
type: :array,
|
118
|
+
required: false,
|
119
|
+
item_schema: {
|
120
|
+
"source" => {
|
121
|
+
type: :string,
|
122
|
+
required: true,
|
123
|
+
min_length: 1
|
124
|
+
},
|
125
|
+
"destination" => {
|
126
|
+
type: :string,
|
127
|
+
required: true,
|
128
|
+
min_length: 1
|
129
|
+
},
|
130
|
+
"process" => {
|
131
|
+
type: :boolean,
|
132
|
+
required: false,
|
133
|
+
default: true
|
134
|
+
},
|
135
|
+
"engine" => {
|
136
|
+
type: :string,
|
137
|
+
required: false,
|
138
|
+
default: "liquid",
|
139
|
+
allowed_values: %w[liquid erb mustache]
|
140
|
+
}
|
141
|
+
}
|
142
|
+
}
|
143
|
+
}
|
144
|
+
}
|
145
|
+
}
|
146
|
+
},
|
147
|
+
"settings" => {
|
148
|
+
type: :hash,
|
149
|
+
required: false,
|
150
|
+
default: {},
|
151
|
+
value_schema: {
|
152
|
+
"auto_cleanup" => {
|
153
|
+
type: :boolean,
|
154
|
+
required: false,
|
155
|
+
default: true
|
156
|
+
},
|
157
|
+
"max_sessions" => {
|
158
|
+
type: :integer,
|
159
|
+
required: false,
|
160
|
+
default: 10,
|
161
|
+
min: 1,
|
162
|
+
max: 100
|
163
|
+
},
|
164
|
+
"worktree_cleanup_days" => {
|
165
|
+
type: :integer,
|
166
|
+
required: false,
|
167
|
+
default: 30,
|
168
|
+
min: 1,
|
169
|
+
max: 365
|
170
|
+
},
|
171
|
+
"default_rules" => {
|
172
|
+
type: :hash,
|
173
|
+
required: false,
|
174
|
+
default: {},
|
175
|
+
value_schema: {
|
176
|
+
"templates" => {
|
177
|
+
type: :array,
|
178
|
+
required: false,
|
179
|
+
item_schema: {
|
180
|
+
"source" => {
|
181
|
+
type: :string,
|
182
|
+
required: true,
|
183
|
+
min_length: 1
|
184
|
+
},
|
185
|
+
"destination" => {
|
186
|
+
type: :string,
|
187
|
+
required: true,
|
188
|
+
min_length: 1
|
189
|
+
}
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
193
|
+
}
|
194
|
+
}
|
195
|
+
}
|
196
|
+
}.freeze
|
197
|
+
|
198
|
+
attr_reader :errors
|
199
|
+
|
200
|
+
def initialize
|
201
|
+
@errors = []
|
202
|
+
end
|
203
|
+
|
204
|
+
# Validate configuration against schema
|
205
|
+
# @param config [Hash] Configuration to validate
|
206
|
+
# @return [Boolean] True if valid
|
207
|
+
def valid?(config)
|
208
|
+
@errors = []
|
209
|
+
|
210
|
+
unless config.is_a?(Hash)
|
211
|
+
@errors << "Configuration must be a hash, got #{config.class}"
|
212
|
+
return false
|
213
|
+
end
|
214
|
+
|
215
|
+
validate_against_schema(config, SCHEMA, "")
|
216
|
+
|
217
|
+
@errors.empty?
|
218
|
+
end
|
219
|
+
|
220
|
+
# Validate and migrate configuration if needed
|
221
|
+
# @param config [Hash] Configuration to validate and migrate
|
222
|
+
# @return [Hash] Validated and migrated configuration
|
223
|
+
# @raise [ConfigurationError] If validation fails
|
224
|
+
def validate_and_migrate(config)
|
225
|
+
# First, migrate if needed
|
226
|
+
migrated_config = migrate_config(config)
|
227
|
+
|
228
|
+
# Debug output
|
229
|
+
# puts "DEBUG: Original config: #{config.inspect}"
|
230
|
+
# puts "DEBUG: Migrated config: #{migrated_config.inspect}"
|
231
|
+
|
232
|
+
# Then validate the migrated config
|
233
|
+
unless valid?(migrated_config)
|
234
|
+
error_message = format_errors
|
235
|
+
raise ConfigurationError, "Configuration validation failed:\n#{error_message}"
|
236
|
+
end
|
237
|
+
|
238
|
+
# Apply defaults for missing values
|
239
|
+
apply_defaults(migrated_config)
|
240
|
+
end
|
241
|
+
|
242
|
+
# Get formatted error messages
|
243
|
+
# @return [String] Formatted error messages
|
244
|
+
def format_errors
|
245
|
+
return "No errors" if @errors.empty?
|
246
|
+
|
247
|
+
@errors.map.with_index(1) do |error, index|
|
248
|
+
" #{index}. #{error}"
|
249
|
+
end.join("\n")
|
250
|
+
end
|
251
|
+
|
252
|
+
# Migrate configuration from older versions
|
253
|
+
# @param config [Hash] Configuration to migrate
|
254
|
+
# @return [Hash] Migrated configuration
|
255
|
+
def migrate_config(config)
|
256
|
+
return config unless config.is_a?(Hash)
|
257
|
+
|
258
|
+
version = config["version"] || 0
|
259
|
+
migrated_config = config.dup
|
260
|
+
|
261
|
+
# Check if this is a v0 config that got merged with system defaults (version = 1)
|
262
|
+
# but still has v0 structure (projects without paths)
|
263
|
+
# Handle invalid version types safely
|
264
|
+
needs_v0_migration = (version.is_a?(Integer) && version.zero?) || needs_v0_to_v1_migration?(config)
|
265
|
+
|
266
|
+
case version
|
267
|
+
when 0
|
268
|
+
# Migrate from unversioned to version 1
|
269
|
+
migrated_config = migrate_v0_to_v1(migrated_config)
|
270
|
+
when 1
|
271
|
+
if needs_v0_migration
|
272
|
+
# This is a v0 config that was merged with v1 defaults, migrate it
|
273
|
+
migrated_config = migrate_v0_to_v1(migrated_config)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
migrated_config
|
278
|
+
end
|
279
|
+
|
280
|
+
private
|
281
|
+
|
282
|
+
# Check if config needs v0 to v1 migration based on structure
|
283
|
+
# @param config [Hash] Configuration to check
|
284
|
+
# @return [Boolean] True if needs migration
|
285
|
+
def needs_v0_to_v1_migration?(config)
|
286
|
+
return false unless config.is_a?(Hash)
|
287
|
+
return false unless config["projects"].is_a?(Hash)
|
288
|
+
|
289
|
+
# Check if any project is missing a path field (indicating v0 structure)
|
290
|
+
config["projects"].any? do |_project_name, project_config|
|
291
|
+
project_config.is_a?(Hash) &&
|
292
|
+
(project_config["path"].nil? || project_config["path"].empty?)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# Validate configuration against schema recursively
|
297
|
+
# @param config [Hash] Configuration to validate
|
298
|
+
# @param schema [Hash] Schema to validate against
|
299
|
+
# @param path [String] Current path for error reporting
|
300
|
+
def validate_against_schema(config, schema, path)
|
301
|
+
schema.each do |key, field_schema|
|
302
|
+
field_path = path.empty? ? key : "#{path}.#{key}"
|
303
|
+
value = config[key]
|
304
|
+
|
305
|
+
# Check required fields
|
306
|
+
if field_schema[:required] && (value.nil? || (value.is_a?(String) && value.empty?))
|
307
|
+
@errors << "Required field '#{field_path}' is missing or empty"
|
308
|
+
next
|
309
|
+
end
|
310
|
+
|
311
|
+
# Skip validation if field is not present and not required
|
312
|
+
next if value.nil? && !field_schema[:required]
|
313
|
+
|
314
|
+
validate_field_type(value, field_schema, field_path)
|
315
|
+
|
316
|
+
# Only validate constraints and nested schemas if type is correct
|
317
|
+
if value_has_correct_type?(value, field_schema)
|
318
|
+
validate_field_constraints(value, field_schema, field_path)
|
319
|
+
validate_nested_schema(value, field_schema, field_path)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Check if value has the correct type according to schema
|
325
|
+
# @param value [Object] Value to check
|
326
|
+
# @param schema [Hash] Field schema
|
327
|
+
# @return [Boolean] True if value has correct type
|
328
|
+
def value_has_correct_type?(value, schema)
|
329
|
+
expected_type = schema[:type]
|
330
|
+
return true unless expected_type
|
331
|
+
|
332
|
+
case expected_type
|
333
|
+
when :string
|
334
|
+
value.is_a?(String)
|
335
|
+
when :integer
|
336
|
+
value.is_a?(Integer)
|
337
|
+
when :boolean
|
338
|
+
[true, false].include?(value)
|
339
|
+
when :array
|
340
|
+
value.is_a?(Array)
|
341
|
+
when :hash
|
342
|
+
value.is_a?(Hash)
|
343
|
+
else
|
344
|
+
false
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Validate field type
|
349
|
+
# @param value [Object] Value to validate
|
350
|
+
# @param schema [Hash] Field schema
|
351
|
+
# @param path [String] Field path for error reporting
|
352
|
+
def validate_field_type(value, schema, path)
|
353
|
+
expected_type = schema[:type]
|
354
|
+
return unless expected_type
|
355
|
+
|
356
|
+
return if value_has_correct_type?(value, schema)
|
357
|
+
|
358
|
+
@errors << "Field '#{path}' must be of type #{expected_type}, got #{value.class.name.downcase}"
|
359
|
+
end
|
360
|
+
|
361
|
+
# Validate field constraints
|
362
|
+
# @param value [Object] Value to validate
|
363
|
+
# @param schema [Hash] Field schema
|
364
|
+
# @param path [String] Field path for error reporting
|
365
|
+
def validate_field_constraints(value, schema, path)
|
366
|
+
# String constraints
|
367
|
+
if value.is_a?(String)
|
368
|
+
if schema[:min_length] && value.length < schema[:min_length]
|
369
|
+
@errors << "Field '#{path}' must be at least #{schema[:min_length]} characters long"
|
370
|
+
end
|
371
|
+
|
372
|
+
if schema[:max_length] && value.length > schema[:max_length]
|
373
|
+
@errors << "Field '#{path}' must be at most #{schema[:max_length]} characters long"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
# Integer constraints
|
378
|
+
if value.is_a?(Integer)
|
379
|
+
@errors << "Field '#{path}' must be at least #{schema[:min]}" if schema[:min] && value < schema[:min]
|
380
|
+
|
381
|
+
@errors << "Field '#{path}' must be at most #{schema[:max]}" if schema[:max] && value > schema[:max]
|
382
|
+
end
|
383
|
+
|
384
|
+
# Array constraints
|
385
|
+
if value.is_a?(Array)
|
386
|
+
@errors << "Field '#{path}' must have at least #{schema[:min_length]} items" if schema[:min_length] && value.length < schema[:min_length]
|
387
|
+
|
388
|
+
@errors << "Field '#{path}' must have at most #{schema[:max_length]} items" if schema[:max_length] && value.length > schema[:max_length]
|
389
|
+
end
|
390
|
+
|
391
|
+
# Allowed values constraint
|
392
|
+
return unless schema[:allowed_values] && !schema[:allowed_values].include?(value)
|
393
|
+
|
394
|
+
@errors << "Field '#{path}' must be one of: #{schema[:allowed_values].join(", ")}"
|
395
|
+
end
|
396
|
+
|
397
|
+
# Validate nested schemas
|
398
|
+
# @param value [Object] Value to validate
|
399
|
+
# @param schema [Hash] Field schema
|
400
|
+
# @param path [String] Field path for error reporting
|
401
|
+
def validate_nested_schema(value, schema, path)
|
402
|
+
return unless value
|
403
|
+
|
404
|
+
# Validate array items
|
405
|
+
if schema[:item_schema] && value.is_a?(Array)
|
406
|
+
value.each_with_index do |item, index|
|
407
|
+
item_path = "#{path}[#{index}]"
|
408
|
+
validate_against_schema(item, schema[:item_schema], item_path) if item.is_a?(Hash)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
# Validate array item types
|
413
|
+
if schema[:item_type] && value.is_a?(Array)
|
414
|
+
value.each_with_index do |item, index|
|
415
|
+
item_path = "#{path}[#{index}]"
|
416
|
+
validate_field_type(item, { type: schema[:item_type] }, item_path)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# Validate hash values
|
421
|
+
if schema[:value_schema] && value.is_a?(Hash)
|
422
|
+
# Check if this is a structured hash (like settings/rules) or dynamic keys hash (like projects)
|
423
|
+
# Structured hashes have their keys defined in value_schema
|
424
|
+
# Dynamic hashes apply value_schema to each value
|
425
|
+
is_structured = schema[:value_schema].is_a?(Hash) &&
|
426
|
+
schema[:value_schema].keys.any? { |k| k.is_a?(String) }
|
427
|
+
|
428
|
+
if is_structured && (path.include?("settings") || path.include?("rules") || path.include?("default_rules"))
|
429
|
+
# For structured hashes like settings/rules, validate the hash itself against value_schema
|
430
|
+
validate_against_schema(value, schema[:value_schema], path)
|
431
|
+
else
|
432
|
+
# For dynamic key hashes like projects, validate each value
|
433
|
+
value.each do |key, nested_value|
|
434
|
+
nested_path = "#{path}.#{key}"
|
435
|
+
validate_against_schema(nested_value, schema[:value_schema], nested_path) if nested_value.is_a?(Hash)
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
# Validate hash value types
|
441
|
+
return unless schema[:value_type] && value.is_a?(Hash)
|
442
|
+
|
443
|
+
value.each do |key, nested_value|
|
444
|
+
nested_path = "#{path}.#{key}"
|
445
|
+
validate_field_type(nested_value, { type: schema[:value_type] }, nested_path)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Apply default values to configuration
|
450
|
+
# @param config [Hash] Configuration to apply defaults to
|
451
|
+
# @return [Hash] Configuration with defaults applied
|
452
|
+
def apply_defaults(config)
|
453
|
+
apply_defaults_recursive(config, SCHEMA, config)
|
454
|
+
end
|
455
|
+
|
456
|
+
# Apply defaults recursively
|
457
|
+
# @param config [Hash] Configuration to modify
|
458
|
+
# @param schema [Hash] Schema with defaults
|
459
|
+
# @param root_config [Hash] Root configuration for reference
|
460
|
+
# @return [Hash] Configuration with defaults applied
|
461
|
+
def apply_defaults_recursive(config, schema, root_config)
|
462
|
+
schema.each do |key, field_schema|
|
463
|
+
# Apply default value if field is missing
|
464
|
+
if config[key].nil? && field_schema.key?(:default)
|
465
|
+
config[key] = begin
|
466
|
+
field_schema[:default].dup
|
467
|
+
rescue StandardError
|
468
|
+
field_schema[:default]
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
# For hash fields with specific structure schema
|
473
|
+
if config[key].is_a?(Hash) && field_schema[:value_schema]
|
474
|
+
# For direct structured hash (like settings)
|
475
|
+
if field_schema[:type] == :hash
|
476
|
+
apply_defaults_recursive(config[key], field_schema[:value_schema], root_config)
|
477
|
+
else
|
478
|
+
# For hash with dynamic keys (like projects)
|
479
|
+
config[key].each_value do |nested_value|
|
480
|
+
apply_defaults_recursive(nested_value, field_schema[:value_schema], root_config) if nested_value.is_a?(Hash)
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
# Apply defaults to array items
|
486
|
+
next unless config[key].is_a?(Array) && field_schema[:item_schema]
|
487
|
+
|
488
|
+
config[key].each do |item|
|
489
|
+
apply_defaults_recursive(item, field_schema[:item_schema], root_config) if item.is_a?(Hash)
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
config
|
494
|
+
end
|
495
|
+
|
496
|
+
# Migrate from version 0 (unversioned) to version 1
|
497
|
+
# @param config [Hash] Configuration to migrate
|
498
|
+
# @return [Hash] Migrated configuration
|
499
|
+
def migrate_v0_to_v1(config)
|
500
|
+
migrated = config.dup
|
501
|
+
|
502
|
+
# Add version field
|
503
|
+
migrated["version"] = 1
|
504
|
+
|
505
|
+
# Ensure required fields have defaults
|
506
|
+
migrated["sessions_folder"] ||= ".sessions"
|
507
|
+
migrated["projects"] ||= {}
|
508
|
+
migrated["settings"] ||= {}
|
509
|
+
|
510
|
+
# Migrate old setting names if they exist
|
511
|
+
migrated["settings"]["auto_cleanup"] = migrated.delete("auto_cleanup") if migrated.key?("auto_cleanup")
|
512
|
+
|
513
|
+
migrated["settings"]["max_sessions"] = migrated.delete("max_sessions") if migrated.key?("max_sessions")
|
514
|
+
|
515
|
+
migrated["settings"]["worktree_cleanup_days"] = migrated.delete("worktree_cleanup_days") if migrated.key?("worktree_cleanup_days")
|
516
|
+
|
517
|
+
# Ensure projects have required fields
|
518
|
+
if migrated["projects"].is_a?(Hash)
|
519
|
+
migrated["projects"].each do |project_name, project_config|
|
520
|
+
next unless project_config.is_a?(Hash)
|
521
|
+
|
522
|
+
# Ensure path is present
|
523
|
+
migrated["projects"][project_name]["path"] = "./#{project_name}" if project_config["path"].nil? || project_config["path"].empty?
|
524
|
+
|
525
|
+
# Convert old rule formats if needed
|
526
|
+
migrate_rules_v0_to_v1(project_config["rules"]) if project_config["rules"]
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
migrated
|
531
|
+
end
|
532
|
+
|
533
|
+
# Migrate rules from version 0 to version 1
|
534
|
+
# @param rules [Hash] Rules to migrate
|
535
|
+
def migrate_rules_v0_to_v1(rules)
|
536
|
+
return unless rules.is_a?(Hash)
|
537
|
+
|
538
|
+
# Convert old copy_files format
|
539
|
+
if rules["copy_files"].is_a?(Array)
|
540
|
+
rules["copy_files"].map! do |rule|
|
541
|
+
if rule.is_a?(String)
|
542
|
+
{ "source" => rule, "strategy" => "copy" }
|
543
|
+
else
|
544
|
+
rule
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
# Convert old setup_commands format
|
550
|
+
return unless rules["setup_commands"].is_a?(Array)
|
551
|
+
|
552
|
+
rules["setup_commands"].map! do |command|
|
553
|
+
if command.is_a?(String)
|
554
|
+
{ "command" => command.split }
|
555
|
+
else
|
556
|
+
command
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|