abt-cli 0.0.22 → 0.0.27

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +2 -2
  3. data/lib/abt.rb +5 -0
  4. data/lib/abt/cli.rb +28 -9
  5. data/lib/abt/cli/prompt.rb +37 -53
  6. data/lib/abt/directory_config.rb +25 -0
  7. data/lib/abt/docs.rb +10 -6
  8. data/lib/abt/docs/markdown.rb +5 -2
  9. data/lib/abt/helpers.rb +26 -8
  10. data/lib/abt/providers/asana.rb +1 -0
  11. data/lib/abt/providers/asana/base_command.rb +37 -3
  12. data/lib/abt/providers/asana/commands/add.rb +0 -4
  13. data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
  14. data/lib/abt/providers/asana/commands/current.rb +1 -20
  15. data/lib/abt/providers/asana/commands/finalize.rb +6 -2
  16. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +7 -5
  17. data/lib/abt/providers/asana/commands/pick.rb +12 -46
  18. data/lib/abt/providers/asana/commands/start.rb +9 -3
  19. data/lib/abt/providers/asana/commands/tasks.rb +2 -7
  20. data/lib/abt/providers/asana/configuration.rb +28 -12
  21. data/lib/abt/providers/asana/path.rb +1 -1
  22. data/lib/abt/providers/asana/services/project_picker.rb +54 -0
  23. data/lib/abt/providers/asana/services/task_picker.rb +83 -0
  24. data/lib/abt/providers/devops.rb +1 -0
  25. data/lib/abt/providers/devops/api.rb +10 -0
  26. data/lib/abt/providers/devops/base_command.rb +38 -14
  27. data/lib/abt/providers/devops/commands/boards.rb +1 -2
  28. data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
  29. data/lib/abt/providers/devops/commands/current.rb +1 -21
  30. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +16 -20
  31. data/lib/abt/providers/devops/commands/pick.rb +18 -39
  32. data/lib/abt/providers/devops/commands/work_items.rb +3 -6
  33. data/lib/abt/providers/devops/configuration.rb +10 -14
  34. data/lib/abt/providers/devops/path.rb +4 -4
  35. data/lib/abt/providers/devops/services/board_picker.rb +54 -0
  36. data/lib/abt/providers/devops/services/project_picker.rb +79 -0
  37. data/lib/abt/providers/devops/services/work_item_picker.rb +93 -0
  38. data/lib/abt/providers/git/commands/branch.rb +7 -3
  39. data/lib/abt/providers/harvest.rb +1 -0
  40. data/lib/abt/providers/harvest/base_command.rb +49 -3
  41. data/lib/abt/providers/harvest/commands/current.rb +1 -30
  42. data/lib/abt/providers/harvest/commands/pick.rb +12 -23
  43. data/lib/abt/providers/harvest/commands/projects.rb +0 -5
  44. data/lib/abt/providers/harvest/commands/tasks.rb +1 -16
  45. data/lib/abt/providers/harvest/commands/track.rb +33 -19
  46. data/lib/abt/providers/harvest/configuration.rb +1 -1
  47. data/lib/abt/providers/harvest/path.rb +1 -1
  48. data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
  49. data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
  50. data/lib/abt/version.rb +1 -1
  51. metadata +10 -5
  52. data/lib/abt/providers/asana/commands/init.rb +0 -42
  53. data/lib/abt/providers/devops/commands/init.rb +0 -76
  54. data/lib/abt/providers/harvest/commands/init.rb +0 -54
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b8172eca54c77feb083acbc9895bf9f221b69efc1c0b8170d87f250b486286a
4
- data.tar.gz: e07e6122c3b3c3bc1307769f5d52d01cb6db9126df0fe10bfb217b940a1eaf12
3
+ metadata.gz: 0bffe51f05670b4fcbf1bf2a023c354e26a387b0a74fd34751ef180b1f619c62
4
+ data.tar.gz: 7e5c476b8e1b803adfbb92590676c883e66a6ac74b5111e83aed9f4d6b9feb8a
5
5
  SHA512:
6
- metadata.gz: 5eee8193b985110d79170134593fa1ac03b50f6be36b43fa99bb99c8bdcf51d531f9e2b10fe5bca919524f388f503a3a46b9d74bf4764226e6f8d94b905d12ca
7
- data.tar.gz: 3a3d7f288dbf3faaab041934bcf703afbaa87b989a1e869bae02a084c83834fd02a1a93df78ed06b2bb6a0b29f4167cdae8a972940c6684e9660dd58d06a78ba
6
+ metadata.gz: 9dba1d7966f28966e5fe8d33fd3b380e823f707536b86e1f21a82bfdb74e20ba4d76b2c08e1fb9da9b7f1dc1d74ea6a787979b6ae86ca59b3ce9305964f7a063
7
+ data.tar.gz: 33b61911f3018b0e51ece32264c98f8327af60fcfb94bb59e4d210171bf8562fb885c120be565f79a827dd7f35c72d4d91acf48aa8497079107e120d8e4535bf
data/bin/abt CHANGED
@@ -6,7 +6,7 @@ require_relative "../lib/abt"
6
6
  begin
7
7
  Abt::Cli.new.perform
8
8
  rescue Abt::Cli::Abort => e
9
- abort(e.message)
9
+ abort(e.message.strip)
10
10
  rescue Interrupt
11
- abort("Aborted")
11
+ exit 130
12
12
  end
data/lib/abt.rb CHANGED
@@ -6,6 +6,7 @@ require "oj"
6
6
  require "open3"
7
7
  require "stringio"
8
8
  require "optparse"
9
+ require "yaml"
9
10
 
10
11
  Dir.glob("#{File.dirname(File.absolute_path(__FILE__))}/abt/*.rb").sort.each do |file|
11
12
  require file
@@ -22,4 +23,8 @@ module Abt
22
23
  const_name = Helpers.command_to_const(scheme)
23
24
  Providers.const_get(const_name) if Providers.const_defined?(const_name)
24
25
  end
26
+
27
+ def self.directory_config
28
+ @directory_config ||= Abt::DirectoryConfig.new
29
+ end
25
30
  end
data/lib/abt/cli.rb CHANGED
@@ -5,20 +5,19 @@ Dir.glob("#{File.expand_path(__dir__)}/cli/*.rb").sort.each do |file|
5
5
  end
6
6
 
7
7
  module Abt
8
- class Cli
8
+ class Cli # rubocop:disable Metrics/ClassLength
9
9
  class Abort < StandardError; end
10
10
 
11
11
  class Exit < StandardError; end
12
12
 
13
- attr_reader :command, :aris, :input, :output, :err_output, :prompt
13
+ attr_reader :command, :remaining_args, :input, :output, :err_output, :prompt
14
14
 
15
15
  def initialize(argv: ARGV, input: $stdin, output: $stdout, err_output: $stderr)
16
- (@command, *remaining_args) = argv
16
+ (@command, *@remaining_args) = argv
17
17
  @input = input
18
18
  @output = output
19
19
  @err_output = err_output
20
20
  @prompt = Abt::Cli::Prompt.new(output: err_output)
21
- @aris = ArgumentsParser.new(sanitized_piped_args + remaining_args).parse
22
21
  end
23
22
 
24
23
  def perform
@@ -27,11 +26,10 @@ module Abt
27
26
  @command = "help"
28
27
  end
29
28
 
30
- if global_command?
31
- process_global_command
32
- else
33
- process_aris
34
- end
29
+ return process_alias if alias?
30
+ return process_global_command if global_command?
31
+
32
+ process_aris
35
33
  end
36
34
 
37
35
  def print_ari(scheme, path, description = nil)
@@ -60,8 +58,29 @@ module Abt
60
58
  raise Exit, message
61
59
  end
62
60
 
61
+ def aris
62
+ @aris ||= ArgumentsParser.new(sanitized_piped_args + remaining_args).parse
63
+ end
64
+
63
65
  private
64
66
 
67
+ def alias?
68
+ command[0] == "@"
69
+ end
70
+
71
+ def process_alias
72
+ matching_alias = Abt.directory_config.dig("aliases", command[1..-1])
73
+
74
+ abort("No such alias #{command}") if matching_alias.nil?
75
+
76
+ with_args = matching_alias.sub("$@", remaining_args.join(" "))
77
+ with_program_name = with_args.gsub("$0", $PROGRAM_NAME).strip
78
+ humanized = with_args.gsub("$0", "abt").strip
79
+
80
+ warn(humanized)
81
+ system(with_program_name)
82
+ end
83
+
65
84
  def global_command?
66
85
  return true if aris.empty?
67
86
  return true if aris.first.scheme.nil?
@@ -11,22 +11,23 @@ module Abt
11
11
 
12
12
  def text(question)
13
13
  output.print("#{question.strip}: ")
14
- read_user_input
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
- end
29
- 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)
30
31
  end
31
32
 
32
33
  def choice(text, options, nil_option: false)
@@ -40,7 +41,7 @@ module Abt
40
41
  end
41
42
 
42
43
  print_options(options)
43
- select_options(options, nil_option)
44
+ select_option(options, nil_option)
44
45
  end
45
46
 
46
47
  def search(text, options)
@@ -60,40 +61,37 @@ module Abt
60
61
  end
61
62
  end
62
63
 
63
- def select_options(options, nil_option)
64
- loop do
65
- number = read_option_number(options.length, nil_option)
66
- if number.nil?
67
- return nil if nil_option
68
-
69
- next
70
- end
64
+ def select_option(options, nil_option)
65
+ number = prompt_valid_option_number(options, nil_option)
71
66
 
72
- option = options[number - 1]
67
+ return nil if number.nil?
73
68
 
74
- output.puts "Selected: (#{number}) #{option['name']}"
75
- return option
76
- end
69
+ option = options[number - 1]
70
+ output.puts "Selected: (#{number}) #{option['name']}"
71
+ option
77
72
  end
78
73
 
79
- def read_option_number(options_length, nil_option)
80
- str = "("
81
- str += options_length > 1 ? "1-#{options_length}" : "1"
82
- str += nil_option_string(nil_option)
83
- str += "): "
84
- output.print(str)
85
-
86
- 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
87
77
 
88
78
  return nil if nil_option && input == nil_option_character(nil_option)
89
79
 
90
80
  option_number = input.to_i
91
- if option_number <= 0 || option_number > options_length
92
- output.puts "Invalid selection"
93
- return nil
94
- end
81
+ return option_number if (1..options.length).cover?(option_number)
82
+
83
+ output.puts "Invalid selection"
84
+
85
+ # Prompt again if the selection was invalid
86
+ prompt_valid_option_number(options, nil_option)
87
+ end
95
88
 
96
- option_number
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
97
95
  end
98
96
 
99
97
  def nil_option_string(nil_option)
@@ -115,10 +113,6 @@ module Abt
115
113
  nil_option[1]
116
114
  end
117
115
 
118
- def read_user_input
119
- open(tty_path, &:gets).strip # rubocop:disable Security/Open
120
- end
121
-
122
116
  def get_search_result(options)
123
117
  matches = matches_for_string(text("Enter search"), options)
124
118
  if matches.empty?
@@ -141,16 +135,6 @@ module Abt
141
135
  def sanitize_string(string)
142
136
  string.downcase.gsub(/[^\w]/, "")
143
137
  end
144
-
145
- def tty_path
146
- @tty_path ||= begin
147
- candidates = ["/dev/tty", "CON:"] # Unix: '/dev/tty', Windows: 'CON:'
148
- selected = candidates.find { |candidate| File.exist?(candidate) }
149
- raise Abort, "Unable to prompt for user input" if selected.nil?
150
-
151
- selected
152
- end
153
- end
154
138
  end
155
139
  end
156
140
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class DirectoryConfig < Hash
5
+ def initialize
6
+ super
7
+ merge!(YAML.load_file(config_file_path)) if config_file_path
8
+ end
9
+
10
+ private
11
+
12
+ def config_file_path
13
+ dir = Dir.pwd
14
+
15
+ until File.exist?(File.join(dir, ".abt.yml"))
16
+ next_dir = File.expand_path("..", dir)
17
+ return if next_dir == dir
18
+
19
+ dir = next_dir
20
+ end
21
+
22
+ File.join(dir, ".abt.yml")
23
+ end
24
+ end
25
+ end
data/lib/abt/docs.rb CHANGED
@@ -10,11 +10,10 @@ module Abt
10
10
  def basic_examples
11
11
  {
12
12
  "Getting started:" => {
13
- "abt init asana harvest" => "Setup asana and harvest project for local git repo",
14
13
  "abt pick harvest" => "Pick harvest task. This will likely stay the same throughout the project",
15
14
  "abt pick asana | abt start harvest" => "Pick asana task and start tracking time",
16
15
  "abt stop harvest" => "Stop time tracker",
17
- "abt start asana harvest" => "Continue working, e.g., after a break",
16
+ "abt track asana harvest" => "Continue tracking time, e.g., after a break",
18
17
  "abt finalize asana" => "Finalize the selected asana task"
19
18
  }
20
19
  }
@@ -31,10 +30,15 @@ module Abt
31
30
  "abt tasks asana | grep -i <name of task> | abt start" => nil
32
31
  },
33
32
  "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
+ "abt share" => "Print current asana and harvest ARIs on a single line",
34
+ "abt share | pbcopy" => "Copy ARIs to clipboard (mac only)",
35
+ "abt track <ARIs from coworker>" => "Start tracking on the task your coworker shared with you",
36
+ "abt current <ARIs from coworker> | abt track" => "Set task as current, then start tracking"
37
+ },
38
+ "One-off tracking on any project": {
39
+ "abt pick asana -dc -- harvest -dc | abt track" =>
40
+ "Find a track any task on any project, without reusing/affecting previous settings",
41
+ "abt pick asana harvest | abt track" => "Can be used instead of the above when outside a git repo"
38
42
  },
39
43
  "Flags:" => {
40
44
  'abt start harvest -c "comment"' => "Add command flags after ARIs",
@@ -50,8 +50,7 @@ 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|
53
+ complete_examples.each_with_index do |(title, commands), index|
55
54
  lines << "" unless index.zero?
56
55
  lines << title
57
56
 
@@ -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
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  Dir.glob("#{File.expand_path(__dir__)}/asana/*.rb").sort.each { |file| require file }
4
4
  Dir.glob("#{File.expand_path(__dir__)}/asana/commands/*.rb").sort.each { |file| require file }
5
+ Dir.glob("#{File.expand_path(__dir__)}/asana/services/*.rb").sort.each { |file| require file }
5
6
 
6
7
  module Abt
7
8
  module Providers
@@ -20,13 +20,47 @@ module Abt
20
20
 
21
21
  private
22
22
 
23
+ def require_local_config!
24
+ abort("Must be run inside a git repository") unless config.local_available?
25
+ end
26
+
23
27
  def require_project!
24
- abort("No current/specified project. Did you initialize Asana?") if project_gid.nil?
28
+ abort("No current/specified project. Did you forget to run `pick`?") if project_gid.nil?
25
29
  end
26
30
 
27
31
  def require_task!
28
- abort("No current/specified project. Did you initialize Asana and pick a task?") if project_gid.nil?
29
- abort("No current/specified task. Did you pick an Asana task?") if task_gid.nil?
32
+ require_project!
33
+ abort("No current/specified task. Did you forget to run `pick`?") if task_gid.nil?
34
+ end
35
+
36
+ def prompt_project!
37
+ result = Services::ProjectPicker.call(cli: cli, config: config)
38
+ @path = result.path
39
+ @project = result.project
40
+ end
41
+
42
+ def prompt_task!
43
+ result = Services::TaskPicker.call(cli: cli, path: path, config: config, project: project)
44
+ @path = result.path
45
+ @task = result.task
46
+ end
47
+
48
+ def task
49
+ @task ||= begin
50
+ warn("Fetching task...")
51
+ api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
52
+ rescue Abt::HttpError::NotFoundError
53
+ nil
54
+ end
55
+ end
56
+
57
+ def project
58
+ @project ||= begin
59
+ warn("Fetching project...")
60
+ api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
61
+ rescue Abt::HttpError::NotFoundError
62
+ nil
63
+ end
30
64
  end
31
65
 
32
66
  def print_project(project)
@@ -56,10 +56,6 @@ module Abt
56
56
  @notes ||= cli.prompt.text("Enter task notes")
57
57
  end
58
58
 
59
- def project
60
- @project ||= api.get("projects/#{project_gid}", opt_fields: "name")
61
- end
62
-
63
59
  def section
64
60
  @section ||= cli.prompt.choice("Add to section?", sections,
65
61
  nil_option: ["q", "Don't add to section"])
@@ -28,19 +28,6 @@ module Abt
28
28
 
29
29
  def ensure_current_is_valid!
30
30
  abort("Invalid task gid: #{task_gid}") if task.nil?
31
-
32
- return if task["memberships"].any? { |m| m.dig("project", "gid") == project_gid }
33
-
34
- abort("Invalid or unmatching project gid: #{project_gid}")
35
- end
36
-
37
- def task
38
- @task ||= begin
39
- warn("Fetching task...")
40
- api.get("tasks/#{task_gid}", opt_fields: "name,memberships.project")
41
- rescue Abt::HttpError::NotFoundError
42
- nil
43
- end
44
31
  end
45
32
  end
46
33
  end