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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Helpers
5
+ def self.const_to_command(string)
6
+ string = string.to_s
7
+ string[0] = string[0].downcase
8
+ string.gsub(/([A-Z])/, '-\1').downcase
9
+ end
10
+
11
+ def self.command_to_const(string)
12
+ inflector = Dry::Inflector.new
13
+ inflector.camelize(inflector.underscore(string))
14
+ end
15
+ end
16
+ end
@@ -1,59 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir.glob("#{File.expand_path(__dir__)}/asana/*.rb").sort.each do |file|
4
- require file
5
- end
3
+ Dir.glob("#{File.expand_path(__dir__)}/asana/*.rb").sort.each { |file| require file }
4
+ Dir.glob("#{File.expand_path(__dir__)}/asana/commands/*.rb").sort.each { |file| require file }
6
5
 
7
6
  module Abt
8
7
  module Providers
9
- class Asana
10
- class << self
11
- def workspace_gid
12
- @workspace_gid ||= begin
13
- current = Abt::GitConfig.global('abt.asana.workspaceGid')
14
- if current.nil?
15
- prompt_workspace['gid']
16
- else
17
- current
18
- end
19
- end
20
- end
21
-
22
- def clear
23
- Abt::GitConfig.unset_local('abt.asana.projectGid')
24
- Abt::GitConfig.unset_local('abt.asana.taskGid')
25
- end
26
-
27
- def clear_global
28
- Abt::GitConfig.unset_global('abt.asana.workspaceGid')
29
- Abt::GitConfig.unset_global('abt.asana.accessToken')
30
- end
31
-
32
- def client
33
- Abt::AsanaClient.new(access_token: access_token)
34
- end
35
-
36
- private
37
-
38
- def prompt_workspace
39
- workspaces = client.get_paged('workspaces')
40
- if workspaces.empty?
41
- abort 'Your asana access token does not have access to any workspaces'
42
- end
43
-
44
- # TODO: Handle if there are multiple workspaces
45
- workspace = workspaces.first
46
- Abt::GitConfig.global('abt.asana.workspaceGid', workspace['gid'])
47
- workspace
48
- end
8
+ module Asana
9
+ def self.command_names
10
+ Commands.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
11
+ end
49
12
 
50
- def access_token
51
- Abt::GitConfig.prompt_global(
52
- 'abt.asana.accessToken',
53
- 'Please enter your personal asana access_token',
54
- 'Create a personal access token here: https://app.asana.com/0/developer-console'
55
- )
56
- end
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)
57
16
  end
58
17
  end
59
18
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ class Api
7
+ API_ENDPOINT = 'https://app.asana.com/api/1.0'
8
+ VERBS = %i[get post put].freeze
9
+
10
+ attr_reader :access_token
11
+
12
+ def initialize(access_token:)
13
+ @access_token = access_token
14
+ end
15
+
16
+ VERBS.each do |verb|
17
+ define_method(verb) do |*args|
18
+ request(verb, *args)['data']
19
+ end
20
+ end
21
+
22
+ def get_paged(path, query = {})
23
+ records = []
24
+
25
+ loop do
26
+ result = request(:get, path, query.merge(limit: 100))
27
+ records += result['data']
28
+ break if result['next_page'].nil?
29
+
30
+ path = result['next_page']['path'][1..-1]
31
+ end
32
+
33
+ records
34
+ end
35
+
36
+ def request(*args)
37
+ response = connection.public_send(*args)
38
+
39
+ if response.success?
40
+ Oj.load(response.body)
41
+ else
42
+ error_class = Abt::HttpError.error_class_for_status(response.status)
43
+ encoded_response_body = response.body.force_encoding('utf-8')
44
+ raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
45
+ end
46
+ end
47
+
48
+ def connection
49
+ @connection ||= Faraday.new(API_ENDPOINT) do |connection|
50
+ connection.headers['Authorization'] = "Bearer #{access_token}"
51
+ connection.headers['Content-Type'] = 'application/json'
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -2,12 +2,13 @@
2
2
 
3
3
  module Abt
4
4
  module Providers
5
- class Asana
5
+ module Asana
6
6
  class BaseCommand
7
- attr_reader :arg_str, :project_gid, :task_gid, :cli
7
+ attr_reader :arg_str, :project_gid, :task_gid, :cli, :config
8
8
 
9
9
  def initialize(arg_str:, cli:)
10
10
  @arg_str = arg_str
11
+ @config = Configuration.new(cli: cli)
11
12
 
12
13
  if arg_str.nil?
13
14
  use_current_args
@@ -19,19 +20,24 @@ module Abt
19
20
 
20
21
  private
21
22
 
23
+ def same_args_as_config?
24
+ project_gid == config.project_gid && task_gid == config.task_gid
25
+ end
26
+
22
27
  def print_project(project)
23
28
  cli.print_provider_command('asana', project['gid'], project['name'])
29
+ cli.warn project['permalink_url'] if project.key?('permalink_url') && cli.output.isatty
24
30
  end
25
31
 
26
32
  def print_task(project, task)
33
+ project = { 'gid' => project } if project.is_a?(String)
27
34
  cli.print_provider_command('asana', "#{project['gid']}/#{task['gid']}", task['name'])
35
+ cli.warn task['permalink_url'] if task.key?('permalink_url') && cli.output.isatty
28
36
  end
29
37
 
30
38
  def use_current_args
31
- @project_gid = Abt::GitConfig.local('abt.asana.projectGid').to_s
32
- @project_gid = nil if project_gid.empty?
33
- @task_gid = Abt::GitConfig.local('abt.asana.taskGid').to_s
34
- @task_gid = nil if task_gid.empty?
39
+ @project_gid = config.project_gid
40
+ @task_gid = config.task_gid
35
41
  end
