abt-cli 0.0.2

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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/bin/abt +14 -0
  3. data/lib/abt.rb +5 -0
  4. data/lib/abt/asana_client.rb +53 -0
  5. data/lib/abt/cli.rb +103 -0
  6. data/lib/abt/cli/dialogs.rb +67 -0
  7. data/lib/abt/git_config.rb +78 -0
  8. data/lib/abt/harvest_client.rb +58 -0
  9. data/lib/abt/help.rb +56 -0
  10. data/lib/abt/help/cli.rb +59 -0
  11. data/lib/abt/help/markdown.rb +66 -0
  12. data/lib/abt/http_error.rb +45 -0
  13. data/lib/abt/providers.rb +5 -0
  14. data/lib/abt/providers/asana.rb +60 -0
  15. data/lib/abt/providers/asana/base_command.rb +62 -0
  16. data/lib/abt/providers/asana/clear.rb +24 -0
  17. data/lib/abt/providers/asana/clear_global.rb +24 -0
  18. data/lib/abt/providers/asana/current.rb +69 -0
  19. data/lib/abt/providers/asana/harvest_link_entry_data.rb +48 -0
  20. data/lib/abt/providers/asana/init.rb +62 -0
  21. data/lib/abt/providers/asana/move.rb +54 -0
  22. data/lib/abt/providers/asana/pick_task.rb +46 -0
  23. data/lib/abt/providers/asana/projects.rb +30 -0
  24. data/lib/abt/providers/asana/start.rb +22 -0
  25. data/lib/abt/providers/asana/tasks.rb +35 -0
  26. data/lib/abt/providers/harvest.rb +52 -0
  27. data/lib/abt/providers/harvest/base_command.rb +70 -0
  28. data/lib/abt/providers/harvest/clear.rb +24 -0
  29. data/lib/abt/providers/harvest/clear_global.rb +24 -0
  30. data/lib/abt/providers/harvest/current.rb +79 -0
  31. data/lib/abt/providers/harvest/init.rb +61 -0
  32. data/lib/abt/providers/harvest/pick_task.rb +45 -0
  33. data/lib/abt/providers/harvest/projects.rb +29 -0
  34. data/lib/abt/providers/harvest/start.rb +58 -0
  35. data/lib/abt/providers/harvest/stop.rb +51 -0
  36. data/lib/abt/providers/harvest/tasks.rb +36 -0
  37. data/lib/abt/version.rb +5 -0
  38. metadata +138 -0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Help
