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.
Files changed (156) hide show
  1. checksums.yaml +7 -0
  2. data/.gem_rbs_collection/addressable/2.8/.rbs_meta.yaml +9 -0
  3. data/.gem_rbs_collection/addressable/2.8/addressable.rbs +62 -0
  4. data/.gem_rbs_collection/async/2.12/.rbs_meta.yaml +9 -0
  5. data/.gem_rbs_collection/async/2.12/async.rbs +119 -0
  6. data/.gem_rbs_collection/async/2.12/kernel.rbs +5 -0
  7. data/.gem_rbs_collection/async/2.12/manifest.yaml +7 -0
  8. data/.gem_rbs_collection/bcrypt/3.1/.rbs_meta.yaml +9 -0
  9. data/.gem_rbs_collection/bcrypt/3.1/bcrypt.rbs +47 -0
  10. data/.gem_rbs_collection/bcrypt/3.1/manifest.yaml +2 -0
  11. data/.gem_rbs_collection/bigdecimal/3.1/.rbs_meta.yaml +9 -0
  12. data/.gem_rbs_collection/bigdecimal/3.1/bigdecimal-math.rbs +119 -0
  13. data/.gem_rbs_collection/bigdecimal/3.1/bigdecimal.rbs +1630 -0
  14. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
  15. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
  16. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +26 -0
  17. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
  18. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +65 -0
  19. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
  20. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
  21. data/.gem_rbs_collection/diff-lcs/1.5/.rbs_meta.yaml +9 -0
  22. data/.gem_rbs_collection/diff-lcs/1.5/diff-lcs.rbs +11 -0
  23. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
  24. data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
  25. data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
  26. data/.gem_rbs_collection/mini_mime/0.1/.rbs_meta.yaml +9 -0
  27. data/.gem_rbs_collection/mini_mime/0.1/mini_mime.rbs +14 -0
  28. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
  29. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
  30. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
  31. data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
  32. data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
  33. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
  34. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +822 -0
  35. data/.gem_rbs_collection/sqlite3/2.0/.rbs_meta.yaml +9 -0
  36. data/.gem_rbs_collection/sqlite3/2.0/database.rbs +20 -0
  37. data/.gem_rbs_collection/sqlite3/2.0/pragmas.rbs +5 -0
  38. data/.rspec +4 -0
  39. data/.rubocop.yml +121 -0
  40. data/.simplecov +51 -0
  41. data/CHANGELOG.md +49 -0
  42. data/Gemfile +24 -0
  43. data/Gemfile.lock +329 -0
  44. data/LICENSE.txt +21 -0
  45. data/README.md +225 -0
  46. data/Rakefile +54 -0
  47. data/Steepfile +50 -0
  48. data/bin/sxn +6 -0
  49. data/lib/sxn/CLI.rb +275 -0
  50. data/lib/sxn/commands/init.rb +137 -0
  51. data/lib/sxn/commands/projects.rb +350 -0
  52. data/lib/sxn/commands/rules.rb +435 -0
  53. data/lib/sxn/commands/sessions.rb +300 -0
  54. data/lib/sxn/commands/worktrees.rb +416 -0
  55. data/lib/sxn/commands.rb +13 -0
  56. data/lib/sxn/config/config_cache.rb +295 -0
  57. data/lib/sxn/config/config_discovery.rb +242 -0
  58. data/lib/sxn/config/config_validator.rb +562 -0
  59. data/lib/sxn/config.rb +259 -0
  60. data/lib/sxn/core/config_manager.rb +290 -0
  61. data/lib/sxn/core/project_manager.rb +307 -0
  62. data/lib/sxn/core/rules_manager.rb +306 -0
  63. data/lib/sxn/core/session_manager.rb +336 -0
  64. data/lib/sxn/core/worktree_manager.rb +281 -0
  65. data/lib/sxn/core.rb +13 -0
  66. data/lib/sxn/database/errors.rb +29 -0
  67. data/lib/sxn/database/session_database.rb +691 -0
  68. data/lib/sxn/database.rb +24 -0
  69. data/lib/sxn/errors.rb +76 -0
  70. data/lib/sxn/rules/base_rule.rb +367 -0
  71. data/lib/sxn/rules/copy_files_rule.rb +346 -0
  72. data/lib/sxn/rules/errors.rb +28 -0
  73. data/lib/sxn/rules/project_detector.rb +871 -0
  74. data/lib/sxn/rules/rules_engine.rb +485 -0
  75. data/lib/sxn/rules/setup_commands_rule.rb +307 -0
  76. data/lib/sxn/rules/template_rule.rb +262 -0
  77. data/lib/sxn/rules.rb +148 -0
  78. data/lib/sxn/runtime_validations.rb +96 -0
  79. data/lib/sxn/security/secure_command_executor.rb +364 -0
  80. data/lib/sxn/security/secure_file_copier.rb +478 -0
  81. data/lib/sxn/security/secure_path_validator.rb +258 -0
  82. data/lib/sxn/security.rb +15 -0
  83. data/lib/sxn/templates/common/gitignore.liquid +99 -0
  84. data/lib/sxn/templates/common/session-info.md.liquid +58 -0
  85. data/lib/sxn/templates/errors.rb +36 -0
  86. data/lib/sxn/templates/javascript/README.md.liquid +59 -0
  87. data/lib/sxn/templates/javascript/session-info.md.liquid +206 -0
  88. data/lib/sxn/templates/rails/CLAUDE.md.liquid +78 -0
  89. data/lib/sxn/templates/rails/database.yml.liquid +31 -0
  90. data/lib/sxn/templates/rails/session-info.md.liquid +144 -0
  91. data/lib/sxn/templates/template_engine.rb +346 -0
  92. data/lib/sxn/templates/template_processor.rb +279 -0
  93. data/lib/sxn/templates/template_security.rb +410 -0
  94. data/lib/sxn/templates/template_variables.rb +713 -0
  95. data/lib/sxn/templates.rb +28 -0
  96. data/lib/sxn/ui/output.rb +103 -0
  97. data/lib/sxn/ui/progress_bar.rb +91 -0
  98. data/lib/sxn/ui/prompt.rb +116 -0
  99. data/lib/sxn/ui/table.rb +183 -0
  100. data/lib/sxn/ui.rb +12 -0
  101. data/lib/sxn/version.rb +5 -0
  102. data/lib/sxn.rb +63 -0
  103. data/rbs_collection.lock.yaml +180 -0
  104. data/rbs_collection.yaml +39 -0
  105. data/scripts/test.sh +31 -0
  106. data/sig/external/liquid.rbs +116 -0
  107. data/sig/external/thor.rbs +99 -0
  108. data/sig/external/tty.rbs +71 -0
  109. data/sig/sxn/cli.rbs +46 -0
  110. data/sig/sxn/commands/init.rbs +38 -0
  111. data/sig/sxn/commands/projects.rbs +72 -0
  112. data/sig/sxn/commands/rules.rbs +95 -0
  113. data/sig/sxn/commands/sessions.rbs +62 -0
  114. data/sig/sxn/commands/worktrees.rbs +82 -0
  115. data/sig/sxn/commands.rbs +6 -0
  116. data/sig/sxn/config/config_cache.rbs +67 -0
  117. data/sig/sxn/config/config_discovery.rbs +64 -0
  118. data/sig/sxn/config/config_validator.rbs +64 -0
  119. data/sig/sxn/config.rbs +74 -0
  120. data/sig/sxn/core/config_manager.rbs +67 -0
  121. data/sig/sxn/core/project_manager.rbs +52 -0
  122. data/sig/sxn/core/rules_manager.rbs +54 -0
  123. data/sig/sxn/core/session_manager.rbs +59 -0
  124. data/sig/sxn/core/worktree_manager.rbs +50 -0
  125. data/sig/sxn/core.rbs +87 -0
  126. data/sig/sxn/database/errors.rbs +37 -0
  127. data/sig/sxn/database/session_database.rbs +151 -0
  128. data/sig/sxn/database.rbs +83 -0
  129. data/sig/sxn/errors.rbs +89 -0
  130. data/sig/sxn/rules/base_rule.rbs +137 -0
  131. data/sig/sxn/rules/copy_files_rule.rbs +65 -0
  132. data/sig/sxn/rules/errors.rbs +33 -0
  133. data/sig/sxn/rules/project_detector.rbs +115 -0
  134. data/sig/sxn/rules/rules_engine.rbs +118 -0
  135. data/sig/sxn/rules/setup_commands_rule.rbs +60 -0
  136. data/sig/sxn/rules/template_rule.rbs +44 -0
  137. data/sig/sxn/rules.rbs +287 -0
  138. data/sig/sxn/runtime_validations.rbs +16 -0
  139. data/sig/sxn/security/secure_command_executor.rbs +63 -0
  140. data/sig/sxn/security/secure_file_copier.rbs +79 -0
  141. data/sig/sxn/security/secure_path_validator.rbs +30 -0
  142. data/sig/sxn/security.rbs +128 -0
  143. data/sig/sxn/templates/errors.rbs +43 -0
  144. data/sig/sxn/templates/template_engine.rbs +50 -0
  145. data/sig/sxn/templates/template_processor.rbs +44 -0
  146. data/sig/sxn/templates/template_security.rbs +62 -0
  147. data/sig/sxn/templates/template_variables.rbs +103 -0
  148. data/sig/sxn/templates.rbs +104 -0
  149. data/sig/sxn/ui/output.rbs +50 -0
  150. data/sig/sxn/ui/progress_bar.rbs +39 -0
  151. data/sig/sxn/ui/prompt.rbs +38 -0
  152. data/sig/sxn/ui/table.rbs +43 -0
  153. data/sig/sxn/ui.rbs +63 -0
  154. data/sig/sxn/version.rbs +5 -0
  155. data/sig/sxn.rbs +29 -0
  156. metadata +635 -0
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ module Sxn
5
+ module Core
6
+ # Manages project registration and configuration
7
+ class ProjectManager
8
+ def initialize(config_manager = nil)
9
+ @config_manager = config_manager || ConfigManager.new
10
+ end
11
+
12
+ def add_project(name, path, type: nil, default_branch: nil)
13
+ validate_project_name!(name)
14
+ validate_project_path!(path)
15
+
16
+ # Detect project type if not provided
17
+ type ||= Sxn::Rules::ProjectDetector.new(path).detect_project_type.to_s
18
+
19
+ # Detect default branch if not provided
20
+ default_branch ||= detect_default_branch(path)
21
+
22
+ @config_manager.add_project(name, path, type: type, default_branch: default_branch)
23
+
24
+ {
25
+ name: name,
26
+ path: File.expand_path(path),
27
+ type: type,
28
+ default_branch: default_branch
29
+ }
30
+ end
31
+
32
+ def remove_project(name)
33
+ project = @config_manager.get_project(name)
34
+ raise Sxn::ProjectNotFoundError, "Project '#{name}' not found" unless project
35
+
36
+ # Check if project is used in any active sessions
37
+ session_manager = SessionManager.new(@config_manager)
38
+ active_sessions = session_manager.list_sessions(status: "active")
39
+
40
+ sessions_using_project = active_sessions.select do |session|
41
+ session[:projects].include?(name)
42
+ end
43
+
44
+ unless sessions_using_project.empty?
45
+ session_names = sessions_using_project.map { |s| s[:name] }.join(", ")
46
+ raise Sxn::ProjectInUseError,
47
+ "Project '#{name}' is used in active sessions: #{session_names}"
48
+ end
49
+
50
+ @config_manager.remove_project(name)
51
+ true
52
+ end
53
+
54
+ def list_projects
55
+ @config_manager.list_projects
56
+ end
57
+
58
+ def get_project(name)
59
+ project = @config_manager.get_project(name)
60
+ raise Sxn::ProjectNotFoundError, "Project '#{name}' not found" unless project
61
+
62
+ project
63
+ end
64
+
65
+ def project_exists?(name)
66
+ project = @config_manager.get_project(name)
67
+ !project.nil?
68
+ end
69
+
70
+ def scan_projects(_base_path = nil)
71
+ @config_manager.detect_projects
72
+ end
73
+
74
+ def detect_projects(base_path = nil)
75
+ base_path ||= Dir.pwd
76
+ detected = []
77
+
78
+ # Scan for common project types
79
+ Dir.glob(File.join(base_path, "*")).each do |path|
80
+ next unless File.directory?(path)
81
+ next if File.basename(path).start_with?(".")
82
+
83
+ project_type = detect_project_type(path)
84
+ next if project_type == "unknown"
85
+
86
+ detected << {
87
+ name: File.basename(path),
88
+ path: path,
89
+ type: project_type
90
+ }
91
+ end
92
+
93
+ detected
94
+ end
95
+
96
+ def detect_project_type(path)
97
+ path = Pathname.new(path)
98
+
99
+ # Rails detection
100
+ return "rails" if (path / "Gemfile").exist? &&
101
+ (path / "config" / "application.rb").exist?
102
+
103
+ # Ruby gem detection
104
+ return "ruby" if (path / "Gemfile").exist? || Dir.glob((path / "*.gemspec").to_s).any?
105
+
106
+ # Node.js/JavaScript detection
107
+ if (path / "package.json").exist?
108
+ begin
109
+ package_json = JSON.parse((path / "package.json").read)
110
+ return "nextjs" if package_json.dig("dependencies", "next")
111
+ return "react" if package_json.dig("dependencies", "react")
112
+ return "typescript" if (path / "tsconfig.json").exist?
113
+
114
+ return "javascript"
115
+ rescue StandardError
116
+ return "javascript"
117
+ end
118
+ end
119
+
120
+ "unknown"
121
+ end
122
+
123
+ def update_project(name, updates = {})
124
+ project = get_project(name)
125
+ raise Sxn::ProjectNotFoundError, "Project '#{name}' not found" unless project
126
+
127
+ # Validate updates
128
+ raise Sxn::InvalidProjectPathError, "Path is not a directory" if updates[:path] && !File.directory?(updates[:path])
129
+
130
+ @config_manager.update_project(name, updates)
131
+ @config_manager.get_project(name) || raise(Sxn::ProjectNotFoundError,
132
+ "Project '#{name}' was deleted during update")
133
+ end
134
+
135
+ def validate_projects
136
+ projects = list_projects
137
+ results = []
138
+
139
+ projects.each do |project|
140
+ result = validate_project(project[:name])
141
+ results << result
142
+ end
143
+
144
+ results
145
+ end
146
+
147
+ def auto_register_projects(detected_projects)
148
+ results = []
149
+
150
+ detected_projects.each do |project|
151
+ result = add_project(
152
+ project[:name],
153
+ project[:path],
154
+ type: project[:type]
155
+ )
156
+ results << { status: :success, project: result }
157
+ rescue StandardError => e
158
+ results << {
159
+ status: :error,
160
+ project: project,
161
+ error: e.message
162
+ }
163
+ end
164
+
165
+ results
166
+ end
167
+
168
+ def validate_project(name)
169
+ project = get_project(name)
170
+ raise Sxn::ProjectNotFoundError, "Project '#{name}' not found" unless project
171
+
172
+ issues = []
173
+
174
+ # Check if path exists
175
+ issues << "Project path does not exist: #{project[:path]}" unless File.directory?(project[:path])
176
+
177
+ # Check if it's a git repository
178
+ issues << "Project path is not a git repository" unless git_repository?(project[:path])
179
+
180
+ # Check if path is readable
181
+ issues << "Project path is not readable" unless File.readable?(project[:path])
182
+
183
+ {
184
+ valid: issues.empty?,
185
+ issues: issues,
186
+ project: project
187
+ }
188
+ end
189
+
190
+ def get_project_rules(name)
191
+ project = get_project(name)
192
+ raise Sxn::ProjectNotFoundError, "Project '#{name}' not found" unless project
193
+
194
+ # Get project-specific rules from config
195
+ config = @config_manager.get_config
196
+ project_config = config.projects[name]
197
+
198
+ rules = project_config&.dig("rules") || {}
199
+
200
+ # Add default rules based on project type
201
+ default_rules = get_default_rules_for_type(project[:type])
202
+
203
+ merge_rules(default_rules, rules)
204
+ end
205
+
206
+ private
207
+
208
+ def validate_project_name!(name)
209
+ unless name.match?(/\A[a-zA-Z0-9_-]+\z/)
210
+ raise Sxn::InvalidProjectNameError,
211
+ "Project name must contain only letters, numbers, hyphens, and underscores"
212
+ end
213
+
214
+ return unless @config_manager.get_project(name)
215
+
216
+ raise Sxn::ProjectAlreadyExistsError, "Project '#{name}' already exists"
217
+ end
218
+
219
+ def validate_project_path!(path)
220
+ expanded_path = File.expand_path(path)
221
+
222
+ raise Sxn::InvalidProjectPathError, "Path is not a directory" unless File.directory?(expanded_path)
223
+
224
+ return if File.readable?(expanded_path)
225
+
226
+ raise Sxn::InvalidProjectPathError, "Path is not readable"
227
+ end
228
+
229
+ def detect_default_branch(path)
230
+ return "master" unless git_repository?(path)
231
+
232
+ begin
233
+ Dir.chdir(path) do
234
+ # Try to get the default branch from remote
235
+ result = `git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null`.strip
236
+ return result.split("/").last if $CHILD_STATUS.success? && !result.empty?
237
+
238
+ # Fall back to current branch
239
+ result = `git branch --show-current 2>/dev/null`.strip
240
+ return result unless result.empty?
241
+
242
+ # Final fallback
243
+ "master"
244
+ end
245
+ rescue StandardError
246
+ "master"
247
+ end
248
+ end
249
+
250
+ def git_repository?(path)
251
+ File.directory?(File.join(path, ".git"))
252
+ end
253
+
254
+ def get_default_rules_for_type(type)
255
+ case type
256
+ when "rails"
257
+ {
258
+ "copy_files" => [
259
+ { "source" => "config/master.key", "strategy" => "copy" },
260
+ { "source" => "config/credentials/*.key", "strategy" => "copy" },
261
+ { "source" => ".env", "strategy" => "copy" },
262
+ { "source" => ".env.development", "strategy" => "copy" },
263
+ { "source" => ".env.test", "strategy" => "copy" }
264
+ ],
265
+ "setup_commands" => [
266
+ { "command" => %w[bundle install] },
267
+ { "command" => ["bin/rails", "db:create"] },
268
+ { "command" => ["bin/rails", "db:migrate"] }
269
+ ]
270
+ }
271
+ when "javascript", "typescript", "nextjs", "react"
272
+ {
273
+ "copy_files" => [
274
+ { "source" => ".env", "strategy" => "copy" },
275
+ { "source" => ".env.local", "strategy" => "copy" },
276
+ { "source" => ".npmrc", "strategy" => "copy" }
277
+ ],
278
+ "setup_commands" => [
279
+ { "command" => %w[npm install] }
280
+ ]
281
+ }
282
+ else
283
+ {}
284
+ end
285
+ end
286
+
287
+ def merge_rules(default_rules, custom_rules)
288
+ result = default_rules.dup
289
+
290
+ custom_rules.each do |rule_type, rule_config|
291
+ result[rule_type] = if result[rule_type]
292
+ # Merge arrays for rules like copy_files and setup_commands
293
+ if result[rule_type].is_a?(Array) && rule_config.is_a?(Array)
294
+ result[rule_type] + rule_config
295
+ else
296
+ rule_config
297
+ end
298
+ else
299
+ rule_config
300
+ end
301
+ end
302
+
303
+ result
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sxn
4
+ module Core
5
+ # Manages project rules and their application
6
+ class RulesManager
7
+ def initialize(config_manager = nil, project_manager = nil)
8
+ @config_manager = config_manager || ConfigManager.new
9
+ @project_manager = project_manager || ProjectManager.new(@config_manager)
10
+ @rules_engine = Sxn::Rules::RulesEngine.new("/tmp", "/tmp")
11
+ end
12
+
13
+ def add_rule(project_name, rule_type, rule_config)
14
+ project = @project_manager.get_project(project_name)
15
+ raise Sxn::ProjectNotFoundError, "Project '#{project_name}' not found" unless project
16
+
17
+ validate_rule_type!(rule_type)
18
+ validate_rule_config!(rule_type, rule_config)
19
+
20
+ # Get current config
21
+ config = @config_manager.get_config
22
+
23
+ # Initialize project rules if not exists
24
+ config.projects[project_name] ||= {}
25
+ config.projects[project_name]["rules"] ||= {}
26
+ config.projects[project_name]["rules"][rule_type] ||= []
27
+
28
+ # Add new rule
29
+ config.projects[project_name]["rules"][rule_type] << rule_config
30
+
31
+ # Save updated config
32
+ save_project_config(project_name, config.projects[project_name])
33
+
34
+ {
35
+ project: project_name,
36
+ type: rule_type,
37
+ config: rule_config
38
+ }
39
+ end
40
+
41
+ def remove_rule(project_name, rule_type, rule_index = nil)
42
+ project = @project_manager.get_project(project_name)
43
+ raise Sxn::ProjectNotFoundError, "Project '#{project_name}' not found" unless project
44
+
45
+ config = @config_manager.get_config
46
+ project_rules = config.projects.dig(project_name, "rules", rule_type)
47
+
48
+ raise Sxn::RuleNotFoundError, "No #{rule_type} rules found for project '#{project_name}'" unless project_rules
49
+
50
+ if rule_index
51
+ raise Sxn::RuleNotFoundError, "Rule index #{rule_index} not found" if rule_index >= project_rules.size
52
+
53
+ removed_rule = project_rules.delete_at(rule_index)
54
+ else
55
+ removed_rule = project_rules.clear
56
+ end
57
+
58
+ save_project_config(project_name, config.projects[project_name])
59
+ removed_rule
60
+ end
61
+
62
+ def list_rules(project_name = nil)
63
+ if project_name
64
+ list_project_rules(project_name)
65
+ else
66
+ list_all_rules
67
+ end
68
+ end
69
+
70
+ def apply_rules(project_name, session_name = nil)
71
+ project = @project_manager.get_project(project_name)
72
+ raise Sxn::ProjectNotFoundError, "Project '#{project_name}' not found" unless project
73
+
74
+ # Get current session if not specified
75
+ session_name ||= @config_manager.current_session
76
+ raise Sxn::NoActiveSessionError, "No active session specified" unless session_name
77
+
78
+ session_manager = SessionManager.new(@config_manager)
79
+ session = session_manager.get_session(session_name)
80
+ raise Sxn::SessionNotFoundError, "Session '#{session_name}' not found" unless session
81
+
82
+ # Get worktree for this project in the session
83
+ worktree_manager = WorktreeManager.new(@config_manager, session_manager)
84
+ worktree = worktree_manager.get_worktree(project_name, session_name: session_name)
85
+ unless worktree
86
+ raise Sxn::WorktreeNotFoundError,
87
+ "No worktree found for project '#{project_name}' in session '#{session_name}'"
88
+ end
89
+
90
+ # Get project rules
91
+ rules = @project_manager.get_project_rules(project_name)
92
+
93
+ # Apply rules to worktree
94
+ @rules_engine.apply_rules(rules)
95
+ end
96
+
97
+ def validate_rules(project_name)
98
+ project = @project_manager.get_project(project_name)
99
+ raise Sxn::ProjectNotFoundError, "Project '#{project_name}' not found" unless project
100
+
101
+ rules = @project_manager.get_project_rules(project_name)
102
+ validation_results = []
103
+
104
+ rules.each do |rule_type, rule_configs|
105
+ Array(rule_configs).each_with_index do |rule_config, index|
106
+ validate_rule_config!(rule_type, rule_config)
107
+ validation_results << {
108
+ type: rule_type,
109
+ index: index,
110
+ config: rule_config,
111
+ valid: true,
112
+ errors: []
113
+ }
114
+ rescue StandardError => e
115
+ validation_results << {
116
+ type: rule_type,
117
+ index: index,
118
+ config: rule_config,
119
+ valid: false,
120
+ errors: [e.message]
121
+ }
122
+ end
123
+ end
124
+
125
+ validation_results
126
+ end
127
+
128
+ def generate_rule_template(rule_type, project_type = nil)
129
+ case rule_type
130
+ when "copy_files"
131
+ generate_copy_files_template(project_type)
132
+ when "setup_commands"
133
+ generate_setup_commands_template(project_type)
134
+ when "template"
135
+ generate_template_rule_template(project_type)
136
+ else
137
+ raise Sxn::InvalidRuleTypeError, "Unknown rule type: #{rule_type}"
138
+ end
139
+ end
140
+
141
+ def get_available_rule_types
142
+ [
143
+ {
144
+ name: "copy_files",
145
+ description: "Copy files from source project to worktree",
146
+ example: { "source" => "config/master.key", "strategy" => "copy" }
147
+ },
148
+ {
149
+ name: "setup_commands",
150
+ description: "Run setup commands in the worktree",
151
+ example: { "command" => %w[bundle install] }
152
+ },
153
+ {
154
+ name: "template",
155
+ description: "Process template files with variable substitution",
156
+ example: { "source" => ".sxn/templates/README.md", "destination" => "README.md" }
157
+ }
158
+ ]
159
+ end
160
+
161
+ private
162
+
163
+ def validate_rule_type!(rule_type)
164
+ valid_types = %w[copy_files setup_commands template]
165
+ return if valid_types.include?(rule_type)
166
+
167
+ raise Sxn::InvalidRuleTypeError, "Invalid rule type: #{rule_type}. Valid types: #{valid_types.join(", ")}"
168
+ end
169
+
170
+ def validate_rule_config!(rule_type, rule_config)
171
+ case rule_type
172
+ when "copy_files"
173
+ validate_copy_files_config!(rule_config)
174
+ when "setup_commands"
175
+ validate_setup_commands_config!(rule_config)
176
+ when "template"
177
+ validate_template_config!(rule_config)
178
+ end
179
+ end
180
+
181
+ def validate_copy_files_config!(config)
182
+ raise Sxn::InvalidRuleConfigError, "copy_files rule must have 'source' field" unless config.is_a?(Hash) && config["source"]
183
+
184
+ return unless config["strategy"] && !%w[copy symlink].include?(config["strategy"])
185
+
186
+ raise Sxn::InvalidRuleConfigError, "copy_files strategy must be 'copy' or 'symlink'"
187
+ end
188
+
189
+ def validate_setup_commands_config!(config)
190
+ raise Sxn::InvalidRuleConfigError, "setup_commands rule must have 'command' field" unless config.is_a?(Hash) && config["command"]
191
+
192
+ return if config["command"].is_a?(Array)
193
+
194
+ raise Sxn::InvalidRuleConfigError, "setup_commands command must be an array"
195
+ end
196
+
197
+ def validate_template_config!(config)
198
+ return if config.is_a?(Hash) && config["source"] && config["destination"]
199
+
200
+ raise Sxn::InvalidRuleConfigError, "template rule must have 'source' and 'destination' fields"
201
+ end
202
+
203
+ def list_project_rules(project_name)
204
+ project = @project_manager.get_project(project_name)
205
+ raise Sxn::ProjectNotFoundError, "Project '#{project_name}' not found" unless project
206
+
207
+ rules = @project_manager.get_project_rules(project_name)
208
+ format_rules_for_display(project_name, rules)
209
+ end
210
+
211
+ def list_all_rules
212
+ projects = @project_manager.list_projects
213
+ all_rules = []
214
+
215
+ projects.each do |project|
216
+ rules = @project_manager.get_project_rules(project[:name])
217
+ all_rules.concat(format_rules_for_display(project[:name], rules))
218
+ end
219
+
220
+ all_rules
221
+ end
222
+
223
+ def format_rules_for_display(project_name, rules)
224
+ formatted_rules = []
225
+
226
+ rules.each do |rule_type, rule_configs|
227
+ Array(rule_configs).each_with_index do |rule_config, index|
228
+ formatted_rules << {
229
+ project: project_name,
230
+ type: rule_type,
231
+ index: index,
232
+ config: rule_config,
233
+ enabled: true # Could be extended to support disabled rules
234
+ }
235
+ end
236
+ end
237
+
238
+ formatted_rules
239
+ end
240
+
241
+ def save_project_config(project_name, _project_config)
242
+ # This would need to be implemented to save back to the config file
243
+ # For now, we'll use the config manager's add_project method to update
244
+ project = @project_manager.get_project(project_name)
245
+ @config_manager.add_project(
246
+ project_name,
247
+ project[:path],
248
+ type: project[:type],
249
+ default_branch: project[:default_branch]
250
+ )
251
+
252
+ # TODO: Implement proper rule saving in config system
253
+ end
254
+
255
+ def generate_copy_files_template(project_type)
256
+ case project_type
257
+ when "rails"
258
+ [
259
+ { "source" => "config/master.key", "strategy" => "copy" },
260
+ { "source" => ".env", "strategy" => "copy" },
261
+ { "source" => ".env.development", "strategy" => "copy" }
262
+ ]
263
+ when "javascript", "typescript"
264
+ [
265
+ { "source" => ".env", "strategy" => "copy" },
266
+ { "source" => ".env.local", "strategy" => "copy" },
267
+ { "source" => ".npmrc", "strategy" => "copy" }
268
+ ]
269
+ else
270
+ [
271
+ { "source" => "path/to/file", "strategy" => "copy" }
272
+ ]
273
+ end
274
+ end
275
+
276
+ def generate_setup_commands_template(project_type)
277
+ case project_type
278
+ when "rails"
279
+ [
280
+ { "command" => %w[bundle install] },
281
+ { "command" => ["bin/rails", "db:create"] },
282
+ { "command" => ["bin/rails", "db:migrate"] }
283
+ ]
284
+ when "javascript", "typescript"
285
+ [
286
+ { "command" => %w[npm install] }
287
+ ]
288
+ else
289
+ [
290
+ { "command" => ["echo", "Replace with your setup command"] }
291
+ ]
292
+ end
293
+ end
294
+
295
+ def generate_template_rule_template(_project_type)
296
+ [
297
+ {
298
+ "source" => ".sxn/templates/session-info.md",
299
+ "destination" => "README.md",
300
+ "process" => true
301
+ }
302
+ ]
303
+ end
304
+ end
305
+ end
306
+ end