abt-cli 0.0.19 → 0.0.24

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +3 -3
  3. data/lib/abt.rb +6 -6
  4. data/lib/abt/ari.rb +2 -2
  5. data/lib/abt/ari_list.rb +1 -1
  6. data/lib/abt/base_command.rb +7 -7
  7. data/lib/abt/cli.rb +49 -47
  8. data/lib/abt/cli/arguments_parser.rb +6 -3
  9. data/lib/abt/cli/global_commands.rb +23 -0
  10. data/lib/abt/cli/global_commands/commands.rb +23 -0
  11. data/lib/abt/cli/global_commands/examples.rb +23 -0
  12. data/lib/abt/cli/global_commands/help.rb +23 -0
  13. data/lib/abt/cli/global_commands/readme.rb +23 -0
  14. data/lib/abt/cli/global_commands/share.rb +36 -0
  15. data/lib/abt/cli/global_commands/version.rb +23 -0
  16. data/lib/abt/cli/prompt.rb +64 -52
  17. data/lib/abt/docs.rb +48 -26
  18. data/lib/abt/docs/cli.rb +3 -3
  19. data/lib/abt/docs/markdown.rb +10 -7
  20. data/lib/abt/git_config.rb +4 -6
  21. data/lib/abt/helpers.rb +26 -8
  22. data/lib/abt/providers/asana/api.rb +9 -9
  23. data/lib/abt/providers/asana/base_command.rb +12 -10
  24. data/lib/abt/providers/asana/commands/add.rb +13 -12
  25. data/lib/abt/providers/asana/commands/branch_name.rb +8 -8
  26. data/lib/abt/providers/asana/commands/clear.rb +7 -8
  27. data/lib/abt/providers/asana/commands/current.rb +14 -15
  28. data/lib/abt/providers/asana/commands/finalize.rb +17 -18
  29. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +18 -16
  30. data/lib/abt/providers/asana/commands/init.rb +8 -41
  31. data/lib/abt/providers/asana/commands/pick.rb +22 -26
  32. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  33. data/lib/abt/providers/asana/commands/share.rb +7 -5
  34. data/lib/abt/providers/asana/commands/start.rb +28 -21
  35. data/lib/abt/providers/asana/commands/tasks.rb +6 -6
  36. data/lib/abt/providers/asana/configuration.rb +37 -29
  37. data/lib/abt/providers/asana/path.rb +6 -6
  38. data/lib/abt/providers/devops/api.rb +12 -12
  39. data/lib/abt/providers/devops/base_command.rb +14 -10
  40. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  41. data/lib/abt/providers/devops/commands/branch_name.rb +9 -9
  42. data/lib/abt/providers/devops/commands/clear.rb +7 -8
  43. data/lib/abt/providers/devops/commands/current.rb +17 -18
  44. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +21 -19
  45. data/lib/abt/providers/devops/commands/init.rb +21 -14
  46. data/lib/abt/providers/devops/commands/pick.rb +25 -19
  47. data/lib/abt/providers/devops/commands/share.rb +7 -5
  48. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  49. data/lib/abt/providers/devops/configuration.rb +15 -15
  50. data/lib/abt/providers/devops/path.rb +7 -6
  51. data/lib/abt/providers/git/commands/branch.rb +23 -21
  52. data/lib/abt/providers/harvest/api.rb +8 -8
  53. data/lib/abt/providers/harvest/base_command.rb +10 -8
  54. data/lib/abt/providers/harvest/commands/clear.rb +7 -8
  55. data/lib/abt/providers/harvest/commands/current.rb +13 -14
  56. data/lib/abt/providers/harvest/commands/init.rb +10 -39
  57. data/lib/abt/providers/harvest/commands/pick.rb +15 -11
  58. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  59. data/lib/abt/providers/harvest/commands/share.rb +7 -5
  60. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  61. data/lib/abt/providers/harvest/commands/stop.rb +12 -12
  62. data/lib/abt/providers/harvest/commands/tasks.rb +7 -7
  63. data/lib/abt/providers/harvest/commands/track.rb +52 -37
  64. data/lib/abt/providers/harvest/configuration.rb +18 -18
  65. data/lib/abt/providers/harvest/path.rb +6 -6
  66. data/lib/abt/version.rb +1 -1
  67. metadata +12 -5
