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

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: 04a0318e910fee2ac10a134b2a9514ceef72921c2b1525d29face4c46a306ad8
4
+ data.tar.gz: 1376543ed73e25721cb220893cb62fbe0fb6bc18d94ea38ecf00df60878fd4d1
5
5
  SHA512:
6
- metadata.gz: 0ea62f18e17ab19662bd3c8fac62dce8dd69582adc857664901e69729670d8de690161a111151df1ab4ee7b403521b9e9254d9d67942bdc36f244278887d2b21
7
- data.tar.gz: a566b97836f0a11b75e35e35692f0996d3817b5b34dfa2d3c169d84dd125cac453cbeac14bfb76cd99f3253d144b4b550fd3e7a939f7b711e27a114242df2997
6
+ metadata.gz: '05909f8f5c99a783831eec436ee94236b279b883c2cda0c62379081c28cc7258d8ab8317dfa4d3aba36c942f763767eed1dc08d45427ee05c00b7513f8488af9'
7
+ data.tar.gz: 16a55871f6754affc2450703f3de24ecfee977ca372566b93dc44537cd3599bf305ecd988ec7f06349c786df9f0f7ab2e5bfed7d608390e53fedc6bdeb05f1cb
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
 
@@ -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
@@ -41,6 +41,8 @@ class Thor
41
41
  ENV['THOR_INTERACTIVE_SESSION'] = 'true'
42
42
  ENV['THOR_INTERACTIVE_LEVEL'] = (nesting_level + 1).to_s
43
43
 
44
+ puts "(Debug: Interactive session started, level #{nesting_level + 1})" if ENV["DEBUG"]
45
+
44
46
  # Adjust prompt for nested sessions if configured
45
47
  display_prompt = @prompt
46
48
  if nesting_level > 0 && @merged_options[:nested_prompt_format]
@@ -51,20 +53,37 @@ class Thor
51
53
 
52
54
  show_welcome(nesting_level)
53
55
 
56
+ puts "(Debug: Entering main loop)" if ENV["DEBUG"]
57
+
54
58
  loop do
55
59
  line = Reline.readline(display_prompt, true)
56
- break if should_exit?(line)
60
+ puts "(Debug: Got input: #{line.inspect})" if ENV["DEBUG"]
61
+
62
+ if should_exit?(line)
63
+ puts "(Debug: Exit condition met)" if ENV["DEBUG"]
64
+ break
65
+ end
57
66
 
58
67
  next if line.nil? || line.strip.empty?
59
68
 
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"]
69
+ begin
70
+ puts "(Debug: Processing input: #{line.strip})" if ENV["DEBUG"]
71
+ process_input(line.strip)
72
+ puts "(Debug: Input processed successfully)" if ENV["DEBUG"]
73
+ rescue Interrupt
74
+ puts "\n(Interrupted - press Ctrl+D or type 'exit' to quit)"
75
+ rescue SystemExit => e
76
+ puts "A command tried to exit with code #{e.status}. Staying in interactive mode."
77
+ puts "(Debug: SystemExit caught in main loop)" if ENV["DEBUG"]
78
+ rescue => e
79
+ puts "Error in main loop: #{e.message}"
80
+ puts e.backtrace.first(5) if ENV["DEBUG"]
81
+ puts "(Debug: Error handled, continuing loop)" if ENV["DEBUG"]
82
+ # Continue the loop - don't let errors break the session
83
+ end
66
84
  end
67
85
 
86
+ puts "(Debug: Exited main loop)" if ENV["DEBUG"]
68
87
  save_history
69
88
  puts nesting_level > 0 ? "Exiting nested session..." : "Goodbye!"
70
89
 
@@ -88,12 +107,22 @@ class Thor
88
107
  end
89
108
 
90
109
  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)
