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,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Sxn
6
+ module Commands
7
+ # Initialize sxn in a project folder
8
+ class Init < Thor
9
+ include Thor::Actions
10
+
11
+ desc "init [FOLDER]", "Initialize sxn in a project folder"
12
+ option :force, type: :boolean, desc: "Force initialization even if already initialized"
13
+ option :auto_detect, type: :boolean, default: true, desc: "Automatically detect and register projects"
14
+ option :quiet, type: :boolean, aliases: "-q", desc: "Suppress interactive prompts"
15
+
16
+ def initialize(args = ARGV, local_options = {}, config = {})
17
+ super
18
+ @ui = Sxn::UI::Output.new
19
+ @prompt = Sxn::UI::Prompt.new
20
+ @config_manager = Sxn::Core::ConfigManager.new
21
+ end
22
+
23
+ def init(folder = nil)
24
+ @ui.section("Sxn Initialization")
25
+
26
+ # Check if already initialized
27
+ if @config_manager.initialized? && !options[:force]
28
+ @ui.warning("Project already initialized")
29
+ @ui.info("Use --force to reinitialize")
30
+ return
31
+ end
32
+
33
+ # Get sessions folder
34
+ sessions_folder = determine_sessions_folder(folder)
35
+
36
+ begin
37
+ # Initialize configuration
38
+ @ui.progress_start("Creating configuration")
39
+ result_folder = @config_manager.initialize_project(sessions_folder, force: options[:force])
40
+ @ui.progress_done
41
+
42
+ @ui.success("Initialized sxn in #{result_folder}")
43
+
44
+ # Auto-detect projects if enabled
45
+ auto_detect_projects if options[:auto_detect] && !options[:quiet]
46
+
47
+ display_next_steps
48
+ rescue Sxn::Error => e
49
+ @ui.error("Initialization failed: #{e.message}")
50
+ exit(e.exit_code)
51
+ rescue StandardError => e
52
+ @ui.error("Unexpected error: #{e.message}")
53
+ @ui.debug(e.backtrace.join("\n")) if ENV["SXN_DEBUG"]
54
+ exit(1)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def determine_sessions_folder(folder)
61
+ return folder if folder && !options[:quiet]
62
+
63
+ if options[:quiet]
64
+ # Use default folder in quiet mode
65
+ return folder || "#{File.basename(Dir.pwd)}-sessions"
66
+ end
67
+
68
+ # Interactive mode
69
+ @prompt.sessions_folder_setup
70
+ end
71
+
72
+ def auto_detect_projects
73
+ @ui.subsection("Project Detection")
74
+
75
+ detected = @config_manager.detect_projects
76
+
77
+ if detected.empty?
78
+ @ui.empty_state("No projects detected in current directory")
79
+ return
80
+ end
81
+
82
+ if @prompt.project_detection_confirm(detected)
83
+ register_detected_projects(detected)
84
+ else
85
+ @ui.info("Skipped project registration")
86
+ @ui.info("You can register projects later with: sxn projects add <name> <path>")
87
+ end
88
+ end
89
+
90
+ def register_detected_projects(projects)
91
+ project_manager = Sxn::Core::ProjectManager.new(@config_manager)
92
+
93
+ Sxn::UI::ProgressBar.with_progress("Registering projects", projects) do |project, progress|
94
+ result = project_manager.add_project(
95
+ project[:name],
96
+ project[:path],
97
+ type: project[:type]
98
+ )
99
+
100
+ progress.log("✅ #{project[:name]} (#{project[:type]})")
101
+ result
102
+ rescue StandardError => e
103
+ progress.log("❌ #{project[:name]}: #{e.message}")
104
+ nil
105
+ end
106
+
107
+ @ui.success("Project registration completed")
108
+ end
109
+
110
+ def display_next_steps
111
+ @ui.newline
112
+ @ui.subsection("Next Steps")
113
+
114
+ @ui.command_example(
115
+ "sxn projects list",
116
+ "View registered projects"
117
+ )
118
+
119
+ @ui.command_example(
120
+ "sxn add my-session",
121
+ "Create your first session"
122
+ )
123
+
124
+ @ui.command_example(
125
+ "sxn worktree add <project> [branch]",
126
+ "Add a worktree to your session"
127
+ )
128
+
129
+ if @config_manager.detect_projects.any?
130
+ @ui.info("💡 Detected projects are ready to use!")
131
+ else
132
+ @ui.recovery_suggestion("Register your projects with 'sxn projects add <name> <path>'")
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,350 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Sxn
6
+ module Commands
7
+ # Manage project configurations
8
+ class Projects < Thor
9
+ include Thor::Actions
10
+
11
+ def initialize(args = ARGV, local_options = {}, config = {})
12
+ super
13
+ @ui = Sxn::UI::Output.new
14
+ @prompt = Sxn::UI::Prompt.new
15
+ @table = Sxn::UI::Table.new
16
+ @config_manager = Sxn::Core::ConfigManager.new
17
+ @project_manager = Sxn::Core::ProjectManager.new(@config_manager)
18
+ end
19
+
20
+ desc "add NAME PATH", "Add a project"
21
+ option :type, type: :string, desc: "Project type (rails, javascript, etc.)"
22
+ option :default_branch, type: :string, desc: "Default branch name"
23
+ option :interactive, type: :boolean, aliases: "-i", desc: "Interactive mode"
24
+
25
+ def add(name = nil, path = nil)
26
+ ensure_initialized!
27
+
28
+ # Interactive mode
29
+ if options[:interactive] || name.nil? || path.nil?
30
+ name ||= @prompt.project_name
31
+ path ||= @prompt.project_path
32
+ end
33
+
34
+ begin
35
+ @ui.progress_start("Adding project '#{name}'")
36
+
37
+ project = @project_manager.add_project(
38
+ name,
39
+ path,
40
+ type: options[:type],
41
+ default_branch: options[:default_branch]
42
+ )
43
+
44
+ @ui.progress_done
45
+ @ui.success("Added project '#{name}'")
46
+
47
+ display_project_info(project)
48
+ rescue Sxn::Error => e
49
+ @ui.progress_failed
50
+ @ui.error(e.message)
51
+ exit(e.exit_code)
52
+ rescue StandardError => e
53
+ @ui.progress_failed
54
+ @ui.error("Unexpected error: #{e.message}")
55
+ @ui.debug(e.backtrace.join("\n")) if ENV["SXN_DEBUG"]
56
+ exit(1)
57
+ end
58
+ end
59
+
60
+ desc "remove NAME", "Remove a project"
61
+ option :force, type: :boolean, aliases: "-f", desc: "Force removal even if used in sessions"
62
+
63
+ def remove(name = nil)
64
+ ensure_initialized!
65
+
66
+ # Interactive selection if name not provided
67
+ if name.nil?
68
+ projects = @project_manager.list_projects
69
+ if projects.empty?
70
+ @ui.empty_state("No projects configured")
71
+ suggest_add_project
72
+ return
73
+ end
74
+
75
+ choices = projects.map { |p| { name: "#{p[:name]} (#{p[:type]})", value: p[:name] } }
76
+ name = @prompt.select("Select project to remove:", choices)
77
+ end
78
+
79
+ unless @prompt.confirm_deletion(name, "project")
80
+ @ui.info("Cancelled")
81
+ return
82
+ end
83
+
84
+ begin
85
+ @ui.progress_start("Removing project '#{name}'")
86
+ @project_manager.remove_project(name)
87
+ @ui.progress_done
88
+ @ui.success("Removed project '#{name}'")
89
+ rescue Sxn::ProjectInUseError => e
90
+ @ui.progress_failed
91
+ @ui.error(e.message)
92
+ @ui.recovery_suggestion("Archive or remove the sessions first, or use --force")
93
+ exit(e.exit_code)
94
+ rescue Sxn::Error => e
95
+ @ui.progress_failed
96
+ @ui.error(e.message)
97
+ exit(e.exit_code)
98
+ end
99
+ end
100
+
101
+ desc "list", "List all projects"
102
+ option :validate, type: :boolean, aliases: "-v", desc: "Validate project paths"
103
+
104
+ def list
105
+ ensure_initialized!
106
+
107
+ begin
108
+ projects = @project_manager.list_projects
109
+
110
+ @ui.section("Registered Projects")
111
+
112
+ if projects.empty?
113
+ @ui.empty_state("No projects configured")
114
+ suggest_add_project
115
+ elsif options[:validate]
116
+ list_with_validation(projects)
117
+ else
118
+ @table.projects(projects)
119
+ @ui.newline
120
+ @ui.info("Total: #{projects.size} projects")
121
+ end
122
+ rescue Sxn::Error => e
123
+ @ui.error(e.message)
124
+ exit(e.exit_code)
125
+ end
126
+ end
127
+
128
+ desc "scan [PATH]", "Scan for projects and optionally register them"
129
+ option :register, type: :boolean, aliases: "-r", desc: "Automatically register detected projects"
130
+ option :interactive, type: :boolean, aliases: "-i", default: true, desc: "Prompt before registering"
131
+
132
+ def scan(base_path = nil)
133
+ ensure_initialized!
134
+
135
+ base_path ||= Dir.pwd
136
+
137
+ begin
138
+ @ui.progress_start("Scanning for projects in #{base_path}")
139
+ detected = @project_manager.scan_projects(base_path)
140
+ @ui.progress_done
141
+
142
+ @ui.section("Detected Projects")
143
+
144
+ if detected.empty?
145
+ @ui.empty_state("No projects detected")
146
+ return
147
+ end
148
+
149
+ display_detected_projects(detected)
150
+
151
+ if options[:register]
152
+ register_projects(detected)
153
+ elsif options[:interactive]
154
+ register_projects(detected) if @prompt.ask_yes_no("Register detected projects?", default: true)
155
+ else
156
+ @ui.info("Use --register to add these projects automatically")
157
+ end
158
+ rescue Sxn::Error => e
159
+ @ui.error(e.message)
160
+ exit(e.exit_code)
161
+ end
162
+ end
163
+
164
+ desc "validate NAME", "Validate a project configuration"
165
+ def validate(name = nil)
166
+ ensure_initialized!
167
+
168
+ if name.nil?
169
+ projects = @project_manager.list_projects
170
+ if projects.empty?
171
+ @ui.empty_state("No projects configured")
172
+ return
173
+ end
174
+
175
+ choices = projects.map { |p| { name: "#{p[:name]} (#{p[:type]})", value: p[:name] } }
176
+ name = @prompt.select("Select project to validate:", choices)
177
+ end
178
+
179
+ begin
180
+ result = @project_manager.validate_project(name)
181
+
182
+ @ui.section("Project Validation: #{name}")
183
+
184
+ if result[:valid]
185
+ @ui.success("Project is valid")
186
+ else
187
+ @ui.error("Project has issues:")
188
+ result[:issues].each { |issue| @ui.list_item(issue) }
189
+ end
190
+
191
+ display_project_info(result[:project])
192
+ rescue Sxn::Error => e
193
+ @ui.error(e.message)
194
+ exit(e.exit_code)
195
+ end
196
+ end
197
+
198
+ desc "info NAME", "Show detailed project information"
199
+ def info(name = nil)
200
+ ensure_initialized!
201
+
202
+ if name.nil?
203
+ projects = @project_manager.list_projects
204
+ if projects.empty?
205
+ @ui.empty_state("No projects configured")
206
+ return
207
+ end
208
+
209
+ choices = projects.map { |p| { name: "#{p[:name]} (#{p[:type]})", value: p[:name] } }
210
+ name = @prompt.select("Select project:", choices)
211
+ end
212
+
213
+ begin
214
+ project = @project_manager.get_project(name)
215
+ raise Sxn::ProjectNotFoundError, "Project '#{name}' not found" unless project
216
+
217
+ @ui.section("Project Information: #{name}")
218
+ display_project_info(project, detailed: true)
219
+
220
+ # Show rules
221
+ rules_manager = Sxn::Core::RulesManager.new(@config_manager, @project_manager)
222
+ begin
223
+ rules = rules_manager.list_rules(name)
224
+ if rules.any?
225
+ @ui.subsection("Rules")
226
+ @table.rules(rules, name)
227
+ else
228
+ @ui.info("No rules configured for this project")
229
+ end
230
+ rescue StandardError => e
231
+ @ui.debug("Could not load rules: #{e.message}")
232
+ end
233
+ rescue Sxn::Error => e
234
+ @ui.error(e.message)
235
+ exit(e.exit_code)
236
+ end
237
+ end
238
+
239
+ private
240
+
241
+ def ensure_initialized!
242
+ return if @config_manager.initialized?
243
+
244
+ @ui.error("Project not initialized")
245
+ @ui.recovery_suggestion("Run 'sxn init' to initialize sxn in this project")
246
+ exit(1)
247
+ end
248
+
249
+ def display_project_info(project, detailed: false)
250
+ @ui.newline
251
+ @ui.key_value("Name", project[:name])
252
+ @ui.key_value("Type", project[:type] || "unknown")
253
+ @ui.key_value("Path", project[:path])
254
+ @ui.key_value("Default Branch", project[:default_branch] || "master")
255
+
256
+ if detailed
257
+ # Additional validation info
258
+ validation = @project_manager.validate_project(project[:name])
259
+ status = validation[:valid] ? "✅ Valid" : "❌ Invalid"
260
+ @ui.key_value("Status", status)
261
+
262
+ unless validation[:valid]
263
+ @ui.subsection("Issues")
264
+ validation[:issues].each { |issue| @ui.list_item(issue) }
265
+ end
266
+ end
267
+
268
+ @ui.newline
269
+ display_project_commands(project[:name])
270
+ end
271
+
272
+ def display_project_commands(project_name)
273
+ @ui.subsection("Available Commands")
274
+
275
+ @ui.command_example(
276
+ "sxn worktree add #{project_name} [branch]",
277
+ "Create worktree for this project"
278
+ )
279
+
280
+ @ui.command_example(
281
+ "sxn rules add #{project_name} <type> <config>",
282
+ "Add setup rules for this project"
283
+ )
284
+
285
+ @ui.command_example(
286
+ "sxn projects validate #{project_name}",
287
+ "Validate project configuration"
288
+ )
289
+ end
290
+
291
+ def display_detected_projects(projects)
292
+ projects.each do |project|
293
+ @ui.list_item("#{project[:name]} (#{project[:type]})", project[:path])
294
+ end
295
+ @ui.newline
296
+ @ui.info("Total: #{projects.size} projects detected")
297
+ end
298
+
299
+ def register_projects(projects)
300
+ return if projects.empty?
301
+
302
+ @ui.subsection("Registering Projects")
303
+
304
+ results = @project_manager.auto_register_projects(projects)
305
+
306
+ success_count = results.count { |r| r[:status] == :success }
307
+ error_count = results.count { |r| r[:status] == :error }
308
+
309
+ results.each do |result|
310
+ if result[:status] == :success
311
+ @ui.success("✅ #{result[:project][:name]}")
312
+ else
313
+ @ui.error("❌ #{result[:project][:name]}: #{result[:error]}")
314
+ end
315
+ end
316
+
317
+ @ui.newline
318
+ @ui.info("Registered #{success_count} projects successfully")
319
+ @ui.warning("#{error_count} projects failed") if error_count.positive?
320
+ end
321
+
322
+ def list_with_validation(projects)
323
+ @ui.subsection("Project Validation")
324
+
325
+ Sxn::UI::ProgressBar.with_progress("Validating projects", projects) do |project, progress|
326
+ validation = @project_manager.validate_project(project[:name])
327
+
328
+ status = validation[:valid] ? "✅" : "❌"
329
+ progress.log("#{status} #{project[:name]}")
330
+
331
+ unless validation[:valid]
332
+ validation[:issues].each do |issue|
333
+ progress.log(" - #{issue}")
334
+ end
335
+ end
336
+
337
+ validation
338
+ end
339
+
340
+ @ui.newline
341
+ @table.projects(projects)
342
+ end
343
+
344
+ def suggest_add_project
345
+ @ui.newline
346
+ @ui.recovery_suggestion("Add projects with 'sxn projects add <name> <path>' or scan with 'sxn projects scan'")
347
+ end
348
+ end
349
+ end
350
+ end