consolle 0.2.6 → 0.2.7

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.
data/lib/consolle/cli.rb CHANGED
@@ -1,18 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thor"
4
- require "fileutils"
5
- require "json"
6
- require "socket"
7
- require "timeout"
8
- require "securerandom"
9
- require "date"
10
- require_relative "adapters/rails_console"
3
+ require 'thor'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'socket'
7
+ require 'timeout'
8
+ require 'securerandom'
9
+ require 'date'
10
+ require_relative 'adapters/rails_console'
11
11
 
12
12
  module Consolle
13
13
  class CLI < Thor
14
- class_option :verbose, type: :boolean, aliases: "-v", desc: "Verbose output"
15
- class_option :target, type: :string, aliases: "-t", desc: "Target session name", default: "cone"
14
+ package_name 'Consolle'
15
+
16
+ class << self
17
+ def start(given_args = ARGV, config = {})
18
+ # Intercept --help at the top level
19
+ if given_args == ['--help'] || given_args == ['-h'] || given_args.empty?
20
+ shell = Thor::Base.shell.new
21
+ help(shell)
22
+ return
23
+ end
24
+ super
25
+ end
26
+
27
+ def help(shell, subcommand = false)
28
+ if subcommand == false
29
+ shell.say 'Consolle - Rails console management tool', :cyan
30
+ shell.say
31
+ shell.say 'USAGE:', :yellow
32
+ shell.say ' cone [COMMAND] [OPTIONS]'
33
+ shell.say
34
+ shell.say 'COMMANDS:', :yellow
35
+ shell.say ' cone start # Start Rails console in background'
36
+ shell.say ' cone stop # Stop Rails console'
37
+ shell.say ' cone restart # Restart Rails console'
38
+ shell.say ' cone status # Show Rails console status'
39
+ shell.say ' cone exec CODE # Execute Ruby code in Rails console'
40
+ shell.say ' cone ls # List active Rails console sessions'
41
+ shell.say ' cone stop_all # Stop all Rails console sessions'
42
+ shell.say ' cone rule FILE # Write cone command guide to FILE'
43
+ shell.say ' cone version # Show version'
44
+ shell.say
45
+ shell.say 'GLOBAL OPTIONS:', :yellow
46
+ shell.say ' -v, --verbose # Enable verbose output'
47
+ shell.say ' -t, --target NAME # Target session name (default: cone)'
48
+ shell.say ' -h, --help # Show this help message'
49
+ shell.say
50
+ shell.say 'EXAMPLES:', :yellow
51
+ shell.say " cone exec 'User.count' # Execute code in default session"
52
+ shell.say " cone start -t api -e production # Start production console named 'api'"
53
+ shell.say " cone exec -t api 'Rails.env' # Execute code in 'api' session"
54
+ shell.say ' cone exec -f script.rb # Execute code from file'
55
+ shell.say
56
+ shell.say 'For more information on a specific command:'
57
+ shell.say ' cone COMMAND --help'
58
+ shell.say
59
+ else
60
+ super
61
+ end
62
+ end
63
+ end
64
+
65
+ class_option :verbose, type: :boolean, aliases: '-v', desc: 'Verbose output'
66
+ class_option :target, type: :string, aliases: '-t', desc: 'Target session name', default: 'cone'
16
67
 
17
68
  def self.exit_on_failure?
18
69
  true
@@ -22,51 +73,67 @@ module Consolle
22
73
  no_commands do
23
74
  def invoke_command(command, *args)
24
75
  # Check if --help or -h is in the original arguments
25
- if ARGV.include?("--help") || ARGV.include?("-h")
76
+ if ARGV.include?('--help') || ARGV.include?('-h')
26
77
  # Show help for the command
27
78
  self.class.command_help(shell, command.name)
28
79
  return
29
80
  end
30
-
81
+
31
82
  # Call original invoke_command
32
83
  super
33
84
  end
34
85
  end
35
-
36
86
 
37
- default_task :default
38
-
39
- desc "default", "Start console"
40
- def default
41
- start
42
- end
87
+ # Remove default_task since we handle it in self.start
43
88
 
44
- desc "version", "Show consolle version"
89
+ desc 'version', 'Show consolle version'
45
90
  def version
46
91
  puts "Consolle version #{Consolle::VERSION}"
47
92
  end
48
93
 
49
- desc "rule FILE", "Write cone command guide to FILE"
94
+ # Override help to use our custom help
95
+ desc 'help [COMMAND]', 'Show help'
96
+ def help(command = nil)
97
+ if command
98
+ self.class.command_help(shell, command)
99
+ else
100
+ self.class.help(shell)
101
+ end
102
+ end
103
+
104
+ desc 'rule FILE', 'Write cone command guide to FILE'
50
105
  def rule(file_path)
51
106
  # Read the embedded rule content
52
- rule_content = File.read(File.expand_path("../../../rule.md", __FILE__))
53
-
107
+ rule_content = File.read(File.expand_path('../../rule.md', __dir__))
108
+
54
109
  # Write to the specified file
