abt-cli 0.0.7 → 0.0.12

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -0
  3. data/lib/abt/cli.rb +5 -5
  4. data/lib/abt/cli/dialogs.rb +36 -14
  5. data/lib/abt/docs.rb +4 -0
  6. data/lib/abt/git_config.rb +13 -0
  7. data/lib/abt/providers/asana/base_command.rb +11 -0
  8. data/lib/abt/providers/asana/commands/add.rb +75 -0
  9. data/lib/abt/providers/asana/commands/current.rb +3 -3
  10. data/lib/abt/providers/asana/commands/finalize.rb +1 -1
  11. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +3 -4
  12. data/lib/abt/providers/asana/commands/init.rb +5 -0
  13. data/lib/abt/providers/asana/commands/pick.rb +17 -4
  14. data/lib/abt/providers/asana/commands/share.rb +3 -3
  15. data/lib/abt/providers/asana/commands/start.rb +1 -1
  16. data/lib/abt/providers/asana/commands/tasks.rb +2 -0
  17. data/lib/abt/providers/asana/configuration.rb +9 -3
  18. data/lib/abt/providers/devops.rb +19 -0
  19. data/lib/abt/providers/devops/api.rb +77 -0
  20. data/lib/abt/providers/devops/base_command.rb +97 -0
  21. data/lib/abt/providers/devops/commands/boards.rb +34 -0
  22. data/lib/abt/providers/devops/commands/clear.rb +24 -0
  23. data/lib/abt/providers/devops/commands/clear_global.rb +24 -0
  24. data/lib/abt/providers/devops/commands/current.rb +93 -0
  25. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +53 -0
  26. data/lib/abt/providers/devops/commands/init.rb +72 -0
  27. data/lib/abt/providers/devops/commands/pick.rb +78 -0
  28. data/lib/abt/providers/devops/commands/share.rb +26 -0
  29. data/lib/abt/providers/devops/commands/work-items.rb +46 -0
  30. data/lib/abt/providers/devops/configuration.rb +110 -0
  31. data/lib/abt/providers/harvest/base_command.rb +11 -0
  32. data/lib/abt/providers/harvest/commands/current.rb +3 -3
  33. data/lib/abt/providers/harvest/commands/pick.rb +1 -0
  34. data/lib/abt/providers/harvest/commands/start.rb +21 -65
  35. data/lib/abt/providers/harvest/commands/tasks.rb +2 -0
  36. data/lib/abt/providers/harvest/commands/track.rb +72 -0
  37. data/lib/abt/providers/harvest/configuration.rb +4 -3
  38. data/lib/abt/version.rb +1 -1
  39. metadata +17 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd99c9123cfbd4222176495fd039015ad56ddaf0841350347169fa6809f8d6b2
4
- data.tar.gz: 33d0adf3e73be8f5b2a6f52d61cea46285e0e2ab036b2d1b0e1b4fe1f2c0a27a
3
+ metadata.gz: 4f1631b9069ca5094ba301eb3ccc5cf42da8e48a85b2678e8499cea1e9e4551c
4
+ data.tar.gz: cd9dc57e8a6276de5a2bca61e59fb537bc8887901615d7ec86f2c4a59145f76c
5
5
  SHA512:
6
- metadata.gz: 2ea6b3ea3f1f2056b9e7911144542f0d2f3b568544fcd68901299fb64c84414290d7cecfc884405bde0125b096bc7033b99d3c2eb87e0598f434de79cfa76ae3
7
- data.tar.gz: 3c8ffb1c3ba6553d16611c0f55a1c1bae3cf97d156fc79242f66b2d0b2ac622b08ab4ef049b98299553c708dc36365fc4e6158e31b556d830d863019134f34eb
6
+ metadata.gz: bdca57213f85db7a3604ac1a5ed50a16a2918fded5b3b3e090e52aeb184414345d7609f08ef5145f205cf3fb9f1576f9e0aaa6b25a2e41684cfd506bed19e851
7
+ data.tar.gz: d7eb95a22516aa0f894262e0a1b52ee659736b88775936040a87db03260711e12dbc6764674799b5d2f8bdb86cb14495f01440a0703d300759c3482db17a810a
data/bin/abt CHANGED
@@ -5,6 +5,7 @@ require 'dry-inflector'
5
5
  require 'faraday'
6
6
  require 'oj'
7
7
  require 'open3'
