abt-cli 0.0.17 → 0.0.22

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 +20 -0
  5. data/lib/abt/ari_list.rb +13 -0
  6. data/lib/abt/base_command.rb +63 -0
  7. data/lib/abt/cli.rb +58 -59
  8. data/lib/abt/cli/arguments_parser.rb +8 -27
  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 +52 -20
  17. data/lib/abt/docs.rb +48 -25
  18. data/lib/abt/docs/cli.rb +7 -7
  19. data/lib/abt/docs/markdown.rb +13 -12
  20. data/lib/abt/git_config.rb +21 -39
  21. data/lib/abt/providers/asana/api.rb +9 -9
  22. data/lib/abt/providers/asana/base_command.rb +16 -38
  23. data/lib/abt/providers/asana/commands/add.rb +18 -15
  24. data/lib/abt/providers/asana/commands/branch_name.rb +13 -8
  25. data/lib/abt/providers/asana/commands/clear.rb +8 -7
  26. data/lib/abt/providers/asana/commands/current.rb +23 -38
  27. data/lib/abt/providers/asana/commands/finalize.rb +11 -16
  28. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +14 -9
  29. data/lib/abt/providers/asana/commands/init.rb +8 -41
  30. data/lib/abt/providers/asana/commands/pick.rb +22 -17
  31. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  32. data/lib/abt/providers/asana/commands/share.rb +6 -8
  33. data/lib/abt/providers/asana/commands/start.rb +26 -23
  34. data/lib/abt/providers/asana/commands/tasks.rb +6 -5
  35. data/lib/abt/providers/asana/configuration.rb +34 -40
  36. data/lib/abt/providers/asana/path.rb +36 -0
  37. data/lib/abt/providers/devops/api.rb +23 -11
  38. data/lib/abt/providers/devops/base_command.rb +18 -43
  39. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  40. data/lib/abt/providers/devops/commands/branch_name.rb +14 -10
  41. data/lib/abt/providers/devops/commands/clear.rb +8 -7
  42. data/lib/abt/providers/devops/commands/current.rb +25 -49
  43. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +20 -12
  44. data/lib/abt/providers/devops/commands/init.rb +29 -25
  45. data/lib/abt/providers/devops/commands/pick.rb +11 -18
  46. data/lib/abt/providers/devops/commands/share.rb +7 -6
  47. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  48. data/lib/abt/providers/devops/configuration.rb +31 -56
  49. data/lib/abt/providers/devops/path.rb +51 -0
  50. data/lib/abt/providers/git/commands/branch.rb +29 -25
  51. data/lib/abt/providers/harvest/api.rb +8 -8
  52. data/lib/abt/providers/harvest/base_command.rb +18 -38
  53. data/lib/abt/providers/harvest/commands/clear.rb +8 -7
  54. data/lib/abt/providers/harvest/commands/current.rb +28 -35
  55. data/lib/abt/providers/harvest/commands/init.rb +10 -39
  56. data/lib/abt/providers/harvest/commands/pick.rb +11 -12
  57. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  58. data/lib/abt/providers/harvest/commands/share.rb +6 -8
  59. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  60. data/lib/abt/providers/harvest/commands/stop.rb +13 -13
  61. data/lib/abt/providers/harvest/commands/tasks.rb +9 -6
  62. data/lib/abt/providers/harvest/commands/track.rb +40 -32
  63. data/lib/abt/providers/harvest/configuration.rb +28 -37
  64. data/lib/abt/providers/harvest/path.rb +36 -0
  65. data/lib/abt/version.rb +1 -1
  66. metadata +18 -6
  67. data/lib/abt/cli/base_command.rb +0 -61
@@ -6,28 +6,26 @@ module Abt
6
6
 
7
7
  class UnsafeNamespaceError < StandardError; end
8
8
 
9
- LOCAL_CONFIG_AVAILABLE_CHECK_COMMAND = 'git config --local -l'
9
+ def initialize(scope = "local", namespace = "")
10
+ @namespace = namespace
10
11
 
11
- def self.local_available?
12
- return @local_available if instance_variables.include?(:@local_available)
12
+ raise ArgumentError, 'scope must be "local" or "global"' unless %w[local global].include?(scope)
13
13
 
14
- @local_available = begin
15
- success = false
16
- Open3.popen3(LOCAL_CONFIG_AVAILABLE_CHECK_COMMAND) do |_i, _o, _e, thread|
17
- success = thread.value.success?
18
- end
19
- success
20
- end
14
+ @scope = scope
21
15
  end
22
16
 
23
- def initialize(namespace: '', scope: 'local')
24
- @namespace = namespace
25
-
26
- unless %w[local global].include? scope
27
- raise ArgumentError, 'scope must be "local" or "global"'
17
+ def available?
18
+ unless instance_variables.include?(:available)
19
+ @available = begin
20
+ success = false
21
+ Open3.popen3(availability_check_call) do |_i, _o, _e, thread|
22
+ success = thread.value.success?
23
+ end
24
+ success
25
+ end
28
26
  end
