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.
- checksums.yaml +7 -0
- data/.rubocop.yml +55 -0
- data/CLAUDE.md +171 -0
- data/DISTRIBUTION.md +287 -0
- data/INSTALL.md +242 -0
- data/LICENSE +21 -0
- data/README.md +371 -0
- data/Rakefile +50 -0
- data/config/base.yml +12 -0
- data/config/commands.yml +28 -0
- data/config/core_prompts/system_prompt.yml +66 -0
- data/config/core_prompts/test_guidance.yml +24 -0
- data/config/languages/elixir.yml +62 -0
- data/config/languages/javascript.yml +64 -0
- data/config/languages/kotlin.yml +64 -0
- data/config/languages/ruby.yml +66 -0
- data/config/languages/shell.yml +63 -0
- data/config/languages/typescript.yml +63 -0
- data/exe/parabot +22 -0
- data/lib/parabot/cli/argument_parser.rb +105 -0
- data/lib/parabot/cli/command_router.rb +114 -0
- data/lib/parabot/cli.rb +71 -0
- data/lib/parabot/commands/base.rb +108 -0
- data/lib/parabot/commands/custom_commands.rb +63 -0
- data/lib/parabot/commands/doctor.rb +196 -0
- data/lib/parabot/commands/init.rb +171 -0
- data/lib/parabot/commands/message.rb +25 -0
- data/lib/parabot/commands/start.rb +35 -0
- data/lib/parabot/commands/test.rb +43 -0
- data/lib/parabot/commands/version.rb +17 -0
- data/lib/parabot/commands.rb +15 -0
- data/lib/parabot/configuration.rb +199 -0
- data/lib/parabot/dry_run_logging.rb +22 -0
- data/lib/parabot/errors.rb +12 -0
- data/lib/parabot/language_detector.rb +158 -0
- data/lib/parabot/language_inference.rb +82 -0
- data/lib/parabot/logging_setup.rb +73 -0
- data/lib/parabot/messaging/adapter.rb +53 -0
- data/lib/parabot/messaging/adapter_factory.rb +33 -0
- data/lib/parabot/messaging/dry_run_adapter.rb +55 -0
- data/lib/parabot/messaging/tmux_adapter.rb +82 -0
- data/lib/parabot/system.rb +41 -0
- data/lib/parabot/test_runner.rb +179 -0
- data/lib/parabot/tmux_manager.rb +245 -0
- data/lib/parabot/version.rb +5 -0
- data/lib/parabot/yaml_text_assembler.rb +155 -0
- data/lib/parabot.rb +30 -0
- data/parabot.gemspec +44 -0
- data/scripts/build-distribution +122 -0
- data/scripts/install +152 -0
- metadata +221 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../configuration"
|
4
|
+
|
5
|
+
module Parabot
|
6
|
+
module CLI
|
7
|
+
# Routes commands to appropriate handlers based on command type
|
8
|
+
class CommandRouter
|
9
|
+
def initialize(registry)
|
10
|
+
@registry = registry
|
11
|
+
end
|
12
|
+
|
13
|
+
def find_builtin_command(args)
|
14
|
+
# Get all registered command names from the registry
|
15
|
+
result = @registry.get([])
|
16
|
+
node = result.instance_variable_get(:@node)
|
17
|
+
children = node.instance_variable_get(:@children).keys
|
18
|
+
aliases = node.instance_variable_get(:@aliases).keys
|
19
|
+
builtin_commands = children + aliases
|
20
|
+
|
21
|
+
args.find { |arg| builtin_commands.include?(arg) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_custom_command(args)
|
25
|
+
custom_commands = Configuration.current.custom_commands
|
26
|
+
args.find { |arg| custom_commands.key?(arg) || custom_commands.key?(arg.to_sym) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def route_command(args, parser)
|
30
|
+
# First, extract options to get project_root before any configuration access
|
31
|
+
options = parser.extract_options(args)
|
32
|
+
|
33
|
+
# Now identify if we have known builtin commands anywhere in args
|
34
|
+
builtin_command = find_builtin_command(args)
|
35
|
+
|
36
|
+
# Load configuration with proper project root early (skip for init command and help)
|
37
|
+
unless builtin_command == "init" || parser.help_requested?(args)
|
38
|
+
CLI.ensure_configuration_loaded(options[:config], options[:project_root])
|
39
|
+
end
|
40
|
+
|
41
|
+
if builtin_command
|
42
|
+
handle_builtin_command(args, builtin_command, parser)
|
43
|
+
elsif args.empty? || parser.only_options?(args)
|
44
|
+
handle_help([], parser)
|
45
|
+
elsif parser.help_requested?(args)
|
46
|
+
handle_help(args, parser)
|
47
|
+
else
|
48
|
+
# Check for custom commands or treat as message
|
49
|
+
custom_command = find_custom_command(args)
|
50
|
+
if custom_command
|
51
|
+
handle_custom_command(args, custom_command, parser)
|
52
|
+
else
|
53
|
+
handle_message(args, parser)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def handle_builtin_command(args, command, parser)
|
61
|
+
# Extract global options first
|
62
|
+
options = parser.extract_options(args)
|
63
|
+
|
64
|
+
# Remove global options, their values, and the command itself
|
65
|
+
remaining_args = parser.remove_options_and_values(args, command)
|
66
|
+
reordered_args = [command] + remaining_args
|
67
|
+
|
68
|
+
{
|
69
|
+
type: :builtin_command,
|
70
|
+
args: reordered_args,
|
71
|
+
options: options
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_custom_command(args, command, parser)
|
76
|
+
# Extract options and build message
|
77
|
+
options = parser.extract_options(args)
|
78
|
+
remaining_args = parser.remove_options_and_values(args, command)
|
79
|
+
|
80
|
+
custom_commands = Configuration.current.custom_commands
|
81
|
+
prompt = custom_commands[command] || custom_commands[command.to_sym]
|
82
|
+
message = [prompt, remaining_args.join(" ")].reject(&:empty?).join(" ")
|
83
|
+
|
84
|
+
{
|
85
|
+
type: :custom_command,
|
86
|
+
message: message,
|
87
|
+
options: options
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def handle_message(args, parser)
|
92
|
+
# Treat entire input as message
|
93
|
+
options = parser.extract_options(args)
|
94
|
+
message_parts = parser.remove_options_and_values(args, nil) # No command to remove
|
95
|
+
message = message_parts.join(" ")
|
96
|
+
|
97
|
+
{
|
98
|
+
type: :message,
|
99
|
+
message: message,
|
100
|
+
options: options
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def handle_help(args, parser)
|
105
|
+
options = parser.extract_options(args)
|
106
|
+
{
|
107
|
+
type: :help,
|
108
|
+
args: args.empty? ? ["help"] : args,
|
109
|
+
options: options
|
110
|
+
}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/parabot/cli.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/cli"
|
4
|
+
require_relative "commands"
|
5
|
+
require_relative "configuration"
|
6
|
+
require_relative "logging_setup"
|
7
|
+
require_relative "cli/argument_parser"
|
8
|
+
require_relative "cli/command_router"
|
9
|
+
|
10
|
+
module Parabot
|
11
|
+
module CLI
|
12
|
+
module Commands
|
13
|
+
extend Dry::CLI::Registry
|
14
|
+
|
15
|
+
register "commands", Parabot::Commands::CustomCommands
|
16
|
+
register "doctor", Parabot::Commands::Doctor
|
17
|
+
register "init", Parabot::Commands::Init
|
18
|
+
register "message", Parabot::Commands::Message, aliases: ["m"]
|
19
|
+
register "start", Parabot::Commands::Start, aliases: ["s"]
|
20
|
+
register "test", Parabot::Commands::Test, aliases: ["t"]
|
21
|
+
register "version", Parabot::Commands::Version, aliases: ["-v", "--version"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.start(args)
|
25
|
+
parser = ArgumentParser.new
|
26
|
+
router = CommandRouter.new(Commands)
|
27
|
+
result = router.route_command(args, parser) # Configuration is already loaded in route_command
|
28
|
+
|
29
|
+
args_with_options =
|
30
|
+
case result[:type]
|
31
|
+
when :builtin_command
|
32
|
+
build_args_with_options(result[:args], result[:options])
|
33
|
+
when :custom_command, :message
|
34
|
+
build_args_with_options(["message", result[:message]], result[:options])
|
35
|
+
when :help
|
36
|
+
result[:args]
|
37
|
+
end
|
38
|
+
|
39
|
+
execute_builtin_command(args_with_options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.ensure_configuration_loaded(config_file = nil, project_root = nil)
|
43
|
+
return if Configuration.instance_variable_get(:@instance)
|
44
|
+
|
45
|
+
Configuration.load(config_file, project_root)
|
46
|
+
LoggingSetup.setup_with_config(Configuration.current)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.build_args_with_options(args, options)
|
50
|
+
result = args.dup
|
51
|
+
|
52
|
+
# Add global options back to args for dry-cli
|
53
|
+
result << "--dry-run" if options[:dry_run]
|
54
|
+
result << "--force" if options[:force]
|
55
|
+
result += ["--config", options[:config]] if options[:config]
|
56
|
+
result += ["--language", options[:language]] if options[:language]
|
57
|
+
result += ["--timeout", options[:timeout].to_s] if options[:timeout]
|
58
|
+
result += ["--project-root", options[:project_root]] if options[:project_root]
|
59
|
+
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
# Simplified CLI that delegates to separate components
|
64
|
+
# This maintains backward compatibility while improving architecture
|
65
|
+
|
66
|
+
def self.execute_builtin_command(args)
|
67
|
+
cli = Dry::CLI.new(Commands)
|
68
|
+
cli.call(arguments: args)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/cli"
|
4
|
+
require_relative "../messaging/adapter_factory"
|
5
|
+
require_relative "../dry_run_logging"
|
6
|
+
|
7
|
+
module Parabot
|
8
|
+
module Commands
|
9
|
+
class Base < Dry::CLI::Command
|
10
|
+
include DryRunLogging
|
11
|
+
|
12
|
+
# Global options available to all commands
|
13
|
+
option :config, aliases: ["c"], type: :string, desc: "Path to configuration file"
|
14
|
+
option :language, aliases: ["l"], type: :string, desc: "Force language detection (elixir, ruby, etc.)"
|
15
|
+
option :dry_run, type: :boolean, default: false, desc: "Show what would be done without executing"
|
16
|
+
option :timeout, aliases: ["t"], type: :integer, default: 30, desc: "Timeout in seconds"
|
17
|
+
option :project_root, aliases: ["p"], type: :string, default: ".",
|
18
|
+
desc: "Project root directory for configuration and file detection"
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def config
|
23
|
+
@config ||= Configuration.current
|
24
|
+
end
|
25
|
+
|
26
|
+
def logger
|
27
|
+
require "semantic_logger"
|
28
|
+
SemanticLogger[self.class.name]
|
29
|
+
end
|
30
|
+
|
31
|
+
def handle_dry_run(message)
|
32
|
+
return false unless dry_run?
|
33
|
+
|
34
|
+
dry_run_log(message)
|
35
|
+
show_dry_run_details if respond_to?(:show_dry_run_details, true)
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def dry_run?
|
40
|
+
options[:dry_run]
|
41
|
+
end
|
42
|
+
|
43
|
+
def init(**options)
|
44
|
+
@options = options
|
45
|
+
@options[:project_root] ||= "." # Set default when not provided
|
46
|
+
@options[:language] = parse_language_option(@options[:language])
|
47
|
+
|
48
|
+
validate_git_repository
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate_git_repository
|
52
|
+
return if git_repository?
|
53
|
+
|
54
|
+
warning_message = "Warning: Not running in a git repository. Some features may be limited."
|
55
|
+
dry_run_log(warning_message)
|
56
|
+
logger.warn(warning_message)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def options
|
62
|
+
@options ||= {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def git_repository?
|
66
|
+
project_root = @options[:project_root] || "."
|
67
|
+
system("git -C #{project_root.shellescape} rev-parse --is-inside-work-tree > /dev/null 2>&1")
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_language_option(language_option)
|
71
|
+
language_option.to_s.split(",").map(&:strip).map(&:to_sym)
|
72
|
+
end
|
73
|
+
|
74
|
+
def language_detector
|
75
|
+
@language_detector ||= LanguageDetector.new(config)
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_runner
|
79
|
+
@test_runner ||= TestRunner.new(messaging_adapter, language_detector, dry_run: dry_run?, config: config,
|
80
|
+
language: options[:language])
|
81
|
+
end
|
82
|
+
|
83
|
+
def messaging_adapter
|
84
|
+
@messaging_adapter ||= Messaging::AdapterFactory.create(config.messaging_adapter, dry_run: dry_run?)
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_unexpected_error(error)
|
88
|
+
logger.error("Unexpected error: #{error.message}")
|
89
|
+
logger.debug(error.backtrace.join("\n"))
|
90
|
+
end
|
91
|
+
|
92
|
+
def format_message(message)
|
93
|
+
<<~MESSAGE
|
94
|
+
Mandatory: Refer to @#{Parabot::System.determine_system_path('.parabot.md', @options[:project_root])} for complete instructions and test analysis. Read it unless you have it in your context already.
|
95
|
+
#{message}
|
96
|
+
MESSAGE
|
97
|
+
end
|
98
|
+
|
99
|
+
def send_message(message)
|
100
|
+
message = format_message(message)
|
101
|
+
logger.info("Sending message to Claude...")
|
102
|
+
dry_run_log "Would send message: #{message}"
|
103
|
+
|
104
|
+
messaging_adapter.send_message(message)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parabot
|
4
|
+
module Commands
|
5
|
+
class CustomCommands < Base
|
6
|
+
desc "List available custom commands"
|
7
|
+
|
8
|
+
def call(**)
|
9
|
+
return if handle_dry_run("Would list custom commands")
|
10
|
+
|
11
|
+
display_custom_commands
|
12
|
+
rescue ConfigurationError => e
|
13
|
+
handle_configuration_error(e)
|
14
|
+
rescue StandardError => e
|
15
|
+
handle_unexpected_error(e)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def display_custom_commands
|
21
|
+
logger.info("Loading and validating custom commands...")
|
22
|
+
custom_commands = config.custom_commands
|
23
|
+
|
24
|
+
if custom_commands.any?
|
25
|
+
show_commands(custom_commands)
|
26
|
+
else
|
27
|
+
show_no_commands_message
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def show_commands(custom_commands)
|
32
|
+
puts "Available custom commands:"
|
33
|
+
custom_commands.each do |command, description|
|
34
|
+
puts " #{command.to_s.ljust(15)} - #{truncated_description(description)}"
|
35
|
+
end
|
36
|
+
puts
|
37
|
+
logger.info("Found #{custom_commands.size} custom commands")
|
38
|
+
end
|
39
|
+
|
40
|
+
def truncated_description(description)
|
41
|
+
description.length > 60 ? "#{description[0..60]}..." : description
|
42
|
+
end
|
43
|
+
|
44
|
+
def show_no_commands_message
|
45
|
+
logger.error("No custom commands found in configuration")
|
46
|
+
puts "No custom commands configured."
|
47
|
+
puts "Add commands to your .parabot.yml file under the 'commands:' section."
|
48
|
+
exit(1)
|
49
|
+
end
|
50
|
+
|
51
|
+
def handle_configuration_error(error)
|
52
|
+
logger.error("Configuration error: #{error.message}")
|
53
|
+
exit(1)
|
54
|
+
end
|
55
|
+
|
56
|
+
def handle_unexpected_error(error)
|
57
|
+
logger.error("Unexpected error: #{error.message}")
|
58
|
+
logger.debug(error.backtrace.join("\n")) if options[:verbose]
|
59
|
+
exit(1)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/cli"
|
4
|
+
require_relative "../configuration"
|
5
|
+
require_relative "../cli"
|
6
|
+
|
7
|
+
module Parabot
|
8
|
+
module Commands
|
9
|
+
class Doctor < Dry::CLI::Command
|
10
|
+
desc "Diagnose configuration and environment health"
|
11
|
+
|
12
|
+
argument :file, required: false, desc: "Specific config file to validate"
|
13
|
+
option :plain, type: :boolean, default: false, desc: "Plain text output for logging or files"
|
14
|
+
option :project_root, aliases: ["p"], type: :string, desc: "Project root directory for configuration"
|
15
|
+
|
16
|
+
example [
|
17
|
+
"parabot doctor # Check default configuration and environment",
|
18
|
+
"parabot doctor custom.yml # Check specific config file",
|
19
|
+
"parabot doctor --plain # Plain text output for logging or files"
|
20
|
+
]
|
21
|
+
|
22
|
+
def call(file: nil, plain: false, project_root: nil, **)
|
23
|
+
@plain_output = plain
|
24
|
+
log("Running Parabot health check...", :health_check)
|
25
|
+
|
26
|
+
begin
|
27
|
+
# Load configuration
|
28
|
+
config = Configuration.load(file, project_root)
|
29
|
+
|
30
|
+
log("Configuration loaded successfully", :ok)
|
31
|
+
validate_config_schema(config)
|
32
|
+
|
33
|
+
log("Health check completed!", :success)
|
34
|
+
rescue ConfigurationError => e
|
35
|
+
log("Configuration error: #{e.message}", :error)
|
36
|
+
exit 1
|
37
|
+
rescue StandardError => e
|
38
|
+
log("Unexpected error: #{e.message}", :fatal)
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def validate_config_schema(config)
|
46
|
+
header("Schema Validation:")
|
47
|
+
|
48
|
+
# Check recommended sections
|
49
|
+
validate_section_presence(config, "languages")
|
50
|
+
validate_section_presence(config, "commands")
|
51
|
+
|
52
|
+
# Validate specific sections
|
53
|
+
validate_test_commands_schema(config)
|
54
|
+
validate_custom_commands_schema(config)
|
55
|
+
validate_languages_schema(config)
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate_section_presence(config, section_name)
|
59
|
+
if config.config.respond_to?(section_name) && config.config.public_send(section_name)
|
60
|
+
log("#{section_name} section found", :ok)
|
61
|
+
else
|
62
|
+
log("Missing recommended section: #{section_name}", :warn)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate_test_commands_schema(config)
|
67
|
+
return unless config.config.languages
|
68
|
+
|
69
|
+
header("Test Commands:")
|
70
|
+
test_commands = {}
|
71
|
+
|
72
|
+
# Extract test commands from language configurations
|
73
|
+
config.config.languages.to_h.each do |lang, lang_config|
|
74
|
+
if lang_config&.[]("test_command") || lang_config&.[](:test_command)
|
75
|
+
test_command = lang_config["test_command"] || lang_config[:test_command]
|
76
|
+
test_commands[lang] = test_command
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
if test_commands.empty?
|
81
|
+
log("No test commands configured in any language", :warn)
|
82
|
+
return
|
83
|
+
end
|
84
|
+
|
85
|
+
test_commands.each do |lang, command|
|
86
|
+
next unless command.is_a?(String) && !command.empty?
|
87
|
+
|
88
|
+
cmd_name = command.split.first
|
89
|
+
if system("which #{cmd_name} > /dev/null 2>&1")
|
90
|
+
log("#{lang}: #{command}", :ok)
|
91
|
+
else
|
92
|
+
log("#{lang}: #{command} (command '#{cmd_name}' not found in PATH)", :error)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def validate_custom_commands_schema(config)
|
98
|
+
return unless config.config.commands
|
99
|
+
|
100
|
+
header("Custom Commands:")
|
101
|
+
commands = config.config.commands.to_h
|
102
|
+
|
103
|
+
if commands.empty?
|
104
|
+
log("No custom commands configured", :info)
|
105
|
+
return
|
106
|
+
end
|
107
|
+
|
108
|
+
commands.each do |cmd, value|
|
109
|
+
if value.is_a?(String) && !value.empty?
|
110
|
+
log("#{cmd}: #{value.length > 50 ? "#{value[0..47]}..." : value}", :ok)
|
111
|
+
else
|
112
|
+
log("#{cmd}: invalid command definition", :error)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def validate_languages_schema(config)
|
118
|
+
return unless config.config.languages
|
119
|
+
|
120
|
+
header("Languages:")
|
121
|
+
languages = config.config.languages
|
122
|
+
|
123
|
+
if languages.to_h.empty?
|
124
|
+
log("No language configurations found", :info)
|
125
|
+
return
|
126
|
+
end
|
127
|
+
|
128
|
+
languages.each do |lang, lang_config|
|
129
|
+
next unless lang_config
|
130
|
+
|
131
|
+
status_items = []
|
132
|
+
status_items << "system_prompt" if lang_config["system_prompt"] || lang_config[:system_prompt]
|
133
|
+
status_items << "test_guidance" if lang_config["test_guidance"] || lang_config[:test_guidance]
|
134
|
+
|
135
|
+
file_extensions = lang_config["file_extensions"] || lang_config[:file_extensions]
|
136
|
+
if file_extensions&.any?
|
137
|
+
extensions = Array(file_extensions).join(", ")
|
138
|
+
status_items << "extensions: #{extensions}"
|
139
|
+
end
|
140
|
+
|
141
|
+
if status_items.any?
|
142
|
+
log("#{lang}: #{status_items.join(', ')}", :ok)
|
143
|
+
else
|
144
|
+
log("#{lang}: incomplete configuration", :warn)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Doctor-specific output methods
|
150
|
+
def log(message, icon = nil)
|
151
|
+
formatted = format_with_icon(message, icon)
|
152
|
+
Kernel.puts formatted
|
153
|
+
end
|
154
|
+
|
155
|
+
def header(message)
|
156
|
+
Kernel.puts "" unless plain_output? # Add blank line before header
|
157
|
+
log(message, :info)
|
158
|
+
end
|
159
|
+
|
160
|
+
def format_with_icon(message, icon)
|
161
|
+
return message unless icon
|
162
|
+
|
163
|
+
# Use plain output if explicitly enabled
|
164
|
+
if plain_output?
|
165
|
+
text_icon = icon_to_text(icon)
|
166
|
+
text_icon ? "#{text_icon} #{message}" : message
|
167
|
+
else
|
168
|
+
emoji_icon = icon_to_emoji(icon)
|
169
|
+
emoji_icon ? "#{emoji_icon} #{message}" : message
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def icon_to_emoji(icon)
|
174
|
+
case icon
|
175
|
+
when :success then "🎉"
|
176
|
+
when :error then "❌"
|
177
|
+
when :warn then "⚠️"
|
178
|
+
when :health_check then "🩺"
|
179
|
+
when :fatal then "💥"
|
180
|
+
when :ok then "🟢"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def icon_to_text(icon)
|
185
|
+
case icon
|
186
|
+
when :health_check then "[HEALTH CHECK]"
|
187
|
+
else "[#{icon.to_s.upcase}]"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def plain_output?
|
192
|
+
@plain_output || false
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|