consolle 0.2.7 → 0.2.9

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: 7b0fccfa081563dfbdbb310a8822ccca827150906aac1208e3b72fa754eb8ab6
4
- data.tar.gz: 05ab3780cce3a62638540641146dae880f7cdbe75d5ede45ce71ba32433af2ec
3
+ metadata.gz: 826cb825a86940fa018abc635b0c7b5914999d85da2c079801d7b98a85166adb
4
+ data.tar.gz: 919448817dd2064b93214be249eeb840bb328ef3c907ab9f8456daf2a8d45992
5
5
  SHA512:
6
- metadata.gz: 45c5bff6c8b43e6029fb1e4d0f12cc83ce2f5925f0882c8b628394868b52f119c92e4fcc6dccaae7190e72ebdf30cc71cc1b7aeb021f6c9fbaff127fd67b9524
7
- data.tar.gz: e96701a1acea38c2c90c51e4c5df56ff9e038e48551b5d34c3d17d74b4c6ad0044a8123553e1ebf2000415654da192a5f60890924b08ab904d9ad13ee8728512
6
+ metadata.gz: a3fb3a9902a388e4fe3d551e867c4d6ceec83aff1087e54a48f972c5bb1e029b9216ca4baa5a7c83d1eeee9905285607093cb09e790eda60583f7a66efa501d3
7
+ data.tar.gz: 4a5de0ff45adc32c57a35595b6743133a7f4d5cf1911d0c6dba50c5cd1ae11b0779942845af3a69d8d01c6ddb77745a81d106dfa2639b3e41ee34c91c593fcf2
data/.version CHANGED
@@ -1 +1 @@
1
- 0.2.7
1
+ 0.2.9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- consolle (0.2.7)
4
+ consolle (0.2.9)
5
5
  logger (~> 1.0)
6
6
  thor (~> 1.0)
7
7
 
@@ -12,7 +12,7 @@ module Consolle
12
12
  attr_reader :socket_path, :process_pid, :pid_path, :log_path
13
13
 
14
14
  def initialize(socket_path: nil, pid_path: nil, log_path: nil, rails_root: nil, rails_env: nil, verbose: false,
15
- command: nil)
15
+ command: nil, wait_timeout: nil)
16
16
  @socket_path = socket_path || default_socket_path
17
17
  @pid_path = pid_path || default_pid_path
18
18
  @log_path = log_path || default_log_path
@@ -20,6 +20,7 @@ module Consolle
20
20
  @rails_env = rails_env || 'development'
21
21
  @verbose = verbose
22
22
  @command = command || 'bin/rails console'
23
+ @wait_timeout = wait_timeout || 15
23
24
  @server_pid = nil
24
25
  end
25
26
 
@@ -30,7 +31,7 @@ module Consolle
30
31
  start_server_daemon
31
32
 
32
33
  # Wait for server to be ready
33
- wait_for_server(timeout: 10)
34
+ wait_for_server(timeout: @wait_timeout)
34
35
 
35
36
  # Get server status
36
37
  status = get_status
@@ -146,7 +147,8 @@ module Consolle
146
147
  @verbose ? 'debug' : 'info',
147
148
  @pid_path,
148
149
  @log_path,
149
- @command
150
+ @command,
151
+ @wait_timeout.to_s
150
152
  ]
151
153
  end
152
154
 
@@ -156,7 +158,8 @@ module Consolle
156
158
  require 'consolle/server/console_socket_server'
157
159
  require 'logger'
158
160
  #{' '}
159
- socket_path, rails_root, rails_env, log_level, pid_path, log_path, command = ARGV
161
+ socket_path, rails_root, rails_env, log_level, pid_path, log_path, command, wait_timeout_str = ARGV
162
+ wait_timeout = wait_timeout_str ? wait_timeout_str.to_i : nil
160
163
  #{' '}
161
164
  # Write initial log
162
165
  log_file = log_path || socket_path.sub(/\\.socket$/, '.log')
@@ -186,7 +189,8 @@ module Consolle
186
189
  rails_root: rails_root,
