consolle 0.3.6 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b3d8a284ca63f38a9e02ca394141f50cd4c0d02a63bae7f78d384a027d0c400
4
- data.tar.gz: 7ddfdc210a765e4fc8c6598e5b2d38ffe78f2997a4811c55f6bf461862ccba0a
3
+ metadata.gz: a88f6019a32aaae300dee3a3e39b1dd1084ea1e2900171d6ee02f880923e071e
4
+ data.tar.gz: e9c468bc8095d8197e8f70cc4c0b75d0cc8da9ba3a7b0af45573300b755e1970
5
5
  SHA512:
6
- metadata.gz: ce17c1df36c8284f051bfd658e63252846547e0886cbe4330005470ce4997f24334a470196de0310f24cccf7f305b4c358985752d301cc76a9514d15c296feb5
7
- data.tar.gz: 93ff0d888e2436be4cc2952508e6b8dba29afdd79303434e0126871022c23f67de70cccbfc0c8e7e33c0d853c2f90aaf8c2be61f6d3e0559ee8694b195b85807
6
+ metadata.gz: e5c0615769edda8936547380360ed5a7beeaaeda7569ccf91322daa4658923a82cad4ad87e5ffabeca4c230e89f3d371ebcaa5eec58663b25172ee0ab8c2ba07
7
+ data.tar.gz: 83dd02825015e9f9e73ff48075436f459722cdc045ee4b0b8d90509c5ca91274c27af682549ef114598efcf21d90628fc460360f33a069134b0da63eeab94c81
data/.version CHANGED
@@ -1 +1 @@
1
- 0.3.6
1
+ 0.3.7
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- consolle (0.3.6)
4
+ consolle (0.3.7)
5
5
  logger (~> 1.0)
6
6
  thor (~> 1.0)
7
7
 
data/lib/consolle/cli.rb CHANGED
@@ -418,7 +418,7 @@ module Consolle
418
418
 
419
419
  stop
420
420
  sleep 1
421
- invoke(:start)
421
+ invoke(:start, [], {})
422
422
  else
423
423
  puts 'Restarting Rails console subprocess...'
424
424
 
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Consolle
6
+ # Configuration loader for .consolle.yml
7
+ class Config
8
+ CONFIG_FILENAME = '.consolle.yml'
9
+
10
+ # Default prompt pattern that matches various console prompts
11
+ # - Custom sentinel: \u001E\u001F<CONSOLLE>\u001F\u001E
12
+ # - Rails app prompts: app(env)> or app(env):001>
13
+ # - IRB prompts: irb(main):001:0> or irb(main):001>
14
+ # - Generic prompts: >> or >
15
+ DEFAULT_PROMPT_PATTERN = /^[^\w]*(\u001E\u001F<CONSOLLE>\u001F\u001E|\w+[-_]?\w*\([^)]*\)(:\d+)?>|irb\([^)]+\):\d+:?\d*[>*]|>>|>)\s*$/
16
+
17
+ attr_reader :rails_root, :prompt_pattern, :raw_prompt_pattern
18
+
19
+ def initialize(rails_root)
20
+ @rails_root = rails_root
21
+ @config = load_config
22
+ @raw_prompt_pattern = @config['prompt_pattern']
23
+ @prompt_pattern = parse_prompt_pattern(@raw_prompt_pattern)
24
+ end
25
+
26
+ def self.load(rails_root)
27
+ new(rails_root)
28
+ end
29
+
30
+ # Check if a custom prompt pattern is configured
31
+ def custom_prompt_pattern?
32
+ !@raw_prompt_pattern.nil?
33
+ end
34
+
35
+ # Get human-readable description of expected prompt patterns
36
+ def prompt_pattern_description
37
+ if custom_prompt_pattern?
38
+ "Custom pattern: #{@raw_prompt_pattern}"
39
+ else
40
+ <<~DESC.strip
41
+ Default patterns:
42
+ - app(env)> or app(env):001> (Rails console)
43
+ - irb(main):001:0> (IRB)
44
+ - >> or > (Generic)
45
+ DESC
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def config_path
52
+ File.join(@rails_root, CONFIG_FILENAME)
53
+ end
54
+
55
+ def load_config
56
+ return {} unless File.exist?(config_path)
57
+
58
+ begin
59
+ YAML.safe_load(File.read(config_path)) || {}
60
+ rescue Psych::SyntaxError => e
61
+ warn "[Consolle] Warning: Failed to parse #{CONFIG_FILENAME}: #{e.message}"
62
+ {}
63
+ end
64
+ end
65
+
66
+ def parse_prompt_pattern(pattern_string)
67
+ return DEFAULT_PROMPT_PATTERN if pattern_string.nil?
68
+
69
+ begin
70
+ Regexp.new(pattern_string)
71
+ rescue RegexpError => e
72
+ warn "[Consolle] Warning: Invalid prompt_pattern '#{pattern_string}': #{e.message}"
73
+ warn "[Consolle] Using default pattern instead."
74
+ DEFAULT_PROMPT_PATTERN
75
+ end
76
+ end
77
+ end
78
+ end
@@ -39,6 +39,56 @@ module Consolle
39
39
  end
