abt-cli 0.0.15 → 0.0.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -1
  3. data/lib/abt.rb +4 -3
  4. data/lib/abt/cli.rb +70 -48
  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 +2 -2
  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 +11 -0
  12. data/lib/abt/providers/asana/base_command.rb +13 -13
  13. data/lib/abt/providers/asana/commands/add.rb +3 -3
  14. data/lib/abt/providers/asana/commands/{branch-name.rb → branch_name.rb} +3 -3
  15. data/lib/abt/providers/asana/commands/clear.rb +17 -6
  16. data/lib/abt/providers/asana/commands/current.rb +3 -3
  17. data/lib/abt/providers/asana/commands/finalize.rb +3 -3
  18. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +3 -3
  19. data/lib/abt/providers/asana/commands/init.rb +3 -3
  20. data/lib/abt/providers/asana/commands/pick.rb +13 -5
  21. data/lib/abt/providers/asana/commands/projects.rb +3 -3
  22. data/lib/abt/providers/asana/commands/share.rb +5 -5
  23. data/lib/abt/providers/asana/commands/start.rb +13 -7
  24. data/lib/abt/providers/asana/commands/tasks.rb +3 -3
  25. data/lib/abt/providers/asana/configuration.rb +5 -13
  26. data/lib/abt/providers/devops/base_command.rb +14 -15
  27. data/lib/abt/providers/devops/commands/boards.rb +6 -4
  28. data/lib/abt/providers/devops/commands/{branch-name.rb → branch_name.rb} +3 -3
  29. data/lib/abt/providers/devops/commands/clear.rb +17 -6
  30. data/lib/abt/providers/devops/commands/current.rb +3 -3
  31. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +3 -3
  32. data/lib/abt/providers/devops/commands/init.rb +3 -3
  33. data/lib/abt/providers/devops/commands/pick.rb +12 -5
  34. data/lib/abt/providers/devops/commands/share.rb +4 -4
  35. data/lib/abt/providers/devops/commands/work-items.rb +3 -3
  36. data/lib/abt/providers/devops/configuration.rb +5 -13
  37. data/lib/abt/providers/git/commands/branch.rb +15 -21
  38. data/lib/abt/providers/harvest/base_command.rb +13 -13
  39. data/lib/abt/providers/harvest/commands/clear.rb +17 -6
  40. data/lib/abt/providers/harvest/commands/current.rb +3 -3
  41. data/lib/abt/providers/harvest/commands/init.rb +3 -3
  42. data/lib/abt/providers/harvest/commands/pick.rb +13 -5
  43. data/lib/abt/providers/harvest/commands/projects.rb +3 -3
  44. data/lib/abt/providers/harvest/commands/share.rb +5 -5
  45. data/lib/abt/providers/harvest/commands/start.rb +6 -42
  46. data/lib/abt/providers/harvest/commands/stop.rb +3 -3
  47. data/lib/abt/providers/harvest/commands/tasks.rb +3 -3
  48. data/lib/abt/providers/harvest/commands/track.rb +49 -11
  49. data/lib/abt/providers/harvest/configuration.rb +5 -11
  50. data/lib/abt/version.rb +1 -1
  51. metadata +6 -7
  52. data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
  53. data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
  54. 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: f176b71200cc6fcc13ab56507e4d48234785e2307ab350afca7be13193ef2a5e
4
- data.tar.gz: '093172885ed64949153cd73457748f446e012887f78d092f2483164f005fbfd0'
3
+ metadata.gz: '09c4dedacc59650be8d22ae65041e78913fc4405bbb61109ef93217801bafcff'
4
+ data.tar.gz: 91f205c4db85e2686b8461c6c2b029a45e6e430c418041aa3ffae725d1ecd16b
5
5
  SHA512:
6
- metadata.gz: 6350132f05617d7e7222f0b7e07255091119ad0edac59277795c891668a6de4fb3ea9e376cdc93ab2672e130b5222d6ff310d80da8979dfaf060ff0e2479e447
7
- data.tar.gz: 14eea7d7cb849f39d1aef4643df214541178d4f8702cecc962ffcff099b0c9143be68b8562aac0fdbc75b2bb2424872bd34661136104ac233df2097031d49394
6
+ metadata.gz: 42f9d332099fdcecdbdf538bd44119b75d643db6399d06fe578d15e5dff664af9dfec59a5ffa1f1235a1b922fa5727d52c20ed0b6a8c4fa795e1750b51f0943d
7
+ data.tar.gz: a88ffec46e7331823de6dc89fde11e442ac3c7bd577495a89dd2af20b879675de7fdafada270d1614556d193bb909892fd9622b8b3cd87f2836fab0aa94b1483
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/cli.rb CHANGED
@@ -6,31 +6,31 @@ 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
- attr_reader :command, :args, :input, :output, :err_output, :prompt
12
+ attr_reader :command, :scheme_arguments, :input, :output, :err_output, :prompt
12
13
 
