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