40
40
  end
41
41
 
42
+ # Prompt detection failure with diagnostic information
43
+ class PromptDetectionError < Error
44
+ attr_reader :received_output, :expected_patterns, :config_path
45
+
46
+ def initialize(timeout:, received_output:, expected_patterns:, config_path:)
47
+ @received_output = received_output
48
+ @expected_patterns = expected_patterns
49
+ @config_path = config_path
50
+
51
+ super(build_message(timeout))
52
+ end
53
+
54
+ private
55
+
56
+ def build_message(timeout)
57
+ # Extract last line that looks like a prompt (ends with > or similar)
58
+ lines = @received_output.to_s.lines.map(&:strip).reject(&:empty?)
59
+ potential_prompt = lines.reverse.find { |l| l.match?(/[>*]\s*$/) } || lines.last
60
+
61
+ <<~MSG.strip
62
+ Prompt not detected after #{timeout} seconds
63
+
64
+ Received output:
65
+ #{@received_output.to_s.lines.last(5).map { |l| l.strip }.join("\n ")}
66
+
67
+ Potential prompt found:
68
+ #{potential_prompt.inspect}
69
+
70
+ #{@expected_patterns}
71
+
72
+ To fix this, add to #{@config_path}:
73
+ prompt_pattern: '#{escape_for_yaml(potential_prompt)}'
74
+
75
+ Or set environment variable:
76
+ CONSOLLE_PROMPT_PATTERN='#{escape_for_yaml(potential_prompt)}' cone start
77
+ MSG
78
+ end
79
+
80
+ def escape_for_yaml(str)
81
+ return '' if str.nil?
82
+ # Escape special regex characters and create a simple pattern
83
+ str.to_s
84
+ .gsub(/\e\[[\d;]*[a-zA-Z]/, '') # Remove ANSI codes
85
+ .gsub(/[\x00-\x1F]/, '') # Remove control characters
86
+ .strip
87
+ .gsub(/(\d+)/, '\d+') # Replace numbers with \d+
88
+ .gsub(/([().\[\]{}|*+?^$\\])/, '\\\\\1') # Escape regex special chars
89
+ end
90
+ end
91
+
42
92
  # Syntax error in executed code
43
93
  class SyntaxError < ExecutionError
44
94
  def initialize(message)
@@ -80,7 +130,8 @@ module Consolle
80
130
  '::RuntimeError' => 'RUNTIME_ERROR',
81
131
  'StandardError' => 'STANDARD_ERROR',
82
132
  'Exception' => 'EXCEPTION',
83
- 'Consolle::Errors::ServerUnhealthy' => 'SERVER_UNHEALTHY'
133
+ 'Consolle::Errors::ServerUnhealthy' => 'SERVER_UNHEALTHY',
134
+ 'Consolle::Errors::PromptDetectionError' => 'PROMPT_DETECTION_ERROR'
84
135
  }.freeze
85
136
 
86
137
  def self.to_code(exception)
