consolle 0.3.9 → 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 +424 -118
- data/lib/consolle/history.rb +210 -0
- data/lib/consolle/session_registry.rb +327 -0
- data/rule.ko.md +74 -5
- data/rule.md +74 -5
- metadata +3 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module Consolle
|
|
7
|
+
# Manages command history for sessions
|
|
8
|
+
class History
|
|
9
|
+
attr_reader :registry
|
|
10
|
+
|
|
11
|
+
def initialize(project_path = Dir.pwd)
|
|
12
|
+
@registry = SessionRegistry.new(project_path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Log a command execution to session history
|
|
16
|
+
def log_command(session_id:, target:, code:, result:)
|
|
17
|
+
log_path = registry.history_log_path(session_id)
|
|
18
|
+
|
|
19
|
+
entry = {
|
|
20
|
+
'timestamp' => Time.now.iso8601,
|
|
21
|
+
'session_id' => session_id,
|
|
22
|
+
'target' => target,
|
|
23
|
+
'request_id' => result['request_id'],
|
|
24
|
+
'code' => code,
|
|
25
|
+
'success' => result['success'],
|
|
26
|
+
'result' => result['result'],
|
|
27
|
+
'error' => result['error'],
|
|
28
|
+
'message' => result['message'],
|
|
29
|
+
'execution_time' => result['execution_time']
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
FileUtils.mkdir_p(File.dirname(log_path))
|
|
33
|
+
File.open(log_path, 'a') do |f|
|
|
34
|
+
f.puts JSON.generate(entry)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Update command count in registry
|
|
38
|
+
registry.record_command(session_id)
|
|
39
|
+
|
|
40
|
+
entry
|
|
41
|
+
rescue StandardError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Query history for a session
|
|
46
|
+
def query(session_id: nil, target: nil, limit: nil, today: false, date: nil,
|
|
47
|
+
success_only: false, failed_only: false, grep: nil, all_sessions: false)
|
|
48
|
+
entries = []
|
|
49
|
+
|
|
50
|
+
# Get sessions to query
|
|
51
|
+
sessions = if session_id
|
|
52
|
+
[registry.find_session(session_id: session_id)]
|
|
53
|
+
elsif target
|
|
54
|
+
if all_sessions
|
|
55
|
+
# All sessions with this target (including stopped)
|
|
56
|
+
registry.list_sessions(include_stopped: true).select { |s| s['target'] == target }
|
|
57
|
+
else
|
|
58
|
+
# Most recent session with this target
|
|
59
|
+
[registry.find_session(target: target)]
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
# Current project sessions
|
|
63
|
+
if all_sessions
|
|
64
|
+
registry.list_sessions(include_stopped: true)
|
|
65
|
+
else
|
|
66
|
+
registry.list_sessions(include_stopped: false)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sessions.compact.each do |session|
|
|
71
|
+
session_entries = load_history(session['id'])
|
|
72
|
+
session_entries.each { |e| e['_session'] = session }
|
|
73
|
+
entries.concat(session_entries)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Apply filters
|
|
77
|
+
entries = filter_by_date(entries, today: today, date: date)
|
|
78
|
+
entries = filter_by_status(entries, success_only: success_only, failed_only: failed_only)
|
|
79
|
+
entries = filter_by_grep(entries, grep) if grep
|
|
80
|
+
|
|
81
|
+
# Sort by timestamp descending
|
|
82
|
+
entries = entries.sort_by { |e| e['timestamp'] }.reverse
|
|
83
|
+
|
|
84
|
+
# Apply limit
|
|
85
|
+
entries = entries.first(limit) if limit
|
|
86
|
+
|
|
87
|
+
entries
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Format history entry for display (compact)
|
|
91
|
+
def format_entry(entry, show_session: true)
|
|
92
|
+
timestamp = Time.parse(entry['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
|
|
93
|
+
session_prefix = show_session ? "[#{entry['_session']&.dig('short_id') || entry['session_id']&.[](0, 4)}] " : ''
|
|
94
|
+
code_preview = entry['code'].to_s.gsub("\n", ' ').strip
|
|
95
|
+
code_preview = "#{code_preview[0, 60]}..." if code_preview.length > 63
|
|
96
|
+
|
|
97
|
+
lines = []
|
|
98
|
+
lines << "#{session_prefix}#{timestamp} | #{code_preview}"
|
|
99
|
+
|
|
100
|
+
if entry['success']
|
|
101
|
+
result_preview = entry['result'].to_s.gsub("\n", ' ').strip
|
|
102
|
+
result_preview = "#{result_preview[0, 70]}..." if result_preview.length > 73
|
|
103
|
+
exec_time = entry['execution_time'] ? " (#{entry['execution_time'].round(3)}s)" : ''
|
|
104
|
+
lines << "#{result_preview}#{exec_time}"
|
|
105
|
+
else
|
|
106
|
+
error_msg = entry['error'] || entry['message'] || 'Error'
|
|
107
|
+
lines << "ERROR: #{error_msg}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
lines.join("\n")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Format history entry for verbose display
|
|
114
|
+
def format_entry_verbose(entry)
|
|
115
|
+
lines = []
|
|
116
|
+
lines << '━' * 60
|
|
117
|
+
timestamp = Time.parse(entry['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
|
|
118
|
+
session_info = entry['_session']
|
|
119
|
+
session_str = session_info ? "#{session_info['short_id']} (#{session_info['target']})" : entry['session_id']
|
|
120
|
+
|
|
121
|
+
lines << "[#{timestamp}] Session: #{session_str}"
|
|
122
|
+
lines << '━' * 60
|
|
123
|
+
lines << 'Code:'
|
|
124
|
+
entry['code'].to_s.lines.each { |line| lines << " #{line.chomp}" }
|
|
125
|
+
lines << ''
|
|
126
|
+
|
|
127
|
+
if entry['success']
|
|
128
|
+
lines << 'Result:'
|
|
129
|
+
entry['result'].to_s.lines.each { |line| lines << " #{line.chomp}" }
|
|
130
|
+
else
|
|
131
|
+
lines << "Error: #{entry['error']}"
|
|
132
|
+
lines << entry['message'] if entry['message']
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
lines << ''
|
|
136
|
+
lines << "Execution time: #{entry['execution_time']&.round(3)}s" if entry['execution_time']
|
|
137
|
+
lines << '━' * 60
|
|
138
|
+
|
|
139
|
+
lines.join("\n")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Format history as JSON
|
|
143
|
+
def format_json(entries)
|
|
144
|
+
JSON.pretty_generate(entries.map { |e| e.except('_session') })
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Get total command count for a session
|
|
148
|
+
def command_count(session_id)
|
|
149
|
+
load_history(session_id).size
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def load_history(session_id)
|
|
155
|
+
log_path = registry.history_log_path(session_id)
|
|
156
|
+
return [] unless File.exist?(log_path)
|
|
157
|
+
|
|
158
|
+
entries = []
|
|
159
|
+
File.readlines(log_path).each do |line|
|
|
160
|
+
entry = JSON.parse(line.strip)
|
|
161
|
+
entries << entry
|
|
162
|
+
rescue JSON::ParserError
|
|
163
|
+
next
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
entries
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def filter_by_date(entries, today: false, date: nil)
|
|
170
|
+
return entries unless today || date
|
|
171
|
+
|
|
172
|
+
target_date = if today
|
|
173
|
+
Date.today
|
|
174
|
+
elsif date.is_a?(String)
|
|
175
|
+
Date.parse(date)
|
|
176
|
+
else
|
|
177
|
+
date
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
entries.select do |e|
|
|
181
|
+
entry_date = Time.parse(e['timestamp']).to_date
|
|
182
|
+
entry_date == target_date
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def filter_by_status(entries, success_only: false, failed_only: false)
|
|
187
|
+
return entries unless success_only || failed_only
|
|
188
|
+
|
|
189
|
+
if success_only
|
|
190
|
+
entries.select { |e| e['success'] }
|
|
191
|
+
elsif failed_only
|
|
192
|
+
entries.reject { |e| e['success'] }
|
|
193
|
+
else
|
|
194
|
+
entries
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def filter_by_grep(entries, pattern)
|
|
199
|
+
regex = Regexp.new(pattern, Regexp::IGNORECASE)
|
|
200
|
+
entries.select do |e|
|
|
201
|
+
e['code']&.match?(regex) || e['result']&.match?(regex)
|
|
202
|
+
end
|
|
203
|
+
rescue RegexpError
|
|
204
|
+
# If invalid regex, treat as literal string
|
|
205
|
+
entries.select do |e|
|
|
206
|
+
e['code']&.include?(pattern) || e['result']&.include?(pattern)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
module Consolle
|
|
8
|
+
# Manages session registry with unique session IDs
|
|
9
|
+
# Provides persistent storage for session metadata even after sessions stop
|
|
10
|
+
class SessionRegistry
|
|
11
|
+
SCHEMA_VERSION = 2
|
|
12
|
+
SESSION_ID_LENGTH = 8
|
|
13
|
+
SHORT_ID_LENGTH = 4
|
|
14
|
+
|
|
15
|
+
attr_reader :project_path
|
|
16
|
+
|
|
17
|
+
def initialize(project_path = Dir.pwd)
|
|
18
|
+
@project_path = project_path
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Generate a new unique session ID (8 hex characters)
|
|
22
|
+
def generate_session_id
|
|
23
|
+
SecureRandom.hex(SESSION_ID_LENGTH / 2)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get short ID (first 4 characters)
|
|
27
|
+
def short_id(session_id)
|
|
28
|
+
session_id[0, SHORT_ID_LENGTH]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Create a new session entry
|
|
32
|
+
def create_session(target:, socket_path:, pid:, rails_env:, mode:)
|
|
33
|
+
session_id = generate_session_id
|
|
34
|
+
|
|
35
|
+
session_data = {
|
|
36
|
+
'id' => session_id,
|
|
37
|
+
'short_id' => short_id(session_id),
|
|
38
|
+
'target' => target,
|
|
39
|
+
'project' => project_path,
|
|
40
|
+
'project_hash' => project_hash,
|
|
41
|
+
'rails_env' => rails_env,
|
|
42
|
+
'mode' => mode,
|
|
43
|
+
'status' => 'running',
|
|
44
|
+
'pid' => pid,
|
|
45
|
+
'socket_path' => socket_path,
|
|
46
|
+
'created_at' => Time.now.iso8601,
|
|
47
|
+
'started_at' => Time.now.iso8601,
|
|
48
|
+
'stopped_at' => nil,
|
|
49
|
+
'last_activity_at' => Time.now.iso8601,
|
|
50
|
+
'command_count' => 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
with_registry_lock do
|
|
54
|
+
registry = load_registry
|
|
55
|
+
registry['sessions'][session_id] = session_data
|
|
56
|
+
save_registry(registry)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Create session directory
|
|
60
|
+
ensure_session_directory(session_id)
|
|
61
|
+
|
|
62
|
+
# Save session metadata
|
|
63
|
+
save_session_metadata(session_id, session_data)
|
|
64
|
+
|
|
65
|
+
session_data
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Update session status to stopped
|
|
69
|
+
def stop_session(session_id: nil, target: nil, reason: 'user_requested')
|
|
70
|
+
session = find_session(session_id: session_id, target: target)
|
|
71
|
+
return nil unless session
|
|
72
|
+
|
|
73
|
+
with_registry_lock do
|
|
74
|
+
registry = load_registry
|
|
75
|
+
if registry['sessions'][session['id']]
|
|
76
|
+
registry['sessions'][session['id']]['status'] = 'stopped'
|
|
77
|
+
registry['sessions'][session['id']]['stopped_at'] = Time.now.iso8601
|
|
78
|
+
registry['sessions'][session['id']]['stop_reason'] = reason
|
|
79
|
+
save_registry(registry)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Update metadata file
|
|
84
|
+
metadata = load_session_metadata(session['id'])
|
|
85
|
+
if metadata
|
|
86
|
+
metadata['status'] = 'stopped'
|
|
87
|
+
metadata['stopped_at'] = Time.now.iso8601
|
|
88
|
+
metadata['stop_reason'] = reason
|
|
89
|
+
save_session_metadata(session['id'], metadata)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
session
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Record command execution
|
|
96
|
+
def record_command(session_id)
|
|
97
|
+
with_registry_lock do
|
|
98
|
+
registry = load_registry
|
|
99
|
+
if registry['sessions'][session_id]
|
|
100
|
+
registry['sessions'][session_id]['command_count'] ||= 0
|
|
101
|
+
registry['sessions'][session_id]['command_count'] += 1
|
|
102
|
+
registry['sessions'][session_id]['last_activity_at'] = Time.now.iso8601
|
|
103
|
+
save_registry(registry)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Find session by ID (full or short) or target name
|
|
109
|
+
def find_session(session_id: nil, target: nil, status: nil)
|
|
110
|
+
registry = load_registry
|
|
111
|
+
|
|
112
|
+
if session_id
|
|
113
|
+
# Try exact match first
|
|
114
|
+
session = registry['sessions'][session_id]
|
|
115
|
+
return session if session && (status.nil? || session['status'] == status)
|
|
116
|
+
|
|
117
|
+
# Try short ID match
|
|
118
|
+
matches = registry['sessions'].values.select do |s|
|
|
119
|
+
s['short_id'] == session_id && s['project'] == project_path
|
|
120
|
+
end
|
|
121
|
+
matches = matches.select { |s| s['status'] == status } if status
|
|
122
|
+
return matches.first if matches.size == 1
|
|
123
|
+
|
|
124
|
+
# Ambiguous short ID
|
|
125
|
+
return nil if matches.size > 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if target
|
|
129
|
+
# Find by target name in current project
|
|
130
|
+
matches = registry['sessions'].values.select do |s|
|
|
131
|
+
s['target'] == target && s['project'] == project_path
|
|
132
|
+
end
|
|
133
|
+
matches = matches.select { |s| s['status'] == status } if status
|
|
134
|
+
|
|
135
|
+
# Return most recent running session, or most recently created
|
|
136
|
+
matches.sort_by { |s| s['created_at'] }.last
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Find running session by target
|
|
141
|
+
def find_running_session(target:)
|
|
142
|
+
find_session(target: target, status: 'running')
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# List sessions for current project
|
|
146
|
+
def list_sessions(include_stopped: false, all_projects: false)
|
|
147
|
+
registry = load_registry
|
|
148
|
+
|
|
149
|
+
sessions = registry['sessions'].values
|
|
150
|
+
|
|
151
|
+
# Filter by project unless all_projects
|
|
152
|
+
sessions = sessions.select { |s| s['project'] == project_path } unless all_projects
|
|
153
|
+
|
|
154
|
+
# Filter by status
|
|
155
|
+
sessions = sessions.select { |s| s['status'] == 'running' } unless include_stopped
|
|
156
|
+
|
|
157
|
+
# Sort by created_at descending
|
|
158
|
+
sessions.sort_by { |s| s['created_at'] }.reverse
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# List only stopped sessions
|
|
162
|
+
def list_stopped_sessions(all_projects: false)
|
|
163
|
+
registry = load_registry
|
|
164
|
+
|
|
165
|
+
sessions = registry['sessions'].values.select { |s| s['status'] == 'stopped' }
|
|
166
|
+
|
|
167
|
+
# Filter by project unless all_projects
|
|
168
|
+
sessions = sessions.select { |s| s['project'] == project_path } unless all_projects
|
|
169
|
+
|
|
170
|
+
# Sort by stopped_at descending
|
|
171
|
+
sessions.sort_by { |s| s['stopped_at'] || s['created_at'] }.reverse
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Remove session and its history
|
|
175
|
+
def remove_session(session_id: nil, target: nil)
|
|
176
|
+
session = find_session(session_id: session_id, target: target)
|
|
177
|
+
return nil unless session
|
|
178
|
+
|
|
179
|
+
# Check if running
|
|
180
|
+
if session['status'] == 'running'
|
|
181
|
+
return { error: 'running', session: session }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
with_registry_lock do
|
|
185
|
+
registry = load_registry
|
|
186
|
+
registry['sessions'].delete(session['id'])
|
|
187
|
+
save_registry(registry)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Remove session directory
|
|
191
|
+
session_dir = session_directory(session['id'])
|
|
192
|
+
FileUtils.rm_rf(session_dir) if Dir.exist?(session_dir)
|
|
193
|
+
|
|
194
|
+
session
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Remove all stopped sessions (prune)
|
|
198
|
+
def prune_sessions(all_projects: false)
|
|
199
|
+
stopped = list_stopped_sessions(all_projects: all_projects)
|
|
200
|
+
removed = []
|
|
201
|
+
|
|
202
|
+
stopped.each do |session|
|
|
203
|
+
result = remove_session(session_id: session['id'])
|
|
204
|
+
# Check if result is session data (success) vs error hash
|
|
205
|
+
removed << result if result && !result.key?(:error)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
removed
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Get session directory path
|
|
212
|
+
def session_directory(session_id)
|
|
213
|
+
File.join(sessions_base_dir, session_id)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Get history log path for session
|
|
217
|
+
def history_log_path(session_id)
|
|
218
|
+
File.join(session_directory(session_id), 'history.log')
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Migration from old sessions.json format
|
|
222
|
+
def migrate_from_legacy
|
|
223
|
+
legacy_sessions_file = File.join(project_path, 'tmp', 'cone', 'sessions.json')
|
|
224
|
+
return unless File.exist?(legacy_sessions_file)
|
|
225
|
+
|
|
226
|
+
legacy_data = JSON.parse(File.read(legacy_sessions_file))
|
|
227
|
+
return if legacy_data.empty?
|
|
228
|
+
|
|
229
|
+
legacy_data.each do |target, info|
|
|
230
|
+
next if target == '_schema'
|
|
231
|
+
|
|
232
|
+
# Create new session entry for running legacy sessions
|
|
233
|
+
if info['process_pid'] && process_alive?(info['process_pid'])
|
|
234
|
+
create_session(
|
|
235
|
+
target: target,
|
|
236
|
+
socket_path: info['socket_path'],
|
|
237
|
+
pid: info['process_pid'],
|
|
238
|
+
rails_env: 'development',
|
|
239
|
+
mode: 'pty'
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private
|
|
246
|
+
|
|
247
|
+
def registry_path
|
|
248
|
+
File.expand_path('~/.cone/registry.json')
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def sessions_base_dir
|
|
252
|
+
File.join(File.expand_path('~/.cone/sessions'), project_hash)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def project_hash
|
|
256
|
+
project_path.gsub('/', '-')
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def ensure_session_directory(session_id)
|
|
260
|
+
dir = session_directory(session_id)
|
|
261
|
+
FileUtils.mkdir_p(dir)
|
|
262
|
+
dir
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def load_registry
|
|
266
|
+
FileUtils.mkdir_p(File.dirname(registry_path))
|
|
267
|
+
|
|
268
|
+
if File.exist?(registry_path)
|
|
269
|
+
data = JSON.parse(File.read(registry_path))
|
|
270
|
+
# Ensure schema version
|
|
271
|
+
data['_schema'] ||= SCHEMA_VERSION
|
|
272
|
+
data['sessions'] ||= {}
|
|
273
|
+
data
|
|
274
|
+
else
|
|
275
|
+
{ '_schema' => SCHEMA_VERSION, 'sessions' => {} }
|
|
276
|
+
end
|
|
277
|
+
rescue JSON::ParserError
|
|
278
|
+
{ '_schema' => SCHEMA_VERSION, 'sessions' => {} }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def save_registry(registry)
|
|
282
|
+
registry['_schema'] = SCHEMA_VERSION
|
|
283
|
+
temp_path = "#{registry_path}.tmp.#{Process.pid}"
|
|
284
|
+
File.write(temp_path, JSON.pretty_generate(registry))
|
|
285
|
+
File.rename(temp_path, registry_path)
|
|
286
|
+
rescue StandardError => e
|
|
287
|
+
File.unlink(temp_path) if File.exist?(temp_path)
|
|
288
|
+
raise e
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def with_registry_lock
|
|
292
|
+
FileUtils.mkdir_p(File.dirname(registry_path))
|
|
293
|
+
lock_file_path = "#{registry_path}.lock"
|
|
294
|
+
|
|
295
|
+
File.open(lock_file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
|
296
|
+
f.flock(File::LOCK_EX)
|
|
297
|
+
yield
|
|
298
|
+
ensure
|
|
299
|
+
f.flock(File::LOCK_UN)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def load_session_metadata(session_id)
|
|
304
|
+
metadata_path = File.join(session_directory(session_id), 'metadata.json')
|
|
305
|
+
return nil unless File.exist?(metadata_path)
|
|
306
|
+
|
|
307
|
+
JSON.parse(File.read(metadata_path))
|
|
308
|
+
rescue JSON::ParserError
|
|
309
|
+
nil
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def save_session_metadata(session_id, metadata)
|
|
313
|
+
ensure_session_directory(session_id)
|
|
314
|
+
metadata_path = File.join(session_directory(session_id), 'metadata.json')
|
|
315
|
+
File.write(metadata_path, JSON.pretty_generate(metadata))
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def process_alive?(pid)
|
|
319
|
+
return false unless pid
|
|
320
|
+
|
|
321
|
+
Process.kill(0, pid)
|
|
322
|
+
true
|
|
323
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
324
|
+
false
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
data/rule.ko.md
CHANGED
|
@@ -8,6 +8,15 @@ Rails console과 마찬가지로 세션 내에서 실행한 결과는 유지되
|
|
|
8
8
|
|
|
9
9
|
사용 전에는 `status`로 상태를 확인하고, 작업 종료 후에는 `stop`해야 합니다.
|
|
10
10
|
|
|
11
|
+
## 설치 참고사항
|
|
12
|
+
|
|
13
|
+
Gemfile이 있는 프로젝트에서는 `bundle exec`를 사용하는 것이 좋습니다:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
$ bundle exec cone start
|
|
17
|
+
$ bundle exec cone exec 'User.count'
|
|
18
|
+
```
|
|
19
|
+
|
|
11
20
|
## Cone의 용도
|
|
12
21
|
|
|
13
22
|
Cone은 디버깅, 데이터 탐색, 그리고 개발 보조 도구로 사용됩니다.
|
|
@@ -27,9 +36,21 @@ $ cone start # 서버 시작 (RAILS_ENV가 없으면 development)
|
|
|
27
36
|
$ RAILS_ENV=test cone start # test 환경에서 console 시작
|
|
28
37
|
```
|
|
29
38
|
|
|
39
|
+
시작 시 고유한 Session ID를 포함한 세션 정보가 표시됩니다:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
$ cone start
|
|
43
|
+
✓ Rails console started
|
|
44
|
+
Session ID: a1b2c3d4 (a1b2)
|
|
45
|
+
Target: cone
|
|
46
|
+
Environment: development
|
|
47
|
+
PID: 12345
|
|
48
|
+
Socket: /path/to/cone.socket
|
|
49
|
+
```
|
|
50
|
+
|
|
30
51
|
중지와 재시작 명령어도 제공합니다.
|
|
31
52
|
|
|
32
|
-
Cone은 한 번에 하나의 세션만 제공하며, 실행 환경을 변경하려면 반드시 중지 후 재시작해야 합니다.
|
|
53
|
+
Cone은 타겟당 한 번에 하나의 세션만 제공하며, 실행 환경을 변경하려면 반드시 중지 후 재시작해야 합니다.
|
|
33
54
|
|
|
34
55
|
```bash
|
|
35
56
|
$ cone stop # 서버 중지
|
|
@@ -37,6 +58,51 @@ $ cone stop # 서버 중지
|
|
|
37
58
|
|
|
38
59
|
작업을 마치면 반드시 종료해 주세요.
|
|
39
60
|
|
|
61
|
+
## 세션 관리
|
|
62
|
+
|
|
63
|
+
### 세션 목록 보기
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
$ cone ls # 활성 세션만 표시
|
|
67
|
+
$ cone ls -a # 종료된 세션 포함 전체 표시
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
출력 예시:
|
|
71
|
+
```
|
|
72
|
+
ACTIVE SESSIONS:
|
|
73
|
+
|
|
74
|
+
ID TARGET ENV STATUS UPTIME COMMANDS
|
|
75
|
+
a1b2 cone development running 2h 15m 42
|
|
76
|
+
e5f6 api production running 1h 30m 15
|
|
77
|
+
|
|
78
|
+
Usage: cone exec -t TARGET CODE
|
|
79
|
+
cone exec --session ID CODE
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 세션 히스토리
|
|
83
|
+
|
|
84
|
+
세션별 명령어 히스토리를 조회합니다:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
$ cone history # 현재 세션 히스토리
|
|
88
|
+
$ cone history -t api # 'api' 타겟의 히스토리
|
|
89
|
+
$ cone history --session a1b2 # 특정 세션 ID의 히스토리
|
|
90
|
+
$ cone history -n 10 # 최근 10개 명령어
|
|
91
|
+
$ cone history --today # 오늘 실행한 명령어만
|
|
92
|
+
$ cone history --failed # 실패한 명령어만
|
|
93
|
+
$ cone history --grep User # 패턴으로 필터링
|
|
94
|
+
$ cone history --json # JSON 형식으로 출력
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 세션 삭제
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
$ cone rm a1b2 # 종료된 세션 ID로 삭제
|
|
101
|
+
$ cone rm -f a1b2 # 강제 삭제 (실행 중이면 중지 후 삭제)
|
|
102
|
+
$ cone prune # 종료된 모든 세션 삭제
|
|
103
|
+
$ cone prune --yes # 확인 없이 삭제
|
|
104
|
+
```
|
|
105
|
+
|
|
40
106
|
## 실행 모드
|
|
41
107
|
|
|
42
108
|
Cone은 세 가지 실행 모드를 지원합니다. `--mode` 옵션으로 지정할 수 있습니다.
|
|
@@ -82,10 +148,13 @@ mode: embed-rails
|
|
|
82
148
|
```bash
|
|
83
149
|
$ cone status
|
|
84
150
|
✓ Rails console is running
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
151
|
+
Session ID: a1b2c3d4 (a1b2)
|
|
152
|
+
Target: cone
|
|
153
|
+
Environment: development
|
|
154
|
+
PID: 12345
|
|
155
|
+
Uptime: 2h 15m
|
|
156
|
+
Commands: 42
|
|
157
|
+
Socket: /path/to/cone.socket
|
|
89
158
|
```
|
|
90
159
|
|
|
91
160
|
## 코드 실행
|