abt-cli 0.0.14 → 0.0.19

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -1
  3. data/lib/abt.rb +4 -3
  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 +89 -54
  8. data/lib/abt/cli/arguments_parser.rb +48 -0
  9. data/lib/abt/cli/{dialogs.rb → prompt.rb} +38 -18
  10. data/lib/abt/docs.rb +35 -28
  11. data/lib/abt/docs/cli.rb +42 -11
  12. data/lib/abt/docs/markdown.rb +38 -11
  13. data/lib/abt/git_config.rb +26 -31
  14. data/lib/abt/providers/asana/base_command.rb +17 -37
  15. data/lib/abt/providers/asana/commands/add.rb +15 -13
  16. data/lib/abt/providers/asana/commands/{branch-name.rb → branch_name.rb} +12 -7
  17. data/lib/abt/providers/asana/commands/clear.rb +19 -6
  18. data/lib/abt/providers/asana/commands/current.rb +22 -37
  19. data/lib/abt/providers/asana/commands/finalize.rb +6 -6
  20. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +12 -7
  21. data/lib/abt/providers/asana/commands/init.rb +11 -11
  22. data/lib/abt/providers/asana/commands/pick.rb +30 -17
  23. data/lib/abt/providers/asana/commands/projects.rb +4 -4
  24. data/lib/abt/providers/asana/commands/share.rb +5 -9
  25. data/lib/abt/providers/asana/commands/start.rb +27 -19
  26. data/lib/abt/providers/asana/commands/tasks.rb +7 -6
  27. data/lib/abt/providers/asana/configuration.rb +23 -37
  28. data/lib/abt/providers/asana/path.rb +36 -0
  29. data/lib/abt/providers/devops/api.rb +12 -0
  30. data/lib/abt/providers/devops/base_command.rb +18 -44
  31. data/lib/abt/providers/devops/commands/boards.rb +7 -5
  32. data/lib/abt/providers/devops/commands/{branch-name.rb → branch_name.rb} +10 -6
  33. data/lib/abt/providers/devops/commands/clear.rb +19 -6
  34. data/lib/abt/providers/devops/commands/current.rb +17 -41
  35. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +12 -4
  36. data/lib/abt/providers/devops/commands/init.rb +20 -20
  37. data/lib/abt/providers/devops/commands/pick.rb +18 -18
  38. data/lib/abt/providers/devops/commands/share.rb +6 -7
  39. data/lib/abt/providers/devops/commands/work-items.rb +4 -4
  40. data/lib/abt/providers/devops/configuration.rb +20 -57
  41. data/lib/abt/providers/devops/path.rb +50 -0
  42. data/lib/abt/providers/git/commands/branch.rb +28 -28
  43. data/lib/abt/providers/harvest/base_command.rb +18 -36
  44. data/lib/abt/providers/harvest/commands/clear.rb +19 -6
  45. data/lib/abt/providers/harvest/commands/current.rb +27 -34
  46. data/lib/abt/providers/harvest/commands/init.rb +10 -11
  47. data/lib/abt/providers/harvest/commands/pick.rb +16 -9
  48. data/lib/abt/providers/harvest/commands/projects.rb +4 -4
  49. data/lib/abt/providers/harvest/commands/share.rb +7 -11
  50. data/lib/abt/providers/harvest/commands/start.rb +6 -42
  51. data/lib/abt/providers/harvest/commands/stop.rb +10 -10
  52. data/lib/abt/providers/harvest/commands/tasks.rb +7 -4
  53. data/lib/abt/providers/harvest/commands/track.rb +66 -21
  54. data/lib/abt/providers/harvest/configuration.rb +23 -38
  55. data/lib/abt/providers/harvest/path.rb +36 -0
  56. data/lib/abt/version.rb +1 -1
  57. metadata +12 -9
  58. data/lib/abt/cli/io.rb +0 -23
  59. data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
  60. data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
  61. data/lib/abt/providers/harvest/commands/clear_global.rb +0 -24
@@ -5,19 +5,19 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class BranchName < BaseCommand
8
- def self.command
9
- 'branch-name asana[:<project-gid>/<task-gid>]'
8
+ def self.usage
9
+ 'abt branch-name asana[:<project-gid>/<task-gid>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Suggest a git branch name for the current/specified task.'
14
14
  end
15
15
 
16
- def call
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
@@ -27,15 +27,20 @@ module Abt
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
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
@@ -5,17 +5,30 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Clear < BaseCommand
8
- def self.command
9
- 'clear asana'
8
+ def self.usage
9
+ 'abt clear asana'
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Clear project/task for current git repository'
13
+ 'Clear asana configuration'
14
14
  end
15
15
 
