sxn 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. checksums.yaml +7 -0
  2. data/.gem_rbs_collection/addressable/2.8/.rbs_meta.yaml +9 -0
  3. data/.gem_rbs_collection/addressable/2.8/addressable.rbs +62 -0
  4. data/.gem_rbs_collection/async/2.12/.rbs_meta.yaml +9 -0
  5. data/.gem_rbs_collection/async/2.12/async.rbs +119 -0
  6. data/.gem_rbs_collection/async/2.12/kernel.rbs +5 -0
  7. data/.gem_rbs_collection/async/2.12/manifest.yaml +7 -0
  8. data/.gem_rbs_collection/bcrypt/3.1/.rbs_meta.yaml +9 -0
  9. data/.gem_rbs_collection/bcrypt/3.1/bcrypt.rbs +47 -0
  10. data/.gem_rbs_collection/bcrypt/3.1/manifest.yaml +2 -0
  11. data/.gem_rbs_collection/bigdecimal/3.1/.rbs_meta.yaml +9 -0
  12. data/.gem_rbs_collection/bigdecimal/3.1/bigdecimal-math.rbs +119 -0
  13. data/.gem_rbs_collection/bigdecimal/3.1/bigdecimal.rbs +1630 -0
  14. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
  15. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
  16. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +26 -0
  17. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
  18. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +65 -0
  19. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
  20. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
  21. data/.gem_rbs_collection/diff-lcs/1.5/.rbs_meta.yaml +9 -0
  22. data/.gem_rbs_collection/diff-lcs/1.5/diff-lcs.rbs +11 -0
  23. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
  24. data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
  25. data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
  26. data/.gem_rbs_collection/mini_mime/0.1/.rbs_meta.yaml +9 -0
  27. data/.gem_rbs_collection/mini_mime/0.1/mini_mime.rbs +14 -0
  28. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
  29. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
  30. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
  31. data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
  32. data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
  33. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
  34. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +822 -0
  35. data/.gem_rbs_collection/sqlite3/2.0/.rbs_meta.yaml +9 -0
  36. data/.gem_rbs_collection/sqlite3/2.0/database.rbs +20 -0
  37. data/.gem_rbs_collection/sqlite3/2.0/pragmas.rbs +5 -0
  38. data/.rspec +4 -0
  39. data/.rubocop.yml +121 -0
  40. data/.simplecov +51 -0
  41. data/CHANGELOG.md +49 -0
  42. data/Gemfile +24 -0
  43. data/Gemfile.lock +329 -0
  44. data/LICENSE.txt +21 -0
  45. data/README.md +225 -0
  46. data/Rakefile +54 -0
  47. data/Steepfile +50 -0
  48. data/bin/sxn +6 -0
  49. data/lib/sxn/CLI.rb +275 -0
  50. data/lib/sxn/commands/init.rb +137 -0
  51. data/lib/sxn/commands/projects.rb +350 -0
  52. data/lib/sxn/commands/rules.rb +435 -0
  53. data/lib/sxn/commands/sessions.rb +300 -0
  54. data/lib/sxn/commands/worktrees.rb +416 -0
  55. data/lib/sxn/commands.rb +13 -0
  56. data/lib/sxn/config/config_cache.rb +295 -0
  57. data/lib/sxn/config/config_discovery.rb +242 -0
  58. data/lib/sxn/config/config_validator.rb +562 -0
  59. data/lib/sxn/config.rb +259 -0
  60. data/lib/sxn/core/config_manager.rb +290 -0
  61. data/lib/sxn/core/project_manager.rb +307 -0
  62. data/lib/sxn/core/rules_manager.rb +306 -0
  63. data/lib/sxn/core/session_manager.rb +336 -0
  64. data/lib/sxn/core/worktree_manager.rb +281 -0
  65. data/lib/sxn/core.rb +13 -0
  66. data/lib/sxn/database/errors.rb +29 -0
  67. data/lib/sxn/database/session_database.rb +691 -0
  68. data/lib/sxn/database.rb +24 -0
  69. data/lib/sxn/errors.rb +76 -0
  70. data/lib/sxn/rules/base_rule.rb +367 -0
  71. data/lib/sxn/rules/copy_files_rule.rb +346 -0
  72. data/lib/sxn/rules/errors.rb +28 -0
  73. data/lib/sxn/rules/project_detector.rb +871 -0
  74. data/lib/sxn/rules/rules_engine.rb +485 -0
  75. data/lib/sxn/rules/setup_commands_rule.rb +307 -0
  76. data/lib/sxn/rules/template_rule.rb +262 -0
  77. data/lib/sxn/rules.rb +148 -0
  78. data/lib/sxn/runtime_validations.rb +96 -0
  79. data/lib/sxn/security/secure_command_executor.rb +364 -0
  80. data/lib/sxn/security/secure_file_copier.rb +478 -0
  81. data/lib/sxn/security/secure_path_validator.rb +258 -0
  82. data/lib/sxn/security.rb +15 -0
  83. data/lib/sxn/templates/common/gitignore.liquid +99 -0
  84. data/lib/sxn/templates/common/session-info.md.liquid +58 -0
  85. data/lib/sxn/templates/errors.rb +36 -0
  86. data/lib/sxn/templates/javascript/README.md.liquid +59 -0
  87. data/lib/sxn/templates/javascript/session-info.md.liquid +206 -0
  88. data/lib/sxn/templates/rails/CLAUDE.md.liquid +78 -0
  89. data/lib/sxn/templates/rails/database.yml.liquid +31 -0
  90. data/lib/sxn/templates/rails/session-info.md.liquid +144 -0
  91. data/lib/sxn/templates/template_engine.rb +346 -0
  92. data/lib/sxn/templates/template_processor.rb +279 -0
  93. data/lib/sxn/templates/template_security.rb +410 -0
  94. data/lib/sxn/templates/template_variables.rb +713 -0
  95. data/lib/sxn/templates.rb +28 -0
  96. data/lib/sxn/ui/output.rb +103 -0
  97. data/lib/sxn/ui/progress_bar.rb +91 -0
  98. data/lib/sxn/ui/prompt.rb +116 -0
  99. data/lib/sxn/ui/table.rb +183 -0
  100. data/lib/sxn/ui.rb +12 -0
  101. data/lib/sxn/version.rb +5 -0
  102. data/lib/sxn.rb +63 -0
  103. data/rbs_collection.lock.yaml +180 -0
  104. data/rbs_collection.yaml +39 -0
  105. data/scripts/test.sh +31 -0
  106. data/sig/external/liquid.rbs +116 -0
  107. data/sig/external/thor.rbs +99 -0
  108. data/sig/external/tty.rbs +71 -0
  109. data/sig/sxn/cli.rbs +46 -0
  110. data/sig/sxn/commands/init.rbs +38 -0
  111. data/sig/sxn/commands/projects.rbs +72 -0
  112. data/sig/sxn/commands/rules.rbs +95 -0
  113. data/sig/sxn/commands/sessions.rbs +62 -0
  114. data/sig/sxn/commands/worktrees.rbs +82 -0
  115. data/sig/sxn/commands.rbs +6 -0
  116. data/sig/sxn/config/config_cache.rbs +67 -0
  117. data/sig/sxn/config/config_discovery.rbs +64 -0
  118. data/sig/sxn/config/config_validator.rbs +64 -0
  119. data/sig/sxn/config.rbs +74 -0
  120. data/sig/sxn/core/config_manager.rbs +67 -0
  121. data/sig/sxn/core/project_manager.rbs +52 -0
  122. data/sig/sxn/core/rules_manager.rbs +54 -0
  123. data/sig/sxn/core/session_manager.rbs +59 -0
  124. data/sig/sxn/core/worktree_manager.rbs +50 -0
  125. data/sig/sxn/core.rbs +87 -0
  126. data/sig/sxn/database/errors.rbs +37 -0
  127. data/sig/sxn/database/session_database.rbs +151 -0
  128. data/sig/sxn/database.rbs +83 -0
  129. data/sig/sxn/errors.rbs +89 -0
  130. data/sig/sxn/rules/base_rule.rbs +137 -0
  131. data/sig/sxn/rules/copy_files_rule.rbs +65 -0
  132. data/sig/sxn/rules/errors.rbs +33 -0
  133. data/sig/sxn/rules/project_detector.rbs +115 -0
  134. data/sig/sxn/rules/rules_engine.rbs +118 -0
  135. data/sig/sxn/rules/setup_commands_rule.rbs +60 -0
  136. data/sig/sxn/rules/template_rule.rbs +44 -0
  137. data/sig/sxn/rules.rbs +287 -0
  138. data/sig/sxn/runtime_validations.rbs +16 -0
  139. data/sig/sxn/security/secure_command_executor.rbs +63 -0
  140. data/sig/sxn/security/secure_file_copier.rbs +79 -0
  141. data/sig/sxn/security/secure_path_validator.rbs +30 -0
  142. data/sig/sxn/security.rbs +128 -0
  143. data/sig/sxn/templates/errors.rbs +43 -0
  144. data/sig/sxn/templates/template_engine.rbs +50 -0
  145. data/sig/sxn/templates/template_processor.rbs +44 -0
  146. data/sig/sxn/templates/template_security.rbs +62 -0
  147. data/sig/sxn/templates/template_variables.rbs +103 -0
  148. data/sig/sxn/templates.rbs +104 -0
  149. data/sig/sxn/ui/output.rbs +50 -0
  150. data/sig/sxn/ui/progress_bar.rbs +39 -0
  151. data/sig/sxn/ui/prompt.rbs +38 -0
  152. data/sig/sxn/ui/table.rbs +43 -0
  153. data/sig/sxn/ui.rbs +63 -0
  154. data/sig/sxn/version.rbs +5 -0
  155. data/sig/sxn.rbs +29 -0
  156. metadata +635 -0
@@ -0,0 +1,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
@@ -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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ui/prompt"
4
+ require_relative "ui/output"
5
+ require_relative "ui/table"
6
+ require_relative "ui/progress_bar"
7
+
8
+ module Sxn
9
+ # UI namespace for all user interface components
10
+ module UI
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sxn
4
+ VERSION = "0.2.0"
5
+ end
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