consolle 0.2.8 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6409b2de73d4612f586ccb5ebf8bcfbf791b1b8f54ce385ff803496dea45a03
4
- data.tar.gz: 446a7bfe92350892d29b10d9cdc16c83f4e1d77e1adf680b3274f793d3cc51b9
3
+ metadata.gz: 1a3baea2a10a98486b313276d58a6bdf7bffcf8418b490c004f756e480f38060
4
+ data.tar.gz: 9385cca18d1b3caa4de57c2efb98c17da15ff7cf9520f35deaf9eb0ebd21aeab
5
5
  SHA512:
6
- metadata.gz: '0892cc7e9268d58ed4152a404e1901099dc0652993f01c00937e15180c3fd45ef95f0d6a0d001af0d319f6df02904e201b4266e7df96de92955af2e0475c51fa'
7
- data.tar.gz: cae1d830feb7101bb50b598b41474a9216c216485b4a036af25d64dc31e9cb8880f9725c0ca2c500807cf16e866a08328ed52e7ff366a46cdd55fcb00078ae15
6
+ metadata.gz: d7adccc6ed3b0d47cfb80058c6f45eb82454dbe064c57a778f6bf642ddbd6755bca99c93bd460057e25ee0e5347fa006fbf63dfb723e6e2cb6c6b9b23c3fcd68
7
+ data.tar.gz: 99d6ac3e933f46de68891d4d48b05e590e6bd044902f00dc322d468ff136bbe2a839f614da5bf1958e8fb3173df5d5e69f535229aff2ba4cdd1d53beb87d76bf
data/.version CHANGED
@@ -1 +1 @@
1
- 0.2.8
1
+ 0.3.0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- consolle (0.2.8)
4
+ consolle (0.3.0)
5
5
  logger (~> 1.0)
6
6
  thor (~> 1.0)
7
7
 
@@ -5,6 +5,7 @@ require 'json'
5
5
  require 'timeout'
6
6
  require 'securerandom'
7
7
  require 'fileutils'
8
+ require_relative '../constants'
8
9
 
9
10
  module Consolle
10
11
  module Adapters
@@ -12,7 +13,7 @@ module Consolle
12
13
  attr_reader :socket_path, :process_pid, :pid_path, :log_path
13
14
 
14
15
  def initialize(socket_path: nil, pid_path: nil, log_path: nil, rails_root: nil, rails_env: nil, verbose: false,
15
- command: nil)
16
+ command: nil, wait_timeout: nil)
16
17
  @socket_path = socket_path || default_socket_path
17
18
  @pid_path = pid_path || default_pid_path
18
19
  @log_path = log_path || default_log_path
@@ -20,6 +21,7 @@ module Consolle
20
21
  @rails_env = rails_env || 'development'
21
22
  @verbose = verbose
22
23
  @command = command || 'bin/rails console'
24
+ @wait_timeout = wait_timeout || Consolle::DEFAULT_WAIT_TIMEOUT
23
25
  @server_pid = nil
24
26
  end
25
27
 
@@ -30,7 +32,7 @@ module Consolle
30
32
  start_server_daemon
31
33
 
32
34
  # Wait for server to be ready
33
- wait_for_server(timeout: 10)
35
+ wait_for_server(timeout: @wait_timeout)
34
36
 
35
37
  # Get server status
36
38
  status = get_status
@@ -146,7 +148,8 @@ module Consolle
146
148
  @verbose ? 'debug' : 'info',
147
149
  @pid_path,
148
150
  @log_path,
149
- @command
151
+ @command,
152
+ @wait_timeout.to_s
150
153
  ]
151
154
  end
152
155
 
@@ -156,7 +159,8 @@ module Consolle
156
159
  require 'consolle/server/console_socket_server'
157
160
  require 'logger'
158
161
  #{' '}
159
- socket_path, rails_root, rails_env, log_level, pid_path, log_path, command = ARGV
162
+ socket_path, rails_root, rails_env, log_level, pid_path, log_path, command, wait_timeout_str = ARGV
163
+ wait_timeout = wait_timeout_str ? wait_timeout_str.to_i : nil
160
164
  #{' '}
161
165
  # Write initial log
162
166
  log_file = log_path || socket_path.sub(/\\.socket$/, '.log')
@@ -186,7 +190,8 @@ module Consolle
186
190
  rails_root: rails_root,
187
191
  rails_env: rails_env,
188
192
  logger: logger,
189
- command: command
193
+ command: command,
194
+ wait_timeout: wait_timeout
190
195
  )
191
196
  #{' '}
192
197
  puts "[Server] Starting server with log level: \#{log_level}..."