16
- def call
17
- cli.warn 'Clearing Asana project configuration'
18
- config.clear_local
16
+ def self.flags
17
+ [
18
+ ['-g', '--global', 'Clear global instead of local asana configuration (credentials etc.)'],
19
+ ['-a', '--all', 'Clear all asana configuration']
20
+ ]
21
+ end
22
+
23
+ def perform
24
+ if flags[:global] && flags[:all]
25
+ abort('Flags --global and --all cannot be used at the same time')
26
+ end
27
+
28
+ config.clear_local unless flags[:global]
29
+ config.clear_global if flags[:global] || flags[:all]
30
+
31
+ warn 'Configuration cleared'
19
32
  end
20
33
  end
21
34
  end
@@ -5,69 +5,54 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Current < BaseCommand
8
- def self.command
9
- 'current asana[:<project-gid>[/<task-gid>]]'
8
+ def self.usage
9
+ 'abt current asana[:<project-gid>[/<task-gid>]]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Get or set project and or task for current git repository'
14
14
  end
15
15
 
16
- def call
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...'
43
+ warn 'Fetching project...'
63
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...'
52
+ warn 'Fetching task...'
70
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
@@ -5,25 +5,25 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Finalize < BaseCommand
8
- def self.command
9
- 'finalize asana[:<project-gid>/<task-gid>]'
8
+ def self.usage
9
+ 'abt finalize asana[:<project-gid>/<task-gid>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Move current/specified task to section (column) for finalized tasks'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
17
  unless config.local_available?
18
- cli.abort 'This is a no-op for tasks outside the current project'
18
+ abort 'This is a no-op for tasks outside the current project'
19
19
  end
20
20
  require_task!
21
21
  print_task(project_gid, task)
22
22
 
23
23
  if task_already_in_finalized_section?
24
- cli.warn "Task already in section: #{current_task_section['name']}"
24
+ warn "Task already in section: #{current_task_section['name']}"
25
25
  else
26
- cli.warn "Moving task to section: #{finalized_section['name']}"
26
+ warn "Moving task to section: #{finalized_section['name']}"
27
27
  move_task
28
28
  end
29
29
  end
@@ -5,15 +5,15 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class HarvestTimeEntryData < BaseCommand
8
- def self.command
9
- 'harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
8
+ def self.usage
9
+ 'abt harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Print Harvest time entry data for Asana task as json. Used by harvest start script.'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
17
  require_task!
18
18
  ensure_current_is_valid!
19
19
 
@@ -26,21 +26,26 @@ module Abt
26
26
  }
27
27
  }
28
28
 
29
- cli.puts Oj.dump(body, mode: :json)
29
+ puts Oj.dump(body, mode: :json)
30
30
  end
31
31
 
32
32
  private
33
33
 
34
34
  def ensure_current_is_valid!
35
- cli.abort "Invalid task gid: #{task_gid}" if task.nil?
35
+ abort "Invalid task gid: #{task_gid}" if task.nil?
36
36
 
37
37
  return if task['memberships'].any? { |m| m.dig('project', 'gid') == project_gid }
38
38
 
39
- cli.abort "Invalid project gid: #{project_gid}"
39
+ abort "Invalid or unmatching project gid: #{project_gid}"
40
40
  end
41
41
 
42
42
  def task
43
- @task ||= api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url,memberships.project')
43
+ @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
48
+ end
44
49
  end
45
50
  end
46
51
  end
@@ -5,8 +5,8 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Init < BaseCommand
8
- def self.command
9
- 'init asana'
8
+ def self.usage
9
+ 'abt init asana'
10
10
  end
11
11
 
12
12
  def self.description
@@ -18,13 +18,13 @@ module Abt
18
18
  @cli = cli
19
19
  end
20
20
 
21
- def call
22
- cli.abort 'Must be run inside a git repository' unless config.local_available?
21
+ def perform
22
+ abort 'Must be run inside a git repository' unless config.local_available?
23
23
 
24
24
  projects # Load projects up front to make it obvious that searches are instant
25
25
  project = find_search_result
26
26
 
27
- config.project_gid = project['gid']
27
+ config.path = Path.from_ids(project['gid'])
28
28
 
29
29
  print_project(project)
30
30
  end
@@ -32,17 +32,17 @@ module Abt
32
32
  private
33
33
 
34
34
  def find_search_result
35
- cli.warn 'Select a project'
35
+ warn 'Select a project'
36
36
 
37
37
  loop do
38
- matches = matches_for_string cli.prompt('Enter search')
38
+ matches = matches_for_string cli.prompt.text('Enter search')
39
39
  if matches.empty?
40
- cli.warn 'No matches'
40
+ warn 'No matches'
41
41
  next
42
42
  end
43
43
 
44
- cli.warn 'Showing the 10 first matches' if matches.size > 10
45
- choice = cli.prompt_choice 'Select a project', matches[0...10], true
44
+ warn 'Showing the 10 first matches' if matches.size > 10
45
+ choice = cli.prompt.choice 'Select a project', matches[0...10], true
46
46
  break choice unless choice.nil?
