abt-cli 0.0.22 → 0.0.27
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 +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
|