@@ -199,6 +204,17 @@ module Consolle
199
204
  rescue => e
200
205
  puts "[Server] Error: \#{e.class}: \#{e.message}"
201
206
  puts e.backtrace.join("\\n")
207
+
208
+ # Clean up socket file if it exists
209
+ if defined?(socket_path) && socket_path && File.exist?(socket_path)
210
+ File.unlink(socket_path) rescue nil
211
+ end
212
+
213
+ # Clean up PID file if it exists
214
+ if defined?(pid_file) && pid_file && File.exist?(pid_file)
215
+ File.unlink(pid_file) rescue nil
216
+ end
217
+
202
218
  exit 1
203
219
  end
204
220
  RUBY
@@ -213,8 +229,8 @@ module Consolle
213
229
  log_file = @log_path
214
230
  pid = Process.spawn(
215
231
  *server_command,
216
- out: log_file,
217
- err: log_file
232
+ out: [log_file, 'a'],
233
+ err: [log_file, 'a']
218
234
  )
219
235
 
220
236
  Process.detach(pid)
@@ -255,16 +271,106 @@ module Consolle
255
271
  File.unlink(pid_file) if File.exist?(pid_file)
256
272
  end
257
273
 
258
- def wait_for_server(timeout: 10)
274
+ def wait_for_server(timeout: Consolle::DEFAULT_WAIT_TIMEOUT)
259
275
  deadline = Time.now + timeout
276
+ server_pid = nil
277
+ error_found = false
278
+ error_message = nil
279
+ last_log_check = Time.now
280
+ ssh_auth_detected = false
281
+
282
+ # Record the initial log file position to avoid reading old errors
283
+ initial_log_pos = if File.exist?(@log_path)
284
+ File.size(@log_path)
285
+ else
286
+ 0
287
+ end
288
+
289
+ puts "Waiting for console to start (timeout: #{timeout}s)..." if @verbose
260
290
 
261
291
  while Time.now < deadline
262
- return true if File.exist?(@socket_path) && get_status
292
+ # Check if server process is still alive by checking pid file
293
+ if File.exist?(@pid_path)
294
+ server_pid ||= File.read(@pid_path).to_i
295
+ begin
296
+ Process.kill(0, server_pid)
297
+ # Process is alive
298
+ rescue Errno::ESRCH
299
+ # Process died - check log for error
300
+ if File.exist?(@log_path)
301
+ File.open(@log_path, 'r') do |f|
302
+ f.seek(initial_log_pos)
303
+ log_content = f.read
304
+ if log_content.include?('[Server] Error:')
305
+ error_lines = log_content.lines.grep(/\[Server\] Error:/)
306
+ error_message = error_lines.last.strip if error_lines.any?
307
+ else
308
+ error_message = "Server process died unexpectedly"
309
+ end
310
+ end
311
+ else
312
+ error_message = "Server process died unexpectedly"
313
+ end
314
+ error_found = true
315
+ break
316
+ end
317
+ end
318
+
319
+ # Check log file periodically for errors or SSH auth messages
320
+ if Time.now - last_log_check > 0.5
321
+ last_log_check = Time.now
322
+ if File.exist?(@log_path) && File.size(@log_path) > initial_log_pos
323
+ File.open(@log_path, 'r') do |f|
324
+ f.seek(initial_log_pos)
325
+ log_content = f.read
326
+
327
+ # Check for explicit errors
328
+ if log_content.include?('[Server] Error:')
329
+ error_lines = log_content.lines.grep(/\[Server\] Error:/)
330
+ error_message = error_lines.last.strip if error_lines.any?
331
+ error_found = true
332
+ break
333
+ end
334
+
335
+ # Check for SSH authentication messages
336
+ if !ssh_auth_detected && (log_content.include?('SSH') ||
337
+ log_content.include?('ssh') ||
338
+ log_content.include?('Authenticating') ||
339
+ log_content.include?('authentication') ||
340
+ log_content.include?('1Password') ||
341
+ @command.include?('kamal') ||
342
+ @command.include?('ssh'))
343
+ ssh_auth_detected = true
344
+ puts "SSH authentication detected, extending timeout..." if @verbose
345
+ # Extend deadline for SSH auth
346
+ deadline = Time.now + [timeout, 60].max
347
+ end
348
+ end
349
+ end
350
+ end
351
+
352
+ # Check if socket exists and server is responsive
353
+ if File.exist?(@socket_path)
354
+ begin
355
+ status = get_status
356
+ if status && status['success'] && status['running']
357
+ return true
358
+ end
359
+ rescue StandardError
360
+ # Socket exists but not ready yet, continue waiting
361
+ end
362
+ end
263
363
 
