consolle 0.3.7 → 0.3.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a88f6019a32aaae300dee3a3e39b1dd1084ea1e2900171d6ee02f880923e071e
4
- data.tar.gz: e9c468bc8095d8197e8f70cc4c0b75d0cc8da9ba3a7b0af45573300b755e1970
3
+ metadata.gz: '0975195db50059158fa8643203c06099ea77c53a6ee199705731244806d7b2d8'
4
+ data.tar.gz: 8d1fc462e0dc7acd21b9f53989f892ffdf03920b1bc327c82fc70516fda8e201
5
5
  SHA512:
6
- metadata.gz: e5c0615769edda8936547380360ed5a7beeaaeda7569ccf91322daa4658923a82cad4ad87e5ffabeca4c230e89f3d371ebcaa5eec58663b25172ee0ab8c2ba07
7
- data.tar.gz: 83dd02825015e9f9e73ff48075436f459722cdc045ee4b0b8d90509c5ca91274c27af682549ef114598efcf21d90628fc460360f33a069134b0da63eeab94c81
6
+ metadata.gz: a281509268f01cdbb60e1a538e9c037c82cce14f26aebd1faf30befeee6aeba6996eaa94f37fa70f13949f0a1d5d749db7604fae39253154d61ec14e6db637ff
7
+ data.tar.gz: 58c2126e05ca127992001c57a597a095b3c4aa193c36b934de5490d3eddb95bc85770c7bd7e1905c3fdd2b4e95bc68d77f9518d35a22cd8f3619f71c4de15638
data/.version CHANGED
@@ -1 +1 @@
1
- 0.3.7
1
+ 0.3.8
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- consolle (0.3.7)
4
+ consolle (0.3.8)
5
5
  logger (~> 1.0)
6
6
  thor (~> 1.0)
7
7
 
@@ -13,7 +13,7 @@ module Consolle
13
13
  attr_reader :socket_path, :process_pid, :pid_path, :log_path
14
14
 
15
15
  def initialize(socket_path: nil, pid_path: nil, log_path: nil, rails_root: nil, rails_env: nil, verbose: false,
16
- command: nil, wait_timeout: nil)
16
+ command: nil, wait_timeout: nil, mode: nil)
17
17
  @socket_path = socket_path || default_socket_path
18
18
  @pid_path = pid_path || default_pid_path
19
19
  @log_path = log_path || default_log_path
@@ -22,6 +22,7 @@ module Consolle
22
22
  @verbose = verbose
23
23
  @command = command || 'bin/rails console'
24
24
  @wait_timeout = wait_timeout || Consolle::DEFAULT_WAIT_TIMEOUT
25
+ @mode = mode # nil means use config file setting
25
26
  @server_pid = nil
26
27
  end
27
28
 
@@ -149,7 +150,8 @@ module Consolle
149
150
  @pid_path,
150
151
  @log_path,
151
152
  @command,
152
- @wait_timeout.to_s
153
+ @wait_timeout.to_s,
154
+ @mode.to_s # empty string if nil (use config file)
153
155
  ]
154
156
  end
155
157
 
@@ -159,8 +161,9 @@ module Consolle
159
161
  require 'consolle/server/console_socket_server'
160
162
  require 'logger'
161
163
  #{' '}
162
- socket_path, rails_root, rails_env, log_level, pid_path, log_path, command, wait_timeout_str = ARGV
164
+ socket_path, rails_root, rails_env, log_level, pid_path, log_path, command, wait_timeout_str, mode_str = ARGV
163
165
  wait_timeout = wait_timeout_str ? wait_timeout_str.to_i : nil
166
+ mode = mode_str && !mode_str.empty? ? mode_str.to_sym : nil # nil = use config file
164
167
  #{' '}
165
168
  # Write initial log
166
169
  log_file = log_path || socket_path.sub(/\\.socket$/, '.log')
@@ -191,10 +194,11 @@ module Consolle
191
194
  rails_env: rails_env,
192
195
  logger: logger,
193
196
  command: command,
194
- wait_timeout: wait_timeout
197
+ wait_timeout: wait_timeout,
198
+ mode: mode
195
199
  )
196
200
  #{' '}
197
- puts "[Server] Starting server with log level: \#{log_level}..."
201
+ puts "[Server] Starting server with log level: \#{log_level}, mode: \#{mode || 'config'}..."
198
202
  server.start
199
203
  #{' '}
200
204
  puts "[Server] Server started, entering sleep..."
@@ -204,17 +208,17 @@ module Consolle
204
208
  rescue => e
205
209
  puts "[Server] Error: \#{e.class}: \#{e.message}"
206
210
  puts e.backtrace.join("\\n")