55
110
  File.write(file_path, rule_content)
56
111
  puts "✓ Cone command guide written to #{file_path}"
57
- rescue => e
112
+ rescue StandardError => e
58
113
  puts "✗ Failed to write rule file: #{e.message}"
59
114
  exit 1
60
115
  end
61
116
 
62
- desc "start", "Start Rails console in background"
63
- method_option :rails_env, type: :string, aliases: "-e", desc: "Rails environment", default: "development"
64
- method_option :command, type: :string, aliases: "-c", desc: "Custom console command", default: "bin/rails console"
117
+ desc 'start', 'Start Rails console in background'
118
+ long_desc <<-LONGDESC
119
+ Starts a Rails console process in the background for the current Rails project.
120
+ The console runs as a daemon and can be accessed through the exec command.
121
+
122
+ You can specify a custom session name with --target to run multiple consoles:
123
+ cone start --target api --rails_env production
124
+ cone start --target worker --rails_env development
125
+
126
+ Custom console commands are supported for special environments:
127
+ cone start --command "kamal app exec -i 'bin/rails console'"
128
+ cone start --command "docker exec -it myapp bin/rails console"
129
+ LONGDESC
130
+ method_option :rails_env, type: :string, aliases: '-e', desc: 'Rails environment', default: 'development'
131
+ method_option :command, type: :string, aliases: '-c', desc: 'Custom console command', default: 'bin/rails console'
65
132
  def start
66
133
  ensure_rails_project!
67
134
  ensure_project_directories
68
135
  validate_session_name!(options[:target])
69
-
136
+
70
137
  # Check if already running using session info
71
138
  session_info = load_session_info
72
139
  if session_info && session_info[:process_pid]
@@ -87,89 +154,112 @@ module Consolle
87
154
  # Session file exists but no valid PID, clean up
88
155
  clear_session_info
89
156
  end
90
-
157
+
91
158
  adapter = create_rails_adapter(options[:rails_env], options[:target], options[:command])
92
159
 
93
- puts "Starting Rails console..."
94
-
160
+ puts 'Starting Rails console...'
161
+
95
162
  begin
96
163
  adapter.start
97
- puts "✓ Rails console started successfully"
164
+ puts '✓ Rails console started successfully'
98
165
  puts " PID: #{adapter.process_pid}"
99
166
  puts " Socket: #{adapter.socket_path}"
100
-
167
+
101
168
  # Save session info
102
169
  save_session_info(adapter)
103
-
170
+
104
171
  # Log session start
105
- log_session_event(adapter.process_pid, "session_start", {
106
- rails_env: options[:rails_env],
107
- socket_path: adapter.socket_path
108
- })
172
+ log_session_event(adapter.process_pid, 'session_start', {
173
+ rails_env: options[:rails_env],
174
+ socket_path: adapter.socket_path
175
+ })
109
176
  rescue StandardError => e
110
177
  puts "✗ Failed to start Rails console: #{e.message}"
111
178
  exit 1
112
179
  end
113
180
  end
114
181
 
115
- desc "status", "Show Rails console status"
182
+ desc 'status', 'Show Rails console status'
116
183
  def status
117
184
  ensure_rails_project!
118
185
  validate_session_name!(options[:target])
119
-
186
+
120
187
  session_info = load_session_info
121
-
188
+
122
189
  if session_info.nil?
123
- puts "No active Rails console session found"
190
+ puts 'No active Rails console session found'
124
191
  return
125
192
  end
126
193
 
127
194
  # Check if server is actually responsive
128
- adapter = create_rails_adapter("development", options[:target])
129
- server_status = adapter.get_status rescue nil
130
- process_running = server_status && server_status["success"] && server_status["running"]
131
-
195
+ adapter = create_rails_adapter('development', options[:target])
196
+ server_status = begin
197
+ adapter.get_status
198
+ rescue StandardError
199
+ nil
200
+ end
201
+ process_running = server_status && server_status['success'] && server_status['running']
202
+
132
203
  if process_running
133
- rails_env = server_status["rails_env"] || "unknown"
134
- console_pid = server_status["pid"] || "unknown"
135
-
136
- puts "✓ Rails console is running"
204
+ rails_env = server_status['rails_env'] || 'unknown'
205
+ console_pid = server_status['pid'] || 'unknown'
206
+
207
+ puts '✓ Rails console is running'
137
208
  puts " PID: #{console_pid}"
138
209
  puts " Environment: #{rails_env}"
139
210
  puts " Session: #{session_info[:socket_path]}"
140
- puts " Ready for input: Yes"
211
+ puts ' Ready for input: Yes'
141
212
  else
142
- puts "✗ Rails console is not running"
213
+ puts '✗ Rails console is not running'
143
214
  clear_session_info
144
215
  end
145
216
  end
146
217
 
