class Gistribute::CLI

Public Class Methods

new() click to toggle source
# File lib/cli.rb, line 9
    def initialize
      @options = OptimistXL.options do
        version "gistribute #{File.read(File.expand_path('../VERSION', __dir__)).strip}"

        banner <<~BANNER
          #{version}

          Usage:
            gistribute SUBCOMMAND [OPTION]... INPUT

          Try `gistribute SUBCOMMAND -h` for more info. Available subcommands:
            * login: log into your GitHub account
            * logout: log out of your GitHub account
            * install: download and install files from a gistribution
            * upload: upload a new gistribution

          Options:
        BANNER

        opt :version, "display version number"
        opt :help, "display a help message"

        # Sub-commands can't access the version from this scope for whatever reason
        v = version

        subcmd :login, "log into your GitHub account"
        subcmd :logout, "log out of your GitHub account"

        subcmd :install, "install from a gistribution" do
          banner <<~BANNER
            #{v}

            Usage:
              gistribute install [OPTION]... URL_OR_ID

            Options:
          BANNER

          opt :yes, "install files without prompting"
          opt :force, "overwrite existing files without prompting"
        end

        subcmd :upload, "upload a gistribution" do
          banner <<~BANNER
            #{v}

            Usage:
              gistribute upload [OPTION]... FILE...
              gistribute upload [OPTION]... DIRECTORY

            Options:
          BANNER

          opt :description, "description for the Gist", type: :string
          opt :private, "use a private Gist"
          opt :yes, "upload files without prompting"
        end

        educate_on_error
      end

      @subcommand, @global_options, @subcommand_options =
        @options.subcommand, @options.global_options, @options.subcommand_options

      authenticate unless @subcommand == "logout"

      case @subcommand
      when "install"
        @gist_input = ARGV.first
      when "upload"
        @files = ARGV.dup
      end
    end

Public Instance Methods

authenticate() click to toggle source
# File lib/cli/auth.rb, line 5
    def authenticate
      access_token = if File.exist?(CONFIG_FILE)
        File.read(CONFIG_FILE).strip
      else
        device_res = URI.decode_www_form(
          Net::HTTP.post_form(
            URI("https://github.com/login/device/code"), "client_id" => CLIENT_ID, "scope" => "gist"
          ).body
        ).to_h

        retry_interval = device_res["interval"].to_i

        Launchy.open(device_res["verification_uri"])
        puts <<~EOS
          Opening GitHub, please enter the authentication code: #{device_res['user_code']}
          If your browser did not open, visit #{device_res['verification_uri']}
        EOS

        uri = URI("https://github.com/login/oauth/access_token")

        # Keep trying until the user enters the code or the device code expires
        token = nil
        loop do
          sleep(retry_interval)

          response = URI.decode_www_form(
            Net::HTTP.post_form(
              uri, "client_id" => CLIENT_ID, "device_code" => device_res["device_code"],
                   "grant_type" => "urn:ietf:params:oauth:grant-type:device_code"
            ).body
          ).to_h

          if (token = response["access_token"])
            File.write(CONFIG_FILE, token)
            break
          elsif response["error"] == "authorization_pending"
            # The user has not yet entered the code; keep waiting silently
            next
          elsif response["error"] == "expired_token"
            panic! "Token expired! Please try again."
          else
            panic! response["error_description"]
          end
        end

        token
      end

      @client = Octokit::Client.new(access_token:)

      success_message = "Logged in as #{@client.user.login}."
      if @subcommand == "login"
        puts success_message.green
      else
        puts success_message
        puts
      end
    end
confirm?(prompt) click to toggle source
# File lib/cli.rb, line 99
def confirm?(prompt)
  print prompt
  input = $stdin.gets.strip.downcase

  input.start_with?("y") || input.empty?
end
get_input(prompt) click to toggle source
# File lib/cli.rb, line 106
def get_input(prompt)
  print prompt
  $stdin.gets.strip