@@ -4,8 +4,8 @@ module Abt
4
4
  module Providers
5
5
  module Asana
6
6
  class Api
7
- API_ENDPOINT = 'https://app.asana.com/api/1.0'
8
- VERBS = %i[get post put].freeze
7
+ API_ENDPOINT = "https://app.asana.com/api/1.0"
8
+ VERBS = [:get, :post, :put].freeze
9
9
 
10
10
  attr_reader :access_token
11
11
 
@@ -15,7 +15,7 @@ module Abt
15
15
 
16
16
  VERBS.each do |verb|
17
17
  define_method(verb) do |*args|
18
- request(verb, *args)['data']
18
+ request(verb, *args)["data"]
19
19
  end
20
20
  end
21
21
 
@@ -24,10 +24,10 @@ module Abt
24
24
 
25
25
  loop do
26
26
  result = request(:get, path, query.merge(limit: 100))
27
- records += result['data']
28
- break if result['next_page'].nil?
27
+ records += result["data"]
28
+ break if result["next_page"].nil?
29
29
 
30
- path = result['next_page']['path'][1..-1]
30
+ path = result["next_page"]["path"][1..-1]
31
31
  end
32
32
 
33
33
  records
@@ -40,15 +40,15 @@ module Abt
40
40
  Oj.load(response.body)
41
41
  else
42
42
  error_class = Abt::HttpError.error_class_for_status(response.status)
43
- encoded_response_body = response.body.force_encoding('utf-8')
43
+ encoded_response_body = response.body.force_encoding("utf-8")
44
44
  raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
45
45
  end
46
46
  end
47
47
 
48
48
  def connection
49
49
  @connection ||= Faraday.new(API_ENDPOINT) do |connection|
50
- connection.headers['Authorization'] = "Bearer #{access_token}"
51
- connection.headers['Content-Type'] = 'application/json'
50
+ connection.headers["Authorization"] = "Bearer #{access_token}"
51
+ connection.headers["Content-Type"] = "application/json"
52
52
  end
53
53
  end
54
54
  end
@@ -20,26 +20,28 @@ module Abt
20
20
 
21
21
  private
22
22
 
23
+ def require_local_config!
24
+ abort("Must be run inside a git repository") unless config.local_available?
25
+ end
26
+
23
27
  def require_project!
24
- abort 'No current/specified project. Did you initialize Asana?' if project_gid.nil?
28
+ abort("No current/specified project. Did you initialize Asana?") if project_gid.nil?
25
29
  end
26
30
 
27
31
  def require_task!
28
- if project_gid.nil?
29
- abort 'No current/specified project. Did you initialize Asana and pick a task?'
30
- end
31
- abort 'No current/specified task. Did you pick an Asana task?' if task_gid.nil?
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
34
  end
33
35
 
34
36
  def print_project(project)
35
- cli.print_ari('asana', project['gid'], project['name'])
36
- warn project['permalink_url'] if project.key?('permalink_url') && cli.output.isatty
37
+ cli.print_ari("asana", project["gid"], project["name"])
38
+ warn(project["permalink_url"]) if project.key?("permalink_url") && cli.output.isatty
37
39
  end
38
40
 
39
41
  def print_task(project, task)
40
- project = { 'gid' => project } if project.is_a?(String)
41
- cli.print_ari('asana', "#{project['gid']}/#{task['gid']}", task['name'])
42
- warn task['permalink_url'] if task.key?('permalink_url') && cli.output.isatty
42
+ project = { "gid" => project } if project.is_a?(String)
43
+ cli.print_ari("asana", "#{project['gid']}/#{task['gid']}", task["name"])
44
+ warn(task["permalink_url"]) if task.key?("permalink_url") && cli.output.isatty
43
45
  end
44
46
 
45
47
  def api
@@ -6,22 +6,22 @@ module Abt
6
6
  module Commands
