abt-cli 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/bin/abt +14 -0
  3. data/lib/abt.rb +5 -0
  4. data/lib/abt/asana_client.rb +53 -0
  5. data/lib/abt/cli.rb +103 -0
  6. data/lib/abt/cli/dialogs.rb +67 -0
  7. data/lib/abt/git_config.rb +78 -0
  8. data/lib/abt/harvest_client.rb +58 -0
  9. data/lib/abt/help.rb +56 -0
  10. data/lib/abt/help/cli.rb +59 -0
  11. data/lib/abt/help/markdown.rb +66 -0
  12. data/lib/abt/http_error.rb +45 -0
  13. data/lib/abt/providers.rb +5 -0
  14. data/lib/abt/providers/asana.rb +60 -0
  15. data/lib/abt/providers/asana/base_command.rb +62 -0
  16. data/lib/abt/providers/asana/clear.rb +24 -0
  17. data/lib/abt/providers/asana/clear_global.rb +24 -0
  18. data/lib/abt/providers/asana/current.rb +69 -0
  19. data/lib/abt/providers/asana/harvest_link_entry_data.rb +48 -0
  20. data/lib/abt/providers/asana/init.rb +62 -0
  21. data/lib/abt/providers/asana/move.rb +54 -0
  22. data/lib/abt/providers/asana/pick_task.rb +46 -0
  23. data/lib/abt/providers/asana/projects.rb +30 -0
  24. data/lib/abt/providers/asana/start.rb +22 -0
  25. data/lib/abt/providers/asana/tasks.rb +35 -0
  26. data/lib/abt/providers/harvest.rb +52 -0
  27. data/lib/abt/providers/harvest/base_command.rb +70 -0
  28. data/lib/abt/providers/harvest/clear.rb +24 -0
  29. data/lib/abt/providers/harvest/clear_global.rb +24 -0
  30. data/lib/abt/providers/harvest/current.rb +79 -0
  31. data/lib/abt/providers/harvest/init.rb +61 -0
  32. data/lib/abt/providers/harvest/pick_task.rb +45 -0
  33. data/lib/abt/providers/harvest/projects.rb +29 -0
  34. data/lib/abt/providers/harvest/start.rb +58 -0
  35. data/lib/abt/providers/harvest/stop.rb +51 -0
  36. data/lib/abt/providers/harvest/tasks.rb +36 -0
  37. data/lib/abt/version.rb +5 -0
  38. metadata +138 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 929ab52c8ebd307b90ddb7daa14f2f63eeeebda8ed9905490a13dcd2f67770da