47
47
  end
48
48
  end
@@ -61,7 +61,7 @@ module Abt
61
61
 
62
62
  def projects
63
63
  @projects ||= begin
64
- cli.warn 'Fetching projects...'
64
+ warn 'Fetching projects...'
65
65
  api.get_paged('projects',
66
66
  workspace: config.workspace_gid,
67
67
  archived: false,
@@ -5,60 +5,73 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Pick < BaseCommand
8
- def self.command
9
- 'pick asana[:<project-gid>]'
8
+ def self.usage
9
+ 'abt pick asana[:<project-gid>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Pick task for current git repository'
14
14
  end
15
15
 
16
- def call
17
- cli.abort 'Must be run inside a git repository' unless config.local_available?
16
+ def self.flags
17
+ [
18
+ ['-d', '--dry-run', 'Keep existing configuration']
19
+ ]
20
+ end
21
+
22
+ def perform
23
+ abort 'Must be run inside a git repository' unless config.local_available?
18
24
  require_project!
19
25
 
20
- cli.warn project['name']
26
+ warn project['name']
21
27
 
22
28
  task = select_task
23
29
 
24
- config.project_gid = project_gid # We might have gotten the project ID as an argument
25
- config.task_gid = task['gid']
26
-
27
30
  print_task(project, task)
31
+
32
+ return if flags[:"dry-run"]
33
+
34
+ config.path = Path.from_ids(project_gid, task['gid'])
28
35
  end
29
36
 
30
37
  private
31
38
 
32
39
  def project
33
- @project ||= api.get("projects/#{project_gid}")
40
+ @project ||= api.get("projects/#{project_gid}", opt_fields: 'name')
34
41
  end
35
42
 
36
43
  def select_task
37
44
  loop do
38
- section = cli.prompt_choice 'Which section?', sections
39
- cli.warn 'Fetching tasks...'
45
+ section = cli.prompt.choice 'Which section?', sections
46
+ warn 'Fetching tasks...'
40
47
  tasks = tasks_in_section(section)
41
48
 
42
49
  if tasks.length.zero?
43
- cli.warn 'Section is empty'
50
+ warn 'Section is empty'
44
51
  next
45
52
  end
46
53
 
47
- task = cli.prompt_choice 'Select a task', tasks, true
54
+ task = cli.prompt.choice 'Select a task', tasks, true
48
55
  return task if task
49
56
  end
50
57
  end
51
58
 
52
59
  def tasks_in_section(section)
53
- api.get_paged('tasks', section: section['gid'], opt_fields: 'name,permalink_url')
60
+ tasks = api.get_paged(
61
+ 'tasks',
62
+ section: section['gid'],
63
+ opt_fields: 'name,completed,permalink_url'
64
+ )
65
+
66
+ # The below filtering is the best we can do with Asanas api, see this:
67
+ # https://forum.asana.com/t/tasks-query-completed-since-is-broken-for-sections/21461
68
+ tasks.filter { |task| !task['completed'] }
54
69
  end
55
70
 
56
71
  def sections
57
72
  @sections ||= begin
58
- cli.warn 'Fetching sections...'
73
+ warn 'Fetching sections...'
59
74
  api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
60
- rescue Abt::HttpError::HttpError
61
- []
62
75
  end
63
76
  end
64
77
  end
@@ -5,15 +5,15 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Projects < BaseCommand
8
- def self.command
9
- 'projects asana'
8
+ def self.usage
9
+ 'abt projects asana'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'List all available projects - useful for piping into grep etc.'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
17
  projects.map do |project|
18
18
  print_project(project)
19
19
  end
@@ -23,7 +23,7 @@ module Abt
23
23
 
24
24
  def projects
25
25
  @projects ||= begin
26
- cli.warn 'Fetching projects...'
26
+ warn 'Fetching projects...'
27
27
  api.get_paged(
28
28
  'projects',
29
29
  workspace: config.workspace_gid,
@@ -5,22 +5,18 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Share < BaseCommand
8
- def self.command
9
- 'share asana[:<project-gid>[/<task-gid>]]'
8
+ def self.usage
9
+ 'abt share asana[:<project-gid>[/<task-gid>]]'
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Print project/task config string'
13
+ 'Print project/task ARI'
14
14
  end
15
15
 
16
- def call
16
+ def perform
17
17
  require_project!
18
18
 
19
- if task_gid.nil?
20
- cli.print_provider_command('asana', project_gid)
21
- else
22
- cli.print_provider_command('asana', "#{project_gid}/#{task_gid}")
23
- end
19
+ cli.print_ari('asana', path)
24
20
  end
25
21
  end
26
22
  end