thor-interactive 0.1.0.pre.1 → 0.1.0.pre.3

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: 9e51ddada105bae31b9bc637ed25fd88e4908c281c16779b46d023d16f31db4e
4
- data.tar.gz: cbccf2e8dc47f73e7f1c101bd1a0dce0c2fa51cc21f9a26e24ed7f577245d2cf
3
+ metadata.gz: '049af6f0fedd7bda65c5845a878bd02afe8e9e5cf705ebad254c333e0151ecd7'
4
+ data.tar.gz: '049e366b5d3d1e4d0076c7c69d69240b00f0768b3b2687d23e9482dd7bf368dd'
5
5
  SHA512:
6
- metadata.gz: 0ea62f18e17ab19662bd3c8fac62dce8dd69582adc857664901e69729670d8de690161a111151df1ab4ee7b403521b9e9254d9d67942bdc36f244278887d2b21
7
- data.tar.gz: a566b97836f0a11b75e35e35692f0996d3817b5b34dfa2d3c169d84dd125cac453cbeac14bfb76cd99f3253d144b4b550fd3e7a939f7b711e27a114242df2997
6
+ metadata.gz: bc5ccc2b2765f3f5c4591c181445dbc4b0ef66982c982758fb6053fa79de968c7583f019be85a747c51474e6e2f55f8834b9c4fd3314b1186dd6185a9be9e092
7
+ data.tar.gz: cc284a24f5f2a24bb016651e4252a9230d89c84d0e86b382fb4b936d11063422f78338d4c0917aedcf0a6e6d9fe7dcb66f8e7fb22c479c2903f355265e00cc79
data/README.md CHANGED
@@ -55,10 +55,11 @@ Now your app supports both modes:
55
55
  # Normal CLI usage (unchanged)
56
56
  ruby myapp.rb hello World
57
57
 
58
- # New interactive mode
58
+ # New interactive mode with slash commands
59
59
  ruby myapp.rb interactive
60
- myapp> hello Alice
60
+ myapp> /hello Alice
61
61
  Hello Alice!
62
+ myapp> Natural language input goes to default handler
62
63
  myapp> exit
63
64
  ```
64
65
 
@@ -116,19 +117,25 @@ In interactive mode:
116
117
  ```bash
117
118
  ruby rag_app.rb interactive
118
119
 
119
- rag> ask "What is Ruby?"
120
- # LLM initializes once
120
+ rag> /ask What is Ruby?
121
+ # LLM initializes once
121
122
  Ruby is a programming language...
122
123
 
123
- rag> ask "Tell me more"
124
+ rag> /ask Tell me more
124
125
  # LLM client reused, conversation context maintained
125
126
  Based on our previous discussion about Ruby...
126
127
 
127
- rag> history
128
+ rag> What's the difference between Ruby and Python?
129
+ # Natural language goes directly to default handler (ask command)
130
+ Ruby and Python differ in several ways...
131
+
132
+ rag> /history
128
133
  1. Q: What is Ruby?
129
134
  A: Ruby is a programming language...
130
135
  2. Q: Tell me more
131
136
  A: Based on our previous discussion about Ruby...
137
+ 3. Q: What's the difference between Ruby and Python?
138
+ A: Ruby and Python differ in several ways...
132
139
  ```
133
140
 
134
141
  ## Configuration
@@ -145,7 +152,10 @@ class MyApp < Thor
145
152
  nested_prompt_format: "[L%d] %s", # Format for nested prompts (if allowed)
146
153
  default_handler: proc do |input, thor_instance|
147
154
  # Handle unrecognized input
148
- thor_instance.invoke(:search, [input])
155
+ # IMPORTANT: Use direct method calls, NOT invoke(), to avoid Thor's
156
+ # silent failure on repeated calls to the same method
157
+ thor_instance.search(input) # ✅ Works repeatedly
158
+ # thor_instance.invoke(:search, [input]) # ❌ Fails after first call
149
159
  end
150
160
  )
151
161
 
@@ -226,6 +236,28 @@ advanced> exit
226
236
  Goodbye!
227
237
  ```
228
238
 
