abt-cli 0.0.3 → 0.0.4
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 +1 -0
- data/lib/abt/cli.rb +16 -7
- data/lib/abt/cli/dialogs.rb +18 -2
- data/lib/abt/cli/io.rb +8 -6
- data/lib/abt/docs.rb +12 -5
- data/lib/abt/docs/cli.rb +1 -1
- data/lib/abt/docs/markdown.rb +1 -1
- data/lib/abt/git_config.rb +55 -49
- data/lib/abt/providers/asana/api.rb +1 -1
- data/lib/abt/providers/asana/base_command.rb +9 -4
- data/lib/abt/providers/asana/commands/current.rb +10 -4
- data/lib/abt/providers/asana/commands/finalize.rb +71 -0
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +2 -2
- data/lib/abt/providers/asana/commands/init.rb +8 -3
- data/lib/abt/providers/asana/commands/{pick_task.rb → pick.rb} +13 -6
- data/lib/abt/providers/asana/commands/projects.rb +9 -2
- data/lib/abt/providers/asana/commands/share.rb +29 -0
- data/lib/abt/providers/asana/commands/start.rb +51 -6
- data/lib/abt/providers/asana/commands/tasks.rb +4 -1
- data/lib/abt/providers/asana/configuration.rb +54 -34
- data/lib/abt/providers/harvest.rb +9 -51
- 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 +26 -0
- data/lib/abt/providers/harvest/commands/clear_global.rb +24 -0
- data/lib/abt/providers/harvest/commands/current.rb +81 -0
- data/lib/abt/providers/harvest/commands/init.rb +66 -0
- data/lib/abt/providers/harvest/commands/pick.rb +49 -0
- data/lib/abt/providers/harvest/commands/projects.rb +34 -0
- data/lib/abt/providers/harvest/commands/share.rb +29 -0
- data/lib/abt/providers/harvest/commands/start.rb +81 -0
- data/lib/abt/providers/harvest/commands/stop.rb +58 -0
- data/lib/abt/providers/harvest/commands/tasks.rb +38 -0
- data/lib/abt/providers/harvest/configuration.rb +90 -0
- data/lib/abt/version.rb +1 -1
- metadata +17 -14
- data/lib/abt/harvest_client.rb +0 -58
- data/lib/abt/providers/asana/commands/move.rb +0 -56
- 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
@@ -13,7 +13,7 @@ module Abt
|
|
13
13
|
'Print Harvest time entry data for Asana task as json. Used by harvest start script.'
|
14
14
|
end
|
15
15
|
|
16
|
-
def call
|
16
|
+
def call # rubocop:disable Metrics/MethodLength
|
17
17
|
ensure_current_is_valid!
|
18
18
|
|
19
19
|
body = {
|
@@ -41,7 +41,7 @@ module Abt
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def task
|
44
|
-
@task ||= api.get("tasks/#{task_gid}")
|
44
|
+
@task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url,memberships.project')
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
@@ -14,7 +14,7 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def call
|
17
|
-
cli.
|
17
|
+
cli.abort 'Must be run inside a git repository' unless config.local_available?
|
18
18
|
|
19
19
|
projects # Load projects up front to make it obvious that searches are instant
|
20
20
|
project = find_search_result
|
@@ -53,8 +53,13 @@ module Abt
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def projects
|
56
|
-
@projects ||=
|
57
|
-
|
56
|
+
@projects ||= begin
|
57
|
+
cli.warn 'Fetching projects...'
|
58
|
+
api.get_paged('projects',
|
59
|
+
workspace: config.workspace_gid,
|
60
|
+
archived: false,
|
61
|
+
opt_fields: 'name,permalink_url')
|
62
|
+
end
|
58
63
|
end
|
59
64
|
end
|
60
65
|
end
|
@@ -4,9 +4,9 @@ module Abt
|
|
4
4
|
module Providers
|
5
5
|
module Asana
|
6
6
|
module Commands
|
7
|
-
class
|
7
|
+
class Pick < BaseCommand
|
8
8
|
def self.command
|
9
|
-
'pick
|
9
|
+
'pick asana[:<project-gid>]'
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
@@ -14,7 +14,10 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def call
|
17
|
+
cli.abort 'Must be run inside a git repository' unless config.local_available?
|
18
|
+
|
17
19
|
cli.warn project['name']
|
20
|
+
|
18
21
|
task = cli.prompt_choice 'Select a task', tasks
|
19
22
|
|
20
23
|
config.project_gid = project_gid # We might have gotten the project ID as an argument
|
@@ -32,14 +35,18 @@ module Abt
|
|
32
35
|
def tasks
|
33
36
|
@tasks ||= begin
|
34
37
|
section = cli.prompt_choice 'Which section?', sections
|
35
|
-
|
38
|
+
cli.warn 'Fetching tasks...'
|
39
|
+
api.get_paged('tasks', section: section['gid'], opt_fields: 'name,permalink_url')
|
36
40
|
end
|
37
41
|
end
|
38
42
|
|
39
43
|
def sections
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
@sections ||= begin
|
45
|
+
cli.warn 'Fetching sections...'
|
46
|
+
api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
|
47
|
+
rescue Abt::HttpError::HttpError
|
48
|
+
[]
|
49
|
+
end
|
43
50
|
end
|
44
51
|
end
|
45
52
|
end
|
@@ -22,8 +22,15 @@ module Abt
|
|
22
22
|
private
|
23
23
|
|
24
24
|
def projects
|
25
|
-
@projects ||=
|
26
|
-
|
25
|
+
@projects ||= begin
|
26
|
+
cli.warn 'Fetching projects...'
|
27
|
+
api.get_paged(
|
28
|
+
'projects',
|
29
|
+
workspace: config.workspace_gid,
|
30
|
+
archived: false,
|
31
|
+
opt_fields: 'name'
|
32
|
+
)
|
33
|
+
end
|
27
34
|
end
|
28
35
|
end
|
29
36
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Commands
|
7
|
+
class Share < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'share asana[:<project-gid>[/<task-gid>]]'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'Print project/task config string'
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
if project_gid.nil?
|
18
|
+
cli.warn 'No project selected'
|
19
|
+
elsif task_gid.nil?
|
20
|
+
cli.print_provider_command('asana', project_gid)
|
21
|
+
else
|
22
|
+
cli.print_provider_command('asana', "#{project_gid}/#{task_gid}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -14,18 +14,53 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def call
|
17
|
-
|
17
|
+
abort 'No current/provided task' if task_gid.nil?
|
18
|
+
|
19
|
+
maybe_override_current_task
|
20
|
+
|
21
|
+
update_assignee_if_needed
|
22
|
+
move_if_needed
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def maybe_override_current_task
|
28
|
+
return if arg_str.nil?
|
29
|
+
return if same_args_as_config?
|
30
|
+
return unless config.local_available?
|
31
|
+
|
32
|
+
should_override = cli.prompt_boolean 'Set selected task as current?'
|
33
|
+
Current.new(arg_str: arg_str, cli: cli).call if should_override
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_assignee_if_needed
|
37
|
+
current_assignee = task.dig('assignee')
|
38
|
+
|
39
|
+
if current_assignee.nil?
|
40
|
+
cli.warn "Assigning task to user: #{current_user['name']}"
|
41
|
+
update_assignee
|
42
|
+
elsif current_assignee['gid'] == current_user['gid']
|
43
|
+
cli.warn 'You are already assigned to this task'
|
44
|
+
elsif cli.prompt_boolean "Task is assigned to: #{current_assignee['name']}, take over?"
|
45
|
+
cli.warn "Reassigning task to user: #{current_user['name']}"
|
46
|
+
update_assignee
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def move_if_needed
|
51
|
+
unless project_gid == config.project_gid
|
52
|
+
cli.warn 'Task was not moved, this is not implemented for tasks outside current project'
|
53
|
+
return
|
54
|
+
end
|
18
55
|
|
19
56
|
if task_already_in_wip_section?
|
20
|
-
cli.warn "Task already in #{current_task_section['name']}"
|
57
|
+
cli.warn "Task already in section: #{current_task_section['name']}"
|
21
58
|
else
|
22
|
-
cli.warn "Moving task to #{wip_section['name']}"
|
59
|
+
cli.warn "Moving task to section: #{wip_section['name']}"
|
23
60
|
move_task
|
24
61
|
end
|
25
62
|
end
|
26
63
|
|
27
|
-
private
|
28
|
-
|
29
64
|
def task_already_in_wip_section?
|
30
65
|
!task_section_membership.nil?
|
31
66
|
end
|
@@ -50,8 +85,18 @@ module Abt
|
|
50
85
|
api.post("sections/#{config.wip_section_gid}/addTask", body_json)
|
51
86
|
end
|
52
87
|
|
88
|
+
def update_assignee
|
89
|
+
body = { data: { assignee: current_user['gid'] } }
|
90
|
+
body_json = Oj.dump(body, mode: :json)
|
91
|
+
api.put("tasks/#{task_gid}", body_json)
|
92
|
+
end
|
93
|
+
|
94
|
+
def current_user
|
95
|
+
@current_user ||= api.get('users/me', opt_fields: 'name')
|
96
|
+
end
|
97
|
+
|
53
98
|
def task
|
54
|
-
@task ||= api.get("tasks/#{task_gid}")
|
99
|
+
@task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,assignee.name')
|
55
100
|
end
|
56
101
|
end
|
57
102
|
end
|
@@ -8,19 +8,24 @@ module Abt
|
|
8
8
|
|
9
9
|
def initialize(cli:)
|
10
10
|
@cli = cli
|
11
|
+
@git = GitConfig.new(namespace: 'abt.asana')
|
12
|
+
end
|
13
|
+
|
14
|
+
def local_available?
|
15
|
+
GitConfig.local_available?
|
11
16
|
end
|
12
17
|
|
13
18
|
def project_gid
|
14
|
-
|
19
|
+
local_available? ? git['projectGid'] : nil
|
15
20
|
end
|
16
21
|
|
17
22
|
def task_gid
|
18
|
-
|
23
|
+
local_available? ? git['taskGid'] : nil
|
19
24
|
end
|
20
25
|
|
21
26
|
def workspace_gid
|
22
27
|
@workspace_gid ||= begin
|
23
|
-
current =
|
28
|
+
current = git.global['workspaceGid']
|
24
29
|
if current.nil?
|
25
30
|
prompt_workspace['gid']
|
26
31
|
else
|
@@ -30,69 +35,84 @@ module Abt
|
|
30
35
|
end
|
31
36
|
|
32
37
|
def wip_section_gid
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
return nil unless local_available?
|
39
|
+
|
40
|
+
@wip_section_gid ||= git['wipSectionGid'] || prompt_wip_section['gid']
|
41
|
+
end
|
42
|
+
|
43
|
+
def finalized_section_gid
|
44
|
+
return nil unless local_available?
|
45
|
+
|
46
|
+
@finalized_section_gid ||= git['finalizedSectionGid'] || prompt_finalized_section['gid']
|
41
47
|
end
|
42
48
|
|
43
49
|
def project_gid=(value)
|
44
50
|
return if project_gid == value
|
45
51
|
|
46
52
|
clear_local
|
47
|
-
|
53
|
+
git['projectGid'] = value unless value.nil?
|
48
54
|
end
|
49
55
|
|
50
56
|
def task_gid=(value)
|
51
|
-
|
52
|
-
Abt::GitConfig.unset_local('abt.asana.taskGid')
|
53
|
-
elsif task_gid != value
|
54
|
-
Abt::GitConfig.local('abt.asana.taskGid', value)
|
55
|
-
end
|
57
|
+
git['taskGid'] = value
|
56
58
|
end
|
57
59
|
|
58
60
|
def clear_local
|
59
|
-
|
60
|
-
|
61
|
-
|
61
|
+
cli.abort 'No local configuration was found' unless local_available?
|
62
|
+
|
63
|
+
git['projectGid'] = nil
|
64
|
+
git['taskGid'] = nil
|
65
|
+
git['wipSectionGid'] = nil
|
66
|
+
git['finalizedSectionGid'] = nil
|
62
67
|
end
|
63
68
|
|
64
69
|
def clear_global
|
65
|
-
|
66
|
-
|
70
|
+
git.global['workspaceGid'] = nil
|
71
|
+
git.global['accessToken'] = nil
|
67
72
|
end
|
68
73
|
|
69
74
|
def access_token
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
'
|
74
|
-
|
75
|
+
return git.global['accessToken'] unless git.global['accessToken'].nil?
|
76
|
+
|
77
|
+
git.global['accessToken'] = cli.prompt([
|
78
|
+
'Please provide your personal access token for Asana.',
|
79
|
+
'If you don\'t have one, create one here: https://app.asana.com/0/developer-console',
|
80
|
+
'',
|
81
|
+
'Enter access token'
|
82
|
+
].join("\n"))
|
75
83
|
end
|
76
84
|
|
77
85
|
private
|
78
86
|
|
79
|
-
|
80
|
-
sections = api.get_paged("projects/#{project_gid}/sections")
|
87
|
+
attr_reader :git
|
81
88
|
|
82
|
-
|
83
|
-
|
89
|
+
def prompt_finalized_section
|
90
|
+
section = prompt_section('Select section for finalized tasks (E.g. "Merged")')
|
91
|
+
git['finalizedSectionGid'] = section['gid']
|
84
92
|
section
|
85
93
|
end
|
86
94
|
|
95
|
+
def prompt_wip_section
|
96
|
+
section = prompt_section('Select WIP (Work In Progress) section')
|
97
|
+
git['wipSectionGid'] = section['gid']
|
98
|
+
section
|
99
|
+
end
|
100
|
+
|
101
|
+
def prompt_section(message)
|
102
|
+
cli.warn 'Fetching sections...'
|
103
|
+
sections = api.get_paged("projects/#{project_gid}/sections")
|
104
|
+
cli.prompt_choice(message, sections)
|
105
|
+
end
|
106
|
+
|
87
107
|
def prompt_workspace
|
108
|
+
cli.warn 'Fetching workspaces...'
|
88
109
|
workspaces = api.get_paged('workspaces')
|
89
110
|
if workspaces.empty?
|
90
111
|
cli.abort 'Your asana access token does not have access to any workspaces'
|
91
112
|
end
|
92
113
|
|
93
|
-
|
94
|
-
|
95
|
-
Abt::GitConfig.global('abt.asana.workspaceGid', workspace['gid'])
|
114
|
+
workspace = cli.prompt_choice('Select Asana workspace', workspaces)
|
115
|
+
git.global['workspaceGid'] = workspace['gid']
|
96
116
|
workspace
|
97
117
|
end
|
98
118
|
|
@@ -1,60 +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
|
-
end
|
14
|
-
|
15
|
-
def command_class(name)
|
16
|
-
const_name = Helpers.command_to_const(name)
|
17
|
-
const_get(const_name) if const_defined?(const_name)
|
18
|
-
end
|
19
|
-
|
20
|
-
def user_id
|
21
|
-
Abt::GitConfig.prompt_global(
|
22
|
-
'abt.harvest.userId',
|
23
|
-
'Please enter your harvest User ID',
|
24
|
-
'In harvest open "My profile". The ID is the number part of the URL you are taken to'
|
25
|
-
)
|
26
|
-
end
|
27
|
-
|
28
|
-
def access_token
|
29
|
-
Abt::GitConfig.prompt_global(
|
30
|
-
'abt.harvest.accessToken',
|
31
|
-
'Please enter your personal harvest access token',
|
32
|
-
'Create your personal access token here: https://id.getharvest.com/developers'
|
33
|
-
)
|
34
|
-
end
|
35
|
-
|
36
|
-
def account_id
|
37
|
-
Abt::GitConfig.prompt_global(
|
38
|
-
'abt.harvest.accountId',
|
39
|
-
'Please enter the harvest account id',
|
40
|
-
'This information is shown next to your generated access token'
|
41
|
-
)
|
42
|
-
end
|
43
|
-
|
44
|
-
def clear
|
45
|
-
Abt::GitConfig.unset_local('abt.harvest.projectId')
|
46
|
-
Abt::GitConfig.unset_local('abt.harvest.taskId')
|
47
|
-
end
|
48
|
-
|
49
|
-
def clear_global
|
50
|
-
Abt::GitConfig.unset_global('abt.harvest.userId')
|
51
|
-
Abt::GitConfig.unset_global('abt.harvest.accountId')
|
52
|
-
Abt::GitConfig.unset_global('abt.harvest.accessToken')
|
53
|
-
end
|
8
|
+
module Harvest
|
9
|
+
def self.command_names
|
10
|
+
Commands.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
|
11
|
+
end
|
54
12
|
|
55
|
-
|
56
|
-
|
57
|
-
|
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)
|
58
16
|
end
|
59
17
|
end
|
60
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
|