207
-
211
+
208
212
  # Clean up socket file if it exists
209
213
  if defined?(socket_path) && socket_path && File.exist?(socket_path)
210
214
  File.unlink(socket_path) rescue nil
211
215
  end
212
-
216
+
213
217
  # Clean up PID file if it exists
214
218
  if defined?(pid_file) && pid_file && File.exist?(pid_file)
215
219
  File.unlink(pid_file) rescue nil
216
220
  end
217
-
221
+
218
222
  exit 1
219
223
  end
220
224
  RUBY
data/lib/consolle/cli.rb CHANGED
@@ -124,15 +124,25 @@ module Consolle
124
124
  RAILS_ENV=production cone start --target api
125
125
  RAILS_ENV=development cone start --target worker
126
126
 
127
- Custom console commands are supported for special environments:
127
+ Supervisor modes (--mode):
128
+ pty - Traditional PTY-based mode, supports custom commands (default)
129
+ embed-irb - Pure IRB embedding, Ruby 3.3+ only
130
+ embed-rails - Rails console embedding, Ruby 3.3+ only
131
+
132
+ Custom console commands are supported for PTY mode:
128
133
  cone start --command "kamal app exec -i 'bin/rails console'"
129
134
  cone start --command "docker exec -it myapp bin/rails console"
130
-
135
+
131
136
  For SSH-based commands that require authentication (e.g., 1Password SSH agent):
132
137
  cone start --command "kamal console" --wait-timeout 60
138
+
139
+ Use embedded modes for faster local execution (200x faster):
140
+ cone start --mode embed-rails
141
+ cone start --mode embed-irb
133
142
  LONGDESC
134
143
  # Rails environment is now controlled via RAILS_ENV, not a CLI option
135
- method_option :command, type: :string, aliases: '-c', desc: 'Custom console command', default: 'bin/rails console'
144
+ method_option :mode, type: :string, aliases: '-m', desc: 'Supervisor mode: pty, embed-irb, embed-rails'
145
+ method_option :command, type: :string, aliases: '-c', desc: 'Custom console command (PTY mode only)', default: 'bin/rails console'
136
146
  method_option :wait_timeout, type: :numeric, aliases: '-w', desc: 'Timeout for console startup (seconds)', default: Consolle::DEFAULT_WAIT_TIMEOUT
137
147
  def start
138
148
  ensure_rails_project!
@@ -160,7 +170,13 @@ module Consolle
160
170
  clear_session_info
161
171
  end
162
172
 
163
- adapter = create_rails_adapter(current_rails_env, options[:target], options[:command], options[:wait_timeout])
173
+ adapter = create_rails_adapter(
174
+ current_rails_env,
175
+ options[:target],
176
+ options[:command],
177
+ options[:wait_timeout],
178
+ options[:mode]
179
+ )
164
180
 
165
181
  puts 'Starting Rails console...'
166
182
 
@@ -557,8 +573,8 @@ module Consolle
557
573
  if result['success']
558
574
  # Always print result, even if empty (multiline code often returns empty string)
559
575
  puts result['result'] unless result['result'].nil?
560
- # Always show execution time when available
561
- puts "Execution time: #{result['execution_time'].round(3)}s" if result['execution_time']
576
+ # Show execution time only in verbose mode
577
+ puts "Execution time: #{result['execution_time'].round(3)}s" if options[:verbose] && result['execution_time']
562
578
  else
563
579
  # Display error information
564
580
  if result['error_code']
@@ -574,10 +590,10 @@ module Consolle
574
590
 
575
591
  puts result['message']
576
592
  puts result['backtrace']&.join("\n") if options[:verbose] && result['backtrace']
577
-
578
- # Show execution time for errors too
579
- puts "Execution time: #{result['execution_time'].round(3)}s" if result['execution_time']
580
-
593
+
594
+ # Show execution time for errors too (verbose only)
595
+ puts "Execution time: #{result['execution_time'].round(3)}s" if options[:verbose] && result['execution_time']
596
+
581
597
  exit 1
582
598
  end
583
599
  end
@@ -702,7 +718,7 @@ module Consolle
702
718
  File.join(Dir.pwd, 'tmp', 'cone', 'sessions.json')
703
719
  end
704
720
 
705
- def create_rails_adapter(rails_env = 'development', target = nil, command = nil, wait_timeout = nil)
721
+ def create_rails_adapter(rails_env = 'development', target = nil, command = nil, wait_timeout = nil, mode = nil)
706
722
  target ||= options[:target]
707
723
 
708
724
  Consolle::Adapters::RailsConsole.new(
@@ -713,7 +729,8 @@ module Consolle
713
729
  rails_env: rails_env,
714
730
  verbose: options[:verbose],
715
731
  command: command,
716
- wait_timeout: wait_timeout
732
+ wait_timeout: wait_timeout,
733
+ mode: mode
717
734
  )