187
190
  rails_env: rails_env,
188
191
  logger: logger,
189
- command: command
192
+ command: command,
193
+ wait_timeout: wait_timeout
190
194
  )
191
195
  #{' '}
192
196
  puts "[Server] Starting server with log level: \#{log_level}..."
@@ -199,6 +203,17 @@ module Consolle
199
203
  rescue => e
200
204
  puts "[Server] Error: \#{e.class}: \#{e.message}"
201
205
  puts e.backtrace.join("\\n")
206
+
207
+ # Clean up socket file if it exists
208
+ if defined?(socket_path) && socket_path && File.exist?(socket_path)
209
+ File.unlink(socket_path) rescue nil
210
+ end
211
+
212
+ # Clean up PID file if it exists
213
+ if defined?(pid_file) && pid_file && File.exist?(pid_file)
214
+ File.unlink(pid_file) rescue nil
215
+ end
216
+
202
217
  exit 1
203
218
  end
204
219
  RUBY
@@ -213,8 +228,8 @@ module Consolle
213
228
  log_file = @log_path
214
229
  pid = Process.spawn(
215
230
  *server_command,
216
- out: log_file,
217
- err: log_file
231
+ out: [log_file, 'a'],
232
+ err: [log_file, 'a']
218
233
  )
219
234
 
220
235
  Process.detach(pid)
@@ -255,16 +270,93 @@ module Consolle
255
270
  File.unlink(pid_file) if File.exist?(pid_file)
256
271
  end
257
272
 
258
- def wait_for_server(timeout: 10)
273
+ def wait_for_server(timeout: 15)
259
274
  deadline = Time.now + timeout
275
+ server_pid = nil
276
+ error_found = false
277
+ error_message = nil
278
+ last_log_check = Time.now
279
+ ssh_auth_detected = false
280
+
281
+ puts "Waiting for console to start (timeout: #{timeout}s)..." if @verbose
260
282
 
261
283
  while Time.now < deadline
262
- return true if File.exist?(@socket_path) && get_status
284
+ # Check if server process is still alive by checking pid file
285
+ if File.exist?(@pid_path)
286
+ server_pid ||= File.read(@pid_path).to_i
287
+ begin
288
+ Process.kill(0, server_pid)
289
+ # Process is alive
290
+ rescue Errno::ESRCH
291
+ # Process died - check log for error
292
+ if File.exist?(@log_path)
293
+ log_content = File.read(@log_path)
294
+ if log_content.include?('[Server] Error:')
295
+ error_lines = log_content.lines.grep(/\[Server\] Error:/)
296
+ error_message = error_lines.last.strip if error_lines.any?
297
+ else
298
+ error_message = "Server process died unexpectedly"
299
+ end
300
+ else
301
+ error_message = "Server process died unexpectedly"
302
+ end
303
+ error_found = true
304
+ break
305
+ end
306
+ end
307
+
308
+ # Check log file periodically for errors or SSH auth messages
309
+ if Time.now - last_log_check > 0.5
310
+ last_log_check = Time.now
311
+ if File.exist?(@log_path)
312
+ log_content = File.read(@log_path)
313
+
314
+ # Check for explicit errors
315
+ if log_content.include?('[Server] Error:')
316
+ error_lines = log_content.lines.grep(/\[Server\] Error:/)
317
+ error_message = error_lines.last.strip if error_lines.any?
318
+ error_found = true
319
+ break
320
+ end
321
+
322
+ # Check for SSH authentication messages
323
+ if !ssh_auth_detected && (log_content.include?('SSH') ||
324
+ log_content.include?('ssh') ||
325
+ log_content.include?('Authenticating') ||
326
+ log_content.include?('authentication') ||
327
+ log_content.include?('1Password') ||
328
+ @command.include?('kamal') ||
329
+ @command.include?('ssh'))
330
+ ssh_auth_detected = true
331
+ puts "SSH authentication detected, extending timeout..." if @verbose
332
+ # Extend deadline for SSH auth
333
+ deadline = Time.now + [timeout, 60].max
334
+ end
335
+ end
336
+ end
337
+
338
+ # Check if socket exists and server is responsive
339
+ if File.exist?(@socket_path)
340
+ begin
341
+ status = get_status
342
+ if status && status['success'] && status['running']
343
+ return true
344
+ end
345
+ rescue StandardError
346
+ # Socket exists but not ready yet, continue waiting
347
+ end
348
+ end
263
349
 