264
364
  sleep 0.1
265
365
  end
266
366
 
267
- raise "Server failed to start within #{timeout} seconds"
367
+ if error_found
368
+ raise "Server failed to start: #{error_message || 'Unknown error'}"
369
+ else
370
+ timeout_msg = "Server failed to start within #{timeout} seconds"
371
+ timeout_msg += " (SSH authentication may be required)" if ssh_auth_detected || @command.include?('ssh') || @command.include?('kamal')
372
+ raise timeout_msg
373
+ end
268
374
  end
269
375
 
270
376
  def send_request(request, timeout: 30)
data/lib/consolle/cli.rb CHANGED
@@ -7,6 +7,7 @@ require 'socket'
7
7
  require 'timeout'
8
8
  require 'securerandom'
9
9
  require 'date'
10
+ require_relative 'constants'
10
11
  require_relative 'adapters/rails_console'
11
12
 
12
13
  module Consolle
@@ -126,9 +127,13 @@ module Consolle
126
127
  Custom console commands are supported for special environments:
127
128
  cone start --command "kamal app exec -i 'bin/rails console'"
128
129
  cone start --command "docker exec -it myapp bin/rails console"
130
+
131
+ For SSH-based commands that require authentication (e.g., 1Password SSH agent):
132
+ cone start --command "kamal console" --wait-timeout 60
129
133
  LONGDESC
130
134
  method_option :rails_env, type: :string, aliases: '-e', desc: 'Rails environment', default: 'development'
131
135
  method_option :command, type: :string, aliases: '-c', desc: 'Custom console command', default: 'bin/rails console'
136
+ method_option :wait_timeout, type: :numeric, aliases: '-w', desc: 'Timeout for console startup (seconds)', default: Consolle::DEFAULT_WAIT_TIMEOUT
132
137
  def start
133
138
  ensure_rails_project!
134
139
  ensure_project_directories
@@ -155,7 +160,7 @@ module Consolle
155
160
  clear_session_info
156
161
  end
157
162
 
158
- adapter = create_rails_adapter(options[:rails_env], options[:target], options[:command])
163
+ adapter = create_rails_adapter(options[:rails_env], options[:target], options[:command], options[:wait_timeout])
159
164
 
160
165
  puts 'Starting Rails console...'
161
166
 
@@ -553,9 +558,20 @@ module Consolle
553
558
  puts result['result'] unless result['result'].nil?
554
559
  puts "Execution time: #{result['execution_time']}s" if options[:verbose] && result['execution_time']
555
560
  else
556
- puts "Error: #{result['error']}"
561
+ # Display error information
562
+ if result['error_code']
563
+ puts "Error: #{result['error_code']}"
564
+ else
565
+ puts "Error: #{result['error']}"
566
+ end
567
+
568
+ # Show error class in verbose mode
569
+ if options[:verbose] && result['error_class']
570
+ puts "Error Class: #{result['error_class']}"
571
+ end
572
+
557
573
  puts result['message']
558
- puts result['backtrace']&.join("\n") if options[:verbose]
574
+ puts result['backtrace']&.join("\n") if options[:verbose] && result['backtrace']
559
575
  exit 1
560
576
  end
561
577
  end
@@ -633,7 +649,7 @@ module Consolle
633
649
  File.join(Dir.pwd, 'tmp', 'cone', 'sessions.json')
634
650
  end
635
651
 
636
- def create_rails_adapter(rails_env = 'development', target = nil, command = nil)
652
+ def create_rails_adapter(rails_env = 'development', target = nil, command = nil, wait_timeout = nil)
637
653
  target ||= options[:target]
638
654
 
639
655
  Consolle::Adapters::RailsConsole.new(
@@ -643,7 +659,8 @@ module Consolle
643
659
  rails_root: Dir.pwd,
644
660
  rails_env: rails_env,
645
661
  verbose: options[:verbose],
646
- command: command
662
+ command: command,
663
+ wait_timeout: wait_timeout
647
664
  )
648
665
  end
649
666
 
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consolle
4
+ # Default timeout for console startup (in seconds)
5
+ DEFAULT_WAIT_TIMEOUT = 25
6
+ end
@@ -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,8 @@ require 'pty'
4
4
  require 'timeout'
5
5
  require 'fcntl'
6
6
  require 'logger'
7
+ require_relative '../constants'
8
+ require_relative '../errors'
7
9
 
8
10
  # Ruby 3.4.0+ extracts base64 as a default gem
9
11
  # Suppress warning by silencing verbose mode temporarily
