abt-cli 0.0.14 → 0.0.19

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -1
  3. data/lib/abt.rb +4 -3
  4. data/lib/abt/ari.rb +20 -0
  5. data/lib/abt/ari_list.rb +13 -0
  6. data/lib/abt/base_command.rb +63 -0
  7. data/lib/abt/cli.rb +89 -54
  8. data/lib/abt/cli/arguments_parser.rb +48 -0
  9. data/lib/abt/cli/{dialogs.rb → prompt.rb} +38 -18
  10. data/lib/abt/docs.rb +35 -28
  11. data/lib/abt/docs/cli.rb +42 -11
  12. data/lib/abt/docs/markdown.rb +38 -11
  13. data/lib/abt/git_config.rb +26 -31
  14. data/lib/abt/providers/asana/base_command.rb +17 -37
  15. data/lib/abt/providers/asana/commands/add.rb +15 -13
  16. data/lib/abt/providers/asana/commands/{branch-name.rb → branch_name.rb} +12 -7
  17. data/lib/abt/providers/asana/commands/clear.rb +19 -6
  18. data/lib/abt/providers/asana/commands/current.rb +22 -37
  19. data/lib/abt/providers/asana/commands/finalize.rb +6 -6
  20. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +12 -7
  21. data/lib/abt/providers/asana/commands/init.rb +11 -11
  22. data/lib/abt/providers/asana/commands/pick.rb +30 -17
  23. data/lib/abt/providers/asana/commands/projects.rb +4 -4
  24. data/lib/abt/providers/asana/commands/share.rb +5 -9
  25. data/lib/abt/providers/asana/commands/start.rb +27 -19
  26. data/lib/abt/providers/asana/commands/tasks.rb +7 -6
  27. data/lib/abt/providers/asana/configuration.rb +23 -37
  28. data/lib/abt/providers/asana/path.rb +36 -0
  29. data/lib/abt/providers/devops/api.rb +12 -0
  30. data/lib/abt/providers/devops/base_command.rb +18 -44
  31. data/lib/abt/providers/devops/commands/boards.rb +7 -5
  32. data/lib/abt/providers/devops/commands/{branch-name.rb → branch_name.rb} +10 -6
  33. data/lib/abt/providers/devops/commands/clear.rb +19 -6
  34. data/lib/abt/providers/devops/commands/current.rb +17 -41
  35. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +12 -4
  36. data/lib/abt/providers/devops/commands/init.rb +20 -20
  37. data/lib/abt/providers/devops/commands/pick.rb +18 -18
  38. data/lib/abt/providers/devops/commands/share.rb +6 -7
  39. data/lib/abt/providers/devops/commands/work-items.rb +4 -4
  40. data/lib/abt/providers/devops/configuration.rb +20 -57
  41. data/lib/abt/providers/devops/path.rb +50 -0
  42. data/lib/abt/providers/git/commands/branch.rb +28 -28
  43. data/lib/abt/providers/harvest/base_command.rb +18 -36
  44. data/lib/abt/providers/harvest/commands/clear.rb +19 -6
  45. data/lib/abt/providers/harvest/commands/current.rb +27 -34
  46. data/lib/abt/providers/harvest/commands/init.rb +10 -11
  47. data/lib/abt/providers/harvest/commands/pick.rb +16 -9
  48. data/lib/abt/providers/harvest/commands/projects.rb +4 -4
  49. data/lib/abt/providers/harvest/commands/share.rb +7 -11
  50. data/lib/abt/providers/harvest/commands/start.rb +6 -42
  51. data/lib/abt/providers/harvest/commands/stop.rb +10 -10
  52. data/lib/abt/providers/harvest/commands/tasks.rb +7 -4
  53. data/lib/abt/providers/harvest/commands/track.rb +66 -21
  54. data/lib/abt/providers/harvest/configuration.rb +23 -38
  55. data/lib/abt/providers/harvest/path.rb +36 -0
  56. data/lib/abt/version.rb +1 -1
  57. metadata +12 -9
  58. data/lib/abt/cli/io.rb +0 -23
  59. data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
  60. data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
  61. 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: d6355433a65cf9e49738ab971a8886e657cefff8d3a8322e44d140a18fa7a6be
