ruby-shell 3.1.0 → 3.2.0
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/PLUGIN_GUIDE.md +757 -0
- data/README.md +64 -1
- data/bin/rsh +433 -36
- metadata +5 -4
data/PLUGIN_GUIDE.md
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
# rsh Plugin Development Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
rsh v3.2.0+ supports a powerful plugin system that allows you to extend the shell with custom functionality. Plugins are Ruby classes that hook into rsh's lifecycle and can add commands, completions, and modify behavior.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### 1. Create Plugin Directory
|
|
12
|
+
|
|
13
|
+
Plugins live in `~/.rsh/plugins/`:
|
|
14
|
+
```bash
|
|
15
|
+
mkdir -p ~/.rsh/plugins
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### 2. Create Your First Plugin
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# ~/.rsh/plugins/hello.rb
|
|
22
|
+
class HelloPlugin
|
|
23
|
+
def initialize(rsh_context)
|
|
24
|
+
@rsh = rsh_context
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def on_startup
|
|
28
|
+
puts "Hello plugin loaded!"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def add_commands
|
|
32
|
+
{
|
|
33
|
+
"hello" => lambda do |*args|
|
|
34
|
+
name = args[0] || "World"
|
|
35
|
+
"Hello, #{name}!"
|
|
36
|
+
end
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Use It
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
rsh
|
|
46
|
+
# Output: Hello plugin loaded!
|
|
47
|
+
|
|
48
|
+
hello
|
|
49
|
+
# Output: Hello, World!
|
|
50
|
+
|
|
51
|
+
hello Geir
|
|
52
|
+
# Output: Hello, Geir!
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Plugin API Reference
|
|
58
|
+
|
|
59
|
+
### Class Naming Convention
|
|
60
|
+
|
|
61
|
+
**File:** `plugin_name.rb`
|
|
62
|
+
**Class:** `PluginNamePlugin`
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
- `git_prompt.rb` → `GitPromptPlugin`
|
|
66
|
+
- `my_tools.rb` → `MyToolsPlugin`
|
|
67
|
+
- `k8s.rb` → `K8sPlugin`
|
|
68
|
+
|
|
69
|
+
### Constructor
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
def initialize(rsh_context)
|
|
73
|
+
@rsh = rsh_context
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**rsh_context** provides:
|
|
78
|
+
```ruby
|
|
79
|
+
{
|
|
80
|
+
version: "3.2.0", # rsh version
|
|
81
|
+
history: [...], # Command history array
|
|
82
|
+
bookmarks: {...}, # Bookmarks hash
|
|
83
|
+
nick: {...}, # Nicks hash
|
|
84
|
+
gnick: {...}, # Gnicks hash
|
|
85
|
+
pwd: "/current/dir", # Current directory
|
|
86
|
+
config: <Method>, # Access to :config
|
|
87
|
+
rsh: <main> # Main rsh object
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Lifecycle Hooks
|
|
94
|
+
|
|
95
|
+
### on_startup
|
|
96
|
+
|
|
97
|
+
Called once when plugin is loaded.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
def on_startup
|
|
101
|
+
puts "Initializing my plugin..."
|
|
102
|
+
@custom_data = load_data()
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Use cases:**
|
|
107
|
+
- Initialize data structures
|
|
108
|
+
- Load configuration
|
|
109
|
+
- Check dependencies
|
|
110
|
+
- Display startup messages
|
|
111
|
+
|
|
112
|
+
### on_command_before(cmd)
|
|
113
|
+
|
|
114
|
+
Called before every command executes.
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
def on_command_before(cmd)
|
|
118
|
+
# Return false to block command
|
|
119
|
+
return false if cmd =~ /dangerous_pattern/
|
|
120
|
+
|
|
121
|
+
# Return modified command to change what executes
|
|
122
|
+
return cmd.gsub('old', 'new') if cmd.include?('old')
|
|
123
|
+
|
|
124
|
+
# Return nil to allow command unchanged
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Return values:**
|
|
130
|
+
- `false` - Block command execution
|
|
131
|
+
- `String` - Replace command with this string
|
|
132
|
+
- `nil` - Allow command unchanged
|
|
133
|
+
|
|
134
|
+
**Use cases:**
|
|
135
|
+
- Command validation
|
|
136
|
+
- Auto-correction
|
|
137
|
+
- Command logging
|
|
138
|
+
- Security checks
|
|
139
|
+
|
|
140
|
+
### on_command_after(cmd, exit_code)
|
|
141
|
+
|
|
142
|
+
Called after every command completes.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
def on_command_after(cmd, exit_code)
|
|
146
|
+
if exit_code != 0
|
|
147
|
+
log_error(cmd, exit_code)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Can track statistics, log commands, etc.
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Parameters:**
|
|
155
|
+
- `cmd` - The command that was executed
|
|
156
|
+
- `exit_code` - Integer exit status (0 = success)
|
|
157
|
+
|
|
158
|
+
**Use cases:**
|
|
159
|
+
- Command logging
|
|
160
|
+
- Error tracking
|
|
161
|
+
- Statistics collection
|
|
162
|
+
- Notifications
|
|
163
|
+
|
|
164
|
+
### on_prompt
|
|
165
|
+
|
|
166
|
+
Called when generating the prompt.
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
def on_prompt
|
|
170
|
+
return "" unless Dir.exist?('.git')
|
|
171
|
+
|
|
172
|
+
branch = `git branch --show-current`.chomp
|
|
173
|
+
" [#{branch}]"
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Return:** String to append to prompt (use ANSI codes for colors)
|
|
178
|
+
|
|
179
|
+
**ANSI Color Format:**
|
|
180
|
+
```ruby
|
|
181
|
+
"\001\e[38;5;11m\002[text]\001\e[0m\002"
|
|
182
|
+
# \001 and \002 wrap escape codes for Readline
|
|
183
|
+
# \e[38;5;11m is color 11
|
|
184
|
+
# \e[0m resets color
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Use cases:**
|
|
188
|
+
- Git branch display
|
|
189
|
+
- Virtual environment indicator
|
|
190
|
+
- Time display
|
|
191
|
+
- Status indicators
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Extension Points
|
|
196
|
+
|
|
197
|
+
### add_completions
|
|
198
|
+
|
|
199
|
+
Add TAB completion for commands.
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
def add_completions
|
|
203
|
+
{
|
|
204
|
+
"docker" => %w[ps images pull push run exec logs],
|
|
205
|
+
"kubectl" => %w[get describe create delete apply],
|
|
206
|
+
"myapp" => %w[start stop restart status]
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Return:** Hash of `"command" => [subcommands]`
|
|
212
|
+
|
|
213
|
+
**Notes:**
|
|
214
|
+
- Merges with existing `@cmd_completions`
|
|
215
|
+
- Works immediately with TAB completion system
|
|
216
|
+
|
|
217
|
+
### add_commands
|
|
218
|
+
|
|
219
|
+
Add custom shell commands.
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
def add_commands
|
|
223
|
+
{
|
|
224
|
+
"weather" => lambda do |*args|
|
|
225
|
+
city = args[0] || "oslo"
|
|
226
|
+
system("curl -s wttr.in/#{city}")
|
|
227
|
+
end,
|
|
228
|
+
"myip" => lambda do
|
|
229
|
+
require 'net/http'
|
|
230
|
+
Net::HTTP.get(URI('https://api.ipify.org'))
|
|
231
|
+
end
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Return:** Hash of `"command" => lambda`
|
|
237
|
+
|
|
238
|
+
**Notes:**
|
|
239
|
+
- Lambdas receive variadic arguments `*args`
|
|
240
|
+
- Return value printed if not nil
|
|
241
|
+
- Executed before user defuns and regular commands
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Complete Plugin Examples
|
|
246
|
+
|
|
247
|
+
### Example 1: Git Prompt
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# ~/.rsh/plugins/git_prompt.rb
|
|
251
|
+
class GitPromptPlugin
|
|
252
|
+
def initialize(rsh_context)
|
|
253
|
+
@rsh = rsh_context
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def on_prompt
|
|
257
|
+
return "" unless Dir.exist?('.git')
|
|
258
|
+
|
|
259
|
+
branch = `git branch --show-current 2>/dev/null`.chomp
|
|
260
|
+
return "" if branch.empty?
|
|
261
|
+
|
|
262
|
+
# Yellow color (11)
|
|
263
|
+
" \001\e[38;5;11m\002[#{branch}]\001\e[0m\002"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Usage:** Automatically shows git branch in prompt when in git repos
|
|
269
|
+
|
|
270
|
+
### Example 2: Command Logger
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
# ~/.rsh/plugins/command_logger.rb
|
|
274
|
+
class CommandLoggerPlugin
|
|
275
|
+
def initialize(rsh_context)
|
|
276
|
+
@rsh = rsh_context
|
|
277
|
+
@log_file = "#{ENV['HOME']}/.rsh_command.log"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def on_startup
|
|
281
|
+
File.write(@log_file, "# Log started at #{Time.now}\n", mode: 'a')
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def on_command_after(cmd, exit_code)
|
|
285
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
|
286
|
+
status = exit_code == 0 ? "OK" : "FAIL(#{exit_code})"
|
|
287
|
+
log_entry = "#{timestamp} | #{status.ljust(10)} | #{cmd}\n"
|
|
288
|
+
|
|
289
|
+
File.write(@log_file, log_entry, mode: 'a')
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def add_commands
|
|
293
|
+
{
|
|
294
|
+
"show_log" => lambda do |*args|
|
|
295
|
+
lines = args[0]&.to_i || 20
|
|
296
|
+
if File.exist?(@log_file)
|
|
297
|
+
puts File.readlines(@log_file).last(lines).join
|
|
298
|
+
else
|
|
299
|
+
"No command log found"
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Usage:**
|
|
308
|
+
- All commands automatically logged
|
|
309
|
+
- `show_log` - Show last 20 log entries
|
|
310
|
+
- `show_log 50` - Show last 50 entries
|
|
311
|
+
|
|
312
|
+
### Example 3: Kubectl Helper
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
# ~/.rsh/plugins/kubectl_completion.rb
|
|
316
|
+
class KubectlCompletionPlugin
|
|
317
|
+
def initialize(rsh_context)
|
|
318
|
+
@rsh = rsh_context
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def on_startup
|
|
322
|
+
@kubectl_available = system("command -v kubectl >/dev/null 2>&1")
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def add_completions
|
|
326
|
+
return {} unless @kubectl_available
|
|
327
|
+
|
|
328
|
+
{
|
|
329
|
+
"kubectl" => %w[get describe create delete apply edit logs exec],
|
|
330
|
+
"k" => %w[get describe create delete apply edit logs exec]
|
|
331
|
+
}
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def add_commands
|
|
335
|
+
{
|
|
336
|
+
"k" => lambda do |*args|
|
|
337
|
+
system("kubectl #{args.join(' ')}")
|
|
338
|
+
nil # Don't print return value
|
|
339
|
+
end,
|
|
340
|
+
"kns" => lambda do |*args|
|
|
341
|
+
if args[0]
|
|
342
|
+
system("kubectl config set-context --current --namespace=#{args[0]}")
|
|
343
|
+
else
|
|
344
|
+
current = `kubectl config view --minify --output 'jsonpath={..namespace}'`.chomp
|
|
345
|
+
"Current namespace: #{current.empty? ? 'default' : current}"
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
}
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Usage:**
|
|
354
|
+
- `k get pods` - Shorthand for kubectl
|
|
355
|
+
- `kns production` - Switch namespace
|
|
356
|
+
- `kns` - Show current namespace
|
|
357
|
+
- `kubectl <TAB>` - Shows completions
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Plugin Management Commands
|
|
362
|
+
|
|
363
|
+
### List Plugins
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
:plugins
|
|
367
|
+
# Shows loaded plugins with status
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Reload Plugins
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
:plugins "reload"
|
|
374
|
+
# Reloads all enabled plugins
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Disable Plugin
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
:plugins "disable", "git_prompt"
|
|
381
|
+
# Disables git_prompt plugin immediately
|
|
382
|
+
# Persists to .rshrc - stays disabled across restarts
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Enable Plugin
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
:plugins "enable", "git_prompt"
|
|
389
|
+
:plugins "reload" # Must reload to activate
|
|
390
|
+
# Removes from disabled list in .rshrc
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Plugin Info
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
:plugins "info", "git_prompt"
|
|
397
|
+
# Shows:
|
|
398
|
+
# - Class name
|
|
399
|
+
# - File location
|
|
400
|
+
# - Available hooks (✓ = implemented, ✗ = not implemented)
|
|
401
|
+
# - Extension points
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## Best Practices
|
|
407
|
+
|
|
408
|
+
### 1. Error Handling
|
|
409
|
+
|
|
410
|
+
Always wrap risky operations:
|
|
411
|
+
```ruby
|
|
412
|
+
def on_command_before(cmd)
|
|
413
|
+
begin
|
|
414
|
+
# Your logic
|
|
415
|
+
rescue => e
|
|
416
|
+
# Fail silently, don't crash shell
|
|
417
|
+
nil
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### 2. Performance
|
|
423
|
+
|
|
424
|
+
Keep hooks fast (<50ms):
|
|
425
|
+
```ruby
|
|
426
|
+
def on_command_before(cmd)
|
|
427
|
+
# Bad: Slow network call
|
|
428
|
+
# response = Net::HTTP.get(URI('slow-api.com'))
|
|
429
|
+
|
|
430
|
+
# Good: Quick local check
|
|
431
|
+
cmd.match?(/pattern/)
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### 3. State Management
|
|
436
|
+
|
|
437
|
+
Use instance variables for state:
|
|
438
|
+
```ruby
|
|
439
|
+
def initialize(rsh_context)
|
|
440
|
+
@rsh = rsh_context
|
|
441
|
+
@counter = 0
|
|
442
|
+
@cache = {}
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def on_command_after(cmd, exit_code)
|
|
446
|
+
@counter += 1
|
|
447
|
+
@cache[cmd] = exit_code
|
|
448
|
+
end
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### 4. Conditional Activation
|
|
452
|
+
|
|
453
|
+
Only activate when needed:
|
|
454
|
+
```ruby
|
|
455
|
+
def on_startup
|
|
456
|
+
@active = File.exist?('.myproject')
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def on_prompt
|
|
460
|
+
return "" unless @active
|
|
461
|
+
" [MyProject]"
|
|
462
|
+
end
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### 5. Return Values
|
|
466
|
+
|
|
467
|
+
Be explicit:
|
|
468
|
+
```ruby
|
|
469
|
+
def on_command_before(cmd)
|
|
470
|
+
return false if should_block?(cmd) # Block
|
|
471
|
+
return new_cmd if should_modify?(cmd) # Modify
|
|
472
|
+
nil # Allow unchanged
|
|
473
|
+
end
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Advanced Patterns
|
|
479
|
+
|
|
480
|
+
### Accessing rsh Internals
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
def on_startup
|
|
484
|
+
# Access history
|
|
485
|
+
recent = @rsh[:history].first(10)
|
|
486
|
+
|
|
487
|
+
# Access bookmarks
|
|
488
|
+
bookmarks = @rsh[:bookmarks]
|
|
489
|
+
|
|
490
|
+
# Get current directory
|
|
491
|
+
pwd = @rsh[:pwd]
|
|
492
|
+
|
|
493
|
+
# Access configuration
|
|
494
|
+
@rsh[:config].call("session_autosave", "300")
|
|
495
|
+
end
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Multi-hook Plugin
|
|
499
|
+
|
|
500
|
+
```ruby
|
|
501
|
+
class FullFeaturedPlugin
|
|
502
|
+
def initialize(rsh_context)
|
|
503
|
+
@rsh = rsh_context
|
|
504
|
+
@stats = {commands: 0, errors: 0}
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def on_startup
|
|
508
|
+
puts "Plugin starting..."
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def on_command_before(cmd)
|
|
512
|
+
# Validate or modify
|
|
513
|
+
@stats[:commands] += 1
|
|
514
|
+
nil
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def on_command_after(cmd, exit_code)
|
|
518
|
+
@stats[:errors] += 1 if exit_code != 0
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def on_prompt
|
|
522
|
+
" [Cmds: #{@stats[:commands]}]"
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def add_completions
|
|
526
|
+
{"mycmd" => %w[sub1 sub2]}
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def add_commands
|
|
530
|
+
{
|
|
531
|
+
"plugin_stats" => lambda do
|
|
532
|
+
"Commands: #{@stats[:commands]}, Errors: #{@stats[:errors]}"
|
|
533
|
+
end
|
|
534
|
+
}
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## Debugging Plugins
|
|
542
|
+
|
|
543
|
+
### Enable Debug Mode
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
RSH_DEBUG=1 rsh
|
|
547
|
+
# Shows plugin load messages and errors
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Common Issues
|
|
551
|
+
|
|
552
|
+
**Problem:** Plugin not loading
|
|
553
|
+
- Check class name matches file name convention
|
|
554
|
+
- Verify syntax: `ruby -c ~/.rsh/plugins/myplugin.rb`
|
|
555
|
+
- Check RSH_DEBUG output
|
|
556
|
+
|
|
557
|
+
**Problem:** Hook not firing
|
|
558
|
+
- Use `:plugins "info", "pluginname"` to see which hooks are detected
|
|
559
|
+
- Verify method name spelling (on_startup, not onstartup)
|
|
560
|
+
|
|
561
|
+
**Problem:** Command not working
|
|
562
|
+
- Check add_commands returns Hash
|
|
563
|
+
- Verify lambda syntax
|
|
564
|
+
- Test command directly in Ruby
|
|
565
|
+
|
|
566
|
+
### Test Plugin Standalone
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
ruby -e '
|
|
570
|
+
load "~/.rsh/plugins/myplugin.rb"
|
|
571
|
+
plugin = MypluginPlugin.new({})
|
|
572
|
+
puts plugin.add_commands.inspect
|
|
573
|
+
'
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Security Considerations
|
|
579
|
+
|
|
580
|
+
### Safe Practices
|
|
581
|
+
|
|
582
|
+
✓ Validate all user input
|
|
583
|
+
✓ Use system() or backticks for shell commands
|
|
584
|
+
✓ Never eval() user input directly
|
|
585
|
+
✓ Limit file access to user directories
|
|
586
|
+
✓ Handle all exceptions
|
|
587
|
+
|
|
588
|
+
### Dangerous Patterns
|
|
589
|
+
|
|
590
|
+
✗ `eval(args.join(' '))` - Command injection risk
|
|
591
|
+
✗ `File.write('/etc/...', data)` - System file modification
|
|
592
|
+
✗ Infinite loops in hooks - Shell hangs
|
|
593
|
+
✗ Network calls without timeout - Slow startup
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## Plugin Ideas
|
|
598
|
+
|
|
599
|
+
### Productivity
|
|
600
|
+
- Project template generator
|
|
601
|
+
- Quick note taker
|
|
602
|
+
- Task timer
|
|
603
|
+
- Pomodoro tracker
|
|
604
|
+
|
|
605
|
+
### Development
|
|
606
|
+
- Test runner shortcuts
|
|
607
|
+
- Deploy helpers
|
|
608
|
+
- Container management
|
|
609
|
+
- API testing tools
|
|
610
|
+
|
|
611
|
+
### System
|
|
612
|
+
- System monitor in prompt
|
|
613
|
+
- Disk space warnings
|
|
614
|
+
- Process management
|
|
615
|
+
- Network diagnostics
|
|
616
|
+
|
|
617
|
+
### Integration
|
|
618
|
+
- Slack/Discord notifications
|
|
619
|
+
- GitHub shortcuts
|
|
620
|
+
- Cloud provider CLIs
|
|
621
|
+
- Database connections
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Publishing Plugins
|
|
626
|
+
|
|
627
|
+
### Share Your Plugin
|
|
628
|
+
|
|
629
|
+
1. Create gist or repo on GitHub
|
|
630
|
+
2. Add README with usage
|
|
631
|
+
3. Share in rsh discussions
|
|
632
|
+
|
|
633
|
+
### Plugin Registry (Future)
|
|
634
|
+
|
|
635
|
+
Planned for v4.0:
|
|
636
|
+
- Central plugin registry
|
|
637
|
+
- One-command install: `:plugin install name`
|
|
638
|
+
- Auto-updates
|
|
639
|
+
- Ratings and reviews
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## API Compatibility
|
|
644
|
+
|
|
645
|
+
**Current:** v3.2.0
|
|
646
|
+
**Stability:** Beta (API may change in 3.x)
|
|
647
|
+
**Stable:** v4.0.0 (locked API)
|
|
648
|
+
|
|
649
|
+
**Breaking changes will be announced in:**
|
|
650
|
+
- CHANGELOG.md
|
|
651
|
+
- GitHub releases
|
|
652
|
+
- This guide
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Getting Help
|
|
657
|
+
|
|
658
|
+
- **Issues:** https://github.com/isene/rsh/issues
|
|
659
|
+
- **Examples:** `~/.rsh/plugins/*.rb` (included plugins)
|
|
660
|
+
- **Debug:** Run with `RSH_DEBUG=1`
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
## Example Plugin Templates
|
|
665
|
+
|
|
666
|
+
### Minimal Plugin
|
|
667
|
+
|
|
668
|
+
```ruby
|
|
669
|
+
class MinimalPlugin
|
|
670
|
+
def initialize(rsh_context)
|
|
671
|
+
@rsh = rsh_context
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Completion Plugin
|
|
677
|
+
|
|
678
|
+
```ruby
|
|
679
|
+
class CompletionPlugin
|
|
680
|
+
def initialize(rsh_context)
|
|
681
|
+
@rsh = rsh_context
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def add_completions
|
|
685
|
+
{
|
|
686
|
+
"mycommand" => %w[sub1 sub2 sub3]
|
|
687
|
+
}
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Command Plugin
|
|
693
|
+
|
|
694
|
+
```ruby
|
|
695
|
+
class CommandPlugin
|
|
696
|
+
def initialize(rsh_context)
|
|
697
|
+
@rsh = rsh_context
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def add_commands
|
|
701
|
+
{
|
|
702
|
+
"mycommand" => lambda do |*args|
|
|
703
|
+
"Executed with args: #{args.join(', ')}"
|
|
704
|
+
end
|
|
705
|
+
}
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### Prompt Plugin
|
|
711
|
+
|
|
712
|
+
```ruby
|
|
713
|
+
class PromptPlugin
|
|
714
|
+
def initialize(rsh_context)
|
|
715
|
+
@rsh = rsh_context
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def on_prompt
|
|
719
|
+
time = Time.now.strftime("%H:%M")
|
|
720
|
+
" \001\e[38;5;12m\002[#{time}]\001\e[0m\002"
|
|
721
|
+
end
|
|
722
|
+
end
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### Lifecycle Plugin
|
|
726
|
+
|
|
727
|
+
```ruby
|
|
728
|
+
class LifecyclePlugin
|
|
729
|
+
def initialize(rsh_context)
|
|
730
|
+
@rsh = rsh_context
|
|
731
|
+
@command_count = 0
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
def on_startup
|
|
735
|
+
puts "Lifecycle plugin loaded"
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def on_command_before(cmd)
|
|
739
|
+
@command_count += 1
|
|
740
|
+
nil # Don't modify
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def on_command_after(cmd, exit_code)
|
|
744
|
+
puts "Command ##{@command_count} completed" if ENV['PLUGIN_VERBOSE']
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def on_prompt
|
|
748
|
+
" [#{@command_count}]"
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
## Happy Plugin Development!
|
|
756
|
+
|
|
757
|
+
Start simple, test thoroughly, and share your creations!
|