trainsh 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b809837d484a4a100b8d6b6d4718d623bb91990ea72d7181b5e70dbe265282a
4
- data.tar.gz: 12a87fc192b92df5f3f0528c620d898c24dc5d7ec849b51081c7d996d57e51fa
3
+ metadata.gz: e16073675a7ba8bcf1dd7940f3437d724f294bcec683995064fa7d5b6ac09cf7
4
+ data.tar.gz: cfb59bdeb766c1c8c03313dc4745c55cb5579eef1e6687a8967491eb9c65f191
5
5
  SHA512:
6
- metadata.gz: adef421ee124699ee90b841a57c0ef0653e7377a2d9cb6b3f6b0faa50948cc903e36fe38af4893ac8d60e8a9c94eb1adf3f8c66cd733c163700fac1480d148a4
7
- data.tar.gz: 98947781c875c65f82563caeafba4637764c65758ebf8edf60fd16bf0765afe3a286032045957c20c0291248baf75f46dc165f25605492eb920b620a1d7737bc
6
+ metadata.gz: 2ea26ef8e8a52d8bf457a46b0ce39156fb81d47757ad6e9a90883bbd901fae275140d46e412b19638589b613be13746f6c83ec606533d707c4c1b8ad02df359c
7
+ data.tar.gz: 33ede37016876d9295bb8e75ef57aa577c8a569413728a040850842a9aae6cdbc924236c3e12d9ee643afc46a769b9475d81313825b494f248121284269d1430
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 0.3.0
4
+
5
+ - Add auto-detection of targets from environment (variable, test-kitchen)
6
+ - Add `!copy` for cross-session copying
7
+ - Add `!help` and `?` for usage in shell
8
+ - Add detailed help for commands
9
+ - Add output on copying files cross-session and editing/saving
10
+ - Add devcontainer for VSCode
11
+ - Change errors to be red, Shell built-in to yellow
12
+ - Fix handling of URL encoded parameters
13
+ - Fix `list-transports` to output all installed non-API transports
14
+ - Fix crashes on unexpected input in shell
15
+ - Fix exception handling in various places
16
+ - Fix missing binary in Gem
17
+
3
18
  ## Version 0.2.0
4
19
 
5
20
  - Renamed to TrainSH
data/README.md CHANGED
@@ -69,6 +69,9 @@ Clear your TrainSH history, for example to remove clutter or sensitive informati
69
69
  `!connect <uri>`
70
70
  Connect to another system. The URI needs to match the format of the used Train transport, which is usually `transportname://host` but varies. See the Train transport's documentation for details.
71
71
 
72
+ `!copy @<session>:/<path> @<session>:/<path>`
73
+ Copy a file between two established sessions.
74
+
72
75
  `!detect`
73
76
  Re-runs the OS detection which is running automatically on start. This will determine the OS, OS-family and general platform information via Train.
74
77
 
@@ -81,6 +84,9 @@ Downloads the remote file as temporary file and opens the system default editor
81
84
  `!env`
82
85
  Prints the environment variables of your remote shell. This will be filled on first command invocation to save IO. **Currently unsupported for Windows remote systems**
83
86
 
87
+ `!help`
88
+ Print out help
89
+
84
90
  `!history`
85
91
  Output your TrainSH command history. As this uses the popular Readline library, you can also navigate your history with the Up/Down arrows or use Ctrl-R for reverse search. You can do auto completion for built-in commands.
86
92
 
@@ -141,3 +147,15 @@ To make this easier, internal commands get attached to your input like this:
141
147
  - Postfix: Retrieve and save new environment variables
142
148
 
143
149
  Output of commands gets separated by outputting a highly random string between, which should not result in false positives. If a false positive occurs for some reason, TrainSH will fail and output an error.
150
+
151
+ ## Target Detection
152
+
153
+ As providing target URLs to connect to can be tedious, TrainSH will detect targets to connect to via plugins. In these cases, `trainsh connect` does not need any parameters.
154
+
155
+ ### Environment Variables
156
+
157
+ This will check the `TARGET` environment variable for a URL and use it to connect. Only one target is allowed.
158
+
159
+ ### Test Kitchen Configuration
160
+
161
+ This will detect if the current directory has a Test Kitchen configuration and a created machine. If so, it will connect to the machine by parsing information in `.kitchen/` and the kitchen configuration file.
data/bin/trainsh ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
3
+ require 'trainsh/cli'
4
+
5
+ TrainSH::Cli.start
data/lib/trainsh/cli.rb CHANGED
@@ -4,14 +4,12 @@ require_relative 'session'
4
4
  require_relative 'mixin/builtin_commands'