239
+ ### ⚠️ Important: Default Handler Implementation
240
+
241
+ **Always use direct method calls in default handlers, NOT `invoke()`:**
242
+
243
+ ```ruby
244
+ # ✅ CORRECT - Works for repeated calls
245
+ configure_interactive(
246
+ default_handler: proc do |input, thor_instance|
247
+ thor_instance.ask(input) # Direct method call
248
+ end
249
+ )
250
+
251
+ # ❌ WRONG - Silent failure after first call
252
+ configure_interactive(
253
+ default_handler: proc do |input, thor_instance|
254
+ thor_instance.invoke(:ask, [input]) # Thor's invoke fails silently on repeat calls
255
+ end
256
+ )
257
+ ```
258
+
259
+ **Why:** Thor's `invoke` method has internal deduplication that prevents repeated calls to the same method on the same instance. This causes silent failures in interactive mode where users expect to be able to repeat commands.
260
+
229
261
  ## Advanced Usage
230
262
 
231
263
  ### Custom Options
@@ -303,13 +335,14 @@ After checking out the repo:
303
335
 
304
336
  ```bash
305
337
  bundle install # Install dependencies
306
- bundle exec rspec # Run full test suite
338
+ bundle exec rspec # Run full test suite with coverage
307
339
  bundle exec rake build # Build gem
340
+ open coverage/index.html # View coverage report (after running tests)
308
341
  ```
309
342
 
310
343
  ### Testing
311
344
 
312
- The gem includes comprehensive tests organized into unit and integration test suites:
345
+ The gem includes comprehensive tests organized into unit and integration test suites with **72%+ code coverage**:
313
346
 
314
347
  ```bash
315
348
  # Run all tests
@@ -318,6 +351,9 @@ bundle exec rspec
318
351
  # Run with detailed output
319
352
  bundle exec rspec --format documentation
320
353
 
354
+ # View coverage report
355
+ open coverage/index.html # Detailed HTML coverage report
356
+
321
357
  # Run specific test suites
322
358
  bundle exec rspec spec/unit/ # Unit tests only
323
359
  bundle exec rspec spec/integration/ # Integration tests only
data/examples/README.md CHANGED
@@ -26,49 +26,55 @@ ruby sample_app.rb interactive
26
26
  Once in interactive mode:
27
27
 
28
28
  ```
29
- sample> hello Alice
29
+ sample> /hello Alice
30
30
  Hello Alice!
31
31
 
32
- sample> count
32
+ sample> /count
33
33
  Count: 1
34
34
 
35
- sample> count
35
+ sample> /count
36
36
  Count: 2
37
37
 
38
- sample> add "First item"
38
+ sample> /add First item
39
39
  Added 'First item'. Total items: 1
40
40
 
41
- sample> add "Second item"
41
+ sample> /add Second item
42
42
  Added 'Second item'. Total items: 2
43
43
 
44
- sample> list
44
+ sample> /list
45
45
  Items:
46
46
  1. First item
47
47
  2. Second item
48
48
 
49
- sample> status
49
+ sample> /status
50
50
  Application Status:
51
51
  Counter: 2
52
52
  Items in list: 2
53
53
  Memory usage: 15234 KB
54
54
 
55
- sample> This is unrecognized text
56
- Echo: This is unrecognized text
55
+ sample> This is unrecognized text that doesn't need quotes
56
+ Echo: This is unrecognized text that doesn't need quotes
57
57
 
58
- sample> help
59
- Available commands:
60
- hello Say hello to NAME
61
- count Show and increment counter (demonstrates state persistence)
62
- add Add item to list (demonstrates state persistence)
63
- list Show all items
64
- clear Clear all items
65
- echo Echo the text back (used as default handler)
66
- status Show application status
67
- interactive Start an interactive REPL for this application
58
+ sample> What about text with "quotes" and apostrophes?
59
+ Echo: What about text with "quotes" and apostrophes?
60
+
61
+ sample> /help
62
+ Available commands (prefix with /):
63
+ /hello Say hello to NAME
64
+ /count Show and increment counter (demonstrates state persistence)
65
+ /add Add item to list (demonstrates state persistence)
66
+ /list Show all items
67
+ /clear Clear all items
68
+ /echo Echo the text back (used as default handler)
69
+ /status Show application status
70
+ /interactive Start an interactive REPL for this application
68
71
 
69
72
  Special commands:
