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.
- checksums.yaml +4 -4
- data/bin/abt +0 -6
- data/lib/abt.rb +8 -0
- data/lib/abt/cli.rb +27 -11
- data/lib/abt/cli/prompt.rb +124 -0
- data/lib/abt/git_config.rb +28 -11
- data/lib/abt/helpers.rb +1 -1
- data/lib/abt/providers/asana/base_command.rb +11 -0
- data/lib/abt/providers/asana/commands/add.rb +75 -0
- data/lib/abt/providers/asana/commands/branch-name.rb +44 -0
- data/lib/abt/providers/asana/commands/current.rb +3 -3
- data/lib/abt/providers/asana/commands/finalize.rb +1 -1
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +3 -4
- data/lib/abt/providers/asana/commands/init.rb +7 -2
- data/lib/abt/providers/asana/commands/pick.rb +3 -2
- data/lib/abt/providers/asana/commands/share.rb +3 -3
- data/lib/abt/providers/asana/commands/start.rb +3 -3
- data/lib/abt/providers/asana/commands/tasks.rb +2 -0
- data/lib/abt/providers/asana/configuration.rb +7 -5
- data/lib/abt/providers/devops.rb +19 -0
- data/lib/abt/providers/devops/api.rb +95 -0
- data/lib/abt/providers/devops/base_command.rb +98 -0
- data/lib/abt/providers/devops/commands/boards.rb +34 -0
- data/lib/abt/providers/devops/commands/branch-name.rb +45 -0
- data/lib/abt/providers/devops/commands/clear.rb +24 -0
- data/lib/abt/providers/devops/commands/clear_global.rb +24 -0
- data/lib/abt/providers/devops/commands/current.rb +93 -0
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +53 -0
- data/lib/abt/providers/devops/commands/init.rb +72 -0
- data/lib/abt/providers/devops/commands/pick.rb +78 -0
- data/lib/abt/providers/devops/commands/share.rb +26 -0
- data/lib/abt/providers/devops/commands/work-items.rb +46 -0
- data/lib/abt/providers/devops/configuration.rb +110 -0
- data/lib/abt/providers/git.rb +19 -0
- data/lib/abt/providers/git/commands/branch.rb +80 -0
- data/lib/abt/providers/harvest/base_command.rb +11 -0
- data/lib/abt/providers/harvest/commands/current.rb +3 -3
- data/lib/abt/providers/harvest/commands/init.rb +2 -2
- data/lib/abt/providers/harvest/commands/pick.rb +2 -1
- data/lib/abt/providers/harvest/commands/start.rb +2 -4
- data/lib/abt/providers/harvest/commands/tasks.rb +2 -0
- data/lib/abt/providers/harvest/commands/track.rb +2 -3
- data/lib/abt/providers/harvest/configuration.rb +6 -5
- data/lib/abt/version.rb +1 -1
- metadata +21 -4
- data/lib/abt/cli/dialogs.rb +0 -86
- data/lib/abt/cli/io.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f176b71200cc6fcc13ab56507e4d48234785e2307ab350afca7be13193ef2a5e
|
4
|
+
data.tar.gz: '093172885ed64949153cd73457748f446e012887f78d092f2483164f005fbfd0'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6350132f05617d7e7222f0b7e07255091119ad0edac59277795c891668a6de4fb3ea9e376cdc93ab2672e130b5222d6ff310d80da8979dfaf060ff0e2479e447
|
7
|
+
data.tar.gz: 14eea7d7cb849f39d1aef4643df214541178d4f8702cecc962ffcff099b0c9143be68b8562aac0fdbc75b2bb2424872bd34661136104ac233df2097031d49394
|
data/bin/abt
CHANGED
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
|
-
|
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
|
-
|
61
|
+
true
|
48
62
|
when '--help', '-h', 'help', 'commands'
|
49
63
|
puts(Abt::Docs::Cli.content)
|
50
|
-
|
64
|
+
true
|
51
65
|
when 'help-md'
|
52
66
|
puts(Abt::Docs::Markdown.content)
|
53
|
-
|
67
|
+
true
|
54
68
|
when '--version', '-v', 'version'
|
55
69
|
puts(Abt::VERSION)
|
56
|
-
|
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
|
-
|
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
|
data/lib/abt/git_config.rb
CHANGED
@@ -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
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
@@ -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
|