abt-cli 0.0.13 → 0.0.18

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -1
  3. data/lib/abt.rb +6 -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/{dialogs.rb → prompt.rb} +37 -18
  8. data/lib/abt/docs.rb +30 -24
  9. data/lib/abt/docs/cli.rb +42 -11
  10. data/lib/abt/docs/markdown.rb +38 -11
  11. data/lib/abt/git_config.rb +25 -14
  12. data/lib/abt/helpers.rb +1 -1
  13. data/lib/abt/providers/asana/base_command.rb +13 -13
  14. data/lib/abt/providers/asana/commands/add.rb +6 -6
  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 +3 -3
  18. data/lib/abt/providers/asana/commands/finalize.rb +3 -3
  19. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +3 -3
  20. data/lib/abt/providers/asana/commands/init.rb +5 -5
  21. data/lib/abt/providers/asana/commands/pick.rb +16 -8
  22. data/lib/abt/providers/asana/commands/projects.rb +3 -3
  23. data/lib/abt/providers/asana/commands/share.rb +5 -5
  24. data/lib/abt/providers/asana/commands/start.rb +14 -8
  25. data/lib/abt/providers/asana/commands/tasks.rb +3 -3
  26. data/lib/abt/providers/asana/configuration.rb +8 -16
  27. data/lib/abt/providers/devops/base_command.rb +14 -15
  28. data/lib/abt/providers/devops/commands/boards.rb +6 -4
  29. data/lib/abt/providers/devops/commands/branch_name.rb +45 -0
  30. data/lib/abt/providers/devops/commands/clear.rb +17 -6
  31. data/lib/abt/providers/devops/commands/current.rb +3 -3
  32. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +3 -3
  33. data/lib/abt/providers/devops/commands/init.rb +5 -5
  34. data/lib/abt/providers/devops/commands/pick.rb +14 -7
  35. data/lib/abt/providers/devops/commands/share.rb +4 -4
  36. data/lib/abt/providers/devops/commands/work-items.rb +3 -3
  37. data/lib/abt/providers/devops/configuration.rb +7 -15
  38. data/lib/abt/providers/git.rb +19 -0
  39. data/lib/abt/providers/git/commands/branch.rb +74 -0
  40. data/lib/abt/providers/harvest/base_command.rb +13 -13
  41. data/lib/abt/providers/harvest/commands/clear.rb +17 -6
  42. data/lib/abt/providers/harvest/commands/current.rb +3 -3
  43. data/lib/abt/providers/harvest/commands/init.rb +5 -5
  44. data/lib/abt/providers/harvest/commands/pick.rb +15 -7
  45. data/lib/abt/providers/harvest/commands/projects.rb +3 -3
  46. data/lib/abt/providers/harvest/commands/share.rb +5 -5
  47. data/lib/abt/providers/harvest/commands/start.rb +6 -42
  48. data/lib/abt/providers/harvest/commands/stop.rb +3 -3
  49. data/lib/abt/providers/harvest/commands/tasks.rb +3 -3
  50. data/lib/abt/providers/harvest/commands/track.rb +49 -11
  51. data/lib/abt/providers/harvest/configuration.rb +7 -13
  52. data/lib/abt/version.rb +1 -1
  53. metadata +9 -7
  54. data/lib/abt/cli/io.rb +0 -23
  55. data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
  56. data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
  57. 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: f61e3ba3cf1e1f6b5b69502861814a80b763849a7a9e8d654a593b55bf53b1bb
4
- data.tar.gz: 497d18071f5a32cc4df92d2075a097452ffa2ffbf7f97ab98916678e6c92fa31
3
+ metadata.gz: df400d04c979d979ab2ead353d68374b8266c0fff678103991cc6443512b703b
4
+ data.tar.gz: 0345141c38eae6904d11f40a3045fccd048d8009b70fe9714f5e0986f1779861
5
5
  SHA512:
6
- metadata.gz: fa028736b67c70cd694a6e75665136e7829865d7afe66828262eae903aa553d5dc92478957e9e36526669f96fde1aa42ccab0036e2c1319f6db6cc47cfd033e4
7
- data.tar.gz: 01af79206699568177e2548cd2662012f63ea996c6289a7dc49795fb99f06ff157692a51df444505336c3851a1f562a63fef659ca046e03579e11123b1e8a7ba
6
+ metadata.gz: 1979110cbd58f0bc71b83ad7211b86e17c5b078e842d4f99ca839c88a88d627ab92506736cd83b6e414cbea2ec2a82592f6e8b3f1effdbd2b57df7decd63793e
7
+ data.tar.gz: 6af0e9bb436b304cda9d3acf6c86733050c9a7acffb691017d968e89e3f4e8e51b9fa4cdf5e3a85373ff2f684fa61e41afce0c1b9011d2e6b5551935b40530ea
data/bin/abt CHANGED
@@ -5,7 +5,7 @@ require_relative '../lib/abt.rb'
5
5
 
