abt-cli 0.0.11 → 0.0.16
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 -7
- data/lib/abt.rb +12 -3
- data/lib/abt/cli.rb +91 -53
- data/lib/abt/cli/arguments_parser.rb +70 -0
- data/lib/abt/cli/base_command.rb +61 -0
- data/lib/abt/cli/prompt.rb +124 -0
- data/lib/abt/docs.rb +24 -18
- data/lib/abt/docs/cli.rb +42 -11
- data/lib/abt/docs/markdown.rb +36 -10
- data/lib/abt/git_config.rb +34 -19
- data/lib/abt/helpers.rb +1 -1
- data/lib/abt/providers/asana/base_command.rb +24 -13
- data/lib/abt/providers/asana/commands/add.rb +75 -0
- data/lib/abt/providers/asana/commands/branch_name.rb +44 -0
- data/lib/abt/providers/asana/commands/clear.rb +17 -6
- data/lib/abt/providers/asana/commands/current.rb +6 -6
- data/lib/abt/providers/asana/commands/finalize.rb +4 -4
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +4 -3
- data/lib/abt/providers/asana/commands/init.rb +5 -5
- data/lib/abt/providers/asana/commands/pick.rb +16 -7
- data/lib/abt/providers/asana/commands/projects.rb +3 -3
- data/lib/abt/providers/asana/commands/share.rb +8 -8
- data/lib/abt/providers/asana/commands/start.rb +15 -9
- data/lib/abt/providers/asana/commands/tasks.rb +5 -3
- data/lib/abt/providers/asana/configuration.rb +8 -16
- data/lib/abt/providers/devops/api.rb +32 -2
- data/lib/abt/providers/devops/base_command.rb +32 -16
- data/lib/abt/providers/devops/commands/boards.rb +36 -0
- data/lib/abt/providers/devops/commands/branch_name.rb +45 -0
- data/lib/abt/providers/devops/commands/clear.rb +17 -6
- data/lib/abt/providers/devops/commands/current.rb +6 -10
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +5 -3
- data/lib/abt/providers/devops/commands/init.rb +5 -5
- data/lib/abt/providers/devops/commands/pick.rb +29 -20
- data/lib/abt/providers/devops/commands/share.rb +7 -13
- data/lib/abt/providers/devops/commands/work-items.rb +46 -0
- data/lib/abt/providers/devops/configuration.rb +7 -15
- data/lib/abt/providers/git.rb +19 -0
- data/lib/abt/providers/git/commands/branch.rb +74 -0
- data/lib/abt/providers/harvest/base_command.rb +24 -13
- data/lib/abt/providers/harvest/commands/clear.rb +17 -6
- data/lib/abt/providers/harvest/commands/current.rb +6 -6
- data/lib/abt/providers/harvest/commands/init.rb +5 -5
- data/lib/abt/providers/harvest/commands/pick.rb +15 -6
- data/lib/abt/providers/harvest/commands/projects.rb +3 -3
- data/lib/abt/providers/harvest/commands/share.rb +5 -5
- data/lib/abt/providers/harvest/commands/start.rb +6 -44
- data/lib/abt/providers/harvest/commands/stop.rb +3 -3
- data/lib/abt/providers/harvest/commands/tasks.rb +5 -3
- data/lib/abt/providers/harvest/commands/track.rb +50 -13
- data/lib/abt/providers/harvest/configuration.rb +7 -13
- data/lib/abt/version.rb +1 -1
- metadata +12 -7
- data/lib/abt/cli/dialogs.rb +0 -86
- data/lib/abt/cli/io.rb +0 -23
- data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
- data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
- data/lib/abt/providers/harvest/commands/clear_global.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '09c4dedacc59650be8d22ae65041e78913fc4405bbb61109ef93217801bafcff'
|
4
|
+
data.tar.gz: 91f205c4db85e2686b8461c6c2b029a45e6e430c418041aa3ffae725d1ecd16b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42f9d332099fdcecdbdf538bd44119b75d643db6399d06fe578d15e5dff664af9dfec59a5ffa1f1235a1b922fa5727d52c20ed0b6a8c4fa795e1750b51f0943d
|
7
|
+
data.tar.gz: a88ffec46e7331823de6dc89fde11e442ac3c7bd577495a89dd2af20b879675de7fdafada270d1614556d193bb909892fd9622b8b3cd87f2836fab0aa94b1483
|
data/bin/abt
CHANGED
@@ -1,17 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require 'dry-inflector'
|
5
|
-
require 'faraday'
|
6
|
-
require 'oj'
|
7
|
-
require 'open3'
|
8
|
-
require 'stringio'
|
9
|
-
|
10
4
|
require_relative '../lib/abt.rb'
|
11
5
|
|
12
6
|
begin
|
13
7
|
Abt::Cli.new.perform
|
14
|
-
rescue Abt::Cli::
|
8
|
+
rescue Abt::Cli::Abort => e
|
15
9
|
abort e.message
|
16
10
|
rescue Interrupt
|
17
11
|
abort 'Aborted'
|
data/lib/abt.rb
CHANGED
@@ -1,16 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'dry-inflector'
|
4
|
+
require 'faraday'
|
5
|
+
require 'oj'
|
6
|
+
require 'open3'
|
7
|
+
require 'stringio'
|
8
|
+
require 'optparse'
|
9
|
+
|
3
10
|
Dir.glob("#{File.dirname(File.absolute_path(__FILE__))}/abt/*.rb").sort.each do |file|
|
4
11
|
require file
|
5
12
|
end
|
6
13
|
|
7
14
|
module Abt
|
8
|
-
|
15
|
+
module Providers; end
|
16
|
+
|
17
|
+
def self.schemes
|
9
18
|
Providers.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
|
10
19
|
end
|
11
20
|
|
12
|
-
def self.
|
13
|
-
const_name = Helpers.command_to_const(
|
21
|
+
def self.scheme_provider(scheme)
|
22
|
+
const_name = Helpers.command_to_const(scheme)
|
14
23
|
Providers.const_get(const_name) if Providers.const_defined?(const_name)
|
15
24
|
end
|
16
25
|
end
|
data/lib/abt/cli.rb
CHANGED
@@ -6,103 +6,141 @@ end
|
|
6
6
|
|
7
7
|
module Abt
|
8
8
|
class Cli
|
9
|
-
class
|
9
|
+
class Abort < StandardError; end
|
10
|
+
class Exit < StandardError; end
|
10
11
|
|
11
|
-
|
12
|
-
include Io
|
13
|
-
|
14
|
-
attr_reader :command, :args, :input, :output, :err_output
|
12
|
+
attr_reader :command, :scheme_arguments, :input, :output, :err_output, :prompt
|
15
13
|
|
16
14
|
def initialize(argv: ARGV, input: STDIN, output: STDOUT, err_output: STDERR)
|
17
|
-
(@command,
|
18
|
-
|
15
|
+
(@command, *remaining_args) = argv
|
19
16
|
@input = input
|
20
17
|
@output = output
|
21
18
|
@err_output = err_output
|
19
|
+
@prompt = Abt::Cli::Prompt.new(output: err_output)
|
22
20
|
|
23
|
-
@
|
21
|
+
@scheme_arguments = ArgumentsParser.new(sanitized_piped_args + remaining_args).parse
|
24
22
|
end
|
25
23
|
|
26
24
|
def perform
|
27
|
-
handle_global_commands!
|
25
|
+
return if handle_global_commands!
|
28
26
|
|
29
|
-
abort('No
|
27
|
+
abort('No scheme arguments') if scheme_arguments.empty?
|
30
28
|
|
31
|
-
|
29
|
+
process_scheme_arguments
|
32
30
|
end
|
33
31
|
|
34
|
-
def
|
35
|
-
command = "#{
|
32
|
+
def print_scheme_argument(scheme, path, description = nil)
|
33
|
+
command = "#{scheme}:#{path}"
|
36
34
|
command += " # #{description}" unless description.nil?
|
37
35
|
output.puts command
|
38
36
|
end
|
39
37
|
|
38
|
+
def warn(*args)
|
39
|
+
err_output.puts(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
def puts(*args)
|
43
|
+
output.puts(*args)
|
44
|
+
end
|
45
|
+
|
46
|
+
def print(*args)
|
47
|
+
output.print(*args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def abort(message)
|
51
|
+
raise Abort, message
|
52
|
+
end
|
53
|
+
|
54
|
+
def exit_with_message(message)
|
55
|
+
raise Exit, message
|
56
|
+
end
|
57
|
+
|
40
58
|
private
|
41
59
|
|
42
|
-
def handle_global_commands!
|
60
|
+
def handle_global_commands!
|
43
61
|
case command
|
44
62
|
when nil
|
45
63
|
warn("No command specified\n\n")
|
46
|
-
puts(Abt::Docs::Cli.
|
47
|
-
|
48
|
-
when '--help', '-h', 'help', 'commands'
|
49
|
-
puts(Abt::Docs::Cli.content)
|
50
|
-
exit
|
51
|
-
when 'help-md'
|
52
|
-
puts(Abt::Docs::Markdown.content)
|
53
|
-
exit
|
64
|
+
puts(Abt::Docs::Cli.help)
|
65
|
+
true
|
54
66
|
when '--version', '-v', 'version'
|
55
67
|
puts(Abt::VERSION)
|
56
|
-
|
68
|
+
true
|
69
|
+
when '--help', '-h', 'help'
|
70
|
+
puts(Abt::Docs::Cli.help)
|
71
|
+
true
|
72
|
+
when 'commands'
|
73
|
+
puts(Abt::Docs::Cli.commands)
|
74
|
+
true
|
75
|
+
when 'examples'
|
76
|
+
puts(Abt::Docs::Cli.examples)
|
77
|
+
true
|
78
|
+
when 'readme'
|
79
|
+
puts(Abt::Docs::Markdown.readme)
|
80
|
+
true
|
81
|
+
else
|
82
|
+
false
|
57
83
|
end
|
58
84
|
end
|
59
85
|
|
60
|
-
def
|
61
|
-
|
86
|
+
def sanitized_piped_args
|
87
|
+
return [] if input.isatty
|
62
88
|
|
63
|
-
|
89
|
+
@sanitized_piped_args ||= begin
|
90
|
+
input_string = input.read.strip
|
64
91
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
92
|
+
abort 'No input from pipe' if input_string.nil? || input_string.empty?
|
93
|
+
|
94
|
+
# Exclude comment part of piped input lines
|
95
|
+
lines_without_comments = input_string.lines.map do |line|
|
96
|
+
line.split(' # ').first
|
97
|
+
end
|
69
98
|
|
70
|
-
|
71
|
-
|
72
|
-
|
99
|
+
# Allow multiple scheme arguments on a single piped input line
|
100
|
+
# TODO: Force the user to pick a single scheme argument
|
101
|
+
joined_lines = lines_without_comments.join(' ').strip
|
102
|
+
joined_lines.split(/\s+/)
|
103
|
+
end
|
73
104
|
end
|
74
105
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
78
|
-
|
106
|
+
def process_scheme_arguments
|
107
|
+
used_schemes = []
|
108
|
+
scheme_arguments.each do |scheme_argument|
|
109
|
+
scheme = scheme_argument.scheme
|
110
|
+
path = scheme_argument.path
|
79
111
|
|
80
|
-
if
|
81
|
-
warn "Dropping command for already used
|
112
|
+
if used_schemes.include?(scheme)
|
113
|
+
warn "Dropping command for already used scheme: #{scheme_argument}"
|
82
114
|
next
|
83
115
|
end
|
84
116
|
|
85
|
-
|
86
|
-
|
117
|
+
command_class = get_command_class(scheme)
|
118
|
+
next if command_class.nil?
|
87
119
|
|
88
|
-
|
89
|
-
|
120
|
+
print_command(command, scheme_argument) if output.isatty
|
121
|
+
begin
|
122
|
+
command_class.new(path: path, cli: self, flags: scheme_argument.flags).perform
|
123
|
+
rescue Exit => e
|
124
|
+
puts e.message
|
125
|
+
end
|
90
126
|
|
91
|
-
|
92
|
-
|
93
|
-
return false if provider.nil?
|
127
|
+
used_schemes << scheme
|
128
|
+
end
|
94
129
|
|
95
|
-
|
96
|
-
|
130
|
+
return unless used_schemes.empty? && output.isatty
|
131
|
+
|
132
|
+
abort 'No providers found for command and scheme argument(s)'
|
133
|
+
end
|
97
134
|
|
98
|
-
|
135
|
+
def get_command_class(scheme)
|
136
|
+
provider = Abt.scheme_provider(scheme)
|
137
|
+
return nil if provider.nil?
|
99
138
|
|
100
|
-
|
101
|
-
true
|
139
|
+
provider.command_class(command)
|
102
140
|
end
|
103
141
|
|
104
|
-
def print_command(name,
|
105
|
-
warn "===== #{name} #{
|
142
|
+
def print_command(name, scheme_argument)
|
143
|
+
warn "===== #{name} #{scheme_argument} =====".upcase
|
106
144
|
end
|
107
145
|
end
|
108
146
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
class Cli
|
5
|
+
class ArgumentsParser
|
6
|
+
class SchemeArgument
|
7
|
+
attr_reader :scheme, :path, :flags
|
8
|
+
|
9
|
+
def initialize(scheme:, path:, flags:)
|
10
|
+
@scheme = scheme
|
11
|
+
@path = path
|
12
|
+
@flags = flags
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
str = scheme
|
17
|
+
str += ":#{path}" if path
|
18
|
+
|
19
|
+
[str, *flags].join(' ')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
class SchemeArguments < Array
|
23
|
+
def to_s
|
24
|
+
map(&:to_s).join(' -- ')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :arguments
|
29
|
+
|
30
|
+
def initialize(arguments)
|
31
|
+
@arguments = arguments
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse
|
35
|
+
result = SchemeArguments.new
|
36
|
+
rest = arguments.dup
|
37
|
+
|
38
|
+
until rest.empty?
|
39
|
+
(scheme, path) = rest.shift.split(':')
|
40
|
+
flags = take_flags(rest)
|
41
|
+
|
42
|
+
result << SchemeArgument.new(scheme: scheme, path: path, flags: flags)
|
43
|
+
end
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def take_flags(rest)
|
51
|
+
flags = []
|
52
|
+
|
53
|
+
if flag?(rest.first)
|
54
|
+
flags << rest.shift until rest.empty? || delimiter?(rest.first)
|
55
|
+
rest.shift if delimiter?(rest.first)
|
56
|
+
end
|
57
|
+
|
58
|
+
flags
|
59
|
+
end
|
60
|
+
|
61
|
+
def flag?(part)
|
62
|
+
part && part[0] == '-'
|
63
|
+
end
|
64
|
+
|
65
|
+
def delimiter?(part)
|
66
|
+
part == '--'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
class Cli
|
5
|
+
class BaseCommand
|
6
|
+
def self.usage
|
7
|
+
raise NotImplementedError, 'Command classes must implement .command'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.description
|
11
|
+
raise NotImplementedError, 'Command classes must implement .description'
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.flags
|
15
|
+
[]
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :path, :flags, :cli
|
19
|
+
|
20
|
+
def initialize(path:, flags:, cli:)
|
21
|
+
@cli = cli
|
22
|
+
@path = path
|
23
|
+
@flags = parse_flags(flags)
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform
|
27
|
+
raise NotImplementedError, 'Command classes must implement #perform'
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def parse_flags(flags)
|
33
|
+
result = {}
|
34
|
+
|
35
|
+
flag_parser.parse!(flags.dup, into: result)
|
36
|
+
|
37
|
+
cli.exit_with_message(flag_parser.help) if result[:help]
|
38
|
+
|
39
|
+
result
|
40
|
+
rescue OptionParser::InvalidOption => e
|
41
|
+
cli.abort e.message
|
42
|
+
end
|
43
|
+
|
44
|
+
def flag_parser
|
45
|
+
@flag_parser ||= OptionParser.new do |opts|
|
46
|
+
opts.banner = <<~TXT
|
47
|
+
#{self.class.description}
|
48
|
+
|
49
|
+
Usage: #{self.class.usage}
|
50
|
+
TXT
|
51
|
+
|
52
|
+
opts.on('-h', '--help')
|
53
|
+
|
54
|
+
self.class.flags.each do |(*flag)|
|
55
|
+
opts.on(*flag)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
class Cli
|
5
|
+
class Prompt
|
6
|
+
attr_reader :output
|
7
|
+
|
8
|
+
def initialize(output:)
|
9
|
+
@output = output
|
10
|
+
end
|
11
|
+
|
12
|
+
def text(question)
|
13
|
+
output.print "#{question}: "
|
14
|
+
read_user_input
|
15
|
+
end
|
16
|
+
|
17
|
+
def boolean(text)
|
18
|
+
output.puts text
|
19
|
+
|
20
|
+
loop do
|
21
|
+
output.print '(y / n): '
|
22
|
+
|
23
|
+
case read_user_input
|
24
|
+
when 'y', 'Y' then return true
|
25
|
+
when 'n', 'N' then return false
|
26
|
+
else
|
27
|
+
output.puts 'Invalid choice'
|
28
|
+
next
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def choice(text, options, nil_option = false)
|
34
|
+
output.puts "#{text}:"
|
35
|
+
|
36
|
+
if options.length.zero?
|
37
|
+
raise Abort, 'No available options' unless nil_option
|
38
|
+
|
39
|
+
output.puts 'No available options'
|
40
|
+
return nil
|
41
|
+
end
|
42
|
+
|
43
|
+
print_options(options)
|
44
|
+
select_options(options, nil_option)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def print_options(options)
|
50
|
+
options.each_with_index do |option, index|
|
51
|
+
output.puts "(#{index + 1}) #{option['name']}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def select_options(options, nil_option)
|
56
|
+
loop do
|
57
|
+
number = read_option_number(options.length, nil_option)
|
58
|
+
if number.nil?
|
59
|
+
return nil if nil_option
|
60
|
+
|
61
|
+
next
|
62
|
+
end
|
63
|
+
|
64
|
+
option = options[number - 1]
|
65
|
+
|
66
|
+
output.puts "Selected: (#{number}) #{option['name']}"
|
67
|
+
return option
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def read_option_number(options_length, nil_option)
|
72
|
+
output.print '('
|
73
|
+
output.print options_length > 1 ? "1-#{options_length}" : '1'
|
74
|
+
output.print nil_option_string(nil_option)
|
75
|
+
output.print '): '
|
76
|
+
|
77
|
+
input = read_user_input
|
78
|
+
|
79
|
+
return nil if nil_option && input == nil_option_character(nil_option)
|
80
|
+
|
81
|
+
option_number = input.to_i
|
82
|
+
if option_number <= 0 || option_number > options_length
|
83
|
+
output.puts 'Invalid selection'
|
84
|
+
return nil
|
85
|
+
end
|
86
|
+
|
87
|
+
option_number
|
88
|
+
end
|
89
|
+
|
90
|
+
def nil_option_string(nil_option)
|
91
|
+
return '' unless nil_option
|
92
|
+
|
93
|
+
", #{nil_option_character(nil_option)}: #{nil_option_description(nil_option)}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def nil_option_character(nil_option)
|
97
|
+
return 'q' if nil_option == true
|
98
|
+
|
99
|
+
nil_option[0]
|
100
|
+
end
|
101
|
+
|
102
|
+
def nil_option_description(nil_option)
|
103
|
+
return 'back' if nil_option == true
|
104
|
+
return nil_option if nil_option.is_a?(String)
|
105
|
+
|
106
|
+
nil_option[1]
|
107
|
+
end
|
108
|
+
|
109
|
+
def read_user_input
|
110
|
+
open(tty_path, &:gets).strip # rubocop:disable Security/Open
|
111
|
+
end
|
112
|
+
|
113
|
+
def tty_path
|
114
|
+
@tty_path ||= begin
|
115
|
+
candidates = ['/dev/tty', 'CON:'] # Unix: '/dev/tty', Windows: 'CON:'
|
116
|
+
selected = candidates.find { |candidate| File.exist?(candidate) }
|
117
|
+
raise Abort, 'Unable to prompt for user input' if selected.nil?
|
118
|
+
|
119
|
+
selected
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|