70
- help [COMMAND] Show help for command
71
- exit/quit/q Exit the REPL
73
+ /help [COMMAND] Show help for command
74
+ /exit, /quit, /q Exit the REPL
75
+
76
+ Natural language mode:
77
+ Type anything without / to use default handler
72
78
 
73
79
  sample> exit
74
80
  Goodbye!
@@ -81,16 +87,17 @@ Goodbye!
81
87
  - In normal CLI mode, each command starts fresh
82
88
 
83
89
  ### 2. Auto-completion
84
- - Tab completion works for command names
85
- - Try typing `h<TAB>` or `a<TAB>` to see completions
90
+ - Tab completion works for command names with slash prefix
91
+ - Try typing `/h<TAB>` or `/co<TAB>` to see completions
86
92
 
87
- ### 3. Default Handler
88
- - Text that doesn't match a command gets sent to the `echo` command
89
- - This is configurable via the `default_handler` option
93
+ ### 3. Natural Language Mode
94
+ - Text without `/` prefix gets sent to the configured default handler
95
+ - No need to worry about quoting or escaping in natural language
96
+ - Perfect for LLM interfaces and conversational commands
90
97
 
91
98
  ### 4. Built-in Help
92
- - `help` shows all available commands
93
- - `help COMMAND` shows help for a specific command
99
+ - `/help` shows all available commands
100
+ - `/help COMMAND` shows help for a specific command
94
101
 
95
102
  ### 5. History
96
103
  - Up/down arrows navigate command history
@@ -98,7 +105,7 @@ Goodbye!
98
105
 
99
106
  ### 6. Graceful Exit
100
107
  - Ctrl+C interrupts current operation
101
- - Ctrl+D or `exit`/`quit`/`q` exits the REPL
108
+ - Ctrl+D, `exit`, `quit`, `q`, `/exit`, `/quit`, or `/q` exits the REPL
102
109
 
103
110
  ## Integration Patterns
104
111
 
@@ -15,7 +15,9 @@ class SampleApp < Thor
15
15
  allow_nested: false, # Prevent nested interactive sessions by default
16
16
  default_handler: proc do |input, thor_instance|
17
17
  # Send unrecognized input to the 'echo' command
18
- thor_instance.invoke(:echo, [input])
18
+ # IMPORTANT: Use direct method calls, NOT invoke(), to avoid Thor's
19
+ # silent deduplication that prevents repeated calls to the same method
20
+ thor_instance.echo(input)
19
21
  end
20
22
  )
