abt-cli 0.0.2 → 0.0.7
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 +4 -4
- data/bin/abt +4 -1
- data/lib/abt.rb +11 -0
- data/lib/abt/cli.rb +37 -32
- data/lib/abt/cli/dialogs.rb +18 -2
- data/lib/abt/cli/io.rb +23 -0
- data/lib/abt/docs.rb +57 -0
- data/lib/abt/{help → docs}/cli.rb +4 -4
- data/lib/abt/{help → docs}/markdown.rb +4 -4
- data/lib/abt/git_config.rb +55 -49
- data/lib/abt/helpers.rb +16 -0
- data/lib/abt/providers/asana.rb +9 -50
- data/lib/abt/providers/asana/api.rb +57 -0
- data/lib/abt/providers/asana/base_command.rb +14 -16
- data/lib/abt/providers/asana/commands/clear.rb +24 -0
- data/lib/abt/providers/asana/commands/clear_global.rb +24 -0
- data/lib/abt/providers/asana/commands/current.rb +77 -0
- data/lib/abt/providers/asana/commands/finalize.rb +71 -0
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +50 -0
- data/lib/abt/providers/asana/commands/init.rb +70 -0
- data/lib/abt/providers/asana/commands/pick.rb +55 -0
- data/lib/abt/providers/asana/commands/projects.rb +39 -0
- data/lib/abt/providers/asana/commands/share.rb +29 -0
- data/lib/abt/providers/asana/commands/start.rb +105 -0
- data/lib/abt/providers/asana/commands/tasks.rb +40 -0
- data/lib/abt/providers/asana/configuration.rb +125 -0
- data/lib/abt/providers/harvest.rb +9 -42
- data/lib/abt/providers/harvest/api.rb +62 -0
- data/lib/abt/providers/harvest/base_command.rb +12 -16
- data/lib/abt/providers/harvest/commands/clear.rb +24 -0
- data/lib/abt/providers/harvest/commands/clear_global.rb +24 -0
- data/lib/abt/providers/harvest/commands/current.rb +83 -0
- data/lib/abt/providers/harvest/commands/init.rb +83 -0
- data/lib/abt/providers/harvest/commands/pick.rb +51 -0
- data/lib/abt/providers/harvest/commands/projects.rb +40 -0
- data/lib/abt/providers/harvest/commands/share.rb +29 -0
- data/lib/abt/providers/harvest/commands/start.rb +101 -0
- data/lib/abt/providers/harvest/commands/stop.rb +58 -0
- data/lib/abt/providers/harvest/commands/tasks.rb +45 -0
- data/lib/abt/providers/harvest/configuration.rb +91 -0
- data/lib/abt/version.rb +1 -1
- metadata +32 -26
- data/lib/abt/asana_client.rb +0 -53
- data/lib/abt/harvest_client.rb +0 -58
- data/lib/abt/help.rb +0 -56
- data/lib/abt/providers/asana/clear.rb +0 -24
- data/lib/abt/providers/asana/clear_global.rb +0 -24
- data/lib/abt/providers/asana/current.rb +0 -69
- data/lib/abt/providers/asana/harvest_link_entry_data.rb +0 -48
- data/lib/abt/providers/asana/init.rb +0 -62
- data/lib/abt/providers/asana/move.rb +0 -54
- data/lib/abt/providers/asana/pick_task.rb +0 -46
- data/lib/abt/providers/asana/projects.rb +0 -30
- data/lib/abt/providers/asana/start.rb +0 -22
- data/lib/abt/providers/asana/tasks.rb +0 -35
- data/lib/abt/providers/harvest/clear.rb +0 -24
- data/lib/abt/providers/harvest/clear_global.rb +0 -24
- data/lib/abt/providers/harvest/current.rb +0 -79
- data/lib/abt/providers/harvest/init.rb +0 -61
- data/lib/abt/providers/harvest/pick_task.rb +0 -45
- data/lib/abt/providers/harvest/projects.rb +0 -29
- data/lib/abt/providers/harvest/start.rb +0 -58
- data/lib/abt/providers/harvest/stop.rb +0 -51
- data/lib/abt/providers/harvest/tasks.rb +0 -36
@@ -1,51 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
Dir.glob("#{File.expand_path(__dir__)}/harvest/*.rb").sort.each
|
4
|
-
|
5
|
-
end
|
3
|
+
Dir.glob("#{File.expand_path(__dir__)}/harvest/*.rb").sort.each { |file| require file }
|
4
|
+
Dir.glob("#{File.expand_path(__dir__)}/harvest/commands/*.rb").sort.each { |file| require file }
|
6
5
|
|
7
6
|
module Abt
|
8
7
|
module Providers
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
'abt.harvest.userId',
|
14
|
-
'Please enter your harvest User ID',
|
15
|
-
'In harvest open "My profile". The ID is the number part of the URL you are taken to'
|
16
|
-
)
|
17
|
-
end
|
18
|
-
|
19
|
-
def access_token
|
20
|
-
Abt::GitConfig.prompt_global(
|
21
|
-
'abt.harvest.accessToken',
|
22
|
-
'Please enter your personal harvest access token',
|
23
|
-
'Create your personal access token here: https://id.getharvest.com/developers'
|
24
|
-
)
|
25
|
-
end
|
26
|
-
|
27
|
-
def account_id
|
28
|
-
Abt::GitConfig.prompt_global(
|
29
|
-
'abt.harvest.accountId',
|
30
|
-
'Please enter the harvest account id',
|
31
|
-
'This information is shown next to your generated access token'
|
32
|
-
)
|
33
|
-
end
|
34
|
-
|
35
|
-
def clear
|
36
|
-
Abt::GitConfig.unset_local('abt.harvest.projectId')
|
37
|
-
Abt::GitConfig.unset_local('abt.harvest.taskId')
|
38
|
-
end
|
39
|
-
|
40
|
-
def clear_global
|
41
|
-
Abt::GitConfig.unset_global('abt.harvest.userId')
|
42
|
-
Abt::GitConfig.unset_global('abt.harvest.accountId')
|
43
|
-
Abt::GitConfig.unset_global('abt.harvest.accessToken')
|
44
|
-
end
|
8
|
+
module Harvest
|
9
|
+
def self.command_names
|
10
|
+
Commands.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
|
11
|
+
end
|
45
12
|
|
46
|
-
|
47
|
-
|
48
|
-
|
13
|
+
def self.command_class(name)
|
14
|
+
const_name = Helpers.command_to_const(name)
|
15
|
+
Commands.const_get(const_name) if Commands.const_defined?(const_name)
|
49
16
|
end
|
50
17
|
end
|
51
18
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Harvest
|
6
|
+
class Api
|
7
|
+
API_ENDPOINT = 'https://api.harvestapp.com/v2'
|
8
|
+
VERBS = %i[get post patch].freeze
|
9
|
+
|
10
|
+
attr_reader :access_token, :account_id
|
11
|
+
|
12
|
+
def initialize(access_token:, account_id:)
|
13
|
+
@access_token = access_token
|
14
|
+
@account_id = account_id
|
15
|
+
end
|
16
|
+
|
17
|
+
VERBS.each do |verb|
|
18
|
+
define_method(verb) do |*args|
|
19
|
+
request(verb, *args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_paged(path, query = {})
|
24
|
+
result_key = path.split('?').first.split('/').last
|
25
|
+
|
26
|
+
page = 1
|
27
|
+
records = []
|
28
|
+
|
29
|
+
loop do
|
30
|
+
result = get(path, query.merge(page: page))
|
31
|
+
records += result[result_key]
|
32
|
+
break if result['total_pages'] == page
|
33
|
+
|
34
|
+
page += 1
|
35
|
+
end
|
36
|
+
|
37
|
+
records
|
38
|
+
end
|
39
|
+
|
40
|
+
def request(*args)
|
41
|
+
response = connection.public_send(*args)
|
42
|
+
|
43
|
+
if response.success?
|
44
|
+
Oj.load(response.body)
|
45
|
+
else
|
46
|
+
error_class = Abt::HttpError.error_class_for_status(response.status)
|
47
|
+
encoded_response_body = response.body.force_encoding('utf-8')
|
48
|
+
raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def connection
|
53
|
+
@connection ||= Faraday.new(API_ENDPOINT) do |connection|
|
54
|
+
connection.headers['Authorization'] = "Bearer #{access_token}"
|
55
|
+
connection.headers['Harvest-Account-Id'] = account_id
|
56
|
+
connection.headers['Content-Type'] = 'application/json'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -2,12 +2,13 @@
|
|
2
2
|
|
3
3
|
module Abt
|
4
4
|
module Providers
|
5
|
-
|
5
|
+
module Harvest
|
6
6
|
class BaseCommand
|
7
|
-
attr_reader :arg_str, :project_id, :task_id, :cli
|
7
|
+
attr_reader :arg_str, :project_id, :task_id, :cli, :config
|
8
8
|
|
9
9
|
def initialize(arg_str:, cli:)
|
10
10
|
@arg_str = arg_str
|
11
|
+
@config = Configuration.new(cli: cli)
|
11
12
|
|
12
13
|
if arg_str.nil?
|
13
14
|
use_current_args
|
@@ -19,6 +20,10 @@ module Abt
|
|
19
20
|
|
20
21
|
private
|
21
22
|
|
23
|
+
def same_args_as_config?
|
24
|
+
project_id == config.project_id && task_id == config.task_id
|
25
|
+
end
|
26
|
+
|
22
27
|
def print_project(project)
|
23
28
|
cli.print_provider_command(
|
24
29
|
'harvest',
|
@@ -36,10 +41,8 @@ module Abt
|
|
36
41
|
end
|
37
42
|
|
38
43
|
def use_current_args
|
39
|
-
@project_id =
|
40
|
-
@
|
41
|
-
@task_id = Abt::GitConfig.local('abt.harvest.taskId').to_s
|
42
|
-
@task_id = nil if task_id.empty?
|
44
|
+
@project_id = config.project_id
|
45
|
+
@task_id = config.task_id
|
43
46
|
end
|
44
47
|
|
45
48
|
def use_arg_str(arg_str)
|
@@ -53,16 +56,9 @@ module Abt
|
|
53
56
|
@task_id = nil if @task_id.empty?
|
54
57
|
end
|
55
58
|
|
56
|
-
def
|
57
|
-
Abt::
|
58
|
-
|
59
|
-
|
60
|
-
def remember_task_id(task_id)
|
61
|
-
if task_id.nil?
|
62
|
-
Abt::GitConfig.unset_local('abt.harvest.taskId')
|
63
|
-
else
|
64
|
-
Abt::GitConfig.local('abt.harvest.taskId', task_id)
|
65
|
-
end
|
59
|
+
def api
|
60
|
+
@api ||= Abt::Providers::Harvest::Api.new(access_token: config.access_token,
|
61
|
+
account_id: config.account_id)
|
66
62
|
end
|
67
63
|
end
|
68
64
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Harvest
|
6
|
+
module Commands
|
7
|
+
class Clear < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'clear harvest'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'Clear project/task for current git repository'
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
cli.warn 'Clearing Harvest project configuration'
|
18
|
+
config.clear_local
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Harvest
|
6
|
+
module Commands
|
7
|
+
class ClearGlobal < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'clear-global harvest'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'Clear all global configuration'
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
cli.warn 'Clearing Harvest project configuration'
|
18
|
+
config.clear_global
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Harvest
|
6
|
+
module Commands
|
7
|
+
class Current < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'current harvest[:<project-id>[/<task-id>]]'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'Get or set project and or task for current git repository'
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
if same_args_as_config? || !config.local_available?
|
18
|
+
show_current_configuration
|
19
|
+
else
|
20
|
+
cli.warn 'Updating configuration'
|
21
|
+
update_configuration
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def show_current_configuration
|
28
|
+
if project_id.nil?
|
29
|
+
cli.warn 'No project selected'
|
30
|
+
elsif task_id.nil?
|
31
|
+
print_project(project)
|
32
|
+
else
|
33
|
+
print_task(project, task)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_configuration
|
38
|
+
ensure_project_is_valid!
|
39
|
+
config.project_id = project_id
|
40
|
+
|
41
|
+
if task_id.nil?
|
42
|
+
print_project(project)
|
43
|
+
config.task_id = nil
|
44
|
+
else
|
45
|
+
ensure_task_is_valid!
|
46
|
+
config.task_id = task_id
|
47
|
+
|
48
|
+
print_task(project, task)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def ensure_project_is_valid!
|
53
|
+
cli.abort "Invalid project: #{project_id}" if project.nil?
|
54
|
+
end
|
55
|
+
|
56
|
+
def ensure_task_is_valid!
|
57
|
+
cli.abort "Invalid task: #{task_id}" if task.nil?
|
58
|
+
end
|
59
|
+
|
60
|
+
def project
|
61
|
+
@project ||= project_assignment['project'].merge('client' => project_assignment['client'])
|
62
|
+
end
|
63
|
+
|
64
|
+
def task
|
65
|
+
@task ||= project_assignment['task_assignments'].map { |ta| ta['task'] }.find do |task|
|
66
|
+
task['id'].to_s == task_id
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def project_assignment
|
71
|
+
@project_assignment ||= begin
|
72
|
+
project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def project_assignments
|
77
|
+
@project_assignments ||= api.get_paged('users/me/project_assignments')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Harvest
|
6
|
+
module Commands
|
7
|
+
class Init < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'init harvest'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'Pick Harvest project for current git repository'
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
cli.abort 'Must be run inside a git repository' unless config.local_available?
|
18
|
+
|
19
|
+
projects # Load projects up front to make it obvious that searches are instant
|
20
|
+
project = find_search_result
|
21
|
+
|
22
|
+
config.project_id = project['id']
|
23
|
+
config.task_id = nil
|
24
|
+
|
25
|
+
print_project(project)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def find_search_result
|
31
|
+
cli.warn 'Select a project'
|
32
|
+
|
33
|
+
loop do
|
34
|
+
matches = matches_for_string cli.prompt('Enter search')
|
35
|
+
if matches.empty?
|
36
|
+
warn 'No matches'
|
37
|
+
next
|
38
|
+
end
|
39
|
+
|
40
|
+
cli.warn 'Showing the 10 first matches' if matches.size > 10
|
41
|
+
choice = cli.prompt_choice 'Select a project', matches[0...10], true
|
42
|
+
break choice['project'] unless choice.nil?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def matches_for_string(string)
|
47
|
+
search_string = sanitize_string(string)
|
48
|
+
|
49
|
+
searchable_projects.select do |project|
|
50
|
+
sanitize_string(project['name']).include?(search_string)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def sanitize_string(string)
|
55
|
+
string.downcase.gsub(/[^\w]/, '')
|
56
|
+
end
|
57
|
+
|
58
|
+
def searchable_projects
|
59
|
+
@searchable_projects ||= projects.map do |project|
|
60
|
+
{
|
61
|
+
'name' => "#{project['client']['name']} > #{project['name']}",
|
62
|
+
'project' => project
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def projects
|
68
|
+
@projects ||= begin
|
69
|
+
cli.warn 'Fetching projects...'
|
70
|
+
project_assignments.map do |project_assignment|
|
71
|
+
project_assignment['project'].merge('client' => project_assignment['client'])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def project_assignments
|
77
|
+
@project_assignments ||= api.get_paged('users/me/project_assignments')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Harvest
|
6
|
+
module Commands
|
7
|
+
class Pick < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'pick harvest[:<project-id>]'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'Pick task for current git repository'
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
cli.abort 'Must be run inside a git repository' unless config.local_available?
|
18
|
+
|
19
|
+
cli.warn project['name']
|
20
|
+
task = cli.prompt_choice 'Select a task', tasks
|
21
|
+
|
22
|
+
config.project_id = project_id # We might have gotten the project ID as an argument
|
23
|
+
config.task_id = task['id']
|
24
|
+
|
25
|
+
print_task(project, task)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def project
|
31
|
+
project_assignment['project']
|
32
|
+
end
|
33
|
+
|
34
|
+
def tasks
|
35
|
+
@tasks ||= project_assignment['task_assignments'].map { |ta| ta['task'] }
|
36
|
+
end
|
37
|
+
|
38
|
+
def project_assignment
|
39
|
+
@project_assignment ||= begin
|
40
|
+
project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def project_assignments
|
45
|
+
@project_assignments ||= api.get_paged('users/me/project_assignments')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|