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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +18 -0
- data/bin/trainsh +5 -0
- data/lib/trainsh/cli.rb +68 -17
- data/lib/trainsh/detectors/target/env.rb +2 -2
- data/lib/trainsh/detectors/target/kitchen.rb +42 -32
- data/lib/trainsh/mixin/builtin_commands.rb +87 -32
- data/lib/trainsh/mixin/sessions.rb +15 -3
- data/lib/trainsh/mixin/shell_output.rb +13 -0
- data/lib/trainsh/session.rb +3 -1
- data/lib/trainsh/version.rb +1 -1
- data/lib/trainsh.rb +3 -0
- metadata +6 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: e16073675a7ba8bcf1dd7940f3437d724f294bcec683995064fa7d5b6ac09cf7
         | 
| 4 | 
            +
              data.tar.gz: cfb59bdeb766c1c8c03313dc4745c55cb5579eef1e6687a8967491eb9c65f191
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 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
    
    
    
        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 | 
            -
                   | 
| 253 | 
            -
                   | 
| 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 " | 
| 309 | 
            +
                  say "Installed transports: #{transports.sort.join(', ')}"
         | 
| 259 310 | 
             
                end
         | 
| 260 311 | 
             
              end
         | 
| 261 312 | 
             
            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 | 
            -
                     | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
                     | 
| 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 | 
            -
                   | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
                   | 
| 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 | 
            -
                   | 
| 37 | 
            -
             | 
| 45 | 
            +
                  def self.prefix_env_vars
         | 
| 46 | 
            +
                    kitchen_vars = ENV.select { |key, _value| key.start_with? 'KITCHEN_' }
         | 
| 38 47 |  | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 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 | 
            -
                       | 
| 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 | 
            -
                   | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 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 | 
            -
                       | 
| 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 | 
            -
             | 
| 43 | 
            -
                     | 
| 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 | 
            -
                       | 
| 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 | 
            -
                     | 
| 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 | 
            -
                     | 
| 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  | 
| 64 | 
            -
                     | 
| 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 | 
            -
                       | 
| 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 | 
            -
                     | 
| 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  | 
| 86 | 
            -
                     | 
| 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 | 
            -
                     | 
| 141 | 
            +
                    show_message session.host
         | 
| 95 142 | 
             
                  end
         | 
| 96 143 |  | 
| 97 144 | 
             
                  def builtincmd_ping(_args = nil)
         | 
| 98 145 | 
             
                    session.run_idle
         | 
| 99 | 
            -
             | 
| 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 | 
            -
                     | 
| 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 | 
            -
                     | 
| 166 | 
            +
                    show_message 'Active sessions:'
         | 
| 112 167 |  | 
| 113 168 | 
             
                    @sessions.each_with_index do |session, idx|
         | 
| 114 | 
            -
                       | 
| 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 | 
            -
                       | 
| 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 | 
            -
                     | 
| 143 | 
            -
                  rescue  | 
| 144 | 
            -
                     | 
| 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 | 
            -
                     | 
| 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 | 
            -
                     | 
| 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 | 
| 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  | 
| 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 |  | 
    
        data/lib/trainsh/session.rb
    CHANGED
    
    | @@ -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
         | 
    
        data/lib/trainsh/version.rb
    CHANGED
    
    
    
        data/lib/trainsh.rb
    CHANGED
    
    
    
        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. | 
| 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- | 
| 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
         |