264
350
  sleep 0.1
265
351
  end
266
352
 
267
- raise "Server failed to start within #{timeout} seconds"
353
+ if error_found
354
+ raise "Server failed to start: #{error_message || 'Unknown error'}"
355
+ else
356
+ timeout_msg = "Server failed to start within #{timeout} seconds"
357
+ timeout_msg += " (SSH authentication may be required)" if ssh_auth_detected || @command.include?('ssh') || @command.include?('kamal')
358
+ raise timeout_msg
359
+ end
268
360
  end
269
361
 
270
362
  def send_request(request, timeout: 30)
data/lib/consolle/cli.rb CHANGED
@@ -126,9 +126,13 @@ module Consolle
126
126
  Custom console commands are supported for special environments:
127
127
  cone start --command "kamal app exec -i 'bin/rails console'"
128
128
  cone start --command "docker exec -it myapp bin/rails console"
129
+
130
+ For SSH-based commands that require authentication (e.g., 1Password SSH agent):
131
+ cone start --command "kamal console" --wait-timeout 60
129
132
  LONGDESC
130
133
  method_option :rails_env, type: :string, aliases: '-e', desc: 'Rails environment', default: 'development'
131
134
  method_option :command, type: :string, aliases: '-c', desc: 'Custom console command', default: 'bin/rails console'
135
+ method_option :wait_timeout, type: :numeric, aliases: '-w', desc: 'Timeout for console startup (seconds)', default: 15
132
136
  def start
133
137
  ensure_rails_project!
134
138
  ensure_project_directories
@@ -155,7 +159,7 @@ module Consolle
155
159
  clear_session_info
156
160
  end
157
161
 
158
- adapter = create_rails_adapter(options[:rails_env], options[:target], options[:command])
162
+ adapter = create_rails_adapter(options[:rails_env], options[:target], options[:command], options[:wait_timeout])
159
163
 
160
164
  puts 'Starting Rails console...'
161
165
 
@@ -553,9 +557,20 @@ module Consolle
553
557
  puts result['result'] unless result['result'].nil?
554
558
  puts "Execution time: #{result['execution_time']}s" if options[:verbose] && result['execution_time']
555
559
  else
556
- puts "Error: #{result['error']}"
560
+ # Display error information
561
+ if result['error_code']
562
+ puts "Error: #{result['error_code']}"
563
+ else
564
+ puts "Error: #{result['error']}"
565
+ end
566
+
567
+ # Show error class in verbose mode
568
+ if options[:verbose] && result['error_class']
569
+ puts "Error Class: #{result['error_class']}"
570
+ end
571
+
557
572
  puts result['message']
558
- puts result['backtrace']&.join("\n") if options[:verbose]
573
+ puts result['backtrace']&.join("\n") if options[:verbose] && result['backtrace']
559
574
  exit 1
560
575
  end
561
576
  end
@@ -633,7 +648,7 @@ module Consolle
633
648
  File.join(Dir.pwd, 'tmp', 'cone', 'sessions.json')
634
649
  end
635
650
 
636
- def create_rails_adapter(rails_env = 'development', target = nil, command = nil)
651
+ def create_rails_adapter(rails_env = 'development', target = nil, command = nil, wait_timeout = nil)
637
652
  target ||= options[:target]
638
653
 
639
654
  Consolle::Adapters::RailsConsole.new(
@@ -643,7 +658,8 @@ module Consolle
643
658
  rails_root: Dir.pwd,
644
659
  rails_env: rails_env,
645
660
  verbose: options[:verbose],
646
- command: command
661
+ command: command,
662
+ wait_timeout: wait_timeout
647
663
  )
648
664
  end