7
7
  class Add < BaseCommand
8
8
  def self.usage
9
- 'abt add asana[:<project-gid>]'
9
+ "abt add asana[:<project-gid>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Create a new task for the current/specified Asana project'
13
+ "Create a new task for the current/specified Asana project"
14
14
  end
15
15
 
16
16
  def perform
17
17
  require_project!
18
18
 
19
19
  task
20
- warn 'Task created'
20
+ warn("Task created")
21
21
 
22
22
  if section
23
23
  move_task
24
- warn "Moved to section: #{section['name']}"
24
+ warn("Moved to section: #{section['name']}")
25
25
  end
26
26
 
27
27
  print_task(project, task)
@@ -38,36 +38,37 @@ module Abt
38
38
  projects: [project_gid]
39
39
  }
40
40
  }
41
- api.post('tasks', Oj.dump(body, mode: :json))
41
+ api.post("tasks", Oj.dump(body, mode: :json))
42
42
  end
43
43
  end
44
44
 
45
45
  def move_task
46
- body = { data: { task: task['gid'] } }
46
+ body = { data: { task: task["gid"] } }
47
47
  body_json = Oj.dump(body, mode: :json)
48
48
  api.post("sections/#{section['gid']}/addTask", body_json)
49
49
  end
50
50
 
51
51
  def name
52
- @name ||= cli.prompt.text 'Enter task description'
52
+ @name ||= cli.prompt.text("Enter task description")
53
53
  end
54
54
 
55
55
  def notes
56
- @notes ||= cli.prompt.text 'Enter task notes'
56
+ @notes ||= cli.prompt.text("Enter task notes")
57
57
  end
58
58
 
59
59
  def project
60
- @project ||= api.get("projects/#{project_gid}", opt_fields: 'name')
60
+ @project ||= api.get("projects/#{project_gid}", opt_fields: "name")
61
61
  end
62
62
 
63
63
  def section
64
- @section ||= cli.prompt.choice 'Add to section?', sections, ['q', 'Don\'t add to section']
64
+ @section ||= cli.prompt.choice("Add to section?", sections,
65
+ nil_option: ["q", "Don't add to section"])
65
66
  end
66
67
 
67
68
  def sections
68
69
  @sections ||= begin
69
- warn 'Fetching sections...'
70
- api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
70
+ warn("Fetching sections...")
71
+ api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
71
72
  end
72
73
  end
73
74
  end
@@ -6,11 +6,11 @@ module Abt
6
6
  module Commands
7
7
  class BranchName < BaseCommand
8
8
  def self.usage
9
- 'abt branch-name asana[:<project-gid>/<task-gid>]'
9
+ "abt branch-name asana[:<project-gid>/<task-gid>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Suggest a git branch name for the current/specified task.'
13
+ "Suggest a git branch name for the current/specified task."
14
14
  end
15
15
 
16
16
  def perform
@@ -23,21 +23,21 @@ module Abt
23
23
  private
24
24
 
25
25
  def name
26
- task['name'].downcase.gsub(/[^\w]+/, '-')
26
+ task["name"].downcase.gsub(/[^\w]+/, "-").gsub(/(^-|-$)/, "")
27
27
  end
28
28
 
29
29
  def ensure_current_is_valid!
30
- abort "Invalid task gid: #{task_gid}" if task.nil?
30
+ abort("Invalid task gid: #{task_gid}") if task.nil?
31
31
 
32
- return if task['memberships'].any? { |m| m.dig('project', 'gid') == project_gid }
32
+ return if task["memberships"].any? { |m| m.dig("project", "gid") == project_gid }
33
33
 
34
- abort "Invalid or unmatching project gid: #{project_gid}"
34
+ abort("Invalid or unmatching project gid: #{project_gid}")
35
35
  end
36
36
 
37
37
  def task
38
38
  @task ||= begin
39
- warn 'Fetching task...'
40
- api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.project')
39
+ warn("Fetching task...")
40
+ api.get("tasks/#{task_gid}", opt_fields: "name,memberships.project")
41
41
  rescue Abt::HttpError::NotFoundError