8
+ require 'stringio'
8
9
 
9
10
  require_relative '../lib/abt.rb'
10
11
 
data/lib/abt/cli.rb CHANGED
@@ -20,7 +20,7 @@ module Abt
20
20
  @output = output
21
21
  @err_output = err_output
22
22
 
23
- @args += args_from_stdin unless input.isatty # Add piped arguments
23
+ @args += args_from_input unless input.isatty # Add piped arguments
24
24
  end
25
25
 
26
26
  def perform
@@ -57,13 +57,13 @@ module Abt
57
57
  end
58
58
  end
59
59
 
60
- def args_from_stdin
61
- input = STDIN.read
60
+ def args_from_input
61
+ input_string = input.read
62
62
 
63
- return [] if input.nil?
63
+ abort 'No input from pipe' if input_string.nil? || input_string.empty?
64
64
 
65
65
  # Exclude comment part of piped input lines
66
- lines_without_comments = input.lines.map do |line|
66
+ lines_without_comments = input_string.lines.map do |line|
67
67
  line.split(' # ').first
68
68
  end
69
69
 
@@ -24,16 +24,18 @@ module Abt
24
24
  end
25
25
  end
26
26
 
27
- def prompt_choice(text, options, allow_back_option = false)
28
- if options.one?
29
- warn "Selected: #{options.first['name']}"
30
- return options.first
31
- end
32
-
27
+ def prompt_choice(text, options, nil_option = false)
33
28
  warn "#{text}:"
34
29
 
30
+ if options.length.zero?
31
+ abort 'No available options' unless nil_option
32
+
33
+ warn 'No available options'
34
+ return nil
35
+ end
36
+
35
37
  print_options(options)
36
- select_options(options, allow_back_option)
38
+ select_options(options, nil_option)
37
39
  end
38
40
 
39
41
  private
@@ -44,12 +46,13 @@ module Abt
44
46
  end
45
47
  end
46
48
 
47
- def select_options(options, allow_back_option)
48
- while (number = read_option_number(options.length, allow_back_option))
49
+ def select_options(options, nil_option)
50
+ loop do
51
+ number = read_option_number(options.length, nil_option)
49
52
  if number.nil?
50
- return nil if allow_back_option
53
+ return nil if nil_option
51
54
 
52
- abort
55
+ next
53
56
  end
54
57
 
55
58
  option = options[number - 1]
@@ -59,12 +62,12 @@ module Abt
59
62
  end
60
63
  end
61
64
 
62
- def read_option_number(options_length, allow_back_option)
63
- err_output.print "(1-#{options_length}#{allow_back_option ? ', q: back' : ''}): "
65
+ def read_option_number(options_length, nil_option)
66
+ err_output.print "(1-#{options_length}#{nil_option_string(nil_option)}): "
64
67
 
65
68
  input = read_user_input
66
69
 
67
- return nil if allow_back_option && input == 'q'
70
+ return nil if nil_option && input == nil_option_character(nil_option)
68
71
 
69
72
  option_number = input.to_i
70
73
  if option_number <= 0 || option_number > options_length
@@ -75,6 +78,25 @@ module Abt
75
78
  option_number
76
79
  end
77
80
 
81
+ def nil_option_string(nil_option)
82
+ return '' unless nil_option
83
+
84
+ ", #{nil_option_character(nil_option)}: #{nil_option_description(nil_option)}"
85
+ end
86
+
87
+ def nil_option_character(nil_option)
88
+ return 'q' if nil_option == true
89
+
90
+ nil_option[0]
91
+ end
92
+
93
+ def nil_option_description(nil_option)
94
+ return 'back' if nil_option == true
95
+ return nil_option if nil_option.is_a?(String)
96
+
97
+ nil_option[1]
98
+ end
99
+
78
100
  def read_user_input
79
101
  open('/dev/tty', &:gets).strip
80
102
  end
data/lib/abt/docs.rb CHANGED
@@ -17,6 +17,10 @@ module Abt
17
17
  'abt start asana harvest' => 'Continue working, e.g. after a break',
18
18
  'abt finalize asana' => 'Finalize the selected asana task'
19
19
  },