649
665
 
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consolle
4
+ module Errors
5
+ # Base error class for all Consolle errors
6
+ class Error < StandardError; end
7
+
8
+ # Timeout errors hierarchy
9
+ class Timeout < Error; end
10
+
11
+ # Socket communication timeout (CLI/Adapter layer)
12
+ class SocketTimeout < Timeout
13
+ def initialize(timeout_seconds)
14
+ super("Socket operation timed out after #{timeout_seconds} seconds")
15
+ end
16
+ end
17
+
18
+ # Request processing timeout (Broker layer)
19
+ class RequestTimeout < Timeout
20
+ def initialize
21
+ super("Request processing timed out")
22
+ end
23
+ end
24
+
25
+ # Code execution timeout (Supervisor layer)
26
+ class ExecutionTimeout < Timeout
27
+ def initialize(timeout_seconds)
28
+ super("Code execution timed out after #{timeout_seconds} seconds")
29
+ end
30
+ end
31
+
32
+ # Execution errors
33
+ class ExecutionError < Error; end
34
+
35
+ # Syntax error in executed code
36
+ class SyntaxError < ExecutionError
37
+ def initialize(message)
38
+ super("Syntax error: #{message}")
39
+ end
40
+ end
41
+
42
+ # Runtime error in executed code
43
+ class RuntimeError < ExecutionError
44
+ def initialize(message)
45
+ super("Runtime error: #{message}")
46
+ end
47
+ end
48
+
49
+ # Load error (missing gem, file, etc.)
50
+ class LoadError < ExecutionError
51
+ def initialize(message)
52
+ super("Load error: #{message}")
53
+ end
54
+ end
55
+
56
+ # Error classifier to map exceptions to error codes
57
+ class ErrorClassifier
58
+ ERROR_CODE_MAP = {
59
+ 'Timeout::Error' => 'EXECUTION_TIMEOUT',
60
+ 'Consolle::Errors::SocketTimeout' => 'SOCKET_TIMEOUT',
61
+ 'Consolle::Errors::RequestTimeout' => 'REQUEST_TIMEOUT',
62
+ 'Consolle::Errors::ExecutionTimeout' => 'EXECUTION_TIMEOUT',
63
+ 'SyntaxError' => 'SYNTAX_ERROR',
64
+ '::SyntaxError' => 'SYNTAX_ERROR',
65
+ 'LoadError' => 'LOAD_ERROR',
66
+ '::LoadError' => 'LOAD_ERROR',
67
+ 'NameError' => 'NAME_ERROR',
68
+ 'NoMethodError' => 'NO_METHOD_ERROR',
69
+ 'ArgumentError' => 'ARGUMENT_ERROR',
70
+ 'TypeError' => 'TYPE_ERROR',
71
+ 'ZeroDivisionError' => 'ZERO_DIVISION_ERROR',
72
+ 'RuntimeError' => 'RUNTIME_ERROR',
73
+ '::RuntimeError' => 'RUNTIME_ERROR',
74
+ 'StandardError' => 'STANDARD_ERROR',
75
+ 'Exception' => 'EXCEPTION'
76
+ }.freeze
77
+
78
+ def self.to_code(exception)
79
+ return 'UNKNOWN_ERROR' unless exception.is_a?(Exception)
80
+
81
+ # Try exact class match first
82
+ error_code = ERROR_CODE_MAP[exception.class.name]
83
+ return error_code if error_code
84
+
85
+ # Try with leading :: for core Ruby errors
86
+ error_code = ERROR_CODE_MAP["::#{exception.class.name}"]
87
+ return error_code if error_code
88
+
89
+ # Check inheritance chain
90
+ exception.class.ancestors.each do |klass|
91
+ error_code = ERROR_CODE_MAP[klass.name]
92
+ return error_code if error_code
93
+ end
94
+
95
+ 'UNKNOWN_ERROR'
96
+ end
97
+
98
+ def self.classify_message(error_message)
99
+ case error_message
100
+ when /syntax error/i
101
+ 'SYNTAX_ERROR'
102
+ when /cannot load such file|no such file to load/i
103
+ 'LOAD_ERROR'
104
+ when /undefined local variable or method/i, /undefined method/i
105
+ 'NAME_ERROR'
106
+ when /wrong number of arguments/i
107
+ 'ARGUMENT_ERROR'
108
+ when /execution timed out/i
109
+ 'EXECUTION_TIMEOUT'
110
+ when /request timed out/i
111
+ 'REQUEST_TIMEOUT'
112
+ else
113
+ 'EXECUTION_ERROR'
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -12,11 +12,12 @@ module Consolle
12
12
  class ConsoleSocketServer