5
+ module Cli
6
+ class << self
7
+ def content
8
+ <<~TXT
9
+ Usage: abt <command> [<provider:arguments>...]
10
+
11
+ #{example_commands}
12
+
13
+ Available commands:
14
+ #{providers_commands}
15
+ TXT
16
+ end
17
+
18
+ private
19
+
20
+ def example_commands
21
+ lines = []
22
+
23
+ Help.examples.each_with_index do |(title, examples), index|
24
+ lines << '' unless index.zero?
25
+ lines << title
26
+
27
+ max_length = examples.keys.map(&:length).max
28
+ examples.each do |(command, description)|
29
+ lines << " #{command.ljust(max_length)} #{description}"
30
+ end
31
+ end
32
+
33
+ lines.join("\n")
34
+ end
35
+
36
+ def providers_commands
37
+ lines = []
38
+
39
+ Help.providers.each_with_index do |(provider_name, commands_definition), index|
40
+ lines << '' unless index.zero?
41
+ lines << "#{inflector.humanize(provider_name)}:"
42
+
43
+ max_length = commands_definition.keys.map(&:length).max
44
+
45
+ commands_definition.each do |(command, description)|
46
+ lines << " #{command.ljust(max_length)} #{description}"
47
+ end
48
+ end
49
+
50
+ lines.join("\n")
51
+ end
52
+
53
+ def inflector
54
+ Dry::Inflector.new
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Help
5
+ module Markdown
6
+ class << self
7
+ def content
8
+ <<~MD
9
+ # Abt
10
+ This readme was generated with `abt help-md > README.md`
11
+
12
+ ## Usage
13
+ `abt <command> [<provider:arguments>...]`
14
+
15
+ #{example_commands}
16
+
17
+ ## Available commands:
18
+ #{provider_commands}
19
+ MD
20
+ end
21
+
22
+ private
23
+
24
+ def example_commands
25
+ lines = []
26
+
27
+ Help.examples.each_with_index do |(title, commands), index|
28
+ lines << '' unless index.zero?
29
+ lines << title
30
+
31
+ commands.each do |(command, description)|
32
+ formatted_description = description.nil? ? '' : ": #{description}"
33
+ lines << "- `#{command}`#{formatted_description}"
34
+ end
35
+ end
36
+
37
+ lines.join("\n")
38
+ end
39
+
40
+ def provider_commands # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
41
+ lines = []
42
+
43
+ Help.providers.each_with_index do |(provider_name, commands), index|
44
+ lines << '' unless index.zero?
45
+ lines << "### #{inflector.humanize(provider_name)}"
46
+ lines << '| Command | Description |'
47
+ lines << '| :------ | :---------- |'
48
+
49
+ max_length = commands.keys.map(&:length).max
50
+
51
+ commands.each do |(command, description)|
52
+ adjusted_command = "`#{command}`".ljust(max_length + 2)
53
+ lines << "| #{adjusted_command} | #{description} |"
54
+ end
55
+ end
56
+
57
+ lines.join("\n")
58
+ end
59
+
60
+ def inflector
61
+ Dry::Inflector.new
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module HttpError
5
+ class HttpError < StandardError; end
6
+
7
+ class BadRequestError < HttpError; end
8
+
9
+ class UnauthorizedError < HttpError; end
10
+
11
+ class ForbiddenError < HttpError; end
12
+
13
+ class NotFoundError < HttpError; end
14
+
15
+ class MethodNotAllowedError < HttpError; end
16
+
17
+ class UnsupportedMediaTypeError < HttpError; end
18
+
19
+ class ProcessingError < HttpError; end
20
+
21
+ class TooManyRequestsError < HttpError; end
22
+
23
+ class InternalServerError < HttpError; end
24
+
25
+ class NotImplementedError < HttpError; end
26
+
27
+ class UnknownError < HttpError; end
28
+
29
+ def self.error_class_for_status(status) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
30
+ case status
31
+ when 400 then BadRequestError
32
+ when 401 then UnauthorizedError
33
+ when 403 then ForbiddenError
34
+ when 404 then NotFoundError
35
+ when 405 then MethodNotAllowedError
36
+ when 415 then UnsupportedMediaTypeError
37
+ when 422 then ProcessingError
38
+ when 429 then TooManyRequestsError
39
+ when 500 then InternalServerError
40
+ when 501 then NotImplementedError
41
+ else UnknownError
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{File.expand_path(__dir__)}/providers/*.rb").sort.each do |file|
4
+ require file
5
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{File.expand_path(__dir__)}/asana/*.rb").sort.each do |file|
4
+ require file
5
+ end
6
+
7
+ module Abt
8
+ 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
49
+
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
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ class Asana
6
+ class BaseCommand
7
+ attr_reader :arg_str, :project_gid, :task_gid, :cli
8
+
9
+ def initialize(arg_str:, cli:)
10
+ @arg_str = arg_str
11
+
12
+ if arg_str.nil?
13
+ use_current_args
14
+ else
15
+ use_arg_str(arg_str)
16
+ end
17
+ @cli = cli
18
+ end
19
+
20
+ private
21
+
22
+ def print_project(project)
23
+ cli.print_provider_command('asana', project['gid'], project['name'])
24
+ end
25
+
26
+ def print_task(project, task)
27
+ cli.print_provider_command('asana', "#{project['gid']}/#{task['gid']}", task['name'])
28
+ end
29
+
30
+ 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?
35
+ end
36
+
37
+ def use_arg_str(arg_str)
38
+ args = arg_str.to_s.split('/')
39
+ @project_gid = args[0].to_s
40
+ @project_gid = nil if project_gid.empty?
41
+
42
+ return if project_gid.nil?
43
+
44
+ @task_gid = args[1].to_s
45
+ @task_gid = nil if @task_gid.empty?
46
+ end
47
+
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
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,69 @@
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
@@ -0,0 +1,48 @@
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