consolle 0.3.8 → 0.4.1
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.lock +1 -1
- data/lib/consolle/cli.rb +478 -118
- data/lib/consolle/history.rb +210 -0
- data/lib/consolle/session_registry.rb +327 -0
- data/rule.ko.md +93 -5
- data/rule.md +93 -5
- metadata +4 -2
data/lib/consolle/cli.rb
CHANGED
|
@@ -8,9 +8,60 @@ require 'timeout'
|
|
|
8
8
|
require 'securerandom'
|
|
9
9
|
require 'date'
|
|
10
10
|
require_relative 'constants'
|
|
11
|
+
require_relative 'session_registry'
|
|
12
|
+
require_relative 'history'
|
|
11
13
|
require_relative 'adapters/rails_console'
|
|
12
14
|
|
|
13
15
|
module Consolle
|
|
16
|
+
# Rails convenience commands subcommand
|
|
17
|
+
class RailsCommands < Thor
|
|
18
|
+
namespace :rails
|
|
19
|
+
|
|
20
|
+
class_option :verbose, type: :boolean, aliases: '-v', desc: 'Verbose output'
|
|
21
|
+
class_option :target, type: :string, aliases: '-t', desc: 'Target session name', default: 'cone'
|
|
22
|
+
|
|
23
|
+
desc 'reload', 'Reload Rails application code (reload!)'
|
|
24
|
+
def reload
|
|
25
|
+
execute_rails_code('reload!')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc 'env', 'Show current Rails environment'
|
|
29
|
+
def env
|
|
30
|
+
execute_rails_code('Rails.env')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc 'db', 'Show database connection information'
|
|
34
|
+
def db
|
|
35
|
+
code = <<~RUBY
|
|
36
|
+
config = ActiveRecord::Base.connection_db_config
|
|
37
|
+
puts "Adapter: \#{config.adapter}"
|
|
38
|
+
puts "Database: \#{config.database}"
|
|
39
|
+
puts "Host: \#{config.host || 'localhost'}" if config.respond_to?(:host)
|
|
40
|
+
puts "Pool: \#{config.pool}" if config.respond_to?(:pool)
|
|
41
|
+
puts "Connected: \#{ActiveRecord::Base.connected?}"
|
|
42
|
+
nil
|
|
43
|
+
RUBY
|
|
44
|
+
execute_rails_code(code)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def execute_rails_code(code)
|
|
50
|
+
# Delegate to main CLI's exec command
|
|
51
|
+
cli = Consolle::CLI.new
|
|
52
|
+
cli.options = {
|
|
53
|
+
target: options[:target] || 'cone',
|
|
54
|
+
verbose: options[:verbose] || false,
|
|
55
|
+
timeout: 60,
|
|
56
|
+
raw: false
|
|
57
|
+
}
|
|
58
|
+
cli.exec(code)
|
|
59
|
+
rescue SystemExit
|
|
60
|
+
# Allow exit from exec
|
|
61
|
+
raise
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
14
65
|
class CLI < Thor
|
|
15
66
|
package_name 'Consolle'
|
|
16
67
|
|
|
@@ -38,7 +89,11 @@ module Consolle
|
|
|
38
89
|
shell.say ' cone restart # Restart Rails console'
|
|
39
90
|
shell.say ' cone status # Show Rails console status'
|
|
40
91
|
shell.say ' cone exec CODE # Execute Ruby code in Rails console'
|
|
41
|
-
shell.say ' cone
|
|
92
|
+
shell.say ' cone rails SUBCOMMAND # Rails convenience commands'
|
|
93
|
+
shell.say ' cone ls # List active sessions (use -a for all)'
|
|
94
|
+
shell.say ' cone history # Show command history'
|
|
95
|
+
shell.say ' cone rm SESSION # Remove session and its history'
|
|
96
|
+
shell.say ' cone prune # Remove all stopped sessions'
|
|
42
97
|
shell.say ' cone stop_all # Stop all Rails console sessions'
|
|
43
98
|
shell.say ' cone rule FILE # Write cone command guide to FILE'
|
|
44
99
|
shell.say ' cone version # Show version'
|
|
@@ -66,6 +121,10 @@ module Consolle
|
|
|
66
121
|
class_option :verbose, type: :boolean, aliases: '-v', desc: 'Verbose output'
|
|
67
122
|
class_option :target, type: :string, aliases: '-t', desc: 'Target session name', default: 'cone'
|
|
68
123
|
|
|
124
|
+
# Register rails subcommand
|
|
125
|
+
desc 'rails SUBCOMMAND', 'Rails convenience commands (reload, env, db)'
|
|
126
|
+
subcommand 'rails', RailsCommands
|
|
127
|
+
|
|
69
128
|
def self.exit_on_failure?
|
|
70
129
|
true
|
|
71
130
|
end
|
|
@@ -182,18 +241,25 @@ module Consolle
|
|
|
182
241
|
|
|
183
242
|
begin
|
|
184
243
|
adapter.start
|
|
185
|
-
|
|
244
|
+
|
|
245
|
+
# Register session in registry
|
|
246
|
+
session = session_registry.create_session(
|
|
247
|
+
target: options[:target],
|
|
248
|
+
socket_path: adapter.socket_path,
|
|
249
|
+
pid: adapter.process_pid,
|
|
250
|
+
rails_env: current_rails_env,
|
|
251
|
+
mode: options[:mode] || 'pty'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
puts '✓ Rails console started'
|
|
255
|
+
puts " Session ID: #{session['id']} (#{session['short_id']})"
|
|
256
|
+
puts " Target: #{session['target']}"
|
|
257
|
+
puts " Environment: #{current_rails_env}"
|
|
186
258
|
puts " PID: #{adapter.process_pid}"
|
|
187
259
|
puts " Socket: #{adapter.socket_path}"
|
|
188
260
|
|
|
189
|
-
#
|
|
190
|
-
save_session_info(adapter)
|
|
191
|
-
|
|
192
|
-
# Log session start
|
|
193
|
-
log_session_event(adapter.process_pid, 'session_start', {
|
|
194
|
-
rails_env: current_rails_env,
|
|
195
|
-
socket_path: adapter.socket_path
|
|
196
|
-
})
|
|
261
|
+
# Also save to legacy sessions.json for backward compatibility
|
|
262
|
+
save_session_info(adapter, session['id'])
|
|
197
263
|
rescue StandardError => e
|
|
198
264
|
puts "✗ Failed to start Rails console: #{e.message}"
|
|
199
265
|
exit 1
|
|
@@ -205,9 +271,11 @@ module Consolle
|
|
|
205
271
|
ensure_rails_project!
|
|
206
272
|
validate_session_name!(options[:target])
|
|
207
273
|
|
|
274
|
+
# Try to find session in registry first
|
|
275
|
+
session = session_registry.find_running_session(target: options[:target])
|
|
208
276
|
session_info = load_session_info
|
|
209
277
|
|
|
210
|
-
if session_info.nil?
|
|
278
|
+
if session_info.nil? && session.nil?
|
|
211
279
|
puts 'No active Rails console session found'
|
|
212
280
|
return
|
|
213
281
|
end
|
|
@@ -224,85 +292,141 @@ module Consolle
|
|
|
224
292
|
if process_running
|
|
225
293
|
rails_env = server_status['rails_env'] || 'unknown'
|
|
226
294
|
console_pid = server_status['pid'] || 'unknown'
|
|
295
|
+
uptime = session_info&.dig(:started_at) ? format_uptime(Time.now - Time.at(session_info[:started_at])) : 'unknown'
|
|
296
|
+
command_count = session ? session['command_count'] : 0
|
|
227
297
|
|
|
228
298
|
puts '✓ Rails console is running'
|
|
229
|
-
|
|
299
|
+
if session
|
|
300
|
+
puts " Session ID: #{session['id']} (#{session['short_id']})"
|
|
301
|
+
end
|
|
302
|
+
puts " Target: #{options[:target]}"
|
|
230
303
|
puts " Environment: #{rails_env}"
|
|
231
|
-
puts "
|
|
232
|
-
puts
|
|
304
|
+
puts " PID: #{console_pid}"
|
|
305
|
+
puts " Uptime: #{uptime}"
|
|
306
|
+
puts " Commands: #{command_count}"
|
|
307
|
+
puts " Socket: #{session_info&.dig(:socket_path) || session&.dig('socket_path')}"
|
|
233
308
|
else
|
|
234
309
|
puts '✗ Rails console is not running'
|
|
310
|
+
# Mark session as stopped in registry
|
|
311
|
+
session_registry.stop_session(target: options[:target], reason: 'process_died') if session
|
|
235
312
|
clear_session_info
|
|
236
313
|
end
|
|
237
314
|
end
|
|
238
315
|
|
|
239
|
-
desc 'ls', 'List
|
|
316
|
+
desc 'ls', 'List Rails console sessions'
|
|
240
317
|
long_desc <<-LONGDESC
|
|
241
|
-
Lists
|
|
318
|
+
Lists Rails console sessions in the current project.
|
|
319
|
+
|
|
320
|
+
By default, shows only active (running) sessions.
|
|
321
|
+
Use -a/--all to include stopped sessions.
|
|
242
322
|
|
|
243
323
|
Shows information about each session including:
|
|
244
|
-
- Session
|
|
245
|
-
-
|
|
324
|
+
- Session ID (short)
|
|
325
|
+
- Target name
|
|
246
326
|
- Rails environment
|
|
247
327
|
- Status (running/stopped)
|
|
328
|
+
- Uptime or stop time
|
|
329
|
+
- Command count
|
|
248
330
|
|
|
249
331
|
Example output:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
- worker [PID: 12347, ENV: development, STATUS: stopped]
|
|
332
|
+
ID TARGET ENV STATUS UPTIME COMMANDS
|
|
333
|
+
a1b2 cone development running 2h 15m 42
|
|
334
|
+
e5f6 api production running 1h 30m 15
|
|
254
335
|
LONGDESC
|
|
336
|
+
method_option :all, type: :boolean, aliases: '-a', desc: 'Include stopped sessions'
|
|
255
337
|
def ls
|
|
256
338
|
ensure_rails_project!
|
|
257
339
|
|
|
258
|
-
|
|
340
|
+
include_stopped = options[:all]
|
|
341
|
+
sessions = session_registry.list_sessions(include_stopped: include_stopped)
|
|
259
342
|
|
|
260
|
-
|
|
261
|
-
|
|
343
|
+
# Also check legacy sessions.json for backward compatibility
|
|
344
|
+
legacy_sessions = load_sessions
|
|
345
|
+
legacy_sessions.each do |name, info|
|
|
346
|
+
next if name == '_schema'
|
|
347
|
+
next unless info['process_pid'] && process_alive?(info['process_pid'])
|
|
348
|
+
|
|
349
|
+
# Check if already in registry
|
|
350
|
+
existing = sessions.find { |s| s['target'] == name && s['status'] == 'running' }
|
|
351
|
+
next if existing
|
|
352
|
+
|
|
353
|
+
# Add legacy session (will be migrated on next start)
|
|
354
|
+
sessions << {
|
|
355
|
+
'short_id' => '----',
|
|
356
|
+
'target' => name,
|
|
357
|
+
'rails_env' => 'development',
|
|
358
|
+
'status' => 'running',
|
|
359
|
+
'pid' => info['process_pid'],
|
|
360
|
+
'created_at' => info['started_at'] ? Time.at(info['started_at']).iso8601 : Time.now.iso8601,
|
|
361
|
+
'command_count' => 0,
|
|
362
|
+
'_legacy' => true
|
|
363
|
+
}
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
if sessions.empty?
|
|
367
|
+
if include_stopped
|
|
368
|
+
puts 'No sessions found'
|
|
369
|
+
else
|
|
370
|
+
puts 'No active sessions'
|
|
371
|
+
puts "Use 'cone ls -a' to see stopped sessions"
|
|
372
|
+
end
|
|
262
373
|
return
|
|
263
374
|
end
|
|
264
375
|
|
|
265
|
-
|
|
266
|
-
|
|
376
|
+
# Verify running sessions are actually running
|
|
377
|
+
sessions.each do |session|
|
|
378
|
+
next unless session['status'] == 'running'
|
|
379
|
+
next if session['_legacy']
|
|
380
|
+
|
|
381
|
+
unless session['pid'] && process_alive?(session['pid'])
|
|
382
|
+
session_registry.stop_session(session_id: session['id'], reason: 'process_died')
|
|
383
|
+
session['status'] = 'stopped'
|
|
384
|
+
end
|
|
385
|
+
end
|
|
267
386
|
|
|
268
|
-
|
|
269
|
-
|
|
387
|
+
# Re-filter if needed
|
|
388
|
+
sessions = sessions.select { |s| s['status'] == 'running' } unless include_stopped
|
|
270
389
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
adapter.get_status
|
|
277
|
-
rescue StandardError
|
|
278
|
-
nil
|
|
279
|
-
end
|
|
390
|
+
if sessions.empty?
|
|
391
|
+
puts 'No active sessions'
|
|
392
|
+
puts "Use 'cone ls -a' to see stopped sessions"
|
|
393
|
+
return
|
|
394
|
+
end
|
|
280
395
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
396
|
+
# Display header
|
|
397
|
+
if include_stopped
|
|
398
|
+
puts 'ALL SESSIONS:'
|
|
399
|
+
else
|
|
400
|
+
puts 'ACTIVE SESSIONS:'
|
|
401
|
+
end
|
|
402
|
+
puts
|
|
403
|
+
puts format(' %-8s %-12s %-12s %-9s %-10s %s', 'ID', 'TARGET', 'ENV', 'STATUS', 'UPTIME', 'COMMANDS')
|
|
404
|
+
|
|
405
|
+
sessions.each do |session|
|
|
406
|
+
short_id = session['short_id'] || session['id']&.[](0, 4) || '----'
|
|
407
|
+
target = session['target'] || 'unknown'
|
|
408
|
+
env = session['rails_env'] || 'dev'
|
|
409
|
+
status = session['status'] || 'unknown'
|
|
410
|
+
commands = session['command_count'] || 0
|
|
411
|
+
|
|
412
|
+
if session['status'] == 'running'
|
|
413
|
+
started = session['started_at'] || session['created_at']
|
|
414
|
+
uptime = started ? format_uptime(Time.now - Time.parse(started)) : '---'
|
|
288
415
|
else
|
|
289
|
-
|
|
416
|
+
stopped = session['stopped_at']
|
|
417
|
+
uptime = stopped ? format_time_ago(Time.now - Time.parse(stopped)) : '---'
|
|
290
418
|
end
|
|
291
|
-
end
|
|
292
419
|
|
|
293
|
-
|
|
294
|
-
if stale_sessions.any?
|
|
295
|
-
with_sessions_lock do
|
|
296
|
-
sessions = load_sessions
|
|
297
|
-
stale_sessions.each { |name| sessions.delete(name) }
|
|
298
|
-
save_sessions(sessions)
|
|
299
|
-
end
|
|
420
|
+
puts format(' %-8s %-12s %-12s %-9s %-10s %d', short_id, target, env, status, uptime, commands)
|
|
300
421
|
end
|
|
301
422
|
|
|
302
|
-
|
|
303
|
-
|
|
423
|
+
puts
|
|
424
|
+
if include_stopped
|
|
425
|
+
puts "Use 'cone history --session ID' to view session history"
|
|
426
|
+
puts "Use 'cone rm ID' to remove session and history"
|
|
304
427
|
else
|
|
305
|
-
|
|
428
|
+
puts 'Usage: cone exec -t TARGET CODE'
|
|
429
|
+
puts ' cone exec --session ID CODE'
|
|
306
430
|
end
|
|
307
431
|
end
|
|
308
432
|
|
|
@@ -319,18 +443,15 @@ module Consolle
|
|
|
319
443
|
if adapter.stop
|
|
320
444
|
puts '✓ Rails console stopped'
|
|
321
445
|
|
|
322
|
-
#
|
|
323
|
-
|
|
324
|
-
if session_info && session_info[:process_pid]
|
|
325
|
-
log_session_event(session_info[:process_pid], 'session_stop', {
|
|
326
|
-
reason: 'user_requested'
|
|
327
|
-
})
|
|
328
|
-
end
|
|
446
|
+
# Mark session as stopped in registry (preserves history)
|
|
447
|
+
session_registry.stop_session(target: options[:target], reason: 'user_requested')
|
|
329
448
|
else
|
|
330
449
|
puts '✗ Failed to stop Rails console'
|
|
331
450
|
end
|
|
332
451
|
else
|
|
333
452
|
puts 'Rails console is not running'
|
|
453
|
+
# Mark as stopped anyway in case registry is out of sync
|
|
454
|
+
session_registry.stop_session(target: options[:target], reason: 'not_running')
|
|
334
455
|
end
|
|
335
456
|
|
|
336
457
|
clear_session_info
|
|
@@ -341,27 +462,31 @@ module Consolle
|
|
|
341
462
|
def stop_all
|
|
342
463
|
ensure_rails_project!
|
|
343
464
|
|
|
344
|
-
sessions
|
|
345
|
-
|
|
465
|
+
# Get running sessions from registry
|
|
466
|
+
running_sessions = session_registry.list_sessions(include_stopped: false)
|
|
346
467
|
|
|
347
|
-
#
|
|
348
|
-
|
|
468
|
+
# Also check legacy sessions
|
|
469
|
+
legacy_sessions = load_sessions
|
|
470
|
+
legacy_sessions.each do |name, info|
|
|
349
471
|
next if name == '_schema'
|
|
472
|
+
next unless info['process_pid'] && process_alive?(info['process_pid'])
|
|
473
|
+
|
|
474
|
+
existing = running_sessions.find { |s| s['target'] == name }
|
|
475
|
+
next if existing
|
|
350
476
|
|
|
351
|
-
|
|
477
|
+
running_sessions << { 'target' => name, 'pid' => info['process_pid'], '_legacy' => true }
|
|
352
478
|
end
|
|
353
479
|
|
|
354
|
-
if
|
|
480
|
+
if running_sessions.empty?
|
|
355
481
|
puts 'No active sessions to stop'
|
|
356
482
|
return
|
|
357
483
|
end
|
|
358
484
|
|
|
359
|
-
puts "Found #{
|
|
485
|
+
puts "Found #{running_sessions.size} active session(s)"
|
|
360
486
|
|
|
361
487
|
# Stop each active session
|
|
362
|
-
|
|
363
|
-
name = session[
|
|
364
|
-
info = session[:info]
|
|
488
|
+
running_sessions.each do |session|
|
|
489
|
+
name = session['target']
|
|
365
490
|
|
|
366
491
|
puts "\nStopping session '#{name}'..."
|
|
367
492
|
|
|
@@ -370,14 +495,10 @@ module Consolle
|
|
|
370
495
|
if adapter.stop
|
|
371
496
|
puts "✓ Session '#{name}' stopped"
|
|
372
497
|
|
|
373
|
-
#
|
|
374
|
-
|
|
375
|
-
log_session_event(info['process_pid'], 'session_stop', {
|
|
376
|
-
reason: 'stop_all_requested'
|
|
377
|
-
})
|
|
378
|
-
end
|
|
498
|
+
# Mark session as stopped in registry
|
|
499
|
+
session_registry.stop_session(target: name, reason: 'stop_all_requested') unless session['_legacy']
|
|
379
500
|
|
|
380
|
-
# Clear
|
|
501
|
+
# Clear from legacy sessions.json
|
|
381
502
|
with_sessions_lock do
|
|
382
503
|
sessions = load_sessions
|
|
383
504
|
sessions.delete(name)
|
|
@@ -598,6 +719,211 @@ module Consolle
|
|
|
598
719
|
end
|
|
599
720
|
end
|
|
600
721
|
|
|
722
|
+
desc 'rm SESSION_ID', 'Remove session and its history'
|
|
723
|
+
long_desc <<-LONGDESC
|
|
724
|
+
Removes a stopped session and all its history.
|
|
725
|
+
|
|
726
|
+
The SESSION_ID can be:
|
|
727
|
+
- Full session ID (8 characters, e.g., a1b2c3d4)
|
|
728
|
+
- Short session ID (4 characters, e.g., a1b2)
|
|
729
|
+
- Target name (e.g., cone, api)
|
|
730
|
+
|
|
731
|
+
Running sessions cannot be removed. Stop them first with 'cone stop -t TARGET'.
|
|
732
|
+
|
|
733
|
+
Use -f/--force to skip confirmation prompt.
|
|
734
|
+
Use -f/--force with a running session to stop and remove it.
|
|
735
|
+
|
|
736
|
+
Examples:
|
|
737
|
+
cone rm a1b2 # Remove by short ID
|
|
738
|
+
cone rm a1b2c3d4 # Remove by full ID
|
|
739
|
+
cone rm -f a1b2 # Remove without confirmation
|
|
740
|
+
LONGDESC
|
|
741
|
+
method_option :force, type: :boolean, aliases: '-f', desc: 'Skip confirmation (or force stop running session)'
|
|
742
|
+
def rm(session_id)
|
|
743
|
+
ensure_rails_project!
|
|
744
|
+
|
|
745
|
+
# Try to find session
|
|
746
|
+
session = session_registry.find_session(session_id: session_id) ||
|
|
747
|
+
session_registry.find_session(target: session_id)
|
|
748
|
+
|
|
749
|
+
unless session
|
|
750
|
+
puts "✗ Session not found: #{session_id}"
|
|
751
|
+
puts "Use 'cone ls -a' to see all sessions"
|
|
752
|
+
exit 1
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# Check if running
|
|
756
|
+
if session['status'] == 'running'
|
|
757
|
+
if options[:force]
|
|
758
|
+
# Force stop first
|
|
759
|
+
puts "Stopping running session '#{session['target']}'..."
|
|
760
|
+
adapter = create_rails_adapter('development', session['target'])
|
|
761
|
+
adapter.stop
|
|
762
|
+
session_registry.stop_session(session_id: session['id'], reason: 'force_remove')
|
|
763
|
+
clear_session_info if options[:target] == session['target']
|
|
764
|
+
else
|
|
765
|
+
puts "✗ Session #{session['short_id']} (#{session['target']}) is still running"
|
|
766
|
+
puts " Use 'cone stop -t #{session['target']}' first, or 'cone rm -f #{session_id}' to force"
|
|
767
|
+
exit 1
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
# Confirm deletion
|
|
772
|
+
unless options[:force]
|
|
773
|
+
command_count = session['command_count'] || 0
|
|
774
|
+
print "Remove session #{session['id']} (#{session['target']}, #{command_count} commands)?\n"
|
|
775
|
+
print 'This will permanently delete all history. [y/N]: '
|
|
776
|
+
response = $stdin.gets&.strip&.downcase
|
|
777
|
+
unless response == 'y' || response == 'yes'
|
|
778
|
+
puts 'Cancelled'
|
|
779
|
+
return
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
# Remove session
|
|
784
|
+
result = session_registry.remove_session(session_id: session['id'])
|
|
785
|
+
|
|
786
|
+
if result && !result.is_a?(Hash)
|
|
787
|
+
puts "✓ Session #{session['id']} removed"
|
|
788
|
+
else
|
|
789
|
+
puts "✗ Failed to remove session"
|
|
790
|
+
exit 1
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
desc 'prune', 'Remove all stopped sessions'
|
|
795
|
+
long_desc <<-LONGDESC
|
|
796
|
+
Removes all stopped sessions and their history.
|
|
797
|
+
|
|
798
|
+
By default, only removes sessions from the current project.
|
|
799
|
+
|
|
800
|
+
Use --yes to skip confirmation prompt.
|
|
801
|
+
|
|
802
|
+
Examples:
|
|
803
|
+
cone prune # Remove stopped sessions (with confirmation)
|
|
804
|
+
cone prune --yes # Remove without confirmation
|
|
805
|
+
LONGDESC
|
|
806
|
+
method_option :yes, type: :boolean, aliases: '-y', desc: 'Skip confirmation'
|
|
807
|
+
def prune
|
|
808
|
+
ensure_rails_project!
|
|
809
|
+
|
|
810
|
+
stopped = session_registry.list_stopped_sessions
|
|
811
|
+
|
|
812
|
+
if stopped.empty?
|
|
813
|
+
puts 'No stopped sessions to remove'
|
|
814
|
+
return
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# Show what will be removed
|
|
818
|
+
total_commands = stopped.sum { |s| s['command_count'] || 0 }
|
|
819
|
+
|
|
820
|
+
puts "Found #{stopped.size} stopped session(s):"
|
|
821
|
+
stopped.each do |session|
|
|
822
|
+
stopped_at = session['stopped_at'] ? Time.parse(session['stopped_at']).strftime('%Y-%m-%d') : '---'
|
|
823
|
+
commands = session['command_count'] || 0
|
|
824
|
+
puts " #{session['short_id']} #{session['target'].ljust(12)} stopped #{stopped_at} #{commands} commands"
|
|
825
|
+
end
|
|
826
|
+
puts
|
|
827
|
+
|
|
828
|
+
# Confirm
|
|
829
|
+
unless options[:yes]
|
|
830
|
+
print "Remove all stopped sessions and their history? [y/N]: "
|
|
831
|
+
response = $stdin.gets&.strip&.downcase
|
|
832
|
+
unless response == 'y' || response == 'yes'
|
|
833
|
+
puts 'Cancelled'
|
|
834
|
+
return
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
# Remove all stopped sessions
|
|
839
|
+
removed = session_registry.prune_sessions
|
|
840
|
+
|
|
841
|
+
puts "✓ Removed #{removed.size} sessions (#{total_commands} commands)"
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
desc 'history', 'Show command history'
|
|
845
|
+
long_desc <<-LONGDESC
|
|
846
|
+
Shows command history for sessions.
|
|
847
|
+
|
|
848
|
+
By default, shows history from the current active session (target).
|
|
849
|
+
|
|
850
|
+
Options:
|
|
851
|
+
--session ID Show history for specific session (by ID or short ID)
|
|
852
|
+
-t, --target Show history for specific target name
|
|
853
|
+
-n, --limit Limit number of entries shown
|
|
854
|
+
--today Show only today's commands
|
|
855
|
+
--date DATE Show commands from specific date (YYYY-MM-DD)
|
|
856
|
+
--success Show only successful commands
|
|
857
|
+
--failed Show only failed commands
|
|
858
|
+
--grep PATTERN Filter by code or result matching pattern
|
|
859
|
+
--all Include history from stopped sessions with same target
|
|
860
|
+
-v, --verbose Show detailed output
|
|
861
|
+
--json Output as JSON
|
|
862
|
+
|
|
863
|
+
Examples:
|
|
864
|
+
cone history # Current session history
|
|
865
|
+
cone history -t api # History for 'api' target
|
|
866
|
+
cone history --session a1b2 # History for specific session
|
|
867
|
+
cone history -n 10 # Last 10 commands
|
|
868
|
+
cone history --today # Today's commands only
|
|
869
|
+
cone history --failed # Failed commands only
|
|
870
|
+
cone history --grep User # Filter by pattern
|
|
871
|
+
LONGDESC
|
|
872
|
+
method_option :session, type: :string, aliases: '-s', desc: 'Session ID or short ID'
|
|
873
|
+
method_option :limit, type: :numeric, aliases: '-n', desc: 'Limit number of entries'
|
|
874
|
+
method_option :today, type: :boolean, desc: 'Show only today'
|
|
875
|
+
method_option :date, type: :string, desc: 'Show specific date (YYYY-MM-DD)'
|
|
876
|
+
method_option :success, type: :boolean, desc: 'Show only successful commands'
|
|
877
|
+
method_option :failed, type: :boolean, desc: 'Show only failed commands'
|
|
878
|
+
method_option :grep, type: :string, aliases: '-g', desc: 'Filter by pattern'
|
|
879
|
+
method_option :all, type: :boolean, desc: 'Include stopped sessions'
|
|
880
|
+
method_option :json, type: :boolean, desc: 'Output as JSON'
|
|
881
|
+
def history
|
|
882
|
+
ensure_rails_project!
|
|
883
|
+
|
|
884
|
+
history_manager = Consolle::History.new
|
|
885
|
+
|
|
886
|
+
entries = history_manager.query(
|
|
887
|
+
session_id: options[:session],
|
|
888
|
+
target: options[:target],
|
|
889
|
+
limit: options[:limit],
|
|
890
|
+
today: options[:today],
|
|
891
|
+
date: options[:date],
|
|
892
|
+
success_only: options[:success],
|
|
893
|
+
failed_only: options[:failed],
|
|
894
|
+
grep: options[:grep],
|
|
895
|
+
all_sessions: options[:all]
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
if entries.empty?
|
|
899
|
+
puts 'No history found'
|
|
900
|
+
if options[:session] || options[:target]
|
|
901
|
+
puts "Try 'cone history' without filters to see all history"
|
|
902
|
+
else
|
|
903
|
+
puts "Execute some commands first with 'cone exec'"
|
|
904
|
+
end
|
|
905
|
+
return
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
if options[:json]
|
|
909
|
+
puts history_manager.format_json(entries)
|
|
910
|
+
elsif options[:verbose]
|
|
911
|
+
entries.each do |entry|
|
|
912
|
+
puts history_manager.format_entry_verbose(entry)
|
|
913
|
+
puts
|
|
914
|
+
end
|
|
915
|
+
else
|
|
916
|
+
entries.each do |entry|
|
|
917
|
+
puts history_manager.format_entry(entry)
|
|
918
|
+
puts
|
|
919
|
+
end
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
unless options[:json]
|
|
923
|
+
puts "Showing #{entries.size} entries"
|
|
924
|
+
end
|
|
925
|
+
end
|
|
926
|
+
|
|
601
927
|
private
|
|
602
928
|
|
|
603
929
|
def current_rails_env
|
|
@@ -734,7 +1060,7 @@ module Consolle
|
|
|
734
1060
|
)
|
|
735
1061
|
end
|
|
736
1062
|
|
|
737
|
-
def save_session_info(adapter)
|
|
1063
|
+
def save_session_info(adapter, session_id = nil)
|
|
738
1064
|
target = options[:target]
|
|
739
1065
|
|
|
740
1066
|
with_sessions_lock do
|
|
@@ -746,7 +1072,8 @@ module Consolle
|
|
|
746
1072
|
'pid_path' => project_pid_path(target),
|
|
747
1073
|
'log_path' => project_log_path(target),
|
|
748
1074
|
'started_at' => Time.now.to_f,
|
|
749
|
-
'rails_root' => Dir.pwd
|
|
1075
|
+
'rails_root' => Dir.pwd,
|
|
1076
|
+
'session_id' => session_id
|
|
750
1077
|
}
|
|
751
1078
|
|
|
752
1079
|
save_sessions(sessions)
|
|
@@ -767,7 +1094,8 @@ module Consolle
|
|
|
767
1094
|
socket_path: session['socket_path'],
|
|
768
1095
|
process_pid: session['process_pid'],
|
|
769
1096
|
started_at: session['started_at'],
|
|
770
|
-
rails_root: session['rails_root']
|
|
1097
|
+
rails_root: session['rails_root'],
|
|
1098
|
+
session_id: session['session_id']
|
|
771
1099
|
}
|
|
772
1100
|
end
|
|
773
1101
|
|
|
@@ -782,47 +1110,44 @@ module Consolle
|
|
|
782
1110
|
end
|
|
783
1111
|
|
|
784
1112
|
def log_session_activity(process_pid, code, result)
|
|
785
|
-
#
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1113
|
+
# Try to use new History class if session_id is available
|
|
1114
|
+
session_info = load_session_info
|
|
1115
|
+
if session_info&.dig(:session_id)
|
|
1116
|
+
history_manager = Consolle::History.new
|
|
1117
|
+
history_manager.log_command(
|
|
1118
|
+
session_id: session_info[:session_id],
|
|
1119
|
+
target: options[:target],
|
|
1120
|
+
code: code,
|
|
1121
|
+
result: result
|
|
1122
|
+
)
|
|
1123
|
+
else
|
|
1124
|
+
# Fallback to legacy logging
|
|
1125
|
+
log_file = File.join(project_session_dir, "session_#{Date.today.strftime('%Y%m%d')}_pid#{process_pid}.log")
|
|
1126
|
+
|
|
1127
|
+
log_entry = {
|
|
1128
|
+
timestamp: Time.now.iso8601,
|
|
1129
|
+
target: options[:target],
|
|
1130
|
+
request_id: result['request_id'],
|
|
1131
|
+
code: code,
|
|
1132
|
+
success: result['success'],
|
|
1133
|
+
result: result['result'],
|
|
1134
|
+
error: result['error'],
|
|
1135
|
+
message: result['message'],
|
|
1136
|
+
execution_time: result['execution_time']
|
|
1137
|
+
}
|
|
799
1138
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1139
|
+
File.open(log_file, 'a') do |f|
|
|
1140
|
+
f.puts JSON.generate(log_entry)
|
|
1141
|
+
end
|
|
803
1142
|
end
|
|
804
1143
|
rescue StandardError => e
|
|
805
1144
|
# Log errors should not crash the command
|
|
806
1145
|
puts "Warning: Failed to log session activity: #{e.message}" if options[:verbose]
|
|
807
1146
|
end
|
|
808
1147
|
|
|
809
|
-
def log_session_event(
|
|
810
|
-
#
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
# Create log entry
|
|
814
|
-
log_entry = {
|
|
815
|
-
timestamp: Time.now.iso8601,
|
|
816
|
-
event: event_type
|
|
817
|
-
}.merge(details)
|
|
818
|
-
|
|
819
|
-
# Append to log file
|
|
820
|
-
File.open(log_file, 'a') do |f|
|
|
821
|
-
f.puts JSON.generate(log_entry)
|
|
822
|
-
end
|
|
823
|
-
rescue StandardError => e
|
|
824
|
-
# Log errors should not crash the command
|
|
825
|
-
puts "Warning: Failed to log session event: #{e.message}" if options[:verbose]
|
|
1148
|
+
def log_session_event(_process_pid, _event_type, _details = {})
|
|
1149
|
+
# Legacy method kept for backward compatibility with tests
|
|
1150
|
+
# Session events are now tracked in registry metadata
|
|
826
1151
|
end
|
|
827
1152
|
|
|
828
1153
|
def load_sessions
|
|
@@ -903,6 +1228,41 @@ module Consolle
|
|
|
903
1228
|
end
|
|
904
1229
|
end
|
|
905
1230
|
|
|
1231
|
+
def session_registry
|
|
1232
|
+
@session_registry ||= Consolle::SessionRegistry.new
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
def format_uptime(seconds)
|
|
1236
|
+
seconds = seconds.to_i
|
|
1237
|
+
if seconds < 60
|
|
1238
|
+
"#{seconds}s"
|
|
1239
|
+
elsif seconds < 3600
|
|
1240
|
+
"#{seconds / 60}m #{seconds % 60}s"
|
|
1241
|
+
elsif seconds < 86400
|
|
1242
|
+
hours = seconds / 3600
|
|
1243
|
+
mins = (seconds % 3600) / 60
|
|
1244
|
+
"#{hours}h #{mins}m"
|
|
1245
|
+
else
|
|
1246
|
+
days = seconds / 86400
|
|
1247
|
+
hours = (seconds % 86400) / 3600
|
|
1248
|
+
"#{days}d #{hours}h"
|
|
1249
|
+
end
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
def format_time_ago(seconds)
|
|
1253
|
+
seconds = seconds.to_i
|
|
1254
|
+
if seconds < 60
|
|
1255
|
+
'just now'
|
|
1256
|
+
elsif seconds < 3600
|
|
1257
|
+
"#{seconds / 60}m ago"
|
|
1258
|
+
elsif seconds < 86400
|
|
1259
|
+
"#{seconds / 3600}h ago"
|
|
1260
|
+
else
|
|
1261
|
+
days = seconds / 86400
|
|
1262
|
+
"#{days}d ago"
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1265
|
+
|
|
906
1266
|
def process_alive?(pid)
|
|
907
1267
|
return false unless pid
|
|
908
1268
|
|