13
13
  attr_reader :socket_path, :logger
14
14
 
15
- def initialize(socket_path:, rails_root:, rails_env: 'development', logger: nil, command: nil)
15
+ def initialize(socket_path:, rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil)
16
16
  @socket_path = socket_path
17
17
  @rails_root = rails_root
18
18
  @rails_env = rails_env
19
19
  @command = command || 'bin/rails console'
20
+ @wait_timeout = wait_timeout
20
21
  @logger = logger || begin
21
22
  log = Logger.new(STDOUT)
22
23
  log.level = Logger::DEBUG
@@ -100,7 +101,8 @@ module Consolle
100
101
  rails_root: @rails_root,
101
102
  rails_env: @rails_env,
102
103
  logger: @logger,
103
- command: @command
104
+ command: @command,
105
+ wait_timeout: @wait_timeout
104
106
  )
105
107
  end
106
108
 
@@ -4,6 +4,7 @@ require 'pty'
4
4
  require 'timeout'
5
5
  require 'fcntl'
6
6
  require 'logger'
7
+ require_relative '../errors'
7
8
 
8
9
  # Ruby 3.4.0+ extracts base64 as a default gem
9
10
  # Suppress warning by silencing verbose mode temporarily
@@ -26,11 +27,12 @@ module Consolle
26
27
  PROMPT_PATTERN = /^[^\w]*(\u001E\u001F<CONSOLLE>\u001F\u001E|\w+[-_]?\w*\([^)]*\)>|irb\([^)]+\):\d+:?\d*[>*]|>>|>)\s*$/
27
28
  CTRL_C = "\x03"
28
29
 
29
- def initialize(rails_root:, rails_env: 'development', logger: nil, command: nil)
30
+ def initialize(rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil)
30
31
  @rails_root = rails_root
31
32
  @rails_env = rails_env
32
33
  @command = command || 'bin/rails console'
33
34
  @logger = logger || Logger.new(STDOUT)
35
+ @wait_timeout = wait_timeout || 15
34
36
  @pid = nil
35
37
  @reader = nil
36
38
  @writer = nil
@@ -72,11 +74,31 @@ module Consolle
72
74
 
73
75
  # Encode code using Base64 to handle special characters and remote consoles
74
76
  # Ensure UTF-8 encoding to handle strings that may be tagged as ASCII-8BIT
75
- utf8_code = code.encoding == Encoding::UTF_8 ? code : code.dup.force_encoding('UTF-8')
77
+ utf8_code = if code.encoding == Encoding::UTF_8
78
+ code
79
+ else
80
+ # Try to handle ASCII-8BIT strings that might contain UTF-8 data
81
+ temp = code.dup
82
+ if code.encoding == Encoding::ASCII_8BIT
83
+ # First try to interpret as UTF-8
84
+ temp.force_encoding('UTF-8')
85
+ if temp.valid_encoding?
86
+ temp
87
+ else
88
+ # If not valid UTF-8, try other common encodings
89
+ # or use replacement characters
90
+ code.encode('UTF-8', invalid: :replace, undef: :replace)
91
+ end
92
+ else
93
+ # For other encodings, convert to UTF-8
94
+ code.encode('UTF-8')
95
+ end
96
+ end
76
97
  encoded_code = Base64.strict_encode64(utf8_code)
77
98
 