21
23
 
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "thor/interactive"
6
+
7
+ class SignalDemo < Thor
8
+ include Thor::Interactive::Command
9
+
10
+ configure_interactive(
11
+ prompt: "signal> ",
12
+ ctrl_c_behavior: :clear_prompt, # Default
13
+ double_ctrl_c_timeout: 0.5 # 500ms window for double Ctrl-C
14
+ )
15
+
16
+ desc "slow", "Simulate a slow command"
17
+ def slow
18
+ puts "Starting slow operation..."
19
+ 5.times do |i|
20
+ puts "Step #{i + 1}/5"
21
+ sleep(1)
22
+ end
23
+ puts "Done!"
24
+ rescue Interrupt
25
+ puts "\nOperation cancelled!"
26
+ end
27
+
28
+ desc "loop", "Run an infinite loop (test Ctrl-C)"
29
+ def loop
30
+ puts "Starting infinite loop (press Ctrl-C to stop)..."
31
+ counter = 0
32
+ while true
33
+ print "\rCounter: #{counter}"
34
+ counter += 1
35
+ sleep(0.1)
36
+ end
37
+ rescue Interrupt
38
+ puts "\nLoop stopped at #{counter}"
39
+ end
40
+
41
+ desc "input", "Test input with special text"
42
+ def input
43
+ puts "Type something with Ctrl chars:"
44
+ puts " - Ctrl-C to clear and start over"
45
+ puts " - Ctrl-D to cancel"
46
+ puts " - Enter to submit"
47
+
48
+ print "input> "
49
+ begin
50
+ text = $stdin.gets
51
+ if text
52
+ puts "You entered: #{text.inspect}"
53
+ else
54
+ puts "Cancelled with Ctrl-D"
55
+ end
56
+ rescue Interrupt
57
+ puts "\nInterrupted - input cancelled"
58
+ end
59
+ end
60
+
61
+ desc "behaviors", "Demo different Ctrl-C behaviors"
62
+ def behaviors
63
+ puts "\n=== Ctrl-C Behavior Options ==="
64
+ puts
65
+ puts "1. :clear_prompt (default)"
66
+ puts " - Shows ^C and hint message"
67
+ puts " - Clear and friendly"
68
+
69
+ puts "\n2. :show_help"
70
+ puts " - Shows help reminder"
71
+ puts " - Good for new users"
72
+
73
+ puts "\n3. :silent"
74
+ puts " - Just clears the line"
75
+ puts " - Minimal interruption"
76
+
77
+ puts "\nYou can configure with:"
78
+ puts " configure_interactive(ctrl_c_behavior: :show_help)"
79
+ end
80
+
81
+ desc "test_clear", "Test with clear_prompt behavior"
82
+ def test_clear
83
+ puts "Starting new shell with :clear_prompt behavior"
84
+ puts "Try pressing Ctrl-C..."
85
+ puts
86
+
87
+ SignalDemo.new.interactive
88
+ end
89
+
90
+ desc "test_help", "Test with show_help behavior"
91
+ def test_help
92
+ puts "Starting new shell with :show_help behavior"
93
+ puts "Try pressing Ctrl-C..."
94
+ puts
95
+
96
+ test_app = Class.new(Thor) do
97
+ include Thor::Interactive::Command
98
+ configure_interactive(
99
+ prompt: "help> ",
100
+ ctrl_c_behavior: :show_help
101
+ )
102
+
103
+ desc "test", "Test command"
104
+ def test
105
+ puts "Test executed"
106
+ end
107
+ end
108
+
109
+ test_app.new.interactive
110
+ end
111
+
112
+ desc "test_silent", "Test with silent behavior"
113
+ def test_silent
114
+ puts "Starting new shell with :silent behavior"
115
+ puts "Try pressing Ctrl-C..."
116
+ puts
117
+
118
+ test_app = Class.new(Thor) do
119
+ include Thor::Interactive::Command
120
+ configure_interactive(
121
+ prompt: "silent> ",
122
+ ctrl_c_behavior: :silent
123
+ )
124
+
125
+ desc "test", "Test command"
126
+ def test
127
+ puts "Test executed"
128
+ end
129
+ end
130
+
131
+ test_app.new.interactive
132
+ end
133
+
134
+ desc "help_signals", "Explain signal handling"
135
+ def help_signals
136
+ puts <<~HELP
137
+
138
+ === Signal Handling in thor-interactive ===
139
+
140
+ CTRL-C (SIGINT):
141
+ Single Press:
142
+ - Clears current input line
143
+ - Shows hint about double Ctrl-C
144
+ - Returns to fresh prompt
145
+
146
+ Double Press (within 500ms):
147
+ - Exits the interactive shell
148
+ - Same as typing 'exit'
149
+
150
+ CTRL-D (EOF):
151
+ - Exits immediately
152
+ - Standard Unix EOF behavior
153
+ - Same as typing 'exit'
154
+
155
+ EXIT COMMANDS:
156
+ - exit
157
+ - quit
158
+ - q
159
+ - /exit, /quit, /q (with slash)
160
+
161
+ CONFIGURATION:
162
+ configure_interactive(
163
+ ctrl_c_behavior: :clear_prompt, # or :show_help, :silent
164
+ double_ctrl_c_timeout: 0.5 # seconds
165
+ )
166
+
167
+ BEHAVIOR OPTIONS:
168
+ :clear_prompt (default)
169
+ Shows "^C" and hint message
170
+
171
+ :show_help
172
+ Shows help reminder on Ctrl-C
173
+
174
+ :silent
175
+ Just clears the line, no message
176
+
177
+ WHY THIS DESIGN?
178
+ - Matches behavior of Python, Node.js REPLs
179
+ - Prevents accidental exit
180
+ - Clear feedback to user
181
+ - Configurable for different preferences
182
+
183
+ HELP
184
+ end
185
+
186
+ default_task :help_signals
187
+ end
188
+
189
+ if __FILE__ == $0
190
+ puts "Signal Handling Demo"
191
+ puts "==================="
192
+ puts
193
+ puts "Try these:"
194
+ puts " 1. Press Ctrl-C once (clears prompt)"
195
+ puts " 2. Press Ctrl-C twice quickly (exits)"
196
+ puts " 3. Press Ctrl-D (exits immediately)"
197
+ puts " 4. Type 'exit', 'quit', or 'q' (exits)"
198
+ puts
199
+ puts "Starting interactive shell..."
200
+ puts
201
+
202
+ SignalDemo.new.interactive
203
+ end
@@ -38,6 +38,16 @@ class Thor
38
38
  def configure_interactive(**options)
