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,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sxn Templates module provides secure, sandboxed template processing
|
4
|
+
# using the Liquid template engine. It includes built-in templates for
|
5
|
+
# common project types and comprehensive security measures.
|
6
|
+
#
|
7
|
+
# Features:
|
8
|
+
# - Liquid-based template processing (safe, no code execution)
|
9
|
+
# - Whitelisted variables and filters
|
10
|
+
# - Built-in templates for Rails, JavaScript, and common projects
|
11
|
+
# - Template security validation
|
12
|
+
# - Variable collection from session, git, project, and environment
|
13
|
+
# - Performance optimizations with caching
|
14
|
+
#
|
15
|
+
# Example usage:
|
16
|
+
# engine = Sxn::Templates::TemplateEngine.new(session: session, project: project)
|
17
|
+
# engine.process_template("rails/CLAUDE.md", "/path/to/output.md")
|
18
|
+
|
19
|
+
require_relative "templates/errors"
|
20
|
+
require_relative "templates/template_security"
|
21
|
+
require_relative "templates/template_processor"
|
22
|
+
require_relative "templates/template_variables"
|
23
|
+
require_relative "templates/template_engine"
|
24
|
+
|
25
|
+
module Sxn
|
26
|
+
module Templates
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pastel"
|
4
|
+
|
5
|
+
module Sxn
|
6
|
+
module UI
|
7
|
+
# Formatted output with colors and status indicators
|
8
|
+
class Output
|
9
|
+
def initialize
|
10
|
+
@pastel = Pastel.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def success(message)
|
14
|
+
puts @pastel.green("✅ #{message}")
|
15
|
+
end
|
16
|
+
|
17
|
+
def error(message)
|
18
|
+
puts @pastel.red("❌ #{message}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def warning(message)
|
22
|
+
puts @pastel.yellow("⚠️ #{message}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def info(message)
|
26
|
+
puts @pastel.blue("ℹ️ #{message}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def debug(message)
|
30
|
+
puts @pastel.dim("🔍 #{message}") if debug_mode?
|
31
|
+
end
|
32
|
+
|
33
|
+
def status(label, message, color = :blue)
|
34
|
+
colored_label = @pastel.public_send(color, "[#{label.upcase}]")
|
35
|
+
puts "#{colored_label} #{message}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def section(title)
|
39
|
+
puts ""
|
40
|
+
puts @pastel.bold(@pastel.cyan("═" * 60))
|
41
|
+
puts @pastel.bold(@pastel.cyan(" #{title}"))
|
42
|
+
puts @pastel.bold(@pastel.cyan("═" * 60))
|
43
|
+
puts ""
|
44
|
+
end
|
45
|
+
|
46
|
+
def subsection(title)
|
47
|
+
puts ""
|
48
|
+
puts @pastel.bold(title.to_s)
|
49
|
+
puts @pastel.dim("─" * title.length)
|
50
|
+
end
|
51
|
+
|
52
|
+
def list_item(item, description = nil)
|
53
|
+
if description
|
54
|
+
puts " • #{@pastel.bold(item)} - #{description}"
|
55
|
+
else
|
56
|
+
puts " • #{item}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def empty_state(message)
|
61
|
+
puts @pastel.dim(" #{message}")
|
62
|
+
end
|
63
|
+
|
64
|
+
def key_value(key, value, indent: 0)
|
65
|
+
spacing = " " * indent
|
66
|
+
puts "#{spacing}#{@pastel.bold(key)}: #{value}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def progress_start(message)
|
70
|
+
print "#{message}... "
|
71
|
+
end
|
72
|
+
|
73
|
+
def progress_done
|
74
|
+
puts @pastel.green("✅")
|
75
|
+
end
|
76
|
+
|
77
|
+
def progress_failed
|
78
|
+
puts @pastel.red("❌")
|
79
|
+
end
|
80
|
+
|
81
|
+
def newline
|
82
|
+
puts ""
|
83
|
+
end
|
84
|
+
|
85
|
+
def recovery_suggestion(message)
|
86
|
+
puts ""
|
87
|
+
puts @pastel.yellow("💡 Suggestion: #{message}")
|
88
|
+
end
|
89
|
+
|
90
|
+
def command_example(command, description = nil)
|
91
|
+
puts " #{@pastel.dim(description)}" if description
|
92
|
+
puts " #{@pastel.cyan("$ #{command}")}"
|
93
|
+
puts ""
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def debug_mode?
|
99
|
+
ENV["SXN_DEBUG"] == "true"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-progressbar"
|
4
|
+
|
5
|
+
module Sxn
|
6
|
+
module UI
|
7
|
+
# Progress bars for long-running operations
|
8
|
+
class ProgressBar
|
9
|
+
def initialize(title, total: 100, format: :classic)
|
10
|
+
format_string = case format
|
11
|
+
when :classic
|
12
|
+
"#{title} [:bar] :percent :elapsed"
|
13
|
+
when :detailed
|
14
|
+
"#{title} [:bar] :current/:total (:percent) :elapsed ETA: :eta"
|
15
|
+
when :simple
|
16
|
+
"#{title} :percent"
|
17
|
+
else
|
18
|
+
title
|
19
|
+
end
|
20
|
+
|
21
|
+
@bar = TTY::ProgressBar.new(format_string, total: total, clear: true)
|
22
|
+
end
|
23
|
+
|
24
|
+
def advance(step = 1)
|
25
|
+
@bar.advance(step)
|
26
|
+
end
|
27
|
+
|
28
|
+
def finish
|
29
|
+
@bar.finish
|
30
|
+
end
|
31
|
+
|
32
|
+
def current
|
33
|
+
@bar.current
|
34
|
+
end
|
35
|
+
|
36
|
+
def total
|
37
|
+
@bar.total
|
38
|
+
end
|
39
|
+
|
40
|
+
def percent
|
41
|
+
@bar.percent
|
42
|
+
end
|
43
|
+
|
44
|
+
def log(message)
|
45
|
+
@bar.log(message)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.with_progress(title, items, format: :classic, &block)
|
49
|
+
return [] if items.empty?
|
50
|
+
|
51
|
+
progress = new(title, total: items.size, format: format)
|
52
|
+
results = []
|
53
|
+
|
54
|
+
items.each do |item|
|
55
|
+
result = block.call(item, progress)
|
56
|
+
results << result
|
57
|
+
progress.advance
|
58
|
+
end
|
59
|
+
|
60
|
+
progress.finish
|
61
|
+
results
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.for_operation(title, total_steps: 5, &block)
|
65
|
+
progress = new(title, total: total_steps, format: :detailed)
|
66
|
+
|
67
|
+
stepper = Stepper.new(progress)
|
68
|
+
result = block.call(stepper)
|
69
|
+
|
70
|
+
progress.finish
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
# Helper class for step-by-step operations
|
75
|
+
class Stepper
|
76
|
+
def initialize(progress_bar)
|
77
|
+
@progress = progress_bar
|
78
|
+
end
|
79
|
+
|
80
|
+
def step(message = nil)
|
81
|
+
@progress.log(message) if message
|
82
|
+
@progress.advance
|
83
|
+
end
|
84
|
+
|
85
|
+
def log(message)
|
86
|
+
@progress.log(message)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-prompt"
|
4
|
+
|
5
|
+
module Sxn
|
6
|
+
module UI
|
7
|
+
# Interactive prompts with validation
|
8
|
+
class Prompt
|
9
|
+
def initialize
|
10
|
+
@prompt = TTY::Prompt.new(interrupt: :exit)
|
11
|
+
end
|
12
|
+
|
13
|
+
def ask(message, options = {}, &)
|
14
|
+
@prompt.ask(message, **options, &)
|
15
|
+
end
|
16
|
+
|
17
|
+
def ask_yes_no(message, default: false)
|
18
|
+
@prompt.yes?(message, default: default)
|
19
|
+
end
|
20
|
+
|
21
|
+
def select(message, choices, options = {})
|
22
|
+
@prompt.select(message, choices, **options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def multi_select(message, choices, options = {})
|
26
|
+
@prompt.multi_select(message, choices, **options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def folder_name(message = "Enter sessions folder name:", default: nil)
|
30
|
+
ask(message, default: default) do |q|
|
31
|
+
q.validate(/\A[a-zA-Z0-9_-]+\z/, "Folder name must contain only letters, numbers, hyphens, and underscores")
|
32
|
+
q.modify :strip
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def session_name(message = "Enter session name:", existing_sessions: [])
|
37
|
+
ask(message) do |q|
|
38
|
+
q.validate(/\A[a-zA-Z0-9_-]+\z/, "Session name must contain only letters, numbers, hyphens, and underscores")
|
39
|
+
q.validate(lambda { |name|
|
40
|
+
!existing_sessions.include?(name)
|
41
|
+
}, "Session name already exists")
|
42
|
+
q.modify :strip
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def project_name(message = "Enter project name:")
|
47
|
+
ask(message) do |q|
|
48
|
+
q.validate(/\A[a-zA-Z0-9_-]+\z/, "Project name must contain only letters, numbers, hyphens, and underscores")
|
49
|
+
q.modify :strip
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def project_path(message = "Enter project path:")
|
54
|
+
ask(message) do |q|
|
55
|
+
q.validate(lambda { |path|
|
56
|
+
expanded = File.expand_path(path)
|
57
|
+
File.directory?(expanded) && File.readable?(expanded)
|
58
|
+
}, "Path must be a readable directory")
|
59
|
+
q.modify :strip
|
60
|
+
q.convert ->(path) { File.expand_path(path) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def branch_name(message = "Enter branch name:", default: nil)
|
65
|
+
ask(message, default: default) do |q|
|
66
|
+
q.validate(%r{\A[a-zA-Z0-9_/-]+\z}, "Branch name must be a valid git branch name")
|
67
|
+
q.modify :strip
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def confirm_deletion(item_name, item_type = "item")
|
72
|
+
ask_yes_no("Are you sure you want to delete #{item_type} '#{item_name}'? This action cannot be undone.",
|
73
|
+
default: false)
|
74
|
+
end
|
75
|
+
|
76
|
+
def rule_type
|
77
|
+
select("Select rule type:", [
|
78
|
+
{ name: "Copy Files", value: "copy_files" },
|
79
|
+
{ name: "Setup Commands", value: "setup_commands" },
|
80
|
+
{ name: "Template", value: "template" }
|
81
|
+
])
|
82
|
+
end
|
83
|
+
|
84
|
+
def sessions_folder_setup
|
85
|
+
puts "Setting up sessions folder..."
|
86
|
+
puts "This will create a folder where all your development sessions will be stored."
|
87
|
+
puts ""
|
88
|
+
|
89
|
+
default_folder = "#{File.basename(Dir.pwd)}-sessions"
|
90
|
+
folder = folder_name("Sessions folder name:", default: default_folder)
|
91
|
+
|
92
|
+
current_dir = ask_yes_no("Create sessions folder in current directory?", default: true)
|
93
|
+
|
94
|
+
unless current_dir
|
95
|
+
base_path = project_path("Base path for sessions folder:")
|
96
|
+
folder = File.join(base_path, folder)
|
97
|
+
end
|
98
|
+
|
99
|
+
folder
|
100
|
+
end
|
101
|
+
|
102
|
+
def project_detection_confirm(detected_projects)
|
103
|
+
return false if detected_projects.empty?
|
104
|
+
|
105
|
+
puts ""
|
106
|
+
puts "Detected projects in current directory:"
|
107
|
+
detected_projects.each do |project|
|
108
|
+
puts " #{project[:name]} (#{project[:type]}) - #{project[:path]}"
|
109
|
+
end
|
110
|
+
puts ""
|
111
|
+
|
112
|
+
ask_yes_no("Would you like to register these projects automatically?", default: true)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/sxn/ui/table.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-table"
|
4
|
+
require "pastel"
|
5
|
+
|
6
|
+
module Sxn
|
7
|
+
module UI
|
8
|
+
# Table formatting for lists and data display
|
9
|
+
class Table
|
10
|
+
def initialize
|
11
|
+
@pastel = Pastel.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def sessions(sessions)
|
15
|
+
return empty_table("No sessions found") if sessions.empty?
|
16
|
+
|
17
|
+
headers = %w[Name Status Projects Created Updated]
|
18
|
+
rows = sessions.map do |session|
|
19
|
+
[
|
20
|
+
session[:name],
|
21
|
+
status_indicator(session[:status]),
|
22
|
+
session[:projects]&.join(", ") || "",
|
23
|
+
format_date(session[:created_at]),
|
24
|
+
format_date(session[:updated_at])
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
render_table(headers, rows)
|
29
|
+
end
|
30
|
+
|
31
|
+
def projects(projects)
|
32
|
+
return empty_table("No projects configured") if projects.empty?
|
33
|
+
|
34
|
+
headers = ["Name", "Type", "Path", "Default Branch"]
|
35
|
+
rows = projects.map do |project|
|
36
|
+
[
|
37
|
+
project[:name],
|
38
|
+
project[:type] || "unknown",
|
39
|
+
truncate_path(project[:path]),
|
40
|
+
project[:default_branch] || "master"
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
render_table(headers, rows)
|
45
|
+
end
|
46
|
+
|
47
|
+
def worktrees(worktrees)
|
48
|
+
return empty_table("No worktrees in current session") if worktrees.empty?
|
49
|
+
|
50
|
+
headers = %w[Project Branch Path Status]
|
51
|
+
rows = worktrees.map do |worktree|
|
52
|
+
[
|
53
|
+
worktree[:project],
|
54
|
+
worktree[:branch],
|
55
|
+
truncate_path(worktree[:path]),
|
56
|
+
worktree_status(worktree)
|
57
|
+
]
|
58
|
+
end
|
59
|
+
|
60
|
+
render_table(headers, rows)
|
61
|
+
end
|
62
|
+
|
63
|
+
def rules(rules, project_filter = nil)
|
64
|
+
filtered_rules = project_filter ? rules.select { |r| r[:project] == project_filter } : rules
|
65
|
+
return empty_table("No rules configured") if filtered_rules.empty?
|
66
|
+
|
67
|
+
headers = %w[Project Type Config Status]
|
68
|
+
rows = filtered_rules.map do |rule|
|
69
|
+
[
|
70
|
+
rule[:project],
|
71
|
+
rule[:type],
|
72
|
+
truncate_config(rule[:config]),
|
73
|
+
rule[:enabled] ? @pastel.green("✓") : @pastel.red("✗")
|
74
|
+
]
|
75
|
+
end
|
76
|
+
|
77
|
+
render_table(headers, rows)
|
78
|
+
end
|
79
|
+
|
80
|
+
def config_summary(config)
|
81
|
+
headers = %w[Setting Value Source]
|
82
|
+
rows = [
|
83
|
+
["Sessions Folder", config[:sessions_folder] || "Not set", "config"],
|
84
|
+
["Current Session", config[:current_session] || "None", "config"],
|
85
|
+
["Auto Cleanup", config[:auto_cleanup] ? "Enabled" : "Disabled", "config"],
|
86
|
+
["Max Sessions", config[:max_sessions] || "Unlimited", "config"]
|
87
|
+
]
|
88
|
+
|
89
|
+
render_table(headers, rows)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Add a header to the table output
|
93
|
+
def header(title)
|
94
|
+
puts "\n#{@pastel.bold.underline(title)}"
|
95
|
+
puts
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def render_table(headers, rows)
|
101
|
+
table = TTY::Table.new(header: headers, rows: rows)
|
102
|
+
puts table.render(:unicode, padding: [0, 1])
|
103
|
+
end
|
104
|
+
|
105
|
+
def empty_table(message)
|
106
|
+
puts @pastel.dim(" #{message}")
|
107
|
+
end
|
108
|
+
|
109
|
+
def status_indicator(status)
|
110
|
+
case status
|
111
|
+
when "active"
|
112
|
+
@pastel.green("● Active")
|
113
|
+
when "inactive"
|
114
|
+
@pastel.yellow("○ Inactive")
|
115
|
+
when "archived"
|
116
|
+
@pastel.dim("◌ Archived")
|
117
|
+
else
|
118
|
+
@pastel.dim("? Unknown")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def worktree_status(worktree)
|
123
|
+
if File.directory?(worktree[:path])
|
124
|
+
if git_clean?(worktree[:path])
|
125
|
+
@pastel.green("Clean")
|
126
|
+
else
|
127
|
+
@pastel.yellow("Modified")
|
128
|
+
end
|
129
|
+
else
|
130
|
+
@pastel.red("Missing")
|
131
|
+
end
|
132
|
+
rescue StandardError
|
133
|
+
@pastel.red("Missing")
|
134
|
+
end
|
135
|
+
|
136
|
+
def git_clean?(path)
|
137
|
+
result = Dir.chdir(path) do
|
138
|
+
system("git diff-index --quiet HEAD --", out: File::NULL, err: File::NULL)
|
139
|
+
end
|
140
|
+
!!result
|
141
|
+
rescue StandardError
|
142
|
+
false
|
143
|
+
end
|
144
|
+
|
145
|
+
def format_date(date_string)
|
146
|
+
return "" unless date_string
|
147
|
+
|
148
|
+
date = Time.parse(date_string)
|
149
|
+
if date > Time.now - 86_400 # Within 24 hours
|
150
|
+
date.strftime("%H:%M")
|
151
|
+
elsif date > Time.now - 604_800 # Within a week
|
152
|
+
date.strftime("%a %H:%M")
|
153
|
+
else
|
154
|
+
date.strftime("%m/%d")
|
155
|
+
end
|
156
|
+
rescue StandardError
|
157
|
+
date_string
|
158
|
+
end
|
159
|
+
|
160
|
+
def truncate_path(path, max_length: 30)
|
161
|
+
return "" unless path
|
162
|
+
return path if path.length <= max_length
|
163
|
+
|
164
|
+
# Show just the filename with "..." prefix
|
165
|
+
filename = File.basename(path)
|
166
|
+
"...#{filename}"
|
167
|
+
end
|
168
|
+
|
169
|
+
def truncate_config(config, max_length: 40)
|
170
|
+
return "" unless config
|
171
|
+
|
172
|
+
config_str = config.is_a?(String) ? config : config.to_s
|
173
|
+
return config_str if config_str.length <= max_length
|
174
|
+
|
175
|
+
# Take the beginning and add "..." at the end
|
176
|
+
# For max_length 20, we want "This is a very lo..." (20 chars total)
|
177
|
+
# So we take first 17 chars + "..." = 20 chars total
|
178
|
+
truncate_length = max_length - 3
|
179
|
+
"#{config_str[0, truncate_length]}..."
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
data/lib/sxn/ui.rb
ADDED
data/lib/sxn/version.rb
ADDED
data/lib/sxn.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zeitwerk"
|
4
|
+
require_relative "sxn/version"
|
5
|
+
require_relative "sxn/errors"
|
6
|
+
require_relative "sxn/runtime_validations"
|
7
|
+
require_relative "sxn/config"
|
8
|
+
require_relative "sxn/core"
|
9
|
+
require_relative "sxn/database"
|
10
|
+
require_relative "sxn/rules"
|
11
|
+
require_relative "sxn/security"
|
12
|
+
require_relative "sxn/templates"
|
13
|
+
require_relative "sxn/ui"
|
14
|
+
require_relative "sxn/commands"
|
15
|
+
require_relative "sxn/CLI"
|
16
|
+
|
17
|
+
module Sxn
|
18
|
+
class << self
|
19
|
+
attr_accessor :logger, :config
|
20
|
+
|
21
|
+
def root
|
22
|
+
File.expand_path("..", __dir__)
|
23
|
+
end
|
24
|
+
|
25
|
+
def lib_root
|
26
|
+
File.expand_path(__dir__)
|
27
|
+
end
|
28
|
+
|
29
|
+
def version
|
30
|
+
VERSION
|
31
|
+
end
|
32
|
+
|
33
|
+
def load_config
|
34
|
+
@config = Config.current
|
35
|
+
end
|
36
|
+
|
37
|
+
def setup_logger(level: :info)
|
38
|
+
require "logger"
|
39
|
+
@logger = Logger.new($stdout)
|
40
|
+
|
41
|
+
# Convert string level to symbol if needed
|
42
|
+
level = level.to_sym if level.is_a?(String)
|
43
|
+
|
44
|
+
@logger.level = case level
|
45
|
+
when :debug then Logger::DEBUG
|
46
|
+
when :info then Logger::INFO
|
47
|
+
when :warn then Logger::WARN
|
48
|
+
when :error then Logger::ERROR
|
49
|
+
else Logger::INFO
|
50
|
+
end
|
51
|
+
|
52
|
+
# Set custom formatter
|
53
|
+
@logger.formatter = proc do |severity, datetime, _progname, msg|
|
54
|
+
"[#{datetime.strftime("%Y-%m-%d %H:%M:%S")}] #{severity}: #{msg}\n"
|
55
|
+
end
|
56
|
+
|
57
|
+
@logger
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Initialize logger on module load unless in test environment
|
62
|
+
@logger = setup_logger unless defined?(RSpec)
|
63
|
+
end
|