4
- data.tar.gz: f71c9909c3c8063f24365d80db1ae91223876be98cda4e6681a5f37e017af2ca
3
+ metadata.gz: 295db35510e60a5b46ea7f20caa01aaad247bb30cb60e61b912cab5f754637ac
4
+ data.tar.gz: e686eb40fc1ac88315c138eac56b96500c29c3f012b4bdb1d97f993a507f80b7
5
5
  SHA512:
6
- metadata.gz: 809f27b09f6bbf41eefca395f95621b9bd7c48cb3f78cd5e7bf62c48a87895669fa74aebad4ea31fdee6ed484bf8944bb6216059b93066db2cfe118bc65c6e34
7
- data.tar.gz: '032284e3f7e465618c2c039dbafa84855548bcee41d56725606fd2b48e58c008ba89010be8c25e78f714f3c1e16cc8a30f3e8021a0a5a531617dc0d54393ce1e'
6
+ metadata.gz: f18aa2b4f33bb6b51689522bca357ddb0fbe7a2d2e968bcc6754959f01bf6d54b9f93c387e8a050d165063cad26e6b360f245d2e7fa317bc1fbad8c0fa0d3379
7
+ data.tar.gz: ab46d17a29d4e447472c8c4d9242aa2e96bd5195bad4083d0476054f312d358ad5555526b44edacdf2512713e646435200689f82e8a097c1dfa306ff2ae1c4df
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,6 +5,7 @@ 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
@@ -13,12 +14,12 @@ end
13
14
  module Abt
14
15
  module Providers; end
15
16
 
16
- def self.provider_names
17
+ def self.schemes
17
18
  Providers.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
18
19
  end
19
20
 
20
- def self.provider_module(name)
21
- const_name = Helpers.command_to_const(name)
21
+ def self.scheme_provider(scheme)
22
+ const_name = Helpers.command_to_const(scheme)
22
23
  Providers.const_get(const_name) if Providers.const_defined?(const_name)
23
24
  end
24
25
  end
data/lib/abt/ari.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class Ari
5
+ attr_reader :scheme, :path, :flags
6
+
7
+ def initialize(scheme:, path: nil, flags: [])
8
+ @scheme = scheme
9
+ @path = path
10
+ @flags = flags
11
+ end
12
+
13
+ def to_s
14
+ str = scheme
15
+ str += ":#{path}" if path
16
+
17
+ [str, *flags].join(' ')
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class AriList < Array
5
+ def to_s
6
+ map(&:to_s).join(' -- ')
7
+ end
8
+
9
+ def -(other)
10
+ AriList.new(to_a - other)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class BaseCommand
5
+ extend Forwardable
6
+
7
+ def self.usage
8
+ raise NotImplementedError, 'Command classes must implement .usage'
9
+ end
10
+
11
+ def self.description
12
+ raise NotImplementedError, 'Command classes must implement .description'
13
+ end
14
+
15
+ def self.flags
16
+ []
17
+ end
18
+
19
+ attr_reader :ari, :cli, :flags
20
+
21
+ def_delegators(:@cli, :warn, :puts, :print, :abort, :exit_with_message)
22
+
23
+ def initialize(ari:, cli:)
24
+ @cli = cli
25
+ @ari = ari
26
+ @flags = parse_flags(ari.flags)
27
+ end
28
+
29
+ def perform
30
+ raise NotImplementedError, 'Command classes must implement #perform'
31
+ end
32
+
33
+ private
34
+
35
+ def parse_flags(flags)
36
+ result = {}
37
+
38
+ flag_parser.parse!(flags.dup, into: result)
39
+
40
+ exit_with_message(flag_parser.help) if result[:help]
41
+
42
+ result
43
+ rescue OptionParser::InvalidOption => e
44
+ abort e.message
45
+ end
46
+
47
+ def flag_parser
48
+ @flag_parser ||= OptionParser.new do |opts|
49
+ opts.banner = <<~TXT
50
+ #{self.class.description}
51
+
52
+ Usage: #{self.class.usage}
53
+ TXT
54
+
55
+ opts.on('-h', '--help')
56
+
57
+ self.class.flags.each do |(*flag)|
58
+ opts.on(*flag)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
data/lib/abt/cli.rb CHANGED
@@ -6,103 +6,138 @@ 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
22
-
23
- @args += args_from_input unless input.isatty # Add piped arguments
19
+ @prompt = Abt::Cli::Prompt.new(output: err_output)
20
+ @aris = ArgumentsParser.new(sanitized_piped_args + remaining_args).parse
24
21
  end