@@ -5,6 +5,7 @@ require 'timeout'
5
5
  require 'fcntl'
6
6
  require 'logger'
7
7
  require_relative '../constants'
8
+ require_relative '../config'
8
9
  require_relative '../errors'
9
10
 
10
11
  # Ruby 3.4.0+ extracts base64 as a default gem
@@ -17,15 +18,13 @@ $VERBOSE = original_verbose
17
18
  module Consolle
18
19
  module Server
19
20
  class ConsoleSupervisor
20
- attr_reader :pid, :reader, :writer, :rails_root, :rails_env, :logger
21
+ attr_reader :pid, :reader, :writer, :rails_root, :rails_env, :logger, :config
21
22
 
22
23
  RESTART_DELAY = 1 # seconds
23
24
  MAX_RESTARTS = 5 # within 5 minutes
24
25
  RESTART_WINDOW = 300 # 5 minutes
25
- # Match various Rails console prompts
26
- # Match various console prompts: custom sentinel, Rails app prompts, IRB prompts, and generic prompts
27
- # Allow optional non-word characters before the prompt (e.g., Unicode symbols like ▽)
28
- PROMPT_PATTERN = /^[^\w]*(\u001E\u001F<CONSOLLE>\u001F\u001E|\w+[-_]?\w*\([^)]*\)>|irb\([^)]+\):\d+:?\d*[>*]|>>|>)\s*$/
26
+ # Legacy constant for backward compatibility - use config.prompt_pattern instead
27
+ PROMPT_PATTERN = Consolle::Config::DEFAULT_PROMPT_PATTERN
29
28
  CTRL_C = "\x03"
30
29
 
31
30
  def initialize(rails_root:, rails_env: 'development', logger: nil, command: nil, wait_timeout: nil)
@@ -34,6 +33,7 @@ module Consolle
34
33
  @command = command || 'bin/rails console'
35
34
  @logger = logger || Logger.new(STDOUT)
36
35
  @wait_timeout = wait_timeout || Consolle::DEFAULT_WAIT_TIMEOUT
36
+ @config = Consolle::Config.load(rails_root)
37
37
  @pid = nil
38
38
  @reader = nil
39
39
  @writer = nil
@@ -227,7 +227,7 @@ module Consolle
227
227
 
228
228
  # Check if we got prompt back
229
229
  clean = strip_ansi(output)
230
- if clean.match?(PROMPT_PATTERN)
230
+ if clean.match?(prompt_pattern)
231
231
  # Wait a bit for any trailing output
232
232
  sleep 0.1
233
233
  begin
@@ -300,6 +300,11 @@ module Consolle
300
300
  end
301
301
  end
302
302
 
303
+ # Returns the prompt pattern to use (custom from config or default)
304
+ def prompt_pattern
305
+ @config&.prompt_pattern || Consolle::Config::DEFAULT_PROMPT_PATTERN
306
+ end
307
+
303
308
  def stop
304
309
  @running = false
305
310
 
@@ -531,7 +536,16 @@ module Consolle
531
536
  logger.error "[ConsoleSupervisor] Timeout reached. Current: #{current_time}, Deadline: #{deadline}, Elapsed: #{current_time - start_time}s"
532
537
  logger.error "[ConsoleSupervisor] Output so far: #{output.inspect}"
533
538
  logger.error "[ConsoleSupervisor] Stripped: #{strip_ansi(output).inspect}"
534
- raise Timeout::Error, "No prompt after #{timeout} seconds"
539
+
540
+ # Raise PromptDetectionError with diagnostic information
541
+ config_path = File.join(@rails_root || '.', Consolle::Config::CONFIG_FILENAME)
542
+ expected_desc = @config&.prompt_pattern_description || "Default prompt patterns"
543
+ raise Consolle::Errors::PromptDetectionError.new(
544
+ timeout: timeout,
545
+ received_output: strip_ansi(output),
546
+ expected_patterns: expected_desc,
547
+ config_path: config_path
548
+ )
535
549
  end
536
550
 
537
551
  # If we found prompt and consume_all is true, continue reading for a bit more
