abt-cli 0.0.19 → 0.0.24

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 (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