29
27
 
30
- @scope = scope
28
+ @available
31
29
  end
32
30
 
33
31
  def [](key)
@@ -49,28 +47,8 @@ module Abt
49
47
  `git config --#{scope} --get-regexp --name-only ^#{namespace}`.lines.map(&:strip)
50
48
  end
51
49
 
52
- def local
53
- @local ||= begin
54
- if scope == 'local'
55
- self
56
- else
57
- self.class.new(namespace: namespace, scope: 'local')
58
- end
59
- end
60
- end
61
-
62
- def global
63
- @global ||= begin
64
- if scope == 'global'
65
- self
66
- else
67
- self.class.new(namespace: namespace, scope: 'global')
68
- end
69
- end
70
- end
71
-
72
50
  def clear(output: nil)
73
- raise UnsafeNamespaceError, 'Keys can only be cleared within a namespace' if namespace.empty?
51
+ raise UnsafeNamespaceError, "Keys can only be cleared within a namespace" if namespace.empty?
74
52
 
75
53
  keys.each do |key|
76
54
  output&.puts "Clearing #{scope}: #{key_with_namespace(key)}"
@@ -80,10 +58,14 @@ module Abt
80
58
 
81
59
  private
82
60
 
61
+ def availability_check_call
62
+ "git config --#{scope} -l"
63
+ end
64
+
83
65
  def ensure_scope_available!
84
- return if scope != 'local' || self.class.local_available?
66
+ return if available?
85
67
 
86
- raise StandardError, 'Local configuration is not available outside a git repository'
68
+ raise StandardError, "Local configuration is not available outside a git repository"
87
69
  end
88
70
 
89
71
  def key_with_namespace(key)
@@ -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
@@ -3,63 +3,41 @@
3
3
  module Abt
4
4
  module Providers
5
5
  module Asana
6
- class BaseCommand < Abt::Cli::BaseCommand
7
- attr_reader :project_gid, :task_gid, :config
6
+ class BaseCommand < Abt::BaseCommand
7
+ extend Forwardable
8
8
 
9
- def initialize(path:, cli:, **)
9
+ attr_reader :path, :config
10
+
11
+ def_delegators(:@path, :project_gid, :task_gid)
12
+
13
+ def initialize(ari:, cli:)
10
14
  super
11
15
 
12
16
  @config = Configuration.new(cli: cli)
13
17
 
14
- if path.nil?
15
- use_current_path
16
- else
17
- use_path(path)
18
- end
18
+ @path = ari.path ? Path.new(ari.path) : config.path
19
19
  end
20
20
 
21
21
  private
22
22
 
23
23
  def require_project!
24
- cli.abort 'No current/specified project. Did you initialize Asana?' if project_gid.nil?
24
+ abort("No current/specified project. Did you initialize Asana?") if project_gid.nil?
25
25
  end
26
26
 
27
27
  def require_task!
28
- if project_gid.nil?
29
- cli.abort 'No current/specified project. Did you initialize Asana and pick a task?'
30
- end
31
- cli.abort 'No current/specified task. Did you pick an Asana task?' if task_gid.nil?
32
- end
33
-
34
- def same_args_as_config?
35
- project_gid == config.project_gid && task_gid == config.task_gid
28
+ abort("No current/specified project. Did you initialize Asana and pick a task?") if project_gid.nil?
29
+ abort("No current/specified task. Did you pick an Asana task?") if task_gid.nil?
36
30
  end
37
31
 
38
32
  def print_project(project)
39
- cli.print_scheme_argument('asana', project['gid'], project['name'])
40
- cli.warn project['permalink_url'] if project.key?('permalink_url') && cli.output.isatty
33
+ cli.print_ari("asana", project["gid"], project["name"])
34
+ warn(project["permalink_url"]) if project.key?("permalink_url") && cli.output.isatty
41
35
  end
42
36
 
43
37
  def print_task(project, task)
44
- project = { 'gid' => project } if project.is_a?(String)
45
- cli.print_scheme_argument('asana', "#{project['gid']}/#{task['gid']}", task['name'])
46
- cli.warn task['permalink_url'] if task.key?('permalink_url') && cli.output.isatty
47
- end
48
-
49
- def use_current_path
50
- @project_gid = config.project_gid
51
- @task_gid = config.task_gid
52
- end
53
-
54
- def use_path(path)
55
- args = path.to_s.split('/')
56
- @project_gid = args[0].to_s
57
- @project_gid = nil if project_gid.empty?
58
-
59
- return if project_gid.nil?
60
-
61
- @task_gid = args[1].to_s
62
- @task_gid = nil if @task_gid.empty?
38
+ project = { "gid" => project } if project.is_a?(String)
39
+ cli.print_ari("asana", "#{project['gid']}/#{task['gid']}", task["name"])
40
+ warn(task["permalink_url"]) if task.key?("permalink_url") && cli.output.isatty
63
41
  end
64
42
 
65
43
  def api
@@ -6,20 +6,25 @@ 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
- print_task(project, task)
20
+ warn("Task created")
21
+
22
+ if section
23
+ move_task
24
+ warn("Moved to section: #{section['name']}")
25
+ end
21
26
 
22
- move_task if section
27
+ print_task(project, task)
23
28
  end
24
29
 
25
30
  private
@@ -33,39 +38,37 @@ module Abt
33
38
  projects: [project_gid]
34
39
  }
35
40
  }
36
- cli.warn 'Creating task'
37
- api.post('tasks', Oj.dump(body, mode: :json))
41
+ api.post("tasks", Oj.dump(body, mode: :json))
38
42
  end
39
43
  end
40
44
 
41
45
  def move_task
42
- body = { data: { task: task['gid'] } }
46
+ body = { data: { task: task["gid"] } }
43
47
  body_json = Oj.dump(body, mode: :json)
44
48
  api.post("sections/#{section['gid']}/addTask", body_json)
45
49
  end
46
50
 
47
51
  def name
48
- @name ||= cli.prompt.text 'Enter task description'
52
+ @name ||= cli.prompt.text("Enter task description")
49
53
  end
50
54
 
51
55
  def notes
52
- @notes ||= cli.prompt.text 'Enter task notes'
56
+ @notes ||= cli.prompt.text("Enter task notes")
53
57
  end
54
58
 
55
59
  def project
56
- @project ||= api.get("projects/#{project_gid}")
60
+ @project ||= api.get("projects/#{project_gid}", opt_fields: "name")
57
61
  end
58
62
 
59
63
  def section
60
- @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"])
61
66
  end
62
67
 
63
68
  def sections
64
69
  @sections ||= begin
65
- cli.warn 'Fetching sections...'
66
- api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
67
- rescue Abt::HttpError::HttpError
68
- []
70
+ warn("Fetching sections...")
71
+ api.get_paged("projects/#{project_gid}/sections", opt_fields: "name")
69
72
  end
70
73
  end
71
74
  end
@@ -6,36 +6,41 @@ 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
17
17
  require_task!
18
18
  ensure_current_is_valid!
19
19
 
20
- cli.puts name
20
+ puts name
21
21
  end
22
22
 
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
- cli.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
- cli.abort "Invalid project gid: #{project_gid}"
34
+ abort("Invalid or unmatching project gid: #{project_gid}")
35
35
  end
36
36
 
37
37
  def task
38
- @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.project')
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
39
44
  end
40
45
  end
41
46
  end
@@ -6,27 +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]
29
+
30
+ warn("Configuration cleared")
30
31
  end
31
32
  end
32
33
  end
@@ -6,68 +6,53 @@ 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
19
  require_project!
20
+ ensure_valid_configuration!
18
21
 
19
- if same_args_as_config? || !config.local_available?
20
- show_current_configuration
21
- else
22
- cli.warn 'Updating configuration'
23
- update_configuration
22
+ if path != config.path
23
+ config.path = path
24
+ warn("Configuration updated")
24
25
  end
25
- end
26
26
 
27
- private
28
-
29
- def show_current_configuration
30
- if task_gid.nil?
31
- print_project(project)
32
- else
33
- print_task(project, task)
34
- end
27
+ print_configuration
35
28
  end
36
29
 
37
- def update_configuration
38
- ensure_project_is_valid!
39
- config.project_gid = project_gid
40
-
41
- if task_gid.nil?
42
- print_project(project)
43
- config.task_gid = nil
44
- else
45
- ensure_task_is_valid!
46
- config.task_gid = task_gid
47
-
48
- print_task(project, task)
49
- end
50
- end
30
+ private
51
31
 
52
- def ensure_project_is_valid!
53
- cli.abort "Invalid project: #{project_gid}" if project.nil?
32
+ def print_configuration
33
+ task_gid.nil? ? print_project(project) : print_task(project, task)
54
34
  end
55
35
 
56
- def ensure_task_is_valid!
57
- cli.abort "Invalid task: #{task_gid}" if task.nil?
36
+ def ensure_valid_configuration!
37
+ abort("Invalid project: #{project_gid}") if project.nil?
38
+ abort("Invalid task: #{task_gid}") if task_gid && task.nil?
58
39
  end
59
40
 
60
41
  def project
61
42
  @project ||= begin
62
- cli.warn 'Fetching project...'
63
- api.get("projects/#{project_gid}", opt_fields: 'name,permalink_url')
43
+ warn("Fetching project...")
44
+ api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
45
+ rescue Abt::HttpError::NotFoundError
46
+ nil
64
47
  end
65
48
  end
66
49
 
67
50
  def task
68
51
  @task ||= begin
69
- cli.warn 'Fetching task...'
70
- api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url')
52
+ warn("Fetching task...")
53
+ api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
54
+ rescue Abt::HttpError::NotFoundError
55
+ nil
71
56
  end
72
57
  end
73
58
  end