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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/PLUGIN_GUIDE.md +757 -0
  3. data/README.md +64 -1
  4. data/bin/rsh +433 -36
  5. 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!