42
42
  nil
43
43
  end
@@ -6,29 +6,28 @@ module Abt
6
6
  module Commands
7
7
  class Clear < BaseCommand
8
8
  def self.usage
9
- 'abt clear asana'
9
+ "abt clear asana"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Clear asana configuration'
13
+ "Clear asana configuration"
14
14
  end
15
15
 
16
16
  def self.flags
17
17
  [
18
- ['-g', '--global', 'Clear global instead of local asana configuration (credentials etc.)'],
19
- ['-a', '--all', 'Clear all asana configuration']
18
+ ["-g", "--global",
19
+ "Clear global instead of local asana configuration (credentials etc.)"],
20
+ ["-a", "--all", "Clear all asana configuration"]
20
21
  ]
21
22
  end
22
23
 
23
24
  def perform
24
- if flags[:global] && flags[:all]
25
- abort('Flags --global and --all cannot be used at the same time')
26
- end
25
+ abort("Flags --global and --all cannot be used at the same time") if flags[:global] && flags[:all]
27
26
 
28
27
  config.clear_local unless flags[:global]
29
28
  config.clear_global if flags[:global] || flags[:all]
30
29
 
31
- warn 'Configuration cleared'
30
+ warn("Configuration cleared")
32
31
  end
33
32
  end
34
33
  end
@@ -6,22 +6,21 @@ module Abt
6
6
  module Commands
7
7
  class Current < BaseCommand
8
8
  def self.usage
9
- 'abt current asana[:<project-gid>[/<task-gid>]]'
9
+ "abt current asana[:<project-gid>[/<task-gid>]]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Get or set project and or task for current git repository'
13
+ "Get or set project and or task for current git repository"
14
14
  end
15
15
 
16
16
  def perform
17
- abort 'Must be run inside a git repository' unless config.local_available?
18
-
17
+ require_local_config!
19
18
  require_project!
20
19
  ensure_valid_configuration!
21
20
 
22
21
  if path != config.path
23
22
  config.path = path
24
- warn 'Configuration updated'
23
+ warn("Configuration updated")
25
24
  end
26
25
 
27
26
  print_configuration
@@ -34,25 +33,25 @@ module Abt
34
33
  end
35
34
 
36
35
  def ensure_valid_configuration!
37
- abort "Invalid project: #{project_gid}" if project.nil?
38
- abort "Invalid task: #{task_gid}" if task_gid && task.nil?
36
+ abort("Invalid project: #{project_gid}") if project.nil?
37
+ abort("Invalid task: #{task_gid}") if task_gid && task.nil?
39
38
  end
40
39
 
41
40
  def project
42
41
  @project ||= begin
43
- warn 'Fetching project...'
44
- api.get("projects/#{project_gid}", opt_fields: 'name,permalink_url')
45
- rescue Abt::HttpError::NotFoundError
46
- nil
42
+ warn("Fetching project...")
43
+ api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
44
+ rescue Abt::HttpError::NotFoundError
45
+ nil
47
46
  end
48
47
  end
49
48
 
50
49
  def task
51
50
  @task ||= begin
52
- warn 'Fetching task...'
53
- api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url')
54
- rescue Abt::HttpError::NotFoundError
55
- nil
51
+ warn("Fetching task...")
52
+ api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
53
+ rescue Abt::HttpError::NotFoundError
54
+ nil
56
55
  end
57
56
  end
58
57
  end
@@ -6,47 +6,49 @@ module Abt
6
6
  module Commands
7
7
  class Finalize < BaseCommand
8
8
  def self.usage
9
- 'abt finalize asana[:<project-gid>/<task-gid>]'
9
+ "abt finalize asana[:<project-gid>/<task-gid>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Move current/specified task to section (column) for finalized tasks'
13
+ "Move current/specified task to section (column) for finalized tasks"
14
14
  end
15
15
 
16
16
  def perform
17
- unless config.local_available?
18
- abort 'This is a no-op for tasks outside the current project'
19
- end
17
+ abort("This is a no-op for tasks outside the current project") unless project_gid == config.path.project_gid
20
18
  require_task!