147
- desc "ls", "List active Rails console sessions"
218
+ desc 'ls', 'List active Rails console sessions'
219
+ long_desc <<-LONGDESC
220
+ Lists all active Rails console sessions in the current project.
221
+
222
+ Shows information about each session including:
223
+ - Session name (target)
224
+ - Process ID (PID)
225
+ - Rails environment
226
+ - Status (running/stopped)
227
+
228
+ Example output:
229
+ Active sessions:
230
+ - cone (default) [PID: 12345, ENV: development, STATUS: running]
231
+ - api [PID: 12346, ENV: production, STATUS: running]
232
+ - worker [PID: 12347, ENV: development, STATUS: stopped]
233
+ LONGDESC
148
234
  def ls
149
235
  ensure_rails_project!
150
-
236
+
151
237
  sessions = load_sessions
152
-
153
- if sessions.empty? || sessions.size == 1 && sessions.key?("_schema")
154
- puts "No active sessions"
238
+
239
+ if sessions.empty? || sessions.size == 1 && sessions.key?('_schema')
240
+ puts 'No active sessions'
155
241
  return
156
242
  end
157
-
243
+
158
244
  active_sessions = []
159
245
  stale_sessions = []
160
-
246
+
161
247
  sessions.each do |name, info|
162
- next if name == "_schema" # Skip schema field
163
-
248
+ next if name == '_schema' # Skip schema field
249
+
164
250
  # Check if process is alive
165
- if info["process_pid"] && process_alive?(info["process_pid"])
251
+ if info['process_pid'] && process_alive?(info['process_pid'])
166
252
  # Try to get server status
167
- adapter = create_rails_adapter("development", name)
168
- server_status = adapter.get_status rescue nil
169
-
170
- if server_status && server_status["success"] && server_status["running"]
171
- rails_env = server_status["rails_env"] || "development"
172
- console_pid = server_status["pid"] || info["process_pid"]
253
+ adapter = create_rails_adapter('development', name)
254
+ server_status = begin
255
+ adapter.get_status
256
+ rescue StandardError
257
+ nil
258
+ end
259
+
260
+ if server_status && server_status['success'] && server_status['running']
261
+ rails_env = server_status['rails_env'] || 'development'
262
+ console_pid = server_status['pid'] || info['process_pid']
173
263
  active_sessions << "#{name} (#{rails_env}) - PID: #{console_pid}"
174
264
  else
175
265
  stale_sessions << name
@@ -178,7 +268,7 @@ module Consolle
178
268
  stale_sessions << name
179
269
  end
180
270
  end
181
-
271
+
182
272
  # Clean up stale sessions
183
273
  if stale_sessions.any?
184
274
  with_sessions_lock do
@@ -187,86 +277,85 @@ module Consolle
187
277
  save_sessions(sessions)
188
278
  end
189
279
  end
190
-
280
+
191
281
  if active_sessions.empty?
192
- puts "No active sessions"
282
+ puts 'No active sessions'
193
283
  else
194
284
  active_sessions.each { |session| puts session }
195
285
  end
196
286
  end
197
287
 
198
- desc "stop", "Stop Rails console"
288
+ desc 'stop', 'Stop Rails console'
199
289
  def stop
200
290
  ensure_rails_project!
201
291
  validate_session_name!(options[:target])
202
-
203
- adapter = create_rails_adapter("development", options[:target])
204
-
292
+
293
+ adapter = create_rails_adapter('development', options[:target])
294
+
205
295
  if adapter.running?
206
- puts "Stopping Rails console..."
207
-
296
+ puts 'Stopping Rails console...'
297
+
208
298
  if adapter.stop
209
- puts "✓ Rails console stopped"
210
-
299
+ puts '✓ Rails console stopped'
300
+
211
301
  # Log session stop
212
302
  session_info = load_session_info
213
303
  if session_info && session_info[:process_pid]
214
- log_session_event(session_info[:process_pid], "session_stop", {
215
- reason: "user_requested"
216
- })
304
+ log_session_event(session_info[:process_pid], 'session_stop', {
305
+ reason: 'user_requested'
306
+ })
217
307
  end
218
308
  else
219
- puts "✗ Failed to stop Rails console"
309
+ puts '✗ Failed to stop Rails console'
220
310
  end
221
311
  else
222
- puts "Rails console is not running"
312
+ puts 'Rails console is not running'
223
313
  end
224
-
314
+
225
315
  clear_session_info
226
316
  end
227
317
 
228
- desc "stop_all", "Stop all Rails console sessions"
229
- map ["stop-all"] => :stop_all
318
+ desc 'stop_all', 'Stop all Rails console sessions'
319
+ map ['stop-all'] => :stop_all
230
320
  def stop_all
231
321
  ensure_rails_project!
232
-
322
+
233
323
  sessions = load_sessions
234
324
  active_sessions = []
235
-
325
+
236
326
  # Filter active sessions (excluding schema)
237
327
  sessions.each do |name, info|
238
- next if name == "_schema"
239
- if info["process_pid"] && process_alive?(info["process_pid"])
240
- active_sessions << { name: name, info: info }
241
- end
328
+ next if name == '_schema'
329
+
330
+ active_sessions << { name: name, info: info } if info['process_pid'] && process_alive?(info['process_pid'])
242
331
  end