36
42
 
37
43
  def use_arg_str(arg_str)
@@ -45,16 +51,8 @@ module Abt
45
51
  @task_gid = nil if @task_gid.empty?
46
52
  end
47
53
 
48
- def remember_project_gid(project_gid)
49
- Abt::GitConfig.local('abt.asana.projectGid', project_gid)
50
- end
51
-
52
- def remember_task_gid(task_gid)
53
- if task_gid.nil?
54
- Abt::GitConfig.unset_local('abt.asana.taskGid')
55
- else
56
- Abt::GitConfig.local('abt.asana.taskGid', task_gid)
57
- end
54
+ def api
55
+ Abt::Providers::Asana::Api.new(access_token: config.access_token)
58
56
  end
59
57
  end
60
58
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Clear < BaseCommand
8
+ def self.command
9
+ 'clear asana'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear project/task for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing Asana project configuration'
18
+ config.clear_local
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class ClearGlobal < BaseCommand
8
+ def self.command
9
+ 'clear-global asana'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear all global configuration'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing Asana project configuration'
18
+ config.clear_global
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Current < BaseCommand
8
+ def self.command
9
+ 'current asana[:<project-gid>[/<task-gid>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Get or set project and or task for current git repository'
14
+ end
15
+
16
+ def call
17
+ if same_args_as_config? || !config.local_available?
18
+ show_current_configuration
19
+ else
20
+ cli.warn 'Updating configuration'
21
+ update_configuration
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def show_current_configuration
28
+ if project_gid.nil?
29
+ cli.warn 'No project selected'
30
+ elsif task_gid.nil?
31
+ print_project(project)
32
+ else
33
+ print_task(project, task)
34
+ end
35
+ end
36
+
37
+ def update_configuration
38
+ ensure_project_is_valid!
39
+ config.project_gid = project_gid
40
+
41
+ if task_gid.nil?
42
+ print_project(project)
43
+ config.task_gid = nil
44
+ else
45
+ ensure_task_is_valid!
46
+ config.task_gid = task_gid
47
+
48
+ print_task(project, task)
49
+ end
50
+ end
51
+
52
+ def ensure_project_is_valid!
53
+ cli.abort "Invalid project: #{project_gid}" if project.nil?
54
+ end
55
+
56
+ def ensure_task_is_valid!
57
+ cli.abort "Invalid task: #{task_gid}" if task.nil?
58
+ end
59
+
60
+ def project
61
+ @project ||= begin
62
+ cli.warn 'Fetching project...'
63
+ api.get("projects/#{project_gid}", opt_fields: 'name,permalink_url')
64
+ end
65
+ end
66
+
67
+ def task
68
+ @task ||= begin
69
+ cli.warn 'Fetching task...'
70
+ api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url')
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Finalize < BaseCommand
8
+ def self.command
9
+ 'finalize asana[:<project-gid>/<task-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Move current/specified task to section (column) for finalized tasks'
14
+ end
15
+
16
+ def call
17
+ unless config.local_available?
18
+ cli.abort 'This is a no-op for tasks outside the current project'
19
+ end
20
+ cli.abort 'No current or specified task' if task.nil?
21
+ print_task(project_gid, task)
22
+
23
+ if task_already_in_finalized_section?
24
+ cli.warn "Task already in section: #{current_task_section['name']}"
25
+ else
26
+ cli.warn "Moving task to section: #{finalized_section['name']}"
27
+ move_task
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def task_already_in_finalized_section?
34
+ !task_section_membership.nil?
35
+ end
36
+
37
+ def current_task_section
38
+ task_section_membership&.dig('section')
39
+ end
40
+
41
+ def task_section_membership
42
+ task['memberships'].find do |membership|
43
+ membership.dig('section', 'gid') == config.finalized_section_gid
44
+ end
45
+ end
46
+
47
+ def finalized_section
48
+ @finalized_section ||= api.get("sections/#{config.finalized_section_gid}",
49
+ opt_fields: 'name')
50
+ end
51
+
52
+ def move_task
53
+ body = { data: { task: task_gid } }
54
+ body_json = Oj.dump(body, mode: :json)
55
+ api.post("sections/#{config.finalized_section_gid}/addTask", body_json)
56
+ end
57
+
58
+ def task
59
+ @task ||= begin
60
+ if task_gid.nil?
61
+ nil
62
+ else
63
+ api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,permalink_url')
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class HarvestTimeEntryData < BaseCommand
8
+ def self.command
9
+ 'harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Print Harvest time entry data for Asana task as json. Used by harvest start script.'
14
+ end
15
+
16
+ def call # rubocop:disable Metrics/MethodLength
17
+ ensure_current_is_valid!
18
+
19
+ body = {
20
+ notes: task['name'],
21
+ external_reference: {
22
+ id: task_gid.to_i,
23
+ group_id: project_gid.to_i,
24
+ permalink: task['permalink_url'],
25
+ service: 'app.asana.com',
26
+ service_icon_url: 'https://proxy.harvestfiles.com/production_harvestapp_public/uploads/platform_icons/app.asana.com.png'
27
+ }
28
+ }
29
+
30
+ cli.puts Oj.dump(body, mode: :json)
31
+ end
32
+
33
+ private
34
+
35
+ def ensure_current_is_valid!
36
+ cli.abort "Invalid task gid: #{task_gid}" if task.nil?
37
+
38
+ return if task['memberships'].any? { |m| m.dig('project', 'gid') == project_gid }
39
+
40
+ cli.abort "Invalid project gid: #{project_gid}"
41
+ end
42
+
43
+ def task
44
+ @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url,memberships.project')
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end