21
19
  print_task(project_gid, task)
22
20
 
21
+ maybe_move_task
22
+ end
23
+
24
+ private
25
+
26
+ def maybe_move_task
23
27
  if task_already_in_finalized_section?
24
- warn "Task already in section: #{current_task_section['name']}"
28
+ warn("Task already in section: #{current_task_section['name']}")
25
29
  else
26
- warn "Moving task to section: #{finalized_section['name']}"
30
+ warn("Moving task to section: #{finalized_section['name']}")
27
31
  move_task
28
32
  end
29
33
  end
30
34
 
31
- private
32
-
33
35
  def task_already_in_finalized_section?
34
36
  !task_section_membership.nil?
35
37
  end
36
38
 
37
39
  def current_task_section
38
- task_section_membership&.dig('section')
40
+ task_section_membership&.dig("section")
39
41
  end
40
42
 
41
43
  def task_section_membership
42
- task['memberships'].find do |membership|
43
- membership.dig('section', 'gid') == config.finalized_section_gid
44
+ task["memberships"].find do |membership|
45
+ membership.dig("section", "gid") == config.finalized_section_gid
44
46
  end
45
47
  end
46
48
 
47
49
  def finalized_section
48
50
  @finalized_section ||= api.get("sections/#{config.finalized_section_gid}",
49
- opt_fields: 'name')
51
+ opt_fields: "name")
50
52
  end
51
53
 
52
54
  def move_task
@@ -57,11 +59,8 @@ module Abt
57
59
 
58
60
  def task
59
61
  @task ||= begin
60
- if task_gid.nil?
61
- nil
62
- else
63
- api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,permalink_url')
64
- end
62
+ api.get("tasks/#{task_gid}",
63
+ opt_fields: "name,memberships.section.name,permalink_url")
65
64
  end
66
65
  end
67
66
  end
@@ -6,45 +6,47 @@ module Abt
6
6
  module Commands
7
7
  class HarvestTimeEntryData < BaseCommand
8
8
  def self.usage
9
- 'abt harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
9
+ "abt harvest-time-entry-data asana[:<project-gid>/<task-gid>]"
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Print Harvest time entry data for Asana task as json. Used by harvest start script.'
13
+ "Print Harvest time entry data for Asana task as json. Used by harvest start script."
14
14
  end
15
15
 
16
16
  def perform
17
17
  require_task!
18
18
  ensure_current_is_valid!
19
19
 
20
- body = {
21
- notes: task['name'],
20
+ puts Oj.dump(body, mode: :json)
21
+ end
22
+
23
+ private
24
+
25
+ def body
26
+ {
27
+ notes: task["name"],
22
28
  external_reference: {
23
29
  id: task_gid.to_i,
24
30
  group_id: project_gid.to_i,
25
- permalink: task['permalink_url']
31
+ permalink: task["permalink_url"]
26
32
  }
27
33
  }
28
-
29
- puts Oj.dump(body, mode: :json)
30
34
  end
31
35
 
32
- private
33
-
34
36
  def ensure_current_is_valid!
35
- abort "Invalid task gid: #{task_gid}" if task.nil?
37
+ abort("Invalid task gid: #{task_gid}") if task.nil?
36
38
 
37
- return if task['memberships'].any? { |m| m.dig('project', 'gid') == project_gid }
39
+ return if task["memberships"].any? { |m| m.dig("project", "gid") == project_gid }
38
40
 
39
- abort "Invalid or unmatching project gid: #{project_gid}"
41
+ abort("Invalid or unmatching project gid: #{project_gid}")
40
42
  end
41
43
 
42
44
  def task
43
45
  @task ||= begin
44
- warn 'Fetching task...'
45
- api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url,memberships.project')
46
- rescue Abt::HttpError::NotFoundError
47
- nil
46
+ warn("Fetching task...")
47
+ api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url,memberships.project")
48
+ rescue Abt::HttpError::NotFoundError
49
+ nil
48
50
  end
49
51
  end
50
52
  end