13
14
  def initialize(argv: ARGV, input: STDIN, output: STDOUT, err_output: STDERR)
14
- (@command, *@args) = argv
15
-
15
+ (@command, *remaining_args) = argv
16
16
  @input = input
17
17
  @output = output
18
18
  @err_output = err_output
19
19
  @prompt = Abt::Cli::Prompt.new(output: err_output)
20
20
 
21
- @args += args_from_input unless input.isatty # Add piped arguments
21
+ @scheme_arguments = ArgumentsParser.new(sanitized_piped_args + remaining_args).parse
22
22
  end
23
23
 
24
24
  def perform
25
25
  return if handle_global_commands!
26
26
 
27
- abort('No provider arguments') if args.empty?
27
+ abort('No scheme arguments') if scheme_arguments.empty?
28
28
 
29
- process_providers
29
+ process_scheme_arguments
30
30
  end
31
31
 
32
- def print_provider_command(provider, arg_str, description = nil)
33
- command = "#{provider}:#{arg_str}"
32
+ def print_scheme_argument(scheme, path, description = nil)
33
+ command = "#{scheme}:#{path}"
34
34
  command += " # #{description}" unless description.nil?
35
35
  output.puts command
36
36
  end
@@ -48,77 +48,99 @@ module Abt
48
48
  end
49
49
 
50
50
  def abort(message)
51
- raise AbortError, message
51
+ raise Abort, message
52
+ end
53
+
54
+ def exit_with_message(message)
55
+ raise Exit, message
52
56
  end
53
57
 
54
58
  private
55
59
 
56
- def handle_global_commands! # rubocop:disable Metrics/MethodLength
60
+ def handle_global_commands!
57
61
  case command
58
62
  when nil
59
63
  warn("No command specified\n\n")
60
- puts(Abt::Docs::Cli.content)
61
- true
62
- when '--help', '-h', 'help', 'commands'
63
- puts(Abt::Docs::Cli.content)
64
- true
65
- when 'help-md'
66
- puts(Abt::Docs::Markdown.content)
64
+ puts(Abt::Docs::Cli.help)
67
65
  true
68
66
  when '--version', '-v', 'version'
69
67
  puts(Abt::VERSION)
70
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
71
81
  else
72
82
  false
73
83
  end
74
84
  end
75
85
 
76
- def args_from_input
77
- input_string = input.read.strip
86
+ def sanitized_piped_args
87
+ return [] if input.isatty
78
88
 
79
- abort 'No input from pipe' if input_string.nil? || input_string.empty?
89
+ @sanitized_piped_args ||= begin
90
+ input_string = input.read.strip
80
91
 
81
- # Exclude comment part of piped input lines
82
- lines_without_comments = input_string.lines.map do |line|
83
- line.split(' # ').first
84
- end
92
+ abort 'No input from pipe' if input_string.nil? || input_string.empty?
85
93
 
86
- # Allow multiple provider arguments on a single piped input line
87
- joined_lines = lines_without_comments.join(' ').strip
88
- joined_lines.split(/\s+/)
94
+ # Exclude comment part of piped input lines
95
+ lines_without_comments = input_string.lines.map do |line|
96
+ line.split(' # ').first
97
+ end
98
+
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
89
104
  end
90
105
 
91
- def process_providers
92
- used_providers = []
93
- args.each do |provider_args|
94
- (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
95
111
 
96
- if used_providers.include?(provider)
97
- 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}"
98
114
  next
99
115
  end
100
116
 
101
- used_providers << provider if process_provider_command(provider, command, arg_str)
102
- end
117
+ command_class = get_command_class(scheme)
118
+ next if command_class.nil?
103
119
 
104
- abort 'No matching providers found for command' if used_providers.empty? && output.isatty
105
- 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
126
+
127
+ used_schemes << scheme
128
+ end
106
129
 
107
- def process_provider_command(provider_name, command_name, arg_str)
108
- provider = Abt.provider_module(provider_name)
109
- return false if provider.nil?
130
+ return unless used_schemes.empty? && output.isatty
110
131
 
111
- command = provider.command_class(command_name)
112
- return false if command.nil?
132
+ abort 'No providers found for command and scheme argument(s)'
133
+ end
113
134
 
114
- 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?
115
138
 
116
- command.new(arg_str: arg_str, cli: self).call
117
- true
139
+ provider.command_class(command)
118
140
  end
119
141
 