110
+ # Handle completion for slash commands
111
+ full_line = preposing + text
112
+
113
+ if full_line.start_with?('/')
114
+ # Command completion mode
115
+ if preposing.strip == '/' || preposing.strip.empty?
116
+ # Complete command names with / prefix
117
+ command_completions = complete_commands(text.sub(/^\//, ''))
118
+ command_completions.map { |cmd| "/#{cmd}" }
119
+ else
120
+ # Complete command arguments (basic implementation)
121
+ complete_command_options(text, preposing)
122
+ end
94
123
  else
95
- # Try to complete command options or let it fall back to file completion
96
- complete_command_options(text, preposing)
124
+ # Natural language mode - no completion for now
125
+ []
97
126
  end
98
127
  end
99
128
 
@@ -114,25 +143,130 @@ class Thor
114
143
  # Handle completely empty input
115
144
  return if input.nil? || input.strip.empty?
116
145
 
117
- args = parse_input(input)
118
- return if args.empty?
146
+ # Check if input starts with / for explicit command mode
147
+ if input.strip.start_with?('/')
148
+ # Explicit command mode: /command args
149
+ handle_slash_command(input.strip[1..-1])
150
+ elsif is_help_request?(input)
151
+ # Special case: treat bare "help" as /help for convenience
152
+ if input.strip.split.length == 1
153
+ show_help
154
+ else
155
+ command_part = input.strip.split[1]
156
+ show_help(command_part)
157
+ end
158
+ else
159
+ # Determine if this looks like a command or natural language
160
+ command_word = input.strip.split(/\s+/, 2).first
161
+
162
+ if thor_command?(command_word)
163
+ # Looks like a command - handle it as a command (backward compatibility)
164
+ handle_command(input.strip)
165
+ elsif @default_handler
166
+ # Natural language mode: send whole input to default handler
167
+ begin
168
+ @default_handler.call(input, @thor_instance)
169
+ rescue => e
170
+ puts "Error in default handler: #{e.message}"
171
+ puts "Input was: #{input}"
172
+ puts "Try using /commands or type '/help' for available commands."
173
+ end
174
+ else
175
+ # No default handler, suggest using command mode
176
+ puts "No default handler configured. Use /command for commands, or type '/help' for available commands."
177
+ end
178
+ end
179
+ end
119
180
 
120
- command = args.shift
181
+ def handle_slash_command(command_input)
182
+ return if command_input.empty?
183
+ handle_command(command_input)
184
+ end
121
185
 
122
- if thor_command?(command)
123
- invoke_thor_command(command, args)
124
- elsif @default_handler
125
- @default_handler.call(input, @thor_instance)
186
+ def handle_command(command_input)
187
+ # Extract command and check if it's a single-text command
188
+ command_word = command_input.split(/\s+/, 2).first
189
+
190
+ if thor_command?(command_word)
191
+ task = @thor_class.tasks[command_word]
192
+
193
+ if task && single_text_command?(task)
194
+ # Single text command - pass everything after command as one argument
195
+ text_part = command_input.sub(/^#{Regexp.escape(command_word)}\s*/, '')
196
+ if text_part.empty?
197
+ invoke_thor_command(command_word, [])
198
+ else
199
+ invoke_thor_command(command_word, [text_part])
200
+ end
201
+ else
202
+ # Multi-argument command, use proper parsing
203
+ args = safe_parse_input(command_input)
204
+ if args && !args.empty?
205
+ command = args.shift
206
+ invoke_thor_command(command, args)
207
+ else
208
+ # Parsing failed, try simple split
209
+ parts = command_input.split(/\s+/)
210
+ command = parts.shift
211
+ invoke_thor_command(command, parts)
212
+ end
213
+ end
126
214
  else
127
- puts "Unknown command: '#{command}'. Type 'help' for available commands."
215
+ puts "Unknown command: '#{command_word}'. Type '/help' for available commands."
128
216
  end
129
217
  end
130
218
 
131
- def parse_input(input)
219
+ def safe_parse_input(input)
220
+ # Try proper shell parsing first
132
221
  Shellwords.split(input)
133
- rescue ArgumentError => e
134
- puts "Error parsing input: #{e.message}"
135
- []
222
+ rescue ArgumentError
223
+ # If parsing fails, return nil so caller can handle it
224
+ nil
225
+ end
226
+
227
+ def parse_input(input)
228
+ # Legacy method - kept for backward compatibility
229
+ safe_parse_input(input) || []
230
+ end
231
+
232
+ def handle_unparseable_command(input, command_word)
233
+ # For commands that failed shell parsing, try intelligent handling
234
+ task = @thor_class.tasks[command_word]
235
+
236
+ # Always try single text approach first for better natural language support
237
+ text_part = input.strip.sub(/^#{Regexp.escape(command_word)}\s*/, '')
238
+ if text_part.empty?
239
+ invoke_thor_command(command_word, [])
240
+ else
241
+ invoke_thor_command(command_word, [text_part])
242
+ end
243
+ end
244
+
245
+ def single_text_command?(task)
246
+ # Heuristic: determine if this is likely a single text command
247
+ return false unless task
248
+
249
+ # Check the method signature to see how many parameters it expects
250
+ method_name = task.name.to_sym
251
+ if @thor_instance.respond_to?(method_name)
252
+ method_obj = @thor_instance.method(method_name)
253
+ param_count = method_obj.parameters.count { |type, _| type == :req }
254
+
255
+ # Only single required parameter = likely text command
256
+ param_count == 1
257
+ else
258
+ # Fallback for introspection issues
259
+ false
260
+ end
261
+ rescue
262
+ # If introspection fails, default to false (safer)
263
+ false
264
+ end
265
+
266
+ def is_help_request?(input)
267
+ # Check if input is a help request (help, ?, etc.)
268
+ stripped = input.strip.downcase
269
+ stripped == "help" || stripped.start_with?("help ")
136
270
  end
137
271
 
138
272
  def thor_command?(command)
@@ -146,14 +280,22 @@ class Thor
146
280
  if command == "help"
147
281
  show_help(args.first)
148
282
  else
149
- # For simple commands, call directly for state persistence
150
- # For complex options/subcommands, this is a basic implementation
283
+ # Always use direct method calls to avoid Thor's invoke deduplication
284
+ # Thor's invoke method silently fails on subsequent calls to the same method
151
285
  if @thor_instance.respond_to?(command)
152
286
  @thor_instance.send(command, *args)
153
287
  else
154
- @thor_instance.invoke(command, args)
288
+ # If method doesn't exist, this will raise a proper error
289
+ @thor_instance.send(command, *args)
155
290
  end
156
291
  end
292
+ rescue SystemExit => e
293
+ if e.status == 0
294
+ puts "Command completed successfully (would have exited with code 0 in CLI mode)"
295
+ else
296
+ puts "Command failed with exit code #{e.status}"
297
+ end
298
+ puts "(Use 'exit' or Ctrl+D to exit the interactive session)" if ENV["DEBUG"]
157
299
  rescue Thor::Error => e
158
300
  puts "Thor Error: #{e.message}"
159
301
  rescue ArgumentError => e
@@ -161,21 +303,36 @@ class Thor
161
303
  puts "Try: help #{command}" if thor_command?(command)
162
304
  rescue StandardError => e
163
305
  puts "Error: #{e.message}"
306
+ puts "Command: #{command}, Args: #{args.inspect}" if ENV["DEBUG"]
164
307
  end
165
308
 
166
309
  def show_help(command = nil)
167
310
  if command && @thor_class.tasks.key?(command)
168
311
  @thor_class.command_help(Thor::Base.shell.new, command)
169
312
  else
170
- puts "Available commands:"
313
+ puts "Available commands (prefix with /):"
171
314
  @thor_class.tasks.each do |name, task|
172
- puts " #{name.ljust(20)} #{task.description}"
315
+ puts " /#{name.ljust(19)} #{task.description}"
173
316
  end
174
317
  puts
175
318
  puts "Special commands:"
176
- puts " help [COMMAND] Show help for command"
177
- puts " exit/quit/q Exit the REPL"
319
+ puts " /help [COMMAND] Show help for command"
320
+ puts " /exit, /quit, /q Exit the REPL"
178
321
  puts
322
+ if @default_handler
323
+ puts "Natural language mode:"
324
+ puts " Type anything without / to use default handler"
325
+ else
326
+ puts "Use /command syntax for all commands"
327
+ end
328
+ puts
329
+ if ENV["DEBUG"]
330
+ puts "Debug info:"
331
+ puts " Thor class: #{@thor_class.name}"
332
+ puts " Available tasks: #{@thor_class.tasks.keys.sort}"
333
+ puts " Instance methods: #{@thor_instance.methods.grep(/^[a-z]/).sort}" if @thor_instance
334
+ puts
335
+ end
179
336
  end
180
337
  end
181
338
 
@@ -183,7 +340,8 @@ class Thor
183
340
  return true if line.nil? # Ctrl+D
184
341
 
185
342
  stripped = line.strip.downcase
186
- EXIT_COMMANDS.include?(stripped)
343
+ # Handle both /exit and exit for convenience
344
+ EXIT_COMMANDS.include?(stripped) || EXIT_COMMANDS.include?(stripped.sub(/^\//, ''))
187
345
  end
188
346
 
189
347
  def show_welcome(nesting_level = 0)
@@ -4,6 +4,6 @@ require "thor"
4
4
 
5
5
  class Thor
6
6
  module Interactive
7
- VERSION = "0.1.0.pre.1"
7
+ VERSION = "0.1.0.pre.2"
8
8
  end
9
9
  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.2
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-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor