abt-cli 0.0.3 → 0.0.4
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 +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
|