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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -1
  3. data/lib/abt.rb +1 -0
  4. data/lib/abt/cli.rb +32 -9
  5. data/lib/abt/cli/prompt.rb +12 -9
  6. data/lib/abt/directory_config.rb +43 -0
  7. data/lib/abt/docs.rb +10 -6
  8. data/lib/abt/providers/asana.rb +1 -0
  9. data/lib/abt/providers/asana/base_command.rb +33 -3
  10. data/lib/abt/providers/asana/commands/add.rb +0 -4
  11. data/lib/abt/providers/asana/commands/branch_name.rb +0 -13
  12. data/lib/abt/providers/asana/commands/current.rb +0 -18
  13. data/lib/abt/providers/asana/commands/finalize.rb +2 -4
  14. data/lib/abt/providers/asana/commands/pick.rb +11 -41
  15. data/lib/abt/providers/asana/commands/tasks.rb +2 -7
  16. data/lib/abt/providers/asana/commands/write_config.rb +73 -0
  17. data/lib/abt/providers/asana/configuration.rb +11 -3
  18. data/lib/abt/providers/asana/path.rb +2 -2
  19. data/lib/abt/providers/asana/services/project_picker.rb +54 -0
  20. data/lib/abt/providers/asana/services/task_picker.rb +83 -0
  21. data/lib/abt/providers/devops.rb +1 -0
  22. data/lib/abt/providers/devops/api.rb +10 -0
  23. data/lib/abt/providers/devops/base_command.rb +34 -14
  24. data/lib/abt/providers/devops/commands/boards.rb +1 -2
  25. data/lib/abt/providers/devops/commands/branch_name.rb +10 -16
  26. data/lib/abt/providers/devops/commands/current.rb +0 -19
  27. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +10 -16
  28. data/lib/abt/providers/devops/commands/pick.rb +11 -47
  29. data/lib/abt/providers/devops/commands/work_items.rb +3 -6
  30. data/lib/abt/providers/devops/commands/write_config.rb +47 -0
  31. data/lib/abt/providers/devops/configuration.rb +1 -1
  32. data/lib/abt/providers/devops/path.rb +3 -3
  33. data/lib/abt/providers/devops/services/board_picker.rb +58 -0
  34. data/lib/abt/providers/devops/services/project_picker.rb +73 -0
  35. data/lib/abt/providers/devops/services/work_item_picker.rb +98 -0
  36. data/lib/abt/providers/git/commands/branch.rb +1 -1
  37. data/lib/abt/providers/harvest.rb +1 -0
  38. data/lib/abt/providers/harvest/base_command.rb +45 -3
  39. data/lib/abt/providers/harvest/commands/current.rb +0 -28
  40. data/lib/abt/providers/harvest/commands/pick.rb +12 -27
  41. data/lib/abt/providers/harvest/commands/projects.rb +2 -9
  42. data/lib/abt/providers/harvest/commands/tasks.rb +2 -19
  43. data/lib/abt/providers/harvest/commands/write_config.rb +41 -0
  44. data/lib/abt/providers/harvest/configuration.rb +1 -1
  45. data/lib/abt/providers/harvest/path.rb +1 -1
  46. data/lib/abt/providers/harvest/services/project_picker.rb +53 -0
  47. data/lib/abt/providers/harvest/services/task_picker.rb +50 -0
  48. data/lib/abt/version.rb +1 -1
  49. metadata +13 -5
  50. data/lib/abt/providers/asana/commands/init.rb +0 -42
  51. data/lib/abt/providers/devops/commands/init.rb +0 -79
  52. 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: 6b21c8f210a7c29d5f43ef7c756cfdcc7d8ef2b13ef4dcaef1cec4f8bf2297a1
4
- data.tar.gz: f7586aa063494d796ded7c2e8696d9bf834e24be3004c621408e2fdd58f9b516
3
+ metadata.gz: 1fbc512b85932a7c8cfa8e6d9ec10176f3e2ac8711df78ad4ce67100ac79f003
4
+ data.tar.gz: 7dc1c727ced7d316c84075458b75839bd931050e50c9ffe39f925324c6a8e63d
5
5
  SHA512:
6
- metadata.gz: b9d5278daeec2067937212108eb0ba478c1ec80d45c14a94025f7d6d83c5a30c4cdb716d010a4dfc807166165cd04af8e7962ec2b776f9f2fb7feee7edb65194
7
- data.tar.gz: 2faba55f9dcf4ee66f82a77b647fe367fbebeb74c70fde23edd6543ad83ce1f995bdeb01673555219c498a971919e0b2fd0ec1f8362484f56ba6a54a2b65503c
6
+ metadata.gz: 74f84e028cf84f156ee18f46177fae4e17f857eb538362daf2baf3b017f1ace428a8b9c16aa912ba0c486fdc13cbf2a41402fa64917fa74ee9578c6e041c6d7b
7
+ data.tar.gz: fe06b4314f9a119950979b7a8c966758a56b4a3619dcabc9328209beaca94f317851bc9dedbc986a955a120afe29da0707225e78896e1517854c24864bba2efc
data/bin/abt CHANGED
@@ -8,5 +8,5 @@ begin
8
8
  rescue Abt::Cli::Abort => e
9
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
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,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?
@@ -14,17 +14,20 @@ module Abt
14
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): ")
21
+ output.print("#{text} (#{choices}): ")
22
22
 
23
- case Abt::Helpers.read_user_input
24
- when "y", "Y" then return true
25
- when "n", "N" then return false
26
- else output.puts "Invalid choice" end
27
- end
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 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",
@@ -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 initialize Asana?") if project_gid.nil?
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
- abort("No current/specified project. Did you initialize Asana and pick a task?") if project_gid.nil?
33
- 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
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 ||= begin
62
- api.get("tasks/#{task_gid}",
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 for current git repository"
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
- require_local_config!
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.path = Path.from_ids(project_gid: project_gid, task_gid: task["gid"])
34
- end
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
- cli.prompt.choice("Select a task", tasks, nil_option: true) || select_task
33
+ warn("No local configuration to update - will function as dry run")
52
34
  end
53
35
  end
54
36
 
55
- def tasks_in_section(section)
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 sections
68
- @sections ||= begin
69
- warn("Fetching sections...")
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
- require_project!
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