abt-cli 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/abt +1 -0
- data/lib/abt/cli.rb +16 -7
- data/lib/abt/cli/dialogs.rb +18 -2
- data/lib/abt/cli/io.rb +8 -6
- data/lib/abt/docs.rb +12 -5
- data/lib/abt/docs/cli.rb +1 -1
- data/lib/abt/docs/markdown.rb +1 -1
- data/lib/abt/git_config.rb +55 -49
- data/lib/abt/providers/asana/api.rb +1 -1
- data/lib/abt/providers/asana/base_command.rb +9 -4
- data/lib/abt/providers/asana/commands/current.rb +10 -4
- data/lib/abt/providers/asana/commands/finalize.rb +71 -0
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +2 -2
- data/lib/abt/providers/asana/commands/init.rb +8 -3
- data/lib/abt/providers/asana/commands/{pick_task.rb → pick.rb} +13 -6
- data/lib/abt/providers/asana/commands/projects.rb +9 -2
- data/lib/abt/providers/asana/commands/share.rb +29 -0
- data/lib/abt/providers/asana/commands/start.rb +51 -6
- data/lib/abt/providers/asana/commands/tasks.rb +4 -1
- data/lib/abt/providers/asana/configuration.rb +54 -34
- data/lib/abt/providers/harvest.rb +9 -51
- 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 +26 -0
- data/lib/abt/providers/harvest/commands/clear_global.rb +24 -0
- data/lib/abt/providers/harvest/commands/current.rb +81 -0
- data/lib/abt/providers/harvest/commands/init.rb +66 -0
- data/lib/abt/providers/harvest/commands/pick.rb +49 -0
- data/lib/abt/providers/harvest/commands/projects.rb +34 -0
- data/lib/abt/providers/harvest/commands/share.rb +29 -0
- data/lib/abt/providers/harvest/commands/start.rb +81 -0
- data/lib/abt/providers/harvest/commands/stop.rb +58 -0
- data/lib/abt/providers/harvest/commands/tasks.rb +38 -0
- data/lib/abt/providers/harvest/configuration.rb +90 -0
- data/lib/abt/version.rb +1 -1
- metadata +17 -14
- data/lib/abt/harvest_client.rb +0 -58
- data/lib/abt/providers/asana/commands/move.rb +0 -56
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c8beb7e6080443354e8ddb0d8f00106694731d931283494e1337daee0547c9a
|
4
|
+
data.tar.gz: df7840006160bbdb850bdd475496202848080ec118753be8df00f99bc5aa7c3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6f1c8df5509096dc414acff24952b57d656b064c95777f2741ada1164576c9ef6fe20bd7a13a50c8a0d30784c92d1d56ff000bf853972c5f8ee8334e69c538d
|
7
|
+
data.tar.gz: 8d3c36f885f175927efb5418547e782fcd864e81646602d8473b435222f3405f89973337b9368b718ff8413b8a8a0e714d90d2ce0e784fe62cbf02bc98bf734c
|
data/bin/abt
CHANGED
data/lib/abt/cli.rb
CHANGED
@@ -31,8 +31,10 @@ module Abt
|
|
31
31
|
process_providers
|
32
32
|
end
|
33
33
|
|
34
|
-
def print_provider_command(provider, arg_str, description)
|
35
|
-
|
34
|
+
def print_provider_command(provider, arg_str, description = nil)
|
35
|
+
command = "#{provider}:#{arg_str}"
|
36
|
+
command += " # #{description}" unless description.nil?
|
37
|
+
output.puts command
|
36
38
|
end
|
37
39
|
|
38
40
|
private
|
@@ -60,9 +62,14 @@ module Abt
|
|
60
62
|
|
61
63
|
return [] if input.nil?
|
62
64
|
|
63
|
-
input
|
64
|
-
|
65
|
+
# Exclude comment part of piped input lines
|
66
|
+
lines_without_comments = input.lines.map do |line|
|
67
|
+
line.split(' # ').first
|
65
68
|
end
|
69
|
+
|
70
|
+
# Allow multiple provider arguments on a single piped input line
|
71
|
+
joined_lines = lines_without_comments.join(' ').strip
|
72
|
+
joined_lines.split(/\s+/)
|
66
73
|
end
|
67
74
|
|
68
75
|
def process_providers
|
@@ -88,12 +95,14 @@ module Abt
|
|
88
95
|
command = provider.command_class(command_name)
|
89
96
|
return false if command.nil?
|
90
97
|
|
91
|
-
if output.isatty
|
92
|
-
warn "===== #{command_name} #{provider_name}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
|
93
|
-
end
|
98
|
+
print_command(command_name, provider_name, arg_str) if output.isatty
|
94
99
|
|
95
100
|
command.new(arg_str: arg_str, cli: self).call
|
96
101
|
true
|
97
102
|
end
|
103
|
+
|
104
|
+
def print_command(name, provider, arg_str)
|
105
|
+
warn "===== #{name} #{provider}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
|
106
|
+
end
|
98
107
|
end
|
99
108
|
end
|
data/lib/abt/cli/dialogs.rb
CHANGED
@@ -4,10 +4,26 @@ module Abt
|
|
4
4
|
class Cli
|
5
5
|
module Dialogs
|
6
6
|
def prompt(question)
|
7
|
-
print "#{question}: "
|
7
|
+
err_output.print "#{question}: "
|
8
8
|
read_user_input.strip
|
9
9
|
end
|
10
10
|
|
11
|
+
def prompt_boolean(text)
|
12
|
+
warn text
|
13
|
+
|
14
|
+
loop do
|
15
|
+
err_output.print '(y / n): '
|
16
|
+
|
17
|
+
case read_user_input.strip
|
18
|
+
when 'y', 'Y' then return true
|
19
|
+
when 'n', 'N' then return false
|
20
|
+
else
|
21
|
+
warn 'Invalid choice'
|
22
|
+
next
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
11
27
|
def prompt_choice(text, options, allow_back_option = false)
|
12
28
|
if options.one?
|
13
29
|
warn "Selected: #{options.first['name']}"
|
@@ -44,7 +60,7 @@ module Abt
|
|
44
60
|
end
|
45
61
|
|
46
62
|
def read_option_number(options_length, allow_back_option)
|
47
|
-
print "(1-#{options_length}#{allow_back_option ? ', q: back' : ''}): "
|
63
|
+
err_output.print "(1-#{options_length}#{allow_back_option ? ', q: back' : ''}): "
|
48
64
|
|
49
65
|
input = read_user_input
|
50
66
|
|
data/lib/abt/cli/io.rb
CHANGED
@@ -3,16 +3,18 @@
|
|
3
3
|
module Abt
|
4
4
|
class Cli
|
5
5
|
module Io
|
6
|
-
%i[puts print].each do |method_name|
|
7
|
-
define_method(method_name) do |*args|
|
8
|
-
output.puts(*args)
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
6
|
def warn(*args)
|
13
7
|
err_output.puts(*args)
|
14
8
|
end
|
15
9
|
|
10
|
+
def puts(*args)
|
11
|
+
output.puts(*args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def print(*args)
|
15
|
+
output.print(*args)
|
16
|
+
end
|
17
|
+
|
16
18
|
def abort(message)
|
17
19
|
raise AbortError, message
|
18
20
|
end
|
data/lib/abt/docs.rb
CHANGED
@@ -9,15 +9,22 @@ module Abt
|
|
9
9
|
class << self
|
10
10
|
def examples # rubocop:disable Metrics/MethodLength
|
11
11
|
{
|
12
|
-
'
|
13
|
-
'abt init asana harvest' =>
|
14
|
-
'abt pick
|
15
|
-
'abt
|
16
|
-
'abt
|
12
|
+
'Getting started:' => {
|
13
|
+
'abt init asana harvest' => 'Setup asana and harvest project git repo in working dir',
|
14
|
+
'abt pick harvest' => 'Pick harvest tasks, for most projects this will stay the same',
|
15
|
+
'abt pick asana | abt start harvest' => 'Pick asana task and start working',
|
16
|
+
'abt stop harvest' => 'Stop time tracker',
|
17
|
+
'abt start asana harvest' => 'Continue working, e.g. after a break',
|
18
|
+
'abt finalize asana' => 'Finalize the selected asana task'
|
17
19
|
},
|
18
20
|
'Command output can be piped, e.g.:' => {
|
19
21
|
'abt tasks asana | grep -i <name of task>' => nil,
|
20
22
|
'abt tasks asana | grep -i <name of task> | abt start' => nil
|
23
|
+
},
|
24
|
+
'Sharing configuration:' => {
|
25
|
+
'abt share asana harvest | tr "\n" " "' => 'Print current configuration',
|
26
|
+
'abt share asana harvest | tr "\n" " " | pbcopy' => 'Copy configuration (mac only)',
|
27
|
+
'abt start <shared configuration>' => 'Start a shared configuration'
|
21
28
|
}
|
22
29
|
}
|
23
30
|
end
|
data/lib/abt/docs/cli.rb
CHANGED
data/lib/abt/docs/markdown.rb
CHANGED
data/lib/abt/git_config.rb
CHANGED
@@ -2,76 +2,82 @@
|
|
2
2
|
|
3
3
|
module Abt
|
4
4
|
class GitConfig
|
5
|
-
|
6
|
-
def local(*args)
|
7
|
-
git_config(true, *args)
|
8
|
-
end
|
9
|
-
|
10
|
-
def global(*args)
|
11
|
-
git_config(false, *args)
|
12
|
-
end
|
5
|
+
attr_reader :namespace, :scope
|
13
6
|
|
14
|
-
|
15
|
-
|
7
|
+
def self.local_available?
|
8
|
+
@local_available ||= begin
|
9
|
+
status = nil
|
10
|
+
Open3.popen3('git config --local -l') do |_i, _o, _e, thread|
|
11
|
+
status = thread.value
|
12
|
+
end
|
13
|
+
status.success?
|
16
14
|
end
|
15
|
+
end
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
end
|
17
|
+
def initialize(namespace: '', scope: 'local')
|
18
|
+
@namespace = namespace
|
21
19
|
|
22
|
-
|
23
|
-
|
20
|
+
unless %w[local global].include? scope
|
21
|
+
raise ArgumentError, 'scope must be "local" or "global"'
|
24
22
|
end
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
end
|
24
|
+
@scope = scope
|
25
|
+
end
|
29
26
|
|
30
|
-
|
27
|
+
def [](key)
|
28
|
+
get(key)
|
29
|
+
end
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
def []=(key, value)
|
32
|
+
set(key, value)
|
33
|
+
end
|
35
34
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
def local
|
36
|
+
@local ||= begin
|
37
|
+
if scope == 'local'
|
38
|
+
self
|
40
39
|
else
|
41
|
-
|
42
|
-
git_value.empty? ? nil : git_value
|
40
|
+
self.class.new(namespace: namespace, scope: 'local')
|
43
41
|
end
|
44
42
|
end
|
43
|
+
end
|
45
44
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
45
|
+
def global
|
46
|
+
@global ||= begin
|
47
|
+
if scope == 'global'
|
48
|
+
self
|
49
|
+
else
|
50
|
+
self.class.new(namespace: namespace, scope: 'global')
|
51
|
+
end
|
51
52
|
end
|
53
|
+
end
|
52
54
|
|
53
|
-
|
54
|
-
value = git_config(local, key)
|
55
|
+
private
|
55
56
|
|
56
|
-
|
57
|
+
def key_with_namespace(key)
|
58
|
+
namespace.empty? ? key : "#{namespace}.#{key}"
|
59
|
+
end
|
57
60
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
TXT
|
61
|
+
def get(key)
|
62
|
+
if scope == 'local' && !self.class.local_available?
|
63
|
+
raise StandardError, 'Local configuration is not available outside a git repository'
|
64
|
+
end
|
63
65
|
|
64
|
-
|
66
|
+
git_value = `git config --#{scope} --get #{key_with_namespace(key).inspect}`.strip
|
67
|
+
git_value.empty? ? nil : git_value
|
68
|
+
end
|
65
69
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
git_config(local, key, new_value)
|
70
|
-
end
|
70
|
+
def set(key, value)
|
71
|
+
if scope == 'local' && !self.class.local_available?
|
72
|
+
raise StandardError, 'Local configuration is not available outside a git repository'
|
71
73
|
end
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
+
if value.nil? || value.empty?
|
76
|
+
`git config --#{scope} --unset #{key_with_namespace(key).inspect}`
|
77
|
+
nil
|
78
|
+
else
|
79
|
+
`git config --#{scope} --replace-all #{key_with_namespace(key).inspect} #{value.inspect}`
|
80
|
+
value
|
75
81
|
end
|
76
82
|
end
|
77
83
|
end
|
@@ -20,19 +20,24 @@ module Abt
|
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
+
def same_args_as_config?
|
24
|
+
project_gid == config.project_gid && task_gid == config.task_gid
|
25
|
+
end
|
26
|
+
|
23
27
|
def print_project(project)
|
24
28
|
cli.print_provider_command('asana', project['gid'], project['name'])
|
29
|
+
cli.warn project['permalink_url'] if project.key?('permalink_url') && cli.output.isatty
|
25
30
|
end
|
26
31
|
|
27
32
|
def print_task(project, task)
|
33
|
+
project = { 'gid' => project } if project.is_a?(String)
|
28
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
|
29
36
|
end
|
30
37
|
|
31
38
|
def use_current_args
|
32
|
-
@project_gid =
|
33
|
-
@
|
34
|
-
@task_gid = Abt::GitConfig.local('abt.asana.taskGid').to_s
|
35
|
-
@task_gid = nil if task_gid.empty?
|
39
|
+
@project_gid = config.project_gid
|
40
|
+
@task_gid = config.task_gid
|
36
41
|
end
|
37
42
|
|
38
43
|
def use_arg_str(arg_str)
|
@@ -14,7 +14,7 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def call
|
17
|
-
if
|
17
|
+
if same_args_as_config? || !config.local_available?
|
18
18
|
show_current_configuration
|
19
19
|
else
|
20
20
|
cli.warn 'Updating configuration'
|
@@ -43,7 +43,7 @@ module Abt
|
|
43
43
|
config.task_gid = nil
|
44
44
|
else
|
45
45
|
ensure_task_is_valid!
|
46
|
-
config.task_gid task_gid
|
46
|
+
config.task_gid = task_gid
|
47
47
|
|
48
48
|
print_task(project, task)
|
49
49
|
end
|
@@ -58,11 +58,17 @@ module Abt
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def project
|
61
|
-
@project ||=
|
61
|
+
@project ||= begin
|
62
|
+
cli.warn 'Fetching project...'
|
63
|
+
api.get("projects/#{project_gid}", opt_fields: 'name,permalink_url')
|
64
|
+
end
|
62
65
|
end
|
63
66
|
|
64
67
|
def task
|
65
|
-
@task ||=
|
68
|
+
@task ||= begin
|
69
|
+
cli.warn 'Fetching task...'
|
70
|
+
api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url')
|
71
|
+
end
|
66
72
|
end
|
67
73
|
end
|
68
74
|
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
|