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,435 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json"
5
+
6
+ module Sxn
7
+ module Commands
8
+ # Manage project setup rules
9
+ class Rules < Thor
10
+ include Thor::Actions
11
+
12
+ def initialize(args = ARGV, local_options = {}, config = {})
13
+ super
14
+ @ui = Sxn::UI::Output.new
15
+ @prompt = Sxn::UI::Prompt.new
16
+ @table = Sxn::UI::Table.new
17
+ @config_manager = Sxn::Core::ConfigManager.new
18
+ @project_manager = Sxn::Core::ProjectManager.new(@config_manager)
19
+ @rules_manager = Sxn::Core::RulesManager.new(@config_manager, @project_manager)
20
+ end
21
+
22
+ desc "add PROJECT TYPE CONFIG", "Add a setup rule for project"
23
+ option :interactive, type: :boolean, aliases: "-i", desc: "Interactive mode"
24
+
25
+ def add(project_name = nil, rule_type = nil, rule_config = nil)
26
+ ensure_initialized!
27
+
28
+ # Interactive mode
29
+ if options[:interactive] || project_name.nil?
30
+ project_name = select_project("Select project for rule:")
31
+ return if project_name.nil?
32
+ end
33
+
34
+ rule_type = @prompt.rule_type if options[:interactive] || rule_type.nil?
35
+
36
+ if options[:interactive] || rule_config.nil?
37
+ rule_config = prompt_rule_config(rule_type)
38
+ else
39
+ # Parse JSON config from command line
40
+ begin
41
+ rule_config = JSON.parse(rule_config)
42
+ rescue JSON::ParserError => e
43
+ @ui.error("Invalid JSON config: #{e.message}")
44
+ exit(1)
45
+ end
46
+ end
47
+
48
+ begin
49
+ @ui.progress_start("Adding #{rule_type} rule for #{project_name}")
50
+
51
+ rule = @rules_manager.add_rule(project_name, rule_type, rule_config)
52
+
53
+ @ui.progress_done
54
+ @ui.success("Added #{rule_type} rule for #{project_name}")
55
+
56
+ display_rule_info(rule)
57
+ rescue Sxn::Error => e
58
+ @ui.progress_failed
59
+ @ui.error(e.message)
60
+ exit(e.exit_code)
61
+ end
62
+ end
63
+
64
+ desc "remove PROJECT TYPE [INDEX]", "Remove a rule"
65
+ option :all, type: :boolean, aliases: "-a", desc: "Remove all rules of this type"
66
+
67
+ def remove(project_name = nil, rule_type = nil, rule_index = nil)
68
+ ensure_initialized!
69
+
70
+ # Interactive selection
71
+ if project_name.nil?
72
+ project_name = select_project("Select project:")
73
+ return if project_name.nil?
74
+ end
75
+
76
+ if rule_type.nil?
77
+ rules = @rules_manager.list_rules(project_name)
78
+ if rules.empty?
79
+ @ui.empty_state("No rules configured for project #{project_name}")
80
+ return
81
+ end
82
+
83
+ rule_types = rules.map { |r| r[:type] }.uniq
84
+ rule_type = @prompt.select("Select rule type to remove:", rule_types)
85
+ end
86
+
87
+ # Convert index to integer
88
+ rule_index = rule_index.to_i if rule_index && !options[:all]
89
+
90
+ unless @prompt.confirm_deletion("#{rule_type} rule(s)", "rule")
91
+ @ui.info("Cancelled")
92
+ return
93
+ end
94
+
95
+ begin
96
+ @ui.progress_start("Removing #{rule_type} rule(s)")
97
+
98
+ if options[:all]
99
+ @rules_manager.remove_rule(project_name, rule_type)
100
+ @ui.progress_done
101
+ @ui.success("Removed all #{rule_type} rules for #{project_name}")
102
+ else
103
+ @rules_manager.remove_rule(project_name, rule_type, rule_index)
104
+ @ui.progress_done
105
+ @ui.success("Removed #{rule_type} rule ##{rule_index} for #{project_name}")
106
+ end
107
+ rescue Sxn::Error => e
108
+ @ui.progress_failed
109
+ @ui.error(e.message)
110
+ exit(e.exit_code)
111
+ end
112
+ end
113
+
114
+ desc "list [PROJECT]", "List all rules or rules for specific project"
115
+ option :type, type: :string, desc: "Filter by rule type"
116
+ option :validate, type: :boolean, aliases: "-v", desc: "Validate rules"
117
+
118
+ def list(project_name = nil)
119
+ ensure_initialized!
120
+
121
+ begin
122
+ rules = @rules_manager.list_rules(project_name)
123
+
124
+ # Filter by type if specified
125
+ rules = rules.select { |r| r[:type] == options[:type] } if options[:type]
126
+
127
+ @ui.section("Project Rules")
128
+
129
+ if rules.empty?
130
+ if project_name
131
+ @ui.empty_state("No rules configured for project #{project_name}")
132
+ else
133
+ @ui.empty_state("No rules configured")
134
+ end
135
+ suggest_add_rule
136
+ elsif options[:validate]
137
+ list_with_validation(rules, project_name)
138
+ else
139
+ @table.rules(rules, project_name)
140
+ @ui.newline
141
+ @ui.info("Total: #{rules.size} rules")
142
+ end
143
+ rescue Sxn::Error => e
144
+ @ui.error(e.message)
145
+ exit(e.exit_code)
146
+ end
147
+ end
148
+
149
+ desc "apply [PROJECT]", "Apply rules to current session"
150
+ option :session, type: :string, aliases: "-s", desc: "Target session (defaults to current)"
151
+ option :dry_run, type: :boolean, aliases: "-d", desc: "Show what would be done without executing"
152
+
153
+ def apply(project_name = nil)
154
+ ensure_initialized!
155
+
156
+ session_name = options[:session] || @config_manager.current_session
157
+ unless session_name
158
+ @ui.error("No active session")
159
+ @ui.recovery_suggestion("Use 'sxn use <session>' or specify --session")
160
+ exit(1)
161
+ end
162
+
163
+ # Interactive selection if project not provided
164
+ if project_name.nil?
165
+ worktree_manager = Sxn::Core::WorktreeManager.new(@config_manager)
166
+ worktrees = worktree_manager.list_worktrees(session_name: session_name)
167
+
168
+ if worktrees.empty?
169
+ @ui.empty_state("No worktrees in current session")
170
+ @ui.recovery_suggestion("Add worktrees with 'sxn worktree add <project>'")
171
+ exit(1)
172
+ end
173
+
174
+ choices = worktrees.map do |w|
175
+ { name: "#{w[:project]} (#{w[:branch]})", value: w[:project] }
176
+ end
177
+ project_name = @prompt.select("Select project to apply rules:", choices)
178
+ end
179
+
180
+ begin
181
+ if options[:dry_run]
182
+ @ui.info("Dry run mode - showing rules that would be applied")
183
+ show_rules_preview(project_name)
184
+ else
185
+ @ui.progress_start("Applying rules for #{project_name}")
186
+
187
+ results = @rules_manager.apply_rules(project_name, session_name)
188
+
189
+ @ui.progress_done
190
+
191
+ if results[:success]
192
+ @ui.success("Applied #{results[:applied_count]} rules successfully")
193
+ else
194
+ @ui.warning("Some rules failed to apply")
195
+ results[:errors].each { |error| @ui.error(" #{error}") }
196
+ end
197
+ end
198
+ rescue Sxn::Error => e
199
+ @ui.progress_failed if options[:dry_run]
200
+ @ui.error(e.message)
201
+ exit(e.exit_code)
202
+ end
203
+ end
204
+
205
+ desc "validate PROJECT", "Validate rules for a project"
206
+ def validate(project_name = nil)
207
+ ensure_initialized!
208
+
209
+ if project_name.nil?
210
+ project_name = select_project("Select project to validate:")
211
+ return if project_name.nil?
212
+ end
213
+
214
+ begin
215
+ results = @rules_manager.validate_rules(project_name)
216
+
217
+ @ui.section("Rule Validation: #{project_name}")
218
+
219
+ if results.nil?
220
+ @ui.error("No validation results returned")
221
+ return
222
+ end
223
+
224
+ valid_count = 0
225
+ invalid_count = 0
226
+
227
+ results.each do |result|
228
+ if result[:valid]
229
+ status = "✅"
230
+ valid_count += 1
231
+ else
232
+ status = "❌"
233
+ invalid_count += 1
234
+ end
235
+ @ui.list_item("#{status} #{result[:type]} ##{result[:index]}")
236
+
237
+ # Show errors for invalid rules
238
+ result[:errors].each { |error| @ui.list_item(" #{error}") } unless result[:valid]
239
+ end
240
+
241
+ @ui.newline
242
+ @ui.info("Valid: #{valid_count}, Invalid: #{invalid_count}")
243
+
244
+ @ui.success("All rules are valid") if invalid_count.zero?
245
+ rescue Sxn::Error => e
246
+ @ui.error(e.message)
247
+ exit(e.exit_code)
248
+ rescue NoMethodError => e
249
+ # Handle case where validate_rules is not fully implemented
250
+ @ui.warning("Validation not yet fully implemented: #{e.message}")
251
+ end
252
+ end
253
+
254
+ desc "template TYPE [PROJECT_TYPE]", "Generate rule template"
255
+ def template(rule_type = nil, project_type = nil)
256
+ ensure_initialized!
257
+
258
+ if rule_type.nil?
259
+ available_types = @rules_manager.get_available_rule_types
260
+ choices = available_types.map do |type|
261
+ { name: "#{type[:name]} - #{type[:description]}", value: type[:name] }
262
+ end
263
+ rule_type = @prompt.select("Select rule type:", choices)
264
+ end
265
+
266
+ begin
267
+ template_data = @rules_manager.generate_rule_template(rule_type, project_type)
268
+
269
+ @ui.section("Rule Template: #{rule_type}")
270
+
271
+ puts JSON.pretty_generate(template_data)
272
+
273
+ @ui.newline
274
+ @ui.info("Copy this template and customize for your project")
275
+ @ui.command_example(
276
+ "sxn rules add <project> #{rule_type} '#{JSON.generate(template_data.first)}'",
277
+ "Add this rule to a project"
278
+ )
279
+ rescue Sxn::Error => e
280
+ @ui.error(e.message)
281
+ exit(e.exit_code)
282
+ end
283
+ end
284
+
285
+ desc "types", "List available rule types"
286
+ def types
287
+ available_types = @rules_manager.get_available_rule_types
288
+
289
+ @ui.section("Available Rule Types")
290
+
291
+ available_types.each do |type|
292
+ @ui.subsection(type[:name])
293
+ @ui.info(type[:description])
294
+ @ui.newline
295
+
296
+ puts "Example:"
297
+ puts JSON.pretty_generate(type[:example])
298
+ @ui.newline
299
+ end
300
+ end
301
+
302
+ private
303
+
304
+ def ensure_initialized!
305
+ return if @config_manager.initialized?
306
+
307
+ @ui.error("Project not initialized")
308
+ @ui.recovery_suggestion("Run 'sxn init' to initialize sxn in this project")
309
+ exit(1)
310
+ end
311
+
312
+ def select_project(message)
313
+ projects = @project_manager.list_projects
314
+ if projects.empty?
315
+ @ui.empty_state("No projects configured")
316
+ @ui.recovery_suggestion("Add projects with 'sxn projects add <name> <path>'")
317
+ return nil
318
+ end
319
+
320
+ choices = projects.map do |p|
321
+ { name: "#{p[:name]} (#{p[:type]})", value: p[:name] }
322
+ end
323
+ @prompt.select(message, choices)
324
+ end
325
+
326
+ def prompt_rule_config(rule_type)
327
+ case rule_type
328
+ when "copy_files"
329
+ prompt_copy_files_config
330
+ when "setup_commands"
331
+ prompt_setup_commands_config
332
+ when "template"
333
+ prompt_template_config
334
+ else
335
+ @ui.error("Unknown rule type: #{rule_type}")
336
+ exit(1)
337
+ end
338
+ end
339
+
340
+ def prompt_copy_files_config
341
+ source = @prompt.ask("Source file path:")
342
+ strategy = @prompt.select("Copy strategy:", %w[copy symlink])
343
+
344
+ config = { "source" => source, "strategy" => strategy }
345
+
346
+ if @prompt.ask_yes_no("Set custom permissions?", default: false)
347
+ permissions = @prompt.ask("Permissions (octal, e.g., 0600):")
348
+ config["permissions"] = permissions.to_i(8)
349
+ end
350
+
351
+ config
352
+ end
353
+
354
+ def prompt_setup_commands_config
355
+ command_str = @prompt.ask("Command (space-separated):")
356
+ command = command_str.split
357
+
358
+ config = { "command" => command }
359
+
360
+ if @prompt.ask_yes_no("Set environment variables?", default: false)
361
+ env = {}
362
+ loop do
363
+ key = @prompt.ask("Environment variable name (blank to finish):")
364
+ break if key.empty?
365
+
366
+ value = @prompt.ask("Value for #{key}:")
367
+ env[key] = value
368
+ end
369
+ config["environment"] = env unless env.empty?
370
+ end
371
+
372
+ config
373
+ end
374
+
375
+ def prompt_template_config
376
+ source = @prompt.ask("Template source path:")
377
+ destination = @prompt.ask("Destination path:")
378
+
379
+ {
380
+ "source" => source,
381
+ "destination" => destination,
382
+ "process" => true
383
+ }
384
+ end
385
+
386
+ def display_rule_info(rule)
387
+ return unless rule
388
+
389
+ @ui.newline
390
+ @ui.key_value("Project", rule[:project] || "Unknown")
391
+ @ui.key_value("Type", rule[:type] || "Unknown")
392
+
393
+ config = rule[:config] || {}
394
+ @ui.key_value("Config", JSON.pretty_generate(config))
395
+ @ui.newline
396
+ end
397
+
398
+ def list_with_validation(rules, project_name)
399
+ if project_name
400
+ validation_results = @rules_manager.validate_rules(project_name)
401
+
402
+ @ui.subsection("Rule Validation")
403
+ validation_results.each do |result|
404
+ status = result[:valid] ? "✅" : "❌"
405
+ @ui.list_item("#{status} #{result[:type]} ##{result[:index]}")
406
+
407
+ result[:errors].each { |error| @ui.list_item(" #{error}") } unless result[:valid]
408
+ end
409
+ @ui.newline
410
+ end
411
+
412
+ @table.rules(rules, project_name)
413
+ end
414
+
415
+ def show_rules_preview(project_name)
416
+ rules = @rules_manager.list_rules(project_name)
417
+
418
+ if rules.empty?
419
+ @ui.empty_state("No rules configured for project #{project_name}")
420
+ return
421
+ end
422
+
423
+ @ui.subsection("Rules that would be applied:")
424
+ rules.each do |rule|
425
+ @ui.list_item("#{rule[:type]}: #{rule[:config]}")
426
+ end
427
+ end
428
+
429
+ def suggest_add_rule
430
+ @ui.newline
431
+ @ui.recovery_suggestion("Add rules with 'sxn rules add <project> <type> <config>' or use 'sxn rules template <type>' for examples")
432
+ end
433
+ end
434
+ end
435
+ end