20
+ 'Tracking meetings (without changing the config):' => {
21
+ 'abt tasks asana | grep -i standup | abt track harvest' => 'Track on asana meeting task without changing any configuration',
22
+ 'abt tasks harvest | grep -i comment | abt track harvest' => 'Track on harvest "Comment"-task (will prompt for a comment)'
23
+ },
20
24
  'Command output can be piped, e.g.:' => {
21
25
  'abt tasks asana | grep -i <name of task>' => nil,
22
26
  'abt tasks asana | grep -i <name of task> | abt start' => nil
@@ -32,6 +32,19 @@ module Abt
32
32
  set(key, value)
33
33
  end
34
34
 
35
+ def full_keys
36
+ if scope == 'local' && !self.class.local_available?
37
+ raise StandardError, 'Local configuration is not available outside a git repository'
38
+ end
39
+
40
+ `git config --#{scope} --get-regexp --name-only ^#{namespace}`.lines.map(&:strip)
41
+ end
42
+
43
+ def keys
44
+ offset = namespace.length + 1
45
+ full_keys.map { |key| key[offset..-1] }
46
+ end
47
+
35
48
  def local
36
49
  @local ||= begin
37
50
  if scope == 'local'
@@ -20,6 +20,17 @@ module Abt
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?
25
+ end
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?
32
+ end
33
+
23
34
  def same_args_as_config?
24
35
  project_gid == config.project_gid && task_gid == config.task_gid
25
36
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Add < BaseCommand
8
+ def self.command
9
+ 'add asana[:<project-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Create a new task for the current/specified Asana project'
14
+ end
15
+
16
+ def call
17
+ require_project!
18
+
19
+ task
20
+ print_task(project, task)
21
+
22
+ move_task if section
23
+ end
24
+
25
+ private
26
+
27
+ def task
28
+ @task ||= begin
29
+ body = {
30
+ data: {
31
+ name: name,
32
+ notes: notes,
33
+ projects: [project_gid]
34
+ }
35
+ }
36
+ cli.warn 'Creating task'
37
+ api.post('tasks', Oj.dump(body, mode: :json))
38
+ end
39
+ end
40
+
41
+ def move_task
42
+ body = { data: { task: task['gid'] } }
43
+ body_json = Oj.dump(body, mode: :json)
44
+ api.post("sections/#{section['gid']}/addTask", body_json)
45
+ end
46
+
47
+ def name
48
+ @name ||= cli.prompt 'Enter task description'
49
+ end
50
+
51
+ def notes
52
+ @notes ||= cli.prompt 'Enter task notes'
53
+ end
54
+
55
+ def project
56
+ @project ||= api.get("projects/#{project_gid}")
57
+ end
58
+
59
+ def section
60
+ @section ||= cli.prompt_choice 'Add to section?', sections, ['q', 'Don\'t add to section']
61
+ end
62
+
63
+ def sections
64
+ @sections ||= begin
65
+ cli.warn 'Fetching sections...'
66
+ api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
67
+ rescue Abt::HttpError::HttpError
68
+ []
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -14,6 +14,8 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
+ require_project!
18
+
17
19
  if same_args_as_config? || !config.local_available?
18
20
  show_current_configuration
19
21
  else
@@ -25,9 +27,7 @@ module Abt
25
27
  private
26
28
 
27
29
  def show_current_configuration
28
- if project_gid.nil?
29
- cli.warn 'No project selected'
30
- elsif task_gid.nil?
30
+ if task_gid.nil?
31
31
  print_project(project)
32
32
  else
33
33
  print_task(project, task)
@@ -17,7 +17,7 @@ module Abt
17
17
  unless config.local_available?
18
18
  cli.abort 'This is a no-op for tasks outside the current project'
19
19
  end
20
- cli.abort 'No current or specified task' if task.nil?
20
+ require_task!
21
21
  print_task(project_gid, task)
22
22
 
23
23
  if task_already_in_finalized_section?
@@ -13,7 +13,8 @@ module Abt
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 # rubocop:disable Metrics/MethodLength
16
+ def call
17
+ require_task!
17
18
  ensure_current_is_valid!
18
19
 
19
20
  body = {
@@ -21,9 +22,7 @@ module Abt
21
22
  external_reference: {
22
23
  id: task_gid.to_i,
23
24
  group_id: project_gid.to_i,
24
- permalink: task['permalink_url'],
25
- service: 'app.asana.com',
26
- service_icon_url: 'https://proxy.harvestfiles.com/production_harvestapp_public/uploads/platform_icons/app.asana.com.png'
25
+ permalink: task['permalink_url']
27
26
  }
28
27
  }
29
28
 
@@ -13,6 +13,11 @@ module Abt
13
13
  'Pick Asana project for current git repository'
14
14
  end
15
15
 
16
+ def initialize(cli:, **)
17
+ @config = Configuration.new(cli: cli)
18
+ @cli = cli
19
+ end
20
+
16
21
  def call
17
22
  cli.abort 'Must be run inside a git repository' unless config.local_available?
18
23
 
@@ -15,10 +15,11 @@ module Abt
15
15
 
16
16
  def call
17
17
  cli.abort 'Must be run inside a git repository' unless config.local_available?
18
+ require_project!
18
19
 
19
20
  cli.warn project['name']
20
21
 
21
- task = cli.prompt_choice 'Select a task', tasks
22
+ task = select_task
22
23
 
23
24
  config.project_gid = project_gid # We might have gotten the project ID as an argument
24
25
  config.task_gid = task['gid']
@@ -32,14 +33,26 @@ module Abt
32
33
  @project ||= api.get("projects/#{project_gid}")
33
34
  end
34
35
 
35
- def tasks
36
- @tasks ||= begin
36
+ def select_task
37
+ loop do
37
38
  section = cli.prompt_choice 'Which section?', sections
38
39
  cli.warn 'Fetching tasks...'
39
- api.get_paged('tasks', section: section['gid'], opt_fields: 'name,permalink_url')
40
+ tasks = tasks_in_section(section)
41
+
42
+ if tasks.length.zero?
43
+ cli.warn 'Section is empty'
44
+ next
45
+ end
46
+
47
+ task = cli.prompt_choice 'Select a task', tasks, true
48
+ return task if task
40
49
  end
41
50
  end
42
51
 
52
+ def tasks_in_section(section)
53
+ api.get_paged('tasks', section: section['gid'], opt_fields: 'name,permalink_url')
54
+ end
55
+
43
56
  def sections
44
57
  @sections ||= begin
45
58
  cli.warn 'Fetching sections...'
@@ -14,9 +14,9 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
- if project_gid.nil?
18
- cli.warn 'No project selected'
19
- elsif task_gid.nil?
17
+ require_project!
18
+
19
+ if task_gid.nil?
20
20
  cli.print_provider_command('asana', project_gid)
21
21
  else
22
22
  cli.print_provider_command('asana', "#{project_gid}/#{task_gid}")
@@ -14,7 +14,7 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
- abort 'No current/provided task' if task_gid.nil?
17
+ require_task!
18
18
 
19
19
  maybe_override_current_task
20
20
 
@@ -14,6 +14,8 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
+ require_project!
18
+
17
19
  tasks.each do |task|
18
20
  print_task(project, task)
19
21
  end
@@ -67,8 +67,10 @@ module Abt
67
67
  end
68
68
 
69
69
  def clear_global
70
- git.global['workspaceGid'] = nil
71
- git.global['accessToken'] = nil
70
+ git.global.keys.each do |key|
71
+ cli.puts 'Deleting configuration: ' + key
72
+ git.global[key] = nil
73
+ end
72
74
  end
73
75
 
74
76
  def access_token
@@ -109,9 +111,13 @@ module Abt
109
111
  workspaces = api.get_paged('workspaces')
110
112
  if workspaces.empty?
111
113
  cli.abort 'Your asana access token does not have access to any workspaces'
114
+ elsif workspaces.one?
115
+ workspace = workspaces.first
116
+ cli.warn "Selected Asana workspace #{workspace['name']}"
117
+ else
118
+ workspace = cli.prompt_choice('Select Asana workspace', workspaces)
112
119
  end
113
120
 
114
- workspace = cli.prompt_choice('Select Asana workspace', workspaces)
115
121
  git.global['workspaceGid'] = workspace['gid']
116
122
  workspace
117
123
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{File.expand_path(__dir__)}/devops/*.rb").sort.each { |file| require file }
4
+ Dir.glob("#{File.expand_path(__dir__)}/devops/commands/*.rb").sort.each { |file| require file }
5
+
6
+ module Abt
7
+ module Providers
8
+ module Devops
9
+ def self.command_names
10
+ Commands.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
11
+ end
12
+
13
+ def self.command_class(name)
14
+ const_name = Helpers.command_to_const(name)
15
+ Commands.const_get(const_name) if Commands.const_defined?(const_name)
16
+ end
17
+ end
18
+ end
19
+ end