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 +4 -4
- data/README.md +45 -9
- data/examples/README.md +36 -29
- data/examples/sample_app.rb +3 -1
- data/examples/signal_demo.rb +203 -0
- data/lib/thor/interactive/command.rb +10 -0
- data/lib/thor/interactive/shell.rb +240 -36
- data/lib/thor/interactive/version.rb +2 -1
- data/lib/thor/interactive/version_constant.rb +8 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '049af6f0fedd7bda65c5845a878bd02afe8e9e5cf705ebad254c333e0151ecd7'
|
4
|
+
data.tar.gz: '049e366b5d3d1e4d0076c7c69d69240b00f0768b3b2687d23e9482dd7bf368dd'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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>
|
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
|
-
|
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
|
38
|
+
sample> /add First item
|
39
39
|
Added 'First item'. Total items: 1
|
40
40
|
|
41
|
-
sample> add
|
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>
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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]
|
71
|
-
exit/quit/q
|
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
|
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.
|
88
|
-
- Text
|
89
|
-
-
|
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
|
-
-
|
93
|
-
-
|
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
|
108
|
+
- Ctrl+D, `exit`, `quit`, `q`, `/exit`, `/quit`, or `/q` exits the REPL
|
102
109
|
|
103
110
|
## Integration Patterns
|
104
111
|
|
data/examples/sample_app.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
#
|
92
|
-
|
93
|
-
|
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
|
-
#
|
96
|
-
|
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
|
-
|
118
|
-
|
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
|
-
|
196
|
+
def handle_slash_command(command_input)
|
197
|
+
return if command_input.empty?
|
198
|
+
handle_command(command_input)
|
199
|
+
end
|
121
200
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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: '#{
|
230
|
+
puts "Unknown command: '#{command_word}'. Type '/help' for available commands."
|
128
231
|
end
|
129
232
|
end
|
130
233
|
|
131
|
-
def
|
234
|
+
def safe_parse_input(input)
|
235
|
+
# Try proper shell parsing first
|
132
236
|
Shellwords.split(input)
|
133
|
-
rescue ArgumentError
|
134
|
-
|
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
|
-
#
|
150
|
-
#
|
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
|
-
|
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 "
|
330
|
+
puts " /#{name.ljust(19)} #{task.description}"
|
173
331
|
end
|
174
332
|
puts
|
175
333
|
puts "Special commands:"
|
176
|
-
puts " help [COMMAND]
|
177
|
-
puts " exit/quit/q
|
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
|
-
|
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)
|
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.
|
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-
|
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:
|