abt-cli 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/abt +3 -1
- data/lib/abt.rb +11 -0
- data/lib/abt/cli.rb +25 -29
- data/lib/abt/cli/dialogs.rb +2 -2
- data/lib/abt/cli/io.rb +21 -0
- data/lib/abt/{help.rb → docs.rb} +8 -14
- data/lib/abt/{help → docs}/cli.rb +3 -3
- data/lib/abt/{help → docs}/markdown.rb +3 -3
- 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 +5 -12
- 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 +71 -0
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +50 -0
- data/lib/abt/providers/asana/commands/init.rb +63 -0
- data/lib/abt/providers/asana/commands/move.rb +56 -0
- data/lib/abt/providers/asana/commands/pick_task.rb +48 -0
- data/lib/abt/providers/asana/commands/projects.rb +32 -0
- data/lib/abt/providers/asana/commands/start.rb +60 -0
- data/lib/abt/providers/asana/commands/tasks.rb +37 -0
- data/lib/abt/providers/asana/configuration.rb +105 -0
- data/lib/abt/providers/harvest.rb +9 -0
- data/lib/abt/version.rb +1 -1
- metadata +18 -15
- data/lib/abt/asana_client.rb +0 -53
- 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
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
class Configuration
|
7
|
+
attr_accessor :cli
|
8
|
+
|
9
|
+
def initialize(cli:)
|
10
|
+
@cli = cli
|
11
|
+
end
|
12
|
+
|
13
|
+
def project_gid
|
14
|
+
Abt::GitConfig.local('abt.asana.projectGid')
|
15
|
+
end
|
16
|
+
|
17
|
+
def task_gid
|
18
|
+
Abt::GitConfig.local('abt.asana.taskGid')
|
19
|
+
end
|
20
|
+
|
21
|
+
def workspace_gid
|
22
|
+
@workspace_gid ||= begin
|
23
|
+
current = Abt::GitConfig.global('abt.asana.workspaceGid')
|
24
|
+
if current.nil?
|
25
|
+
prompt_workspace['gid']
|
26
|
+
else
|
27
|
+
current
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def wip_section_gid
|
33
|
+
@wip_section_gid ||= begin
|
34
|
+
current = Abt::GitConfig.global('abt.asana.wipSectionGid')
|
35
|
+
if current.nil?
|
36
|
+
prompt_wip_section['gid']
|
37
|
+
else
|
38
|
+
current
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def project_gid=(value)
|
44
|
+
return if project_gid == value
|
45
|
+
|
46
|
+
clear_local
|
47
|
+
Abt::GitConfig.local('abt.asana.projectGid', value) unless value.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
def task_gid=(value)
|
51
|
+
if value.nil?
|
52
|
+
Abt::GitConfig.unset_local('abt.asana.taskGid')
|
53
|
+
elsif task_gid != value
|
54
|
+
Abt::GitConfig.local('abt.asana.taskGid', value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def clear_local
|
59
|
+
Abt::GitConfig.unset_local('abt.asana.projectGid')
|
60
|
+
Abt::GitConfig.unset_local('abt.asana.taskGid')
|
61
|
+
Abt::GitConfig.unset_local('abt.asana.wipSectionGid')
|
62
|
+
end
|
63
|
+
|
64
|
+
def clear_global
|
65
|
+
Abt::GitConfig.unset_global('abt.asana.workspaceGid')
|
66
|
+
Abt::GitConfig.unset_global('abt.asana.accessToken')
|
67
|
+
end
|
68
|
+
|
69
|
+
def access_token
|
70
|
+
Abt::GitConfig.prompt_global(
|
71
|
+
'abt.asana.accessToken',
|
72
|
+
'Please enter your personal asana access_token',
|
73
|
+
'Create a personal access token here: https://app.asana.com/0/developer-console'
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def prompt_wip_section
|
80
|
+
sections = api.get_paged("projects/#{project_gid}/sections")
|
81
|
+
|
82
|
+
section = cli.prompt_choice('Select WIP (Work In Progress) section', sections)
|
83
|
+
Abt::GitConfig.global('abt.asana.wipSectionGid', section['gid'])
|
84
|
+
section
|
85
|
+
end
|
86
|
+
|
87
|
+
def prompt_workspace
|
88
|
+
workspaces = api.get_paged('workspaces')
|
89
|
+
if workspaces.empty?
|
90
|
+
cli.abort 'Your asana access token does not have access to any workspaces'
|
91
|
+
end
|
92
|
+
|
93
|
+
# TODO: Handle if there are multiple workspaces
|
94
|
+
workspace = workspaces.first
|
95
|
+
Abt::GitConfig.global('abt.asana.workspaceGid', workspace['gid'])
|
96
|
+
workspace
|
97
|
+
end
|
98
|
+
|
99
|
+
def api
|
100
|
+
Abt::Providers::Asana::Api.new(access_token: access_token)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -8,6 +8,15 @@ module Abt
|
|
8
8
|
module Providers
|
9
9
|
class Harvest
|
10
10
|
class << self
|
11
|
+
def command_names
|
12
|
+
constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def command_class(name)
|
16
|
+
const_name = Helpers.command_to_const(name)
|
17
|
+
const_get(const_name) if const_defined?(const_name)
|
18
|
+
end
|
19
|
+
|
11
20
|
def user_id
|
12
21
|
Abt::GitConfig.prompt_global(
|
13
22
|
'abt.harvest.userId',
|
data/lib/abt/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abt-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jesper Sørensen
|
@@ -75,28 +75,31 @@ extensions: []
|
|
75
75
|
extra_rdoc_files: []
|
76
76
|
files:
|
77
77
|
- "./lib/abt.rb"
|
78
|
-
- "./lib/abt/asana_client.rb"
|
79
78
|
- "./lib/abt/cli.rb"
|
80
79
|
- "./lib/abt/cli/dialogs.rb"
|
80
|
+
- "./lib/abt/cli/io.rb"
|
81
|
+
- "./lib/abt/docs.rb"
|
82
|
+
- "./lib/abt/docs/cli.rb"
|
83
|
+
- "./lib/abt/docs/markdown.rb"
|
81
84
|
- "./lib/abt/git_config.rb"
|
82
85
|
- "./lib/abt/harvest_client.rb"
|
83
|
-
- "./lib/abt/
|
84
|
-
- "./lib/abt/help/cli.rb"
|
85
|
-
- "./lib/abt/help/markdown.rb"
|
86
|
+
- "./lib/abt/helpers.rb"
|
86
87
|
- "./lib/abt/http_error.rb"
|
87
88
|
- "./lib/abt/providers.rb"
|
88
89
|
- "./lib/abt/providers/asana.rb"
|
90
|
+
- "./lib/abt/providers/asana/api.rb"
|
89
91
|
- "./lib/abt/providers/asana/base_command.rb"
|
90
|
-
- "./lib/abt/providers/asana/clear.rb"
|
91
|
-
- "./lib/abt/providers/asana/clear_global.rb"
|
92
|
-
- "./lib/abt/providers/asana/current.rb"
|
93
|
-
- "./lib/abt/providers/asana/
|
94
|
-
- "./lib/abt/providers/asana/init.rb"
|
95
|
-
- "./lib/abt/providers/asana/move.rb"
|
96
|
-
- "./lib/abt/providers/asana/pick_task.rb"
|
97
|
-
- "./lib/abt/providers/asana/projects.rb"
|
98
|
-
- "./lib/abt/providers/asana/start.rb"
|
99
|
-
- "./lib/abt/providers/asana/tasks.rb"
|
92
|
+
- "./lib/abt/providers/asana/commands/clear.rb"
|
93
|
+
- "./lib/abt/providers/asana/commands/clear_global.rb"
|
94
|
+
- "./lib/abt/providers/asana/commands/current.rb"
|
95
|
+
- "./lib/abt/providers/asana/commands/harvest_time_entry_data.rb"
|
96
|
+
- "./lib/abt/providers/asana/commands/init.rb"
|
97
|
+
- "./lib/abt/providers/asana/commands/move.rb"
|
98
|
+
- "./lib/abt/providers/asana/commands/pick_task.rb"
|
99
|
+
- "./lib/abt/providers/asana/commands/projects.rb"
|
100
|
+
- "./lib/abt/providers/asana/commands/start.rb"
|
101
|
+
- "./lib/abt/providers/asana/commands/tasks.rb"
|
102
|
+
- "./lib/abt/providers/asana/configuration.rb"
|
100
103
|
- "./lib/abt/providers/harvest.rb"
|
101
104
|
- "./lib/abt/providers/harvest/base_command.rb"
|
102
105
|
- "./lib/abt/providers/harvest/clear.rb"
|
data/lib/abt/asana_client.rb
DELETED
@@ -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,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
|