78
- # Use eval to execute the Base64-decoded code
79
- eval_command = "eval(Base64.decode64('#{encoded_code}'), IRB.CurrentContext.workspace.binding)"
99
+ # Use eval to execute the Base64-decoded code with exception handling
100
+ # Ensure decoded string is properly handled as UTF-8
101
+ eval_command = "begin; eval(Base64.decode64('#{encoded_code}').force_encoding('UTF-8'), IRB.CurrentContext.workspace.binding); rescue Exception => e; puts \"\#{e.class}: \#{e.message}\"; nil; end"
80
102
  logger.debug '[ConsoleSupervisor] Sending eval command (Base64 encoded)'
81
103
  @writer.puts eval_command
82
104
  @writer.flush
@@ -93,11 +115,7 @@ module Consolle
93
115
  @writer.flush
94
116
  sleep 0.5
95
117
  clear_buffer
96
- return {
97
- success: false,
98
- output: "Execution timed out after #{timeout} seconds",
99
- execution_time: timeout
100
- }
118
+ return build_timeout_response(timeout)
101
119
  end
102
120
 
103
121
  begin
@@ -122,25 +140,29 @@ module Consolle
122
140
  # PTY can throw EIO when no data available
123
141
  IO.select([@reader], nil, nil, 0.1)
124
142
  rescue EOFError
125
- return {
126
- success: false,
127
- output: 'Console terminated',
143
+ return build_error_response(
144
+ EOFError.new('Console terminated'),
128
145
  execution_time: nil
129
- }
146
+ )
130
147
  end
131
148
  end
132
149
 
133
150
  # Parse and return result
134
- result = parse_output(output, eval_command)
151
+ parsed_result = parse_output(output, eval_command)
135
152
 
136
153
  # Log for debugging object output issues
137
154
  logger.debug "[ConsoleSupervisor] Raw output: #{output.inspect}"
138
- logger.debug "[ConsoleSupervisor] Parsed result: #{result.inspect}"
155
+ logger.debug "[ConsoleSupervisor] Parsed result: #{parsed_result.inspect}"
139
156
 
140
- { success: true, output: result, execution_time: nil }
157
+ # Check if the output contains an error
158
+ if parsed_result.is_a?(Hash) && parsed_result[:error]
159
+ build_error_response(parsed_result[:exception], execution_time: nil)
160
+ else
161
+ { success: true, output: parsed_result, execution_time: nil }
162
+ end
141
163
  rescue StandardError => e
142
164
  logger.error "[ConsoleSupervisor] Eval error: #{e.message}"
143
- { success: false, output: "Error: #{e.message}", execution_time: nil }
165
+ build_error_response(e, execution_time: nil)
144
166
  end
145
167
  end
146
168
  end
@@ -184,6 +206,39 @@ module Consolle
184
206
 
185
207
  private
186
208
 
209
+ def build_timeout_response(timeout_seconds)
210
+ error = Consolle::Errors::ExecutionTimeout.new(timeout_seconds)
211
+ {
212
+ success: false,
213
+ error_class: error.class.name,
214
+ error_code: Consolle::Errors::ErrorClassifier.to_code(error),
215
+ output: error.message,
216
+ execution_time: timeout_seconds
217
+ }
218
+ end
219
+
220
+ def build_error_response(exception, execution_time: nil)
221
+ # Handle string messages that come from eval output parsing
222
+ if exception.is_a?(String)
223
+ error_code = Consolle::Errors::ErrorClassifier.classify_message(exception)
224
+ return {
225
+ success: false,
226
+ error_class: 'RuntimeError',
227
+ error_code: error_code,
228
+ output: exception,
229
+ execution_time: execution_time
230
+ }
231
+ end
232
+
233
+ {
234
+ success: false,
235
+ error_class: exception.class.name,
236
+ error_code: Consolle::Errors::ErrorClassifier.to_code(exception),
237
+ output: "#{exception.class}: #{exception.message}",
238
+ execution_time: execution_time
239
+ }
240
+ end
241
+
187
242
  def spawn_console
