parabot 1.0.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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +55 -0
  3. data/CLAUDE.md +171 -0
  4. data/DISTRIBUTION.md +287 -0
  5. data/INSTALL.md +242 -0
  6. data/LICENSE +21 -0
  7. data/README.md +371 -0
  8. data/Rakefile +50 -0
  9. data/config/base.yml +12 -0
  10. data/config/commands.yml +28 -0
  11. data/config/core_prompts/system_prompt.yml +66 -0
  12. data/config/core_prompts/test_guidance.yml +24 -0
  13. data/config/languages/elixir.yml +62 -0
  14. data/config/languages/javascript.yml +64 -0
  15. data/config/languages/kotlin.yml +64 -0
  16. data/config/languages/ruby.yml +66 -0
  17. data/config/languages/shell.yml +63 -0
  18. data/config/languages/typescript.yml +63 -0
  19. data/exe/parabot +22 -0
  20. data/lib/parabot/cli/argument_parser.rb +105 -0
  21. data/lib/parabot/cli/command_router.rb +114 -0
  22. data/lib/parabot/cli.rb +71 -0
  23. data/lib/parabot/commands/base.rb +108 -0
  24. data/lib/parabot/commands/custom_commands.rb +63 -0
  25. data/lib/parabot/commands/doctor.rb +196 -0
  26. data/lib/parabot/commands/init.rb +171 -0
  27. data/lib/parabot/commands/message.rb +25 -0
  28. data/lib/parabot/commands/start.rb +35 -0
  29. data/lib/parabot/commands/test.rb +43 -0
  30. data/lib/parabot/commands/version.rb +17 -0
  31. data/lib/parabot/commands.rb +15 -0
  32. data/lib/parabot/configuration.rb +199 -0
  33. data/lib/parabot/dry_run_logging.rb +22 -0
  34. data/lib/parabot/errors.rb +12 -0
  35. data/lib/parabot/language_detector.rb +158 -0
  36. data/lib/parabot/language_inference.rb +82 -0
  37. data/lib/parabot/logging_setup.rb +73 -0
  38. data/lib/parabot/messaging/adapter.rb +53 -0
  39. data/lib/parabot/messaging/adapter_factory.rb +33 -0
  40. data/lib/parabot/messaging/dry_run_adapter.rb +55 -0
  41. data/lib/parabot/messaging/tmux_adapter.rb +82 -0
  42. data/lib/parabot/system.rb +41 -0
  43. data/lib/parabot/test_runner.rb +179 -0
  44. data/lib/parabot/tmux_manager.rb +245 -0
  45. data/lib/parabot/version.rb +5 -0
  46. data/lib/parabot/yaml_text_assembler.rb +155 -0
  47. data/lib/parabot.rb +30 -0
  48. data/parabot.gemspec +44 -0
  49. data/scripts/build-distribution +122 -0
  50. data/scripts/install +152 -0
  51. metadata +221 -0
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "ostruct"
5
+ require "shellwords"
6
+ require_relative "dry_run_logging"
7
+
8
+ module Parabot
9
+ class TmuxManager
10
+ include DryRunLogging
11
+
12
+ def initialize(dry_run: false)
13
+ @dry_run = dry_run
14
+ end
15
+
16
+ def find_claude_session
17
+ # First try to find by name pattern
18
+ sessions = list_sessions
19
+ claude_session = sessions.find { |session| session.match?(/claude/i) }
20
+ return claude_session if claude_session
21
+
22
+ # Then check for Claude processes in any session
23
+ sessions.each do |session|
24
+ panes = list_panes(session)
25
+ claude_pane = panes.find { |pane| pane[:command].match?(/claude/i) }
26
+ return session if claude_pane
27
+ end
28
+
29
+ nil
30
+ rescue SystemCallError
31
+ nil
32
+ end
33
+
34
+ def find_claude_target(session)
35
+ # First check current window for Claude
36
+ current_window = current_window_index(session)
37
+ claude_pane = find_claude_pane_in_window(session, current_window)
38
+ return "#{current_window}:#{claude_pane}" if claude_pane
39
+
40
+ # Check for window named claude
41
+ windows = list_windows(session)
42
+ claude_window = windows.find { |w| w[:name].match?(/claude/i) }
43
+
44
+ if claude_window
45
+ claude_pane = find_claude_pane_in_window(session, claude_window[:index])
46
+ return "#{claude_window[:index]}:#{claude_pane || 0}"
47
+ end
48
+
49
+ # Check all windows for Claude process
50
+ windows.each do |window|
51
+ next if window[:index] == current_window
52
+
53
+ claude_pane = find_claude_pane_in_window(session, window[:index])
54
+ return "#{window[:index]}:#{claude_pane}" if claude_pane
55
+ end
56
+
57
+ # Default to current window, first pane
58
+ "#{current_window}:0"
59
+ rescue SystemCallError
60
+ "0:0"
61
+ end
62
+
63
+ def send_to_claude(message)
64
+ session = find_claude_session
65
+ raise TmuxError, "No tmux session with Claude found" unless session
66
+
67
+ target = find_claude_target(session)
68
+ logger.info("Found Claude in session '#{session}', target '#{target}'")
69
+
70
+ # Send the message
71
+ logger.info("Sending message to Claude...")
72
+ send_keys(session, target, "Escape")
73
+ wait_for_claude(0.1)
74
+ send_keys(session, target, message)
75
+ wait_for_claude(0.2)
76
+ send_keys(session, target, "Enter")
77
+ wait_for_claude(0.1)
78
+ send_keys(session, target, "Enter")
79
+
80
+ logger.info("Message sent successfully")
81
+ true
82
+ rescue SystemCallError => e
83
+ raise TmuxError, "Failed to send message to Claude: #{e.message}"
84
+ end
85
+
86
+ def start_claude_session(system_prompt: nil)
87
+ current_session = current_session_name
88
+ raise TmuxError, "Not in a tmux session. Please run this command from within tmux." unless current_session
89
+
90
+ logger.info("Creating vertical pane (30% width)...")
91
+
92
+ # Create vertical pane on the left (30% of screen)
93
+ result = run_command("tmux split-window -h -l 30% -c '#{Dir.pwd}' -b")
94
+ raise TmuxError, "Failed to create tmux pane" unless result.success?
95
+
96
+ # Get the newly created pane
97
+ new_pane = run_command('tmux display-message -p \'#{pane_index}\'').out.strip
98
+
99
+ # Rename the pane for easier identification
100
+ run_command("tmux select-pane -t #{new_pane} -T claude")
101
+
102
+ # Start claude with system prompt if provided
103
+ claude_command = if system_prompt
104
+ escaped_prompt = system_prompt.shellescape
105
+ "claude --system-prompt #{escaped_prompt}"
106
+ else
107
+ "claude"
108
+ end
109
+
110
+ # Send keys to start claude (let tmux use current context like shell version)
111
+ run_command("tmux send-keys -t #{new_pane} #{claude_command.shellescape} Enter")
112
+
113
+ # Switch back to original pane
114
+ run_command("tmux select-pane -R")
115
+
116
+ logger.info("Claude session started in pane #{new_pane}")
117
+ logger.info("Use 'Ctrl-B + arrow keys' to navigate between panes")
118
+ true
119
+ rescue SystemCallError => e
120
+ raise TmuxError, "Failed to start Claude session: #{e.message}"
121
+ end
122
+
123
+ def list_sessions
124
+ result = run_command("tmux list-sessions")
125
+ return [] unless result.success?
126
+
127
+ result.out.lines.map do |line|
128
+ line.split(":").first.strip
129
+ end
130
+ rescue SystemCallError
131
+ []
132
+ end
133
+
134
+ private
135
+
136
+ def dry_run?
137
+ !!@dry_run
138
+ end
139
+
140
+ def current_session_name
141
+ result = run_command('tmux display-message -p \'#{session_name}\'')
142
+ result.success? ? result.out.strip : nil
143
+ rescue SystemCallError
144
+ nil
145
+ end
146
+
147
+ def current_window_index(session)
148
+ command = "tmux display-message -t " + session + ' -p \'#{window_index}\''
149
+ result = run_command(command)
150
+ result.success? ? result.out.strip : "0"
151
+ rescue SystemCallError
152
+ "0"
153
+ end
154
+
155
+ def list_panes(session)
156
+ command = "tmux list-panes -t " + session + ' -F \'#{pane_index}:#{pane_current_command}\''
157
+ result = run_command(command)
158
+ return [] unless result.success?
159
+
160
+ result.out.lines.map do |line|
161
+ index, command = line.strip.split(":", 2)
162
+ { index: index, command: command }
163
+ end
164
+ rescue SystemCallError
165
+ []
166
+ end
167
+
168
+ def list_windows(session)
169
+ command = "tmux list-windows -t " + session + ' -F \'#{window_index}:#{window_name}\''
170
+ result = run_command(command)
171
+ return [] unless result.success?
172
+
173
+ result.out.lines.map do |line|
174
+ index, name = line.strip.split(":", 2)
175
+ { index: index, name: name }
176
+ end
177
+ rescue SystemCallError
178
+ []
179
+ end
180
+
181
+ def find_claude_pane_in_window(session, window)
182
+ command = "tmux list-panes -t " + session + ":" + window + ' -F \'#{pane_index}:#{pane_current_command}\''
183
+ panes = run_command(command)
184
+ return nil unless panes.success?
185
+
186
+ claude_pane = panes.out.lines.find { |line| line.match?(/claude/i) }
187
+ claude_pane&.split(":")&.first
188
+ rescue SystemCallError
189
+ nil
190
+ end
191
+
192
+ def send_keys(session, target, keys)
193
+ # Convert window:pane format to window.pane format for tmux
194
+ target_formatted = target.include?(":") ? target.tr(":", ".") : target
195
+ run_command("tmux send-keys -t #{session}:#{target_formatted} #{keys.shellescape}")
196
+ end
197
+
198
+ def wait_for_claude(seconds)
199
+ sleep(seconds) unless dry_run?
200
+ end
201
+
202
+ def run_command(command)
203
+ if dry_run?
204
+ dry_run_log(command)
205
+ # Return realistic mock output based on command type
206
+ mock_output = case command
207
+ when /list-sessions/
208
+ "main: 1 windows (created Mon Jan 1 12:00:00 2024)\nclaude: 1 windows (created Mon Jan 1 12:00:00 2024)"
209
+ when /display-message.*session_name/
210
+ "main"
211
+ when /display-message.*pane_index/
212
+ "1"
213
+ when /display-message.*window_index/
214
+ "0"
215
+ when /list-panes.*pane_index:pane_current_command/
216
+ "0:bash\n1:claude"
217
+ when /list-windows/
218
+ "0: main* (1 panes) [80x24] [layout active]\n1: claude- (1 panes) [80x24]"
219
+ else
220
+ ""
221
+ end
222
+
223
+ OpenStruct.new(
224
+ out: mock_output,
225
+ err: "",
226
+ success?: true,
227
+ exit_status: 0
228
+ )
229
+ else
230
+ stdout, stderr, status = Open3.capture3(command)
231
+ OpenStruct.new(
232
+ out: stdout,
233
+ err: stderr,
234
+ success?: status.success?,
235
+ exit_status: status.exitstatus
236
+ )
237
+ end
238
+ end
239
+
240
+ def logger
241
+ require "semantic_logger"
242
+ SemanticLogger[self.class.name]
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parabot
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "logger"
5
+ require_relative "system"
6
+
7
+ module Parabot
8
+ class YamlTextAssembler
9
+ def self.assemble_for_languages(detected_languages)
10
+ supported, unsupported = partition_language_support(detected_languages)
11
+ warn_about_unsupported_languages(unsupported) if unsupported.any?
12
+
13
+ # Convert symbols to strings for consistent processing
14
+ detected_strings = detected_languages.map(&:to_s)
15
+
16
+ # Build YAML as text directly to preserve formatting
17
+ build_yaml_text(detected_strings, supported, unsupported)
18
+ end
19
+
20
+ def self.supported_languages
21
+ @supported_languages ||= Dir.glob(System.determine_config_path("languages/*.yml"))
22
+ .map { |file| File.basename(file, ".yml") }
23
+ .sort
24
+ end
25
+
26
+ def self.partition_language_support(languages)
27
+ string_languages = languages.map(&:to_s)
28
+ supported = string_languages.select { |lang| supported_languages.include?(lang) }
29
+ unsupported = string_languages - supported
30
+ [supported, unsupported]
31
+ end
32
+
33
+ def self.warn_about_unsupported_languages(unsupported)
34
+ unsupported.each do |lang|
35
+ logger.warn("#{lang.capitalize} has minimal support - using basic TDD guidance")
36
+ end
37
+ end
38
+
39
+ def self.logger
40
+ @logger ||= Logger.new($stderr).tap { |l| l.level = Logger::WARN }
41
+ end
42
+
43
+ def self.read_config_file(path)
44
+ File.read(System.determine_config_path(path))
45
+ end
46
+
47
+ def self.indent_lines(content, spaces = 2)
48
+ prefix = " " * spaces
49
+ content.lines.map { |line| line.empty? ? line : "#{prefix}#{line}" }.join
50
+ end
51
+
52
+ def self.build_yaml_block(key, content, style = "|")
53
+ "#{key}: #{style}\n" + indent_lines(content).chomp
54
+ end
55
+
56
+ def self.build_yaml_text(_detected_languages, supported, unsupported)
57
+ sections = []
58
+
59
+ # Add YAML document start
60
+ sections << "---"
61
+
62
+ # Add base configuration
63
+ sections << read_config_file("base.yml")
64
+ sections << read_config_file("commands.yml")
65
+ sections << read_config_file("core_prompts/test_guidance.yml")
66
+ sections << read_config_file("core_prompts/system_prompt.yml")
67
+
68
+ # Add languages section
69
+ sections << build_languages_section(supported, unsupported)
70
+
71
+ sections.join("\n\n")
72
+ end
73
+
74
+ def self.build_languages_section(supported, unsupported)
75
+ result = "languages:"
76
+
77
+ supported.each { |lang| result += build_supported_language(lang) }
78
+ unsupported.each { |lang| result += build_unsupported_language(lang) }
79
+
80
+ result
81
+ end
82
+
83
+ def self.build_supported_language(lang)
84
+ lang_file = System.determine_config_path("languages/#{lang}.yml")
85
+ return "" unless File.exist?(lang_file)
86
+
87
+ lang_content = File.read(lang_file).strip
88
+ "\n #{lang}:\n" + indent_lines(lang_content, 4).rstrip
89
+ end
90
+
91
+ def self.build_unsupported_language(lang)
92
+ result = "\n #{lang}:"
93
+ result += build_language_config_arrays(lang)
94
+ result += build_minimal_prompt_block(lang)
95
+ result
96
+ end
97
+
98
+ def self.build_language_config_arrays(lang)
99
+ config = ""
100
+ config += "\n file_extensions:"
101
+ LanguageInference.infer_extensions(lang).each { |ext| config += "\n - \"#{ext}\"" }
102
+ config += "\n test_dir:"
103
+ LanguageInference.infer_test_dirs(lang).each { |dir| config += "\n - #{dir}" }
104
+ config += "\n project_files:"
105
+ LanguageInference.infer_project_files(lang).each { |file| config += "\n - #{file}" }
106
+ config
107
+ end
108
+
109
+ def self.build_minimal_prompt_block(lang)
110
+ minimal_prompt = build_minimal_system_prompt(lang)
111
+ "\n system_prompt: |-\n" + indent_lines(minimal_prompt, 6).rstrip
112
+ end
113
+
114
+ def self.build_minimal_system_prompt(lang)
115
+ <<~PROMPT
116
+ #{lang.upcase} TDD PATTERNS (MINIMAL SUPPORT):
117
+ - Follow test-first development: write failing test, make it pass, refactor
118
+ - Use descriptive test names that explain the behavior being tested
119
+ - Keep tests isolated and independent of each other
120
+ - Test both success and failure cases
121
+ - Mock external dependencies to ensure test reliability
122
+
123
+ #{lang.upcase} BASIC ANTI-PATTERNS TO AVOID:
124
+ - NEVER replace specific assertions with generic "does not raise/throw" checks
125
+ - NEVER convert detailed expectations to simple execution validation
126
+ - NEVER gut tests by removing behavior validation to make them pass
127
+ - ALWAYS fix root causes (configuration, mocking, environment) not symptoms
128
+ - ALWAYS understand what your test assertions are validating before changing them
129
+
130
+ DEBUGGING STEPS FOR #{lang.upcase}:
131
+ - Check project dependencies and build configuration
132
+ - Verify test framework setup and imports
133
+ - Use language-specific debugging tools and logging
134
+ - Ensure proper test isolation and cleanup
135
+ - Check for missing mocks or test data setup
136
+
137
+ ⚠️ MINIMAL LANGUAGE SUPPORT:
138
+ This is basic TDD guidance for #{lang}. For comprehensive patterns, debugging tips,
139
+ and language-specific best practices, consider contributing full #{lang} support:
140
+ https://github.com/AlexParamonov/parabot#contributing-language-support
141
+
142
+ Current minimal support includes:
143
+ - Generic TDD workflow guidance
144
+ - Basic anti-pattern prevention
145
+ - Universal debugging approaches
146
+
147
+ Missing advanced support:
148
+ - #{lang}-specific testing frameworks and patterns
149
+ - Language-specific debugging techniques
150
+ - Framework-specific best practices
151
+ - Advanced #{lang} TDD patterns
152
+ PROMPT
153
+ end
154
+ end
155
+ end
data/lib/parabot.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parabot/version"
4
+ require_relative "parabot/errors"
5
+ require_relative "parabot/configuration"
6
+ require_relative "parabot/language_detector"
7
+ require_relative "parabot/tmux_manager"
8
+ require_relative "parabot/test_runner"
9
+ require_relative "parabot/cli"
10
+
11
+ module Parabot
12
+ class Error < StandardError; end
13
+
14
+ # Main entry point for the CLI
15
+ def self.start(args = ARGV)
16
+ CLI.start(args)
17
+ rescue Parabot::Error => e
18
+ logger.error(e.message)
19
+ exit(1)
20
+ rescue StandardError => e
21
+ logger.error("Unexpected error: #{e.message}")
22
+ logger.debug(e.backtrace.join("\n"))
23
+ exit(1)
24
+ end
25
+
26
+ def self.logger
27
+ require "semantic_logger"
28
+ SemanticLogger["Parabot"]
29
+ end
30
+ end
data/parabot.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/parabot/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "parabot"
7
+ spec.version = Parabot::VERSION
8
+ spec.authors = ["Alexander Paramonov"]
9
+ spec.email = ["alexander.n.paramonov@gmail.com"]
10
+
11
+ spec.summary = "AI-powered Test-Driven Development parallel assistant with tmux integration"
12
+ spec.description = "Parabot is a multi-language TDD assistant that provides intelligent test-driven development support across various programming languages and testing frameworks. Features tmux integration, YAML configuration, and automatic project type detection."
13
+ spec.homepage = "https://github.com/AlexParamonov/parabot"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) ||
25
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables << "parabot"
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Runtime dependencies
33
+ spec.add_dependency "dry-cli", "~> 1.1"
34
+ spec.add_dependency "config", "~> 5.0"
35
+ spec.add_dependency "semantic_logger", "~> 4.15"
36
+
37
+ # Development dependencies
38
+ spec.add_development_dependency "rake", "~> 13.0"
39
+ spec.add_development_dependency "rspec", "~> 3.12"
40
+ spec.add_development_dependency "rubocop", "~> 1.21"
41
+ spec.add_development_dependency "rubocop-rspec", "~> 2.0"
42
+ spec.add_development_dependency "pry", "~> 0.14"
43
+ spec.add_development_dependency "simplecov", "~> 0.22"
44
+ end
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ #
5
+ # Parabot CLI Distribution Builder
6
+ #
7
+ # Creates a standalone executable for distribution to Linux/Mac systems
8
+ #
9
+
10
+ # Configuration
11
+ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ readonly PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
13
+ readonly BUILD_DIR="${BUILD_DIR:-$PROJECT_ROOT/build}"
14
+ readonly EXECUTABLE_NAME="parabot"
15
+ readonly SOURCE_EXECUTABLE="$PROJECT_ROOT/exe/parabot"
16
+
17
+ # Logging functions
18
+ log() { echo "🏗️ $1"; }
19
+ step() { echo "📦 $1"; }
20
+ success() { echo "✅ $1"; }
21
+ error() { echo "❌ $1" >&2; }
22
+
23
+ # Validation
24
+ validate_prerequisites() {
25
+ if [[ ! -f "$SOURCE_EXECUTABLE" ]]; then
26
+ error "Source executable not found: $SOURCE_EXECUTABLE"
27
+ exit 1
28
+ fi
29
+
30
+ if [[ ! -x "$SOURCE_EXECUTABLE" ]]; then
31
+ error "Source executable is not executable: $SOURCE_EXECUTABLE"
32
+ exit 1
33
+ fi
34
+ }
35
+
36
+ # Build process
37
+ build_distribution() {
38
+ log "Building Parabot CLI Distribution"
39
+ echo "=================================="
40
+
41
+ validate_prerequisites
42
+
43
+ # Clean build directory
44
+ step "Cleaning build directory..."
45
+ rm -rf "$BUILD_DIR"
46
+ mkdir -p "$BUILD_DIR"
47
+
48
+ # Copy the bundler-isolated executable
49
+ step "Creating standalone executable..."
50
+ cp "$SOURCE_EXECUTABLE" "$BUILD_DIR/$EXECUTABLE_NAME"
51
+ chmod +x "$BUILD_DIR/$EXECUTABLE_NAME"
52
+ }
53
+
54
+ # Test and report
55
+ test_executable() {
56
+ local executable="$BUILD_DIR/$EXECUTABLE_NAME"
57
+
58
+ step "Testing the executable..."
59
+
60
+ # Test version command
61
+ if "$executable" --version >/dev/null 2>&1; then
62
+ success "Version command works"
63
+ else
64
+ error "Version command failed"
65
+ return 1
66
+ fi
67
+
68
+ # Test help command (check output rather than exit code)
69
+ local help_output
70
+ help_output=$("$executable" --help 2>&1 || true)
71
+ if echo "$help_output" | grep -q "commands"; then
72
+ success "Help command works"
73
+ else
74
+ error "Help command failed"
75
+ return 1
76
+ fi
77
+ }
78
+
79
+ show_distribution_info() {
80
+ local file_size
81
+ file_size=$(du -h "$BUILD_DIR/$EXECUTABLE_NAME" | cut -f1)
82
+
83
+ success "Build complete!"
84
+ echo ""
85
+ echo "📊 Distribution Info:"
86
+ echo " Executable: $BUILD_DIR/$EXECUTABLE_NAME"
87
+ echo " Size: $file_size"
88
+ echo " Ruby Version Required: >= 3.0.0"
89
+ echo ""
90
+ echo "🚀 Installation Instructions:"
91
+ echo ""
92
+ echo " Local installation:"
93
+ echo " sudo cp $BUILD_DIR/$EXECUTABLE_NAME /usr/local/bin/"
94
+ echo ""
95
+ echo " User installation (if ~/bin is in PATH):"
96
+ echo " cp $BUILD_DIR/$EXECUTABLE_NAME ~/bin/"
97
+ echo ""
98
+ echo " Direct usage:"
99
+ echo " $BUILD_DIR/$EXECUTABLE_NAME --help"
100
+ echo ""
101
+ }
102
+
103
+ show_checklist() {
104
+ echo "🎉 Distribution ready for deployment!"
105
+ echo ""
106
+ echo "📋 Distribution checklist:"
107
+ echo " □ Test on target Linux system"
108
+ echo " □ Test on target macOS system"
109
+ echo " □ Verify Ruby 3.0+ requirement"
110
+ echo " □ Test all major commands"
111
+ echo " □ Document installation process"
112
+ }
113
+
114
+ # Main execution
115
+ main() {
116
+ build_distribution
117
+ test_executable || exit 1
118
+ show_distribution_info
119
+ show_checklist
120
+ }
121
+
122
+ main "$@"