abt-cli 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/abt +14 -0
- data/lib/abt.rb +5 -0
- data/lib/abt/asana_client.rb +53 -0
- data/lib/abt/cli.rb +103 -0
- data/lib/abt/cli/dialogs.rb +67 -0
- data/lib/abt/git_config.rb +78 -0
- data/lib/abt/harvest_client.rb +58 -0
- data/lib/abt/help.rb +56 -0
- data/lib/abt/help/cli.rb +59 -0
- data/lib/abt/help/markdown.rb +66 -0
- data/lib/abt/http_error.rb +45 -0
- data/lib/abt/providers.rb +5 -0
- data/lib/abt/providers/asana.rb +60 -0
- data/lib/abt/providers/asana/base_command.rb +62 -0
- data/lib/abt/providers/asana/clear.rb +24 -0
- data/lib/abt/providers/asana/clear_global.rb +24 -0
- data/lib/abt/providers/asana/current.rb +69 -0
- data/lib/abt/providers/asana/harvest_link_entry_data.rb +48 -0
- data/lib/abt/providers/asana/init.rb +62 -0
- data/lib/abt/providers/asana/move.rb +54 -0
- data/lib/abt/providers/asana/pick_task.rb +46 -0
- data/lib/abt/providers/asana/projects.rb +30 -0
- data/lib/abt/providers/asana/start.rb +22 -0
- data/lib/abt/providers/asana/tasks.rb +35 -0
- data/lib/abt/providers/harvest.rb +52 -0
- data/lib/abt/providers/harvest/base_command.rb +70 -0
- data/lib/abt/providers/harvest/clear.rb +24 -0
- data/lib/abt/providers/harvest/clear_global.rb +24 -0
- data/lib/abt/providers/harvest/current.rb +79 -0
- data/lib/abt/providers/harvest/init.rb +61 -0
- data/lib/abt/providers/harvest/pick_task.rb +45 -0
- data/lib/abt/providers/harvest/projects.rb +29 -0
- data/lib/abt/providers/harvest/start.rb +58 -0
- data/lib/abt/providers/harvest/stop.rb +51 -0
- data/lib/abt/providers/harvest/tasks.rb +36 -0
- data/lib/abt/version.rb +5 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -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
data/lib/abt.rb
ADDED
@@ -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
|
data/lib/abt/cli.rb
ADDED
@@ -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
|
data/lib/abt/help.rb
ADDED
@@ -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
|