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
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Commands
|
7
|
+
class Init < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'init asana'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'Pick Asana 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_gid = project['gid']
|
23
|
+
|
24
|
+
print_project(project)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def find_search_result
|
30
|
+
cli.warn 'Select a project'
|
31
|
+
|
32
|
+
loop do
|
33
|
+
matches = matches_for_string cli.prompt('Enter search')
|
34
|
+
if matches.empty?
|
35
|
+
cli.warn 'No matches'
|
36
|
+
next
|
37
|
+
end
|
38
|
+
|
39
|
+
cli.warn 'Showing the 10 first matches' if matches.size > 10
|
40
|
+
choice = cli.prompt_choice 'Select a project', matches[0...10], true
|
41
|
+
break choice unless choice.nil?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def matches_for_string(string)
|
46
|
+
search_string = sanitize_string(string)
|
47
|
+
|
48
|
+
projects.select do |project|
|
49
|
+
sanitize_string(project['name']).include?(search_string)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def sanitize_string(string)
|
54
|
+
string.downcase.gsub(/[^\w]/, '')
|
55
|
+
end
|
56
|
+
|
57
|
+
def projects
|
58
|
+
@projects ||= begin
|
59
|
+
cli.warn 'Fetching projects...'
|
60
|
+
api.get_paged('projects',
|
61
|
+
workspace: config.workspace_gid,
|
62
|
+
archived: false,
|
63
|
+
opt_fields: 'name,permalink_url')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Commands
|
7
|
+
class Pick < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'pick asana[:<project-gid>]'
|
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
|
+
|
21
|
+
task = cli.prompt_choice 'Select a task', tasks
|
22
|
+
|
23
|
+
config.project_gid = project_gid # We might have gotten the project ID as an argument
|
24
|
+
config.task_gid = task['gid']
|
25
|
+
|
26
|
+
print_task(project, task)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def project
|
32
|
+
@project ||= api.get("projects/#{project_gid}")
|
33
|
+
end
|
34
|
+
|
35
|
+
def tasks
|
36
|
+
@tasks ||= begin
|
37
|
+
section = cli.prompt_choice 'Which section?', sections
|
38
|
+
cli.warn 'Fetching tasks...'
|
39
|
+
api.get_paged('tasks', section: section['gid'], opt_fields: 'name,permalink_url')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def sections
|
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
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Commands
|
7
|
+
class Projects < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'projects asana'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'List all available projects - useful for piping into grep etc.'
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
projects.map do |project|
|
18
|
+
print_project(project)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def projects
|
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
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
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
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Commands
|
7
|
+
class Start < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'start asana[:<project-gid>/<task-gid>]'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'Set current task and move it to a section (column) of your choice'
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
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
|
55
|
+
|
56
|
+
if task_already_in_wip_section?
|
57
|
+
cli.warn "Task already in section: #{current_task_section['name']}"
|
58
|
+
else
|
59
|
+
cli.warn "Moving task to section: #{wip_section['name']}"
|
60
|
+
move_task
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def task_already_in_wip_section?
|
65
|
+
!task_section_membership.nil?
|
66
|
+
end
|
67
|
+
|
68
|
+
def current_task_section
|
69
|
+
task_section_membership&.dig('section')
|
70
|
+
end
|
71
|
+
|
72
|
+
def task_section_membership
|
73
|
+
task['memberships'].find do |membership|
|
74
|
+
membership.dig('section', 'gid') == config.wip_section_gid
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def wip_section
|
79
|
+
@wip_section ||= api.get("sections/#{config.wip_section_gid}")
|
80
|
+
end
|
81
|
+
|
82
|
+
def move_task
|
83
|
+
body = { data: { task: task_gid } }
|
84
|
+
body_json = Oj.dump(body, mode: :json)
|
85
|
+
api.post("sections/#{config.wip_section_gid}/addTask", body_json)
|
86
|
+
end
|
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
|
+
|
98
|
+
def task
|
99
|
+
@task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,assignee.name')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Commands
|
7
|
+
class Tasks < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'tasks asana'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'List available tasks on project - useful for piping into grep etc.'
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
tasks.each do |task|
|
18
|
+
print_task(project, task)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def project
|
25
|
+
@project ||= begin
|
26
|
+
api.get("projects/#{project_gid}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def tasks
|
31
|
+
@tasks ||= begin
|
32
|
+
cli.warn 'Fetching tasks...'
|
33
|
+
api.get_paged('tasks', project: project['gid'], opt_fields: 'name')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
class Configuration
|
7
|
+
attr_accessor :cli
|
8
|
+
|
9
|
+
def initialize(cli:)
|
10
|
+
@cli = cli
|
11
|
+
@git = GitConfig.new(namespace: 'abt.asana')
|
12
|
+
end
|
13
|
+
|
14
|
+
def local_available?
|
15
|
+
GitConfig.local_available?
|
16
|
+
end
|
17
|
+
|
18
|
+
def project_gid
|
19
|
+
local_available? ? git['projectGid'] : nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def task_gid
|
23
|
+
local_available? ? git['taskGid'] : nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def workspace_gid
|
27
|
+
@workspace_gid ||= begin
|
28
|
+
current = git.global['workspaceGid']
|
29
|
+
if current.nil?
|
30
|
+
prompt_workspace['gid']
|
31
|
+
else
|
32
|
+
current
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def wip_section_gid
|
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']
|
47
|
+
end
|
48
|
+
|
49
|
+
def project_gid=(value)
|
50
|
+
return if project_gid == value
|
51
|
+
|
52
|
+
clear_local
|
53
|
+
git['projectGid'] = value unless value.nil?
|
54
|
+
end
|
55
|
+
|
56
|
+
def task_gid=(value)
|
57
|
+
git['taskGid'] = value
|
58
|
+
end
|
59
|
+
|
60
|
+
def clear_local
|
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
|
67
|
+
end
|
68
|
+
|
69
|
+
def clear_global
|
70
|
+
git.global['workspaceGid'] = nil
|
71
|
+
git.global['accessToken'] = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def access_token
|
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"))
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
attr_reader :git
|
88
|
+
|
89
|
+
def prompt_finalized_section
|
90
|
+
section = prompt_section('Select section for finalized tasks (E.g. "Merged")')
|
91
|
+
git['finalizedSectionGid'] = section['gid']
|
92
|
+
section
|
93
|
+
end
|
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
|
+
|
107
|
+
def prompt_workspace
|
108
|
+
cli.warn 'Fetching workspaces...'
|
109
|
+
workspaces = api.get_paged('workspaces')
|
110
|
+
if workspaces.empty?
|
111
|
+
cli.abort 'Your asana access token does not have access to any workspaces'
|
112
|
+
end
|
113
|
+
|
114
|
+
workspace = cli.prompt_choice('Select Asana workspace', workspaces)
|
115
|
+
git.global['workspaceGid'] = workspace['gid']
|
116
|
+
workspace
|
117
|
+
end
|
118
|
+
|
119
|
+
def api
|
120
|
+
Abt::Providers::Asana::Api.new(access_token: access_token)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|