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.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/Gemfile +2 -2
- data/Gemfile.lock +1 -1
- data/bin/cone +2 -2
- data/bin/consolle +2 -2
- data/consolle.gemspec +20 -20
- data/lib/consolle/adapters/rails_console.rb +82 -77
- data/lib/consolle/cli.rb +380 -255
- data/lib/consolle/server/console_socket_server.rb +79 -72
- data/lib/consolle/server/console_supervisor.rb +194 -174
- data/lib/consolle/server/request_broker.rb +96 -99
- data/lib/consolle/version.rb +2 -2
- data/lib/consolle.rb +6 -6
- metadata +3 -3
data/lib/consolle/cli.rb
CHANGED
|
@@ -1,18 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require_relative
|
|
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
|
-
|
|
15
|
-
|
|
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?(
|
|
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
|
|
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
|
|
89
|
+
desc 'version', 'Show consolle version'
|
|
45
90
|
def version
|
|
46
91
|
puts "Consolle version #{Consolle::VERSION}"
|
|
47
92
|
end
|
|
48
93
|
|
|
49
|
-
|
|
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(
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
94
|
-
|
|
160
|
+
puts 'Starting Rails console...'
|
|
161
|
+
|
|
95
162
|
begin
|
|
96
163
|
adapter.start
|
|
97
|
-
puts
|
|
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,
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
|
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(
|
|
129
|
-
server_status =
|
|
130
|
-
|
|
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[
|
|
134
|
-
console_pid = server_status[
|
|
135
|
-
|
|
136
|
-
puts
|
|
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
|
|
211
|
+
puts ' Ready for input: Yes'
|
|
141
212
|
else
|
|
142
|
-
puts
|
|
213
|
+
puts '✗ Rails console is not running'
|
|
143
214
|
clear_session_info
|
|
144
215
|
end
|
|
145
216
|
end
|
|
146
217
|
|
|
147
|
-
desc
|
|
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?(
|
|
154
|
-
puts
|
|
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 ==
|
|
163
|
-
|
|
248
|
+
next if name == '_schema' # Skip schema field
|
|
249
|
+
|
|
164
250
|
# Check if process is alive
|
|
165
|
-
if info[
|
|
251
|
+
if info['process_pid'] && process_alive?(info['process_pid'])
|
|
166
252
|
# Try to get server status
|
|
167
|
-
adapter = create_rails_adapter(
|
|
168
|
-
server_status =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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
|
|
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(
|
|
204
|
-
|
|
292
|
+
|
|
293
|
+
adapter = create_rails_adapter('development', options[:target])
|
|
294
|
+
|
|
205
295
|
if adapter.running?
|
|
206
|
-
puts
|
|
207
|
-
|
|
296
|
+
puts 'Stopping Rails console...'
|
|
297
|
+
|
|
208
298
|
if adapter.stop
|
|
209
|
-
puts
|
|
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],
|
|
215
|
-
|
|
216
|
-
|
|
304
|
+
log_session_event(session_info[:process_pid], 'session_stop', {
|
|
305
|
+
reason: 'user_requested'
|
|
306
|
+
})
|
|
217
307
|
end
|
|
218
308
|
else
|
|
219
|
-
puts
|
|
309
|
+
puts '✗ Failed to stop Rails console'
|
|
220
310
|
end
|
|
221
311
|
else
|
|
222
|
-
puts
|
|
312
|
+
puts 'Rails console is not running'
|
|
223
313
|
end
|
|
224
|
-
|
|
314
|
+
|
|
225
315
|
clear_session_info
|
|
226
316
|
end
|
|
227
317
|
|
|
228
|
-
desc
|
|
229
|
-
map [
|
|
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 ==
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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(
|
|
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[
|
|
265
|
-
log_session_event(info[
|
|
266
|
-
|
|
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
|
|
285
|
-
|
|
286
|
-
|
|
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 =
|
|
296
|
-
|
|
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
|
|
409
|
+
puts 'Performing full server restart...'
|
|
303
410
|
else
|
|
304
|
-
puts
|
|
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
|
|
318
|
-
|
|
424
|
+
puts 'Restarting Rails console subprocess...'
|
|
425
|
+
|
|
319
426
|
# Send restart request to the socket server
|
|
320
427
|
request = {
|
|
321
|
-
|
|
322
|
-
|
|
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[
|
|
337
|
-
puts
|
|
338
|
-
puts " New PID: #{response[
|
|
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[
|
|
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
|
|
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
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
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
|
-
|
|
390
|
-
|
|
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[
|
|
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
|
|
409
|
-
puts
|
|
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[
|
|
550
|
+
|
|
551
|
+
if result['success']
|
|
427
552
|
# Always print result, even if empty (multiline code often returns empty string)
|
|
428
|
-
puts result[
|
|
429
|
-
puts "Execution time: #{result[
|
|
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[
|
|
432
|
-
puts result[
|
|
433
|
-
puts result[
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
{
|
|
627
|
+
{ 'success' => false, 'error' => 'Timeout', 'message' => "Request timed out after #{timeout} seconds" }
|
|
503
628
|
rescue StandardError => e
|
|
504
|
-
{
|
|
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,
|
|
633
|
+
File.join(Dir.pwd, 'tmp', 'cone', 'sessions.json')
|
|
509
634
|
end
|
|
510
635
|
|
|
511
|
-
def create_rails_adapter(rails_env =
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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[
|
|
556
|
-
process_pid: session[
|
|
557
|
-
started_at: session[
|
|
558
|
-
rails_root: session[
|
|
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[
|
|
704
|
+
request_id: result['request_id'],
|
|
580
705
|
code: code,
|
|
581
|
-
success: result[
|
|
582
|
-
result: result[
|
|
583
|
-
error: result[
|
|
584
|
-
message: result[
|
|
585
|
-
execution_time: result[
|
|
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,
|
|
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,
|
|
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,
|
|
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?(
|
|
754
|
+
if json.key?('socket_path')
|
|
630
755
|
# Legacy single session format - convert to new format
|
|
631
|
-
{
|
|
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
|
-
|
|
646
|
-
|
|
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
|
|
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 = {
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|