120
- def print_command(name, provider, arg_str)
121
- warn "===== #{name} #{provider}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
142
+ def print_command(name, scheme_argument)
143
+ warn "===== #{name} #{scheme_argument} =====".upcase
122
144
  end
123
145
  end
124
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
@@ -34,7 +34,7 @@ module Abt
34
34
  output.puts "#{text}:"
35
35
 
36
36
  if options.length.zero?
37
- raise AbortError, 'No available options' unless nil_option
37
+ raise Abort, 'No available options' unless nil_option
38
38
 
39
39
  output.puts 'No available options'
40
40
  return nil
@@ -114,7 +114,7 @@ module Abt
114
114
  @tty_path ||= begin
115
115
  candidates = ['/dev/tty', 'CON:'] # Unix: '/dev/tty', Windows: 'CON:'
116
116
  selected = candidates.find { |candidate| File.exist?(candidate) }
117
- raise AbortError, 'Unable to prompt for user input' if selected.nil?
117
+ raise Abort, 'Unable to prompt for user input' if selected.nil?
118
118
 
119
119
  selected
120
120
  end
data/lib/abt/docs.rb CHANGED
@@ -7,7 +7,7 @@ end
7
7
  module Abt
8
8
  module Docs
9
9
  class << self
10
- def examples # rubocop:disable Metrics/MethodLength
10
+ def basic_examples
11
11
  {
12
12
  'Getting started:' => {
13
13
  'abt init asana harvest' => 'Setup asana and harvest project git repo in working dir',
@@ -16,10 +16,15 @@ module Abt
16
16
  'abt stop harvest' => 'Stop time tracker',
17
17
  'abt start asana harvest' => 'Continue working, e.g. after a break',
18
18
  'abt finalize asana' => 'Finalize the selected asana task'
19
- },
19
+ }
20
+ }
21
+ end
22
+
23
+ def extended_examples
24
+ {
20
25
  'Tracking meetings (without changing the config):' => {
21
- 'abt tasks asana | grep -i standup | abt track harvest' => 'Track on asana meeting task without changing any configuration',
22
- 'abt tasks harvest | grep -i comment | abt track harvest' => 'Track on harvest "Comment"-task (will prompt for a comment)'
26
+ 'abt pick asana -d | abt track harvest' => 'Track on asana meeting task',
27
+ 'abt pick harvest -d | abt track harvest -c "Name of meeting"' => 'Track on separate harvest-task'
23
28
  },
24
29
  'Command output can be piped, e.g.:' => {
25
30
  'abt tasks asana | grep -i <name of task>' => nil,
@@ -29,30 +34,31 @@ module Abt
29
34
  'abt share asana harvest | tr "\n" " "' => 'Print current configuration',
30
35
  'abt share asana harvest | tr "\n" " " | pbcopy' => 'Copy configuration (mac only)',
31
36
  'abt start <shared configuration>' => 'Start a shared configuration'
37
+ },
38
+ 'Flags:' => {
39
+ 'abt start harvest -c "comment"' => 'Add command flags after <scheme>:<path>',
40
+ 'abt start harvest -c "comment" -- asana' => 'Use -- to mark the end of a flag list if it\'s to be followed by a <scheme-argument>',
41
+ 'abt pick harvest | abt start -c "comment"' => 'Flags placed directly after a command applies to piped in <scheme-argument>'
32
42
  }
33
43
  }
34
44
  end
35
45
 
36
46
  def providers
37
- provider_definitions
47
+ @providers ||= Abt.schemes.sort.each_with_object({}) do |scheme, definition|
48
+ definition[scheme] = command_definitions(scheme)
49
+ end
38
50
  end
39
51
 
40
52
  private
41
53
 
42
- def provider_definitions
43
- Abt.provider_names.sort.each_with_object({}) do |name, definition|
44
- provider_module = Abt.provider_module(name)
45
-
46
- definition[name] = command_definitions(provider_module)
47
- end
48
- end
49
-
50
- def command_definitions(provider_module)
51
- provider_module.command_names.each_with_object({}) do |name, definition|
52
- command_class = provider_module.command_class(name)
54
+ def command_definitions(scheme)
55
+ provider = Abt.scheme_provider(scheme)
56
+ provider.command_names.each_with_object({}) do |name, definition|
57
+ command_class = provider.command_class(name)
58
+ full_name = "abt #{name} #{scheme}"
53
59
 
54
- if command_class.respond_to?(:command) && command_class.respond_to?(:description)
55
- definition[command_class.command] = command_class.description
60
+ if command_class.respond_to?(:usage) && command_class.respond_to?(:description)
61
+ definition[full_name] = [command_class.usage, command_class.description]
56
62
  end
57
63
  end
58
64
  end