188
243
  env = {
189
244
  'RAILS_ENV' => @rails_env,
@@ -226,7 +281,7 @@ module Consolle
226
281
  trim_restart_history
227
282
 
228
283
  # Wait for initial prompt
229
- wait_for_prompt(timeout: 15)
284
+ wait_for_prompt(timeout: @wait_timeout)
230
285
 
231
286
  # Configure IRB settings for automation
232
287
  configure_irb_for_automation
@@ -470,6 +525,7 @@ module Consolle
470
525
  lines = clean.lines
471
526
  result_lines = []
472
527
  skip_echo = true
528
+ error_exception = nil
473
529
 
474
530
  lines.each_with_index do |line, idx|
475
531
  # Skip the eval command echo (both file-based and Base64)
@@ -489,12 +545,43 @@ module Consolle
489
545
  next
490
546
  end
491
547
 
548
+ # Check for error patterns
549
+ if !error_exception && line.match?(/^(.*Error|.*Exception):/)
550
+ error_match = line.match(/^((?:\w+::)*\w*(?:Error|Exception)):\s*(.*)/)
551
+ if error_match
552
+ error_class = error_match[1]
553
+ error_message = error_match[2]
554
+
555
+ # Try to create the actual exception object
556
+ begin
557
+ # Handle namespaced errors
558
+ if error_class.include?('::')
559
+ parts = error_class.split('::')
560
+ klass = Object
561
+ parts.each { |part| klass = klass.const_get(part) }
562
+ error_exception = klass.new(error_message)
563
+ else
564
+ # Try core Ruby error classes
565
+ error_exception = Object.const_get(error_class).new(error_message)
566
+ end
567
+ rescue NameError
568
+ # If we can't find the exact error class, use a generic RuntimeError
569
+ error_exception = RuntimeError.new("#{error_class}: #{error_message}")
570
+ end
571
+ end
572
+ end
573
+
492
574
  # Collect all other lines (including return values and side effects)
493
575
  result_lines << line
494
576
  end
495
577
 
496
- # Join all lines - this includes both side effects and return values
497
- result_lines.join.strip
578
+ # If an error was detected, return it as a hash
579
+ if error_exception
580
+ { error: true, exception: error_exception, output: result_lines.join.strip }
581
+ else
582
+ # Join all lines - this includes both side effects and return values
583
+ result_lines.join.strip
584
+ end
498
585
  end
499
586
 
500
587
  def trim_restart_history
@@ -172,11 +172,18 @@ module Consolle
172
172
  'execution_time' => result[:execution_time]
173
173
  }
174
174
  else
175
- {
175
+ response = {
176
176
  'success' => false,
177
- 'error' => 'ExecutionError',
177
+ 'error' => result[:error_code] || 'ExecutionError', # Use error_code for backward compatibility
178
178
  'message' => result[:output]
179
179
  }
180
+
181
+ # Add new fields if present
182
+ response['error_class'] = result[:error_class] if result[:error_class]
183
+ response['error_code'] = result[:error_code] if result[:error_code]
184
+ response['execution_time'] = result[:execution_time] if result[:execution_time]
185
+
186
+ response
180
187
  end
181
188
  end
182
189
 
data/lib/consolle.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'consolle/version'
4
+ require_relative 'consolle/errors'
4
5
  require_relative 'consolle/cli'
5
6
 
6
7
  # Server components
@@ -9,5 +10,4 @@ require_relative 'consolle/server/console_supervisor'
9
10
  require_relative 'consolle/server/request_broker'
10
11
 
11
12
  module Consolle
12
- class Error < StandardError; end
13
13
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: consolle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - nacyot
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-07-24 00:00:00.000000000 Z
10
+ date: 2025-07-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: logger
@@ -79,6 +79,7 @@ files:
79
79
  - lib/consolle.rb
80
80
  - lib/consolle/adapters/rails_console.rb
81
81
  - lib/consolle/cli.rb
82
+ - lib/consolle/errors.rb
82
83
  - lib/consolle/server/console_socket_server.rb
83
84
  - lib/consolle/server/console_supervisor.rb
84
85
  - lib/consolle/server/request_broker.rb