25
22
 
26
23
  def perform
27
- handle_global_commands!
24
+ return if handle_global_commands!
28
25
 
29
- abort('No provider arguments') if args.empty?
26
+ abort('No ARIs') if aris.empty?
30
27
 
31
- process_providers
28
+ process_aris
32
29
  end
33
30
 
34
- def print_provider_command(provider, arg_str, description = nil)
35
- command = "#{provider}:#{arg_str}"
31
+ def print_ari(scheme, path, description = nil)
32
+ command = "#{scheme}:#{path}"
36
33
  command += " # #{description}" unless description.nil?
37
34
  output.puts command
38
35
  end
39
36
 
37
+ def warn(*args)
38
+ err_output.puts(*args)
39
+ end
40
+
41
+ def puts(*args)
42
+ output.puts(*args)
43
+ end
44
+
45
+ def print(*args)
46
+ output.print(*args)
47
+ end
48
+
49
+ def abort(message)
50
+ raise Abort, message
51
+ end
52
+
53
+ def exit_with_message(message)
54
+ raise Exit, message
55
+ end
56
+
40
57
  private
41
58
 
42
- def handle_global_commands! # rubocop:disable Metrics/MethodLength
59
+ def handle_global_commands!
43
60
  case command
44
61
  when nil
45
62
  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
63
+ puts(Abt::Docs::Cli.help)
64
+ true
54
65
  when '--version', '-v', 'version'
55
66
  puts(Abt::VERSION)
56
- exit
67
+ true
68
+ when '--help', '-h', 'help'
69
+ puts(Abt::Docs::Cli.help)
70
+ true
71
+ when 'commands'
72
+ puts(Abt::Docs::Cli.commands)
73
+ true
74
+ when 'examples'
75
+ puts(Abt::Docs::Cli.examples)
76
+ true
77
+ when 'readme'
78
+ puts(Abt::Docs::Markdown.readme)
79
+ true
80
+ else
81
+ false
57
82
  end
58
83
  end
59
84
 
60
- def args_from_input
61
- input_string = input.read
85
+ def sanitized_piped_args
86
+ return [] if input.isatty
62
87
 
63
- abort 'No input from pipe' if input_string.nil? || input_string.empty?
88
+ @sanitized_piped_args ||= begin
89
+ input_string = input.read.strip
64
90
 
65
- # Exclude comment part of piped input lines
66
- lines_without_comments = input_string.lines.map do |line|
67
- line.split(' # ').first
68
- end
91
+ abort 'No input from pipe' if input_string.nil? || input_string.empty?
92
+
93
+ # Exclude comment part of piped input lines
94
+ lines_without_comments = input_string.lines.map do |line|
95
+ line.split(' # ').first
96
+ end
69
97
 
70
- # Allow multiple provider arguments on a single piped input line
71
- joined_lines = lines_without_comments.join(' ').strip
72
- joined_lines.split(/\s+/)
98
+ # Allow multiple ARIs on a single piped input line
99
+ # TODO: Force the user to pick a single ARI
100
+ joined_lines = lines_without_comments.join(' ').strip
101
+ joined_lines.split(/\s+/)
102
+ end
73
103
  end
74
104
 
75
- def process_providers
76
- used_providers = []
77
- args.each do |provider_args|
78
- (provider, arg_str) = provider_args.split(':')
105
+ def process_aris
106
+ used_schemes = []
79
107
 
80
- if used_providers.include?(provider)
81
- warn "Dropping command for already used provider: #{provider_args}"
108
+ aris.each do |ari|
109
+ if used_schemes.include?(ari.scheme)
110
+ warn "Dropping command for already used scheme: #{ari}"
82
111
  next
83
112
  end
84
113
 
85
- used_providers << provider if process_provider_command(provider, command, arg_str)
86
- end
114
+ command_class = get_command_class(ari.scheme)
115
+ next if command_class.nil?
87
116
 
88
- warn 'No matching providers found for command' if used_providers.empty? && output.isatty
89
- end
117
+ print_command(command, ari) if output.isatty
118
+ begin
119
+ command_class.new(ari: ari, cli: self).perform
120
+ rescue Exit => e
121
+ puts e.message
122
+ end
90
123
 
91
- def process_provider_command(provider_name, command_name, arg_str)
92
- provider = Abt.provider_module(provider_name)
93
- return false if provider.nil?
124
+ used_schemes << ari.scheme
125
+ end
94
126
 
95
- command = provider.command_class(command_name)
96
- return false if command.nil?
127
+ return unless used_schemes.empty? && output.isatty
128
+
129
+ abort 'No providers found for command and ARI(s)'
130
+ end
97
131
 
98
- print_command(command_name, provider_name, arg_str) if output.isatty
132
+ def get_command_class(scheme)
133
+ provider = Abt.scheme_provider(scheme)
134
+ return nil if provider.nil?
99
135
 
100
- command.new(arg_str: arg_str, cli: self).call
101
- true
136
+ provider.command_class(command)
102
137
  end
103
138
 
104
- def print_command(name, provider, arg_str)
105
- warn "===== #{name} #{provider}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
139
+ def print_command(name, ari)
140
+ warn "===== #{name.upcase} #{ari} ====="
106
141
  end
107
142
  end
108
143
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class Cli
5
+ class ArgumentsParser
6
+ attr_reader :arguments
7
+
8
+ def initialize(arguments)
9
+ @arguments = arguments
10
+ end
11
+
12
+ def parse
13
+ result = AriList.new
14
+ rest = arguments.dup
15
+
16
+ until rest.empty?
17
+ (scheme, path) = rest.shift.split(':')
18
+ flags = take_flags(rest)
19
+
20
+ result << Ari.new(scheme: scheme, path: path, flags: flags)
21
+ end
22
+
23
+ result
24
+ end
25
+
26
+ private
27
+
28
+ def take_flags(rest)
29
+ flags = []
30
+
31
+ if flag?(rest.first)
32
+ flags << rest.shift until rest.empty? || delimiter?(rest.first)
33
+ rest.shift if delimiter?(rest.first)
34
+ end
35
+
36
+ flags
37
+ end
38
+
39
+ def flag?(part)
40
+ part && part[0] == '-'
41
+ end
42
+
43
+ def delimiter?(part)
44
+ part == '--'
45
+ end
46
+ end
47
+ end
48
+ 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,17 @@ 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
+ str = '('
73
+ str += options_length > 1 ? "1-#{options_length}" : '1'
74
+ str += nil_option_string(nil_option)
75
+ str += '): '
76
+ output.print str
67
77
 
68
78
  input = read_user_input
69
79
 
@@ -71,7 +81,7 @@ module Abt
71
81
 
72
82
  option_number = input.to_i
73
83
  if option_number <= 0 || option_number > options_length
74
- warn 'Invalid selection'
84
+ output.puts 'Invalid selection'
75
85
  return nil
76
86
  end
77
87
 
@@ -98,7 +108,17 @@ module Abt
98
108
  end
99
109
 
100
110
  def read_user_input
101
- open('/dev/tty', &:gets).strip
111
+ open(tty_path, &:gets).strip # rubocop:disable Security/Open
112
+ end
113
+
114
+ def tty_path
115
+ @tty_path ||= begin
116
+ candidates = ['/dev/tty', 'CON:'] # Unix: '/dev/tty', Windows: 'CON:'
117
+ selected = candidates.find { |candidate| File.exist?(candidate) }
118
+ raise Abort, 'Unable to prompt for user input' if selected.nil?
119
+
120
+ selected
121
+ end
102
122
  end
103
123
  end
104
124
  end