243
-
332
+
244
333
  if active_sessions.empty?
245
- puts "No active sessions to stop"
334
+ puts 'No active sessions to stop'
246
335
  return
247
336
  end
248
-
337
+
249
338
  puts "Found #{active_sessions.size} active session(s)"
250
-
339
+
251
340
  # Stop each active session
252
341
  active_sessions.each do |session|
253
342
  name = session[:name]
254
343
  info = session[:info]
255
-
344
+
256
345
  puts "\nStopping session '#{name}'..."
257
-
258
- adapter = create_rails_adapter("development", name)
259
-
346
+
347
+ adapter = create_rails_adapter('development', name)
348
+
260
349
  if adapter.stop
261
350
  puts "✓ Session '#{name}' stopped"
262
-
351
+
263
352
  # Log session stop
264
- if info["process_pid"]
265
- log_session_event(info["process_pid"], "session_stop", {
266
- reason: "stop_all_requested"
267
- })
353
+ if info['process_pid']
354
+ log_session_event(info['process_pid'], 'session_stop', {
355
+ reason: 'stop_all_requested'
356
+ })
268
357
  end
269
-
358
+
270
359
  # Clear session info
271
360
  with_sessions_lock do
272
361
  sessions = load_sessions
@@ -277,51 +366,69 @@ module Consolle
277
366
  puts "✗ Failed to stop session '#{name}'"
278
367
  end
279
368
  end
280
-
369
+
281
370
  puts "\n✓ All sessions stopped"
282
371
  end
283
372
 
284
- desc "restart", "Restart Rails console"
285
- method_option :rails_env, type: :string, aliases: "-e", desc: "Rails environment", default: "development"
286
- method_option :force, type: :boolean, aliases: "-f", desc: "Force restart the entire server"
373
+ desc 'restart', 'Restart Rails console'
374
+ long_desc <<-LONGDESC
375
+ Restarts the Rails console process.
376
+
377
+ By default, only restarts the Rails console subprocess while keeping the#{' '}
378
+ socket server running. This is faster and maintains socket connections.
379
+
380
+ Use --force to restart the entire server including the socket server:
381
+ cone restart # Quick restart (subprocess only)
382
+ cone restart --force # Full restart (entire server)
383
+ #{' '}
384
+ Target specific sessions:
385
+ cone restart -t api
386
+ cone restart --target worker --force
387
+ LONGDESC
388
+ method_option :rails_env, type: :string, aliases: '-e', desc: 'Rails environment', default: 'development'
389
+ method_option :force, type: :boolean, aliases: '-f', desc: 'Force restart the entire server'
287
390
  def restart
288
391
  ensure_rails_project!
289
392
  validate_session_name!(options[:target])
290
-
393
+
291
394
  adapter = create_rails_adapter(options[:rails_env], options[:target])
292
-
395
+
293
396
  if adapter.running?
294
397
  # Check if environment needs to be changed
295
- current_status = adapter.get_status rescue nil
296
- current_env = current_status&.dig("rails_env") || "development"
398
+ current_status = begin
399
+ adapter.get_status
400
+ rescue StandardError
401
+ nil
402
+ end
403
+ current_env = current_status&.dig('rails_env') || 'development'
297
404
  needs_full_restart = options[:force] || (current_env != options[:rails_env])
298
-
405
+
299
406
  if needs_full_restart
300
407
  if current_env != options[:rails_env]
301
408
  puts "Environment change detected (#{current_env} -> #{options[:rails_env]})"
302
- puts "Performing full server restart..."
409
+ puts 'Performing full server restart...'
303
410
  else
304
- puts "Force restarting Rails console server..."
411
+ puts 'Force restarting Rails console server...'
305
412
  end
306
-
413
+
307
414
  # Save current rails_env for start command
308
415
  old_env = @rails_env
309
416
  @rails_env = options[:rails_env]
310
-
417
+
311
418
  stop
312
419
  sleep 1
313
420
  invoke(:start, [], { rails_env: options[:rails_env] })
314
-
421
+
315
422
  @rails_env = old_env
316
423
  else
317
- puts "Restarting Rails console subprocess..."
318
-
424
+ puts 'Restarting Rails console subprocess...'
425
+
319
426
  # Send restart request to the socket server
320
427
  request = {
321
- "action" => "restart",
322
- "request_id" => SecureRandom.uuid
428
+ 'action' => 'restart',
429
+ 'request_id' => SecureRandom.uuid
323
430
  }
324
-
431
+
325
432
  begin
326
433
  # Use direct socket connection for restart request
327
434
  socket = UNIXSocket.new(adapter.socket_path)
@@ -330,14 +437,14 @@ module Consolle
330
437
  socket.flush
331
438
  response_data = socket.gets
332
439
  socket.close
333
-
440
+
334
441
  response = JSON.parse(response_data)
