abt-cli 0.0.2
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 +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
|