abt-cli 0.0.22 → 0.0.27
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/abt +2 -2
- data/lib/abt.rb +5 -0
- data/lib/abt/cli.rb +28 -9
- data/lib/abt/cli/prompt.rb +37 -53
- data/lib/abt/directory_config.rb +25 -0
- data/lib/abt/docs.rb +10 -6
- data/lib/abt/docs/markdown.rb +5 -2
- data/lib/abt/helpers.rb +26 -8
- data/lib/abt/providers/asana.rb +1 -0
- data/lib/abt/providers/asana/base_command.rb +37 -3
- data/lib/abt/providers/asana/commands/add.rb +0 -4
- data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
- data/lib/abt/providers/asana/commands/current.rb +1 -20
- data/lib/abt/providers/asana/commands/finalize.rb +6 -2
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +7 -5
- data/lib/abt/providers/asana/commands/pick.rb +12 -46
- data/lib/abt/providers/asana/commands/start.rb +9 -3
- data/lib/abt/providers/asana/commands/tasks.rb +2 -7
- data/lib/abt/providers/asana/configuration.rb +28 -12
- data/lib/abt/providers/asana/path.rb +1 -1
- data/lib/abt/providers/asana/services/project_picker.rb +54 -0
- data/lib/abt/providers/asana/services/task_picker.rb +83 -0
- data/lib/abt/providers/devops.rb +1 -0
- data/lib/abt/providers/devops/api.rb +10 -0
- data/lib/abt/providers/devops/base_command.rb +38 -14
- data/lib/abt/providers/devops/commands/boards.rb +1 -2
- data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
- data/lib/abt/providers/devops/commands/current.rb +1 -21
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +16 -20
- data/lib/abt/providers/devops/commands/pick.rb +18 -39
- data/lib/abt/providers/devops/commands/work_items.rb +3 -6
- data/lib/abt/providers/devops/configuration.rb +10 -14
- data/lib/abt/providers/devops/path.rb +4 -4
- data/lib/abt/providers/devops/services/board_picker.rb +54 -0
- data/lib/abt/providers/devops/services/project_picker.rb +79 -0
- data/lib/abt/providers/devops/services/work_item_picker.rb +93 -0
- data/lib/abt/providers/git/commands/branch.rb +7 -3
- data/lib/abt/providers/harvest.rb +1 -0
- data/lib/abt/providers/harvest/base_command.rb +49 -3
- data/lib/abt/providers/harvest/commands/current.rb +1 -30
- data/lib/abt/providers/harvest/commands/pick.rb +12 -23
- data/lib/abt/providers/harvest/commands/projects.rb +0 -5
- data/lib/abt/providers/harvest/commands/tasks.rb +1 -16
- data/lib/abt/providers/harvest/commands/track.rb +33 -19
- data/lib/abt/providers/harvest/configuration.rb +1 -1
- data/lib/abt/providers/harvest/path.rb +1 -1
- data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
- data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
- data/lib/abt/version.rb +1 -1
- metadata +10 -5
- data/lib/abt/providers/asana/commands/init.rb +0 -42
- data/lib/abt/providers/devops/commands/init.rb +0 -76
- data/lib/abt/providers/harvest/commands/init.rb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0bffe51f05670b4fcbf1bf2a023c354e26a387b0a74fd34751ef180b1f619c62
|
4
|
+
data.tar.gz: 7e5c476b8e1b803adfbb92590676c883e66a6ac74b5111e83aed9f4d6b9feb8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9dba1d7966f28966e5fe8d33fd3b380e823f707536b86e1f21a82bfdb74e20ba4d76b2c08e1fb9da9b7f1dc1d74ea6a787979b6ae86ca59b3ce9305964f7a063
|
7
|
+
data.tar.gz: 33b61911f3018b0e51ece32264c98f8327af60fcfb94bb59e4d210171bf8562fb885c120be565f79a827dd7f35c72d4d91acf48aa8497079107e120d8e4535bf
|
data/bin/abt
CHANGED
data/lib/abt.rb
CHANGED
@@ -6,6 +6,7 @@ require "oj"
|
|
6
6
|
require "open3"
|
7
7
|
require "stringio"
|
8
8
|
require "optparse"
|
9
|
+
require "yaml"
|
9
10
|
|
10
11
|
Dir.glob("#{File.dirname(File.absolute_path(__FILE__))}/abt/*.rb").sort.each do |file|
|
11
12
|
require file
|
@@ -22,4 +23,8 @@ module Abt
|
|
22
23
|
const_name = Helpers.command_to_const(scheme)
|
23
24
|
Providers.const_get(const_name) if Providers.const_defined?(const_name)
|
24
25
|
end
|
26
|
+
|
27
|
+
def self.directory_config
|
28
|
+
@directory_config ||= Abt::DirectoryConfig.new
|
29
|
+
end
|
25
30
|
end
|
data/lib/abt/cli.rb
CHANGED
@@ -5,20 +5,19 @@ Dir.glob("#{File.expand_path(__dir__)}/cli/*.rb").sort.each do |file|
|
|
5
5
|
end
|
6
6
|
|
7
7
|
module Abt
|
8
|
-
class Cli
|
8
|
+
class Cli # rubocop:disable Metrics/ClassLength
|
9
9
|
class Abort < StandardError; end
|
10
10
|
|
11
11
|
class Exit < StandardError; end
|
12
12
|
|
13
|
-
attr_reader :command, :
|
13
|
+
attr_reader :command, :remaining_args, :input, :output, :err_output, :prompt
|
14
14
|
|
15
15
|
def initialize(argv: ARGV, input: $stdin, output: $stdout, err_output: $stderr)
|
16
|
-
(@command,
|
16
|
+
(@command, *@remaining_args) = argv
|
17
17
|
@input = input
|
18
18
|
@output = output
|
19
19
|
@err_output = err_output
|
20
20
|
@prompt = Abt::Cli::Prompt.new(output: err_output)
|
21
|
-
@aris = ArgumentsParser.new(sanitized_piped_args + remaining_args).parse
|
22
21
|
end
|
23
22
|
|
24
23
|
def perform
|
@@ -27,11 +26,10 @@ module Abt
|
|
27
26
|
@command = "help"
|
28
27
|
end
|
29
28
|
|
30
|
-
if
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
end
|
29
|
+
return process_alias if alias?
|
30
|
+
return process_global_command if global_command?
|
31
|
+
|
32
|
+
process_aris
|
35
33
|
end
|
36
34
|
|
37
35
|
def print_ari(scheme, path, description = nil)
|
@@ -60,8 +58,29 @@ module Abt
|
|
60
58
|
raise Exit, message
|
61
59
|
end
|
62
60
|
|
61
|
+
def aris
|
62
|
+
@aris ||= ArgumentsParser.new(sanitized_piped_args + remaining_args).parse
|
63
|
+
end
|
64
|
+
|
63
65
|
private
|
64
66
|
|
67
|
+
def alias?
|
68
|
+
command[0] == "@"
|
69
|
+
end
|
70
|
+
|
71
|
+
def process_alias
|
72
|
+
matching_alias = Abt.directory_config.dig("aliases", command[1..-1])
|
73
|
+
|
74
|
+
abort("No such alias #{command}") if matching_alias.nil?
|
75
|
+
|
76
|
+
with_args = matching_alias.sub("$@", remaining_args.join(" "))
|
77
|
+
with_program_name = with_args.gsub("$0", $PROGRAM_NAME).strip
|
78
|
+
humanized = with_args.gsub("$0", "abt").strip
|
79
|
+
|
80
|
+
warn(humanized)
|
81
|
+
system(with_program_name)
|
82
|
+
end
|
83
|
+
|
65
84
|
def global_command?
|
66
85
|
return true if aris.empty?
|
67
86
|
return true if aris.first.scheme.nil?
|
data/lib/abt/cli/prompt.rb
CHANGED
@@ -11,22 +11,23 @@ module Abt
|
|
11
11
|
|
12
12
|
def text(question)
|
13
13
|
output.print("#{question.strip}: ")
|
14
|
-
read_user_input
|
14
|
+
Abt::Helpers.read_user_input
|
15
15
|
end
|
16
16
|
|
17
|
-
def boolean(text)
|
18
|
-
|
17
|
+
def boolean(text, default: nil)
|
18
|
+
choices = [default == true ? "Y" : "y",
|
19
|
+
default == false ? "N" : "n"].join("/")
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
21
|
+
output.print("#{text} (#{choices}): ")
|
22
|
+
|
23
|
+
input = Abt::Helpers.read_user_input.downcase
|
24
|
+
|
25
|
+
return true if input == "y"
|
26
|
+
return false if input == "n"
|
27
|
+
return default if input.empty? && !default.nil?
|
28
|
+
|
29
|
+
output.puts "Invalid choice"
|
30
|
+
boolean(text, default: default)
|
30
31
|
end
|
31
32
|
|
32
33
|
def choice(text, options, nil_option: false)
|
@@ -40,7 +41,7 @@ module Abt
|
|
40
41
|
end
|
41
42
|
|
42
43
|
print_options(options)
|
43
|
-
|
44
|
+
select_option(options, nil_option)
|
44
45
|
end
|
45
46
|
|
46
47
|
def search(text, options)
|
@@ -60,40 +61,37 @@ module Abt
|
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
63
|
-
def
|
64
|
-
|
65
|
-
number = read_option_number(options.length, nil_option)
|
66
|
-
if number.nil?
|
67
|
-
return nil if nil_option
|
68
|
-
|
69
|
-
next
|
70
|
-
end
|
64
|
+
def select_option(options, nil_option)
|
65
|
+
number = prompt_valid_option_number(options, nil_option)
|
71
66
|
|
72
|
-
|
67
|
+
return nil if number.nil?
|
73
68
|
|
74
|
-
|
75
|
-
|
76
|
-
|
69
|
+
option = options[number - 1]
|
70
|
+
output.puts "Selected: (#{number}) #{option['name']}"
|
71
|
+
option
|
77
72
|
end
|
78
73
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
82
|
-
str += nil_option_string(nil_option)
|
83
|
-
str += "): "
|
84
|
-
output.print(str)
|
85
|
-
|
86
|
-
input = read_user_input
|
74
|
+
def prompt_valid_option_number(options, nil_option)
|
75
|
+
output.print(options_info(options, nil_option))
|
76
|
+
input = Abt::Helpers.read_user_input
|
87
77
|
|
88
78
|
return nil if nil_option && input == nil_option_character(nil_option)
|
89
79
|
|
90
80
|
option_number = input.to_i
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
81
|
+
return option_number if (1..options.length).cover?(option_number)
|
82
|
+
|
83
|
+
output.puts "Invalid selection"
|
84
|
+
|
85
|
+
# Prompt again if the selection was invalid
|
86
|
+
prompt_valid_option_number(options, nil_option)
|
87
|
+
end
|
95
88
|
|
96
|
-
|
89
|
+
def options_info(options, nil_option)
|
90
|
+
str = "("
|
91
|
+
str += options.length > 1 ? "1-#{options.length}" : "1"
|
92
|
+
str += nil_option_string(nil_option)
|
93
|
+
str += "): "
|
94
|
+
str
|
97
95
|
end
|
98
96
|
|
99
97
|
def nil_option_string(nil_option)
|
@@ -115,10 +113,6 @@ module Abt
|
|
115
113
|
nil_option[1]
|
116
114
|
end
|
117
115
|
|
118
|
-
def read_user_input
|
119
|
-
open(tty_path, &:gets).strip # rubocop:disable Security/Open
|
120
|
-
end
|
121
|
-
|
122
116
|
def get_search_result(options)
|
123
117
|
matches = matches_for_string(text("Enter search"), options)
|
124
118
|
if matches.empty?
|
@@ -141,16 +135,6 @@ module Abt
|
|
141
135
|
def sanitize_string(string)
|
142
136
|
string.downcase.gsub(/[^\w]/, "")
|
143
137
|
end
|
144
|
-
|
145
|
-
def tty_path
|
146
|
-
@tty_path ||= begin
|
147
|
-
candidates = ["/dev/tty", "CON:"] # Unix: '/dev/tty', Windows: 'CON:'
|
148
|
-
selected = candidates.find { |candidate| File.exist?(candidate) }
|
149
|
-
raise Abort, "Unable to prompt for user input" if selected.nil?
|
150
|
-
|
151
|
-
selected
|
152
|
-
end
|
153
|
-
end
|
154
138
|
end
|
155
139
|
end
|
156
140
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
class DirectoryConfig < Hash
|
5
|
+
def initialize
|
6
|
+
super
|
7
|
+
merge!(YAML.load_file(config_file_path)) if config_file_path
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def config_file_path
|
13
|
+
dir = Dir.pwd
|
14
|
+
|
15
|
+
until File.exist?(File.join(dir, ".abt.yml"))
|
16
|
+
next_dir = File.expand_path("..", dir)
|
17
|
+
return if next_dir == dir
|
18
|
+
|
19
|
+
dir = next_dir
|
20
|
+
end
|
21
|
+
|
22
|
+
File.join(dir, ".abt.yml")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/abt/docs.rb
CHANGED
@@ -10,11 +10,10 @@ module Abt
|
|
10
10
|
def basic_examples
|
11
11
|
{
|
12
12
|
"Getting started:" => {
|
13
|
-
"abt init asana harvest" => "Setup asana and harvest project for local git repo",
|
14
13
|
"abt pick harvest" => "Pick harvest task. This will likely stay the same throughout the project",
|
15
14
|
"abt pick asana | abt start harvest" => "Pick asana task and start tracking time",
|
16
15
|
"abt stop harvest" => "Stop time tracker",
|
17
|
-
"abt
|
16
|
+
"abt track asana harvest" => "Continue tracking time, e.g., after a break",
|
18
17
|
"abt finalize asana" => "Finalize the selected asana task"
|
19
18
|
}
|
20
19
|
}
|
@@ -31,10 +30,15 @@ module Abt
|
|
31
30
|
"abt tasks asana | grep -i <name of task> | abt start" => nil
|
32
31
|
},
|
33
32
|
"Sharing ARIs:" => {
|
34
|
-
|
35
|
-
|
36
|
-
"abt
|
37
|
-
"abt current <ARIs from coworker> | abt
|
33
|
+
"abt share" => "Print current asana and harvest ARIs on a single line",
|
34
|
+
"abt share | pbcopy" => "Copy ARIs to clipboard (mac only)",
|
35
|
+
"abt track <ARIs from coworker>" => "Start tracking on the task your coworker shared with you",
|
36
|
+
"abt current <ARIs from coworker> | abt track" => "Set task as current, then start tracking"
|
37
|
+
},
|
38
|
+
"One-off tracking on any project": {
|
39
|
+
"abt pick asana -dc -- harvest -dc | abt track" =>
|
40
|
+
"Find a track any task on any project, without reusing/affecting previous settings",
|
41
|
+
"abt pick asana harvest | abt track" => "Can be used instead of the above when outside a git repo"
|
38
42
|
},
|
39
43
|
"Flags:" => {
|
40
44
|
'abt start harvest -c "comment"' => "Add command flags after ARIs",
|
data/lib/abt/docs/markdown.rb
CHANGED
@@ -50,8 +50,7 @@ module Abt
|
|
50
50
|
def example_commands
|
51
51
|
lines = []
|
52
52
|
|
53
|
-
|
54
|
-
examples.each_with_index do |(title, commands), index|
|
53
|
+
complete_examples.each_with_index do |(title, commands), index|
|
55
54
|
lines << "" unless index.zero?
|
56
55
|
lines << title
|
57
56
|
|
@@ -84,6 +83,10 @@ module Abt
|
|
84
83
|
lines.join("\n")
|
85
84
|
end
|
86
85
|
|
86
|
+
def complete_examples
|
87
|
+
Docs.basic_examples.merge(Docs.extended_examples)
|
88
|
+
end
|
89
|
+
|
87
90
|
def inflector
|
88
91
|
Dry::Inflector.new
|
89
92
|
end
|
data/lib/abt/helpers.rb
CHANGED
@@ -2,15 +2,33 @@
|
|
2
2
|
|
3
3
|
module Abt
|
4
4
|
module Helpers
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
class << self
|
6
|
+
def const_to_command(string)
|
7
|
+
string = string.to_s.dup
|
8
|
+
string[0] = string[0].downcase
|
9
|
+
string.gsub(/([A-Z])/, '-\1').downcase
|
10
|
+
end
|
11
|
+
|
12
|
+
def command_to_const(string)
|
13
|
+
inflector = Dry::Inflector.new
|
14
|
+
inflector.camelize(inflector.underscore(string))
|
15
|
+
end
|
16
|
+
|
17
|
+
def read_user_input
|
18
|
+
open(tty_path, &:gets).strip # rubocop:disable Security/Open
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def tty_path
|
24
|
+
@tty_path ||= begin
|
25
|
+
candidates = ["/dev/tty", "CON:"] # Unix: '/dev/tty', Windows: 'CON:'
|
26
|
+
selected = candidates.find { |candidate| File.exist?(candidate) }
|
27
|
+
raise Abort, "Unable to prompt for user input" if selected.nil?
|
10
28
|
|
11
|
-
|
12
|
-
|
13
|
-
|
29
|
+
selected
|
30
|
+
end
|
31
|
+
end
|
14
32
|
end
|
15
33
|
end
|
16
34
|
end
|
data/lib/abt/providers/asana.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
Dir.glob("#{File.expand_path(__dir__)}/asana/*.rb").sort.each { |file| require file }
|
4
4
|
Dir.glob("#{File.expand_path(__dir__)}/asana/commands/*.rb").sort.each { |file| require file }
|
5
|
+
Dir.glob("#{File.expand_path(__dir__)}/asana/services/*.rb").sort.each { |file| require file }
|
5
6
|
|
6
7
|
module Abt
|
7
8
|
module Providers
|
@@ -20,13 +20,47 @@ module Abt
|
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
+
def require_local_config!
|
24
|
+
abort("Must be run inside a git repository") unless config.local_available?
|
25
|
+
end
|
26
|
+
|
23
27
|
def require_project!
|
24
|
-
abort("No current/specified project. Did you
|
28
|
+
abort("No current/specified project. Did you forget to run `pick`?") if project_gid.nil?
|
25
29
|
end
|
26
30
|
|
27
31
|
def require_task!
|
28
|
-
|
29
|
-
abort("No current/specified task. Did you
|
32
|
+
require_project!
|
33
|
+
abort("No current/specified task. Did you forget to run `pick`?") if task_gid.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def prompt_project!
|
37
|
+
result = Services::ProjectPicker.call(cli: cli, config: config)
|
38
|
+
@path = result.path
|
39
|
+
@project = result.project
|
40
|
+
end
|
41
|
+
|
42
|
+
def prompt_task!
|
43
|
+
result = Services::TaskPicker.call(cli: cli, path: path, config: config, project: project)
|
44
|
+
@path = result.path
|
45
|
+
@task = result.task
|
46
|
+
end
|
47
|
+
|
48
|
+
def task
|
49
|
+
@task ||= begin
|
50
|
+
warn("Fetching task...")
|
51
|
+
api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
|
52
|
+
rescue Abt::HttpError::NotFoundError
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def project
|
58
|
+
@project ||= begin
|
59
|
+
warn("Fetching project...")
|
60
|
+
api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
|
61
|
+
rescue Abt::HttpError::NotFoundError
|
62
|
+
nil
|
63
|
+
end
|
30
64
|
end
|
31
65
|
|
32
66
|
def print_project(project)
|
@@ -56,10 +56,6 @@ module Abt
|
|
56
56
|
@notes ||= cli.prompt.text("Enter task notes")
|
57
57
|
end
|
58
58
|
|
59
|
-
def project
|
60
|
-
@project ||= api.get("projects/#{project_gid}", opt_fields: "name")
|
61
|
-
end
|
62
|
-
|
63
59
|
def section
|
64
60
|
@section ||= cli.prompt.choice("Add to section?", sections,
|
65
61
|
nil_option: ["q", "Don't add to section"])
|
@@ -28,19 +28,6 @@ module Abt
|
|
28
28
|
|
29
29
|
def ensure_current_is_valid!
|
30
30
|
abort("Invalid task gid: #{task_gid}") if task.nil?
|
31
|
-
|
32
|
-
return if task["memberships"].any? { |m| m.dig("project", "gid") == project_gid }
|
33
|
-
|
34
|
-
abort("Invalid or unmatching project gid: #{project_gid}")
|
35
|
-
end
|
36
|
-
|
37
|
-
def task
|
38
|
-
@task ||= begin
|
39
|
-
warn("Fetching task...")
|
40
|
-
api.get("tasks/#{task_gid}", opt_fields: "name,memberships.project")
|
41
|
-
rescue Abt::HttpError::NotFoundError
|
42
|
-
nil
|
43
|
-
end
|
44
31
|
end
|
45
32
|
end
|
46
33
|
end
|