abt-cli 0.0.24 → 0.0.29

Sign up to get free protection for your applications and to get access to all the features.
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