718
735
  end
719
736
 
@@ -14,13 +14,23 @@ module Consolle
14
14
  # - Generic prompts: >> or >
15
15
  DEFAULT_PROMPT_PATTERN = /^[^\w]*(\u001E\u001F<CONSOLLE>\u001F\u001E|\w+[-_]?\w*\([^)]*\)(:\d+)?>|irb\([^)]+\):\d+:?\d*[>*]|>>|>)\s*$/
16
16
 
17
- attr_reader :rails_root, :prompt_pattern, :raw_prompt_pattern
17
+ # Valid supervisor modes
18
+ # - pty: PTY-based, supports custom command (local/remote)
19
+ # - embed-irb: Pure IRB embedding (Ruby 3.3+, local only)
20
+ # - embed-rails: Rails console embedding (Ruby 3.3+, local only)
21
+ VALID_MODES = %w[pty embed-irb embed-rails].freeze
22
+ DEFAULT_MODE = :pty
23
+ DEFAULT_COMMAND = 'bin/rails console'
24
+
25
+ attr_reader :rails_root, :prompt_pattern, :raw_prompt_pattern, :mode, :command
18
26
 
19
27
  def initialize(rails_root)
20
28
  @rails_root = rails_root
21
29
  @config = load_config
22
30
  @raw_prompt_pattern = @config['prompt_pattern']
23
31
  @prompt_pattern = parse_prompt_pattern(@raw_prompt_pattern)
32
+ @mode = parse_mode(@config['mode'])
33
+ @command = @config['command'] || DEFAULT_COMMAND
24
34
  end
25
35
 
26
36
  def self.load(rails_root)
@@ -74,5 +84,21 @@ module Consolle
74
84
  DEFAULT_PROMPT_PATTERN
75
85
  end
76
86
  end
87
+
88
+ def parse_mode(mode_string)
89
+ return DEFAULT_MODE if mode_string.nil?
90
+
91
+ # Normalize: convert underscores/symbols, handle legacy 'embedded' -> 'embed-rails'
92
+ normalized = mode_string.to_s.downcase.tr('_', '-')
93
+ normalized = 'embed-rails' if normalized == 'embedded'
94
+ normalized = 'pty' if normalized == 'auto'
95
+
96
+ unless VALID_MODES.include?(normalized)
97
+ warn "[Consolle] Warning: Invalid mode '#{mode_string}'. Using '#{DEFAULT_MODE}'."
98
+ return DEFAULT_MODE
99
+ end
100
+
101
+ normalized.tr('-', '_').to_sym # :pty, :embed_irb, :embed_rails
102
+ end
77
103
  end
78
104
  end
@@ -110,6 +110,20 @@ module Consolle
110
110
  end
111
111
  end
112
112
 
113
+ # Configuration error
114
+ class ConfigurationError < Error
115
+ def initialize(message)
116
+ super("Configuration error: #{message}")
117
+ end
118
+ end
119
+
120
+ # Unsupported Ruby version for embedded mode
121
+ class UnsupportedRubyVersion < Error
122
+ def initialize(message)
123
+ super(message)
124
+ end
125
+ end
126
+
113
127
  # Error classifier to map exceptions to error codes
114
128
  class ErrorClassifier
