trainsh 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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