abt-cli 0.0.19 → 0.0.24

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +3 -3
  3. data/lib/abt.rb +6 -6
  4. data/lib/abt/ari.rb +2 -2
  5. data/lib/abt/ari_list.rb +1 -1
  6. data/lib/abt/base_command.rb +7 -7
  7. data/lib/abt/cli.rb +49 -47
  8. data/lib/abt/cli/arguments_parser.rb +6 -3
  9. data/lib/abt/cli/global_commands.rb +23 -0
  10. data/lib/abt/cli/global_commands/commands.rb +23 -0
  11. data/lib/abt/cli/global_commands/examples.rb +23 -0
  12. data/lib/abt/cli/global_commands/help.rb +23 -0
  13. data/lib/abt/cli/global_commands/readme.rb +23 -0
  14. data/lib/abt/cli/global_commands/share.rb +36 -0
  15. data/lib/abt/cli/global_commands/version.rb +23 -0
  16. data/lib/abt/cli/prompt.rb +64 -52
  17. data/lib/abt/docs.rb +48 -26
  18. data/lib/abt/docs/cli.rb +3 -3
  19. data/lib/abt/docs/markdown.rb +10 -7
  20. data/lib/abt/git_config.rb +4 -6
  21. data/lib/abt/helpers.rb +26 -8
  22. data/lib/abt/providers/asana/api.rb +9 -9
  23. data/lib/abt/providers/asana/base_command.rb +12 -10
  24. data/lib/abt/providers/asana/commands/add.rb +13 -12
  25. data/lib/abt/providers/asana/commands/branch_name.rb +8 -8
  26. data/lib/abt/providers/asana/commands/clear.rb +7 -8
  27. data/lib/abt/providers/asana/commands/current.rb +14 -15
  28. data/lib/abt/providers/asana/commands/finalize.rb +17 -18
  29. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +18 -16
  30. data/lib/abt/providers/asana/commands/init.rb +8 -41
  31. data/lib/abt/providers/asana/commands/pick.rb +22 -26
  32. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  33. data/lib/abt/providers/asana/commands/share.rb +7 -5
  34. data/lib/abt/providers/asana/commands/start.rb +28 -21
  35. data/lib/abt/providers/asana/commands/tasks.rb +6 -6
  36. data/lib/abt/providers/asana/configuration.rb +37 -29
  37. data/lib/abt/providers/asana/path.rb +6 -6
  38. data/lib/abt/providers/devops/api.rb +12 -12
  39. data/lib/abt/providers/devops/base_command.rb +14 -10
  40. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  41. data/lib/abt/providers/devops/commands/branch_name.rb +9 -9
  42. data/lib/abt/providers/devops/commands/clear.rb +7 -8
  43. data/lib/abt/providers/devops/commands/current.rb +17 -18
  44. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +21 -19
  45. data/lib/abt/providers/devops/commands/init.rb +21 -14
  46. data/lib/abt/providers/devops/commands/pick.rb +25 -19
  47. data/lib/abt/providers/devops/commands/share.rb +7 -5
  48. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  49. data/lib/abt/providers/devops/configuration.rb +15 -15
  50. data/lib/abt/providers/devops/path.rb +7 -6
  51. data/lib/abt/providers/git/commands/branch.rb +23 -21
  52. data/lib/abt/providers/harvest/api.rb +8 -8
  53. data/lib/abt/providers/harvest/base_command.rb +10 -8
  54. data/lib/abt/providers/harvest/commands/clear.rb +7 -8
  55. data/lib/abt/providers/harvest/commands/current.rb +13 -14
  56. data/lib/abt/providers/harvest/commands/init.rb +10 -39
  57. data/lib/abt/providers/harvest/commands/pick.rb +15 -11
  58. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  59. data/lib/abt/providers/harvest/commands/share.rb +7 -5
  60. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  61. data/lib/abt/providers/harvest/commands/stop.rb +12 -12
  62. data/lib/abt/providers/harvest/commands/tasks.rb +7 -7
  63. data/lib/abt/providers/harvest/commands/track.rb +52 -37
  64. data/lib/abt/providers/harvest/configuration.rb +18 -18
  65. data/lib/abt/providers/harvest/path.rb +6 -6
  66. data/lib/abt/version.rb +1 -1
  67. metadata +12 -5
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class Cli
5
+ module GlobalCommands
6
+ class Version < Abt::BaseCommand
7
+ def self.usage
8
+ "abt version"
9
+ end
10
+
11
+ def self.description
12
+ "Print abt version"
13
+ end
14
+
15
+ attr_reader :cli
16
+
17
+ def perform
18
+ puts(Abt::VERSION)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -10,38 +10,44 @@ module Abt
10
10
  end
11
11
 
12
12
  def text(question)
13
- output.print "#{question}: "
14
- read_user_input
13
+ output.print("#{question.strip}: ")
14
+ Abt::Helpers.read_user_input
15
15
  end
16
16
 
17
17
  def boolean(text)
18
18
  output.puts text
19
19
 
20
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
21
+ output.print("(y / n): ")
22
+
23
+ case Abt::Helpers.read_user_input
24
+ when "y", "Y" then return true
25
+ when "n", "N" then return false
26
+ else output.puts "Invalid choice" end
30
27
  end
31
28
  end
32
29
 
33
- def choice(text, options, nil_option = false)
34
- output.puts "#{text}:"
30
+ def choice(text, options, nil_option: false)
31
+ output.puts "#{text.strip}:"
35
32
 
36
33
  if options.length.zero?
37
- raise Abort, 'No available options' unless nil_option
34
+ raise Abort, "No available options" unless nil_option
38
35
 
39
- output.puts 'No available options'
36
+ output.puts "No available options"
40
37
  return nil
41
38
  end
42
39
 
43
40
  print_options(options)
44
- select_options(options, nil_option)
41
+ select_option(options, nil_option)
42
+ end
43
+
44
+ def search(text, options)
45
+ output.puts text
46
+
47
+ loop do
48
+ choice = get_search_result(options)
49
+ break choice unless choice.nil?
50
+ end
45
51
  end
46
52
 
47
53
  private
@@ -52,74 +58,80 @@ module Abt
52
58
  end
53
59
  end
54
60
 
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
61
+ def select_option(options, nil_option)
62
+ number = prompt_valid_option_number(options, nil_option)
63
63
 
64
- option = options[number - 1]
64
+ return nil if number.nil?
65
65
 
66
- output.puts "Selected: (#{number}) #{option['name']}"
67
- return option
68
- end
66
+ option = options[number - 1]
67
+ output.puts "Selected: (#{number}) #{option['name']}"
68
+ option
69
69
  end
70
70
 