335
-
336
- if response["success"]
337
- puts "✓ Rails console subprocess restarted"
338
- puts " New PID: #{response["pid"]}" if response["pid"]
442
+
443
+ if response['success']
444
+ puts '✓ Rails console subprocess restarted'
445
+ puts " New PID: #{response['pid']}" if response['pid']
339
446
  else
340
- puts "✗ Failed to restart: #{response["message"]}"
447
+ puts "✗ Failed to restart: #{response['message']}"
341
448
  puts "You can try 'cone restart --force' to restart the entire server"
342
449
  end
343
450
  rescue StandardError => e
@@ -346,91 +453,109 @@ module Consolle
346
453
  end
347
454
  end
348
455
  else
349
- puts "Rails console is not running. Starting it..."
456
+ puts 'Rails console is not running. Starting it...'
350
457
  invoke(:start, [], { rails_env: options[:rails_env] })
351
458
  end
352
459
  end
353
460
 
354
- desc "exec CODE", "Execute Ruby code in Rails console"
355
- method_option :timeout, type: :numeric, aliases: "-t", desc: "Timeout in seconds", default: 15
356
- method_option :file, type: :string, aliases: "-f", desc: "Read Ruby code from FILE"
357
- method_option :raw, type: :boolean, desc: "Do not apply escape fixes for Claude Code (keep \\! as is)"
461
+ desc 'exec CODE', 'Execute Ruby code in Rails console'
462
+ long_desc <<-LONGDESC
463
+ Executes Ruby code in the running Rails console and returns the result.
464
+
465
+ Basic usage:
466
+ cone exec 'User.count'
467
+ cone exec 'Rails.env'
468
+ #{' '}
469
+ Execute code from a file:
470
+ cone exec -f script.rb
471
+ cone exec --file complex_query.rb
472
+ #{' '}
473
+ Set custom timeout for long-running operations:
474
+ cone exec 'User.where(active: true).update_all(status: "verified")' --timeout 60
475
+ #{' '}
476
+ Target specific console sessions:
477
+ cone exec -t api 'Order.pending.count'
478
+ cone exec --target worker 'Job.failed.destroy_all'
479
+ #{' '}
480
+ The console must be started first with 'cone start'.
481
+ LONGDESC
482
+ method_option :timeout, type: :numeric, desc: 'Timeout in seconds', default: 15
483
+ method_option :file, type: :string, aliases: '-f', desc: 'Read Ruby code from FILE'
484
+ method_option :raw, type: :boolean, desc: 'Do not apply escape fixes for Claude Code (keep \\! as is)'
358
485
  def exec(*code_parts)
359
486
  ensure_rails_project!
360
487
  ensure_project_directories
361
488
  validate_session_name!(options[:target])
362
-
489
+
363
490
  # Handle code input from file or arguments first
364
491
  code = if options[:file]
365
- path = File.expand_path(options[:file])
366
- unless File.file?(path)
367
- puts "Error: File not found: #{path}"
368
- exit 1
369
- end
370
- File.read(path, mode: "r:UTF-8")
371
- else
372
- code_parts.join(" ")
373
- end
492
+ path = File.expand_path(options[:file])
493
+ unless File.file?(path)
494
+ puts "Error: File not found: #{path}"
495
+ exit 1
496
+ end
497
+ File.read(path, mode: 'r:UTF-8')
498
+ else
499
+ code_parts.join(' ')
500
+ end
374
501
 
375
502
  if code.strip.empty?
376
- puts "Error: No code provided (pass CODE or use -f FILE)"
503
+ puts 'Error: No code provided (pass CODE or use -f FILE)'
377
504
  exit 1
378
505
  end
379
-
506
+
380
507
  session_info = load_session_info
381
508
  server_running = false
382
-
509
+
383
510
  # Check if server is running
384
511
  if session_info
385
512
  begin
386
513
  # Try to connect to socket and get status
387
514
  socket = UNIXSocket.new(session_info[:socket_path])
388
515
  request = {
389
- "action" => "status",
390
- "request_id" => SecureRandom.uuid
516
+ 'action' => 'status',
517
+ 'request_id' => SecureRandom.uuid
391
518
  }
392
519
  socket.write(JSON.generate(request))
393
520
  socket.write("\n")
394
521
  socket.flush
395
522
  response_data = socket.gets
396
523
  socket.close
397
-
524
+
398
525
  response = JSON.parse(response_data)
399
- server_running = response["success"] && response["running"]
526
+ server_running = response['success'] && response['running']
400
527
  rescue StandardError
401
528
  # Server not responsive
402
529
  server_running = false
403
530
  end
404
531
  end
405
-
532
+
406
533
  # Check if server is running
407
534
  unless server_running
408
- puts "✗ Rails console is not running"
409
- puts "Please start it first with: cone start"
535
+ puts '✗ Rails console is not running'
536
+ puts 'Please start it first with: cone start'
410
537
  exit 1
411
538
  end
412
539
 
