abt-cli 0.0.18 → 0.0.23

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 (68) 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 +51 -52
  8. data/lib/abt/cli/arguments_parser.rb +7 -26
  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 -51
  17. data/lib/abt/docs.rb +48 -25
  18. data/lib/abt/docs/cli.rb +3 -3
  19. data/lib/abt/docs/markdown.rb +11 -8
  20. data/lib/abt/git_config.rb +21 -39
  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 +20 -38
  24. data/lib/abt/providers/asana/commands/add.rb +18 -15
  25. data/lib/abt/providers/asana/commands/branch_name.rb +13 -8
  26. data/lib/abt/providers/asana/commands/clear.rb +8 -7
  27. data/lib/abt/providers/asana/commands/current.rb +22 -38
  28. data/lib/abt/providers/asana/commands/finalize.rb +17 -18
  29. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +20 -13
  30. data/lib/abt/providers/asana/commands/init.rb +8 -41
  31. data/lib/abt/providers/asana/commands/pick.rb +27 -26
  32. data/lib/abt/providers/asana/commands/projects.rb +5 -5
  33. data/lib/abt/providers/asana/commands/share.rb +6 -8
  34. data/lib/abt/providers/asana/commands/start.rb +33 -24
  35. data/lib/abt/providers/asana/commands/tasks.rb +6 -5
  36. data/lib/abt/providers/asana/configuration.rb +46 -44
  37. data/lib/abt/providers/asana/path.rb +36 -0
  38. data/lib/abt/providers/devops/api.rb +23 -11
  39. data/lib/abt/providers/devops/base_command.rb +22 -43
  40. data/lib/abt/providers/devops/commands/boards.rb +5 -7
  41. data/lib/abt/providers/devops/commands/branch_name.rb +14 -10
  42. data/lib/abt/providers/devops/commands/clear.rb +8 -7
  43. data/lib/abt/providers/devops/commands/current.rb +24 -49
  44. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +26 -16
  45. data/lib/abt/providers/devops/commands/init.rb +33 -26
  46. data/lib/abt/providers/devops/commands/pick.rb +23 -24
  47. data/lib/abt/providers/devops/commands/share.rb +7 -6
  48. data/lib/abt/providers/devops/commands/{work-items.rb → work_items.rb} +3 -3
  49. data/lib/abt/providers/devops/configuration.rb +27 -56
  50. data/lib/abt/providers/devops/path.rb +51 -0
  51. data/lib/abt/providers/git/commands/branch.rb +25 -19
  52. data/lib/abt/providers/harvest/api.rb +8 -8
  53. data/lib/abt/providers/harvest/base_command.rb +20 -36
  54. data/lib/abt/providers/harvest/commands/clear.rb +8 -7
  55. data/lib/abt/providers/harvest/commands/current.rb +27 -35
  56. data/lib/abt/providers/harvest/commands/init.rb +10 -40
  57. data/lib/abt/providers/harvest/commands/pick.rb +15 -12
  58. data/lib/abt/providers/harvest/commands/projects.rb +5 -5
  59. data/lib/abt/providers/harvest/commands/share.rb +6 -8
  60. data/lib/abt/providers/harvest/commands/start.rb +5 -3
  61. data/lib/abt/providers/harvest/commands/stop.rb +13 -13
  62. data/lib/abt/providers/harvest/commands/tasks.rb +9 -6
  63. data/lib/abt/providers/harvest/commands/track.rb +60 -38
  64. data/lib/abt/providers/harvest/configuration.rb +28 -37
  65. data/lib/abt/providers/harvest/path.rb +36 -0
  66. data/lib/abt/version.rb +1 -1
  67. metadata +18 -6
  68. 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)
data/lib/abt/helpers.rb CHANGED
@@ -2,15 +2,33 @@
2
2
 
3
3
  module Abt
4
4
  module Helpers