71
- def read_option_number(options_length, 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
77
-
78
- input = read_user_input
71
+ def prompt_valid_option_number(options, nil_option)
72
+ output.print(options_info(options, nil_option))
73
+ input = Abt::Helpers.read_user_input
79
74
 
80
75
  return nil if nil_option && input == nil_option_character(nil_option)
81
76
 
82
77
  option_number = input.to_i
83
- if option_number <= 0 || option_number > options_length
84
- output.puts 'Invalid selection'
85
- return nil
86
- end
78
+ return option_number if (1..options.length).cover?(option_number)
87
79
 
88
- option_number
80
+ output.puts "Invalid selection"
81
+
82
+ # Prompt again if the selection was invalid
83
+ prompt_valid_option_number(options, nil_option)
84
+ end
85
+
86
+ def options_info(options, nil_option)
87
+ str = "("
88
+ str += options.length > 1 ? "1-#{options.length}" : "1"
89
+ str += nil_option_string(nil_option)
90
+ str += "): "
91
+ str
89
92
  end
90
93
 
91
94
  def nil_option_string(nil_option)
92
- return '' unless nil_option
95
+ return "" unless nil_option
93
96
 
94
97
  ", #{nil_option_character(nil_option)}: #{nil_option_description(nil_option)}"
95
98
  end
96
99
 
97
100
  def nil_option_character(nil_option)
98
- return 'q' if nil_option == true
101
+ return "q" if nil_option == true
99
102
 
100
103
  nil_option[0]
101
104
  end
102
105
 
103
106
  def nil_option_description(nil_option)
104
- return 'back' if nil_option == true
107
+ return "back" if nil_option == true
105
108
  return nil_option if nil_option.is_a?(String)
106
109
 
107
110
  nil_option[1]
108
111
  end
109
112
 
110
- def read_user_input
111
- open(tty_path, &:gets).strip # rubocop:disable Security/Open
113
+ def get_search_result(options)
114
+ matches = matches_for_string(text("Enter search"), options)
115
+ if matches.empty?
116
+ output.puts("No matches")
117
+ return
118
+ end
119
+
120
+ output.puts("Showing the 10 first matches") if matches.size > 10
121
+ choice("Select a match", matches[0...10], nil_option: true)
112
122
  end
113
123
 
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?
124
+ def matches_for_string(string, options)
125
+ search_string = sanitize_string(string)
119
126
 
120
- selected
127
+ options.select do |option|
128
+ sanitize_string(option["name"]).include?(search_string)
121
129
  end
122
130
  end
131
+
132
+ def sanitize_string(string)
133
+ string.downcase.gsub(/[^\w]/, "")
134
+ end
123
135
  end
124
136
  end
125
137
  end
data/lib/abt/docs.rb CHANGED
@@ -9,49 +9,71 @@ module Abt
9
9
  class << self
10
10
  def basic_examples
11
11
  {
12
- 'Getting started:' => {
13
- 'abt init asana harvest' => 'Setup asana and harvest project for local git repo',
14
- 'abt pick harvest' => 'Pick harvest task. This will likely stay the same throughout the project',
15
- 'abt pick asana | abt start harvest' => 'Pick asana task and start tracking time',
16
- 'abt stop harvest' => 'Stop time tracker',
17
- 'abt start asana harvest' => 'Continue working, e.g., after a break',
18
- 'abt finalize asana' => 'Finalize the selected asana task'
12
+ "Getting started:" => {
13
+ "abt init asana harvest" => "Setup asana and harvest project for local git repo",
14
+ "abt pick harvest" => "Pick harvest task. This will likely stay the same throughout the project",
15
+ "abt pick asana | abt start harvest" => "Pick asana task and start tracking time",
16
+ "abt stop harvest" => "Stop time tracker",
17
+ "abt start asana harvest" => "Continue working, e.g., after a break",
18
+ "abt finalize asana" => "Finalize the selected asana task"
19
19
  }
20
20
  }
21
21
  end
22
22
 
23
- def extended_examples
23
+ def extended_examples # rubocop:disable Metrics/MethodLength
24
24
  {
25
- 'Tracking meetings (without switching current task setting):' => {
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'
25
+ "Tracking meetings (without switching current task setting):" => {
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"
28
28
  },
29
- 'Many commands output ARIs that can be piped into other commands:' => {
30
- 'abt tasks asana | grep -i <name of task>' => nil,
31
- 'abt tasks asana | grep -i <name of task> | abt start' => nil
29
+ "Many commands output ARIs that can be piped into other commands:" => {
30
+ "abt tasks asana | grep -i <name of task>" => nil,
31
+ "abt tasks asana | grep -i <name of task> | abt start" => nil
32
32
  },
33
- 'Sharing ARIs:' => {
34
- 'abt share asana harvest | tr "\n" " "' => 'Print current asana and harvest ARIs on a single line',
35
- 'abt share asana harvest | tr "\n" " " | pbcopy' => 'Copy ARIs to clipboard (mac only)',
36
- 'abt start <ARIs from coworker>' => 'Work on a task your coworker shared with you',
37
- 'abt current <ARIs from coworker> | abt start' => 'Set task as current, then start it'
33
+ "Sharing ARIs:" => {
34
+ 'abt share asana harvest | tr "\n" " "' => "Print current asana and harvest ARIs on a single line",
35
+ 'abt share asana harvest | tr "\n" " " | pbcopy' => "Copy ARIs to clipboard (mac only)",
36
+ "abt start <ARIs from coworker>" => "Work on a task your coworker shared with you",
37
+ "abt current <ARIs from coworker> | abt start" => "Set task as current, then start it"
38
38
  },
39
- 'Flags:' => {
40
- 'abt start harvest -c "comment"' => 'Add command flags after ARIs',
41
- 'abt start harvest -c "comment" -- asana' => 'Use -- to end a list of flags, so that it can be followed by another ARI',
42
- 'abt pick harvest | abt start -c "comment"' => 'Flags placed directly after a command applies to the piped in ARI'
39
+ "Flags:" => {
40
+ 'abt start harvest -c "comment"' => "Add command flags after ARIs",
41
+ 'abt start harvest -c "comment" -- asana' =>
42
+ "Use -- to end a list of flags, so that it can be followed by another ARI",
43
+ 'abt pick harvest | abt start -c "comment"' =>
44
+ "Flags placed directly after a command applies to the piped in ARI"
43
45
  }
44
46
  }
45
47
  end
46
48
 
47
49
  def providers
48
- @providers ||= Abt.schemes.sort.each_with_object({}) do |scheme, definition|
49
- definition[scheme] = command_definitions(scheme)
50
+ @providers ||= begin
51
+ providers = {}
52
+
53
+ providers["Global"] = global_command_definitions
54
+
55
+ Abt.schemes.sort.each_with_object(providers) do |scheme, definition|
56
+ definition[scheme] = command_definitions(scheme)
57
+ end
58
+
59
+ providers
50
60
  end
51
61
  end
52
62
 
53
63
  private
54
64
 
65
+ def global_command_definitions
66
+ global_command_names = Abt::Cli::GlobalCommands.command_names
67
+ global_command_names.each_with_object({}) do |name, definition|
68
+ command_class = Abt::Cli::GlobalCommands.command_class(name)
69
+ full_name = "abt #{name}"
70
+
71
+ if command_class.respond_to?(:usage) && command_class.respond_to?(:description)
72
+ definition[full_name] = [command_class.usage.strip, command_class.description.strip]
73
+ end
74
+ end
75
+ end
76
+
55
77
  def command_definitions(scheme)
56
78
  provider = Abt.scheme_provider(scheme)
57
79
  provider.command_names.each_with_object({}) do |name, definition|
@@ -59,7 +81,7 @@ module Abt
59
81
  full_name = "abt #{name} #{scheme}"
60
82
 
61
83
  if command_class.respond_to?(:usage) && command_class.respond_to?(:description)
62
- definition[full_name] = [command_class.usage, command_class.description]
84
+ definition[full_name] = [command_class.usage.strip, command_class.description.strip]
63
85
  end
64
86
  end
65
87
  end
data/lib/abt/docs/cli.rb CHANGED
@@ -45,14 +45,14 @@ module Abt
45
45
  private
46
46
 
47
47
  def usage_line
48
- 'abt <command> [<ARI>] [<options> --] [<ARI>] ...'
48
+ "abt <command> [<ARI>] [<options> --] [<ARI>] ..."
49
49
  end
50
50
 
51
51
  def formatted_examples(example_groups)
52
52
  lines = []
53
53
 
54
54
  example_groups.each_with_index do |(title, examples), index|
55
- lines << '' unless index.zero?
55
+ lines << "" unless index.zero?
56
56
  lines << title
57
57
 
58
58
  max_length = examples.keys.map(&:length).max
@@ -68,7 +68,7 @@ module Abt
68
68
  lines = []
69
69
 
70
70
  Docs.providers.each_with_index do |(scheme, commands_definition), index|
71
- lines << '' unless index.zero?
71
+ lines << "" unless index.zero?
72
72
  lines << "#{inflector.humanize(scheme)}:"
73
73
 
74
74
  max_length = commands_definition.keys.map(&:length).max
@@ -50,13 +50,12 @@ module Abt
50
50
  def example_commands
51
51
  lines = []
52
52
 
53
- examples = Docs.basic_examples.merge(Docs.extended_examples)
54
- examples.each_with_index do |(title, commands), index|
55
- lines << '' unless index.zero?
53
+ complete_examples.each_with_index do |(title, commands), index|
54
+ lines << "" unless index.zero?
56
55
  lines << title
57
56
 
58
57
  commands.each do |(command, description)|
59
- formatted_description = description.nil? ? '' : ": #{description}"
58
+ formatted_description = description.nil? ? "" : ": #{description}"
60
59
  lines << "- `#{command}`#{formatted_description}"
61
60
  end
62
61
  end
@@ -68,10 +67,10 @@ module Abt
68
67
  lines = []
69
68
 
70
69
  Docs.providers.each_with_index do |(scheme, commands), index|
71
- lines << '' unless index.zero?
70
+ lines << "" unless index.zero?
72
71
  lines << "### #{inflector.humanize(scheme)}"
73
- lines << '| Command | Description |'
74
- lines << '| :------ | :---------- |'
72
+ lines << "| Command | Description |"
73
+ lines << "| :------ | :---------- |"
75
74
 
76
75
  max_length = commands.values.map(&:first).map(&:length).max
77
76
 
@@ -84,6 +83,10 @@ module Abt
84
83
  lines.join("\n")
85
84
  end
86
85
 
86
+ def complete_examples
87
+ Docs.basic_examples.merge(Docs.extended_examples)
88
+ end
89
+
87
90
  def inflector
88
91
  Dry::Inflector.new
89
92
  end
@@ -6,12 +6,10 @@ module Abt
6
6
 
7
7
  class UnsafeNamespaceError < StandardError; end
8
8
 
9
- def initialize(scope = 'local', namespace = '')
9
+ def initialize(scope = "local", namespace = "")
10
10
  @namespace = namespace
11
11
 
12
- unless %w[local global].include? scope
13
- raise ArgumentError, 'scope must be "local" or "global"'
14
- end
12
+ raise ArgumentError, 'scope must be "local" or "global"' unless %w[local global].include?(scope)
15
13
 
16
14
  @scope = scope
17
15
  end
@@ -50,7 +48,7 @@ module Abt
50
48
  end
51
49
 
52
50
  def clear(output: nil)
53
- raise UnsafeNamespaceError, 'Keys can only be cleared within a namespace' if namespace.empty?
51
+ raise UnsafeNamespaceError, "Keys can only be cleared within a namespace" if namespace.empty?
54
52
 
55
53
  keys.each do |key|
56
54
  output&.puts "Clearing #{scope}: #{key_with_namespace(key)}"
@@ -67,7 +65,7 @@ module Abt
67
65
  def ensure_scope_available!
68
66
  return if available?
69
67
 
70
- raise StandardError, 'Local configuration is not available outside a git repository'
68
+ raise StandardError, "Local configuration is not available outside a git repository"
71
69
  end
72
70
 
73
71
  def key_with_namespace(key)
data/lib/abt/helpers.rb CHANGED
@@ -2,15 +2,33 @@
2
2
 
3
3
  module Abt
4
4
  module Helpers
5
- def self.const_to_command(string)
6
- string = string.to_s.dup
7
- string[0] = string[0].downcase
8
- string.gsub(/([A-Z])/, '-\1').downcase
9
- end
5
+ class << self
6
+ def const_to_command(string)
7
+ string = string.to_s.dup
8
+ string[0] = string[0].downcase
9
+ string.gsub(/([A-Z])/, '-\1').downcase
10
+ end
11
+
12
+ def command_to_const(string)
13
+ inflector = Dry::Inflector.new
14
+ inflector.camelize(inflector.underscore(string))
15
+ end
16
+
17
+ def read_user_input
18
+ open(tty_path, &:gets).strip # rubocop:disable Security/Open
19
+ end
20
+
21
+ private
22
+
23
+ def tty_path
24
+ @tty_path ||= begin
25
+ candidates = ["/dev/tty", "CON:"] # Unix: '/dev/tty', Windows: 'CON:'
26
+ selected = candidates.find { |candidate| File.exist?(candidate) }
27
+ raise Abort, "Unable to prompt for user input" if selected.nil?
10
28
 
11
- def self.command_to_const(string)
12
- inflector = Dry::Inflector.new
13
- inflector.camelize(inflector.underscore(string))
29
+ selected
30
+ end
31
+ end
14
32
  end
15
33
  end
16
34
  end