39
39
  interactive_options.merge!(options)
40
40
  end
41
+
42
+ # Check if currently running in interactive mode
43
+ def interactive?
44
+ ENV['THOR_INTERACTIVE_SESSION'] == 'true'
45
+ end
46
+ end
47
+
48
+ # Instance method version for use in commands
49
+ def interactive?
50
+ self.class.interactive?
41
51
  end
42
52
  end
43
53
  end
@@ -29,6 +29,12 @@ class Thor
29
29
  @prompt = merged_options[:prompt] || DEFAULT_PROMPT
30
30
  @history_file = File.expand_path(merged_options[:history_file] || DEFAULT_HISTORY_FILE)
31
31
 
32
+ # Ctrl-C handling configuration
33
+ @ctrl_c_behavior = merged_options[:ctrl_c_behavior] || :clear_prompt
34
+ @double_ctrl_c_timeout = merged_options.key?(:double_ctrl_c_timeout) ?
35
+ merged_options[:double_ctrl_c_timeout] : 0.5
36
+ @last_interrupt_time = nil
37
+
32
38
  setup_completion
33
39
  load_history
34
40
  end
@@ -41,6 +47,8 @@ class Thor
41
47
  ENV['THOR_INTERACTIVE_SESSION'] = 'true'
42
48
  ENV['THOR_INTERACTIVE_LEVEL'] = (nesting_level + 1).to_s
43
49
 
50
+ puts "(Debug: Interactive session started, level #{nesting_level + 1})" if ENV["DEBUG"]
51
+
44
52
  # Adjust prompt for nested sessions if configured
45
53
  display_prompt = @prompt
46
54
  if nesting_level > 0 && @merged_options[:nested_prompt_format]
@@ -51,20 +59,46 @@ class Thor
51
59
 
52
60
  show_welcome(nesting_level)
53
61
 
62
+ puts "(Debug: Entering main loop)" if ENV["DEBUG"]
63
+
54
64
  loop do
55
- line = Reline.readline(display_prompt, true)
56
- break if should_exit?(line)
57
-
58
- next if line.nil? || line.strip.empty?
59
-
60
- process_input(line.strip)
61
- rescue Interrupt
62
- puts "\n(Interrupted - press Ctrl+D or type 'exit' to quit)"
63
- rescue => e
64
- puts "Error: #{e.message}"
65
- puts e.backtrace.first(3) if ENV["DEBUG"]
65
+ begin
66
+ line = Reline.readline(display_prompt, true)
67
+ puts "(Debug: Got input: #{line.inspect})" if ENV["DEBUG"]
68
+
69
+ # Reset interrupt tracking on successful input
70
+ @last_interrupt_time = nil if line
71
+
72
+ if should_exit?(line)
73
+ puts "(Debug: Exit condition met)" if ENV["DEBUG"]
74
+ break
75
+ end
76
+
77
+ next if line.nil? || line.strip.empty?
78
+
79
+ puts "(Debug: Processing input: #{line.strip})" if ENV["DEBUG"]
80
+ process_input(line.strip)
81
+ puts "(Debug: Input processed successfully)" if ENV["DEBUG"]
82
+
83
+ rescue Interrupt
84
+ # Handle Ctrl-C
85
+ if handle_interrupt
86
+ break # Exit on double Ctrl-C
87
+ end
88
+ next # Continue on single Ctrl-C
89
+
90
+ rescue SystemExit => e
91
+ puts "A command tried to exit with code #{e.status}. Staying in interactive mode."
92
+ puts "(Debug: SystemExit caught in main loop)" if ENV["DEBUG"]
93
+ rescue => e
94
+ puts "Error: #{e.message}"
95
+ puts e.backtrace.first(5) if ENV["DEBUG"]
96
+ puts "(Debug: Error handled, continuing loop)" if ENV["DEBUG"]
97
+ # Continue the loop - don't let errors break the session
98
+ end
66
99
  end