@@ -560,7 +574,7 @@ module Consolle
560
574
  clean = strip_ansi(output)
561
575
  # Check each line for prompt pattern
562
576
  clean.lines.each do |line|
563
- if line.match?(PROMPT_PATTERN)
577
+ if line.match?(prompt_pattern)
564
578
  logger.info '[ConsoleSupervisor] Found prompt!'
565
579
  prompt_found = true
566
580
  end
@@ -647,7 +661,7 @@ module Consolle
647
661
  # Wait for prompt after configuration with reasonable timeout
648
662
  begin
649
663
  wait_for_prompt(timeout: 2, consume_all: false)
650
- rescue Timeout::Error
664
+ rescue Timeout::Error, Consolle::Errors::PromptDetectionError
651
665
  # This can fail with some console types, but that's okay
652
666
  logger.debug '[ConsoleSupervisor] No prompt after IRB configuration, continuing'
653
667
  end
@@ -686,7 +700,7 @@ module Consolle
686
700
  end
687
701
 
688
702
  # Skip prompts (but not return values that start with =>)
689
- next if line.match?(PROMPT_PATTERN) && !line.start_with?('=>')
703
+ next if line.match?(prompt_pattern) && !line.start_with?('=>')
690
704
 
691
705
  # Skip common IRB configuration output patterns
692
706
  if line.match?(/^(IRB\.conf|DISABLE_PRY_RAILS|Switch to inspect mode|Loading .*\.rb|nil)$/) ||
data/lib/consolle.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative 'consolle/version'
4
4
  require_relative 'consolle/constants'
5
5
  require_relative 'consolle/errors'
6
+ require_relative 'consolle/config'
6
7
  require_relative 'consolle/cli'
7
8
 
8
9
  # Server components
data/rule.ko.md CHANGED
@@ -20,10 +20,10 @@ Cone은 디버깅, 데이터 탐색, 그리고 개발 보조 도구로 사용됩
20
20
 
21
21
  ## Cone 서버 시작과 중지
22
22
 
23
- `start` 명령어로 cone을 시작할 수 있으며, `-e`로 실행 환경을 지정할 있습니다.
23
+ `start` 명령어로 cone을 시작할 수 있습니다. 실행 환경 지정은 `RAILS_ENV` 환경변수를 사용합니다.
24
24
 
25
25
  ```bash
26
- $ cone start # 서버 시작
26
+ $ cone start # 서버 시작 (RAILS_ENV가 없으면 development)
27
27
  $ RAILS_ENV=test cone start # test 환경에서 console 시작
28
28
  ```
29
29
 
@@ -115,3 +115,34 @@ $ cone exec -f complex_task.rb
115
115
  ```
116
116
 
117
117
  모든 방법은 세션 상태를 유지하므로 변수와 객체가 실행 간에 지속됩니다.
118
+
119
+ ## 실행 안전장치 & 타임아웃
120
+
121
+ - 기본 타임아웃: 60초
122
+ - 타임아웃 우선순위: `CONSOLLE_TIMEOUT`(설정되고 0보다 클 때) > CLI `--timeout` > 기본값(60초)
123
+ - 사전 Ctrl‑C(프롬프트 분리):
124
+ - 매 `exec` 전에 Ctrl‑C를 보내고 IRB 프롬프트를 최대 3초 대기해 깨끗한 상태를 보장합니다.
125
+ - 3초 내 프롬프트가 돌아오지 않으면 콘솔 하위 프로세스를 재시작하고 요청은 `SERVER_UNHEALTHY`로 실패합니다.
126
+ - 서버 전역 비활성화: `CONSOLLE_DISABLE_PRE_SIGINT=1 cone start`
127
+ - 호출 단위 제어: `--pre-sigint` / `--no-pre-sigint`
128
+
129
+ ### 예시
130
+
131
+ ```bash
132
+ # CLI로 타임아웃 지정(환경변수 미설정 시 유효)
133
+ cone exec 'heavy_task' --timeout 120
134
+
135
+ # 최우선 타임아웃(클라이언트·서버 모두 적용)
136
+ CONSOLLE_TIMEOUT=90 cone exec 'heavy_task'
137
+
138
+ # 타임아웃 이후 복구 확인
139
+ cone exec 'sleep 999' --timeout 2 # -> EXECUTION_TIMEOUT로 실패
140
+ cone exec "puts :after_timeout; :ok" # -> 정상 동작(프롬프트 복구)
141
+
142
+ # 호출 단위로 사전 Ctrl‑C 비활성화
143
+ cone exec --no-pre-sigint 'code'
144
+ ```
145
+
146
+ ### 에러 코드
147
+ - `EXECUTION_TIMEOUT`: 실행한 코드가 타임아웃을 초과함
148
+ - `SERVER_UNHEALTHY`: 사전 프롬프트 확인(3초) 실패로 콘솔 재시작, 요청 실패
data/rule.md CHANGED
@@ -20,10 +20,10 @@ Existing objects also reference old code, so you need to create new ones to use
20
20
 
21
21
  ## Starting and Stopping Cone Server
22
22
 
23
- You can start cone with the `start` command and specify the execution environment with `-e`.
23
+ You can start cone with the `start` command. To select the Rails environment, set the `RAILS_ENV` environment variable.
24
24
 
25
25
  ```bash
