abt-cli 0.0.10 → 0.0.15

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +0 -6
  3. data/lib/abt.rb +8 -0
  4. data/lib/abt/cli.rb +27 -11
  5. data/lib/abt/cli/prompt.rb +124 -0
  6. data/lib/abt/git_config.rb +28 -11
  7. data/lib/abt/helpers.rb +1 -1
  8. data/lib/abt/providers/asana/base_command.rb +11 -0
  9. data/lib/abt/providers/asana/commands/add.rb +75 -0
  10. data/lib/abt/providers/asana/commands/branch-name.rb +44 -0
  11. data/lib/abt/providers/asana/commands/current.rb +3 -3
  12. data/lib/abt/providers/asana/commands/finalize.rb +1 -1
  13. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +3 -4
  14. data/lib/abt/providers/asana/commands/init.rb +7 -2
  15. data/lib/abt/providers/asana/commands/pick.rb +3 -2
  16. data/lib/abt/providers/asana/commands/share.rb +3 -3
  17. data/lib/abt/providers/asana/commands/start.rb +3 -3
  18. data/lib/abt/providers/asana/commands/tasks.rb +2 -0
  19. data/lib/abt/providers/asana/configuration.rb +7 -5
  20. data/lib/abt/providers/devops.rb +19 -0
  21. data/lib/abt/providers/devops/api.rb +95 -0
  22. data/lib/abt/providers/devops/base_command.rb +98 -0
  23. data/lib/abt/providers/devops/commands/boards.rb +34 -0
  24. data/lib/abt/providers/devops/commands/branch-name.rb +45 -0
  25. data/lib/abt/providers/devops/commands/clear.rb +24 -0
  26. data/lib/abt/providers/devops/commands/clear_global.rb +24 -0
  27. data/lib/abt/providers/devops/commands/current.rb +93 -0
  28. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +53 -0
  29. data/lib/abt/providers/devops/commands/init.rb +72 -0
  30. data/lib/abt/providers/devops/commands/pick.rb +78 -0
  31. data/lib/abt/providers/devops/commands/share.rb +26 -0
  32. data/lib/abt/providers/devops/commands/work-items.rb +46 -0
  33. data/lib/abt/providers/devops/configuration.rb +110 -0
  34. data/lib/abt/providers/git.rb +19 -0
  35. data/lib/abt/providers/git/commands/branch.rb +80 -0
  36. data/lib/abt/providers/harvest/base_command.rb +11 -0
  37. data/lib/abt/providers/harvest/commands/current.rb +3 -3
  38. data/lib/abt/providers/harvest/commands/init.rb +2 -2
  39. data/lib/abt/providers/harvest/commands/pick.rb +2 -1
  40. data/lib/abt/providers/harvest/commands/start.rb +2 -4
  41. data/lib/abt/providers/harvest/commands/tasks.rb +2 -0
  42. data/lib/abt/providers/harvest/commands/track.rb +2 -3
  43. data/lib/abt/providers/harvest/configuration.rb +6 -5
  44. data/lib/abt/version.rb +1 -1
  45. metadata +21 -4
  46. data/lib/abt/cli/dialogs.rb +0 -86
  47. data/lib/abt/cli/io.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b24bcf0b7e31cf18b1f86381f6dabba6434e8dfbd4a077f5447afdd20f08d7e
4
- data.tar.gz: a66846cb48a109ea21bb7366c1432c8f7d647b5384f42565c90fa609c51d8b6b
3
+ metadata.gz: f176b71200cc6fcc13ab56507e4d48234785e2307ab350afca7be13193ef2a5e
4
+ data.tar.gz: '093172885ed64949153cd73457748f446e012887f78d092f2483164f005fbfd0'
5
5
  SHA512:
6
- metadata.gz: b3610428c6ded90f53c49396d41b07fc54134dd88cb8019133c35dab230058b5506d93cfab0dc4b811b81161e45e24c1b0502dd7e03a8f3a65248db864886fb3
7
- data.tar.gz: 6affbc39d2572a73b700367d8cdabbe84275faf663b66b0c020c1683a3ffc7f584d43b828674cc2d32f1f05a8e2695443f13c64a2d1727e7fdb7708f93a0bcf5
6
+ metadata.gz: 6350132f05617d7e7222f0b7e07255091119ad0edac59277795c891668a6de4fb3ea9e376cdc93ab2672e130b5222d6ff310d80da8979dfaf060ff0e2479e447
7
+ data.tar.gz: 14eea7d7cb849f39d1aef4643df214541178d4f8702cecc962ffcff099b0c9143be68b8562aac0fdbc75b2bb2424872bd34661136104ac233df2097031d49394
data/bin/abt CHANGED
@@ -1,12 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'dry-inflector'
5
- require 'faraday'
6
- require 'oj'
7
- require 'open3'
8
- require 'stringio'
9
-
10
4
  require_relative '../lib/abt.rb'
11
5
 
12
6
  begin
data/lib/abt.rb CHANGED
@@ -1,10 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dry-inflector'
4
+ require 'faraday'
5
+ require 'oj'
6
+ require 'open3'
7
+ require 'stringio'
8
+
3
9
  Dir.glob("#{File.dirname(File.absolute_path(__FILE__))}/abt/*.rb").sort.each do |file|
4
10
  require file
5
11
  end
6
12
 
7
13
  module Abt
14
+ module Providers; end
15
+
8
16
  def self.provider_names
9
17
  Providers.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
10
18
  end
data/lib/abt/cli.rb CHANGED
@@ -8,10 +8,7 @@ module Abt
8
8
  class Cli
9
9
  class AbortError < StandardError; end
10
10
 
11
- include Dialogs
12
- include Io
13
-
14
- attr_reader :command, :args, :input, :output, :err_output
11
+ attr_reader :command, :args, :input, :output, :err_output, :prompt
15
12
 
16
13
  def initialize(argv: ARGV, input: STDIN, output: STDOUT, err_output: STDERR)
17
14
  (@command, *@args) = argv
@@ -19,12 +16,13 @@ module Abt
19
16
  @input = input
20
17
  @output = output
21
18
  @err_output = err_output
19
+ @prompt = Abt::Cli::Prompt.new(output: err_output)
22
20
 
23
21
  @args += args_from_input unless input.isatty # Add piped arguments
24
22
  end
25
23
 
26
24
  def perform
27
- handle_global_commands!
25
+ return if handle_global_commands!
28
26
 
29
27
  abort('No provider arguments') if args.empty?
30
28
 
@@ -37,6 +35,22 @@ module Abt
37
35
  output.puts command
38
36
  end
39
37
 
38
+ def warn(*args)
39
+ err_output.puts(*args)
40
+ end
41
+
42
+ def puts(*args)
43
+ output.puts(*args)
44
+ end
45
+
46
+ def print(*args)
47
+ output.print(*args)
48
+ end
49
+
50
+ def abort(message)
51
+ raise AbortError, message
52
+ end
53
+
40
54
  private
41
55
 
42
56
  def handle_global_commands! # rubocop:disable Metrics/MethodLength
@@ -44,21 +58,23 @@ module Abt
44
58
  when nil
45
59
  warn("No command specified\n\n")
46
60
  puts(Abt::Docs::Cli.content)
47
- exit
61
+ true
48
62
  when '--help', '-h', 'help', 'commands'
49
63
  puts(Abt::Docs::Cli.content)
50
- exit
64
+ true
51
65
  when 'help-md'
52
66
  puts(Abt::Docs::Markdown.content)
53
- exit
67
+ true
54
68
  when '--version', '-v', 'version'
55
69
  puts(Abt::VERSION)
56
- exit
70
+ true
71
+ else
72
+ false
57
73
  end
58
74
  end
59
75
 
60
76
  def args_from_input
61
- input_string = input.read
77
+ input_string = input.read.strip
62
78
 
63
79
  abort 'No input from pipe' if input_string.nil? || input_string.empty?
64
80
 
@@ -85,7 +101,7 @@ module Abt
85
101
  used_providers << provider if process_provider_command(provider, command, arg_str)
86
102
  end
87
103
 
88
- warn 'No matching providers found for command' if used_providers.empty? && output.isatty
104
+ abort 'No matching providers found for command' if used_providers.empty? && output.isatty
89
105
  end
90
106
 
91
107
  def process_provider_command(provider_name, command_name, arg_str)
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class Cli
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
15
+ end
16
+
17
+ def boolean(text)
18
+ output.puts text
19
+
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
31
+ end
32
+
33
+ def choice(text, options, nil_option = false)
34
+ output.puts "#{text}:"
35
+
36
+ if options.length.zero?
37
+ raise AbortError, 'No available options' unless nil_option
38
+
39
+ output.puts 'No available options'
40
+ return nil
41
+ end
42
+
43
+ print_options(options)
44
+ select_options(options, nil_option)
45
+ end
46
+
47
+ private
48
+
49
+ def print_options(options)
50
+ options.each_with_index do |option, index|
51
+ output.puts "(#{index + 1}) #{option['name']}"
52
+ end
53
+ end
54
+
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
63
+
64
+ option = options[number - 1]
65
+
66
+ output.puts "Selected: (#{number}) #{option['name']}"
67
+ return option
68
+ end
69
+ end
70
+
71
+ def read_option_number(options_length, nil_option)
72
+ output.print '('
73
+ output.print options_length > 1 ? "1-#{options_length}" : '1'
74
+ output.print nil_option_string(nil_option)
75
+ output.print '): '
76
+
77
+ input = read_user_input
78
+
79
+ return nil if nil_option && input == nil_option_character(nil_option)
80
+
81
+ option_number = input.to_i
82
+ if option_number <= 0 || option_number > options_length
83
+ output.puts 'Invalid selection'
84
+ return nil
85
+ end
86
+
87
+ option_number
88
+ end
89
+
90
+ def nil_option_string(nil_option)
91
+ return '' unless nil_option
92
+
93
+ ", #{nil_option_character(nil_option)}: #{nil_option_description(nil_option)}"
94
+ end
95
+
96
+ def nil_option_character(nil_option)
97
+ return 'q' if nil_option == true
98
+
99
+ nil_option[0]
100
+ end
101
+
102
+ def nil_option_description(nil_option)
103
+ return 'back' if nil_option == true
104
+ return nil_option if nil_option.is_a?(String)
105
+
106
+ nil_option[1]
107
+ end
108
+
109
+ def read_user_input
110
+ open(tty_path, &:gets).strip # rubocop:disable Security/Open
111
+ end
112
+
113
+ def tty_path
114
+ @tty_path ||= begin
115
+ candidates = ['/dev/tty', 'CON:'] # Unix: '/dev/tty', Windows: 'CON:'
116
+ selected = candidates.find { |candidate| File.exist?(candidate) }
117
+ raise AbortError, 'Unable to prompt for user input' if selected.nil?
118
+
119
+ selected
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -4,13 +4,17 @@ module Abt
4
4
  class GitConfig
5
5
  attr_reader :namespace, :scope
6
6
 
7
+ LOCAL_CONFIG_AVAILABLE_CHECK_COMMAND = 'git config --local -l'
8
+
7
9
  def self.local_available?
8
- @local_available ||= begin
9
- status = nil
10
- Open3.popen3('git config --local -l') do |_i, _o, _e, thread|
11
- status = thread.value
10
+ return @local_available if instance_variables.include?(:@local_available)
11
+
12
+ @local_available = begin
13
+ success = false
14
+ Open3.popen3(LOCAL_CONFIG_AVAILABLE_CHECK_COMMAND) do |_i, _o, _e, thread|
15
+ success = thread.value.success?
12
16
  end
13
- status.success?
17
+ success
14
18
  end
15
19
  end
16
20
 
@@ -32,6 +36,17 @@ module Abt
32
36
  set(key, value)
33
37
  end
34
38
 
39
+ def keys
40
+ offset = namespace.length + 1
41
+ full_keys.map { |key| key[offset..-1] }
42
+ end
43
+
44
+ def full_keys
45
+ ensure_scope_available!
46
+
47
+ `git config --#{scope} --get-regexp --name-only ^#{namespace}`.lines.map(&:strip)
48
+ end
49
+
35
50
  def local
36
51
  @local ||= begin
37
52
  if scope == 'local'
@@ -54,23 +69,25 @@ module Abt
54
69
 
55
70
  private
56
71
 
72
+ def ensure_scope_available!
73
+ return if scope != 'local' || self.class.local_available?
74
+
75
+ raise StandardError, 'Local configuration is not available outside a git repository'
76
+ end
77
+
57
78
  def key_with_namespace(key)
58
79
  namespace.empty? ? key : "#{namespace}.#{key}"
59
80
  end
60
81
 
61
82
  def get(key)
62
- if scope == 'local' && !self.class.local_available?
63
- raise StandardError, 'Local configuration is not available outside a git repository'
64
- end
83
+ ensure_scope_available!
65
84
 
66
85
  git_value = `git config --#{scope} --get #{key_with_namespace(key).inspect}`.strip
67
86
  git_value.empty? ? nil : git_value
68
87
  end
69
88
 
70
89
  def set(key, value)
71
- if scope == 'local' && !self.class.local_available?
72
- raise StandardError, 'Local configuration is not available outside a git repository'
73
- end
90
+ ensure_scope_available!
74
91
 
75
92
  if value.nil? || value.empty?
76
93
  `git config --#{scope} --unset #{key_with_namespace(key).inspect}`
data/lib/abt/helpers.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Abt
4
4
  module Helpers
5
5
  def self.const_to_command(string)
6
- string = string.to_s
6
+ string = string.to_s.dup
7
7
  string[0] = string[0].downcase
8
8
  string.gsub(/([A-Z])/, '-\1').downcase
9
9
  end
@@ -20,6 +20,17 @@ module Abt
20
20
 
21
21
  private
22
22
 
23
+ def require_project!
24
+ cli.abort 'No current/specified project. Did you initialize Asana?' if project_gid.nil?
25
+ end
26
+
27
+ def require_task!
28
+ if project_gid.nil?
29
+ cli.abort 'No current/specified project. Did you initialize Asana and pick a task?'
30
+ end
31
+ cli.abort 'No current/specified task. Did you pick an Asana task?' if task_gid.nil?
32
+ end
33
+
23
34
  def same_args_as_config?
24
35
  project_gid == config.project_gid && task_gid == config.task_gid
25
36
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Add < BaseCommand
8
+ def self.command
9
+ 'add asana[:<project-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Create a new task for the current/specified Asana project'
14
+ end
15
+
16
+ def call
17
+ require_project!
18
+
19
+ task
20
+ print_task(project, task)
21
+
22
+ move_task if section
23
+ end
24
+
25
+ private
26
+
27
+ def task
28
+ @task ||= begin
29
+ body = {
30
+ data: {
31
+ name: name,
32
+ notes: notes,
33
+ projects: [project_gid]
34
+ }
35
+ }
36
+ cli.warn 'Creating task'
37
+ api.post('tasks', Oj.dump(body, mode: :json))
38
+ end
39
+ end
40
+
41
+ def move_task
42
+ body = { data: { task: task['gid'] } }
43
+ body_json = Oj.dump(body, mode: :json)
44
+ api.post("sections/#{section['gid']}/addTask", body_json)
45
+ end
46
+
47
+ def name
48
+ @name ||= cli.prompt.text 'Enter task description'
49
+ end
50
+
51
+ def notes
52
+ @notes ||= cli.prompt.text 'Enter task notes'
53
+ end
54
+
55
+ def project
56
+ @project ||= api.get("projects/#{project_gid}")
57
+ end
58
+
59
+ def section
60
+ @section ||= cli.prompt.choice 'Add to section?', sections, ['q', 'Don\'t add to section']
61
+ end
62
+
63
+ def sections
64
+ @sections ||= begin
65
+ cli.warn 'Fetching sections...'
66
+ api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
67
+ rescue Abt::HttpError::HttpError
68
+ []
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class BranchName < BaseCommand
8
+ def self.command
9
+ 'branch-name asana[:<project-gid>/<task-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Suggest a git branch name for the current/specified task.'
14
+ end
15
+
16
+ def call
17
+ require_task!
18
+ ensure_current_is_valid!
19
+
20
+ cli.puts name
21
+ end
22
+
23
+ private
24
+
25
+ def name
26
+ task['name'].downcase.gsub(/[^\w]+/, '-')
27
+ end
28
+
29
+ def ensure_current_is_valid!
30
+ cli.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
+ cli.abort "Invalid project gid: #{project_gid}"
35
+ end
36
+
37
+ def task
38
+ @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.project')
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end