consolle 0.3.7 → 0.3.8
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/adapters/rails_console.rb +12 -8
- data/lib/consolle/cli.rb +29 -12
- data/lib/consolle/config.rb +27 -1
- data/lib/consolle/errors.rb +14 -0
- data/lib/consolle/server/base_supervisor.rb +92 -0
- data/lib/consolle/server/console_socket_server.rb +13 -3
- data/lib/consolle/server/console_supervisor.rb +6 -0
- data/lib/consolle/server/embedded_supervisor.rb +258 -0
- data/lib/consolle/server/supervisor_factory.rb +86 -0
- data/rule.ko.md +44 -1
- data/rule.md +44 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0975195db50059158fa8643203c06099ea77c53a6ee199705731244806d7b2d8'
|
|
4
|
+
data.tar.gz: 8d1fc462e0dc7acd21b9f53989f892ffdf03920b1bc327c82fc70516fda8e201
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a281509268f01cdbb60e1a538e9c037c82cce14f26aebd1faf30befeee6aeba6996eaa94f37fa70f13949f0a1d5d749db7604fae39253154d61ec14e6db637ff
|
|
7
|
+
data.tar.gz: 58c2126e05ca127992001c57a597a095b3c4aa193c36b934de5490d3eddb95bc85770c7bd7e1905c3fdd2b4e95bc68d77f9518d35a22cd8f3619f71c4de15638
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.8
|
data/Gemfile.lock
CHANGED
|
@@ -13,7 +13,7 @@ module Consolle
|
|
|
13
13
|
attr_reader :socket_path, :process_pid, :pid_path, :log_path
|
|
14
14
|
|
|
15
15
|
def initialize(socket_path: nil, pid_path: nil, log_path: nil, rails_root: nil, rails_env: nil, verbose: false,
|
|
16
|
-
command: nil, wait_timeout: nil)
|
|
16
|
+
command: nil, wait_timeout: nil, mode: nil)
|
|
17
17
|
@socket_path = socket_path || default_socket_path
|
|
18
18
|
@pid_path = pid_path || default_pid_path
|
|
19
19
|
@log_path = log_path || default_log_path
|
|
@@ -22,6 +22,7 @@ module Consolle
|
|
|
22
22
|
@verbose = verbose
|
|
23
23
|
@command = command || 'bin/rails console'
|
|
24
24
|
@wait_timeout = wait_timeout || Consolle::DEFAULT_WAIT_TIMEOUT
|
|
25
|
+
@mode = mode # nil means use config file setting
|
|
25
26
|
@server_pid = nil
|
|
26
27
|
end
|
|
27
28
|
|
|
@@ -149,7 +150,8 @@ module Consolle
|
|
|
149
150
|
@pid_path,
|
|
150
151
|
@log_path,
|
|
151
152
|
@command,
|
|
152
|
-
@wait_timeout.to_s
|
|
153
|
+
@wait_timeout.to_s,
|
|
154
|
+
@mode.to_s # empty string if nil (use config file)
|
|
153
155
|
]
|
|
154
156
|
end
|
|
155
157
|
|
|
@@ -159,8 +161,9 @@ module Consolle
|
|
|
159
161
|
require 'consolle/server/console_socket_server'
|
|
160
162
|
require 'logger'
|
|
161
163
|
#{' '}
|
|
162
|
-
socket_path, rails_root, rails_env, log_level, pid_path, log_path, command, wait_timeout_str = ARGV
|
|
164
|
+
socket_path, rails_root, rails_env, log_level, pid_path, log_path, command, wait_timeout_str, mode_str = ARGV
|
|
163
165
|
wait_timeout = wait_timeout_str ? wait_timeout_str.to_i : nil
|
|
166
|
+
mode = mode_str && !mode_str.empty? ? mode_str.to_sym : nil # nil = use config file
|
|
164
167
|
#{' '}
|
|
165
168
|
# Write initial log
|
|
166
169
|
log_file = log_path || socket_path.sub(/\\.socket$/, '.log')
|
|
@@ -191,10 +194,11 @@ module Consolle
|
|
|
191
194
|
rails_env: rails_env,
|
|
192
195
|
logger: logger,
|
|
193
196
|
command: command,
|
|
194
|
-
wait_timeout: wait_timeout
|
|
197
|
+
wait_timeout: wait_timeout,
|
|
198
|
+
mode: mode
|
|
195
199
|
)
|
|
196
200
|
#{' '}
|
|
197
|
-
puts "[Server] Starting server with log level: \#{log_level}..."
|
|
201
|
+
puts "[Server] Starting server with log level: \#{log_level}, mode: \#{mode || 'config'}..."
|
|
198
202
|
server.start
|
|
199
203
|
#{' '}
|
|
200
204
|
puts "[Server] Server started, entering sleep..."
|
|
@@ -204,17 +208,17 @@ module Consolle
|
|
|
204
208
|
rescue => e
|
|
205
209
|
puts "[Server] Error: \#{e.class}: \#{e.message}"
|
|
206
210
|
puts e.backtrace.join("\\n")
|
|
207
|
-
|
|
211
|
+
|
|
208
212
|
# Clean up socket file if it exists
|
|
209
213
|
if defined?(socket_path) && socket_path && File.exist?(socket_path)
|
|
210
214
|
File.unlink(socket_path) rescue nil
|
|
211
215
|
end
|
|
212
|
-
|
|
216
|
+
|
|
213
217
|
# Clean up PID file if it exists
|
|
214
218
|
if defined?(pid_file) && pid_file && File.exist?(pid_file)
|
|
215
219
|
File.unlink(pid_file) rescue nil
|
|
216
220
|
end
|
|
217
|
-
|
|
221
|
+
|
|
218
222
|
exit 1
|
|
219
223
|
end
|
|
220
224
|
RUBY
|
data/lib/consolle/cli.rb
CHANGED
|
@@ -124,15 +124,25 @@ module Consolle
|
|
|
124
124
|
RAILS_ENV=production cone start --target api
|
|
125
125
|
RAILS_ENV=development cone start --target worker
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
Supervisor modes (--mode):
|
|
128
|
+
pty - Traditional PTY-based mode, supports custom commands (default)
|
|
129
|
+
embed-irb - Pure IRB embedding, Ruby 3.3+ only
|
|
130
|
+
embed-rails - Rails console embedding, Ruby 3.3+ only
|
|
131
|
+
|
|
132
|
+
Custom console commands are supported for PTY mode:
|
|
128
133
|
cone start --command "kamal app exec -i 'bin/rails console'"
|
|
129
134
|
cone start --command "docker exec -it myapp bin/rails console"
|
|
130
|
-
|
|
135
|
+
|
|
131
136
|
For SSH-based commands that require authentication (e.g., 1Password SSH agent):
|
|
132
137
|
cone start --command "kamal console" --wait-timeout 60
|
|
138
|
+
|
|
139
|
+
Use embedded modes for faster local execution (200x faster):
|
|
140
|
+
cone start --mode embed-rails
|
|
141
|
+
cone start --mode embed-irb
|
|
133
142
|
LONGDESC
|
|
134
143
|
# Rails environment is now controlled via RAILS_ENV, not a CLI option
|
|
135
|
-
method_option :
|
|
144
|
+
method_option :mode, type: :string, aliases: '-m', desc: 'Supervisor mode: pty, embed-irb, embed-rails'
|
|
145
|
+
method_option :command, type: :string, aliases: '-c', desc: 'Custom console command (PTY mode only)', default: 'bin/rails console'
|
|
136
146
|
method_option :wait_timeout, type: :numeric, aliases: '-w', desc: 'Timeout for console startup (seconds)', default: Consolle::DEFAULT_WAIT_TIMEOUT
|
|
137
147
|
def start
|
|
138
148
|
ensure_rails_project!
|
|
@@ -160,7 +170,13 @@ module Consolle
|
|
|
160
170
|
clear_session_info
|
|
161
171
|
end
|
|
162
172
|
|
|
163
|
-
adapter = create_rails_adapter(
|
|
173
|
+
adapter = create_rails_adapter(
|
|
174
|
+
current_rails_env,
|
|
175
|
+
options[:target],
|
|
176
|
+
options[:command],
|
|
177
|
+
options[:wait_timeout],
|
|
178
|
+
options[:mode]
|
|
179
|
+
)
|
|
164
180
|
|
|
165
181
|
puts 'Starting Rails console...'
|
|
166
182
|
|
|
@@ -557,8 +573,8 @@ module Consolle
|
|
|
557
573
|
if result['success']
|
|
558
574
|
# Always print result, even if empty (multiline code often returns empty string)
|
|
559
575
|
puts result['result'] unless result['result'].nil?
|
|
560
|
-
#
|
|
561
|
-
puts "Execution time: #{result['execution_time'].round(3)}s" if result['execution_time']
|
|
576
|
+
# Show execution time only in verbose mode
|
|
577
|
+
puts "Execution time: #{result['execution_time'].round(3)}s" if options[:verbose] && result['execution_time']
|
|
562
578
|
else
|
|
563
579
|
# Display error information
|
|
564
580
|
if result['error_code']
|
|
@@ -574,10 +590,10 @@ module Consolle
|
|
|
574
590
|
|
|
575
591
|
puts result['message']
|
|
576
592
|
puts result['backtrace']&.join("\n") if options[:verbose] && result['backtrace']
|
|
577
|
-
|
|
578
|
-
# Show execution time for errors too
|
|
579
|
-
puts "Execution time: #{result['execution_time'].round(3)}s" if result['execution_time']
|
|
580
|
-
|
|
593
|
+
|
|
594
|
+
# Show execution time for errors too (verbose only)
|
|
595
|
+
puts "Execution time: #{result['execution_time'].round(3)}s" if options[:verbose] && result['execution_time']
|
|
596
|
+
|
|
581
597
|
exit 1
|
|
582
598
|
end
|
|
583
599
|
end
|
|
@@ -702,7 +718,7 @@ module Consolle
|
|
|
702
718
|
File.join(Dir.pwd, 'tmp', 'cone', 'sessions.json')
|
|
703
719
|
end
|
|
704
720
|
|
|
705
|
-
def create_rails_adapter(rails_env = 'development', target = nil, command = nil, wait_timeout = nil)
|
|
721
|
+
def create_rails_adapter(rails_env = 'development', target = nil, command = nil, wait_timeout = nil, mode = nil)
|
|
706
722
|
target ||= options[:target]
|
|
707
723
|
|
|
708
724
|
Consolle::Adapters::RailsConsole.new(
|
|
@@ -713,7 +729,8 @@ module Consolle
|
|
|
713
729
|
rails_env: rails_env,
|
|
714
730
|
verbose: options[:verbose],
|
|
715
731
|
command: command,
|
|
716
|
-
wait_timeout: wait_timeout
|
|
732
|
+
wait_timeout: wait_timeout,
|
|
733
|
+
mode: mode
|
|
717
734
|
)
|
|
718
735
|
end
|
|
719
736
|
|
data/lib/consolle/config.rb
CHANGED
|
@@ -14,13 +14,23 @@ module Consolle
|
|
|
14
14
|
# - Generic prompts: >> or >
|
|
15
15
|
DEFAULT_PROMPT_PATTERN = /^[^\w]*(\u001E\u001F<CONSOLLE>\u001F\u001E|\w+[-_]?\w*\([^)]*\)(:\d+)?>|irb\([^)]+\):\d+:?\d*[>*]|>>|>)\s*$/
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# Valid supervisor modes
|
|
18
|
+
# - pty: PTY-based, supports custom command (local/remote)
|
|
19
|
+
# - embed-irb: Pure IRB embedding (Ruby 3.3+, local only)
|
|
20
|
+
# - embed-rails: Rails console embedding (Ruby 3.3+, local only)
|
|
21
|
+
VALID_MODES = %w[pty embed-irb embed-rails].freeze
|
|
22
|
+
DEFAULT_MODE = :pty
|
|
23
|
+
DEFAULT_COMMAND = 'bin/rails console'
|
|
24
|
+
|
|
25
|
+
attr_reader :rails_root, :prompt_pattern, :raw_prompt_pattern, :mode, :command
|
|
18
26
|
|
|
19
27
|
def initialize(rails_root)
|
|
20
28
|
@rails_root = rails_root
|
|
21
29
|
@config = load_config
|
|
22
30
|
@raw_prompt_pattern = @config['prompt_pattern']
|
|
23
31
|
@prompt_pattern = parse_prompt_pattern(@raw_prompt_pattern)
|
|
32
|
+
@mode = parse_mode(@config['mode'])
|
|
33
|
+
@command = @config['command'] || DEFAULT_COMMAND
|
|
24
34
|
end
|
|
25
35
|
|
|
26
36
|
def self.load(rails_root)
|
|
@@ -74,5 +84,21 @@ module Consolle
|
|
|
74
84
|
DEFAULT_PROMPT_PATTERN
|
|
75
85
|
end
|
|
76
86
|
end
|
|
87
|
+
|
|
88
|
+
def parse_mode(mode_string)
|
|
89
|
+
return DEFAULT_MODE if mode_string.nil?
|
|
90
|
+
|
|
91
|
+
# Normalize: convert underscores/symbols, handle legacy 'embedded' -> 'embed-rails'
|
|
92
|
+
normalized = mode_string.to_s.downcase.tr('_', '-')
|
|
93
|
+
normalized = 'embed-rails' if normalized == 'embedded'
|
|
94
|
+
normalized = 'pty' if normalized == 'auto'
|
|
95
|
+
|
|
96
|
+
unless VALID_MODES.include?(normalized)
|
|
97
|
+
warn "[Consolle] Warning: Invalid mode '#{mode_string}'. Using '#{DEFAULT_MODE}'."
|
|
98
|
+
return DEFAULT_MODE
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
normalized.tr('-', '_').to_sym # :pty, :embed_irb, :embed_rails
|
|
102
|
+
end
|
|
77
103
|
end
|
|
78
104
|
end
|
data/lib/consolle/errors.rb
CHANGED
|
@@ -110,6 +110,20 @@ module Consolle
|
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
# Configuration error
|
|
114
|
+
class ConfigurationError < Error
|
|
115
|
+
def initialize(message)
|
|
116
|
+
super("Configuration error: #{message}")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Unsupported Ruby version for embedded mode
|
|
121
|
+
class UnsupportedRubyVersion < Error
|
|
122
|
+
def initialize(message)
|
|
123
|
+
super(message)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
113
127
|
# Error classifier to map exceptions to error codes
|
|
114
128
|
class ErrorClassifier
|
|
115
129
|
ERROR_CODE_MAP = {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Consolle
|
|
4
|
+
module Server
|
|
5
|
+
# Base interface for console supervisors
|
|
6
|
+
# Both PTY-based and embedded IRB modes inherit from this class
|
|
7
|
+
class BaseSupervisor
|
|
8
|
+
attr_reader :rails_root, :rails_env, :logger, :config
|
|
9
|
+
|
|
10
|
+
def initialize(rails_root:, rails_env: 'development', logger: nil)
|
|
11
|
+
@rails_root = rails_root
|
|
12
|
+
@rails_env = rails_env
|
|
13
|
+
@logger = logger || Logger.new($stdout)
|
|
14
|
+
@config = Consolle::Config.load(rails_root)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Execute code and return result
|
|
18
|
+
# @param code [String] Ruby code to evaluate
|
|
19
|
+
# @param timeout [Integer] Timeout in seconds
|
|
20
|
+
# @return [Hash] Result hash with :success, :output, :execution_time, etc.
|
|
21
|
+
def eval(code, timeout: 60)
|
|
22
|
+
raise NotImplementedError, "#{self.class} must implement #eval"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Check if the console is running and ready
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def running?
|
|
28
|
+
raise NotImplementedError, "#{self.class} must implement #running?"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Stop the console
|
|
32
|
+
def stop
|
|
33
|
+
raise NotImplementedError, "#{self.class} must implement #stop"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Restart the console
|
|
37
|
+
def restart
|
|
38
|
+
raise NotImplementedError, "#{self.class} must implement #restart"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the mode name for logging/debugging
|
|
42
|
+
# @return [Symbol] :pty or :embedded
|
|
43
|
+
def mode
|
|
44
|
+
raise NotImplementedError, "#{self.class} must implement #mode"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
protected
|
|
48
|
+
|
|
49
|
+
def build_success_response(output, execution_time:, result: nil)
|
|
50
|
+
response = {
|
|
51
|
+
success: true,
|
|
52
|
+
output: output,
|
|
53
|
+
execution_time: execution_time
|
|
54
|
+
}
|
|
55
|
+
response[:result] = result if result
|
|
56
|
+
response
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_error_response(exception, execution_time: nil)
|
|
60
|
+
if exception.is_a?(String)
|
|
61
|
+
error_code = Consolle::Errors::ErrorClassifier.classify_message(exception)
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
error_class: 'RuntimeError',
|
|
65
|
+
error_code: error_code,
|
|
66
|
+
output: exception,
|
|
67
|
+
execution_time: execution_time
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
success: false,
|
|
73
|
+
error_class: exception.class.name,
|
|
74
|
+
error_code: Consolle::Errors::ErrorClassifier.to_code(exception),
|
|
75
|
+
output: "#{exception.class}: #{exception.message}",
|
|
76
|
+
execution_time: execution_time
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_timeout_response(timeout_seconds)
|
|
81
|
+
error = Consolle::Errors::ExecutionTimeout.new(timeout_seconds)
|
|
82
|
+
{
|
|
83
|
+
success: false,
|
|
84
|
+
error_class: error.class.name,
|
|
85
|
+
error_code: Consolle::Errors::ErrorClassifier.to_code(error),
|
|
86
|
+
output: error.message,
|
|
87
|
+
execution_time: timeout_seconds
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -5,6 +5,8 @@ require 'json'
|
|
|
5
5
|
require 'logger'
|
|
6
6
|
require 'fileutils'
|
|
7
7
|
require_relative 'console_supervisor'
|
|
8
|
+
require_relative 'embedded_supervisor'
|
|
9
|
+
require_relative 'supervisor_factory'
|
|
8
10
|
require_relative 'request_broker'
|
|
9
11
|
|
|
10
12
|
module Consolle
|
|
@@ -12,14 +14,15 @@ module Consolle
|
|
|
12
14
|
class ConsoleSocketServer
|
|
13
15
|
attr_reader :socket_path, :logger
|
|
14
16
|
|
|
15
|
-
def initialize(socket_path:, rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil)
|
|
17
|
+
def initialize(socket_path:, rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil, mode: nil)
|
|
16
18
|
@socket_path = socket_path
|
|
17
19
|
@rails_root = rails_root
|
|
18
20
|
@rails_env = rails_env
|
|
19
21
|
@command = command || 'bin/rails console'
|
|
20
22
|
@wait_timeout = wait_timeout
|
|
23
|
+
@mode = mode # nil = use config file, otherwise :pty, :embed_irb, :embed_rails
|
|
21
24
|
@logger = logger || begin
|
|
22
|
-
log = Logger.new(
|
|
25
|
+
log = Logger.new($stdout)
|
|
23
26
|
log.level = Logger::DEBUG
|
|
24
27
|
log
|
|
25
28
|
end
|
|
@@ -97,13 +100,20 @@ module Consolle
|
|
|
97
100
|
end
|
|
98
101
|
|
|
99
102
|
def setup_supervisor
|
|
100
|
-
|
|
103
|
+
# Load config to get default mode if not explicitly specified
|
|
104
|
+
config = Consolle::Config.load(@rails_root)
|
|
105
|
+
effective_mode = @mode || config.mode
|
|
106
|
+
|
|
107
|
+
@supervisor = SupervisorFactory.create(
|
|
101
108
|
rails_root: @rails_root,
|
|
109
|
+
mode: effective_mode,
|
|
102
110
|
rails_env: @rails_env,
|
|
103
111
|
logger: @logger,
|
|
104
112
|
command: @command,
|
|
105
113
|
wait_timeout: @wait_timeout
|
|
106
114
|
)
|
|
115
|
+
|
|
116
|
+
@logger.info "[ConsoleSocketServer] Using #{@supervisor.mode} mode"
|
|
107
117
|
end
|
|
108
118
|
|
|
109
119
|
def setup_broker
|
|
@@ -300,6 +300,12 @@ module Consolle
|
|
|
300
300
|
end
|
|
301
301
|
end
|
|
302
302
|
|
|
303
|
+
# Returns the mode name for logging/debugging
|
|
304
|
+
# @return [Symbol] :pty
|
|
305
|
+
def mode
|
|
306
|
+
:pty
|
|
307
|
+
end
|
|
308
|
+
|
|
303
309
|
# Returns the prompt pattern to use (custom from config or default)
|
|
304
310
|
def prompt_pattern
|
|
305
311
|
@config&.prompt_pattern || Consolle::Config::DEFAULT_PROMPT_PATTERN
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'timeout'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
require_relative 'base_supervisor'
|
|
6
|
+
require_relative '../errors'
|
|
7
|
+
|
|
8
|
+
module Consolle
|
|
9
|
+
module Server
|
|
10
|
+
# IRB embedding mode supervisor (Ruby 3.3+)
|
|
11
|
+
# Runs IRB directly in-process without PTY
|
|
12
|
+
# Supports two modes:
|
|
13
|
+
# - :embed_irb - Pure IRB without Rails
|
|
14
|
+
# - :embed_rails - Rails console with IRB
|
|
15
|
+
class EmbeddedSupervisor < BaseSupervisor
|
|
16
|
+
MINIMUM_RUBY_VERSION = Gem::Version.new('3.3.0')
|
|
17
|
+
VALID_MODES = %i[embed_irb embed_rails].freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def supported?
|
|
21
|
+
Gem::Version.new(RUBY_VERSION) >= MINIMUM_RUBY_VERSION
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def check_support!
|
|
25
|
+
return if supported?
|
|
26
|
+
|
|
27
|
+
raise Consolle::Errors::UnsupportedRubyVersion.new(
|
|
28
|
+
"Embedded mode requires Ruby #{MINIMUM_RUBY_VERSION}+, " \
|
|
29
|
+
"current version: #{RUBY_VERSION}"
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param rails_root [String] Project root directory
|
|
35
|
+
# @param rails_env [String] Rails environment (only for embed_rails mode)
|
|
36
|
+
# @param logger [Logger] Logger instance
|
|
37
|
+
# @param embed_mode [Symbol] :embed_irb or :embed_rails
|
|
38
|
+
def initialize(rails_root:, rails_env: 'development', logger: nil, embed_mode: :embed_rails)
|
|
39
|
+
super(rails_root: rails_root, rails_env: rails_env, logger: logger)
|
|
40
|
+
@embed_mode = validate_embed_mode(embed_mode)
|
|
41
|
+
@running = false
|
|
42
|
+
@workspace = nil
|
|
43
|
+
@mutex = Mutex.new
|
|
44
|
+
|
|
45
|
+
boot_environment
|
|
46
|
+
setup_irb
|
|
47
|
+
@running = true
|
|
48
|
+
|
|
49
|
+
mode_name = @embed_mode == :embed_rails ? 'Rails console' : 'IRB'
|
|
50
|
+
logger.info "[EmbeddedSupervisor] #{mode_name} embedded mode initialized (Ruby #{RUBY_VERSION})"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def eval(code, timeout: nil, pre_sigint: nil)
|
|
54
|
+
# pre_sigint is ignored in embedded mode (no PTY)
|
|
55
|
+
|
|
56
|
+
env_timeout = ENV['CONSOLLE_TIMEOUT']&.to_i
|
|
57
|
+
timeout = if env_timeout && env_timeout.positive?
|
|
58
|
+
env_timeout
|
|
59
|
+
else
|
|
60
|
+
timeout || 60
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
raise 'Console is not running' unless running?
|
|
65
|
+
|
|
66
|
+
start_time = Time.now
|
|
67
|
+
execute_code(code, timeout, start_time)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def running?
|
|
72
|
+
@running && @workspace
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def stop
|
|
76
|
+
@running = false
|
|
77
|
+
@workspace = nil
|
|
78
|
+
logger.info '[EmbeddedSupervisor] Stopped'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def restart
|
|
82
|
+
logger.info '[EmbeddedSupervisor] Restarting IRB workspace...'
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
setup_irb
|
|
85
|
+
end
|
|
86
|
+
logger.info '[EmbeddedSupervisor] IRB workspace restarted'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def mode
|
|
90
|
+
@embed_mode
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns the current process PID (embedded mode runs in-process)
|
|
94
|
+
# @return [Integer]
|
|
95
|
+
def pid
|
|
96
|
+
Process.pid
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns the prompt pattern (for compatibility with PTY mode)
|
|
100
|
+
def prompt_pattern
|
|
101
|
+
@config&.prompt_pattern || Consolle::Config::DEFAULT_PROMPT_PATTERN
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def validate_embed_mode(mode)
|
|
107
|
+
mode_sym = mode.to_sym
|
|
108
|
+
return mode_sym if VALID_MODES.include?(mode_sym)
|
|
109
|
+
|
|
110
|
+
raise ArgumentError, "Invalid embed_mode: #{mode}. Must be one of: #{VALID_MODES.join(', ')}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def boot_environment
|
|
114
|
+
return boot_rails if @embed_mode == :embed_rails
|
|
115
|
+
|
|
116
|
+
# For embed_irb mode, no Rails loading needed
|
|
117
|
+
logger.info '[EmbeddedSupervisor] Pure IRB mode - skipping Rails environment'
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def boot_rails
|
|
121
|
+
return if defined?(Rails) && Rails.application
|
|
122
|
+
|
|
123
|
+
ENV['RAILS_ENV'] = @rails_env
|
|
124
|
+
|
|
125
|
+
environment_file = File.join(@rails_root, 'config', 'environment.rb')
|
|
126
|
+
unless File.exist?(environment_file)
|
|
127
|
+
raise Consolle::Errors::ConfigurationError.new(
|
|
128
|
+
"Rails environment file not found: #{environment_file}"
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
logger.info "[EmbeddedSupervisor] Loading Rails environment from #{environment_file}"
|
|
133
|
+
require environment_file
|
|
134
|
+
logger.info "[EmbeddedSupervisor] Rails #{Rails.version} loaded (#{Rails.env})"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def setup_irb
|
|
138
|
+
require 'irb'
|
|
139
|
+
|
|
140
|
+
# Initialize IRB if not already done
|
|
141
|
+
IRB.setup(nil, argv: []) unless IRB.conf[:PROMPT]
|
|
142
|
+
|
|
143
|
+
# Configure IRB for automation
|
|
144
|
+
IRB.conf[:USE_COLORIZE] = false
|
|
145
|
+
IRB.conf[:USE_AUTOCOMPLETE] = false
|
|
146
|
+
IRB.conf[:USE_PAGER] = false
|
|
147
|
+
IRB.conf[:VERBOSE] = false
|
|
148
|
+
IRB.conf[:USE_MULTILINE] = false
|
|
149
|
+
|
|
150
|
+
# Create workspace with top-level binding for Rails Console-like behavior
|
|
151
|
+
@workspace = IRB::WorkSpace.new(TOPLEVEL_BINDING)
|
|
152
|
+
|
|
153
|
+
# Inject Rails console helpers if available
|
|
154
|
+
inject_rails_console_methods
|
|
155
|
+
|
|
156
|
+
logger.debug '[EmbeddedSupervisor] IRB workspace configured'
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def inject_rails_console_methods
|
|
160
|
+
# Only inject for embed_rails mode
|
|
161
|
+
return unless @embed_mode == :embed_rails
|
|
162
|
+
|
|
163
|
+
begin
|
|
164
|
+
# Rails 7.1 and earlier: use Rails::ConsoleMethods
|
|
165
|
+
if defined?(Rails::ConsoleMethods)
|
|
166
|
+
@workspace.binding.eval('extend Rails::ConsoleMethods')
|
|
167
|
+
logger.debug '[EmbeddedSupervisor] Rails::ConsoleMethods injected'
|
|
168
|
+
else
|
|
169
|
+
# Rails 7.2+: ConsoleMethods moved, define reload! directly
|
|
170
|
+
inject_reload_method
|
|
171
|
+
logger.debug '[EmbeddedSupervisor] reload! method injected directly'
|
|
172
|
+
end
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
logger.warn "[EmbeddedSupervisor] Failed to inject console methods: #{e.message}"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def inject_reload_method
|
|
179
|
+
# Define reload! method that calls Rails reloader
|
|
180
|
+
@workspace.binding.eval(<<~RUBY)
|
|
181
|
+
def reload!(print = true)
|
|
182
|
+
puts "Reloading..." if print
|
|
183
|
+
Rails.application.reloader.reload!
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
RUBY
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def execute_code(code, timeout, start_time)
|
|
190
|
+
stdout_capture = StringIO.new
|
|
191
|
+
stderr_capture = StringIO.new
|
|
192
|
+
result = nil
|
|
193
|
+
error = nil
|
|
194
|
+
|
|
195
|
+
# Capture stdout/stderr
|
|
196
|
+
original_stdout = $stdout
|
|
197
|
+
original_stderr = $stderr
|
|
198
|
+
$stdout = stdout_capture
|
|
199
|
+
$stderr = stderr_capture
|
|
200
|
+
|
|
201
|
+
begin
|
|
202
|
+
Timeout.timeout(timeout) do
|
|
203
|
+
# Use workspace.binding.eval for full IRB context
|
|
204
|
+
result = @workspace.binding.eval(code, '(consolle)', 1)
|
|
205
|
+
end
|
|
206
|
+
rescue Timeout::Error
|
|
207
|
+
$stdout = original_stdout
|
|
208
|
+
$stderr = original_stderr
|
|
209
|
+
return build_timeout_response(timeout)
|
|
210
|
+
rescue SyntaxError => e
|
|
211
|
+
error = e
|
|
212
|
+
rescue StandardError => e
|
|
213
|
+
error = e
|
|
214
|
+
ensure
|
|
215
|
+
$stdout = original_stdout
|
|
216
|
+
$stderr = original_stderr
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
execution_time = Time.now - start_time
|
|
220
|
+
captured_output = stdout_capture.string + stderr_capture.string
|
|
221
|
+
|
|
222
|
+
if error
|
|
223
|
+
build_error_response_with_output(error, captured_output, execution_time)
|
|
224
|
+
else
|
|
225
|
+
build_success_response_with_result(result, captured_output, execution_time)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def build_error_response_with_output(error, captured_output, execution_time)
|
|
230
|
+
output = captured_output.empty? ? "#{error.class}: #{error.message}" : captured_output
|
|
231
|
+
|
|
232
|
+
{
|
|
233
|
+
success: false,
|
|
234
|
+
error_class: error.class.name,
|
|
235
|
+
error_code: Consolle::Errors::ErrorClassifier.to_code(error),
|
|
236
|
+
output: output,
|
|
237
|
+
execution_time: execution_time
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def build_success_response_with_result(result, captured_output, execution_time)
|
|
242
|
+
# Format output like Rails console
|
|
243
|
+
# If there's captured stdout, include it
|
|
244
|
+
# Always include the return value with => prefix
|
|
245
|
+
output_parts = []
|
|
246
|
+
output_parts << captured_output unless captured_output.empty?
|
|
247
|
+
output_parts << "=> #{result.inspect}"
|
|
248
|
+
|
|
249
|
+
{
|
|
250
|
+
success: true,
|
|
251
|
+
output: output_parts.join("\n"),
|
|
252
|
+
result: result,
|
|
253
|
+
execution_time: execution_time
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'console_supervisor'
|
|
4
|
+
require_relative 'embedded_supervisor'
|
|
5
|
+
|
|
6
|
+
module Consolle
|
|
7
|
+
module Server
|
|
8
|
+
# Factory for creating the appropriate supervisor based on configuration
|
|
9
|
+
#
|
|
10
|
+
# Modes:
|
|
11
|
+
# - :pty - PTY-based, supports custom command (local/remote)
|
|
12
|
+
# - :embed_irb - Pure IRB embedding (Ruby 3.3+, local only)
|
|
13
|
+
# - :embed_rails - Rails console embedding (Ruby 3.3+, local only)
|
|
14
|
+
class SupervisorFactory
|
|
15
|
+
MODES = %i[pty embed_irb embed_rails].freeze
|
|
16
|
+
EMBEDDED_MODES = %i[embed_irb embed_rails].freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Create a supervisor instance
|
|
20
|
+
# @param rails_root [String] Path to project root
|
|
21
|
+
# @param mode [Symbol] :pty, :embed_irb, or :embed_rails
|
|
22
|
+
# @param options [Hash] Additional options passed to supervisor
|
|
23
|
+
# @return [BaseSupervisor] PTY or Embedded supervisor
|
|
24
|
+
def create(rails_root:, mode: :pty, **options)
|
|
25
|
+
mode = normalize_mode(mode)
|
|
26
|
+
validate_mode!(mode)
|
|
27
|
+
|
|
28
|
+
case mode
|
|
29
|
+
when :pty
|
|
30
|
+
create_pty_supervisor(rails_root, options)
|
|
31
|
+
when :embed_irb
|
|
32
|
+
create_embedded_supervisor(rails_root, :embed_irb, options)
|
|
33
|
+
when :embed_rails
|
|
34
|
+
create_embedded_supervisor(rails_root, :embed_rails, options)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if embedded mode is available (Ruby 3.3+)
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def embedded_available?
|
|
41
|
+
EmbeddedSupervisor.supported?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def normalize_mode(mode)
|
|
47
|
+
mode_sym = mode.to_s.tr('-', '_').to_sym
|
|
48
|
+
|
|
49
|
+
# Handle legacy mode names
|
|
50
|
+
case mode_sym
|
|
51
|
+
when :embedded then :embed_rails
|
|
52
|
+
when :auto then :pty
|
|
53
|
+
else mode_sym
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_mode!(mode)
|
|
58
|
+
return if MODES.include?(mode)
|
|
59
|
+
|
|
60
|
+
raise ArgumentError, "Invalid mode: #{mode}. Must be one of: #{MODES.join(', ')}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def create_pty_supervisor(rails_root, options)
|
|
64
|
+
ConsoleSupervisor.new(
|
|
65
|
+
rails_root: rails_root,
|
|
66
|
+
rails_env: options[:rails_env] || 'development',
|
|
67
|
+
logger: options[:logger],
|
|
68
|
+
command: options[:command],
|
|
69
|
+
wait_timeout: options[:wait_timeout]
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def create_embedded_supervisor(rails_root, embed_mode, options)
|
|
74
|
+
EmbeddedSupervisor.check_support!
|
|
75
|
+
|
|
76
|
+
EmbeddedSupervisor.new(
|
|
77
|
+
rails_root: rails_root,
|
|
78
|
+
rails_env: options[:rails_env] || 'development',
|
|
79
|
+
logger: options[:logger],
|
|
80
|
+
embed_mode: embed_mode
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/rule.ko.md
CHANGED
|
@@ -37,6 +37,46 @@ $ cone stop # 서버 중지
|
|
|
37
37
|
|
|
38
38
|
작업을 마치면 반드시 종료해 주세요.
|
|
39
39
|
|
|
40
|
+
## 실행 모드
|
|
41
|
+
|
|
42
|
+
Cone은 세 가지 실행 모드를 지원합니다. `--mode` 옵션으로 지정할 수 있습니다.
|
|
43
|
+
|
|
44
|
+
| 모드 | 설명 | Ruby 요구사항 | 실행 속도 |
|
|
45
|
+
|------|------|--------------|----------|
|
|
46
|
+
| `pty` | PTY 기반, 커스텀 명령어 지원 (기본값) | 모든 버전 | ~0.6s |
|
|
47
|
+
| `embed-rails` | Rails 콘솔 임베딩 | Ruby 3.3+ | ~0.001s |
|
|
48
|
+
| `embed-irb` | 순수 IRB 임베딩 (Rails 미로드) | Ruby 3.3+ | ~0.001s |
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
$ cone start # PTY 모드 (기본값)
|
|
52
|
+
$ cone start --mode embed-rails # Rails 콘솔 임베딩 (200배 빠름)
|
|
53
|
+
$ cone start --mode embed-irb # 순수 IRB 임베딩 (Rails 없이)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 모드 선택 기준
|
|
57
|
+
|
|
58
|
+
- **`pty`**: 원격 환경(SSH, Docker, Kamal)이나 커스텀 명령어가 필요한 경우
|
|
59
|
+
- **`embed-rails`**: 로컬 Rails 개발에서 빠른 실행이 필요한 경우
|
|
60
|
+
- **`embed-irb`**: Rails 없이 순수 Ruby 코드만 실행하는 경우
|
|
61
|
+
|
|
62
|
+
### 커스텀 명령어 (PTY 모드 전용)
|
|
63
|
+
|
|
64
|
+
PTY 모드에서는 `--command` 옵션으로 커스텀 콘솔 명령어를 지정할 수 있습니다.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
$ cone start --command "docker exec -it app bin/rails console"
|
|
68
|
+
$ cone start --command "kamal console" --wait-timeout 60
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 설정 파일
|
|
72
|
+
|
|
73
|
+
프로젝트 루트에 `.consolle.yml` 파일로 기본 모드를 설정할 수 있습니다. CLI 옵션은 설정 파일보다 우선합니다.
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
mode: embed-rails
|
|
77
|
+
# command: "bin/rails console" # PTY 모드 전용
|
|
78
|
+
```
|
|
79
|
+
|
|
40
80
|
## Cone 서버 상태 확인
|
|
41
81
|
|
|
42
82
|
```bash
|
|
@@ -74,10 +114,13 @@ $ cone exec 'puts u'
|
|
|
74
114
|
$ cone exec -f example.rb
|
|
75
115
|
```
|
|
76
116
|
|
|
77
|
-
디버깅을 위한 `-v` 옵션(Verbose 출력)이 제공됩니다.
|
|
117
|
+
디버깅을 위한 `-v` 옵션(Verbose 출력)이 제공됩니다. 실행 시간 및 추가 정보를 표시합니다.
|
|
78
118
|
|
|
79
119
|
```bash
|
|
80
120
|
$ cone exec -v 'puts "hello, world"'
|
|
121
|
+
hello, world
|
|
122
|
+
=> nil
|
|
123
|
+
Execution time: 0.001s
|
|
81
124
|
```
|
|
82
125
|
|
|
83
126
|
## 코드 입력 모범 사례
|
data/rule.md
CHANGED
|
@@ -37,6 +37,46 @@ $ cone stop # Stop server
|
|
|
37
37
|
|
|
38
38
|
Always terminate when you finish your work.
|
|
39
39
|
|
|
40
|
+
## Execution Modes
|
|
41
|
+
|
|
42
|
+
Cone supports three execution modes. You can specify the mode with the `--mode` option.
|
|
43
|
+
|
|
44
|
+
| Mode | Description | Ruby Requirement | Execution Speed |
|
|
45
|
+
|------|-------------|-----------------|-----------------|
|
|
46
|
+
| `pty` | PTY-based, supports custom commands (default) | All versions | ~0.6s |
|
|
47
|
+
| `embed-rails` | Rails console embedding | Ruby 3.3+ | ~0.001s |
|
|
48
|
+
| `embed-irb` | Pure IRB embedding (no Rails) | Ruby 3.3+ | ~0.001s |
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
$ cone start # PTY mode (default)
|
|
52
|
+
$ cone start --mode embed-rails # Rails console embedding (200x faster)
|
|
53
|
+
$ cone start --mode embed-irb # Pure IRB embedding (without Rails)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Mode Selection Guide
|
|
57
|
+
|
|
58
|
+
- **`pty`**: For remote environments (SSH, Docker, Kamal) or when custom commands are needed
|
|
59
|
+
- **`embed-rails`**: For local Rails development when fast execution is required
|
|
60
|
+
- **`embed-irb`**: When running pure Ruby code without Rails
|
|
61
|
+
|
|
62
|
+
### Custom Commands (PTY Mode Only)
|
|
63
|
+
|
|
64
|
+
In PTY mode, you can specify a custom console command with the `--command` option.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
$ cone start --command "docker exec -it app bin/rails console"
|
|
68
|
+
$ cone start --command "kamal console" --wait-timeout 60
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Configuration File
|
|
72
|
+
|
|
73
|
+
You can set the default mode in a `.consolle.yml` file at the project root. CLI options take precedence over the configuration file.
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
mode: embed-rails
|
|
77
|
+
# command: "bin/rails console" # PTY mode only
|
|
78
|
+
```
|
|
79
|
+
|
|
40
80
|
## Checking Cone Server Status
|
|
41
81
|
|
|
42
82
|
```bash
|
|
@@ -74,10 +114,13 @@ You can also execute Ruby files directly using the `-f` option. Unlike Rails Run
|
|
|
74
114
|
$ cone exec -f example.rb
|
|
75
115
|
```
|
|
76
116
|
|
|
77
|
-
A `-v` option (Verbose output) is provided for debugging.
|
|
117
|
+
A `-v` option (Verbose output) is provided for debugging. It shows execution time and additional details.
|
|
78
118
|
|
|
79
119
|
```bash
|
|
80
120
|
$ cone exec -v 'puts "hello, world"'
|
|
121
|
+
hello, world
|
|
122
|
+
=> nil
|
|
123
|
+
Execution time: 0.001s
|
|
81
124
|
```
|
|
82
125
|
|
|
83
126
|
## Best Practices for Code Input
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: consolle
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- nacyot
|
|
@@ -82,9 +82,12 @@ files:
|
|
|
82
82
|
- lib/consolle/config.rb
|
|
83
83
|
- lib/consolle/constants.rb
|
|
84
84
|
- lib/consolle/errors.rb
|
|
85
|
+
- lib/consolle/server/base_supervisor.rb
|
|
85
86
|
- lib/consolle/server/console_socket_server.rb
|
|
86
87
|
- lib/consolle/server/console_supervisor.rb
|
|
88
|
+
- lib/consolle/server/embedded_supervisor.rb
|
|
87
89
|
- lib/consolle/server/request_broker.rb
|
|
90
|
+
- lib/consolle/server/supervisor_factory.rb
|
|
88
91
|
- lib/consolle/version.rb
|
|
89
92
|
- mise/release.sh
|
|
90
93
|
- rule.ko.md
|