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.
- checksums.yaml +4 -4
- data/bin/abt +4 -1
- data/lib/abt.rb +11 -0
- data/lib/abt/cli.rb +37 -32
- data/lib/abt/cli/dialogs.rb +18 -2
- data/lib/abt/cli/io.rb +23 -0
- data/lib/abt/docs.rb +57 -0
- data/lib/abt/{help → docs}/cli.rb +4 -4
- data/lib/abt/{help → docs}/markdown.rb +4 -4
- data/lib/abt/git_config.rb +55 -49
- data/lib/abt/helpers.rb +16 -0
- data/lib/abt/providers/asana.rb +9 -50
- data/lib/abt/providers/asana/api.rb +57 -0
- data/lib/abt/providers/asana/base_command.rb +14 -16
- data/lib/abt/providers/asana/commands/clear.rb +24 -0
- data/lib/abt/providers/asana/commands/clear_global.rb +24 -0
- data/lib/abt/providers/asana/commands/current.rb +77 -0
- data/lib/abt/providers/asana/commands/finalize.rb +71 -0
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +50 -0
- data/lib/abt/providers/asana/commands/init.rb +70 -0
- data/lib/abt/providers/asana/commands/pick.rb +55 -0
- data/lib/abt/providers/asana/commands/projects.rb +39 -0
- data/lib/abt/providers/asana/commands/share.rb +29 -0
- data/lib/abt/providers/asana/commands/start.rb +105 -0
- data/lib/abt/providers/asana/commands/tasks.rb +40 -0
- data/lib/abt/providers/asana/configuration.rb +125 -0
- data/lib/abt/providers/harvest.rb +9 -42
- data/lib/abt/providers/harvest/api.rb +62 -0
- data/lib/abt/providers/harvest/base_command.rb +12 -16
- data/lib/abt/providers/harvest/commands/clear.rb +24 -0
- data/lib/abt/providers/harvest/commands/clear_global.rb +24 -0
- data/lib/abt/providers/harvest/commands/current.rb +83 -0
- data/lib/abt/providers/harvest/commands/init.rb +83 -0
- data/lib/abt/providers/harvest/commands/pick.rb +51 -0
- data/lib/abt/providers/harvest/commands/projects.rb +40 -0
- data/lib/abt/providers/harvest/commands/share.rb +29 -0
- data/lib/abt/providers/harvest/commands/start.rb +101 -0
- data/lib/abt/providers/harvest/commands/stop.rb +58 -0
- data/lib/abt/providers/harvest/commands/tasks.rb +45 -0
- data/lib/abt/providers/harvest/configuration.rb +91 -0
- data/lib/abt/version.rb +1 -1
- metadata +32 -26
- data/lib/abt/asana_client.rb +0 -53
- data/lib/abt/harvest_client.rb +0 -58
- data/lib/abt/help.rb +0 -56
- data/lib/abt/providers/asana/clear.rb +0 -24
- data/lib/abt/providers/asana/clear_global.rb +0 -24
- data/lib/abt/providers/asana/current.rb +0 -69
- data/lib/abt/providers/asana/harvest_link_entry_data.rb +0 -48
- data/lib/abt/providers/asana/init.rb +0 -62
- data/lib/abt/providers/asana/move.rb +0 -54
- data/lib/abt/providers/asana/pick_task.rb +0 -46
- data/lib/abt/providers/asana/projects.rb +0 -30
- data/lib/abt/providers/asana/start.rb +0 -22
- data/lib/abt/providers/asana/tasks.rb +0 -35
- data/lib/abt/providers/harvest/clear.rb +0 -24
- data/lib/abt/providers/harvest/clear_global.rb +0 -24
- data/lib/abt/providers/harvest/current.rb +0 -79
- data/lib/abt/providers/harvest/init.rb +0 -61
- data/lib/abt/providers/harvest/pick_task.rb +0 -45
- data/lib/abt/providers/harvest/projects.rb +0 -29
- data/lib/abt/providers/harvest/start.rb +0 -58
- data/lib/abt/providers/harvest/stop.rb +0 -51
- data/lib/abt/providers/harvest/tasks.rb +0 -36
data/lib/abt/helpers.rb
ADDED
@@ -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
|
data/lib/abt/providers/asana.rb
CHANGED
@@ -1,59 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
Dir.glob("#{File.expand_path(__dir__)}/asana/*.rb").sort.each
|
4
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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 =
|
32
|
-
@
|
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
|
49
|
-
Abt::
|
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
|