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
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Abt
4
- class AsanaClient
5
- API_ENDPOINT = 'https://app.asana.com/api/1.0'
6
- VERBS = %i[get post patch].freeze
7
-
8
- attr_reader :access_token
9
-
10
- def initialize(access_token:)
11
- @access_token = access_token
12
- end
13
-
14
- VERBS.each do |verb|
15
- define_method(verb) do |*args|
16
- request(verb, *args)['data']
17
- end
18
- end
19
-
20
- def get_paged(path, query = {})
21
- records = []
22
-
23
- loop do
24
- result = request(:get, path, query.merge(limit: 100))
25
- records += result['data']
26
- break if result['next_page'].nil?
27
-
28
- path = result['next_page']['path'][1..-1]
29
- end
30
-
31
- records
32
- end
33
-
34
- def request(*args)
35
- response = connection.public_send(*args)
36
-
37
- if response.success?
38
- Oj.load(response.body)
39
- else
40
- error_class = Abt::HttpError.error_class_for_status(response.status)
41
- encoded_response_body = response.body.force_encoding('utf-8')
42
- raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
43
- end
44
- end
45
-
46
- def connection
47
- @connection ||= Faraday.new(API_ENDPOINT) do |connection|
48
- connection.headers['Authorization'] = "Bearer #{access_token}"
49
- connection.headers['Content-Type'] = 'application/json'
50
- end
51
- end
52
- end
53
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Abt
4
- class HarvestClient
5
- API_ENDPOINT = 'https://api.harvestapp.com/v2'
6
- VERBS = %i[get post patch].freeze
7
-
8
- attr_reader :access_token, :account_id
9
-
10
- def initialize(access_token:, account_id:)
11
- @access_token = access_token
12
- @account_id = account_id
13
- end
14
-
15
- VERBS.each do |verb|
16
- define_method(verb) do |*args|
17
- request(verb, *args)
18
- end
19
- end
20
-
21
- def get_paged(path, query = {})
22
- result_key = path.split('?').first.split('/').last
23
-
24
- page = 1
25
- records = []
26
-
27
- loop do
28
- result = get(path, query.merge(page: page))
29
- records += result[result_key]
30
- break if result['total_pages'] == page
31
-
32
- page += 1
33
- end
34
-
35
- records
36
- end
37
-
38
- def request(*args)
39
- response = connection.public_send(*args)
40
-
41
- if response.success?
42
- Oj.load(response.body)
43
- else
44
- error_class = Abt::HttpError.error_class_for_status(response.status)
45
- encoded_response_body = response.body.force_encoding('utf-8')
46
- raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
47
- end
48
- end
49
-
50
- def connection
51
- @connection ||= Faraday.new(API_ENDPOINT) do |connection|
52
- connection.headers['Authorization'] = "Bearer #{access_token}"
53
- connection.headers['Harvest-Account-Id'] = account_id
54
- connection.headers['Content-Type'] = 'application/json'
55
- end
56
- end
57
- end
58
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Dir.glob("#{File.expand_path(__dir__)}/help/*.rb").sort.each do |file|
4
- require file
5
- end
6
-
7
- module Abt
8
- module Help
9
- class << self
10
- def examples # rubocop:disable Metrics/MethodLength
11
- {
12
- 'Multiple providers and arguments can be passed, e.g.:' => {
13
- 'abt init asana harvest' => nil,
14
- 'abt pick-task asana harvest' => nil,
15
- 'abt start asana harvest' => nil,
16
- 'abt clear asana harvest' => nil
17
- },
18
- 'Command output can be piped, e.g.:' => {
19
- 'abt tasks asana | grep -i <name of task>' => nil,
20
- 'abt tasks asana | grep -i <name of task> | abt start' => nil
21
- }
22
- }
23
- end
24
-
25
- def providers
26
- provider_definitions
27
- end
28
-
29
- private
30
-
31
- def commandize(string)
32
- string = string.to_s
33
- string[0] = string[0].downcase
34
- string.gsub(/([A-Z])/, '-\1').downcase
35
- end
36
-
37
- def provider_definitions
38
- Abt::Providers.constants.sort.each_with_object({}) do |provider_name, definition|
39
- provider_class = Abt::Providers.const_get(provider_name)
40
-
41
- definition[commandize(provider_name)] = command_definitions(provider_class)
42
- end
43
- end
44
-
45
- def command_definitions(provider_class)
46
- provider_class.constants.sort.each_with_object({}) do |command_name, definition|
47
- command_class = provider_class.const_get(command_name)
48
-
49
- if command_class.respond_to?(:command) && command_class.respond_to?(:description)
50
- definition[command_class.command] = command_class.description
51
- end
52
- end
53
- end
54
- end
55
- end
56
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Abt
4
- module Providers
5
- class Asana
6
- class Clear
7
- def self.command
8
- 'clear asana'
9
- end
10
-
11
- def self.description
12
- 'Clear project/task for current git repository'
13
- end
14
-
15
- def initialize(**); end
16
-
17
- def call
18
- warn 'Clearing Asana project configuration'
19
- Asana.clear
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Abt
4
- module Providers
5
- class Asana
6
- class ClearGlobal
7
- def self.command
8
- 'clear-global asana'
9
- end
10
-
11
- def self.description
12
- 'Clear all global configuration'
13
- end
14
-
15
- def initialize(**); end
16
-
17
- def call
18
- warn 'Clearing Asana project configuration'
19
- Asana.clear_global
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Abt
4
- module Providers
5
- class Asana
6
- class Current < BaseCommand
7
- def self.command
8
- 'current asana[:<project-gid>[/<task-gid>]]'
9
- end
10
-
11
- def self.description
12
- 'Get or set project and or task for current git repository'
13
- end
14
-
15
- def call
16
- if arg_str.nil?
17
- show_current_configuration
18
- else
19
- warn 'Updating configuration'
20
- update_configuration
21
- end
22
- end
23
-
24
- private
25
-
26
- def show_current_configuration
27
- if project_gid.nil?
28
- warn 'No project selected'
29
- elsif task_gid.nil?
30
- print_project(project)
31
- else
32
- print_task(project, task)
33
- end
34
- end
35
-
36
- def update_configuration
37
- ensure_project_is_valid!
38
- remember_project_gid(project_gid)
39
-
40
- if task_gid.nil?
41
- print_project(project)
42
- remember_task_gid(nil)
43
- else
44
- ensure_task_is_valid!
45
- remember_task_gid(task_gid)
46
-
47
- print_task(project, task)
48
- end
49
- end
50
-
51
- def ensure_project_is_valid!
52
- abort "Invalid project: #{project_gid}" if project.nil?
53
- end
54
-
55
- def ensure_task_is_valid!
56
- abort "Invalid task: #{task_gid}" if task.nil?
57
- end
58
-
59
- def project
60
- @project ||= Asana.client.get("projects/#{project_gid}")
61
- end
62
-
63
- def task
64
- @task ||= Asana.client.get("tasks/#{task_gid}")
65
- end
66
- end
67
- end
68
- end
69
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Abt
4
- module Providers
5
- class Asana
6
- class HarvestTimeEntryData < BaseCommand
7
- def self.command
8
- 'harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
9
- end
10
-
11
- def self.description
12
- 'Print Harvest time entry data for Asana task as json. Used by harvest start script.'
13
- end
14
-
15
- def call # rubocop:disable Metrics/MethodLength
16
- ensure_current_is_valid!
17
-
18
- body = {
19
- notes: task['name'],
20
- external_reference: {
21
- id: task_gid.to_i,
22
- group_id: project_gid.to_i,
23
- permalink: task['permalink_url'],
24
- service: 'app.asana.com',
25
- service_icon_url: 'https://proxy.harvestfiles.com/production_harvestapp_public/uploads/platform_icons/app.asana.com.png'
26
- }
27
- }
28
-
29
- puts Oj.dump(body, mode: :json)
30
- end
31
-
32
- private
33
-
34
- def ensure_current_is_valid!
35
- abort "Invalid task gid: #{task_gid}" if task.nil?
36
-
37
- return if task['memberships'].any? { |m| m.dig('project', 'gid') == project_gid }
38
-
39
- abort "Invalid project gid: #{project_gid}"
40
- end
41
-
42
- def task
43
- @task ||= Asana.client.get("tasks/#{task_gid}")
44
- end
45
- end
46
- end
47
- end
48
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Abt
4
- module Providers
5
- class Asana
6
- class Init < BaseCommand
7
- def self.command
8
- 'init asana'
9
- end
10
-
11
- def self.description
12
- 'Pick Asana project for current git repository'
13
- end
14
-
15
- def call
16
- warn 'Loading projects'
17
-
18
- projects # Load projects up front to make it obvious that searches are instant
19
- project = find_search_result
20
-
21
- remember_project_gid(project['gid'])
22
- remember_task_gid(nil)
23
-
24
- print_project(project)
25
- end
26
-
27
- private
28
-
29
- def find_search_result
30
- loop do
31
- matches = matches_for_string cli.prompt('Enter search')
32
- if matches.empty?
33
- warn 'No matches'
34
- next
35
- end
36
-
37
- warn 'Showing the 10 first matches' if matches.size > 10
38
- choice = cli.prompt_choice 'Select a project', matches[0...10], true
39
- break choice unless choice.nil?
40
- end
41
- end
42
-
43
- def matches_for_string(string)
44
- search_string = sanitize_string(string)
45
-
46
- projects.select do |project|
47
- sanitize_string(project['name']).include?(search_string)
48
- end
49
- end
50
-
51
- def sanitize_string(string)
52
- string.downcase.gsub(/[^\w]/, '')
53
- end
54
-
55
- def projects
56
- @projects ||=
57
- Asana.client.get_paged('projects', workspace: Asana.workspace_gid, archived: false)
58
- end
59
- end
60
- end
61
- end
62
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Abt
4
- module Providers
5
- class Asana
6
- class Move < BaseCommand
7
- def self.command
8
- 'move asana[:<project-gid>/<task-gid>]'
9
- end
10
-
11
- def self.description
12
- 'Move current or specified task to another section (column)'
13
- end
14
-
15
- def call
16
- print_task(project, task)
17
-
18
- move_task
19
-
20
- warn "Asana task moved to #{section['name']}"
21
- rescue Abt::HttpError::HttpError => e
22
- warn e
23
- abort 'Unable to move asana task'
24
- end
25
-
26
- private
27
-
28
- def task
29
- @task ||= Asana.client.get("tasks/#{task_gid}")
30
- end
31
-
32
- def move_task
33
- body = { data: { task: task_gid } }
34
- body_json = Oj.dump(body, mode: :json)
35
- Asana.client.post("sections/#{section['gid']}/addTask", body_json)
36
- end
37
-
38
- def section
39
- @section ||= cli.prompt_choice 'Move asana task to?', sections
40
- end
41
-
42
- def project
43
- @project ||= Asana.client.get("projects/#{project_gid}")
44
- end
45
-
46
- def sections
47
- Asana.client.get_paged("projects/#{project_gid}/sections")
48
- rescue Abt::HttpError::HttpError
49
- []
50
- end
51
- end
52
- end
53
- end
54
- end