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,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
|