67
100
 
101
+ puts "(Debug: Exited main loop)" if ENV["DEBUG"]
68
102
  save_history
69
103
  puts nesting_level > 0 ? "Exiting nested session..." : "Goodbye!"
70
104
 
@@ -88,12 +122,22 @@ class Thor
88
122
  end
89
123
 
90
124
  def complete_input(text, preposing)
91
- # If we're at the start of the line, complete command names
92
- if preposing.strip.empty?
93
- complete_commands(text)
125
+ # Handle completion for slash commands
126
+ full_line = preposing + text
127
+
128
+ if full_line.start_with?('/')
129
+ # Command completion mode
130
+ if preposing.strip == '/' || preposing.strip.empty?
131
+ # Complete command names with / prefix
132
+ command_completions = complete_commands(text.sub(/^\//, ''))
133
+ command_completions.map { |cmd| "/#{cmd}" }
134
+ else
135
+ # Complete command arguments (basic implementation)
136
+ complete_command_options(text, preposing)
137
+ end
94
138
  else
95
- # Try to complete command options or let it fall back to file completion
96
- complete_command_options(text, preposing)
139
+ # Natural language mode - no completion for now
140
+ []
97
141
  end
98
142
  end
99
143
 
@@ -114,25 +158,130 @@ class Thor
114
158
  # Handle completely empty input
115
159
  return if input.nil? || input.strip.empty?
116
160
 
117
- args = parse_input(input)
118
- return if args.empty?
161
+ # Check if input starts with / for explicit command mode
162
+ if input.strip.start_with?('/')
163
+ # Explicit command mode: /command args
164
+ handle_slash_command(input.strip[1..-1])
165
+ elsif is_help_request?(input)
166
+ # Special case: treat bare "help" as /help for convenience
167
+ if input.strip.split.length == 1
168
+ show_help
169
+ else
170
+ command_part = input.strip.split[1]
171
+ show_help(command_part)
172
+ end
173
+ else
174
+ # Determine if this looks like a command or natural language
175
+ command_word = input.strip.split(/\s+/, 2).first
176
+
177
+ if thor_command?(command_word)
178
+ # Looks like a command - handle it as a command (backward compatibility)
179
+ handle_command(input.strip)
180
+ elsif @default_handler
181
+ # Natural language mode: send whole input to default handler
182
+ begin
183
+ @default_handler.call(input, @thor_instance)
184
+ rescue => e
185
+ puts "Error in default handler: #{e.message}"
186
+ puts "Input was: #{input}"
187
+ puts "Try using /commands or type '/help' for available commands."
188
+ end
189
+ else
190
+ # No default handler, suggest using command mode
191
+ puts "No default handler configured. Use /command for commands, or type '/help' for available commands."
192
+ end
193
+ end
194
+ end
119
195
 
120
- command = args.shift
196
+ def handle_slash_command(command_input)
197
+ return if command_input.empty?
198
+ handle_command(command_input)
199
+ end
121
200
 
122
- if thor_command?(command)
123
- invoke_thor_command(command, args)
124
- elsif @default_handler
125
- @default_handler.call(input, @thor_instance)
201
+ def handle_command(command_input)
202
+ # Extract command and check if it's a single-text command
203
+ command_word = command_input.split(/\s+/, 2).first
204
+
205
+ if thor_command?(command_word)
206
+ task = @thor_class.tasks[command_word]
207
+
208
+ if task && single_text_command?(task)
209
+ # Single text command - pass everything after command as one argument
210
+ text_part = command_input.sub(/^#{Regexp.escape(command_word)}\s*/, '')
211
+ if text_part.empty?
212
+ invoke_thor_command(command_word, [])
213
+ else
214
+ invoke_thor_command(command_word, [text_part])
215
+ end
216
+ else
217
+ # Multi-argument command, use proper parsing
218
+ args = safe_parse_input(command_input)
219
+ if args && !args.empty?
220
+ command = args.shift
221
+ invoke_thor_command(command, args)
222
+ else
223
+ # Parsing failed, try simple split
224
+ parts = command_input.split(/\s+/)
225
+ command = parts.shift
226
+ invoke_thor_command(command, parts)
227
+ end
228
+ end
126
229
  else
127
- puts "Unknown command: '#{command}'. Type 'help' for available commands."
230
+ puts "Unknown command: '#{command_word}'. Type '/help' for available commands."
128
231
  end
129
232
  end
130
233
 
131
- def parse_input(input)
234
+ def safe_parse_input(input)
235
+ # Try proper shell parsing first
132
236
  Shellwords.split(input)
133
- rescue ArgumentError => e
134
- puts "Error parsing input: #{e.message}"
135
- []
237
+ rescue ArgumentError
238
+ # If parsing fails, return nil so caller can handle it
239
+ nil
240
+ end
241
+
242
+ def parse_input(input)
243
+ # Legacy method - kept for backward compatibility
244
+ safe_parse_input(input) || []
245
+ end
246
+
247
+ def handle_unparseable_command(input, command_word)
248
+ # For commands that failed shell parsing, try intelligent handling
249
+ task = @thor_class.tasks[command_word]
250
+
251
+ # Always try single text approach first for better natural language support
252
+ text_part = input.strip.sub(/^#{Regexp.escape(command_word)}\s*/, '')
253
+ if text_part.empty?
254
+ invoke_thor_command(command_word, [])
255
+ else
256
+ invoke_thor_command(command_word, [text_part])
257
+ end
258
+ end
259
+
260
+ def single_text_command?(task)
261
+ # Heuristic: determine if this is likely a single text command
262
+ return false unless task
263
+
264
+ # Check the method signature to see how many parameters it expects
265
+ method_name = task.name.to_sym
266
+ if @thor_instance.respond_to?(method_name)
267
+ method_obj = @thor_instance.method(method_name)
268
+ param_count = method_obj.parameters.count { |type, _| type == :req }
269
+
270
+ # Only single required parameter = likely text command
271
+ param_count == 1
272
+ else
273
+ # Fallback for introspection issues
274
+ false
275
+ end
276
+ rescue
277
+ # If introspection fails, default to false (safer)
278
+ false
279
+ end
280
+
281
+ def is_help_request?(input)
282
+ # Check if input is a help request (help, ?, etc.)
283
+ stripped = input.strip.downcase
284
+ stripped == "help" || stripped.start_with?("help ")
136
285
  end
137
286
 
138
287
  def thor_command?(command)
@@ -146,14 +295,22 @@ class Thor
146
295
  if command == "help"
147
296
  show_help(args.first)
148
297
  else
149
- # For simple commands, call directly for state persistence
150
- # For complex options/subcommands, this is a basic implementation
298
+ # Always use direct method calls to avoid Thor's invoke deduplication
299
+ # Thor's invoke method silently fails on subsequent calls to the same method
151
300
  if @thor_instance.respond_to?(command)
152
301
  @thor_instance.send(command, *args)
153
302
  else
154
- @thor_instance.invoke(command, args)
303
+ # If method doesn't exist, this will raise a proper error
304
+ @thor_instance.send(command, *args)
155
305
  end
156
306
  end
307
+ rescue SystemExit => e
308
+ if e.status == 0
309
+ puts "Command completed successfully (would have exited with code 0 in CLI mode)"
310
+ else
311
+ puts "Command failed with exit code #{e.status}"
312
+ end
313
+ puts "(Use 'exit' or Ctrl+D to exit the interactive session)" if ENV["DEBUG"]
157
314
  rescue Thor::Error => e
158
315
  puts "Thor Error: #{e.message}"
159
316
  rescue ArgumentError => e
@@ -161,21 +318,36 @@ class Thor
161
318
  puts "Try: help #{command}" if thor_command?(command)
162
319
  rescue StandardError => e
163
320
  puts "Error: #{e.message}"
321
+ puts "Command: #{command}, Args: #{args.inspect}" if ENV["DEBUG"]
164
322
  end
165
323
 
166
324
  def show_help(command = nil)
167
325
  if command && @thor_class.tasks.key?(command)
168
326
  @thor_class.command_help(Thor::Base.shell.new, command)
169
327
  else
170
- puts "Available commands:"
328
+ puts "Available commands (prefix with /):"
171
329
  @thor_class.tasks.each do |name, task|
172
- puts " #{name.ljust(20)} #{task.description}"
330
+ puts " /#{name.ljust(19)} #{task.description}"
173
331
  end
174
332
  puts
175
333
  puts "Special commands:"
176
- puts " help [COMMAND] Show help for command"
177
- puts " exit/quit/q Exit the REPL"
334
+ puts " /help [COMMAND] Show help for command"
335
+ puts " /exit, /quit, /q Exit the REPL"
178
336
  puts
337
+ if @default_handler
338
+ puts "Natural language mode:"
339
+ puts " Type anything without / to use default handler"
340
+ else
341
+ puts "Use /command syntax for all commands"
342
+ end
343
+ puts
344
+ if ENV["DEBUG"]
345
+ puts "Debug info:"
346
+ puts " Thor class: #{@thor_class.name}"
347
+ puts " Available tasks: #{@thor_class.tasks.keys.sort}"
348
+ puts " Instance methods: #{@thor_instance.methods.grep(/^[a-z]/).sort}" if @thor_instance
349
+ puts
350
+ end
179
351
  end
180
352
  end
181
353
 
@@ -183,7 +355,39 @@ class Thor
183
355
  return true if line.nil? # Ctrl+D
184
356
 
185
357
  stripped = line.strip.downcase
186
- EXIT_COMMANDS.include?(stripped)
358
+ # Handle both /exit and exit for convenience
359
+ EXIT_COMMANDS.include?(stripped) || EXIT_COMMANDS.include?(stripped.sub(/^\//, ''))
360
+ end
361
+
362
+ def handle_interrupt
363
+ current_time = Time.now
364
+
365
+ # Check for double Ctrl-C
366
+ if @last_interrupt_time && @double_ctrl_c_timeout && (current_time - @last_interrupt_time) < @double_ctrl_c_timeout
367
+ puts "\n(Interrupted twice - exiting)"
368
+ @last_interrupt_time = nil # Reset for next time
369
+ return true # Signal to exit
370
+ end
371
+
372
+ @last_interrupt_time = current_time
373
+
374
+ # Single Ctrl-C behavior
375
+ case @ctrl_c_behavior
376
+ when :clear_prompt
377
+ puts "^C"
378
+ puts "(Press Ctrl-C again quickly or Ctrl-D to exit)"
379
+ when :show_help
380
+ puts "\n^C - Interrupt"
381
+ puts "Press Ctrl-C again to exit, or type 'help' for commands"
382
+ when :silent
383
+ # Just clear the line, no message
384
+ print "\r#{' ' * 80}\r"
385
+ else
386
+ # Default behavior
387
+ puts "^C"
388
+ end
389
+
390
+ false # Don't exit, just clear prompt
187
391
  end
188
392
 
189
393
  def show_welcome(nesting_level = 0)
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
+ require_relative "version_constant"
4
5
 
5
6
  class Thor
6
7
  module Interactive
7
- VERSION = "0.1.0.pre.1"
8
+ VERSION = ThorInteractive::VERSION
8
9
  end
9
10
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Version constant for thor-interactive gem
4
+ # This file is separate to avoid circular dependencies during gem installation
5
+
6
+ module ThorInteractive
7
+ VERSION = "0.1.0.pre.3"
8
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thor-interactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.1
4
+ version: 0.1.0.pre.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Petersen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-06 00:00:00.000000000 Z
11
+ date: 2025-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -57,11 +57,13 @@ files:
57
57
  - examples/demo_session.rb
58
58
  - examples/nested_example.rb
59
59
  - examples/sample_app.rb
60
+ - examples/signal_demo.rb
60
61
  - examples/test_interactive.rb
61
62
  - lib/thor/interactive.rb
62
63
  - lib/thor/interactive/command.rb
63
64
  - lib/thor/interactive/shell.rb
64
65
  - lib/thor/interactive/version.rb
66
+ - lib/thor/interactive/version_constant.rb
65
67
  - sig/thor/interactive.rbs
66
68
  homepage: https://github.com/scientist-labs/thor-interactive
67
69
  licenses: