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.
- checksums.yaml +7 -0
- data/bin/abt +14 -0
- data/lib/abt.rb +5 -0
- data/lib/abt/asana_client.rb +53 -0
- data/lib/abt/cli.rb +103 -0
- data/lib/abt/cli/dialogs.rb +67 -0
- data/lib/abt/git_config.rb +78 -0
- data/lib/abt/harvest_client.rb +58 -0
- data/lib/abt/help.rb +56 -0
- data/lib/abt/help/cli.rb +59 -0
- data/lib/abt/help/markdown.rb +66 -0
- data/lib/abt/http_error.rb +45 -0
- data/lib/abt/providers.rb +5 -0
- data/lib/abt/providers/asana.rb +60 -0
- data/lib/abt/providers/asana/base_command.rb +62 -0
- data/lib/abt/providers/asana/clear.rb +24 -0
- data/lib/abt/providers/asana/clear_global.rb +24 -0
- data/lib/abt/providers/asana/current.rb +69 -0
- data/lib/abt/providers/asana/harvest_link_entry_data.rb +48 -0
- data/lib/abt/providers/asana/init.rb +62 -0
- data/lib/abt/providers/asana/move.rb +54 -0
- data/lib/abt/providers/asana/pick_task.rb +46 -0
- data/lib/abt/providers/asana/projects.rb +30 -0
- data/lib/abt/providers/asana/start.rb +22 -0
- data/lib/abt/providers/asana/tasks.rb +35 -0
- data/lib/abt/providers/harvest.rb +52 -0
- data/lib/abt/providers/harvest/base_command.rb +70 -0
- data/lib/abt/providers/harvest/clear.rb +24 -0
- data/lib/abt/providers/harvest/clear_global.rb +24 -0
- data/lib/abt/providers/harvest/current.rb +79 -0
- data/lib/abt/providers/harvest/init.rb +61 -0
- data/lib/abt/providers/harvest/pick_task.rb +45 -0
- data/lib/abt/providers/harvest/projects.rb +29 -0
- data/lib/abt/providers/harvest/start.rb +58 -0
- data/lib/abt/providers/harvest/stop.rb +51 -0
- data/lib/abt/providers/harvest/tasks.rb +36 -0
- data/lib/abt/version.rb +5 -0
- metadata +138 -0
data/lib/abt/help/cli.rb
ADDED
@@ -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,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
|