26
- $ cone start # Start server
26
+ $ cone start # Start server (uses RAILS_ENV or defaults to development)
27
27
  $ RAILS_ENV=test cone start # Start console in test environment
28
28
  ```
29
29
 
@@ -115,3 +115,34 @@ $ cone exec -f complex_task.rb
115
115
  ```
116
116
 
117
117
  All methods maintain the session state, so variables and objects persist between executions.
118
+
119
+ ## Execution Safety & Timeouts
120
+
121
+ - Default timeout: 60s
122
+ - Timeout precedence: `CONSOLLE_TIMEOUT` (if set and > 0) > CLI `--timeout` > default (60s)
123
+ - Pre-exec Ctrl-C (prompt separation):
124
+ - Before each `exec`, cone sends Ctrl-C and waits up to 3 seconds for the IRB prompt to ensure a clean state.
125
+ - If the prompt does not return in 3 seconds, the console subprocess is restarted and the request fails with `SERVER_UNHEALTHY`.
126
+ - Disable globally for the server: `CONSOLLE_DISABLE_PRE_SIGINT=1 cone start`
127
+ - Per-call control: `--pre-sigint` / `--no-pre-sigint`
128
+
129
+ ### Examples
130
+
131
+ ```bash
132
+ # Set timeout via CLI (fallback when CONSOLLE_TIMEOUT is not set)
133
+ cone exec 'heavy_task' --timeout 120
134
+
135
+ # Highest priority timeout (applies on client and server)
136
+ CONSOLLE_TIMEOUT=90 cone exec 'heavy_task'
137
+
138
+ # Verify recovery after a timeout
139
+ cone exec 'sleep 999' --timeout 2 # -> fails with EXECUTION_TIMEOUT
140
+ cone exec "puts :after_timeout; :ok" # -> should succeed (prompt recovered)
141
+
142
+ # Disable pre-exec Ctrl-C for a single call
143
+ cone exec --no-pre-sigint 'code'
144
+ ```
145
+
146
+ ### Error Codes
147
+ - `EXECUTION_TIMEOUT`: The executed code exceeded its timeout.
148
+ - `SERVER_UNHEALTHY`: The pre-exec prompt did not return within 3 seconds; the console subprocess was restarted and the request failed.
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: consolle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.6
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - nacyot
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-09-01 00:00:00.000000000 Z
10
+ date: 2025-12-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: logger
@@ -79,6 +79,7 @@ files:
79
79
  - lib/consolle.rb
80
80
  - lib/consolle/adapters/rails_console.rb
81
81
  - lib/consolle/cli.rb
82
+ - lib/consolle/config.rb
82
83
  - lib/consolle/constants.rb
83
84
  - lib/consolle/errors.rb
84
85
  - lib/consolle/server/console_socket_server.rb