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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -7
  3. data/lib/abt.rb +12 -3
  4. data/lib/abt/cli.rb +91 -53
  5. data/lib/abt/cli/arguments_parser.rb +70 -0
  6. data/lib/abt/cli/base_command.rb +61 -0
  7. data/lib/abt/cli/prompt.rb +124 -0
  8. data/lib/abt/docs.rb +24 -18
  9. data/lib/abt/docs/cli.rb +42 -11
  10. data/lib/abt/docs/markdown.rb +36 -10
  11. data/lib/abt/git_config.rb +34 -19
  12. data/lib/abt/helpers.rb +1 -1
  13. data/lib/abt/providers/asana/base_command.rb +24 -13
  14. data/lib/abt/providers/asana/commands/add.rb +75 -0
  15. data/lib/abt/providers/asana/commands/branch_name.rb +44 -0
  16. data/lib/abt/providers/asana/commands/clear.rb +17 -6
  17. data/lib/abt/providers/asana/commands/current.rb +6 -6
  18. data/lib/abt/providers/asana/commands/finalize.rb +4 -4
  19. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +4 -3
  20. data/lib/abt/providers/asana/commands/init.rb +5 -5
  21. data/lib/abt/providers/asana/commands/pick.rb +16 -7
  22. data/lib/abt/providers/asana/commands/projects.rb +3 -3
  23. data/lib/abt/providers/asana/commands/share.rb +8 -8
  24. data/lib/abt/providers/asana/commands/start.rb +15 -9
  25. data/lib/abt/providers/asana/commands/tasks.rb +5 -3
  26. data/lib/abt/providers/asana/configuration.rb +8 -16
  27. data/lib/abt/providers/devops/api.rb +32 -2
  28. data/lib/abt/providers/devops/base_command.rb +32 -16
  29. data/lib/abt/providers/devops/commands/boards.rb +36 -0
  30. data/lib/abt/providers/devops/commands/branch_name.rb +45 -0
  31. data/lib/abt/providers/devops/commands/clear.rb +17 -6
  32. data/lib/abt/providers/devops/commands/current.rb +6 -10
  33. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +5 -3
  34. data/lib/abt/providers/devops/commands/init.rb +5 -5
  35. data/lib/abt/providers/devops/commands/pick.rb +29 -20
  36. data/lib/abt/providers/devops/commands/share.rb +7 -13
  37. data/lib/abt/providers/devops/commands/work-items.rb +46 -0
  38. data/lib/abt/providers/devops/configuration.rb +7 -15
  39. data/lib/abt/providers/git.rb +19 -0
  40. data/lib/abt/providers/git/commands/branch.rb +74 -0
  41. data/lib/abt/providers/harvest/base_command.rb +24 -13
  42. data/lib/abt/providers/harvest/commands/clear.rb +17 -6
  43. data/lib/abt/providers/harvest/commands/current.rb +6 -6
  44. data/lib/abt/providers/harvest/commands/init.rb +5 -5
  45. data/lib/abt/providers/harvest/commands/pick.rb +15 -6
  46. data/lib/abt/providers/harvest/commands/projects.rb +3 -3
  47. data/lib/abt/providers/harvest/commands/share.rb +5 -5
  48. data/lib/abt/providers/harvest/commands/start.rb +6 -44
  49. data/lib/abt/providers/harvest/commands/stop.rb +3 -3
  50. data/lib/abt/providers/harvest/commands/tasks.rb +5 -3
  51. data/lib/abt/providers/harvest/commands/track.rb +50 -13
  52. data/lib/abt/providers/harvest/configuration.rb +7 -13
  53. data/lib/abt/version.rb +1 -1
  54. metadata +12 -7
  55. data/lib/abt/cli/dialogs.rb +0 -86
  56. data/lib/abt/cli/io.rb +0 -23
  57. data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
  58. data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
  59. 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: 0cc34f7579a887a49ae83db56c8b3808a0e00b09c55631f9b4224228477a0762
4
- data.tar.gz: 9c26a06c3af53ebe55ca5e53f93b5d6d73854962daebef31d131000657be1c63
3
+ metadata.gz: '09c4dedacc59650be8d22ae65041e78913fc4405bbb61109ef93217801bafcff'
4
+ data.tar.gz: 91f205c4db85e2686b8461c6c2b029a45e6e430c418041aa3ffae725d1ecd16b
5
5
  SHA512:
6
- metadata.gz: 4fe10297a130b747d93dc1398bdf404996d5c07987680deac59a5365d6893ab3d0466ebd764ef087f5eb2409b99a7733dc55a869432d81286f3460ec510d247d
7
- data.tar.gz: 3684a1d3113f9df06637a6c7294f334b48e2c8cd5e54d4334c1ec1884571519f6e5aedeec327d8292f5364d1251453d6a7a8131074f91b2f94935b7a3586ddb9
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::AbortError => e
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
- def self.provider_names
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.provider_module(name)
13
- const_name = Helpers.command_to_const(name)
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 AbortError < StandardError; end
9
+ class Abort < StandardError; end
10
+ class Exit < StandardError; end
10
11
 
11
- include Dialogs
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, *@args) = argv
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
- @args += args_from_input unless input.isatty # Add piped arguments
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 provider arguments') if args.empty?
27
+ abort('No scheme arguments') if scheme_arguments.empty?
30
28
 
31
- process_providers
29
+ process_scheme_arguments
32
30
  end
33
31
 
34
- def print_provider_command(provider, arg_str, description = nil)
35
- command = "#{provider}:#{arg_str}"
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! # rubocop:disable Metrics/MethodLength
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.content)
47
- exit
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
- exit
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 args_from_input
61
- input_string = input.read
86
+ def sanitized_piped_args
87
+ return [] if input.isatty
62
88
 
63
- abort 'No input from pipe' if input_string.nil? || input_string.empty?
89
+ @sanitized_piped_args ||= begin
90
+ input_string = input.read.strip
64
91
 
65
- # Exclude comment part of piped input lines
66
- lines_without_comments = input_string.lines.map do |line|
67
- line.split(' # ').first
68
- end
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
- # Allow multiple provider arguments on a single piped input line
71
- joined_lines = lines_without_comments.join(' ').strip
72
- joined_lines.split(/\s+/)
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 process_providers
76
- used_providers = []
77
- args.each do |provider_args|
78
- (provider, arg_str) = provider_args.split(':')
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 used_providers.include?(provider)
81
- warn "Dropping command for already used provider: #{provider_args}"
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
- used_providers << provider if process_provider_command(provider, command, arg_str)
86
- end
117
+ command_class = get_command_class(scheme)
118
+ next if command_class.nil?
87
119
 
88
- warn 'No matching providers found for command' if used_providers.empty? && output.isatty
89
- end
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
- def process_provider_command(provider_name, command_name, arg_str)
92
- provider = Abt.provider_module(provider_name)
93
- return false if provider.nil?
127
+ used_schemes << scheme
128
+ end
94
129
 
95
- command = provider.command_class(command_name)
96
- return false if command.nil?
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
- print_command(command_name, provider_name, arg_str) if output.isatty
135
+ def get_command_class(scheme)
136
+ provider = Abt.scheme_provider(scheme)
137
+ return nil if provider.nil?
99
138
 
100
- command.new(arg_str: arg_str, cli: self).call
101
- true
139
+ provider.command_class(command)
102
140
  end
103
141
 
104
- def print_command(name, provider, arg_str)
105
- warn "===== #{name} #{provider}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
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