413
540
  # Apply Claude Code escape fix unless --raw option is specified
414
- unless options[:raw]
415
- code = code.gsub('\\!', '!')
416
- end
541
+ code = code.gsub('\\!', '!') unless options[:raw]
417
542
 
418
543
  puts "Executing: #{code}" if options[:verbose]
419
-
544
+
420
545
  # Send code to socket
421
546
  result = send_code_to_socket(session_info[:socket_path], code, timeout: options[:timeout])
422
-
547
+
423
548
  # Log the request and response
424
549
  log_session_activity(session_info[:process_pid], code, result)
425
-
426
- if result["success"]
550
+
551
+ if result['success']
427
552
  # Always print result, even if empty (multiline code often returns empty string)
428
- puts result["result"] unless result["result"].nil?
429
- puts "Execution time: #{result["execution_time"]}s" if options[:verbose] && result["execution_time"]
553
+ puts result['result'] unless result['result'].nil?
554
+ puts "Execution time: #{result['execution_time']}s" if options[:verbose] && result['execution_time']
430
555
  else
431
- puts "Error: #{result["error"]}"
432
- puts result["message"]
433
- puts result["backtrace"]&.join("\n") if options[:verbose]
556
+ puts "Error: #{result['error']}"
557
+ puts result['message']
558
+ puts result['backtrace']&.join("\n") if options[:verbose]
434
559
  exit 1
435
560
  end
436
561
  end
@@ -438,17 +563,17 @@ module Consolle
438
563
  private
439
564
 
440
565
  def ensure_rails_project!
441
- unless File.exist?("config/environment.rb") || File.exist?("config/application.rb")
442
- puts "Error: This command must be run from a Rails project root directory"
443
- exit 1
444
- end
566
+ return if File.exist?('config/environment.rb') || File.exist?('config/application.rb')
567
+
568
+ puts 'Error: This command must be run from a Rails project root directory'
569
+ exit 1
445
570
  end
446
571
 
447
572
  def ensure_project_directories
448
573
  # Create tmp/cone directory for socket
449
- socket_dir = File.join(Dir.pwd, "tmp", "cone")
574
+ socket_dir = File.join(Dir.pwd, 'tmp', 'cone')
450
575
  FileUtils.mkdir_p(socket_dir) unless Dir.exist?(socket_dir)
451
-
576
+
452
577
  # Create session directory based on PWD
453
578
  session_dir = project_session_dir
454
579
  FileUtils.mkdir_p(session_dir) unless Dir.exist?(session_dir)
@@ -456,61 +581,61 @@ module Consolle
456
581
 
457
582
  def project_session_dir
458
583
  # Convert PWD to directory name (Claude Code style)
459
- pwd_as_dirname = Dir.pwd.gsub("/", "-")
584
+ pwd_as_dirname = Dir.pwd.gsub('/', '-')
460
585
  File.expand_path("~/.cone/sessions/#{pwd_as_dirname}")
461
586
  end
462
587
 
463
588
  def project_socket_path(target = nil)
464
589
  target ||= options[:target]
465
- File.join(Dir.pwd, "tmp", "cone", "#{target}.socket")
590
+ File.join(Dir.pwd, 'tmp', 'cone', "#{target}.socket")
466
591
  end
467
592
 
468
593
  def project_pid_path(target = nil)
469
594
  target ||= options[:target]
470
- File.join(Dir.pwd, "tmp", "cone", "#{target}.pid")
595
+ File.join(Dir.pwd, 'tmp', 'cone', "#{target}.pid")
471
596
  end
472
597
 
473
598
  def project_log_path(target = nil)
474
599
  target ||= options[:target]
475
- File.join(Dir.pwd, "tmp", "cone", "#{target}.log")
600
+ File.join(Dir.pwd, 'tmp', 'cone', "#{target}.log")
476
601
  end
477
602
 
478
603
  def send_code_to_socket(socket_path, code, timeout: 15)
479
604
  request_id = SecureRandom.uuid
480
605
  request = {
481
- "action" => "eval",
482
- "code" => code,
483
- "timeout" => timeout,
484
- "request_id" => request_id
606
+ 'action' => 'eval',
607
+ 'code' => code,
608
+ 'timeout' => timeout,
609
+ 'request_id' => request_id
485
610
  }
486
611
 
487
612
  Timeout.timeout(timeout + 5) do
488
613
  socket = UNIXSocket.new(socket_path)
489
-
614
+
490
615
  # Send request as single line JSON
491
616
  socket.write(JSON.generate(request))
492
617
  socket.write("\n")
493
618
  socket.flush
494
-
619
+
495
620
  # Read response
496
621
  response_data = socket.gets
497
622
  socket.close
498
-
623
+
499
624
  JSON.parse(response_data)
500
625
  end
501
626
  rescue Timeout::Error
502
- { "success" => false, "error" => "Timeout", "message" => "Request timed out after #{timeout} seconds" }
627
+ { 'success' => false, 'error' => 'Timeout', 'message' => "Request timed out after #{timeout} seconds" }
503
628
  rescue StandardError => e
