abt-cli 0.0.24 → 0.0.29
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 +1 -1
- data/lib/abt.rb +1 -0
- data/lib/abt/cli.rb +32 -9
- data/lib/abt/cli/prompt.rb +12 -9
- data/lib/abt/directory_config.rb +43 -0
- data/lib/abt/docs.rb +10 -6
- data/lib/abt/providers/asana.rb +1 -0
- data/lib/abt/providers/asana/base_command.rb +33 -3
- data/lib/abt/providers/asana/commands/add.rb +0 -4
- data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
- data/lib/abt/providers/asana/commands/current.rb +0 -18
- data/lib/abt/providers/asana/commands/finalize.rb +2 -4
- data/lib/abt/providers/asana/commands/pick.rb +11 -41
- data/lib/abt/providers/asana/commands/tasks.rb +2 -7
- data/lib/abt/providers/asana/commands/write_config.rb +73 -0
- data/lib/abt/providers/asana/configuration.rb +11 -3
- data/lib/abt/providers/asana/path.rb +2 -2
- data/lib/abt/providers/asana/services/project_picker.rb +54 -0
- data/lib/abt/providers/asana/services/task_picker.rb +83 -0
- data/lib/abt/providers/devops.rb +1 -0
- data/lib/abt/providers/devops/api.rb +10 -0
- data/lib/abt/providers/devops/base_command.rb +34 -14
- data/lib/abt/providers/devops/commands/boards.rb +1 -2
- data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
- data/lib/abt/providers/devops/commands/current.rb +0 -19
- data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +10 -16
- data/lib/abt/providers/devops/commands/pick.rb +11 -47
- data/lib/abt/providers/devops/commands/work_items.rb +3 -6
- data/lib/abt/providers/devops/commands/write_config.rb +47 -0
- data/lib/abt/providers/devops/configuration.rb +1 -1
- data/lib/abt/providers/devops/path.rb +3 -3
- data/lib/abt/providers/devops/services/board_picker.rb +58 -0
- data/lib/abt/providers/devops/services/project_picker.rb +73 -0
- data/lib/abt/providers/devops/services/work_item_picker.rb +98 -0
- data/lib/abt/providers/git/commands/branch.rb +1 -1
- data/lib/abt/providers/harvest.rb +1 -0
- data/lib/abt/providers/harvest/base_command.rb +45 -3
- data/lib/abt/providers/harvest/commands/current.rb +0 -28
- data/lib/abt/providers/harvest/commands/pick.rb +12 -27
- data/lib/abt/providers/harvest/commands/projects.rb +2 -9
- data/lib/abt/providers/harvest/commands/tasks.rb +2 -19
- data/lib/abt/providers/harvest/commands/write_config.rb +41 -0
- data/lib/abt/providers/harvest/configuration.rb +1 -1
- data/lib/abt/providers/harvest/path.rb +1 -1
- data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
- data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
- data/lib/abt/version.rb +1 -1
- metadata +13 -5
- data/lib/abt/providers/asana/commands/init.rb +0 -42
- data/lib/abt/providers/devops/commands/init.rb +0 -79
- data/lib/abt/providers/harvest/commands/init.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1fbc512b85932a7c8cfa8e6d9ec10176f3e2ac8711df78ad4ce67100ac79f003
|
4
|
+
data.tar.gz: 7dc1c727ced7d316c84075458b75839bd931050e50c9ffe39f925324c6a8e63d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 74f84e028cf84f156ee18f46177fae4e17f857eb538362daf2baf3b017f1ace428a8b9c16aa912ba0c486fdc13cbf2a41402fa64917fa74ee9578c6e041c6d7b
|
7
|
+
data.tar.gz: fe06b4314f9a119950979b7a8c966758a56b4a3619dcabc9328209beaca94f317851bc9dedbc986a955a120afe29da0707225e78896e1517854c24864bba2efc
|
data/bin/abt
CHANGED
data/lib/abt.rb
CHANGED
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, :
|
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,
|
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
|
31
|
-
|
32
|
-
|
33
|
-
|
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,33 @@ 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
|
+
|
65
|
+
def directory_config
|
66
|
+
@directory_config ||= Abt::DirectoryConfig.new
|
67
|
+
end
|
68
|
+
|
63
69
|
private
|
64
70
|
|
71
|
+
def alias?
|
72
|
+
command[0] == "@"
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_alias
|
76
|
+
matching_alias = directory_config.dig("aliases", command[1..-1])
|
77
|
+
|
78
|
+
abort("No such alias #{command}") if matching_alias.nil?
|
79
|
+
|
80
|
+
with_args = matching_alias.sub("$@", remaining_args.join(" "))
|
81
|
+
with_program_name = with_args.gsub("$0", $PROGRAM_NAME).strip
|
82
|
+
humanized = with_args.gsub("$0", "abt").strip
|
83
|
+
|
84
|
+
warn(humanized)
|
85
|
+
system(with_program_name)
|
86
|
+
end
|
87
|
+
|
65
88
|
def global_command?
|
66
89
|
return true if aris.empty?
|
67
90
|
return true if aris.first.scheme.nil?
|
data/lib/abt/cli/prompt.rb
CHANGED
@@ -14,17 +14,20 @@ module Abt
|
|
14
14
|
Abt::Helpers.read_user_input
|
15
15
|
end
|
16
16
|
|
17
|
-
def boolean(text)
|
18
|
-
|
17
|
+
def boolean(text, default: nil)
|
18
|
+
choices = [default == true ? "Y" : "y",
|
19
|
+
default == false ? "N" : "n"].join("/")
|
19
20
|
|
20
|
-
|
21
|
-
output.print("(y / n): ")
|
21
|
+
output.print("#{text} (#{choices}): ")
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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)
|
28
31
|
end
|
29
32
|
|
30
33
|
def choice(text, options, nil_option: false)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
class DirectoryConfig < Hash
|
5
|
+
FILE_NAME = ".abt.yml"
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
load! if config_file_path && File.exist?(config_file_path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def available?
|
13
|
+
!config_file_path.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def load!
|
17
|
+
merge!(YAML.load_file(config_file_path))
|
18
|
+
end
|
19
|
+
|
20
|
+
def save!
|
21
|
+
raise Abt::Cli::Abort("Configuration files are not available outside of git repositories") unless available?
|
22
|
+
|
23
|
+
config_file = File.open(config_file_path, "w")
|
24
|
+
YAML.dump(to_h, config_file)
|
25
|
+
config_file.close
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def config_file_path
|
31
|
+
@config_file_path ||= begin
|
32
|
+
path = nil
|
33
|
+
Open3.popen3("git rev-parse --show-toplevel") do |_i, output, _e, thread|
|
34
|
+
if thread.value.success?
|
35
|
+
repo_root = output.read.chomp
|
36
|
+
path = File.join(repo_root, FILE_NAME)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
path
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
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
|
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
|
-
|
35
|
-
|
36
|
-
"abt
|
37
|
-
"abt current <ARIs from coworker> | abt
|
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",
|
data/lib/abt/providers/asana.rb
CHANGED
@@ -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
|
@@ -25,12 +25,42 @@ module Abt
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def require_project!
|
28
|
-
abort("No current/specified project. Did you
|
28
|
+
abort("No current/specified project. Did you forget to run `pick`?") if project_gid.nil?
|
29
29
|
end
|
30
30
|
|
31
31
|
def require_task!
|
32
|
-
|
33
|
-
abort("No current/specified task. Did you
|
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
|
34
64
|
end
|
35
65
|
|
36
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
|
@@ -36,24 +36,6 @@ module Abt
|
|
36
36
|
abort("Invalid project: #{project_gid}") if project.nil?
|
37
37
|
abort("Invalid task: #{task_gid}") if task_gid && task.nil?
|
38
38
|
end
|
39
|
-
|
40
|
-
def project
|
41
|
-
@project ||= begin
|
42
|
-
warn("Fetching project...")
|
43
|
-
api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
|
44
|
-
rescue Abt::HttpError::NotFoundError
|
45
|
-
nil
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def task
|
50
|
-
@task ||= begin
|
51
|
-
warn("Fetching task...")
|
52
|
-
api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
|
53
|
-
rescue Abt::HttpError::NotFoundError
|
54
|
-
nil
|
55
|
-
end
|
56
|
-
end
|
57
39
|
end
|
58
40
|
end
|
59
41
|
end
|
@@ -58,10 +58,8 @@ module Abt
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def task
|
61
|
-
@task ||=
|
62
|
-
|
63
|
-
opt_fields: "name,memberships.section.name,permalink_url")
|
64
|
-
end
|
61
|
+
@task ||= api.get("tasks/#{task_gid}",
|
62
|
+
opt_fields: "name,memberships.section.name,permalink_url")
|
65
63
|
end
|
66
64
|
end
|
67
65
|
end
|
@@ -10,65 +10,35 @@ module Abt
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.description
|
13
|
-
"Pick task
|
13
|
+
"Pick a task and - unless told not to - make it current"
|
14
14
|
end
|
15
15
|
|
16
16
|
def self.flags
|
17
17
|
[
|
18
|
-
["-d", "--dry-run", "Keep existing configuration"]
|
18
|
+
["-d", "--dry-run", "Keep existing configuration"],
|
19
|
+
["-c", "--clean", "Don't reuse project configuration"]
|
19
20
|
]
|
20
21
|
end
|
21
22
|
|
22
23
|
def perform
|
23
|
-
|
24
|
-
require_project!
|
25
|
-
|
26
|
-
warn(project["name"])
|
27
|
-
task = select_task
|
24
|
+
pick!
|
28
25
|
|
29
26
|
print_task(project, task)
|
30
27
|
|
31
28
|
return if flags[:"dry-run"]
|
32
29
|
|
33
|
-
config.
|
34
|
-
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def project
|
39
|
-
@project ||= api.get("projects/#{project_gid}", opt_fields: "name")
|
40
|
-
end
|
41
|
-
|
42
|
-
def select_task
|
43
|
-
section = cli.prompt.choice("Which section?", sections)
|
44
|
-
warn("Fetching tasks...")
|
45
|
-
tasks = tasks_in_section(section)
|
46
|
-
|
47
|
-
if tasks.length.zero?
|
48
|
-
warn("Section is empty")
|
49
|
-
select_task
|
30
|
+
if config.local_available?
|
31
|
+
config.path = path
|
50
32
|
else
|
51
|
-
|
33
|
+
warn("No local configuration to update - will function as dry run")
|
52
34
|
end
|
53
35
|
end
|
54
36
|
|
55
|
-
|
56
|
-
tasks = api.get_paged(
|
57
|
-
"tasks",
|
58
|
-
section: section["gid"],
|
59
|
-
opt_fields: "name,completed,permalink_url"
|
60
|
-
)
|
61
|
-
|
62
|
-
# The below filtering is the best we can do with Asanas api, see this:
|
63
|
-
# https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
|
64
|
-
tasks.reject { |task| task["completed"] }
|
65
|
-
end
|
37
|
+
private
|
66
38
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
|
71
|
-
end
|
39
|
+
def pick!
|
40
|
+
prompt_project! if project_gid.nil? || flags[:clean]
|
41
|
+
prompt_task!
|
72
42
|
end
|
73
43
|
end
|
74
44
|
end
|
@@ -14,7 +14,7 @@ module Abt
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def perform
|
17
|
-
|
17
|
+
prompt_project! unless project_gid
|
18
18
|
|
19
19
|
tasks.each do |task|
|
20
20
|
print_task(project, task)
|
@@ -23,14 +23,9 @@ module Abt
|
|
23
23
|
|
24
24
|
private
|
25
25
|
|
26
|
-
def project
|
27
|
-
@project ||= begin
|
28
|
-
api.get("projects/#{project_gid}", opt_fields: "name")
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
26
|
def tasks
|
33
27
|
@tasks ||= begin
|
28
|
+
project
|
34
29
|
warn("Fetching tasks...")
|
35
30
|
tasks = api.get_paged("tasks", project: project["gid"], opt_fields: "name,completed")
|
36
31
|
tasks.reject { |task| task["completed"] }
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Abt
|
4
|
+
module Providers
|
5
|
+
module Asana
|
6
|
+
module Commands
|
7
|
+
class WriteConfig < BaseCommand
|
8
|
+
def self.usage
|
9
|
+
"abt write-config asana[:<project-gid>]"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
"Write Asana settings to .abt.yml"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.flags
|
17
|
+
[
|
18
|
+
["-c", "--clean", "Don't reuse project configuration"]
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform
|
23
|
+
cli.directory_config["asana"] = config_data
|
24
|
+
cli.directory_config.save!
|
25
|
+
|
26
|
+
warn("Asana configuration written to #{Abt::DirectoryConfig::FILE_NAME}")
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def config_data
|
32
|
+
{
|
33
|
+
"path" => project_gid,
|
34
|
+
"wip_section_gid" => wip_section_gid,
|
35
|
+
"finalized_section_gid" => finalized_section_gid
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def project_gid
|
40
|
+
@project_gid ||= begin
|
41
|
+
prompt_project! if super.nil? || flags[:clean]
|
42
|
+
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def wip_section_gid
|
48
|
+
return config.wip_section_gid if use_previous_config?
|
49
|
+
|
50
|
+
cli.prompt.choice("Select WIP (Work In Progress) section", sections)["gid"]
|
51
|
+
end
|
52
|
+
|
53
|
+
def finalized_section_gid
|
54
|
+
return config.finalized_section_gid if use_previous_config?
|
55
|
+
|
56
|
+
cli.prompt.choice('Select section for finalized tasks (E.g. "Merged")', sections)["gid"]
|
57
|
+
end
|
58
|
+
|
59
|
+
def use_previous_config?
|
60
|
+
project_gid == config.path.project_gid
|
61
|
+
end
|
62
|
+
|
63
|
+
def sections
|
64
|
+
@sections ||= begin
|
65
|
+
warn("Fetching sections...")
|
66
|
+
api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|