thor-interactive 0.1.0.pre.1
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 +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +472 -0
- data/Rakefile +10 -0
- data/examples/README.md +144 -0
- data/examples/demo_session.rb +64 -0
- data/examples/nested_example.rb +80 -0
- data/examples/sample_app.rb +81 -0
- data/examples/test_interactive.rb +67 -0
- data/lib/thor/interactive/command.rb +44 -0
- data/lib/thor/interactive/shell.rb +219 -0
- data/lib/thor/interactive/version.rb +9 -0
- data/lib/thor/interactive.rb +16 -0
- data/sig/thor/interactive.rbs +6 -0
- metadata +93 -0
data/examples/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# Thor Interactive Examples
|
2
|
+
|
3
|
+
## Sample Application
|
4
|
+
|
5
|
+
The `sample_app.rb` demonstrates both normal Thor CLI usage and interactive REPL mode.
|
6
|
+
|
7
|
+
### Normal CLI Usage
|
8
|
+
|
9
|
+
```bash
|
10
|
+
# Run individual commands
|
11
|
+
ruby sample_app.rb hello World
|
12
|
+
ruby sample_app.rb count
|
13
|
+
ruby sample_app.rb add "Buy groceries"
|
14
|
+
ruby sample_app.rb list
|
15
|
+
ruby sample_app.rb status
|
16
|
+
ruby sample_app.rb help
|
17
|
+
```
|
18
|
+
|
19
|
+
### Interactive REPL Mode
|
20
|
+
|
21
|
+
```bash
|
22
|
+
# Start interactive mode
|
23
|
+
ruby sample_app.rb interactive
|
24
|
+
```
|
25
|
+
|
26
|
+
Once in interactive mode:
|
27
|
+
|
28
|
+
```
|
29
|
+
sample> hello Alice
|
30
|
+
Hello Alice!
|
31
|
+
|
32
|
+
sample> count
|
33
|
+
Count: 1
|
34
|
+
|
35
|
+
sample> count
|
36
|
+
Count: 2
|
37
|
+
|
38
|
+
sample> add "First item"
|
39
|
+
Added 'First item'. Total items: 1
|
40
|
+
|
41
|
+
sample> add "Second item"
|
42
|
+
Added 'Second item'. Total items: 2
|
43
|
+
|
44
|
+
sample> list
|
45
|
+
Items:
|
46
|
+
1. First item
|
47
|
+
2. Second item
|
48
|
+
|
49
|
+
sample> status
|
50
|
+
Application Status:
|
51
|
+
Counter: 2
|
52
|
+
Items in list: 2
|
53
|
+
Memory usage: 15234 KB
|
54
|
+
|
55
|
+
sample> This is unrecognized text
|
56
|
+
Echo: This is unrecognized text
|
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
|
68
|
+
|
69
|
+
Special commands:
|
70
|
+
help [COMMAND] Show help for command
|
71
|
+
exit/quit/q Exit the REPL
|
72
|
+
|
73
|
+
sample> exit
|
74
|
+
Goodbye!
|
75
|
+
```
|
76
|
+
|
77
|
+
## Key Features Demonstrated
|
78
|
+
|
79
|
+
### 1. State Persistence
|
80
|
+
- The `@@counter` and `@@items` class variables maintain their state between commands in interactive mode
|
81
|
+
- In normal CLI mode, each command starts fresh
|
82
|
+
|
83
|
+
### 2. Auto-completion
|
84
|
+
- Tab completion works for command names
|
85
|
+
- Try typing `h<TAB>` or `a<TAB>` to see completions
|
86
|
+
|
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
|
90
|
+
|
91
|
+
### 4. Built-in Help
|
92
|
+
- `help` shows all available commands
|
93
|
+
- `help COMMAND` shows help for a specific command
|
94
|
+
|
95
|
+
### 5. History
|
96
|
+
- Up/down arrows navigate command history
|
97
|
+
- History is persistent across sessions
|
98
|
+
|
99
|
+
### 6. Graceful Exit
|
100
|
+
- Ctrl+C interrupts current operation
|
101
|
+
- Ctrl+D or `exit`/`quit`/`q` exits the REPL
|
102
|
+
|
103
|
+
## Integration Patterns
|
104
|
+
|
105
|
+
### Minimal Integration
|
106
|
+
```ruby
|
107
|
+
class MyApp < Thor
|
108
|
+
include Thor::Interactive::Command
|
109
|
+
|
110
|
+
# Your existing Thor commands...
|
111
|
+
desc "hello NAME", "Say hello"
|
112
|
+
def hello(name)
|
113
|
+
puts "Hello #{name}!"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Now available: ruby myapp.rb interactive
|
118
|
+
```
|
119
|
+
|
120
|
+
### Custom Configuration
|
121
|
+
```ruby
|
122
|
+
class MyApp < Thor
|
123
|
+
include Thor::Interactive::Command
|
124
|
+
|
125
|
+
configure_interactive(
|
126
|
+
prompt: "myapp> ",
|
127
|
+
default_handler: proc do |input, thor_instance|
|
128
|
+
# Handle unrecognized input
|
129
|
+
thor_instance.invoke(:search, [input])
|
130
|
+
end
|
131
|
+
)
|
132
|
+
|
133
|
+
# Your commands...
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
### Programmatic Usage
|
138
|
+
```ruby
|
139
|
+
# Instead of using the mixin, start programmatically
|
140
|
+
Thor::Interactive.start(MyApp,
|
141
|
+
prompt: "custom> ",
|
142
|
+
default_handler: proc { |input, instance| puts "Got: #{input}" }
|
143
|
+
)
|
144
|
+
```
|
@@ -0,0 +1,64 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Demo script showing the difference between CLI and interactive modes
|
5
|
+
|
6
|
+
require_relative "../lib/thor/interactive"
|
7
|
+
require_relative "sample_app"
|
8
|
+
|
9
|
+
puts "=== Thor Interactive Demo ==="
|
10
|
+
puts
|
11
|
+
puts "1. CLI Mode (each command runs fresh):"
|
12
|
+
puts " $ ruby sample_app.rb count"
|
13
|
+
puts " $ ruby sample_app.rb count"
|
14
|
+
puts
|
15
|
+
|
16
|
+
system("ruby sample_app.rb count")
|
17
|
+
system("ruby sample_app.rb count")
|
18
|
+
|
19
|
+
puts "\n Notice: Both show 'Count: 1' because state resets"
|
20
|
+
puts
|
21
|
+
|
22
|
+
puts "2. Interactive Mode (state persists):"
|
23
|
+
puts " To see interactive mode, run:"
|
24
|
+
puts " $ ruby sample_app.rb interactive"
|
25
|
+
puts
|
26
|
+
puts " Then try these commands in sequence:"
|
27
|
+
puts " sample> count"
|
28
|
+
puts " sample> count"
|
29
|
+
puts " sample> add \"first item\""
|
30
|
+
puts " sample> add \"second item\""
|
31
|
+
puts " sample> list"
|
32
|
+
puts " sample> status"
|
33
|
+
puts " sample> help"
|
34
|
+
puts " sample> This text goes to default handler"
|
35
|
+
puts " sample> exit"
|
36
|
+
puts
|
37
|
+
puts "=== Key Differences ==="
|
38
|
+
puts
|
39
|
+
puts "CLI Mode:"
|
40
|
+
puts "- Fresh Thor instance each command"
|
41
|
+
puts "- No state persistence"
|
42
|
+
puts "- Standard Thor behavior"
|
43
|
+
puts
|
44
|
+
puts "Interactive Mode:"
|
45
|
+
puts "- Single persistent Thor instance"
|
46
|
+
puts "- State maintained between commands"
|
47
|
+
puts "- Auto-completion with TAB"
|
48
|
+
puts "- Command history with arrow keys"
|
49
|
+
puts "- Default handler for unrecognized input"
|
50
|
+
puts "- Built-in help system"
|
51
|
+
puts
|
52
|
+
puts "=== Features Implemented ==="
|
53
|
+
puts
|
54
|
+
puts "✓ Generic design - works with any Thor application"
|
55
|
+
puts "✓ State persistence through single Thor instance"
|
56
|
+
puts "✓ Auto-completion for command names"
|
57
|
+
puts "✓ Configurable default handlers"
|
58
|
+
puts "✓ Command history with persistent storage"
|
59
|
+
puts "✓ Both CLI and interactive modes supported"
|
60
|
+
puts "✓ Proper error handling and signal management"
|
61
|
+
puts "✓ Help system integration"
|
62
|
+
puts "✓ Comprehensive test suite"
|
63
|
+
puts
|
64
|
+
puts "Ready for your Claude Code-like RAG application!"
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "../lib/thor/interactive"
|
5
|
+
|
6
|
+
# Example app that allows nested interactive sessions
|
7
|
+
class NestedApp < Thor
|
8
|
+
include Thor::Interactive::Command
|
9
|
+
|
10
|
+
configure_interactive(
|
11
|
+
prompt: "nested> ",
|
12
|
+
allow_nested: true, # Allow nested interactive sessions
|
13
|
+
nested_prompt_format: "[L%d] %s" # Custom format for nested prompts
|
14
|
+
)
|
15
|
+
|
16
|
+
desc "hello NAME", "Say hello"
|
17
|
+
def hello(name)
|
18
|
+
puts "Hello #{name}!"
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "demo", "Show nesting demo info"
|
22
|
+
def demo
|
23
|
+
level = ENV['THOR_INTERACTIVE_LEVEL']
|
24
|
+
puts "Current nesting level: #{level || 'not in interactive mode'}"
|
25
|
+
puts "Try running 'interactive' to see nested sessions!"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "status", "Show session status"
|
29
|
+
def status
|
30
|
+
if ENV['THOR_INTERACTIVE_SESSION']
|
31
|
+
level = ENV['THOR_INTERACTIVE_LEVEL'].to_i
|
32
|
+
puts "In interactive session - Level #{level}"
|
33
|
+
else
|
34
|
+
puts "Not in interactive session"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Example app that prevents nested sessions (default behavior)
|
40
|
+
class SafeApp < Thor
|
41
|
+
include Thor::Interactive::Command
|
42
|
+
|
43
|
+
configure_interactive(
|
44
|
+
prompt: "safe> ",
|
45
|
+
allow_nested: false # This is the default, but being explicit
|
46
|
+
)
|
47
|
+
|
48
|
+
desc "hello NAME", "Say hello"
|
49
|
+
def hello(name)
|
50
|
+
puts "Hello #{name}!"
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "demo", "Show nesting prevention"
|
54
|
+
def demo
|
55
|
+
puts "This app prevents nested sessions."
|
56
|
+
puts "Try running 'interactive' to see the protection!"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if __FILE__ == $0
|
61
|
+
puts "=== Nested Interactive Sessions Demo ==="
|
62
|
+
puts
|
63
|
+
puts "1. NestedApp - allows nested sessions:"
|
64
|
+
puts " ruby nested_example.rb nested"
|
65
|
+
puts
|
66
|
+
puts "2. SafeApp - prevents nested sessions:"
|
67
|
+
puts " ruby nested_example.rb safe"
|
68
|
+
puts
|
69
|
+
|
70
|
+
case ARGV[0]
|
71
|
+
when "nested"
|
72
|
+
ARGV.shift
|
73
|
+
NestedApp.start(ARGV)
|
74
|
+
when "safe"
|
75
|
+
ARGV.shift
|
76
|
+
SafeApp.start(ARGV)
|
77
|
+
else
|
78
|
+
puts "Usage: ruby nested_example.rb [nested|safe] [command]"
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "thor"
|
5
|
+
require_relative "../lib/thor/interactive"
|
6
|
+
|
7
|
+
# Example Thor application that demonstrates both normal CLI usage
|
8
|
+
# and interactive REPL mode
|
9
|
+
class SampleApp < Thor
|
10
|
+
include Thor::Interactive::Command
|
11
|
+
|
12
|
+
# Configure interactive mode
|
13
|
+
configure_interactive(
|
14
|
+
prompt: "sample> ",
|
15
|
+
allow_nested: false, # Prevent nested interactive sessions by default
|
16
|
+
default_handler: proc do |input, thor_instance|
|
17
|
+
# Send unrecognized input to the 'echo' command
|
18
|
+
thor_instance.invoke(:echo, [input])
|
19
|
+
end
|
20
|
+
)
|
21
|
+
|
22
|
+
# Class variable to demonstrate state persistence in interactive mode
|
23
|
+
class_variable_set(:@@counter, 0)
|
24
|
+
class_variable_set(:@@items, [])
|
25
|
+
|
26
|
+
desc "hello NAME", "Say hello to NAME"
|
27
|
+
def hello(name)
|
28
|
+
puts "Hello #{name}!"
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "count", "Show and increment counter (demonstrates state persistence)"
|
32
|
+
def count
|
33
|
+
@@counter += 1
|
34
|
+
puts "Count: #{@@counter}"
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "add ITEM", "Add item to list (demonstrates state persistence)"
|
38
|
+
def add(item)
|
39
|
+
@@items << item
|
40
|
+
puts "Added '#{item}'. Total items: #{@@items.length}"
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "list", "Show all items"
|
44
|
+
def list
|
45
|
+
if @@items.empty?
|
46
|
+
puts "No items in the list"
|
47
|
+
else
|
48
|
+
puts "Items:"
|
49
|
+
@@items.each_with_index do |item, index|
|
50
|
+
puts " #{index + 1}. #{item}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "clear", "Clear all items"
|
56
|
+
def clear
|
57
|
+
@@items.clear
|
58
|
+
puts "List cleared"
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "echo TEXT", "Echo the text back (used as default handler)"
|
62
|
+
def echo(*words)
|
63
|
+
text = words.join(" ")
|
64
|
+
puts "Echo: #{text}"
|
65
|
+
end
|
66
|
+
|
67
|
+
desc "status", "Show application status"
|
68
|
+
def status
|
69
|
+
puts "Application Status:"
|
70
|
+
puts " Counter: #{@@counter}"
|
71
|
+
puts " Items in list: #{@@items.length}"
|
72
|
+
puts " Memory usage: #{`ps -o rss= -p #{Process.pid}`.strip} KB" rescue "Unknown"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# This allows the file to work both ways:
|
77
|
+
# 1. Normal Thor CLI: ruby sample_app.rb hello World
|
78
|
+
# 2. Interactive mode: ruby sample_app.rb interactive
|
79
|
+
if __FILE__ == $0
|
80
|
+
SampleApp.start(ARGV)
|
81
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Test script to verify interactive functionality
|
5
|
+
# This simulates what would happen in interactive mode
|
6
|
+
|
7
|
+
require_relative "../lib/thor/interactive"
|
8
|
+
|
9
|
+
class TestApp < Thor
|
10
|
+
include Thor::Interactive::Command
|
11
|
+
|
12
|
+
configure_interactive(
|
13
|
+
prompt: "test> ",
|
14
|
+
default_handler: proc do |input, instance|
|
15
|
+
puts "Default handler got: #{input}"
|
16
|
+
end
|
17
|
+
)
|
18
|
+
|
19
|
+
class_variable_set(:@@counter, 0)
|
20
|
+
|
21
|
+
desc "count", "Increment and show counter"
|
22
|
+
def count
|
23
|
+
@@counter += 1
|
24
|
+
puts "Count: #{@@counter}"
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "hello NAME", "Say hello"
|
28
|
+
def hello(name)
|
29
|
+
puts "Hello #{name}!"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Test normal CLI mode
|
34
|
+
puts "=== Testing Normal CLI Mode ==="
|
35
|
+
puts "Running: hello Alice"
|
36
|
+
TestApp.start(["hello", "Alice"])
|
37
|
+
puts "\nRunning: count (twice - should both be 1)"
|
38
|
+
TestApp.start(["count"])
|
39
|
+
TestApp.start(["count"])
|
40
|
+
|
41
|
+
puts "\n=== Testing Interactive Shell Creation ==="
|
42
|
+
shell = Thor::Interactive::Shell.new(TestApp)
|
43
|
+
puts "Shell created successfully with Thor class: #{shell.thor_class}"
|
44
|
+
puts "Shell has persistent Thor instance: #{shell.thor_instance.class}"
|
45
|
+
|
46
|
+
puts "\n=== Testing Command Recognition ==="
|
47
|
+
# Test if commands are recognized
|
48
|
+
test_commands = %w[hello count help exit]
|
49
|
+
test_commands.each do |cmd|
|
50
|
+
recognized = shell.send(:thor_command?, cmd)
|
51
|
+
puts "Command '#{cmd}': #{recognized ? 'recognized' : 'not recognized'}"
|
52
|
+
end
|
53
|
+
|
54
|
+
puts "\n=== Testing Completion ==="
|
55
|
+
completions = shell.send(:complete_commands, "h")
|
56
|
+
puts "Completions for 'h': #{completions}"
|
57
|
+
|
58
|
+
puts "\n=== Interactive mode ready! ==="
|
59
|
+
puts "To test interactively, run: ruby examples/sample_app.rb interactive"
|
60
|
+
puts "Then try commands like:"
|
61
|
+
puts " count (multiple times to see state persistence)"
|
62
|
+
puts " add item1"
|
63
|
+
puts " add item2"
|
64
|
+
puts " list"
|
65
|
+
puts " hello Alice"
|
66
|
+
puts " help"
|
67
|
+
puts " exit"
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "shell"
|
4
|
+
|
5
|
+
class Thor
|
6
|
+
module Interactive
|
7
|
+
# Mixin to add an interactive command to Thor applications
|
8
|
+
module Command
|
9
|
+
def self.included(base)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
base.class_eval do
|
12
|
+
desc "interactive", "Start an interactive REPL for this application"
|
13
|
+
option :prompt, type: :string, desc: "Custom prompt for the REPL"
|
14
|
+
option :history_file, type: :string, desc: "Custom history file location"
|
15
|
+
|
16
|
+
def interactive
|
17
|
+
# Check for nested sessions unless explicitly allowed
|
18
|
+
if ENV['THOR_INTERACTIVE_SESSION'] && !self.class.interactive_options[:allow_nested]
|
19
|
+
puts "Already in an interactive session."
|
20
|
+
puts "To allow nested sessions, configure with: configure_interactive(allow_nested: true)"
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
opts = self.class.interactive_options.dup
|
25
|
+
opts[:prompt] = options[:prompt] || options["prompt"] if options[:prompt] || options["prompt"]
|
26
|
+
opts[:history_file] = options[:history_file] || options["history_file"] if options[:history_file] || options["history_file"]
|
27
|
+
|
28
|
+
Thor::Interactive::Shell.new(self.class, opts).start
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
def interactive_options
|
35
|
+
@interactive_options ||= { allow_nested: false }
|
36
|
+
end
|
37
|
+
|
38
|
+
def configure_interactive(**options)
|
39
|
+
interactive_options.merge!(options)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "reline"
|
4
|
+
require "shellwords"
|
5
|
+
require "thor"
|
6
|
+
|
7
|
+
class Thor
|
8
|
+
module Interactive
|
9
|
+
class Shell
|
10
|
+
DEFAULT_PROMPT = "> "
|
11
|
+
DEFAULT_HISTORY_FILE = "~/.thor_interactive_history"
|
12
|
+
EXIT_COMMANDS = %w[exit quit q].freeze
|
13
|
+
|
14
|
+
attr_reader :thor_class, :thor_instance, :prompt
|
15
|
+
|
16
|
+
def initialize(thor_class, options = {})
|
17
|
+
@thor_class = thor_class
|
18
|
+
@thor_instance = thor_class.new
|
19
|
+
|
20
|
+
# Merge class-level interactive options if available
|
21
|
+
merged_options = {}
|
22
|
+
if thor_class.respond_to?(:interactive_options)
|
23
|
+
merged_options.merge!(thor_class.interactive_options)
|
24
|
+
end
|
25
|
+
merged_options.merge!(options)
|
26
|
+
|
27
|
+
@merged_options = merged_options
|
28
|
+
@default_handler = merged_options[:default_handler]
|
29
|
+
@prompt = merged_options[:prompt] || DEFAULT_PROMPT
|
30
|
+
@history_file = File.expand_path(merged_options[:history_file] || DEFAULT_HISTORY_FILE)
|
31
|
+
|
32
|
+
setup_completion
|
33
|
+
load_history
|
34
|
+
end
|
35
|
+
|
36
|
+
def start
|
37
|
+
# Track that we're in an interactive session
|
38
|
+
was_in_session = ENV['THOR_INTERACTIVE_SESSION']
|
39
|
+
nesting_level = ENV['THOR_INTERACTIVE_LEVEL'].to_i
|
40
|
+
|
41
|
+
ENV['THOR_INTERACTIVE_SESSION'] = 'true'
|
42
|
+
ENV['THOR_INTERACTIVE_LEVEL'] = (nesting_level + 1).to_s
|
43
|
+
|
44
|
+
# Adjust prompt for nested sessions if configured
|
45
|
+
display_prompt = @prompt
|
46
|
+
if nesting_level > 0 && @merged_options[:nested_prompt_format]
|
47
|
+
display_prompt = @merged_options[:nested_prompt_format] % [nesting_level + 1, @prompt]
|
48
|
+
elsif nesting_level > 0
|
49
|
+
display_prompt = "(#{nesting_level + 1}) #{@prompt}"
|
50
|
+
end
|
51
|
+
|
52
|
+
show_welcome(nesting_level)
|
53
|
+
|
54
|
+
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"]
|
66
|
+
end
|
67
|
+
|
68
|
+
save_history
|
69
|
+
puts nesting_level > 0 ? "Exiting nested session..." : "Goodbye!"
|
70
|
+
|
71
|
+
ensure
|
72
|
+
# Restore previous session state
|
73
|
+
if was_in_session
|
74
|
+
ENV['THOR_INTERACTIVE_SESSION'] = 'true'
|
75
|
+
ENV['THOR_INTERACTIVE_LEVEL'] = nesting_level.to_s
|
76
|
+
else
|
77
|
+
ENV.delete('THOR_INTERACTIVE_SESSION')
|
78
|
+
ENV.delete('THOR_INTERACTIVE_LEVEL')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def setup_completion
|
85
|
+
Reline.completion_proc = proc do |text, preposing|
|
86
|
+
complete_input(text, preposing)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
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)
|
94
|
+
else
|
95
|
+
# Try to complete command options or let it fall back to file completion
|
96
|
+
complete_command_options(text, preposing)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def complete_commands(text)
|
101
|
+
return [] if text.nil?
|
102
|
+
|
103
|
+
command_names = @thor_class.tasks.keys + EXIT_COMMANDS + ["help"]
|
104
|
+
command_names.select { |cmd| cmd.start_with?(text) }.sort
|
105
|
+
end
|
106
|
+
|
107
|
+
def complete_command_options(text, preposing)
|
108
|
+
# Basic implementation - can be enhanced later
|
109
|
+
# For now, just return empty array to let Reline handle file completion
|
110
|
+
[]
|
111
|
+
end
|
112
|
+
|
113
|
+
def process_input(input)
|
114
|
+
# Handle completely empty input
|
115
|
+
return if input.nil? || input.strip.empty?
|
116
|
+
|
117
|
+
args = parse_input(input)
|
118
|
+
return if args.empty?
|
119
|
+
|
120
|
+
command = args.shift
|
121
|
+
|
122
|
+
if thor_command?(command)
|
123
|
+
invoke_thor_command(command, args)
|
124
|
+
elsif @default_handler
|
125
|
+
@default_handler.call(input, @thor_instance)
|
126
|
+
else
|
127
|
+
puts "Unknown command: '#{command}'. Type 'help' for available commands."
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def parse_input(input)
|
132
|
+
Shellwords.split(input)
|
133
|
+
rescue ArgumentError => e
|
134
|
+
puts "Error parsing input: #{e.message}"
|
135
|
+
[]
|
136
|
+
end
|
137
|
+
|
138
|
+
def thor_command?(command)
|
139
|
+
@thor_class.tasks.key?(command) ||
|
140
|
+
@thor_class.subcommand_classes.key?(command) ||
|
141
|
+
command == "help"
|
142
|
+
end
|
143
|
+
|
144
|
+
def invoke_thor_command(command, args)
|
145
|
+
# Use the persistent instance to maintain state
|
146
|
+
if command == "help"
|
147
|
+
show_help(args.first)
|
148
|
+
else
|
149
|
+
# For simple commands, call directly for state persistence
|
150
|
+
# For complex options/subcommands, this is a basic implementation
|
151
|
+
if @thor_instance.respond_to?(command)
|
152
|
+
@thor_instance.send(command, *args)
|
153
|
+
else
|
154
|
+
@thor_instance.invoke(command, args)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
rescue Thor::Error => e
|
158
|
+
puts "Thor Error: #{e.message}"
|
159
|
+
rescue ArgumentError => e
|
160
|
+
puts "Thor Error: #{e.message}"
|
161
|
+
puts "Try: help #{command}" if thor_command?(command)
|
162
|
+
rescue StandardError => e
|
163
|
+
puts "Error: #{e.message}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def show_help(command = nil)
|
167
|
+
if command && @thor_class.tasks.key?(command)
|
168
|
+
@thor_class.command_help(Thor::Base.shell.new, command)
|
169
|
+
else
|
170
|
+
puts "Available commands:"
|
171
|
+
@thor_class.tasks.each do |name, task|
|
172
|
+
puts " #{name.ljust(20)} #{task.description}"
|
173
|
+
end
|
174
|
+
puts
|
175
|
+
puts "Special commands:"
|
176
|
+
puts " help [COMMAND] Show help for command"
|
177
|
+
puts " exit/quit/q Exit the REPL"
|
178
|
+
puts
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def should_exit?(line)
|
183
|
+
return true if line.nil? # Ctrl+D
|
184
|
+
|
185
|
+
stripped = line.strip.downcase
|
186
|
+
EXIT_COMMANDS.include?(stripped)
|
187
|
+
end
|
188
|
+
|
189
|
+
def show_welcome(nesting_level = 0)
|
190
|
+
if nesting_level > 0
|
191
|
+
puts "#{@thor_class.name} Interactive Shell (nested level #{nesting_level + 1})"
|
192
|
+
puts "Type 'exit' to return to previous level, or 'help' for commands"
|
193
|
+
else
|
194
|
+
puts "#{@thor_class.name} Interactive Shell"
|
195
|
+
puts "Type 'help' for available commands, 'exit' to quit"
|
196
|
+
end
|
197
|
+
puts
|
198
|
+
end
|
199
|
+
|
200
|
+
def load_history
|
201
|
+
return unless File.exist?(@history_file)
|
202
|
+
|
203
|
+
File.readlines(@history_file, chomp: true).each do |line|
|
204
|
+
Reline::HISTORY << line
|
205
|
+
end
|
206
|
+
rescue => e
|
207
|
+
# Ignore history loading errors
|
208
|
+
end
|
209
|
+
|
210
|
+
def save_history
|
211
|
+
return unless Reline::HISTORY.size > 0
|
212
|
+
|
213
|
+
File.write(@history_file, Reline::HISTORY.to_a.join("\n"))
|
214
|
+
rescue => e
|
215
|
+
# Ignore history saving errors
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|