@@ -26,11 +28,12 @@ module Consolle
26
28
  PROMPT_PATTERN = /^[^\w]*(\u001E\u001F<CONSOLLE>\u001F\u001E|\w+[-_]?\w*\([^)]*\)>|irb\([^)]+\):\d+:?\d*[>*]|>>|>)\s*$/
27
29
  CTRL_C = "\x03"
28
30
 
29
- def initialize(rails_root:, rails_env: 'development', logger: nil, command: nil)
31
+ def initialize(rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil)
30
32
  @rails_root = rails_root
31
33
  @rails_env = rails_env
32
34
  @command = command || 'bin/rails console'
33
35
  @logger = logger || Logger.new(STDOUT)
36
+ @wait_timeout = wait_timeout || Consolle::DEFAULT_WAIT_TIMEOUT
34
37
  @pid = nil
35
38
  @reader = nil
36
39
  @writer = nil
@@ -94,9 +97,9 @@ module Consolle
94
97
  end
95
98
  encoded_code = Base64.strict_encode64(utf8_code)
96
99
 
97
- # Use eval to execute the Base64-decoded code
100
+ # Use eval to execute the Base64-decoded code with exception handling
98
101
  # Ensure decoded string is properly handled as UTF-8
99
- eval_command = "eval(Base64.decode64('#{encoded_code}').force_encoding('UTF-8'), IRB.CurrentContext.workspace.binding)"
102
+ 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"
100
103
  logger.debug '[ConsoleSupervisor] Sending eval command (Base64 encoded)'
101
104
  @writer.puts eval_command
102
105
  @writer.flush
@@ -113,11 +116,7 @@ module Consolle
113
116
  @writer.flush
114
117
  sleep 0.5
115
118
  clear_buffer
116
- return {
117
- success: false,
118
- output: "Execution timed out after #{timeout} seconds",
119
- execution_time: timeout
120
- }
119
+ return build_timeout_response(timeout)
121
120
  end
122
121
 
123
122
  begin
@@ -142,25 +141,29 @@ module Consolle
142
141
  # PTY can throw EIO when no data available
143
142
  IO.select([@reader], nil, nil, 0.1)
144
143
  rescue EOFError
145
- return {
146
- success: false,
147
- output: 'Console terminated',
144
+ return build_error_response(
145
+ EOFError.new('Console terminated'),
148
146
  execution_time: nil
149
- }
147
+ )
150
148
  end
151
149
  end
152
150
 
153
151
  # Parse and return result
154
- result = parse_output(output, eval_command)
152
+ parsed_result = parse_output(output, eval_command)
155
153
 
156
154
  # Log for debugging object output issues
157
155
  logger.debug "[ConsoleSupervisor] Raw output: #{output.inspect}"
158
- logger.debug "[ConsoleSupervisor] Parsed result: #{result.inspect}"
156
+ logger.debug "[ConsoleSupervisor] Parsed result: #{parsed_result.inspect}"
159
157
 
160
- { success: true, output: result, execution_time: nil }
158
+ # Check if the output contains an error
159
+ if parsed_result.is_a?(Hash) && parsed_result[:error]
160
+ build_error_response(parsed_result[:exception], execution_time: nil)
161
+ else
162
+ { success: true, output: parsed_result, execution_time: nil }
163
+ end
161
164
  rescue StandardError => e
162
165
  logger.error "[ConsoleSupervisor] Eval error: #{e.message}"
163
- { success: false, output: "Error: #{e.message}", execution_time: nil }
166
+ build_error_response(e, execution_time: nil)
164
167
  end
165
168
  end
166
169
  end
@@ -204,6 +207,39 @@ module Consolle
204
207
 
205
208
  private
206
209
 
210
+ def build_timeout_response(timeout_seconds)
211
+ error = Consolle::Errors::ExecutionTimeout.new(timeout_seconds)
212
+ {
213
+ success: false,
214
+ error_class: error.class.name,
215
+ error_code: Consolle::Errors::ErrorClassifier.to_code(error),
216
+ output: error.message,
217
+ execution_time: timeout_seconds
218
+ }
219
+ end
220
+
221
+ def build_error_response(exception, execution_time: nil)
222
+ # Handle string messages that come from eval output parsing
223
+ if exception.is_a?(String)
224
+ error_code = Consolle::Errors::ErrorClassifier.classify_message(exception)
225
+ return {
226
+ success: false,
227
+ error_class: 'RuntimeError',
228
+ error_code: error_code,
229
+ output: exception,
230
+ execution_time: execution_time
231
+ }
232
+ end
233
+
234
+ {
235
+ success: false,
236
+ error_class: exception.class.name,
237
+ error_code: Consolle::Errors::ErrorClassifier.to_code(exception),
238
+ output: "#{exception.class}: #{exception.message}",
239
+ execution_time: execution_time
240
+ }
241
+ end
242
+
207
243
  def spawn_console
208
244
  env = {
209
245
  'RAILS_ENV' => @rails_env,
@@ -246,7 +282,7 @@ module Consolle
246
282
  trim_restart_history
247
283
 
248
284
  # Wait for initial prompt
249
- wait_for_prompt(timeout: 15)
285
+ wait_for_prompt(timeout: @wait_timeout)
250
286
 
251
287
  # Configure IRB settings for automation
252
288
  configure_irb_for_automation
@@ -352,12 +388,17 @@ module Consolle
352
388
 
353
389
  def wait_for_prompt(timeout: 15, consume_all: false)
354
390
  output = +''
355
- deadline = Time.now + timeout
391
+ start_time = Time.now
392
+ deadline = start_time + timeout
356
393
  prompt_found = false
357
394
  last_data_time = Time.now
358
395
 
396
+ logger.info "[ConsoleSupervisor] Waiting for prompt with timeout: #{timeout}s (deadline: #{deadline}, now: #{start_time})"
397
+
359
398
  loop do
360
- if Time.now > deadline
399
+ current_time = Time.now
400
+ if current_time > deadline
401
+ logger.error "[ConsoleSupervisor] Timeout reached. Current: #{current_time}, Deadline: #{deadline}, Elapsed: #{current_time - start_time}s"
361
402
  logger.error "[ConsoleSupervisor] Output so far: #{output.inspect}"
362
403
  logger.error "[ConsoleSupervisor] Stripped: #{strip_ansi(output).inspect}"
363
404
  raise Timeout::Error, "No prompt after #{timeout} seconds"
@@ -490,6 +531,7 @@ module Consolle
490
531
  lines = clean.lines
491
532
  result_lines = []
492
533
  skip_echo = true
534
+ error_exception = nil
493
535
 
494
536
  lines.each_with_index do |line, idx|
495
537
  # Skip the eval command echo (both file-based and Base64)
@@ -509,12 +551,43 @@ module Consolle
509
551
  next
510
552
  end
511
553
 
554
+ # Check for error patterns
555
+ if !error_exception && line.match?(/^(.*Error|.*Exception):/)
556
+ error_match = line.match(/^((?:\w+::)*\w*(?:Error|Exception)):\s*(.*)/)
557
+ if error_match
558
+ error_class = error_match[1]
559
+ error_message = error_match[2]
560
+
561
+ # Try to create the actual exception object
562
+ begin
563
+ # Handle namespaced errors
564
+ if error_class.include?('::')
565
+ parts = error_class.split('::')
566
+ klass = Object
567
+ parts.each { |part| klass = klass.const_get(part) }
568
+ error_exception = klass.new(error_message)
569
+ else
570
+ # Try core Ruby error classes
571
+ error_exception = Object.const_get(error_class).new(error_message)
572
+ end
573
+ rescue NameError
574
+ # If we can't find the exact error class, use a generic RuntimeError
575
+ error_exception = RuntimeError.new("#{error_class}: #{error_message}")
576
+ end
577
+ end
578
+ end
579
+
512
580
  # Collect all other lines (including return values and side effects)
513
581
  result_lines << line
514
582
  end
515
583
 
516
- # Join all lines - this includes both side effects and return values
517
- result_lines.join.strip
584
+ # If an error was detected, return it as a hash
585
+ if error_exception
586
+ { error: true, exception: error_exception, output: result_lines.join.strip }
587
+ else
588
+ # Join all lines - this includes both side effects and return values
589
+ result_lines.join.strip
590
+ end
518
591
  end
519
592
 
520
593
  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,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'consolle/version'
4
+ require_relative 'consolle/constants'
5
+ require_relative 'consolle/errors'
4
6
  require_relative 'consolle/cli'
5
7
 
6
8
  # Server components
@@ -9,5 +11,4 @@ require_relative 'consolle/server/console_supervisor'
9
11
  require_relative 'consolle/server/request_broker'
10
12
 
11
13
  module Consolle
12
- class Error < StandardError; end
13
14
  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.8
4
+ version: 0.3.0
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,8 @@ files:
79
79
  - lib/consolle.rb
80
80
  - lib/consolle/adapters/rails_console.rb
81
81
  - lib/consolle/cli.rb
82
+ - lib/consolle/constants.rb
83
+ - lib/consolle/errors.rb
82
84
  - lib/consolle/server/console_socket_server.rb
83
85
  - lib/consolle/server/console_supervisor.rb
84
86
  - lib/consolle/server/request_broker.rb