abt-cli 0.0.4 → 0.0.9
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 +1 -0
- data/lib/abt/cli.rb +1 -1
- data/lib/abt/cli/dialogs.rb +10 -7
- data/lib/abt/docs.rb +4 -0
- data/lib/abt/providers/asana/commands/init.rb +2 -0
- data/lib/abt/providers/asana/commands/pick.rb +16 -4
- data/lib/abt/providers/asana/configuration.rb +5 -1
- data/lib/abt/providers/harvest/commands/clear.rb +1 -3
- data/lib/abt/providers/harvest/commands/current.rb +11 -9
- data/lib/abt/providers/harvest/commands/init.rb +20 -3
- data/lib/abt/providers/harvest/commands/pick.rb +9 -7
- data/lib/abt/providers/harvest/commands/projects.rb +7 -1
- data/lib/abt/providers/harvest/commands/start.rb +22 -45
- data/lib/abt/providers/harvest/commands/stop.rb +1 -1
- data/lib/abt/providers/harvest/commands/tasks.rb +16 -9
- data/lib/abt/providers/harvest/commands/track.rb +70 -0
- data/lib/abt/providers/harvest/configuration.rb +9 -8
- data/lib/abt/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5044c8e5cb4330dd9db124834ee6f9bffca78a0ffaa08c268ba35eb3b0a21fe9
|
4
|
+
data.tar.gz: d9eb98460e021a032f9efdc0d9b2b98a20b57aba6267ef9024ee6f840bcf6858
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e2536f6b91bebf10d0fe637864045f61d04f90aa1b1ede6bd0a0db62ad54ef54cb541eda71fca2554cd741fe0011cb5586a0bb904ec412a6a07d90fc6c477208
|
7
|
+
data.tar.gz: 10c5fcf70d15dcfff152cf62a3d39109db499768c5fbb96d0eadecc107c85150e9277f647da482ea72d6d2cab68974b950500c2e433a0a3f8c1547c6c6fe74a0
|
data/bin/abt
CHANGED
data/lib/abt/cli.rb
CHANGED
data/lib/abt/cli/dialogs.rb
CHANGED
@@ -25,13 +25,15 @@ module Abt
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def prompt_choice(text, options, allow_back_option = false)
|
28
|
-
if options.one?
|
29
|
-
warn "Selected: #{options.first['name']}"
|
30
|
-
return options.first
|
31
|
-
end
|
32
|
-
|
33
28
|
warn "#{text}:"
|
34
29
|
|
30
|
+
if options.length.zero?
|
31
|
+
abort 'No available options' unless allow_back_option
|
32
|
+
|
33
|
+
warn 'No available options'
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
|
35
37
|
print_options(options)
|
36
38
|
select_options(options, allow_back_option)
|
37
39
|
end
|
@@ -45,11 +47,12 @@ module Abt
|
|
45
47
|
end
|
46
48
|
|
47
49
|
def select_options(options, allow_back_option)
|
48
|
-
|
50
|
+
loop do
|
51
|
+
number = read_option_number(options.length, allow_back_option)
|
49
52
|
if number.nil?
|
50
53
|
return nil if allow_back_option
|
51
54
|
|
52
|
-
|
55
|
+
next
|
53
56
|
end
|
54
57
|
|
55
58
|
option = options[number - 1]
|
data/lib/abt/docs.rb
CHANGED
@@ -17,6 +17,10 @@ module Abt
|
|
17
17
|
'abt start asana harvest' => 'Continue working, e.g. after a break',
|
18
18
|
'abt finalize asana' => 'Finalize the selected asana task'
|
19
19
|
},
|
20
|
+
'Tracking meetings (without changing the config):' => {
|
21
|
+
'abt tasks asana | grep -i standup | abt track harvest' => 'Track on asana meeting task without changing any configuration',
|
22
|
+
'abt tasks harvest | grep -i comment | abt track harvest' => 'Track on harvest "Comment"-task (will prompt for a comment)'
|
23
|
+
},
|
20
24
|
'Command output can be piped, e.g.:' => {
|
21
25
|
'abt tasks asana | grep -i <name of task>' => nil,
|
22
26
|
'abt tasks asana | grep -i <name of task> | abt start' => nil
|
@@ -18,7 +18,7 @@ module Abt
|
|
18
18
|
|
19
19
|
cli.warn project['name']
|
20
20
|
|
21
|
-
task =
|
21
|
+
task = select_task
|
22
22
|
|
23
23
|
config.project_gid = project_gid # We might have gotten the project ID as an argument
|
24
24
|
config.task_gid = task['gid']
|
@@ -32,14 +32,26 @@ module Abt
|
|
32
32
|
@project ||= api.get("projects/#{project_gid}")
|
33
33
|
end
|
34
34
|
|
35
|
-
def
|
36
|
-
|
35
|
+
def select_task
|
36
|
+
loop do
|
37
37
|
section = cli.prompt_choice 'Which section?', sections
|
38
38
|
cli.warn 'Fetching tasks...'
|
39
|
-
|
39
|
+
tasks = tasks_in_section(section)
|
40
|
+
|
41
|
+
if tasks.length.zero?
|
42
|
+
cli.warn 'Section is empty'
|
43
|
+
next
|
44
|
+
end
|
45
|
+
|
46
|
+
task = cli.prompt_choice 'Select a task', tasks, true
|
47
|
+
return task if task
|
40
48
|
end
|
41
49
|
end
|
42
50
|
|
51
|
+
def tasks_in_section(section)
|
52
|
+
api.get_paged('tasks', section: section['gid'], opt_fields: 'name,permalink_url')
|
53
|
+
end
|
54
|
+
|
43
55
|
def sections
|
44
56
|
@sections ||= begin
|
45
57
|
cli.warn 'Fetching sections...'
|
@@ -109,9 +109,13 @@ module Abt
|
|
109
109
|
workspaces = api.get_paged('workspaces')
|
110
110
|
if workspaces.empty?
|
111
111
|
cli.abort 'Your asana access token does not have access to any workspaces'
|
112
|
+
elsif workspaces.one?
|
113
|
+
workspace = workspaces.first
|
114
|
+
cli.warn "Selected Asana workspace #{workspace['name']}"
|
115
|
+
else
|
116
|
+
workspace = cli.prompt_choice('Select Asana workspace', workspaces)
|
112
117
|
end
|
113
118
|
|
114
|
-
workspace = cli.prompt_choice('Select Asana workspace', workspaces)
|
115
119
|
git.global['workspaceGid'] = workspace['gid']
|
116
120
|
workspace
|
117
121
|
end
|
@@ -58,22 +58,24 @@ module Abt
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def project
|
61
|
-
@project ||=
|
61
|
+
@project ||= project_assignment['project'].merge('client' => project_assignment['client'])
|
62
62
|
end
|
63
63
|
|
64
64
|
def task
|
65
|
-
|
66
|
-
|
67
|
-
|
65
|
+
@task ||= project_assignment['task_assignments'].map { |ta| ta['task'] }.find do |task|
|
66
|
+
task['id'].to_s == task_id
|
67
|
+
end
|
68
68
|
end
|
69
69
|
|
70
|
-
def
|
71
|
-
@
|
72
|
-
|
73
|
-
rescue Abt::HttpError::HttpError # rubocop:disable Layout/RescueEnsureAlignment
|
74
|
-
[]
|
70
|
+
def project_assignment
|
71
|
+
@project_assignment ||= begin
|
72
|
+
project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
|
75
73
|
end
|
76
74
|
end
|
75
|
+
|
76
|
+
def project_assignments
|
77
|
+
@project_assignments ||= api.get_paged('users/me/project_assignments')
|
78
|
+
end
|
77
79
|
end
|
78
80
|
end
|
79
81
|
end
|
@@ -28,6 +28,8 @@ module Abt
|
|
28
28
|
private
|
29
29
|
|
30
30
|
def find_search_result
|
31
|
+
cli.warn 'Select a project'
|
32
|
+
|
31
33
|
loop do
|
32
34
|
matches = matches_for_string cli.prompt('Enter search')
|
33
35
|
if matches.empty?
|
@@ -37,14 +39,14 @@ module Abt
|
|
37
39
|
|
38
40
|
cli.warn 'Showing the 10 first matches' if matches.size > 10
|
39
41
|
choice = cli.prompt_choice 'Select a project', matches[0...10], true
|
40
|
-
break choice unless choice.nil?
|
42
|
+
break choice['project'] unless choice.nil?
|
41
43
|
end
|
42
44
|
end
|
43
45
|
|
44
46
|
def matches_for_string(string)
|
45
47
|
search_string = sanitize_string(string)
|
46
48
|
|
47
|
-
|
49
|
+
searchable_projects.select do |project|
|
48
50
|
sanitize_string(project['name']).include?(search_string)
|
49
51
|
end
|
50
52
|
end
|
@@ -53,12 +55,27 @@ module Abt
|
|
53
55
|
string.downcase.gsub(/[^\w]/, '')
|
54
56
|
end
|
55
57
|
|
58
|
+
def searchable_projects
|
59
|
+
@searchable_projects ||= projects.map do |project|
|
60
|
+
{
|
61
|
+
'name' => "#{project['client']['name']} > #{project['name']}",
|
62
|
+
'project' => project
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
56
67
|
def projects
|
57
68
|
@projects ||= begin
|
58
69
|
cli.warn 'Fetching projects...'
|
59
|
-
|
70
|
+
project_assignments.map do |project_assignment|
|
71
|
+
project_assignment['project'].merge('client' => project_assignment['client'])
|
72
|
+
end
|
60
73
|
end
|
61
74
|
end
|
75
|
+
|
76
|
+
def project_assignments
|
77
|
+
@project_assignments ||= api.get_paged('users/me/project_assignments')
|
78
|
+
end
|
62
79
|
end
|
63
80
|
end
|
64
81
|
end
|
@@ -28,20 +28,22 @@ module Abt
|
|
28
28
|
private
|
29
29
|
|
30
30
|
def project
|
31
|
-
|
31
|
+
project_assignment['project']
|
32
32
|
end
|
33
33
|
|
34
34
|
def tasks
|
35
|
-
|
35
|
+
@tasks ||= project_assignment['task_assignments'].map { |ta| ta['task'] }
|
36
36
|
end
|
37
37
|
|
38
|
-
def
|
39
|
-
@
|
40
|
-
|
41
|
-
rescue Abt::HttpError::HttpError # rubocop:disable Layout/RescueEnsureAlignment
|
42
|
-
[]
|
38
|
+
def project_assignment
|
39
|
+
@project_assignment ||= begin
|
40
|
+
project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
|
43
41
|
end
|
44
42
|
end
|
43
|
+
|
44
|
+
def project_assignments
|
45
|
+
@project_assignments ||= api.get_paged('users/me/project_assignments')
|
46
|
+
end
|
45
47
|
end
|
46
48
|
end
|
47
49
|
end
|
@@ -24,9 +24,15 @@ module Abt
|
|
24
24
|
def projects
|
25
25
|
@projects ||= begin
|
26
26
|
cli.warn 'Fetching projects...'
|
27
|
-
|
27
|
+
project_assignments.map do |project_assignment|
|
28
|
+
project_assignment['project'].merge('client' => project_assignment['client'])
|
29
|
+
end
|
28
30
|
end
|
29
31
|
end
|
32
|
+
|
33
|
+
def project_assignments
|
34
|
+
@project_assignments ||= api.get_paged('users/me/project_assignments')
|
35
|
+
end
|
30
36
|
end
|
31
37
|
end
|
32
38
|
end
|
@@ -10,21 +10,16 @@ module Abt
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
'
|
13
|
+
'As track, but also lets the user override the current task and triggers `start` commands for other providers ' # rubocop:disable Layout/LineLength
|
14
14
|
end
|
15
15
|
|
16
16
|
def call
|
17
|
-
|
17
|
+
start_output = call_start
|
18
|
+
puts start_output
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
print_task(project, task)
|
22
|
-
|
23
|
-
cli.abort('No task selected') if task_id.nil?
|
20
|
+
use_arg_str(arg_str_from_start_output(start_output))
|
24
21
|
|
25
|
-
|
26
|
-
|
27
|
-
cli.warn 'Tracker successfully started'
|
22
|
+
maybe_override_current_task
|
28
23
|
rescue Abt::HttpError::HttpError => e
|
29
24
|
cli.warn e
|
30
25
|
cli.abort 'Unable to start tracker'
|
@@ -32,47 +27,29 @@ module Abt
|
|
32
27
|
|
33
28
|
private
|
34
29
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
return unless config.local_available?
|
39
|
-
|
40
|
-
should_override = cli.prompt_boolean 'Set selected task as current?'
|
41
|
-
Current.new(arg_str: arg_str, cli: cli).call if should_override
|
30
|
+
def arg_str_from_start_output(output)
|
31
|
+
output = output.split(' # ').first
|
32
|
+
output.split(':')[1]
|
42
33
|
end
|
43
34
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
task_id: task_id,
|
48
|
-
user_id: config.user_id,
|
49
|
-
spent_date: Date.today.iso8601
|
50
|
-
}.merge(external_link_data), mode: :json)
|
51
|
-
api.post('time_entries', body)
|
52
|
-
end
|
53
|
-
|
54
|
-
def project
|
55
|
-
@project ||= api.get("projects/#{project_id}")
|
56
|
-
end
|
35
|
+
def call_start
|
36
|
+
output = StringIO.new
|
37
|
+
Abt::Cli.new(argv: ['track', *cli.args], output: output).perform
|
57
38
|
|
58
|
-
|
59
|
-
|
39
|
+
output_str = output.string.strip
|
40
|
+
cli.abort 'No task provided' if output_str.empty?
|
41
|
+
output_str
|
60
42
|
end
|
61
43
|
|
62
|
-
def
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
return {} if lines.empty?
|
68
|
-
|
69
|
-
# TODO: Make user choose which reference to use by printing the urls
|
70
|
-
if lines.length > 1
|
71
|
-
cli.abort('Multiple providers had harvest reference data, only one is supported at a time') # rubocop:disable Layout/LineLength
|
72
|
-
end
|
44
|
+
def maybe_override_current_task
|
45
|
+
return if arg_str.nil?
|
46
|
+
return if same_args_as_config?
|
47
|
+
return unless config.local_available?
|
48
|
+
return unless cli.prompt_boolean 'Set selected task as current?'
|
73
49
|
|
74
|
-
|
75
|
-
|
50
|
+
output = StringIO.new
|
51
|
+
Abt::Cli.new(argv: ['current', "harvest:#{project_id}/#{task_id}"],
|
52
|
+
output: output).perform
|
76
53
|
end
|
77
54
|
end
|
78
55
|
end
|
@@ -14,23 +14,30 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def call
|
17
|
-
|
18
|
-
project = a['project']
|
19
|
-
task = a['task']
|
20
|
-
|
17
|
+
tasks.each do |task|
|
21
18
|
print_task(project, task)
|
22
19
|
end
|
23
20
|
end
|
24
21
|
|
25
22
|
private
|
26
23
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
def project
|
25
|
+
project_assignment['project']
|
26
|
+
end
|
27
|
+
|
28
|
+
def tasks
|
29
|
+
@tasks ||= project_assignment['task_assignments'].map { |ta| ta['task'] }
|
30
|
+
end
|
31
|
+
|
32
|
+
def project_assignment
|
33
|
+
@project_assignment ||= begin
|
34
|
+
project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
|
32
35
|
end
|
33
36
|
end
|
37
|
+
|
38
|
+
def project_assignments
|
39
|
+
@project_assignments ||= api.get_paged('users/me/project_assignments')
|
40
|
+
end
|
34
41
|
end
|
35
42
|
end
|
36
43
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Harvest
|
6
|
+
module Commands
|
7
|
+
class Track < BaseCommand
|
8
|
+
def self.command
|
9
|
+
'track harvest[:<project-id>/<task-id>]'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
'Start tracker for current or specified task. Add a relevant provider to link the time entry: E.g. `abt start harvest asana`' # rubocop:disable Layout/LineLength
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
abort 'No current/provided task' if task_id.nil?
|
18
|
+
cli.abort('No task selected') if task_id.nil?
|
19
|
+
|
20
|
+
print_task(created_time_entry['project'], created_time_entry['task'])
|
21
|
+
|
22
|
+
cli.warn 'Tracker successfully started'
|
23
|
+
rescue Abt::HttpError::HttpError => e
|
24
|
+
cli.abort 'Invalid task'
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def created_time_entry
|
30
|
+
@created_time_entry ||= create_time_entry
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_time_entry
|
34
|
+
body = {
|
35
|
+
project_id: project_id,
|
36
|
+
task_id: task_id,
|
37
|
+
user_id: config.user_id,
|
38
|
+
spent_date: Date.today.iso8601
|
39
|
+
}
|
40
|
+
|
41
|
+
if external_link_data
|
42
|
+
body.merge! external_link_data
|
43
|
+
else
|
44
|
+
cli.warn 'No external link provided'
|
45
|
+
body[:notes] ||= cli.prompt('Fill in comment (optional)')
|
46
|
+
end
|
47
|
+
|
48
|
+
api.post('time_entries', Oj.dump(body, mode: :json))
|
49
|
+
end
|
50
|
+
|
51
|
+
def external_link_data
|
52
|
+
@external_link_data ||= begin
|
53
|
+
arg_strs = cli.args.join(' ')
|
54
|
+
lines = `#{$PROGRAM_NAME} harvest-time-entry-data #{arg_strs}`.split("\n")
|
55
|
+
|
56
|
+
return if lines.empty?
|
57
|
+
|
58
|
+
# TODO: Make user choose which reference to use by printing the urls
|
59
|
+
if lines.length > 1
|
60
|
+
cli.abort('Multiple providers had harvest reference data, only one is supported at a time') # rubocop:disable Layout/LineLength
|
61
|
+
end
|
62
|
+
|
63
|
+
Oj.load(lines.first)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -24,13 +24,15 @@ module Abt
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def project_id=(value)
|
27
|
+
value = value.to_s unless value.nil?
|
27
28
|
return if project_id == value
|
28
29
|
|
29
30
|
clear_local
|
30
|
-
git['projectId'] = value
|
31
|
+
git['projectId'] = value
|
31
32
|
end
|
32
33
|
|
33
34
|
def task_id=(value)
|
35
|
+
value = value.to_s unless value.nil?
|
34
36
|
git['taskId'] = value
|
35
37
|
end
|
36
38
|
|
@@ -72,18 +74,17 @@ module Abt
|
|
72
74
|
def user_id
|
73
75
|
return git.global['userId'] unless git.global['userId'].nil?
|
74
76
|
|
75
|
-
git.global['userId'] =
|
76
|
-
'Please provide your harvest User ID.',
|
77
|
-
'To find it open "My profile" inside the harvest web UI.',
|
78
|
-
'The ID is the number part of the URL for that page.',
|
79
|
-
'',
|
80
|
-
'Enter user id'
|
81
|
-
].join("\n"))
|
77
|
+
git.global['userId'] = api.get('users/me')['id'].to_s
|
82
78
|
end
|
83
79
|
|
84
80
|
private
|
85
81
|
|
86
82
|
attr_reader :git
|
83
|
+
|
84
|
+
def api
|
85
|
+
@api ||=
|
86
|
+
Abt::Providers::Harvest::Api.new(access_token: access_token, account_id: account_id)
|
87
|
+
end
|
87
88
|
end
|
88
89
|
end
|
89
90
|
end
|
data/lib/abt/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jesper Sørensen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-01-
|
11
|
+
date: 2021-01-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-inflector
|
@@ -113,6 +113,7 @@ files:
|
|
113
113
|
- "./lib/abt/providers/harvest/commands/start.rb"
|
114
114
|
- "./lib/abt/providers/harvest/commands/stop.rb"
|
115
115
|
- "./lib/abt/providers/harvest/commands/tasks.rb"
|
116
|
+
- "./lib/abt/providers/harvest/commands/track.rb"
|
116
117
|
- "./lib/abt/providers/harvest/configuration.rb"
|
117
118
|
- "./lib/abt/version.rb"
|
118
119
|
- bin/abt
|