504
- { "success" => false, "error" => e.class.name, "message" => e.message }
629
+ { 'success' => false, 'error' => e.class.name, 'message' => e.message }
505
630
  end
506
631
 
507
632
  def sessions_file_path
508
- File.join(Dir.pwd, "tmp", "cone", "sessions.json")
633
+ File.join(Dir.pwd, 'tmp', 'cone', 'sessions.json')
509
634
  end
510
635
 
511
- def create_rails_adapter(rails_env = "development", target = nil, command = nil)
636
+ def create_rails_adapter(rails_env = 'development', target = nil, command = nil)
512
637
  target ||= options[:target]
513
-
638
+
514
639
  Consolle::Adapters::RailsConsole.new(
515
640
  socket_path: project_socket_path(target),
516
641
  pid_path: project_pid_path(target),
@@ -524,19 +649,19 @@ module Consolle
524
649
 
525
650
  def save_session_info(adapter)
526
651
  target = options[:target]
527
-
652
+
528
653
  with_sessions_lock do
529
654
  sessions = load_sessions
530
-
655
+
531
656
  sessions[target] = {
532
- "socket_path" => adapter.socket_path,
533
- "process_pid" => adapter.process_pid,
534
- "pid_path" => project_pid_path(target),
535
- "log_path" => project_log_path(target),
536
- "started_at" => Time.now.to_f,
537
- "rails_root" => Dir.pwd
657
+ 'socket_path' => adapter.socket_path,
658
+ 'process_pid' => adapter.process_pid,
659
+ 'pid_path' => project_pid_path(target),
660
+ 'log_path' => project_log_path(target),
661
+ 'started_at' => Time.now.to_f,
662
+ 'rails_root' => Dir.pwd
538
663
  }
539
-
664
+
540
665
  save_sessions(sessions)
541
666
  end
542
667
  end
@@ -544,24 +669,24 @@ module Consolle
544
669
  def load_session_info
545
670
  target = options[:target]
546
671
  sessions = load_sessions
547
-
672
+
548
673
  return nil if sessions.empty?
549
-
674
+
550
675
  session = sessions[target]
551
676
  return nil unless session
552
-
677
+
553
678
  # Convert to symbolized keys for backward compatibility
554
679
  {
555
- socket_path: session["socket_path"],
556
- process_pid: session["process_pid"],
557
- started_at: session["started_at"],
558
- rails_root: session["rails_root"]
680
+ socket_path: session['socket_path'],
681
+ process_pid: session['process_pid'],
682
+ started_at: session['started_at'],
683
+ rails_root: session['rails_root']
559
684
  }
560
685
  end
561
686
 
562
687
  def clear_session_info
563
688
  target = options[:target]
564
-
689
+
565
690
  with_sessions_lock do
566
691
  sessions = load_sessions
567
692
  sessions.delete(target)
@@ -572,21 +697,21 @@ module Consolle
572
697
  def log_session_activity(process_pid, code, result)
573
698
  # Create log filename based on date and PID
574
699
  log_file = File.join(project_session_dir, "session_#{Date.today.strftime('%Y%m%d')}_pid#{process_pid}.log")
575
-
700
+
576
701
  # Create log entry
577
702
  log_entry = {
578
703
  timestamp: Time.now.iso8601,
579
- request_id: result["request_id"],
704
+ request_id: result['request_id'],
580
705
  code: code,
581
- success: result["success"],
582
- result: result["result"],
583
- error: result["error"],
584
- message: result["message"],
585
- execution_time: result["execution_time"]
706
+ success: result['success'],
707
+ result: result['result'],
708
+ error: result['error'],
709
+ message: result['message'],
710
+ execution_time: result['execution_time']
586
711
  }
587
-
712
+
588
713
  # Append to log file
589
- File.open(log_file, "a") do |f|
714
+ File.open(log_file, 'a') do |f|
590
715
  f.puts JSON.generate(log_entry)
591
716
  end
592
717
  rescue StandardError => e
@@ -597,38 +722,38 @@ module Consolle
597
722
  def log_session_event(process_pid, event_type, details = {})
598
723
  # Create log filename based on date and PID
599
724
  log_file = File.join(project_session_dir, "session_#{Date.today.strftime('%Y%m%d')}_pid#{process_pid}.log")
600
-
725
+
601
726
  # Create log entry
602
727
  log_entry = {
603
728
  timestamp: Time.now.iso8601,
604
729
  event: event_type
605
730
  }.merge(details)
606
-
731
+
607
732
  # Append to log file
608
- File.open(log_file, "a") do |f|
733
+ File.open(log_file, 'a') do |f|
609
734
  f.puts JSON.generate(log_entry)
610
735
  end
611
736
  rescue StandardError => e
612
737
  # Log errors should not crash the command
613
738
  puts "Warning: Failed to log session event: #{e.message}" if options[:verbose]
614
739
  end
615
-
740
+
616
741
  def load_sessions
617
742
  # Check for legacy session.json file first
618
- legacy_file = File.join(Dir.pwd, "tmp", "cone", "session.json")
743
+ legacy_file = File.join(Dir.pwd, 'tmp', 'cone', 'session.json')
619
744
  if File.exist?(legacy_file) && !File.exist?(sessions_file_path)
620
745
  # Migrate from old format
621
746
  migrate_legacy_session(legacy_file)
622
747
  end
623
-
748
+
624
749
  return {} unless File.exist?(sessions_file_path)
625
-
750
+
626
751
  json = JSON.parse(File.read(sessions_file_path))
627
-
752
+
628
753
  # Handle backward compatibility with old single-session format
629
- if json.key?("socket_path")
754
+ if json.key?('socket_path')
630
755
  # Legacy single session format - convert to new format
631
- { "cone" => json }
756
+ { 'cone' => json }
632
757
  else
633
758
  # New multi-session format
634
759
  json
@@ -636,35 +761,35 @@ module Consolle
636
761
  rescue JSON::ParserError, Errno::ENOENT
637
762
  {}
638
763
  end
639
-
764
+
640
765
  def migrate_legacy_session(legacy_file)
641
766
  legacy_data = JSON.parse(File.read(legacy_file))
642
-
767
+
643
768
  # Convert to new format
644
769
  new_sessions = {
645
- "_schema" => 1,
646
- "cone" => legacy_data
770
+ '_schema' => 1,
771
+ 'cone' => legacy_data
647
772
  }
648
-
773
+
649
774
  # Write new format
650
775
  File.write(sessions_file_path, JSON.pretty_generate(new_sessions))
651
-
776
+
652
777
  # Remove old file
653
778
  File.delete(legacy_file)
654
-
655
- puts "Migrated session data to new multi-session format" if options[:verbose]
779
+
780
+ puts 'Migrated session data to new multi-session format' if options[:verbose]
656
781
  rescue StandardError => e
657
782
  puts "Warning: Failed to migrate legacy session: #{e.message}" if options[:verbose]
658
783
  end
659
-
784
+
660
785
  def save_sessions(sessions)
661
786
  # Add schema version for future migrations
662
- sessions_with_schema = { "_schema" => 1 }.merge(sessions)
663
-
787
+ sessions_with_schema = { '_schema' => 1 }.merge(sessions)
788
+
664
789
  # Write to temp file first for atomicity - use PID to avoid conflicts
665
790
  temp_path = "#{sessions_file_path}.tmp.#{Process.pid}"
666
791
  File.write(temp_path, JSON.pretty_generate(sessions_with_schema))
667
-
792
+
668
793
  # Atomic rename - will overwrite existing file
669
794
  File.rename(temp_path, sessions_file_path)
670
795
  rescue StandardError => e
@@ -672,47 +797,47 @@ module Consolle
672
797
  File.unlink(temp_path) if File.exist?(temp_path)
673
798
  raise e
674
799
  end
675
-
676
- def with_sessions_lock(&block)
800
+
801
+ def with_sessions_lock
677
802
  # Ensure directory exists
678
803
  FileUtils.mkdir_p(File.dirname(sessions_file_path))
679
-
804
+
680
805
  # Create lock file separate from sessions file to avoid issues
681
806
  lock_file_path = "#{sessions_file_path}.lock"
682
-
807
+
683
808
  # Use file locking to prevent concurrent access
684
- File.open(lock_file_path, File::RDWR | File::CREAT, 0644) do |f|
809
+ File.open(lock_file_path, File::RDWR | File::CREAT, 0o644) do |f|
685
810
  f.flock(File::LOCK_EX)
686
-
811
+
687
812
  # Execute the block
688
813
  yield
689
814
  ensure
690
815
  f.flock(File::LOCK_UN)
691
816
  end
692
817
  end
693
-
818
+
694
819
  def process_alive?(pid)
695
820
  return false unless pid
696
-
821
+
697
822
  Process.kill(0, pid)
698
823
  true
699
824
  rescue Errno::ESRCH, Errno::EPERM
700
825
  false
701
826
  end
702
-
827
+
703
828
  def validate_session_name!(name)
704
829
  # Allow alphanumeric, hyphen, and underscore only
705
830
  unless name.match?(/\A[a-zA-Z0-9_-]+\z/)
706
831
  puts "Error: Invalid session name '#{name}'"
707
- puts "Session names can only contain letters, numbers, hyphens (-), and underscores (_)"
832
+ puts 'Session names can only contain letters, numbers, hyphens (-), and underscores (_)'
708
833
  exit 1
709
834
  end
710
-
835
+
711
836
  # Check length (reasonable limit)
712
- if name.length > 50
713
- puts "Error: Session name is too long (maximum 50 characters)"
714
- exit 1
715
- end
837
+ return unless name.length > 50
838
+
839
+ puts 'Error: Session name is too long (maximum 50 characters)'
840
+ exit 1
716
841
  end
717
842
  end
718
- end
843
+ end