abt-cli 0.0.19 → 0.0.24
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 +3 -3
- data/lib/abt.rb +6 -6
- data/lib/abt/ari.rb +2 -2
- data/lib/abt/ari_list.rb +1 -1
- data/lib/abt/base_command.rb +7 -7
- data/lib/abt/cli.rb +49 -47
- data/lib/abt/cli/arguments_parser.rb +6 -3
- data/lib/abt/cli/global_commands.rb +23 -0
- data/lib/abt/cli/global_commands/commands.rb +23 -0
- data/lib/abt/cli/global_commands/examples.rb +23 -0
- data/lib/abt/cli/global_commands/help.rb +23 -0
- data/lib/abt/cli/global_commands/readme.rb +23 -0
- data/lib/abt/cli/global_commands/share.rb +36 -0
- data/lib/abt/cli/global_commands/version.rb +23 -0
- data/lib/abt/cli/prompt.rb +64 -52
- data/lib/abt/docs.rb +48 -26
- data/lib/abt/docs/cli.rb +3 -3
- data/lib/abt/docs/markdown.rb +10 -7
- data/lib/abt/git_config.rb +4 -6
- data/lib/abt/helpers.rb +26 -8
- data/lib/abt/providers/asana/api.rb +9 -9
- data/lib/abt/providers/asana/base_command.rb +12 -10
- data/lib/abt/providers/asana/commands/add.rb +13 -12
- data/lib/abt/providers/asana/commands/branch_name.rb +8 -8
- data/lib/abt/providers/asana/commands/clear.rb +7 -8
- data/lib/abt/providers/asana/commands/current.rb +14 -15
- data/lib/abt/providers/asana/commands/finalize.rb +17 -18
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +18 -16
- data/lib/abt/providers/asana/commands/init.rb +8 -41
- data/lib/abt/providers/asana/commands/pick.rb +22 -26
- data/lib/abt/providers/asana/commands/projects.rb +5 -5
- data/lib/abt/providers/asana/commands/share.rb +7 -5
- data/lib/abt/providers/asana/commands/start.rb +28 -21
- data/lib/abt/providers/asana/commands/tasks.rb +6 -6
- data/lib/abt/providers/asana/configuration.rb +37 -29
- data/lib/abt/providers/asana/path.rb +6 -6
- data/lib/abt/providers/devops/api.rb +12 -12
- data/lib/abt/providers/devops/base_command.rb +14 -10
- data/lib/abt/providers/devops/commands/boards.rb +5 -7
- data/lib/abt/providers/devops/commands/branch_name.rb +9 -9
- data/lib/abt/providers/devops/commands/clear.rb +7 -8
- data/lib/abt/providers/devops/commands/current.rb +17 -18
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +21 -19
- data/lib/abt/providers/devops/commands/init.rb +21 -14
- data/lib/abt/providers/devops/commands/pick.rb +25 -19
- data/lib/abt/providers/devops/commands/share.rb +7 -5
- data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
- data/lib/abt/providers/devops/configuration.rb +15 -15
- data/lib/abt/providers/devops/path.rb +7 -6
- data/lib/abt/providers/git/commands/branch.rb +23 -21
- data/lib/abt/providers/harvest/api.rb +8 -8
- data/lib/abt/providers/harvest/base_command.rb +10 -8
- data/lib/abt/providers/harvest/commands/clear.rb +7 -8
- data/lib/abt/providers/harvest/commands/current.rb +13 -14
- data/lib/abt/providers/harvest/commands/init.rb +10 -39
- data/lib/abt/providers/harvest/commands/pick.rb +15 -11
- data/lib/abt/providers/harvest/commands/projects.rb +5 -5
- data/lib/abt/providers/harvest/commands/share.rb +7 -5
- data/lib/abt/providers/harvest/commands/start.rb +5 -3
- data/lib/abt/providers/harvest/commands/stop.rb +12 -12
- data/lib/abt/providers/harvest/commands/tasks.rb +7 -7
- data/lib/abt/providers/harvest/commands/track.rb +52 -37
- data/lib/abt/providers/harvest/configuration.rb +18 -18
- data/lib/abt/providers/harvest/path.rb +6 -6
- data/lib/abt/version.rb +1 -1
- metadata +12 -5
@@ -6,66 +6,33 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Init < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt init asana"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize(cli:, **)
|
17
|
-
@config = Configuration.new(cli: cli)
|
18
|
-
@cli = cli
|
13
|
+
"Pick Asana project for current git repository"
|
19
14
|
end
|
20
15
|
|
21
16
|
def perform
|
22
|
-
|
17
|
+
require_local_config!
|
23
18
|
|
24
19
|
projects # Load projects up front to make it obvious that searches are instant
|
25
|
-
project =
|
20
|
+
project = cli.prompt.search("Select a project", projects)
|
26
21
|
|
27
|
-
config.path = Path.from_ids(project[
|
22
|
+
config.path = Path.from_ids(project_gid: project["gid"])
|
28
23
|
|
29
24
|
print_project(project)
|
30
25
|
end
|
31
26
|
|
32
27
|
private
|
33
28
|
|
34
|
-
def find_search_result
|
35
|
-
warn 'Select a project'
|
36
|
-
|
37
|
-
loop do
|
38
|
-
matches = matches_for_string cli.prompt.text('Enter search')
|
39
|
-
if matches.empty?
|
40
|
-
warn 'No matches'
|
41
|
-
next
|
42
|
-
end
|
43
|
-
|
44
|
-
warn 'Showing the 10 first matches' if matches.size > 10
|
45
|
-
choice = cli.prompt.choice 'Select a project', matches[0...10], true
|
46
|
-
break choice unless choice.nil?
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def matches_for_string(string)
|
51
|
-
search_string = sanitize_string(string)
|
52
|
-
|
53
|
-
projects.select do |project|
|
54
|
-
sanitize_string(project['name']).include?(search_string)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def sanitize_string(string)
|
59
|
-
string.downcase.gsub(/[^\w]/, '')
|
60
|
-
end
|
61
|
-
|
62
29
|
def projects
|
63
30
|
@projects ||= begin
|
64
|
-
warn
|
65
|
-
api.get_paged(
|
31
|
+
warn("Fetching projects...")
|
32
|
+
api.get_paged("projects",
|
66
33
|
workspace: config.workspace_gid,
|
67
34
|
archived: false,
|
68
|
-
opt_fields:
|
35
|
+
opt_fields: "name,permalink_url")
|
69
36
|
end
|
70
37
|
end
|
71
38
|
end
|
@@ -6,72 +6,68 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Pick < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt pick asana[:<project-gid>]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Pick task for current git repository"
|
14
14
|
end
|
15
15
|
|
16
16
|
def self.flags
|
17
17
|
[
|
18
|
-
[
|
18
|
+
["-d", "--dry-run", "Keep existing configuration"]
|
19
19
|
]
|
20
20
|
end
|
21
21
|
|
22
22
|
def perform
|
23
|
-
|
23
|
+
require_local_config!
|
24
24
|
require_project!
|
25
25
|
|
26
|
-
warn
|
27
|
-
|
26
|
+
warn(project["name"])
|
28
27
|
task = select_task
|
29
28
|
|
30
29
|
print_task(project, task)
|
31
30
|
|
32
31
|
return if flags[:"dry-run"]
|
33
32
|
|
34
|
-
config.path = Path.from_ids(project_gid, task[
|
33
|
+
config.path = Path.from_ids(project_gid: project_gid, task_gid: task["gid"])
|
35
34
|
end
|
36
35
|
|
37
36
|
private
|
38
37
|
|
39
38
|
def project
|
40
|
-
@project ||= api.get("projects/#{project_gid}", opt_fields:
|
39
|
+
@project ||= api.get("projects/#{project_gid}", opt_fields: "name")
|
41
40
|
end
|
42
41
|
|
43
42
|
def select_task
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
task = cli.prompt.choice 'Select a task', tasks, true
|
55
|
-
return task if task
|
43
|
+
section = cli.prompt.choice("Which section?", sections)
|
44
|
+
warn("Fetching tasks...")
|
45
|
+
tasks = tasks_in_section(section)
|
46
|
+
|
47
|
+
if tasks.length.zero?
|
48
|
+
warn("Section is empty")
|
49
|
+
select_task
|
50
|
+
else
|
51
|
+
cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
|
56
52
|
end
|
57
53
|
end
|
58
54
|
|
59
55
|
def tasks_in_section(section)
|
60
56
|
tasks = api.get_paged(
|
61
|
-
|
62
|
-
section: section[
|
63
|
-
opt_fields:
|
57
|
+
"tasks",
|
58
|
+
section: section["gid"],
|
59
|
+
opt_fields: "name,completed,permalink_url"
|
64
60
|
)
|
65
61
|
|
66
62
|
# The below filtering is the best we can do with Asanas api, see this:
|
67
63
|
# https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
|
68
|
-
tasks.
|
64
|
+
tasks.reject { |task| task["completed"] }
|
69
65
|
end
|
70
66
|
|
71
67
|
def sections
|
72
68
|
@sections ||= begin
|
73
|
-
warn
|
74
|
-
api.get_paged("projects/#{project_gid}/sections", opt_fields:
|
69
|
+
warn("Fetching sections...")
|
70
|
+
api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
|
75
71
|
end
|
76
72
|
end
|
77
73
|
end
|
@@ -6,11 +6,11 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Projects < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt projects asana"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"List all available projects - useful for piping into grep etc."
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
@@ -23,12 +23,12 @@ module Abt
|
|
23
23
|
|
24
24
|
def projects
|
25
25
|
@projects ||= begin
|
26
|
-
warn
|
26
|
+
warn("Fetching projects...")
|
27
27
|
api.get_paged(
|
28
|
-
|
28
|
+
"projects",
|
29
29
|
workspace: config.workspace_gid,
|
30
30
|
archived: false,
|
31
|
-
opt_fields:
|
31
|
+
opt_fields: "name"
|
32
32
|
)
|
33
33
|
end
|
34
34
|
end
|
@@ -6,17 +6,19 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Share < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt share asana[:<project-gid>[/<task-gid>]]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Print project/task ARI"
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
18
|
-
|
19
|
-
cli.
|
17
|
+
if path != ""
|
18
|
+
cli.print_ari("asana", path)
|
19
|
+
elsif cli.output.isatty
|
20
|
+
warn("No configuration for project. Did you initialize Asana?")
|
21
|
+
end
|
20
22
|
end
|
21
23
|
end
|
22
24
|
end
|
@@ -6,16 +6,16 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Start < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt start asana[:<project-gid>/<task-gid>]"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"Move current or specified task to WIP section (column) and assign it to you"
|
14
14
|
end
|
15
15
|
|
16
16
|
def self.flags
|
17
17
|
[
|
18
|
-
[
|
18
|
+
["-s", "--set", "Set specified task as current"]
|
19
19
|
]
|
20
20
|
end
|
21
21
|
|
@@ -38,33 +38,39 @@ module Abt
|
|
38
38
|
return unless config.local_available?
|
39
39
|
|
40
40
|
config.path = path
|
41
|
-
warn
|
41
|
+
warn("Current task updated")
|
42
42
|
end
|
43
43
|
|
44
44
|
def update_assignee_if_needed
|
45
|
-
current_assignee = task.dig('assignee')
|
46
|
-
|
47
45
|
if current_assignee.nil?
|
48
|
-
warn
|
46
|
+
warn("Assigning task to user: #{current_user['name']}")
|
49
47
|
update_assignee
|
50
|
-
elsif current_assignee[
|
51
|
-
warn
|
52
|
-
elsif
|
53
|
-
warn
|
48
|
+
elsif current_assignee["gid"] == current_user["gid"]
|
49
|
+
warn("You are already assigned to this task")
|
50
|
+
elsif should_reassign?
|
51
|
+
warn("Reassigning task to user: #{current_user['name']}")
|
54
52
|
update_assignee
|
55
53
|
end
|
56
54
|
end
|
57
55
|
|
56
|
+
def current_assignee
|
57
|
+
task["assignee"]
|
58
|
+
end
|
59
|
+
|
60
|
+
def should_reassign?
|
61
|
+
cli.prompt.boolean("Task is assigned to: #{current_assignee['name']}, take over?")
|
62
|
+
end
|
63
|
+
|
58
64
|
def move_if_needed
|
59
65
|
unless project_gid == config.path.project_gid
|
60
|
-
warn
|
66
|
+
warn("Task was not moved, this is not implemented for tasks outside current project")
|
61
67
|
return
|
62
68
|
end
|
63
69
|
|
64
70
|
if task_already_in_wip_section?
|
65
|
-
warn
|
71
|
+
warn("Task already in section: #{current_task_section['name']}")
|
66
72
|
else
|
67
|
-
warn
|
73
|
+
warn("Moving task to section: #{wip_section['name']}")
|
68
74
|
move_task
|
69
75
|
end
|
70
76
|
end
|
@@ -74,17 +80,17 @@ module Abt
|
|
74
80
|
end
|
75
81
|
|
76
82
|
def current_task_section
|
77
|
-
task_section_membership&.dig(
|
83
|
+
task_section_membership&.dig("section")
|
78
84
|
end
|
79
85
|
|
80
86
|
def task_section_membership
|
81
|
-
task[
|
82
|
-
membership.dig(
|
87
|
+
task["memberships"].find do |membership|
|
88
|
+
membership.dig("section", "gid") == config.wip_section_gid
|
83
89
|
end
|
84
90
|
end
|
85
91
|
|
86
92
|
def wip_section
|
87
|
-
@wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields:
|
93
|
+
@wip_section ||= api.get("sections/#{config.wip_section_gid}", opt_fields: "name")
|
88
94
|
end
|
89
95
|
|
90
96
|
def move_task
|
@@ -94,17 +100,18 @@ module Abt
|
|
94
100
|
end
|
95
101
|
|
96
102
|
def update_assignee
|
97
|
-
body = { data: { assignee: current_user[
|
103
|
+
body = { data: { assignee: current_user["gid"] } }
|
98
104
|
body_json = Oj.dump(body, mode: :json)
|
99
105
|
api.put("tasks/#{task_gid}", body_json)
|
100
106
|
end
|
101
107
|
|
102
108
|
def current_user
|
103
|
-
@current_user ||= api.get(
|
109
|
+
@current_user ||= api.get("users/me", opt_fields: "name")
|
104
110
|
end
|
105
111
|
|
106
112
|
def task
|
107
|
-
@task ||= api.get("tasks/#{task_gid}",
|
113
|
+
@task ||= api.get("tasks/#{task_gid}",
|
114
|
+
opt_fields: "name,memberships.section.name,assignee.name,permalink_url")
|
108
115
|
end
|
109
116
|
end
|
110
117
|
end
|
@@ -6,11 +6,11 @@ module Abt
|
|
6
6
|
module Commands
|
7
7
|
class Tasks < BaseCommand
|
8
8
|
def self.usage
|
9
|
-
|
9
|
+
"abt tasks asana"
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
|
13
|
+
"List available tasks on project - useful for piping into grep etc."
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
@@ -25,15 +25,15 @@ module Abt
|
|
25
25
|
|
26
26
|
def project
|
27
27
|
@project ||= begin
|
28
|
-
api.get("projects/#{project_gid}", opt_fields:
|
28
|
+
api.get("projects/#{project_gid}", opt_fields: "name")
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
def tasks
|
33
33
|
@tasks ||= begin
|
34
|
-
warn
|
35
|
-
tasks = api.get_paged(
|
36
|
-
tasks.
|
34
|
+
warn("Fetching tasks...")
|
35
|
+
tasks = api.get_paged("tasks", project: project["gid"], opt_fields: "name,completed")
|
36
|
+
tasks.reject { |task| task["completed"] }
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
@@ -15,18 +15,18 @@ module Abt
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def path
|
18
|
-
Path.new(local_available? && git[
|
18
|
+
Path.new(local_available? && git["path"] || "")
|
19
19
|
end
|
20
20
|
|
21
21
|
def path=(new_path)
|
22
|
-
git[
|
22
|
+
git["path"] = new_path
|
23
23
|
end
|
24
24
|
|
25
25
|
def workspace_gid
|
26
26
|
@workspace_gid ||= begin
|
27
|
-
current = git_global[
|
27
|
+
current = git_global["workspaceGid"]
|
28
28
|
if current.nil?
|
29
|
-
|
29
|
+
prompt_workspace_gid
|
30
30
|
else
|
31
31
|
current
|
32
32
|
end
|
@@ -36,13 +36,13 @@ module Abt
|
|
36
36
|
def wip_section_gid
|
37
37
|
return nil unless local_available?
|
38
38
|
|
39
|
-
@wip_section_gid ||= git[
|
39
|
+
@wip_section_gid ||= git["wipSectionGid"] || prompt_wip_section["gid"]
|
40
40
|
end
|
41
41
|
|
42
42
|
def finalized_section_gid
|
43
43
|
return nil unless local_available?
|
44
44
|
|
45
|
-
@finalized_section_gid ||= git[
|
45
|
+
@finalized_section_gid ||= git["finalizedSectionGid"] || prompt_finalized_section["gid"]
|
46
46
|
end
|
47
47
|
|
48
48
|
def clear_local(verbose: true)
|
@@ -54,58 +54,66 @@ module Abt
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def access_token
|
57
|
-
return git_global[
|
57
|
+
return git_global["accessToken"] unless git_global["accessToken"].nil?
|
58
58
|
|
59
|
-
git_global[
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
59
|
+
git_global["accessToken"] = cli.prompt.text([
|
60
|
+
"Please provide your personal access token for Asana.",
|
61
|
+
"If you don't have one, create one here: https://app.asana.com/0/developer-console",
|
62
|
+
"",
|
63
|
+
"Enter access token"
|
64
64
|
].join("\n"))
|
65
65
|
end
|
66
66
|
|
67
67
|
private
|
68
68
|
|
69
69
|
def git
|
70
|
-
@git ||= GitConfig.new(
|
70
|
+
@git ||= GitConfig.new("local", "abt.asana")
|
71
71
|
end
|
72
72
|
|
73
73
|
def git_global
|
74
|
-
@git_global ||= GitConfig.new(
|
74
|
+
@git_global ||= GitConfig.new("global", "abt.asana")
|
75
75
|
end
|
76
76
|
|
77
77
|
def prompt_finalized_section
|
78
78
|
section = prompt_section('Select section for finalized tasks (E.g. "Merged")')
|
79
|
-
git[
|
79
|
+
git["finalizedSectionGid"] = section["gid"]
|
80
80
|
section
|
81
81
|
end
|
82
82
|
|
83
83
|
def prompt_wip_section
|
84
|
-
section = prompt_section(
|
85
|
-
git[
|
84
|
+
section = prompt_section("Select WIP (Work In Progress) section")
|
85
|
+
git["wipSectionGid"] = section["gid"]
|
86
86
|
section
|
87
87
|
end
|
88
88
|
|
89
89
|
def prompt_section(message)
|
90
|
-
cli.warn
|
91
|
-
sections = api.get_paged("projects/#{path.project_gid}/sections")
|
90
|
+
cli.warn("Fetching sections...")
|
91
|
+
sections = api.get_paged("projects/#{path.project_gid}/sections", opt_fields: "name")
|
92
92
|
cli.prompt.choice(message, sections)
|
93
93
|
end
|
94
94
|
|
95
|
-
def
|
96
|
-
cli.
|
97
|
-
|
98
|
-
if workspaces.
|
99
|
-
cli.abort 'Your asana access token does not have access to any workspaces'
|
100
|
-
elsif workspaces.one?
|
95
|
+
def prompt_workspace_gid
|
96
|
+
cli.abort("Your asana access token does not have access to any workspaces") if workspaces.empty?
|
97
|
+
|
98
|
+
if workspaces.one?
|
101
99
|
workspace = workspaces.first
|
102
|
-
cli.warn
|
100
|
+
cli.warn("Selected Asana workspace: #{workspace['name']}")
|
103
101
|
else
|
104
|
-
workspace =
|
102
|
+
workspace = pick_workspace
|
105
103
|
end
|
106
104
|
|
107
|
-
git_global[
|
108
|
-
|
105
|
+
git_global["workspaceGid"] = workspace["gid"]
|
106
|
+
end
|
107
|
+
|
108
|
+
def pick_workspace
|
109
|
+
cli.prompt.choice("Select Asana workspace", workspaces)
|
110
|
+
end
|
111
|
+
|
112
|
+
def workspaces
|
113
|
+
@workspaces ||= begin
|
114
|
+
cli.warn("Fetching workspaces...")
|
115
|
+
api.get_paged("workspaces", opt_fields: "name")
|
116
|
+
end
|
109
117
|
end
|
110
118
|
|
111
119
|
def api
|