5
- def self.const_to_command(string)
6
- string = string.to_s.dup
7
- string[0] = string[0].downcase
8
- string.gsub(/([A-Z])/, '-\1').downcase
9
- end
5
+ class << self
6
+ def const_to_command(string)
7
+ string = string.to_s.dup
8
+ string[0] = string[0].downcase
9
+ string.gsub(/([A-Z])/, '-\1').downcase
10
+ end
11
+
12
+ def command_to_const(string)
13
+ inflector = Dry::Inflector.new
14
+ inflector.camelize(inflector.underscore(string))
15
+ end
16
+
17
+ def read_user_input
18
+ open(tty_path, &:gets).strip # rubocop:disable Security/Open
19
+ end
20
+
21
+ private
22
+
23
+ def tty_path
24
+ @tty_path ||= begin
25
+ candidates = ["/dev/tty", "CON:"] # Unix: '/dev/tty', Windows: 'CON:'
26
+ selected = candidates.find { |candidate| File.exist?(candidate) }
27
+ raise Abort, "Unable to prompt for user input" if selected.nil?
10
28
 
11
- def self.command_to_const(string)
12
- inflector = Dry::Inflector.new
13
- inflector.camelize(inflector.underscore(string))
29
+ selected
30
+ end
31
+ end
14
32
  end
15
33
  end
16
34
  end
@@ -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,45 @@
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
- def require_project!
24
- cli.abort 'No current/specified project. Did you initialize Asana?' if project_gid.nil?
23
+ def require_local_config!
24
+ abort("Must be run inside a git repository") unless config.local_available?
25
25
  end
26
26
 
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?
27
+ def require_project!
28
+ abort("No current/specified project. Did you initialize Asana?") if project_gid.nil?
32
29
  end
33
30
 
34
- def same_args_as_config?
35
- project_gid == config.project_gid && task_gid == config.task_gid
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?
36
34
  end
37
35
 
38
36
  def print_project(project)
39
- cli.print_ari('asana', project['gid'], project['name'])
40
- cli.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
41
39
  end
42
40
 
43
41
  def print_task(project, task)
44
- project = { 'gid' => project } if project.is_a?(String)
45
- cli.print_ari('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?
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
63
45
  end
64
46
 
65
47
  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,52 @@ 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
+ require_local_config!
17
18
  require_project!
19
+ ensure_valid_configuration!
18
20
 
19
- if same_args_as_config? || !config.local_available?
20
- show_current_configuration
21
- else
22
- cli.warn 'Updating configuration'
23
- update_configuration
21
+ if path != config.path
22
+ config.path = path
23
+ warn("Configuration updated")
24
24
  end
25
- end
26
-
27
- private
28
25
 
29
- def show_current_configuration
30
- if task_gid.nil?
31
- print_project(project)
32
- else
33
- print_task(project, task)
34
- end
26
+ print_configuration
35
27
  end
36
28
 
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
29
+ private
51
30
 
52
- def ensure_project_is_valid!
53
- cli.abort "Invalid project: #{project_gid}" if project.nil?
31
+ def print_configuration
32
+ task_gid.nil? ? print_project(project) : print_task(project, task)
54
33
  end
55
34
 
56
- def ensure_task_is_valid!
57
- cli.abort "Invalid task: #{task_gid}" if task.nil?
35
+ def ensure_valid_configuration!
36
+ abort("Invalid project: #{project_gid}") if project.nil?
37
+ abort("Invalid task: #{task_gid}") if task_gid && task.nil?
58
38
  end
59
39
 
60
40
  def project
61
41
  @project ||= begin
62
- cli.warn 'Fetching project...'
63
- api.get("projects/#{project_gid}", opt_fields: 'name,permalink_url')
42
+ warn("Fetching project...")
43
+ api.get("projects/#{project_gid}", opt_fields: "name,permalink_url")
44
+ rescue Abt::HttpError::NotFoundError
45
+ nil
64
46
  end
65
47
  end
66
48
 
67
49
  def task
68
50
  @task ||= begin
69
- cli.warn 'Fetching task...'
70
- api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url')
51
+ warn("Fetching task...")
52
+ api.get("tasks/#{task_gid}", opt_fields: "name,permalink_url")
53
+ rescue Abt::HttpError::NotFoundError
54
+ nil
71
55
  end
72
56
  end
73
57
  end