6
6
  begin
7
7
  Abt::Cli.new.perform
8
- rescue Abt::Cli::AbortError => e
8
+ rescue Abt::Cli::Abort => e
9
9
  abort e.message
10
10
  rescue Interrupt
11
11
  abort 'Aborted'
data/lib/abt.rb CHANGED
@@ -5,18 +5,21 @@ require 'faraday'
5
5
  require 'oj'
6
6
  require 'open3'
7
7
  require 'stringio'
8
+ require 'optparse'
8
9
 
9
10
  Dir.glob("#{File.dirname(File.absolute_path(__FILE__))}/abt/*.rb").sort.each do |file|
10
11
  require file
11
12
  end
12
13
 
13
14
  module Abt
14
- def self.provider_names
15
+ module Providers; end
16
+
17
+ def self.schemes
15
18
  Providers.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
16
19
  end
17
20
 
18
- def self.provider_module(name)
19
- const_name = Helpers.command_to_const(name)
21
+ def self.scheme_provider(scheme)
22
+ const_name = Helpers.command_to_const(scheme)
20
23
  Providers.const_get(const_name) if Providers.const_defined?(const_name)
21
24
  end
22
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, :aris, :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
+ @aris = 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 ARIs') if aris.empty?
30
28
 
31
- process_providers
29
+ process_aris
32
30
  end
33
31
 
34
- def print_provider_command(provider, arg_str, description = nil)
35
- command = "#{provider}:#{arg_str}"
32
+ def print_ari(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 ARIs on a single piped input line
100
+ # TODO: Force the user to pick a single ARI
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_aris
107
+ used_schemes = []
108
+ aris.each do |ari|
109
+ scheme = ari.scheme
110
+ path = ari.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: #{ari}"
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, ari) if output.isatty
121
+ begin
122
+ command_class.new(path: path, cli: self, flags: ari.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 ARI(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, ari)
143
+ warn "===== #{name} #{ari} =====".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 Ari
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 Aris < 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 = Aris.new
36
+ rest = arguments.dup
37
+
38
+ until rest.empty?
39
+ (scheme, path) = rest.shift.split(':')
40
+ flags = take_flags(rest)
41
+
42
+ result << Ari.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
@@ -2,35 +2,41 @@
2
2
 
3
3
  module Abt
4
4
  class Cli
5
- module Dialogs
6
- def prompt(question)
7
- err_output.print "#{question}: "
8
- read_user_input.strip
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
9
15
  end
10
16
 
11
- def prompt_boolean(text)
12
- warn text
17
+ def boolean(text)
18
+ output.puts text
13
19
 
14
20
  loop do
15
- err_output.print '(y / n): '
21
+ output.print '(y / n): '
16
22
 
17
- case read_user_input.strip
23
+ case read_user_input
18
24
  when 'y', 'Y' then return true
19
25
  when 'n', 'N' then return false
20
26
  else
21
- warn 'Invalid choice'
27
+ output.puts 'Invalid choice'
22
28
  next
23
29
  end
24
30
  end
25
31
  end
26
32
 
27
- def prompt_choice(text, options, nil_option = false)
28
- warn "#{text}:"
33
+ def choice(text, options, nil_option = false)
34
+ output.puts "#{text}:"
29
35
 
30
36
  if options.length.zero?
31
- abort 'No available options' unless nil_option
37
+ raise Abort, 'No available options' unless nil_option
32
38
 
33
- warn 'No available options'
39
+ output.puts 'No available options'
34
40
  return nil
35
41
  end
36
42
 
@@ -42,7 +48,7 @@ module Abt
42
48
 
43
49
  def print_options(options)
44
50
  options.each_with_index do |option, index|
45
- warn "(#{index + 1}) #{option['name']}"
51
+ output.puts "(#{index + 1}) #{option['name']}"
46
52
  end
47
53
  end
48
54
 
@@ -57,13 +63,16 @@ module Abt
57
63
 
58
64
  option = options[number - 1]
59
65
 
60
- warn "Selected: (#{number}) #{option['name']}"
66
+ output.puts "Selected: (#{number}) #{option['name']}"
61
67
  return option
62
68
  end
63
69
  end
64
70
 
65
71
  def read_option_number(options_length, nil_option)
66
- err_output.print "(1-#{options_length}#{nil_option_string(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 '): '
67
76
 
68
77
  input = read_user_input
69
78
 
@@ -71,7 +80,7 @@ module Abt
71
80
 
72
81
  option_number = input.to_i
73
82
  if option_number <= 0 || option_number > options_length
74
- warn 'Invalid selection'
83
+ output.puts 'Invalid selection'
75
84
  return nil
76
85
  end
77
86
 
@@ -98,7 +107,17 @@ module Abt
98
107
  end
99
108
 
100
109
  def read_user_input
101
- open('/dev/tty', &:gets).strip
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
102
121
  end
103
122
  end
104
123
  end