sxn 0.2.2 โ 0.2.5
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 +4 -4
- data/CHANGELOG.md +51 -0
- data/Gemfile.lock +1 -1
- data/README.md +76 -8
- data/lib/sxn/CLI.rb +99 -4
- data/lib/sxn/commands/init.rb +136 -0
- data/lib/sxn/commands/sessions.rb +197 -7
- data/lib/sxn/commands/worktrees.rb +40 -5
- data/lib/sxn/core/config_manager.rb +17 -1
- data/lib/sxn/core/project_manager.rb +19 -2
- data/lib/sxn/core/rules_manager.rb +61 -3
- data/lib/sxn/core/session_config.rb +91 -0
- data/lib/sxn/core/session_manager.rb +21 -1
- data/lib/sxn/core/worktree_manager.rb +133 -7
- data/lib/sxn/core.rb +1 -0
- data/lib/sxn/errors.rb +10 -1
- data/lib/sxn/ui/prompt.rb +4 -0
- data/lib/sxn/version.rb +1 -1
- data/script/setup-hooks +52 -0
- data/sxn.gemspec +85 -0
- metadata +4 -1
|
@@ -14,7 +14,7 @@ module Sxn
|
|
|
14
14
|
@project_manager = ProjectManager.new(@config_manager)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def add_worktree(project_name, branch = nil, session_name: nil)
|
|
17
|
+
def add_worktree(project_name, branch = nil, session_name: nil, verbose: false)
|
|
18
18
|
# Use current session if not specified
|
|
19
19
|
session_name ||= @config_manager.current_session
|
|
20
20
|
raise Sxn::NoActiveSessionError, "No active session. Use 'sxn use <session>' first." unless session_name
|
|
@@ -25,8 +25,32 @@ module Sxn
|
|
|
25
25
|
project = @project_manager.get_project(project_name)
|
|
26
26
|
raise Sxn::ProjectNotFoundError, "Project '#{project_name}' not found" unless project
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
branch
|
|
28
|
+
# Determine branch name
|
|
29
|
+
# If no branch specified, use session's default branch from .sxnrc, then fallback to session name
|
|
30
|
+
# If branch starts with "remote:", handle remote branch tracking
|
|
31
|
+
if branch.nil?
|
|
32
|
+
session_config = SessionConfig.new(session[:path])
|
|
33
|
+
branch = session_config.default_branch if session_config.exists?
|
|
34
|
+
branch ||= session_name
|
|
35
|
+
elsif branch.start_with?("remote:")
|
|
36
|
+
remote_branch = branch.sub("remote:", "")
|
|
37
|
+
# Fetch the remote branch first
|
|
38
|
+
begin
|
|
39
|
+
fetch_remote_branch(project[:path], remote_branch)
|
|
40
|
+
branch = remote_branch
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
raise Sxn::WorktreeCreationError, "Failed to fetch remote branch: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if ENV["SXN_DEBUG"]
|
|
47
|
+
puts "[DEBUG] Adding worktree:"
|
|
48
|
+
puts " Project: #{project_name}"
|
|
49
|
+
puts " Project path: #{project[:path]}"
|
|
50
|
+
puts " Session: #{session_name}"
|
|
51
|
+
puts " Session path: #{session[:path]}"
|
|
52
|
+
puts " Branch: #{branch}"
|
|
53
|
+
end
|
|
30
54
|
|
|
31
55
|
# Check if worktree already exists in this session
|
|
32
56
|
existing_worktrees = @session_manager.get_session_worktrees(session_name)
|
|
@@ -38,9 +62,14 @@ module Sxn
|
|
|
38
62
|
# Create worktree path
|
|
39
63
|
worktree_path = File.join(session[:path], project_name)
|
|
40
64
|
|
|
65
|
+
puts " Worktree path: #{worktree_path}" if ENV["SXN_DEBUG"]
|
|
66
|
+
|
|
41
67
|
begin
|
|
68
|
+
# Handle orphaned worktree if it exists
|
|
69
|
+
handle_orphaned_worktree(project[:path], worktree_path)
|
|
70
|
+
|
|
42
71
|
# Create the worktree
|
|
43
|
-
create_git_worktree(project[:path], worktree_path, branch)
|
|
72
|
+
create_git_worktree(project[:path], worktree_path, branch, verbose: verbose)
|
|
44
73
|
|
|
45
74
|
# Register worktree with session
|
|
46
75
|
@session_manager.add_worktree_to_session(session_name, project_name, worktree_path, branch)
|
|
@@ -54,6 +83,11 @@ module Sxn
|
|
|
54
83
|
rescue StandardError => e
|
|
55
84
|
# Clean up on failure
|
|
56
85
|
FileUtils.rm_rf(worktree_path)
|
|
86
|
+
|
|
87
|
+
# If it's already our error with details, re-raise it
|
|
88
|
+
raise e if e.is_a?(Sxn::WorktreeCreationError)
|
|
89
|
+
|
|
90
|
+
# Otherwise wrap it
|
|
57
91
|
raise Sxn::WorktreeCreationError, "Failed to create worktree: #{e.message}"
|
|
58
92
|
end
|
|
59
93
|
end
|
|
@@ -169,7 +203,48 @@ module Sxn
|
|
|
169
203
|
|
|
170
204
|
private
|
|
171
205
|
|
|
172
|
-
def
|
|
206
|
+
def handle_orphaned_worktree(project_path, worktree_path)
|
|
207
|
+
Dir.chdir(project_path) do
|
|
208
|
+
# Try to prune worktrees first
|
|
209
|
+
system("git worktree prune", out: File::NULL, err: File::NULL)
|
|
210
|
+
|
|
211
|
+
# Check if this worktree is registered (whether it exists or not)
|
|
212
|
+
output = `git worktree list --porcelain 2>/dev/null`
|
|
213
|
+
if output.include?(worktree_path)
|
|
214
|
+
# Force remove the orphaned/existing worktree
|
|
215
|
+
system("git worktree remove --force #{Shellwords.escape(worktree_path)}", out: File::NULL, err: File::NULL)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Remove the directory if it still exists
|
|
220
|
+
FileUtils.rm_rf(worktree_path)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def fetch_remote_branch(project_path, branch_name)
|
|
224
|
+
Dir.chdir(project_path) do
|
|
225
|
+
# First, fetch all remotes to ensure we have the latest branches
|
|
226
|
+
raise "Failed to fetch remote branches" unless system("git fetch --all", out: File::NULL, err: File::NULL)
|
|
227
|
+
|
|
228
|
+
# Check if the branch exists on any remote
|
|
229
|
+
remotes = `git remote`.lines.map(&:strip)
|
|
230
|
+
branch_found = false
|
|
231
|
+
|
|
232
|
+
remotes.each do |remote|
|
|
233
|
+
next unless system("git show-ref --verify --quiet refs/remotes/#{remote}/#{Shellwords.escape(branch_name)}",
|
|
234
|
+
out: File::NULL, err: File::NULL)
|
|
235
|
+
|
|
236
|
+
branch_found = true
|
|
237
|
+
# Set up tracking for the remote branch
|
|
238
|
+
system("git branch --track #{Shellwords.escape(branch_name)} #{remote}/#{Shellwords.escape(branch_name)}",
|
|
239
|
+
out: File::NULL, err: File::NULL)
|
|
240
|
+
break
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
raise "Remote branch '#{branch_name}' not found on any remote" unless branch_found
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def create_git_worktree(project_path, worktree_path, branch, verbose: false)
|
|
173
248
|
Dir.chdir(project_path) do
|
|
174
249
|
# Check if branch exists
|
|
175
250
|
branch_exists = system("git show-ref --verify --quiet refs/heads/#{Shellwords.escape(branch)}",
|
|
@@ -183,8 +258,59 @@ module Sxn
|
|
|
183
258
|
["git", "worktree", "add", "-b", branch, worktree_path]
|
|
184
259
|
end
|
|
185
260
|
|
|
186
|
-
|
|
187
|
-
|
|
261
|
+
# Capture stderr for better error messages
|
|
262
|
+
require "open3"
|
|
263
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
264
|
+
|
|
265
|
+
unless status.success?
|
|
266
|
+
error_msg = stderr.empty? ? stdout : stderr
|
|
267
|
+
error_msg = "Git worktree command failed" if error_msg.strip.empty?
|
|
268
|
+
|
|
269
|
+
# Add more context to common errors
|
|
270
|
+
if error_msg.include?("already exists")
|
|
271
|
+
error_msg += "\nTry removing the existing worktree first with: sxn worktree remove #{File.basename(worktree_path)}"
|
|
272
|
+
elsif error_msg.include?("is already checked out")
|
|
273
|
+
error_msg += "\nThis branch is already checked out in another worktree"
|
|
274
|
+
elsif error_msg.include?("not a git repository")
|
|
275
|
+
error_msg = "Project '#{File.basename(project_path)}' is not a git repository"
|
|
276
|
+
elsif error_msg.include?("fatal: invalid reference")
|
|
277
|
+
# This typically means the branch doesn't exist and we're trying to create from a non-existent base
|
|
278
|
+
error_msg += "\nMake sure the repository has at least one commit or specify an existing branch"
|
|
279
|
+
elsif error_msg.include?("fatal:")
|
|
280
|
+
# Extract just the fatal error message for cleaner output
|
|
281
|
+
error_msg = error_msg.lines.grep(/fatal:/).first&.strip || error_msg
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
details = []
|
|
285
|
+
details << "Command: #{cmd.join(" ")}"
|
|
286
|
+
details << "Working directory: #{project_path}"
|
|
287
|
+
details << "Target path: #{worktree_path}"
|
|
288
|
+
details << "Branch: #{branch}"
|
|
289
|
+
details << ""
|
|
290
|
+
details << "Git output:"
|
|
291
|
+
details << "STDOUT: #{stdout.strip.empty? ? "(empty)" : stdout}"
|
|
292
|
+
details << "STDERR: #{stderr.strip.empty? ? "(empty)" : stderr}"
|
|
293
|
+
details << "Exit status: #{status.exitstatus}"
|
|
294
|
+
|
|
295
|
+
# Check for common issues
|
|
296
|
+
if !File.directory?(project_path)
|
|
297
|
+
details << "\nโ ๏ธ Project directory does not exist: #{project_path}"
|
|
298
|
+
elsif !File.directory?(File.join(project_path, ".git"))
|
|
299
|
+
details << "\nโ ๏ธ Not a git repository: #{project_path}"
|
|
300
|
+
details << " This might be a git submodule. Try:"
|
|
301
|
+
details << " 1. Ensure the project path points to the submodule directory"
|
|
302
|
+
details << " 2. Check if 'git submodule update --init' has been run"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
details << "\nโ ๏ธ Target path already exists: #{worktree_path}" if File.exist?(worktree_path)
|
|
306
|
+
|
|
307
|
+
if ENV["SXN_DEBUG"] || verbose
|
|
308
|
+
puts "[DEBUG] Git worktree command failed:"
|
|
309
|
+
details.each { |line| puts " #{line}" }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
raise Sxn::WorktreeCreationError.new(error_msg, details: details.join("\n"))
|
|
313
|
+
end
|
|
188
314
|
end
|
|
189
315
|
end
|
|
190
316
|
|
data/lib/sxn/core.rb
CHANGED
data/lib/sxn/errors.rb
CHANGED
|
@@ -37,7 +37,16 @@ module Sxn
|
|
|
37
37
|
class WorktreeError < GitError; end
|
|
38
38
|
class WorktreeExistsError < WorktreeError; end
|
|
39
39
|
class WorktreeNotFoundError < WorktreeError; end
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
class WorktreeCreationError < WorktreeError
|
|
42
|
+
attr_reader :details
|
|
43
|
+
|
|
44
|
+
def initialize(message, details: nil)
|
|
45
|
+
super(message)
|
|
46
|
+
@details = details
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
class WorktreeRemovalError < WorktreeError; end
|
|
42
51
|
class BranchError < GitError; end
|
|
43
52
|
|
data/lib/sxn/ui/prompt.rb
CHANGED
|
@@ -68,6 +68,10 @@ module Sxn
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
def default_branch(session_name:)
|
|
72
|
+
branch_name("Default branch for worktrees:", default: session_name)
|
|
73
|
+
end
|
|
74
|
+
|
|
71
75
|
def confirm_deletion(item_name, item_type = "item")
|
|
72
76
|
ask_yes_no("Are you sure you want to delete #{item_type} '#{item_name}'? This action cannot be undone.",
|
|
73
77
|
default: false)
|
data/lib/sxn/version.rb
CHANGED
data/script/setup-hooks
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Setup git hooks for sxn development
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
echo "๐ง Setting up git hooks for sxn development..."
|
|
9
|
+
|
|
10
|
+
# Create hooks directory if it doesn't exist
|
|
11
|
+
mkdir -p .git/hooks
|
|
12
|
+
|
|
13
|
+
# Create pre-push hook
|
|
14
|
+
cat > .git/hooks/pre-push << 'EOF'
|
|
15
|
+
#!/bin/sh
|
|
16
|
+
#
|
|
17
|
+
# Pre-push hook for sxn project
|
|
18
|
+
# Runs RuboCop and RSpec before allowing push
|
|
19
|
+
#
|
|
20
|
+
|
|
21
|
+
echo "๐ Running pre-push checks..."
|
|
22
|
+
|
|
23
|
+
# Run RuboCop
|
|
24
|
+
echo "๐ Checking code style with RuboCop..."
|
|
25
|
+
bundle exec rubocop
|
|
26
|
+
if [ $? -ne 0 ]; then
|
|
27
|
+
echo "โ RuboCop failed! Please fix linting issues before pushing."
|
|
28
|
+
echo " Run: bundle exec rubocop -a"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Run RSpec tests
|
|
33
|
+
echo "๐งช Running tests with RSpec..."
|
|
34
|
+
bundle exec rspec --format progress
|
|
35
|
+
if [ $? -ne 0 ]; then
|
|
36
|
+
echo "โ Tests failed! Please fix failing tests before pushing."
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
echo "โ
All checks passed! Proceeding with push..."
|
|
41
|
+
exit 0
|
|
42
|
+
EOF
|
|
43
|
+
|
|
44
|
+
chmod +x .git/hooks/pre-push
|
|
45
|
+
|
|
46
|
+
echo "โ
Git hooks installed successfully!"
|
|
47
|
+
echo ""
|
|
48
|
+
echo "The following hooks have been set up:"
|
|
49
|
+
echo " โข pre-push: Runs RuboCop and RSpec before pushing"
|
|
50
|
+
echo ""
|
|
51
|
+
echo "To bypass hooks in emergency (not recommended):"
|
|
52
|
+
echo " git push --no-verify"
|
data/sxn.gemspec
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/sxn/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "sxn"
|
|
7
|
+
spec.version = Sxn::VERSION
|
|
8
|
+
spec.authors = ["Ernest Sim"]
|
|
9
|
+
spec.email = ["ernest.codes@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Session management for multi-repository development"
|
|
12
|
+
spec.description = "Sxn simplifies git worktree management with intelligent project rules and secure automation"
|
|
13
|
+
spec.homepage = "https://github.com/idl3/sxn"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
21
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
22
|
+
|
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
27
|
+
(f == __FILE__) ||
|
|
28
|
+
f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) ||
|
|
29
|
+
f.match(/\.db-(?:shm|wal)\z/) || # Exclude SQLite temp files
|
|
30
|
+
f.match(/\.gem\z/) # Exclude gem files
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
spec.bindir = "bin"
|
|
35
|
+
spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
|
|
36
|
+
spec.require_paths = ["lib"]
|
|
37
|
+
|
|
38
|
+
# Core CLI dependencies
|
|
39
|
+
spec.add_dependency "pastel", "~> 0.8" # Terminal colors
|
|
40
|
+
spec.add_dependency "thor", "~> 1.3" # CLI framework
|
|
41
|
+
spec.add_dependency "tty-progressbar", "~> 0.18" # Progress bars
|
|
42
|
+
spec.add_dependency "tty-prompt", "~> 0.23" # Interactive prompts
|
|
43
|
+
spec.add_dependency "tty-table", "~> 0.12" # Table formatting
|
|
44
|
+
|
|
45
|
+
# Configuration and data management
|
|
46
|
+
spec.add_dependency "dry-configurable", "~> 1.0" # Configuration management
|
|
47
|
+
spec.add_dependency "sqlite3", "~> 1.6" # Session database
|
|
48
|
+
spec.add_dependency "zeitwerk", "~> 2.6" # Code loading
|
|
49
|
+
|
|
50
|
+
# Template engine (secure, sandboxed)
|
|
51
|
+
spec.add_dependency "liquid", "~> 5.4" # Safe template processing
|
|
52
|
+
|
|
53
|
+
# MCP server dependencies
|
|
54
|
+
spec.add_dependency "async", "~> 2.0" # Async operations
|
|
55
|
+
spec.add_dependency "json-schema", "~> 4.0" # Schema validation
|
|
56
|
+
|
|
57
|
+
# Security and encryption
|
|
58
|
+
spec.add_dependency "bcrypt", "~> 3.1" # Password hashing
|
|
59
|
+
spec.add_dependency "openssl", ">= 3.0" # Encryption support
|
|
60
|
+
spec.add_dependency "ostruct" # OpenStruct for Ruby 3.5+ compatibility
|
|
61
|
+
|
|
62
|
+
# File system operations
|
|
63
|
+
spec.add_dependency "listen", "~> 3.8" # File watching for config cache
|
|
64
|
+
spec.add_dependency "parallel", "~> 1.23" # Parallel execution
|
|
65
|
+
|
|
66
|
+
# Development dependencies
|
|
67
|
+
spec.add_development_dependency "aruba", "~> 2.1" # CLI testing
|
|
68
|
+
spec.add_development_dependency "benchmark" # Benchmark for Ruby 3.5+ compatibility
|
|
69
|
+
spec.add_development_dependency "benchmark-ips", "~> 2.12" # Performance benchmarking
|
|
70
|
+
spec.add_development_dependency "bundler", "~> 2.4"
|
|
71
|
+
spec.add_development_dependency "climate_control", "~> 1.2" # Environment variable testing
|
|
72
|
+
spec.add_development_dependency "faker", "~> 3.2" # Test data generation
|
|
73
|
+
spec.add_development_dependency "memory_profiler", "~> 1.0" # Memory profiling
|
|
74
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
75
|
+
spec.add_development_dependency "rspec", "~> 3.12"
|
|
76
|
+
spec.add_development_dependency "rubocop", "~> 1.50" # Code linting
|
|
77
|
+
spec.add_development_dependency "rubocop-performance", "~> 1.16"
|
|
78
|
+
spec.add_development_dependency "rubocop-rspec", "~> 2.19"
|
|
79
|
+
spec.add_development_dependency "simplecov", "~> 0.22" # Code coverage
|
|
80
|
+
spec.add_development_dependency "vcr", "~> 6.2" # HTTP interaction recording
|
|
81
|
+
spec.add_development_dependency "webmock", "~> 3.19" # HTTP mocking for MCP tests
|
|
82
|
+
|
|
83
|
+
# For more information and examples about making a new gem, check out our
|
|
84
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
85
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sxn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ernest Sim
|
|
@@ -518,6 +518,7 @@ files:
|
|
|
518
518
|
- lib/sxn/core/config_manager.rb
|
|
519
519
|
- lib/sxn/core/project_manager.rb
|
|
520
520
|
- lib/sxn/core/rules_manager.rb
|
|
521
|
+
- lib/sxn/core/session_config.rb
|
|
521
522
|
- lib/sxn/core/session_manager.rb
|
|
522
523
|
- lib/sxn/core/worktree_manager.rb
|
|
523
524
|
- lib/sxn/database.rb
|
|
@@ -558,6 +559,7 @@ files:
|
|
|
558
559
|
- lib/sxn/version.rb
|
|
559
560
|
- rbs_collection.lock.yaml
|
|
560
561
|
- rbs_collection.yaml
|
|
562
|
+
- script/setup-hooks
|
|
561
563
|
- scripts/test.sh
|
|
562
564
|
- sig/external/liquid.rbs
|
|
563
565
|
- sig/external/thor.rbs
|
|
@@ -609,6 +611,7 @@ files:
|
|
|
609
611
|
- sig/sxn/ui/prompt.rbs
|
|
610
612
|
- sig/sxn/ui/table.rbs
|
|
611
613
|
- sig/sxn/version.rbs
|
|
614
|
+
- sxn.gemspec
|
|
612
615
|
homepage: https://github.com/idl3/sxn
|
|
613
616
|
licenses:
|
|
614
617
|
- MIT
|