115
129
  ERROR_CODE_MAP = {
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consolle
4
+ module Server
5
+ # Base interface for console supervisors
6
+ # Both PTY-based and embedded IRB modes inherit from this class
7
+ class BaseSupervisor
8
+ attr_reader :rails_root, :rails_env, :logger, :config
9
+
10
+ def initialize(rails_root:, rails_env: 'development', logger: nil)
11
+ @rails_root = rails_root
12
+ @rails_env = rails_env
13
+ @logger = logger || Logger.new($stdout)
14
+ @config = Consolle::Config.load(rails_root)
15
+ end
16
+
17
+ # Execute code and return result
18
+ # @param code [String] Ruby code to evaluate
19
+ # @param timeout [Integer] Timeout in seconds
20
+ # @return [Hash] Result hash with :success, :output, :execution_time, etc.
21
+ def eval(code, timeout: 60)
22
+ raise NotImplementedError, "#{self.class} must implement #eval"
23
+ end
24
+
25
+ # Check if the console is running and ready
26
+ # @return [Boolean]
27
+ def running?
28
+ raise NotImplementedError, "#{self.class} must implement #running?"
29
+ end
30
+
31
+ # Stop the console
32
+ def stop
33
+ raise NotImplementedError, "#{self.class} must implement #stop"
34
+ end
35
+
36
+ # Restart the console
37
+ def restart
38
+ raise NotImplementedError, "#{self.class} must implement #restart"
39
+ end
40
+
41
+ # Returns the mode name for logging/debugging
42
+ # @return [Symbol] :pty or :embedded
43
+ def mode
44
+ raise NotImplementedError, "#{self.class} must implement #mode"
45
+ end
46
+
47
+ protected
48
+
49
+ def build_success_response(output, execution_time:, result: nil)
50
+ response = {
51
+ success: true,
52
+ output: output,
53
+ execution_time: execution_time
54
+ }
55
+ response[:result] = result if result
56
+ response
57
+ end
58
+
59
+ def build_error_response(exception, execution_time: nil)
60
+ if exception.is_a?(String)
61
+ error_code = Consolle::Errors::ErrorClassifier.classify_message(exception)
62
+ return {
63
+ success: false,
64
+ error_class: 'RuntimeError',
65
+ error_code: error_code,
66
+ output: exception,
67
+ execution_time: execution_time
68
+ }
69
+ end
70
+
71
+ {
72
+ success: false,
73
+ error_class: exception.class.name,
74
+ error_code: Consolle::Errors::ErrorClassifier.to_code(exception),
75
+ output: "#{exception.class}: #{exception.message}",
76
+ execution_time: execution_time
77
+ }
78
+ end
79
+
80
+ def build_timeout_response(timeout_seconds)
81
+ error = Consolle::Errors::ExecutionTimeout.new(timeout_seconds)
82
+ {
83
+ success: false,
84
+ error_class: error.class.name,
85
+ error_code: Consolle::Errors::ErrorClassifier.to_code(error),
86
+ output: error.message,
87
+ execution_time: timeout_seconds
88
+ }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -5,6 +5,8 @@ require 'json'
5
5
  require 'logger'
6
6
  require 'fileutils'
7
7
  require_relative 'console_supervisor'
8
+ require_relative 'embedded_supervisor'
9
+ require_relative 'supervisor_factory'
8
10
  require_relative 'request_broker'
9
11
 
10
12
  module Consolle
@@ -12,14 +14,15 @@ module Consolle
12
14
  class ConsoleSocketServer
13
15
  attr_reader :socket_path, :logger
14
16
 
15
- def initialize(socket_path:, rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil)
17
+ def initialize(socket_path:, rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil, mode: nil)
16
18
  @socket_path = socket_path
17
19
  @rails_root = rails_root
18
20
  @rails_env = rails_env
19
21
  @command = command || 'bin/rails console'
20
22
  @wait_timeout = wait_timeout
23
+ @mode = mode # nil = use config file, otherwise :pty, :embed_irb, :embed_rails
21
24
  @logger = logger || begin
22
- log = Logger.new(STDOUT)
25
+ log = Logger.new($stdout)
23
26
  log.level = Logger::DEBUG
24
27
  log
25
28
  end
@@ -97,13 +100,20 @@ module Consolle
97
100
  end
98
101
 
99
102
  def setup_supervisor
100
- @supervisor = ConsoleSupervisor.new(
103
+ # Load config to get default mode if not explicitly specified
104
+ config = Consolle::Config.load(@rails_root)
105
+ effective_mode = @mode || config.mode
106
+
107
+ @supervisor = SupervisorFactory.create(
101
108
  rails_root: @rails_root,
109
+ mode: effective_mode,
102
110
  rails_env: @rails_env,
103
111
  logger: @logger,
104
112
  command: @command,
105
113
  wait_timeout: @wait_timeout
106
114
  )
115
+
116
+ @logger.info "[ConsoleSocketServer] Using #{@supervisor.mode} mode"
107
117
  end
108
118
 
109
119
  def setup_broker
@@ -300,6 +300,12 @@ module Consolle
300
300
  end
301
301
  end
302
302
 
303
+ # Returns the mode name for logging/debugging
304
+ # @return [Symbol] :pty
305
+ def mode
306
+ :pty
307
+ end
308
+
303
309
  # Returns the prompt pattern to use (custom from config or default)
304
310
  def prompt_pattern
305
311
  @config&.prompt_pattern || Consolle::Config::DEFAULT_PROMPT_PATTERN
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+ require 'stringio'
5
+ require_relative 'base_supervisor'
6
+ require_relative '../errors'
7
+
8
+ module Consolle
9
+ module Server
10
+ # IRB embedding mode supervisor (Ruby 3.3+)
11
+ # Runs IRB directly in-process without PTY
12
+ # Supports two modes:
13
+ # - :embed_irb - Pure IRB without Rails
14
+ # - :embed_rails - Rails console with IRB
15
+ class EmbeddedSupervisor < BaseSupervisor
16
+ MINIMUM_RUBY_VERSION = Gem::Version.new('3.3.0')
17
+ VALID_MODES = %i[embed_irb embed_rails].freeze
18
+
19
+ class << self
20
+ def supported?
21
+ Gem::Version.new(RUBY_VERSION) >= MINIMUM_RUBY_VERSION
22
+ end
23
+
24
+ def check_support!
25
+ return if supported?
26
+
27
+ raise Consolle::Errors::UnsupportedRubyVersion.new(
28
+ "Embedded mode requires Ruby #{MINIMUM_RUBY_VERSION}+, " \
29
+ "current version: #{RUBY_VERSION}"
30
+ )
31
+ end
32
+ end
33
+
34
+ # @param rails_root [String] Project root directory
35
+ # @param rails_env [String] Rails environment (only for embed_rails mode)
36
+ # @param logger [Logger] Logger instance
37
+ # @param embed_mode [Symbol] :embed_irb or :embed_rails
38
+ def initialize(rails_root:, rails_env: 'development', logger: nil, embed_mode: :embed_rails)
39
+ super(rails_root: rails_root, rails_env: rails_env, logger: logger)
40
+ @embed_mode = validate_embed_mode(embed_mode)
41
+ @running = false
42
+ @workspace = nil
43
+ @mutex = Mutex.new
44
+
45
+ boot_environment
46
+ setup_irb
47
+ @running = true
48
+
49
+ mode_name = @embed_mode == :embed_rails ? 'Rails console' : 'IRB'
50
+ logger.info "[EmbeddedSupervisor] #{mode_name} embedded mode initialized (Ruby #{RUBY_VERSION})"
51
+ end
52
+
53
+ def eval(code, timeout: nil, pre_sigint: nil)
54
+ # pre_sigint is ignored in embedded mode (no PTY)
55
+
56
+ env_timeout = ENV['CONSOLLE_TIMEOUT']&.to_i
57
+ timeout = if env_timeout && env_timeout.positive?
58
+ env_timeout
59
+ else
60
+ timeout || 60
61
+ end
62
+
63
+ @mutex.synchronize do
64
+ raise 'Console is not running' unless running?
65
+
66
+ start_time = Time.now
67
+ execute_code(code, timeout, start_time)
68
+ end
69
+ end
70
+
71
+ def running?
72
+ @running && @workspace
73
+ end
74
+
75
+ def stop
76
+ @running = false
77
+ @workspace = nil
78
+ logger.info '[EmbeddedSupervisor] Stopped'
79
+ end
80
+
81
+ def restart
82
+ logger.info '[EmbeddedSupervisor] Restarting IRB workspace...'
83
+ @mutex.synchronize do
84
+ setup_irb
85
+ end
86
+ logger.info '[EmbeddedSupervisor] IRB workspace restarted'
87
+ end
88
+
89
+ def mode
90
+ @embed_mode
91
+ end
92
+
93
+ # Returns the current process PID (embedded mode runs in-process)
94
+ # @return [Integer]
95
+ def pid
96
+ Process.pid
97
+ end
98
+
99
+ # Returns the prompt pattern (for compatibility with PTY mode)
100
+ def prompt_pattern
101
+ @config&.prompt_pattern || Consolle::Config::DEFAULT_PROMPT_PATTERN
102
+ end
103
+
104
+ private
105
+
106
+ def validate_embed_mode(mode)
107
+ mode_sym = mode.to_sym
108
+ return mode_sym if VALID_MODES.include?(mode_sym)
109
+
110
+ raise ArgumentError, "Invalid embed_mode: #{mode}. Must be one of: #{VALID_MODES.join(', ')}"
111
+ end
112
+
113
+ def boot_environment
114
+ return boot_rails if @embed_mode == :embed_rails
115
+
116
+ # For embed_irb mode, no Rails loading needed
117
+ logger.info '[EmbeddedSupervisor] Pure IRB mode - skipping Rails environment'
118
+ end
119
+
120
+ def boot_rails
121
+ return if defined?(Rails) && Rails.application
122
+
123
+ ENV['RAILS_ENV'] = @rails_env
124
+
125
+ environment_file = File.join(@rails_root, 'config', 'environment.rb')
126
+ unless File.exist?(environment_file)
127
+ raise Consolle::Errors::ConfigurationError.new(
128
+ "Rails environment file not found: #{environment_file}"
129
+ )
130
+ end
131
+
132
+ logger.info "[EmbeddedSupervisor] Loading Rails environment from #{environment_file}"
133
+ require environment_file
134
+ logger.info "[EmbeddedSupervisor] Rails #{Rails.version} loaded (#{Rails.env})"
135
+ end
136
+
137
+ def setup_irb
138
+ require 'irb'
139
+
140
+ # Initialize IRB if not already done
141
+ IRB.setup(nil, argv: []) unless IRB.conf[:PROMPT]
142
+
143
+ # Configure IRB for automation
144
+ IRB.conf[:USE_COLORIZE] = false
145
+ IRB.conf[:USE_AUTOCOMPLETE] = false
146
+ IRB.conf[:USE_PAGER] = false
147
+ IRB.conf[:VERBOSE] = false
148
+ IRB.conf[:USE_MULTILINE] = false
149
+
150
+ # Create workspace with top-level binding for Rails Console-like behavior
151
+ @workspace = IRB::WorkSpace.new(TOPLEVEL_BINDING)
152
+
153
+ # Inject Rails console helpers if available
154
+ inject_rails_console_methods
155
+
156
+ logger.debug '[EmbeddedSupervisor] IRB workspace configured'
157
+ end
158
+
159
+ def inject_rails_console_methods
160
+ # Only inject for embed_rails mode
161
+ return unless @embed_mode == :embed_rails
162
+
163
+ begin
164
+ # Rails 7.1 and earlier: use Rails::ConsoleMethods
165
+ if defined?(Rails::ConsoleMethods)
166
+ @workspace.binding.eval('extend Rails::ConsoleMethods')
167
+ logger.debug '[EmbeddedSupervisor] Rails::ConsoleMethods injected'
168
+ else
169
+ # Rails 7.2+: ConsoleMethods moved, define reload! directly
170
+ inject_reload_method
171
+ logger.debug '[EmbeddedSupervisor] reload! method injected directly'
172
+ end
173
+ rescue StandardError => e
174
+ logger.warn "[EmbeddedSupervisor] Failed to inject console methods: #{e.message}"
175
+ end
176
+ end
177
+
178
+ def inject_reload_method
179
+ # Define reload! method that calls Rails reloader
180
+ @workspace.binding.eval(<<~RUBY)
181
+ def reload!(print = true)
182
+ puts "Reloading..." if print
183
+ Rails.application.reloader.reload!
184
+ true
185
+ end
186
+ RUBY
187
+ end
188
+
189
+ def execute_code(code, timeout, start_time)
190
+ stdout_capture = StringIO.new
191
+ stderr_capture = StringIO.new
192
+ result = nil
193
+ error = nil
194
+
195
+ # Capture stdout/stderr
196
+ original_stdout = $stdout
197
+ original_stderr = $stderr
198
+ $stdout = stdout_capture
199
+ $stderr = stderr_capture
200
+
201
+ begin
202
+ Timeout.timeout(timeout) do
203
+ # Use workspace.binding.eval for full IRB context
204
+ result = @workspace.binding.eval(code, '(consolle)', 1)
205
+ end
206
+ rescue Timeout::Error
207
+ $stdout = original_stdout
208
+ $stderr = original_stderr
209
+ return build_timeout_response(timeout)
210
+ rescue SyntaxError => e
211
+ error = e
212
+ rescue StandardError => e
213
+ error = e
214
+ ensure
215
+ $stdout = original_stdout
216
+ $stderr = original_stderr
217
+ end
218
+
219
+ execution_time = Time.now - start_time
220
+ captured_output = stdout_capture.string + stderr_capture.string
221
+
222
+ if error
223
+ build_error_response_with_output(error, captured_output, execution_time)
224
+ else
225
+ build_success_response_with_result(result, captured_output, execution_time)
226
+ end
227
+ end
228
+
229
+ def build_error_response_with_output(error, captured_output, execution_time)
230
+ output = captured_output.empty? ? "#{error.class}: #{error.message}" : captured_output
231
+
232
+ {
233
+ success: false,
234
+ error_class: error.class.name,
235
+ error_code: Consolle::Errors::ErrorClassifier.to_code(error),
236
+ output: output,
237
+ execution_time: execution_time
238
+ }
239
+ end
240
+
241
+ def build_success_response_with_result(result, captured_output, execution_time)
242
+ # Format output like Rails console
243
+ # If there's captured stdout, include it
244
+ # Always include the return value with => prefix
245
+ output_parts = []
246
+ output_parts << captured_output unless captured_output.empty?
247
+ output_parts << "=> #{result.inspect}"
248
+
249
+ {
250
+ success: true,
251
+ output: output_parts.join("\n"),
252
+ result: result,
253
+ execution_time: execution_time
254
+ }
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'console_supervisor'
4
+ require_relative 'embedded_supervisor'
5
+
6
+ module Consolle
7
+ module Server
8
+ # Factory for creating the appropriate supervisor based on configuration
9
+ #
10
+ # Modes:
11
+ # - :pty - PTY-based, supports custom command (local/remote)
12
+ # - :embed_irb - Pure IRB embedding (Ruby 3.3+, local only)
13
+ # - :embed_rails - Rails console embedding (Ruby 3.3+, local only)
14
+ class SupervisorFactory
15
+ MODES = %i[pty embed_irb embed_rails].freeze
16
+ EMBEDDED_MODES = %i[embed_irb embed_rails].freeze
17
+
18
+ class << self
19
+ # Create a supervisor instance
20
+ # @param rails_root [String] Path to project root
21
+ # @param mode [Symbol] :pty, :embed_irb, or :embed_rails
22
+ # @param options [Hash] Additional options passed to supervisor
23
+ # @return [BaseSupervisor] PTY or Embedded supervisor
24
+ def create(rails_root:, mode: :pty, **options)
25
+ mode = normalize_mode(mode)
26
+ validate_mode!(mode)
27
+
28
+ case mode
29
+ when :pty
30
+ create_pty_supervisor(rails_root, options)
31
+ when :embed_irb
32
+ create_embedded_supervisor(rails_root, :embed_irb, options)
33
+ when :embed_rails
34
+ create_embedded_supervisor(rails_root, :embed_rails, options)
35
+ end
36
+ end
37
+
38
+ # Check if embedded mode is available (Ruby 3.3+)
39
+ # @return [Boolean]
40
+ def embedded_available?
41
+ EmbeddedSupervisor.supported?
42
+ end
43
+
44
+ private
45
+
46
+ def normalize_mode(mode)
47
+ mode_sym = mode.to_s.tr('-', '_').to_sym
48
+
49
+ # Handle legacy mode names
50
+ case mode_sym
51
+ when :embedded then :embed_rails
52
+ when :auto then :pty
53
+ else mode_sym
54
+ end
55
+ end
56
+
57
+ def validate_mode!(mode)
58
+ return if MODES.include?(mode)
59
+
60
+ raise ArgumentError, "Invalid mode: #{mode}. Must be one of: #{MODES.join(', ')}"
61
+ end
62
+
63
+ def create_pty_supervisor(rails_root, options)
64
+ ConsoleSupervisor.new(
65
+ rails_root: rails_root,
66
+ rails_env: options[:rails_env] || 'development',
67
+ logger: options[:logger],
68
+ command: options[:command],
69
+ wait_timeout: options[:wait_timeout]
70
+ )
71
+ end
72
+
73
+ def create_embedded_supervisor(rails_root, embed_mode, options)
74
+ EmbeddedSupervisor.check_support!
75
+
76
+ EmbeddedSupervisor.new(
77
+ rails_root: rails_root,
78
+ rails_env: options[:rails_env] || 'development',
79
+ logger: options[:logger],
80
+ embed_mode: embed_mode
81
+ )
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
data/rule.ko.md CHANGED
@@ -37,6 +37,46 @@ $ cone stop # 서버 중지
37
37
 
38
38
  작업을 마치면 반드시 종료해 주세요.
39
39
 
40
+ ## 실행 모드
41
+
42
+ Cone은 세 가지 실행 모드를 지원합니다. `--mode` 옵션으로 지정할 수 있습니다.
43
+
44
+ | 모드 | 설명 | Ruby 요구사항 | 실행 속도 |
45
+ |------|------|--------------|----------|
46
+ | `pty` | PTY 기반, 커스텀 명령어 지원 (기본값) | 모든 버전 | ~0.6s |
47
+ | `embed-rails` | Rails 콘솔 임베딩 | Ruby 3.3+ | ~0.001s |
48
+ | `embed-irb` | 순수 IRB 임베딩 (Rails 미로드) | Ruby 3.3+ | ~0.001s |
49
+
50
+ ```bash
51
+ $ cone start # PTY 모드 (기본값)
52
+ $ cone start --mode embed-rails # Rails 콘솔 임베딩 (200배 빠름)
53
+ $ cone start --mode embed-irb # 순수 IRB 임베딩 (Rails 없이)
54
+ ```
55
+
56
+ ### 모드 선택 기준
57
+
58
+ - **`pty`**: 원격 환경(SSH, Docker, Kamal)이나 커스텀 명령어가 필요한 경우
59
+ - **`embed-rails`**: 로컬 Rails 개발에서 빠른 실행이 필요한 경우
60
+ - **`embed-irb`**: Rails 없이 순수 Ruby 코드만 실행하는 경우
61
+
62
+ ### 커스텀 명령어 (PTY 모드 전용)
63
+
64
+ PTY 모드에서는 `--command` 옵션으로 커스텀 콘솔 명령어를 지정할 수 있습니다.
65
+
66
+ ```bash
67
+ $ cone start --command "docker exec -it app bin/rails console"
68
+ $ cone start --command "kamal console" --wait-timeout 60
69
+ ```
70
+
71
+ ### 설정 파일
72
+
73
+ 프로젝트 루트에 `.consolle.yml` 파일로 기본 모드를 설정할 수 있습니다. CLI 옵션은 설정 파일보다 우선합니다.
74
+
75
+ ```yaml
76
+ mode: embed-rails
77
+ # command: "bin/rails console" # PTY 모드 전용
78
+ ```
79
+
40
80
  ## Cone 서버 상태 확인
41
81
 
42
82
  ```bash
@@ -74,10 +114,13 @@ $ cone exec 'puts u'
74
114
  $ cone exec -f example.rb
75
115
  ```
76
116
 
77
- 디버깅을 위한 `-v` 옵션(Verbose 출력)이 제공됩니다.
117
+ 디버깅을 위한 `-v` 옵션(Verbose 출력)이 제공됩니다. 실행 시간 및 추가 정보를 표시합니다.
78
118
 
79
119
  ```bash
80
120
  $ cone exec -v 'puts "hello, world"'
121
+ hello, world
122
+ => nil
123
+ Execution time: 0.001s
81
124
  ```
82
125
 
83
126
  ## 코드 입력 모범 사례
data/rule.md CHANGED
@@ -37,6 +37,46 @@ $ cone stop # Stop server
37
37
 
38
38
  Always terminate when you finish your work.
39
39
 
40
+ ## Execution Modes
41
+
42
+ Cone supports three execution modes. You can specify the mode with the `--mode` option.
43
+
44
+ | Mode | Description | Ruby Requirement | Execution Speed |
45
+ |------|-------------|-----------------|-----------------|
46
+ | `pty` | PTY-based, supports custom commands (default) | All versions | ~0.6s |
47
+ | `embed-rails` | Rails console embedding | Ruby 3.3+ | ~0.001s |
48
+ | `embed-irb` | Pure IRB embedding (no Rails) | Ruby 3.3+ | ~0.001s |
49
+
50
+ ```bash
51
+ $ cone start # PTY mode (default)
52
+ $ cone start --mode embed-rails # Rails console embedding (200x faster)
53
+ $ cone start --mode embed-irb # Pure IRB embedding (without Rails)
54
+ ```
55
+
56
+ ### Mode Selection Guide
57
+
58
+ - **`pty`**: For remote environments (SSH, Docker, Kamal) or when custom commands are needed
59
+ - **`embed-rails`**: For local Rails development when fast execution is required
60
+ - **`embed-irb`**: When running pure Ruby code without Rails
61
+
62
+ ### Custom Commands (PTY Mode Only)
63
+
64
+ In PTY mode, you can specify a custom console command with the `--command` option.
65
+
66
+ ```bash
67
+ $ cone start --command "docker exec -it app bin/rails console"
68
+ $ cone start --command "kamal console" --wait-timeout 60
69
+ ```
70
+
71
+ ### Configuration File
72
+
73
+ You can set the default mode in a `.consolle.yml` file at the project root. CLI options take precedence over the configuration file.
74
+
75
+ ```yaml
76
+ mode: embed-rails
77
+ # command: "bin/rails console" # PTY mode only
78
+ ```
79
+
40
80
  ## Checking Cone Server Status
41
81
 
42
82
  ```bash
@@ -74,10 +114,13 @@ You can also execute Ruby files directly using the `-f` option. Unlike Rails Run
74
114
  $ cone exec -f example.rb
75
115
  ```
76
116
 
77
- A `-v` option (Verbose output) is provided for debugging.
117
+ A `-v` option (Verbose output) is provided for debugging. It shows execution time and additional details.
78
118
 
79
119
  ```bash
80
120
  $ cone exec -v 'puts "hello, world"'
121
+ hello, world
122
+ => nil
123
+ Execution time: 0.001s
81
124
  ```
82
125
 
83
126
  ## Best Practices for Code Input
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: consolle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.7
4
+ version: 0.3.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - nacyot
@@ -82,9 +82,12 @@ files:
82
82
  - lib/consolle/config.rb
83
83
  - lib/consolle/constants.rb
84
84
  - lib/consolle/errors.rb
85
+ - lib/consolle/server/base_supervisor.rb
85
86
  - lib/consolle/server/console_socket_server.rb
86
87
  - lib/consolle/server/console_supervisor.rb
88
+ - lib/consolle/server/embedded_supervisor.rb
87
89
  - lib/consolle/server/request_broker.rb
90
+ - lib/consolle/server/supervisor_factory.rb
88
91
  - lib/consolle/version.rb
89
92
  - mise/release.sh
90
93
  - rule.ko.md