5
5
  require_relative 'mixin/file_helpers'
6
6
  require_relative 'mixin/sessions'
7
-
8
- # TODO
9
- # require_relative 'detectors/target/env.rb'
10
- # require_relative 'detectors/target/kitchen.rb'
7
+ require_relative 'mixin/shell_output'
11
8
 
12
9
  require 'colored'
13
10
  require 'fileutils'
14
11
  require 'readline'
12
+ require 'rubygems'
15
13
  require 'train'
16
14
  require 'thor'
17
15
 
@@ -19,7 +17,6 @@ module TrainSH
19
17
  class Cli < Thor
20
18
  include Thor::Actions
21
19
  check_unknown_options!
22
- # add_runtime_options!
23
20
 
24
21
  def self.exit_on_failure?
25
22
  true
@@ -34,10 +31,14 @@ module TrainSH
34
31
  EXIT_COMMANDS = %w[!!! exit quit logout disconnect].freeze
35
32
  INTERACTIVE_COMMANDS = %w[more less vi vim nano].freeze
36
33
 
34
+ NON_OS_TRANSPORTS = %w[aws core kubernetes azure pgsql vsphere vault digitalocean rest].freeze
35
+ CORE_TRANSPORTS = %w[docker ssh].freeze
36
+
37
37
  no_commands do
38
38
  include TrainSH::Mixin::BuiltInCommands
39
39
  include TrainSH::Mixin::FileHelpers
40
40
  include TrainSH::Mixin::Sessions
41
+ include TrainSH::Mixin::ShellOutput
41
42
 
42
43
  def __disconnect
43
44
  session.close
@@ -70,12 +71,19 @@ module TrainSH
70
71
  end
71
72
 
72
73
  def target_detectors
73
- Dir[File.join(__dir__, 'lib', '*.rb')].sort.each { |file| require file }
74
-
75
74
  TrainSH::Detectors::TargetDetector.descendants
76
75
  end
77
76
 
77
+ def detect_target
78
+ target_detectors.detect(&:url).url
79
+ end
80
+
78
81
  def execute(input)
82
+ if input == '?'
83
+ execute_builtin 'help'
84
+ return
85
+ end
86
+
79
87
  case input[0]
80
88
  when '.'
81
89
  execute_locally input[1..]
@@ -142,7 +150,7 @@ module TrainSH
142
150
  end
143
151
 
144
152
  def prompt
145
- exitcode = current_session.exitcode
153
+ exitcode = current_session.exitcode || 0
146
154
  exitcode_prefix = exitcode.zero? ? 'OK '.green : format('E%02d ', exitcode).red
147
155
 