4
+ data.tar.gz: e2582299b29e39faa8d4baccaba42954e7140a447d30c3c56a92507ac00f4a09
5
+ SHA512:
6
+ metadata.gz: dec30158debfe3e97c4969d8724d5f56d04725f0cab1aedf79840bb01ef81d9f17c5245cc538c559e47bfa245f36c5aeed8d02dc6ff21fcbae46b30f79e88c5b
7
+ data.tar.gz: c74d6a7ae33ff5843385796cf21d764de61d0981501b5ac7e24691a7c61c85ae6f646abacd1f1b74417d29559ae022881969dd71598acf1190487c6b64c0af02
data/bin/abt ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'dry-inflector'
5
+ require 'faraday'
6
+ require 'oj'
7
+
8
+ require_relative '../lib/abt.rb'
9
+
10
+ begin
11
+ Abt::Cli.new(ARGV).perform
12
+ rescue Interrupt
13
+ abort 'Aborted'
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{File.dirname(File.absolute_path(__FILE__))}/abt/*.rb").sort.each do |file|
4
+ require file
5
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class AsanaClient
5
+ API_ENDPOINT = 'https://app.asana.com/api/1.0'
6
+ VERBS = %i[get post patch].freeze
7
+
8
+ attr_reader :access_token
9
+
10
+ def initialize(access_token:)
11
+ @access_token = access_token
12
+ end
13
+
14
+ VERBS.each do |verb|
15
+ define_method(verb) do |*args|
16
+ request(verb, *args)['data']
17
+ end
18
+ end
19
+
20
+ def get_paged(path, query = {})
21
+ records = []
22
+
23
+ loop do
24
+ result = request(:get, path, query.merge(limit: 100))
25
+ records += result['data']
26
+ break if result['next_page'].nil?
27
+
28
+ path = result['next_page']['path'][1..-1]
29
+ end
30
+
31
+ records
32
+ end
33
+
34
+ def request(*args)
35
+ response = connection.public_send(*args)
36
+
37
+ if response.success?
38
+ Oj.load(response.body)
39
+ else
40
+ error_class = Abt::HttpError.error_class_for_status(response.status)
41
+ encoded_response_body = response.body.force_encoding('utf-8')
42
+ raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
43
+ end
44
+ end
45
+
46
+ def connection
47
+ @connection ||= Faraday.new(API_ENDPOINT) do |connection|
48
+ connection.headers['Authorization'] = "Bearer #{access_token}"
49
+ connection.headers['Content-Type'] = 'application/json'
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{File.expand_path(__dir__)}/cli/*.rb").sort.each do |file|
4
+ require file
5
+ end
6
+
7
+ module Abt
8
+ class Cli
9
+ include Dialogs
10
+
11
+ attr_reader :command, :args
12
+
13
+ def initialize(argv)
14
+ (@command, *@args) = argv
15
+
16
+ @args += args_from_stdin unless STDIN.isatty # Add piped arguments
17
+ end
18
+
19
+ def perform(command: @command, args: @args)
20
+ handle_global_commands!
21
+
22
+ abort('No provider arguments') if args.empty?
23
+
24
+ process_providers(command: command, args: args)
25
+ end
26
+
27
+ def print_provider_command(provider, arg_str, description)
28
+ puts "#{provider}:#{arg_str} # #{description}"
29
+ end
30
+
31
+ private
32
+
33
+ def handle_global_commands! # rubocop:disable Metrics/MethodLength
34
+ case command
35
+ when nil
36
+ warn("No command specified\n\n")
37
+ puts(Abt::Help::Cli.content)
38
+ exit
39
+ when '--help', '-h', 'help', 'commands'
40
+ puts(Abt::Help::Cli.content)
41
+ exit
42
+ when 'help-md'
43
+ puts(Abt::Help::Markdown.content)
44
+ exit
45
+ when '--version', '-v', 'version'
46
+ puts(Abt::VERSION)
47
+ exit
48
+ end
49
+ end
50
+
51
+ def args_from_stdin
52
+ input = STDIN.read
53
+
54
+ return [] if input.nil?
55
+
56
+ input.split("\n").map do |line|
57
+ line.split(' # ').first # Exclude comment part of piped input lines
58
+ end
59
+ end
60
+
61
+ def process_providers(command:, args:)
62
+ used_providers = []
63
+ args.each do |provider_args|
64
+ (provider, arg_str) = provider_args.split(':')
65
+
66
+ if used_providers.include?(provider)
67
+ warn "Dropping command for already used provider: #{provider_args}"
68
+ next
69
+ end
70
+
71
+ used_providers << provider if process_provider_command(provider, command, arg_str)
72
+ end
73
+
74
+ warn 'No matching providers found for command' if used_providers.empty?
75
+ end
76
+
77
+ def process_provider_command(provider, command, arg_str)
78
+ command_class = class_for_provider_and_command(provider, command)
79
+
80
+ return false unless command_class
81
+
82
+ if STDOUT.isatty
83
+ warn "===== #{command} #{provider}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
84
+ end
85
+
86
+ command_class.new(arg_str: arg_str, cli: self).call
87
+ true
88
+ end
89
+
90
+ def class_for_provider_and_command(provider, command)
91
+ inflector = Dry::Inflector.new
92
+ provider_class_name = inflector.camelize(inflector.underscore(provider))
93
+
94
+ return unless Abt::Providers.const_defined? provider_class_name
95
+
96
+ provider_class = Abt::Providers.const_get provider_class_name
97
+ command_class_name = inflector.camelize(inflector.underscore(command))
98
+ return unless provider_class.const_defined? command_class_name
99
+
100
+ provider_class.const_get command_class_name
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class Cli
5
+ module Dialogs
6
+ def prompt(question)
7
+ STDERR.print "#{question}: "
8
+ read_user_input.strip
9
+ end
10
+
11
+ def prompt_choice(text, options, allow_back_option = false)
12
+ if options.one?
13
+ warn "Selected: #{options.first['name']}"
14
+ return options.first
15
+ end
16
+
17
+ warn "#{text}:"
18
+
19
+ print_options(options)
20
+ select_options(options, allow_back_option)
21
+ end
22
+
23
+ private
24
+
25
+ def print_options(options)
26
+ options.each_with_index do |option, index|
27
+ warn "(#{index + 1}) #{option['name']}"
28
+ end
29
+ end
30
+
31
+ def select_options(options, allow_back_option)
32
+ while (number = read_option_number(options.length, allow_back_option))
33
+ if number.nil?
34
+ return nil if allow_back_option
35
+
36
+ abort
37
+ end
38
+
39
+ option = options[number - 1]
40
+
41
+ warn "Selected: (#{number}) #{option['name']}"
42
+ return option
43
+ end
44
+ end
45
+
46
+ def read_option_number(options_length, allow_back_option)
47
+ STDERR.print "(1-#{options_length}#{allow_back_option ? ', q: back' : ''}): "
48
+
49
+ input = read_user_input
50
+
51
+ return nil if allow_back_option && input == 'q'
52
+
53
+ option_number = input.to_i
54
+ if option_number <= 0 || option_number > options_length
55
+ warn 'Invalid selection'
56
+ return nil
57
+ end
58
+
59
+ option_number
60
+ end
61
+
62
+ def read_user_input
63
+ open('/dev/tty', &:gets).strip
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class GitConfig
5
+ class << self
6
+ def local(*args)
7
+ git_config(true, *args)
8
+ end
9
+
10
+ def global(*args)
11
+ git_config(false, *args)
12
+ end
13
+
14
+ def prompt_local(*args)
15
+ prompt_for_config(true, *args)
16
+ end
17
+
18
+ def prompt_global(*args)
19
+ prompt_for_config(false, *args)
20
+ end
21
+
22
+ def unset_local(key)
23
+ unset(true, key)
24
+ end
25
+
26
+ def unset_global(key)
27
+ unset(false, key)
28
+ end
29
+
30
+ private
31
+
32
+ def unset(local, key)
33
+ `git config --#{local ? 'local' : 'global'} --unset #{key.inspect}`
34
+ end
35
+
36
+ def git_config(local, key, value = nil)
37
+ if value
38
+ `git config --#{local ? 'local' : 'global'} --replace-all #{key.inspect} #{value.inspect}`
39
+ value
40
+ else
41
+ git_value = `git config --get #{key.inspect}`.strip
42
+ git_value.empty? ? nil : git_value
43
+ end
44
+ end
45
+
46
+ def prompt(msg)
47
+ STDERR.print "#{msg} > "
48
+ value = read_user_input.strip
49
+ warn
50
+ value
51
+ end
52
+
53
+ def prompt_for_config(local, key, prompt_msg, remedy = '') # rubocop:disable Metrics/MethodLength
54
+ value = git_config(local, key)
55
+
56
+ return value unless value == '' || value.nil?
57
+
58
+ warn <<~TXT
59
+ Missing git config "#{key}":
60
+ To find this value:
61
+ #{remedy}
62
+ TXT
63
+
64
+ new_value = prompt(prompt_msg)
65
+
66
+ if new_value.empty?
67
+ abort 'Empty value, aborting'
68
+ else
69
+ git_config(local, key, new_value)
70
+ end
71
+ end
72
+
73
+ def read_user_input
74
+ open('/dev/tty', &:gets)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class HarvestClient
5
+ API_ENDPOINT = 'https://api.harvestapp.com/v2'
6
+ VERBS = %i[get post patch].freeze
7
+
8
+ attr_reader :access_token, :account_id
9
+
10
+ def initialize(access_token:, account_id:)
11
+ @access_token = access_token
12
+ @account_id = account_id
13
+ end
14
+
15
+ VERBS.each do |verb|
16
+ define_method(verb) do |*args|
17
+ request(verb, *args)
18
+ end
19
+ end
20
+
21
+ def get_paged(path, query = {})
22
+ result_key = path.split('?').first.split('/').last
23
+
24
+ page = 1
25
+ records = []
26
+
27
+ loop do
28
+ result = get(path, query.merge(page: page))
29
+ records += result[result_key]
30
+ break if result['total_pages'] == page
31
+
32
+ page += 1
33
+ end
34
+
35
+ records
36
+ end
37
+
38
+ def request(*args)
39
+ response = connection.public_send(*args)
40
+
41
+ if response.success?
42
+ Oj.load(response.body)
43
+ else
44
+ error_class = Abt::HttpError.error_class_for_status(response.status)
45
+ encoded_response_body = response.body.force_encoding('utf-8')
46
+ raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
47
+ end
48
+ end
49
+
50
+ def connection
51
+ @connection ||= Faraday.new(API_ENDPOINT) do |connection|
52
+ connection.headers['Authorization'] = "Bearer #{access_token}"
53
+ connection.headers['Harvest-Account-Id'] = account_id
54
+ connection.headers['Content-Type'] = 'application/json'
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{File.expand_path(__dir__)}/help/*.rb").sort.each do |file|
4
+ require file
5
+ end
6
+
7
+ module Abt
8
+ module Help
9
+ class << self
10
+ def examples # rubocop:disable Metrics/MethodLength
11
+ {
12
+ 'Multiple providers and arguments can be passed, e.g.:' => {
13
+ 'abt init asana harvest' => nil,
14
+ 'abt pick-task asana harvest' => nil,
15
+ 'abt start asana harvest' => nil,
16
+ 'abt clear asana harvest' => nil
17
+ },
18
+ 'Command output can be piped, e.g.:' => {
19
+ 'abt tasks asana | grep -i <name of task>' => nil,
20
+ 'abt tasks asana | grep -i <name of task> | abt start' => nil
21
+ }
22
+ }
23
+ end
24
+
25
+ def providers
26
+ provider_definitions
27
+ end
28
+
29
+ private
30
+
31
+ def commandize(string)
32
+ string = string.to_s
33
+ string[0] = string[0].downcase
34
+ string.gsub(/([A-Z])/, '-\1').downcase
35
+ end
36
+
37
+ def provider_definitions
38
+ Abt::Providers.constants.sort.each_with_object({}) do |provider_name, definition|
39
+ provider_class = Abt::Providers.const_get(provider_name)
40
+
41
+ definition[commandize(provider_name)] = command_definitions(provider_class)
42
+ end
43
+ end
44
+
45
+ def command_definitions(provider_class)
46
+ provider_class.constants.sort.each_with_object({}) do |command_name, definition|
47
+ command_class = provider_class.const_get(command_name)
48
+
49
+ if command_class.respond_to?(:command) && command_class.respond_to?(:description)
50
+ definition[command_class.command] = command_class.description
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end