end
install() click to toggle source
# File lib/cli/install.rb, line 5
    def install
      print "Downloading data..."

      id = Gistribute.parse_id(ARGV.first)

      begin
        gist = @client.gist(id)
      rescue Octokit::Error => e
        $stderr.print <<~EOS.chop.red
          \rThere was an error downloading the requested Gist.
          The error is as follows:
        EOS
        $stderr.puts " #{e.response_status} #{JSON.parse(e.response_body)['message']}"

        $stderr.print "The ID that was queried is: ".red
        $stderr.puts id

        exit 1
      end

      # Regular expression word wrap to keep lines less than 80 characters. Then
      # check to see if it's empty- if not, put newlines on each side so that it
      # will be padded when displayed in the output.
      gist_description = gist.description.gsub(/(.{1,79})(\s+|\Z)/, "\\1\n").strip
      gist_description = "\n#{gist_description}\n" unless gist_description.empty?

      # Process files
      files = gist.files.map do |filename, data|
        metadata = filename.to_s.split("||").map(&:strip)

        # | as path separator in the Gist's file name, as Gist doesn't allow the
        # usage of /.
        path = Gistribute.decode(metadata.last)
        # Default description is the name of the file.
        description = metadata.size == 1 ? File.basename(path) : metadata.first

        { description:, path:, content: data[:content] }
      end

      puts <<~EOS
        \rFinished downloading Gist from: #{gist.html_url}
        Gist uploaded by #{
          gist.owner ? "user #{gist.owner[:login]}" : 'an anonymous user'
        }.
        #{gist_description}
      EOS

      unless @subcommand_options.yes
        puts "Files:"

        files.each do |f|
          print "#{f[:description]}: " unless f[:description].empty?
          puts f[:path]
        end
      end

      if @subcommand_options.yes || confirm?("\nWould you like to install these files? [Yn] ")
        puts unless @subcommand_options.yes

        files.each do |f|
          if File.exist?(f[:path]) && !@subcommand_options.force &&
             !confirm?("File already exists: #{f[:path]}\nWould you like to overwrite it? [Yn] ")
            puts " #{'*'.red} #{f[:description]} skipped."
            next
          end

          # Handle directories that don't exist.
          FileUtils.mkdir_p File.dirname(f[:path])
          File.write(f[:path], f[:content])

          # If using `--yes`, we print the path in the this string rather than
          # above with the prompt
          puts " #{'*'.green} #{f[:description]} installed#{
            @subcommand_options.yes ? " to: #{f[:path]}" : '.'
          }"
        end
      else
        puts "Aborting.".red
      end
    end
panic!(message) click to toggle source

Prints an error message and exits the program.

# File lib/cli.rb, line 112
def panic!(message)
  $stderr.puts "#{'Error'.red}: #{message}"
  exit 1
end
process_file(file) click to toggle source
# File lib/cli/upload.rb, line 33
def process_file(file)
  file = File.expand_path(file)

  if File.directory?(file)
    # Recursively process every file in the directory
    process_files(
      Dir.glob("#{file}/**/*", File::FNM_DOTMATCH).reject { |f| File.directory? f }
    )
  else
    unless (content = File.read(file))
      panic! "Files cannot be empty."
    end

    puts "File: #{file}"
    desc = get_input("Enter pretty file name (leave blank to use raw file name): ")

    # Return a hash directly for single files
    { "#{"#{desc} || " unless desc.empty?}#{Gistribute.encode file}" => { content: } }
  end
end
process_files(files) click to toggle source
# File lib/cli/upload.rb, line 27
def process_files(files)
  files.each_with_object({}) do |file, hash|
    hash.merge!(process_file file)
  end
end
run() click to toggle source
# File lib/cli.rb, line 83
def run
  case @subcommand
  when "login"
    # Do nothing, #authenticate is run from the constructor
  when "logout"
    FileUtils.rm_rf CONFIG_FILE
    puts "Logged out.".green
  else
    if ARGV.empty?
      OptimistXL.educate
    end

    eval @subcommand
  end
end
upload() click to toggle source
# File lib/cli/upload.rb, line 5
def upload
  files_json = process_files(@files)

  if files_json.empty?
    panic! "No files found, aborting."
  end

  if @subcommand_options.yes || confirm?("\nUpload these files? [Yn] ")
    gist = @client.create_gist({
      description: "[gistribution] #{@subcommand_options.description}".strip,
      public: !@subcommand_options.private,
      files: files_json
    })

    puts if @subcommand_options.yes
    print "Gistribution uploaded to: ".green
    puts gist.html_url
  else
    puts "Aborted.".red
  end
end