abt-cli 0.0.20 → 0.0.25

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 +71 -56
  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 -14
  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 +37 -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,47 @@ 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
- def boolean(text)
18
- output.puts text
17
+ def boolean(text, default: nil)
18
+ choices = [default == true ? "Y" : "y",
19
+ default == false ? "N" : "n"].join("/")
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
30
- end
21
+ output.print("#{text} (#{choices}): ")
22
+
23
+ input = Abt::Helpers.read_user_input.downcase
24
+
25
+ return true if input == "y"
26
+ return false if input == "n"
27
+ return default if input.empty? && !default.nil?
28
+
29
+ output.puts "Invalid choice"
30
+ boolean(text, default: default)
31
31
  end
32
32
 
33
- def choice(text, options, nil_option = false)
34
- output.puts "#{text}:"
33
+ def choice(text, options, nil_option: false)
34
+ output.puts "#{text.strip}:"
35
35
 
36
36
  if options.length.zero?
37
- raise Abort, 'No available options' unless nil_option
37
+ raise Abort, "No available options" unless nil_option
38
38
 
39
- output.puts 'No available options'
39
+ output.puts "No available options"
40
40
  return nil
41
41
  end
42
42
 
43
43
  print_options(options)
44
- select_options(options, nil_option)
44
+ select_option(options, nil_option)
45
+ end
46
+
47
+ def search(text, options)
48
+ output.puts text
49
+
50
+ loop do
51
+ choice = get_search_result(options)
52
+ break choice unless choice.nil?
53
+ end
45
54
  end
46
55
 
47
56
  private
@@ -52,74 +61,80 @@ module Abt
52
61
  end
53
62
  end
54
63
 
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
64
+ def select_option(options, nil_option)
65
+ number = prompt_valid_option_number(options, nil_option)
63
66
 
64
- option = options[number - 1]
67
+ return nil if number.nil?
65
68
 
66
- output.puts "Selected: (#{number}) #{option['name']}"
67
- return option
68
- end
69
+ option = options[number - 1]
70
+ output.puts "Selected: (#{number}) #{option['name']}"
71
+ option
69
72
  end
70
73
 
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
74
+ def prompt_valid_option_number(options, nil_option)
75
+ output.print(options_info(options, nil_option))
76
+ input = Abt::Helpers.read_user_input
79
77
 
80
78
  return nil if nil_option && input == nil_option_character(nil_option)
81
79
 
82
80
  option_number = input.to_i
83
- if option_number <= 0 || option_number > options_length
84
- output.puts 'Invalid selection'
85
- return nil
86
- end
81
+ return option_number if (1..options.length).cover?(option_number)
82
+
83
+ output.puts "Invalid selection"
87
84
 
88
- option_number
85
+ # Prompt again if the selection was invalid
86
+ prompt_valid_option_number(options, nil_option)
87
+ end
88
+
89
+ def options_info(options, nil_option)
90
+ str = "("
91
+ str += options.length > 1 ? "1-#{options.length}" : "1"
92
+ str += nil_option_string(nil_option)
93
+ str += "): "
94
+ str
89
95
  end
90
96
 
91
97
  def nil_option_string(nil_option)
92
- return '' unless nil_option
98
+ return "" unless nil_option
93
99
 
94
100
  ", #{nil_option_character(nil_option)}: #{nil_option_description(nil_option)}"
95
101
  end
96
102
 
97
103
  def nil_option_character(nil_option)
98
- return 'q' if nil_option == true
104
+ return "q" if nil_option == true
99
105
 
100
106
  nil_option[0]
101
107
  end
102
108
 
103
109
  def nil_option_description(nil_option)
104
- return 'back' if nil_option == true
110
+ return "back" if nil_option == true
105
111
  return nil_option if nil_option.is_a?(String)
106
112
 
107
113
  nil_option[1]
108
114
  end
109
115
 
110
- def read_user_input
111
- open(tty_path, &:gets).strip # rubocop:disable Security/Open
116
+ def get_search_result(options)
117
+ matches = matches_for_string(text("Enter search"), options)
118
+ if matches.empty?
119
+ output.puts("No matches")
120
+ return
121
+ end
122
+
123
+ output.puts("Showing the 10 first matches") if matches.size > 10
124
+ choice("Select a match", matches[0...10], nil_option: true)
112
125
  end
113
126
 
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?
127
+ def matches_for_string(string, options)
128
+ search_string = sanitize_string(string)
119
129
 
120
- selected
130
+ options.select do |option|
131
+ sanitize_string(option["name"]).include?(search_string)
121
132
  end
122
133
  end
134
+
135
+ def sanitize_string(string)
136
+ string.downcase.gsub(/[^\w]/, "")
137
+ end
123
138
  end
124
139
  end
125
140
  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