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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +4 -1
  3. data/lib/abt.rb +11 -0
  4. data/lib/abt/cli.rb +37 -32
  5. data/lib/abt/cli/dialogs.rb +18 -2
  6. data/lib/abt/cli/io.rb +23 -0
  7. data/lib/abt/docs.rb +57 -0
  8. data/lib/abt/{help → docs}/cli.rb +4 -4
  9. data/lib/abt/{help → docs}/markdown.rb +4 -4
  10. data/lib/abt/git_config.rb +55 -49
  11. data/lib/abt/helpers.rb +16 -0
  12. data/lib/abt/providers/asana.rb +9 -50
  13. data/lib/abt/providers/asana/api.rb +57 -0
  14. data/lib/abt/providers/asana/base_command.rb +14 -16
  15. data/lib/abt/providers/asana/commands/clear.rb +24 -0
  16. data/lib/abt/providers/asana/commands/clear_global.rb +24 -0
  17. data/lib/abt/providers/asana/commands/current.rb +77 -0
  18. data/lib/abt/providers/asana/commands/finalize.rb +71 -0
  19. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +50 -0
  20. data/lib/abt/providers/asana/commands/init.rb +70 -0
  21. data/lib/abt/providers/asana/commands/pick.rb +55 -0
  22. data/lib/abt/providers/asana/commands/projects.rb +39 -0
  23. data/lib/abt/providers/asana/commands/share.rb +29 -0
  24. data/lib/abt/providers/asana/commands/start.rb +105 -0
  25. data/lib/abt/providers/asana/commands/tasks.rb +40 -0
  26. data/lib/abt/providers/asana/configuration.rb +125 -0
  27. data/lib/abt/providers/harvest.rb +9 -42
  28. data/lib/abt/providers/harvest/api.rb +62 -0
  29. data/lib/abt/providers/harvest/base_command.rb +12 -16
  30. data/lib/abt/providers/harvest/commands/clear.rb +24 -0
  31. data/lib/abt/providers/harvest/commands/clear_global.rb +24 -0
  32. data/lib/abt/providers/harvest/commands/current.rb +83 -0
  33. data/lib/abt/providers/harvest/commands/init.rb +83 -0
  34. data/lib/abt/providers/harvest/commands/pick.rb +51 -0
  35. data/lib/abt/providers/harvest/commands/projects.rb +40 -0
  36. data/lib/abt/providers/harvest/commands/share.rb +29 -0
  37. data/lib/abt/providers/harvest/commands/start.rb +101 -0
  38. data/lib/abt/providers/harvest/commands/stop.rb +58 -0
  39. data/lib/abt/providers/harvest/commands/tasks.rb +45 -0
  40. data/lib/abt/providers/harvest/configuration.rb +91 -0
  41. data/lib/abt/version.rb +1 -1
  42. metadata +32 -26
  43. data/lib/abt/asana_client.rb +0 -53
  44. data/lib/abt/harvest_client.rb +0 -58
  45. data/lib/abt/help.rb +0 -56
  46. data/lib/abt/providers/asana/clear.rb +0 -24
  47. data/lib/abt/providers/asana/clear_global.rb +0 -24
  48. data/lib/abt/providers/asana/current.rb +0 -69
  49. data/lib/abt/providers/asana/harvest_link_entry_data.rb +0 -48
  50. data/lib/abt/providers/asana/init.rb +0 -62
  51. data/lib/abt/providers/asana/move.rb +0 -54
  52. data/lib/abt/providers/asana/pick_task.rb +0 -46
  53. data/lib/abt/providers/asana/projects.rb +0 -30
  54. data/lib/abt/providers/asana/start.rb +0 -22
  55. data/lib/abt/providers/asana/tasks.rb +0 -35
  56. data/lib/abt/providers/harvest/clear.rb +0 -24
  57. data/lib/abt/providers/harvest/clear_global.rb +0 -24
  58. data/lib/abt/providers/harvest/current.rb +0 -79
  59. data/lib/abt/providers/harvest/init.rb +0 -61
  60. data/lib/abt/providers/harvest/pick_task.rb +0 -45
  61. data/lib/abt/providers/harvest/projects.rb +0 -29
  62. data/lib/abt/providers/harvest/start.rb +0 -58
  63. data/lib/abt/providers/harvest/stop.rb +0 -51
  64. 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