lib/license_acceptance/strategy/prompt.rb



require 'tty-prompt'
require 'pastel'
require "license_acceptance/logger"
require "license_acceptance/strategy/base"
require "timeout"

module LicenseAcceptance
  module Strategy

    # Interactive prompt for accepting and persistnce license acceptance, or failing with custom exit code
    class Prompt < Base
      include Logger

      attr_reader :output

      def initialize(config)
        @output = config.output
      end

      WIDTH = 50.freeze
      PASTEL = Pastel.new
      BORDER = "+---------------------------------------------+".freeze
      YES = PASTEL.green.bold("yes")
      CHECK  = PASTEL.green("✔")

      def request(missing_licenses, &persist_callback)
        logger.debug("Requesting a license for #{missing_licenses.map(&:id)}")
        c = missing_licenses.size
        s = c > 1 ? "s": ""

        acceptance_question = "Do you accept the #{c} product license#{s} (#{YES}/no)?"
        output.puts <<~EOM
        #{BORDER}
                    Chef License Acceptance

        Before you can continue, #{c} product license#{s}
        must be accepted. View the license at
        https://www.chef.io/end-user-license-agreement/

        License#{s} that need accepting:
          * #{missing_licenses.map(&:pretty_name).join("\n  * ")}

        #{acceptance_question}

        EOM

        if ask(output, c, s, persist_callback)
          output.puts BORDER
          return true
        end

        output.puts <<~EOM

        If you do not accept this license you will
        not be able to use Chef products.

        #{acceptance_question}

        EOM

        answer = ask(output, c, s, persist_callback)
        if answer != "yes"
          output.puts BORDER
        end
        return answer
      end

      private

      def ask(output, c, s, persist_callback)
        logger.debug("Attempting to request interactive prompt on TTY")
        prompt = TTY::Prompt.new(track_history: false, active_color: :bold, interrupt: :exit, output: output)

        answer = "no"
        begin
          Timeout::timeout(60, PromptTimeout) do
            answer = prompt.ask(">") do |q|
              q.modify :down, :trim
              q.required true
              q.messages[:required?] = "You must enter 'yes' or 'no'"
              q.validate /^\s*(yes|no)\s*$/i
              q.messages[:valid?] = "You must enter 'yes' or 'no'"
            end
          end
        rescue PromptTimeout
          prompt.unsubscribe(prompt.reader)
          output.puts "Prompt timed out. Use non-interactive flags or enter an answer within 60 seconds."
        end

        if answer == "yes"
          output.puts
          output.puts "Persisting #{c} product license#{s}..."
          errs = persist_callback.call
          if errs.empty?
            output.puts "#{CHECK} #{c} product license#{s} persisted.\n\n"
          else
            output.puts <<~EOM
            #{CHECK} #{c} product license#{s} accepted.
            Could not persist acceptance:\n\t* #{errs.map(&:message).join("\n\t* ")}
            EOM
          end
          return true
        end
        return false
      end
    end

    class PromptTimeout < StandardError; end

  end
end