148
156
  format(::TrainSH::PROMPT,
@@ -159,7 +167,7 @@ module TrainSH
159
167
 
160
168
  choices.concat(builtin_commands.map { |cmd| "!#{cmd.tr('_', '-')}" })
161
169
  choices.concat(sessions.map { |session_id| "@#{session_id}" })
162
- choices.concat %w[!!!]
170
+ choices.concat %w[!!! ?]
163
171
 
164
172
  choices.filter { |choice| choice.start_with? partial }
165
173
  end
@@ -175,14 +183,55 @@ module TrainSH
175
183
  #
176
184
  # Logger.const_get(l.upcase)
177
185
  # end
186
+
187
+ def local_gems
188
+ Gem::Specification.sort_by { |g| [g.name.downcase, g.version] }.group_by(&:name)
189
+ end
178
190
  end
179
191
 
180
192
  # class_option :log_level, desc: "Log level", aliases: "-l", default: :info
181
193
  class_option :messy, desc: 'Skip deletion of temporary files for speedup', default: false, type: :boolean
182
194
 
183
195
  desc 'connect URL', 'Connect to a destination interactively'
196
+ long_desc <<-DESC
197
+ Create an interactive shell session with the remote system. The specified URL has to match the
198
+ chosen transport plugin.
199
+
200
+ If no URL was given, possible targets are detected from the environment variable TARGET or any
201
+ existing Test Kitchen instances (max: 1).
202
+
203
+ URL Examples:
204
+ docker://d9443b195d16
205
+ local://
206
+ ssh://user@remote.example.com
207
+ winrm://Administrator:PASSWORD@10.2.42.1
208
+
209
+ URL Examples from non-standard transports:
210
+ aws-ssm://i-1234567890ab
211
+ serial://dev/ttyUSB1/9600
212
+ telnet://127.0.0.1
213
+ vsphere-gom://Administrator@vcenter.server/virtual.machine
214
+
215
+ Every transport has its own, proprietary options which can currently only be added as URL
216
+ query parameters:
217
+ ssh://user@remote.example.com?key_files=/home/ubuntu/test.pem
218
+
219
+ Passwords currently have to be part of the URL.
220
+ DESC
221
+ def connect(url = nil)
222
+ # TODO: Pass options to `use_session`
223
+ unless url
224
+ show_message 'No URL given, trying to detect ...'
225
+ url = detect_target
226
+
227
+ show_message "Detected URL to be #{url}" if url
228
+ end
229
+
230
+ unless url
231
+ show_error 'No target could be detected'
232
+ exit
233
+ end
184
234
 
185
- def connect(url)
186
235
  exit unless use_session(url)
187
236
 
188
237
  say format('Connected to %<url>s', url: session.url).bold
@@ -227,14 +276,19 @@ module TrainSH
227
276
 
228
277
  execute input
229
278
  end
279
+ rescue Interrupt
280
+ show_error 'Interrupted execution'
230
281
  end
231
282
 
232
283
  # desc 'copy FILE/DIR|URL FILE/DIR|URL', 'Copy files or directories'
233
284
  # def copy(url_or_file, url_or_file)
234
- # # TODO?
285
+ # # TODO
235
286
  # end
236
287
 
237
288
  desc 'detect URL', 'Retrieve remote OS and platform information'
289
+ long_desc <<~DESC
290
+ Detect remote OS via Train. Uses the same schema as URLs for `connect`.
291
+ DESC
238
292
  def detect(url)
239
293
  exit unless use_session(url)
240
294
  __detect
@@ -249,13 +303,10 @@ module TrainSH
249
303
 
250
304
  desc 'list-transports', 'List available transports'
251
305
  def list_transports
252
- # TODO: Train only lazy-loads and does not have a full registry
253
- # https://github.com/inspec/train/blob/be90ca53ea1c1e8aa7439c504fbee86f4b399d83/lib/train.rb#L38-L61
254
-
255
- # TODO: Filter for only "OS" transports as well
256
- transports = %w[local ssh winrm docker]
306
+ installed = local_gems.select { |name| name.start_with? 'train-' }.keys.map { |name| name.delete_prefix('train-') }
307
+ transports = installed - NON_OS_TRANSPORTS + CORE_TRANSPORTS
257
308
 
258
- say "Available transports: #{transports.sort.join(', ')}"
309
+ say "Installed transports: #{transports.sort.join(', ')}"
259
310
  end
260
311
  end
261
312
  end
@@ -3,8 +3,8 @@ require_relative '../target'
3
3
  module TrainSH
4
4
  module Detectors
5
5
  class EnvTarget < TargetDetector
6
- def url
7
- ENV['target']
6
+ def self.url
7
+ ENV['TARGET']
8
8
  end
9
9
  end
10
10
  end
@@ -1,44 +1,54 @@
1
1
  require_relative '../target'
2
2
 
3
+ require 'yaml' unless defined?(YAML)
4
+
3
5
  module TrainSH
4
6
  module Detectors
5
7
  class KitchenTarget < TargetDetector
6
- def url
7
- # return unless kitchen_available?
8
- # return unless kitchen_instance
9
-
10
- # kitchen_config['transport']
8
+ def self.url
9
+ return unless kitchen_directory
10
+
11
+ files = Dir.glob("#{kitchen_directory}/*.yml")
12
+ return if files.empty?
13
+
14
+ # TODO: allow connecting to multiple instances
15
+ if files.count > 1
16
+ say "Found #{files.count} active kitchen instances, while only supporting 1"
17
+ exit
18
+ end
19
+
20
+ # Can get IP only from YAML files
21
+ instance_yaml = YAML.load_file(files.first)
22
+
23
+ # Can get user + protocol only from kitchen
24
+ instance_name = File.basename(files.first, '.yml')
25
+ env_prefix = prefix_env_vars
26
+ cmd = "#{env_prefix} kitchen diagnose #{instance_name}"
27
+ instance_data = YAML.safe_load(`#{cmd}`, [Symbol, Array, String])
28
+
29
+ transport = instance_data.dig('instances', instance_name, 'transport')
30
+
31
+ # TODO: Additional parameters like keypair etc
32
+ format('%<transport>s://%<user>s%<password>s@%<host>s',
33
+ transport: transport['name'],
34
+ user: transport['username'] || transport['user'],
35
+ password: transport['password'] ? ":#{transport['password']}" : '',
36
+ host: instance_yaml['hostname'] || instance_yaml['host']
37
+ )
11
38
  end
12
39
 
13
- # private
14
-
15
- # def kitchen_available?
16
- # `kitchen`
17
- # true
18
- # rescue
19
- # false
20
- # end
21
-
22
- # attr_writer :kitchen_instances
23
- # def kitchen_instances
24
- # return if @kitchen_instances
25
-
26
- # @kitchen_instances ||= YAML.load(`KITCHEN_LOCAL_YAML=".kitchen.ec2.yaml" kitchen list --json`)
27
- # end
28
-
29
- # def kitchen_config
30
- # return if @kitchen_config
31
-
32
- # parsed_output = YAML.load(`KITCHEN_LOCAL_YAML=".kitchen.ec2.yaml" kitchen diagnose customize-amazon2`)
33
- # @kitchen_config = kitchen_config['instances'].keys.first
34
- # end
40
+ def self.kitchen_directory
41
+ # TODO: Recurse up
42
+ '.kitchen' if Dir.exist?('.kitchen')
43
+ end
35
44
 
36
- # def kitchen_instance_name
37
- # kitchen_instances.select! { |instance| instance['last_action'] != nil }
45
+ def self.prefix_env_vars
46
+ kitchen_vars = ENV.select { |key, _value| key.start_with? 'KITCHEN_' }
38
47
 
39
- # return nil if kitchen_instances.count != 1
40
- # kitchen_instances.first['instance']
41
- # end
48
+ # rubocop:disable Style/StringConcatenation
49
+ kitchen_vars.map { |key, value| "#{key}=\"#{value}\"" }.join(' ') + ' '
50
+ # rubocop:enable Style/StringConcatenation
51
+ end
42
52
  end
43
53
  end
44
54
  end
@@ -4,6 +4,7 @@ module TrainSH
4
4
  module Mixin
5
5
  module BuiltInCommands
6
6
  BUILTIN_PREFIX = 'builtincmd_'.freeze
7
+ SESSION_PATH_REGEX = %r{(/@(\d+):(/.*)$/)}.freeze
7
8
 
8
9
  def builtin_commands
9
10
  methods.sort.filter { |method| method.to_s.start_with? BUILTIN_PREFIX }.map { |method| method.to_s.delete_prefix BUILTIN_PREFIX }
@@ -15,16 +16,33 @@ module TrainSH
15
16
 
16
17
  def builtincmd_connect(url = nil)
17
18
  if url.nil? || url.strip.empty?
18
- say 'Expecting session url, e.g. `!connect docker://123456789abcdef0`'.red
19
+ show_error 'Expecting session url, e.g. `!connect docker://123456789abcdef0`'
19
20
  return false
20
21
  end
21
22
 
22
23
  use_session(url)
23
24
  end
24
25
 
25
- # def builtincmd_copy(source = nil, destination = nil)
26
- # # TODO: Copy files between sessions
27
- # end
26
+ def builtincmd_copy(src = nil, dst = nil)
27
+ src_id, src_path = src&.match(SESSION_PATH_REGEX)&.captures
28
+ dst_id, dst_path = dst&.match(SESSION_PATH_REGEX)&.captures
29
+ unless src && dst && src_id && dst_id && src_path && dst_path
30
+ show_error 'Expecting source and destination, e.g. `!copy @0:/etc/hosts @1:/home/ubuntu/old_hosts'
31
+ return
32
+ end
33
+
34
+ src_session = session(src_id)
35
+ dst_session = session(dst_id)
36
+ unless src_session && dst_session
37
+ show_error 'Expecting valid session identifiers. Check available sessions via !sessions'
38
+ return
39
+ end
40
+
41
+ content = src_session.file(src_path)
42
+ dst_session.file(dst_path).content = content
43
+
44
+ show_message "Copied #{content.size} bytes successfully"
45
+ end
28
46
 
29
47
  def builtincmd_detect(_args = nil)
30
48
  __detect
@@ -32,45 +50,72 @@ module TrainSH
32
50
 
33
51
  def builtincmd_download(remote_path = nil, local_path = nil)
34
52
  if remote_path.nil? || local_path.nil?
35
- say 'Expecting remote path and local path, e.g. `!download /etc/passwd /home/ubuntu`'
53
+ show_error 'Expecting remote path and local path, e.g. `!download /etc/passwd /home/ubuntu`'
36
54
  return false
37
55
  end
38
56
 
39
57
  return unless train_mutable?
40
58
 
41
59
  session.download(remote_path, local_path)
42
- rescue ::Train::NotImplementedError
43
- say 'Backend for session does not implement download operation'.red
60
+
61
+ show_message "Downloaded #{remote_path} successfully"
62
+ rescue NotImplementedError
63
+ show_error 'Backend for session does not implement file operations'
64
+ rescue StandardError => e
65
+ show_error "Error occured: #{e.message}"
44
66
  end
45
67
 
46
68
  def builtincmd_edit(path = nil)
47
69
  if path.nil? || path.strip.empty?
48
- say 'Expecting remote path, e.g. `!less /tmp/somefile.txt`'.red
70
+ show_error 'Expecting remote path, e.g. `!less /tmp/somefile.txt`'
49
71
  return false
50
72
  end
51
73
 
52
74
  tempfile = read_file(path)
75
+ old_content = File.read(tempfile.path)
53
76
 
54
77
  localeditor = ENV['EDITOR'] || ENV['VISUAL'] || 'vi' # TODO: configuration, Windows, ...
55
- say format('Using local editor `%<editor>s` for %<tempfile>s', editor: localeditor, tempfile: tempfile.path)
78
+ show_message format('Using local editor `%<editor>s` for %<tempfile>s', editor: localeditor, tempfile: tempfile.path)
56
79
 
57
80
  system("#{localeditor} #{tempfile.path}")
58
-
59
81
  new_content = File.read(tempfile.path)
60
82
 
61
- write_file(path, new_content)
83
+ if new_content == old_content
84
+ show_message 'No changes detected'
85
+ else
86
+ write_file(path, new_content)
87
+
88
+ show_message "Wrote #{new_content.size} bytes successfully"
89
+ end
90
+
62
91
  tempfile.unlink
63
- rescue ::Train::NotImplementedError
64
- say 'Backend for session does not implement file operations'.red
92
+ rescue NotImplementedError
93
+ show_error 'Backend for session does not implement file operations'
94
+ rescue StandardError => e
95
+ show_error "Error occured: #{e.message}"
65
96
  end
66
97
 
67
98
  def builtincmd_env(_args = nil)
68
99
  puts session.env
69
100
  end
70
101
 
102
+ def builtincmd_help(_args = nil)
103
+ show_message <<~HELP
104
+ Unprefixed commands get sent to the remote host of the active session.
105
+
106
+ Commands with a prefix of `@n` with n being a number will be executed on the specified session. For a list of sessions check `!sessions`.
107
+
108
+ Commands with a prefix of `.` get executed locally.
109
+
110
+ Builtin commands are prefixed with `!`:
111
+ HELP
112
+
113
+ builtin_commands.each { |cmd| show_message " !#{cmd}" }
114
+ end
115
+
71
116
  def builtincmd_read(path = nil)
72
117
  if path.nil? || path.strip.empty?
73
- say 'Expecting remote path, e.g. `!read /tmp/somefile.txt`'.red
118
+ show_error 'Expecting remote path, e.g. `!read /tmp/somefile.txt`'
74
119
  return false
75
120
  end
76
121
 
@@ -78,12 +123,14 @@ module TrainSH
78
123
  return false unless tempfile
79
124
 
80
125
  localpager = ENV['PAGER'] || 'less' # TODO: configuration, Windows, ...
81
- say format('Using local pager `%<pager>s` for %<tempfile>s', pager: localpager, tempfile: tempfile.path)
126
+ show_message format('Using local pager `%<pager>s` for %<tempfile>s', pager: localpager, tempfile: tempfile.path)
82
127
  system("#{localpager} #{tempfile.path}")
83
128
 
84
129
  tempfile.unlink
85
- rescue ::NotImplementedError
86
- say 'Backend for session does not implement file operations'.red
130
+ rescue NotImplementedError
131
+ show_error 'Backend for session does not implement file operations'
132
+ rescue StandardError => e
133
+ show_error "Error occured: #{e.message}"
87
134
  end
88
135
 
89
136
  def builtincmd_history(_args = nil)
@@ -91,16 +138,24 @@ module TrainSH
91
138
  end
92
139
 
93
140
  def builtincmd_host(_args = nil)
94
- say session.host
141
+ show_message session.host
95
142
  end
96
143
 
97
144
  def builtincmd_ping(_args = nil)
98
145
  session.run_idle
99
- say format('Ping: %<ping>dms', ping: session.ping)
146
+
147
+ show_message format('Ping: %<ping>dms', ping: session.ping)
100
148
  end
101
149
 
150
+ # rubocop:disable Lint/Debugger
151
+ def builtincmd_pry(_args = nil)
152
+ require 'pry' unless defined?(binding.pry)
153
+ binding.pry
154
+ end
155
+ # rubocop:enable Lint/Debugger
156
+
102
157
  def builtincmd_pwd(_args = nil)
103
- say session.pwd
158
+ show_message session.pwd
104
159
  end
105
160
 
106
161
  def builtincmd_reconnect(_args = nil)
@@ -108,20 +163,16 @@ module TrainSH
108
163
  end
109
164
 
110
165
  def builtincmd_sessions(_args = nil)
111
- say 'Active sessions:'
166
+ show_message 'Active sessions:'
112
167
 
113
168
  @sessions.each_with_index do |session, idx|
114
- say format('[%<idx>d] %<session>s', idx: idx, session: session.url)
169
+ show_message format('[%<idx>d] %<session>s', idx: idx, session: session.url)
115
170
  end
116
171
  end
117
172
 
118
173
  def builtincmd_session(session_id = nil)
119
174
  session_id = validate_session_id(session_id)
120
-
121
- if session_id.nil?
122
- say 'Expecting valid session id, e.g. `!session 2`'.red
123
- return false
124
- end
175
+ return if session_id.nil?
125
176
 
126
177
  # TODO: Make this more pretty
127
178
  session_url = @sessions[session_id].url
@@ -131,17 +182,21 @@ module TrainSH
131
182
 
132
183
  def builtincmd_upload(local_path = nil, remote_path = nil)
133
184
  if remote_path.nil? || local_path.nil?
134
- say 'Expecting remote path and local path, e.g. `!download /home/ubuntu/passwd /etc`'
185
+ show_error 'Expecting remote path and local path, e.g. `!download /home/ubuntu/passwd /etc'
135
186
  return false
136
187
  end
137
188
 
138
189
  return unless train_mutable?
139
190
 
140
191
  session.upload(local_path, remote_path)
192
+
193
+ show_message "Uploaded to #{remote_path} successfully"
141
194
  rescue ::Errno::ENOENT
142
- say "Local file/directory '#{local_path}' does not exist".red
143
- rescue ::NotImplementedError
144
- say 'Backend for session does not implement upload operation'.red
195
+ show_error "Local file/directory '#{local_path}' does not exist"
196
+ rescue NotImplementedError
197
+ show_error 'Backend for session does not implement upload operation'
198
+ rescue StandardError => e
199
+ show_error "Error occured: #{e.message}"
145
200
  end
146
201
 
147
202
  private
@@ -149,7 +204,7 @@ module TrainSH
149
204
  def train_mutable?
150
205
  return true if session.respond_to?(:upload)
151
206
 
152
- say "Support for remote file modification needs at least Train #{::TrainSH::TRAIN_MUTABLE_VERSION} (is: #{::Train::VERSION})".red
207
+ show_error "Support for remote file modification needs at least Train #{::TrainSH::TRAIN_MUTABLE_VERSION} (is: #{::Train::VERSION})"
153
208
  end
154
209
  end
155
210
  end
@@ -20,7 +20,9 @@ module TrainSH
20
20
  end
21
21
 
22
22
  def session(session_id = current_session_id)
23
- @sessions[session_id]
23
+ id = validate_session_id(session_id)
24
+
25
+ @sessions[id] if id
24
26
  end
25
27
 
26
28
  # ?
@@ -37,13 +39,23 @@ module TrainSH
37
39
  end
38
40
 
39
41
  def validate_session_id(session_id)
40
- unless session_id.match?(/^[0-9]+$/)
42
+ unless session_id
43
+ say 'Expecting valid session id, e.g. `!session 2`'.red
44
+ return
45
+ end
46
+
47
+ unless session_id.to_s.match?(/^[0-9]+$/)
41
48
  say 'Expected session id to be numeric'.red
42
49
  return
43
50
  end
44
51
 
45
52
  if @sessions[session_id.to_i].nil?
46
- say format('No session id [%s] found', session_id).red
53
+ say 'Expecting valid session id, e.g. `!session 2`'.red
54
+
55
+ say "\nActive sessions:"
56
+ @sessions.each_with_index { |data, idx| say "[#{idx}] #{data.url}" }
57
+ say
58
+
47
59
  return
48
60
  end
49
61
 
@@ -0,0 +1,13 @@
1
+ module TrainSH
2
+ module Mixin
3
+ module ShellOutput
4
+ def show_error(message)
5
+ say message.red
6
+ end
7
+
8
+ def show_message(message)
9
+ say message.yellow
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,10 +1,11 @@
1
1
  require 'benchmark'
2
2
  require 'forwardable'
3
-
3
+ require 'cgi'
4
4
  require 'train'
5
5
 
6
6
  module TrainSH
7
7
  class Command
8
+ # Used for command separation, randomly generated
8
9
  MAGIC_STRING = 'mVDK6afaqa6fb7kcMqTpR2aoUFbYsRt889G4eGoI'.freeze
9
10
 
10
11
  attr_writer :connection
@@ -98,6 +99,7 @@ module TrainSH
98
99
  @url = url
99
100
 
100
101
  data = Train.unpack_target_from_uri(url)
102
+ data.transform_values! { |val| CGI.unescape(val) }
101
103
 
102
104
  # TODO: Wire up with "messy" parameter
103
105
  data[:cleanup] = false
@@ -1,3 +1,3 @@
1
1
  module TrainSH
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
data/lib/trainsh.rb CHANGED
@@ -3,3 +3,6 @@ require_relative 'trainsh/constants'
3
3
  require_relative 'trainsh/errors'
4
4
  # require_relative 'trainsh/log'
5
5
  require_relative 'trainsh/version'
6
+
7
+ require_relative 'trainsh/detectors/target/env'
8
+ require_relative 'trainsh/detectors/target/kitchen'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trainsh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Heinen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-25 00:00:00.000000000 Z
11
+ date: 2021-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bump
@@ -168,12 +168,14 @@ description: Based on the Train ecosystem, provide a shell to manage systems via
168
168
  multitude of transports.
169
169
  email:
170
170
  - theinen@tecracer.de
171
- executables: []
171
+ executables:
172
+ - trainsh
172
173
  extensions: []
173
174
  extra_rdoc_files: []
174
175
  files:
175
176
  - CHANGELOG.md
176
177
  - README.md
178
+ - bin/trainsh
177
179
  - lib/trainsh.rb
178
180
  - lib/trainsh/cli.rb
179
181
  - lib/trainsh/config.rb
@@ -186,6 +188,7 @@ files:
186
188
  - lib/trainsh/mixin/builtin_commands.rb
187
189
  - lib/trainsh/mixin/file_helpers.rb
188
190
  - lib/trainsh/mixin/sessions.rb
191
+ - lib/trainsh/mixin/shell_output.rb
189
192
  - lib/trainsh/session.rb
190
193
  - lib/trainsh/version.rb
191
194
  homepage: https://github.com/tecracer-chef/trainsh