abt-cli 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b24bcf0b7e31cf18b1f86381f6dabba6434e8dfbd4a077f5447afdd20f08d7e
4
- data.tar.gz: a66846cb48a109ea21bb7366c1432c8f7d647b5384f42565c90fa609c51d8b6b
3
+ metadata.gz: 0cc34f7579a887a49ae83db56c8b3808a0e00b09c55631f9b4224228477a0762
4
+ data.tar.gz: 9c26a06c3af53ebe55ca5e53f93b5d6d73854962daebef31d131000657be1c63
5
5
  SHA512:
6
- metadata.gz: b3610428c6ded90f53c49396d41b07fc54134dd88cb8019133c35dab230058b5506d93cfab0dc4b811b81161e45e24c1b0502dd7e03a8f3a65248db864886fb3
7
- data.tar.gz: 6affbc39d2572a73b700367d8cdabbe84275faf663b66b0c020c1683a3ffc7f584d43b828674cc2d32f1f05a8e2695443f13c64a2d1727e7fdb7708f93a0bcf5
6
+ metadata.gz: 4fe10297a130b747d93dc1398bdf404996d5c07987680deac59a5365d6893ab3d0466ebd764ef087f5eb2409b99a7733dc55a869432d81286f3460ec510d247d
7
+ data.tar.gz: 3684a1d3113f9df06637a6c7294f334b48e2c8cd5e54d4334c1ec1884571519f6e5aedeec327d8292f5364d1251453d6a7a8131074f91b2f94935b7a3586ddb9
@@ -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'
@@ -13,7 +13,7 @@ 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
17
  ensure_current_is_valid!
18
18
 
19
19
  body = {
@@ -21,9 +21,7 @@ module Abt
21
21
  external_reference: {
22
22
  id: task_gid.to_i,
23
23
  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'
24
+ permalink: task['permalink_url']
27
25
  }
28
26
  }
29
27
 
@@ -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
 
@@ -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
@@ -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
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class Api
7
+ VERBS = %i[get post put].freeze
8
+
9
+ attr_reader :organization_name, :project_name, :username, :access_token
10
+
11
+ def initialize(organization_name:, project_name:, username:, access_token:)
12
+ @organization_name = organization_name
13
+ @project_name = project_name
14
+ @username = username
15
+ @access_token = access_token
16
+ end
17
+
18
+ VERBS.each do |verb|
19
+ define_method(verb) do |*args|
20
+ request(verb, *args)
21
+ end
22
+ end
23
+
24
+ def get_paged(path, query = {})
25
+ result = request(:get, path, query)
26
+ result['value']
27
+
28
+ # TODO: Loop if necessary
29
+ end
30
+
31
+ def request(*args)
32
+ response = connection.public_send(*args)
33
+
34
+ if response.success?
35
+ Oj.load(response.body)
36
+ else
37
+ error_class = Abt::HttpError.error_class_for_status(response.status)
38
+ encoded_response_body = response.body.force_encoding('utf-8')
39
+ raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
40
+ end
41
+ end
42
+
43
+ def base_url
44
+ "https://#{organization_name}.visualstudio.com/#{project_name}"
45
+ end
46
+
47
+ def api_endpoint
48
+ "#{base_url}/_apis"
49
+ end
50
+
51
+ def url_for_work_item(work_item)
52
+ "#{base_url}/_workitems/edit/#{work_item['id']}"
53
+ end
54
+
55
+ def connection
56
+ @connection ||= Faraday.new(api_endpoint) do |connection|
57
+ connection.basic_auth username, access_token
58
+ connection.headers['Content-Type'] = 'application/json'
59
+ connection.headers['Accept'] = 'application/json; api-version=6.0'
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class BaseCommand
7
+ attr_reader :arg_str, :organization_name, :project_name, :board_id, :work_item_id, :cli, :config
8
+
9
+ def initialize(arg_str:, cli:)
10
+ @arg_str = arg_str
11
+
12
+ @config = Configuration.new(cli: cli)
13
+ @cli = cli
14
+
15
+ if arg_str.nil?
16
+ use_current_args
17
+ else
18
+ use_arg_str(arg_str)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def sanitize_work_item(work_item)
25
+ return nil if work_item.nil?
26
+
27
+ work_item.merge(
28
+ 'id' => work_item['id'].to_s,
29
+ 'name' => work_item['fields']['System.Title'],
30
+ 'url' => api.url_for_work_item(work_item)
31
+ )
32
+ end
33
+
34
+ def same_args_as_config?
35
+ organization_name == config.organization_name &&
36
+ project_name == config.project_name &&
37
+ board_id == config.board_id &&
38
+ work_item_id == config.work_item_id
39
+ end
40
+
41
+ def print_board(organization_name, project_name, board)
42
+ arg_str = "#{organization_name}/#{project_name}/#{board['id']}"
43
+
44
+ cli.print_provider_command('devops', arg_str, board['name'])
45
+ # cli.warn board['url'] if board.key?('url') && cli.output.isatty # TODO: Web URL
46
+ end
47
+
48
+ def print_work_item(organization, project, board, work_item)
49
+ arg_str = "#{organization}/#{project}/#{board['id']}/#{work_item['id']}"
50
+
51
+ cli.print_provider_command('devops', arg_str, work_item['name'])
52
+ cli.warn work_item['url'] if work_item.key?('url') && cli.output.isatty
53
+ end
54
+
55
+ def use_current_args
56
+ @organization_name = config.organization_name
57
+ @project_name = config.project_name
58
+ @board_id = config.board_id
59
+ @work_item_id = config.work_item_id
60
+ end
61
+
62
+ def use_arg_str(arg_str)
63
+ args = arg_str.to_s.split('/')
64
+
65
+ if args.length < 3
66
+ cli.abort 'Argument format is <organization>/<project>/<board-id>[/<work-item-id>]'
67
+ end
68
+
69
+ (@organization_name, @project_name, @board_id, @work_item_id) = args
70
+ end
71
+
72
+ def api
73
+ Abt::Providers::Devops::Api.new(organization_name: organization_name,
74
+ project_name: project_name,
75
+ username: config.username_for_organization(organization_name),
76
+ access_token: config.access_token_for_organization(organization_name))
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Clear < BaseCommand
8
+ def self.command
9
+ 'clear devops'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear DevOps config for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing configuration'
18
+ config.clear_local
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class ClearGlobal < BaseCommand
8
+ def self.command
9
+ 'clear-global devops'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear all global configuration'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing global DevOps configuration'
18
+ config.clear_global
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Current < BaseCommand
8
+ def self.command
9
+ 'current devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Get or set DevOps configuration for current git repository'
14
+ end
15
+
16
+ def call
17
+ if same_args_as_config? || !config.local_available?
18
+ show_current_configuration
19
+ else
20
+ cli.warn 'Updating configuration'
21
+ update_configuration
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def show_current_configuration
28
+ if organization_name.nil?
29
+ cli.warn 'No organization selected'
30
+ elsif project_name.nil?
31
+ cli.warn 'No project selected'
32
+ elsif board_id.nil?
33
+ cli.warn 'No board selected'
34
+ elsif work_item_id.nil?
35
+ print_board(organization_name, project_name, board)
36
+ else
37
+ print_work_item(organization_name, project_name, board, work_item)
38
+ end
39
+ end
40
+
41
+ def update_configuration
42
+ ensure_board_is_valid!
43
+
44
+ if work_item_id.nil?
45
+ update_board_config
46
+ config.work_item_id = nil
47
+
48
+ print_board(organization_name, project_name, board)
49
+ else
50
+ ensure_work_item_is_valid!
51
+
52
+ update_board_config
53
+ config.work_item_id = work_item_id
54
+
55
+ print_work_item(organization_name, project_name, board, work_item)
56
+ end
57
+ end
58
+
59
+ def update_board_config
60
+ config.organization_name = organization_name
61
+ config.project_name = project_name
62
+ config.board_id = board_id
63
+ end
64
+
65
+ def ensure_board_is_valid!
66
+ if board.nil?
67
+ cli.abort 'Board could not be found, ensure that settings for organization, project, and board are correct'
68
+ end
69
+ end
70
+
71
+ def ensure_work_item_is_valid!
72
+ cli.abort "No such work item: ##{work_item_id}" if work_item.nil?
73
+ end
74
+
75
+ def board
76
+ @board ||= begin
77
+ cli.warn 'Fetching board...'
78
+ api.get("work/boards/#{board_id}")
79
+ rescue HttpError::NotFoundError
80
+ nil
81
+ end
82
+ end
83
+
84
+ def work_item
85
+ @work_item ||= begin
86
+ cli.warn 'Fetching work item...'
87
+ work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
88
+ sanitize_work_item(work_item)
89
+ rescue HttpError::NotFoundError
90
+ nil
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class HarvestTimeEntryData < BaseCommand
8
+ def self.command
9
+ 'harvest-time-entry-data devops[:<organization-name>/<project-name>/<board-id>/<work-item-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Print Harvest time entry data for DevOps work item as json. Used by harvest start script.'
14
+ end
15
+
16
+ def call
17
+ body = {
18
+ notes: notes,
19
+ external_reference: {
20
+ id: work_item['id'],
21
+ group_id: 'AzureDevOpsWorkItem',
22
+ permalink: work_item['url']
23
+ }
24
+ }
25
+
26
+ cli.puts Oj.dump(body, mode: :json)
27
+ end
28
+
29
+ private
30
+
31
+ def notes
32
+ [
33
+ 'Azure DevOps',
34
+ work_item['fields']['System.WorkItemType'],
35
+ "##{work_item['id']}",
36
+ '-',
37
+ work_item['name']
38
+ ].join(' ')
39
+ end
40
+
41
+ def work_item
42
+ @work_item ||= begin
43
+ work_item = api.get_paged('wit/workitems', ids: work_item_id)[0]
44
+ sanitize_work_item(work_item)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Init < BaseCommand
8
+ AZURE_DEV_URL_REGEX = %r{^https://dev\.azure\.com/(?<organization>[^/]+)/(?<project>[^/]+)}.freeze
9
+ VS_URL_REGEX = %r{^https://(?<organization>[^.]+)\.visualstudio\.com/(?<project>[^/]+)}.freeze
10
+
11
+ def self.command
12
+ 'init devops'
13
+ end
14
+
15
+ def self.description
16
+ 'Pick DevOps board for current git repository'
17
+ end
18
+
19
+ def call
20
+ cli.abort 'Must be run inside a git repository' unless config.local_available?
21
+
22
+ @organization_name = config.organization_name = organization_name_from_url
23
+ @project_name = config.project_name = project_name_from_url
24
+
25
+ board = cli.prompt_choice 'Select a project work board', boards
26
+
27
+ config.board_id = board['id']
28
+
29
+ print_board(organization_name, project_name, board)
30
+ end
31
+
32
+ private
33
+
34
+ def boards
35
+ @boards ||= api.get_paged('work/boards')
36
+ end
37
+
38
+ def project_name_from_url
39
+ if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
40
+ (match = VS_URL_REGEX.match(project_url))
41
+ match[:project]
42
+ end
43
+ end
44
+
45
+ def organization_name_from_url
46
+ if (match = AZURE_DEV_URL_REGEX.match(project_url)) ||
47
+ (match = VS_URL_REGEX.match(project_url))
48
+ match[:organization]
49
+ end
50
+ end
51
+
52
+ def project_url
53
+ @project_url ||= begin
54
+ loop do
55
+ url = cli.prompt([
56
+ 'Please provide the URL for the devops project',
57
+ 'For instance https://{organization}.visualstudio.com/{project} or https://dev.azure.com/{organization}/{project}',
58
+ '',
59
+ 'Enter URL'
60
+ ].join("\n"))
61
+
62
+ break url if AZURE_DEV_URL_REGEX =~ url || VS_URL_REGEX =~ url
63
+
64
+ cli.warn 'Invalid URL'
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Pick < BaseCommand
8
+ def self.command
9
+ 'pick devops[:<organization-name>/<project-name>/<board-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Pick work item for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.abort 'Must be run inside a git repository' unless config.local_available?
18
+
19
+ cli.warn "#{project_name} - #{board['name']}"
20
+
21
+ work_item = select_work_item
22
+
23
+ # We might have gotten org, project, board as arg str
24
+ config.organization_name = organization_name
25
+ config.project_name = project_name
26
+ config.board_id = board_id
27
+ config.work_item_id = work_item['id']
28
+
29
+ print_work_item(organization_name, project_name, board, work_item)
30
+ end
31
+
32
+ private
33
+
34
+ def select_work_item
35
+ loop do
36
+ column = cli.prompt_choice 'Which column?', columns
37
+ cli.warn 'Fetching work items...'
38
+ work_items = work_items_in_column(column)
39
+
40
+ if work_items.length.zero?
41
+ cli.warn 'Section is empty'
42
+ next
43
+ end
44
+
45
+ work_item = cli.prompt_choice 'Select a work item', work_items, true
46
+ return work_item if work_item
47
+ end
48
+ end
49
+
50
+ def work_items_in_column(column)
51
+ wiql = <<~WIQL
52
+ SELECT [System.Id]
53
+ FROM WorkItems
54
+ WHERE [System.BoardColumn] = '#{column['name']}'
55
+ ORDER BY [Microsoft.VSTS.Common.BacklogPriority] ASC
56
+ WIQL
57
+
58
+ response = api.post('wit/wiql', Oj.dump({ query: wiql }, mode: :json))
59
+ ids = response['workItems'].map { |work_item| work_item['id'] }
60
+ work_items = api.get_paged('wit/workitems', ids: ids.join(','))
61
+
62
+ work_items.map { |work_item| sanitize_work_item(work_item) }
63
+ end
64
+
65
+ def columns
66
+ board['columns']
67
+ end
68
+
69
+ def board
70
+ @board ||= api.get("work/boards/#{board_id}")
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ module Commands
7
+ class Share < BaseCommand
8
+ def self.command
9
+ 'share devops[:<organization-name>/<project-name>/<board-id>[/<work-item-id>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Print DevOps config string'
14
+ end
15
+
16
+ def call
17
+ if organization_name.nil?
18
+ cli.warn 'No organization selected'
19
+ elsif project_name.nil?
20
+ cli.warn 'No project selected'
21
+ elsif board_id.nil?
22
+ cli.warn 'No board selected'
23
+ else
24
+ args = [organization_name, project_name, board_id, work_item_id].compact
25
+ cli.print_provider_command('devops', args.join('/'))
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Devops
6
+ class Configuration
7
+ attr_accessor :cli
8
+
9
+ def initialize(cli:)
10
+ @cli = cli
11
+ @git = GitConfig.new(namespace: 'abt.devops')
12
+ end
13
+
14
+ def local_available?
15
+ GitConfig.local_available?
16
+ end
17
+
18
+ def organization_name
19
+ local_available? ? git['organizationName'] : nil
20
+ end
21
+
22
+ def project_name
23
+ local_available? ? git['projectName'] : nil
24
+ end
25
+
26
+ def board_id
27
+ local_available? ? git['boardId'] : nil
28
+ end
29
+
30
+ def work_item_id
31
+ local_available? ? git['workItemId'] : nil
32
+ end
33
+
34
+ def organization_name=(value)
35
+ return if organization_name == value
36
+
37
+ clear_local
38
+ git['organizationName'] = value unless value.nil?
39
+ end
40
+
41
+ def project_name=(value)
42
+ return if project_name == value
43
+
44
+ git['projectName'] = value unless value.nil?
45
+ git['boardId'] = nil
46
+ git['workItemId'] = nil
47
+ end
48
+
49
+ def board_id=(value)
50
+ return if board_id == value
51
+
52
+ git['boardId'] = value unless value.nil?
53
+ git['workItemId'] = nil
54
+ end
55
+
56
+ def work_item_id=(value)
57
+ git['workItemId'] = value
58
+ end
59
+
60
+ def clear_local
61
+ cli.abort 'No local configuration was found' unless local_available?
62
+
63
+ git['organizationName'] = nil
64
+ git['projectName'] = nil
65
+ git['boardId'] = nil
66
+ git['workItemId'] = nil
67
+ end
68
+
69
+ def clear_global
70
+ git.global.keys.each do |key|
71
+ cli.puts 'Deleting configuration: ' + key
72
+ git.global[key] = nil
73
+ end
74
+ end
75
+
76
+ def username_for_organization(organization_name)
77
+ username_key = "organizations.#{organization_name}.username"
78
+
79
+ return git.global[username_key] unless git.global[username_key].nil?
80
+
81
+ git.global[username_key] = cli.prompt([
82
+ "Please provide your username for the DevOps organization (#{organization_name}).",
83
+ '',
84
+ 'Enter username'
85
+ ].join("\n"))
86
+ end
87
+
88
+ def access_token_for_organization(organization_name)
89
+ access_token_key = "organizations.#{organization_name}.accessToken"
90
+
91
+ return git.global[access_token_key] unless git.global[access_token_key].nil?
92
+
93
+ git.global[access_token_key] = cli.prompt([
94
+ "Please provide your personal access token for the DevOps organization (#{organization_name}).",
95
+ 'If you don\'t have one, follow the guide here: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate',
96
+ '',
97
+ 'The token MUST have "Read" permission for Work Items',
98
+ 'Future features will likely require "Write" or "Manage"',
99
+ '',
100
+ 'Enter access token'
101
+ ].join("\n"))
102
+ end
103
+
104
+ private
105
+
106
+ attr_reader :git
107
+ end
108
+ end
109
+ end
110
+ end
@@ -44,9 +44,10 @@ module Abt
44
44
  end
45
45
 
46
46
  def clear_global
47
- git.global['userId'] = nil
48
- git.global['accountId'] = nil
49
- git.global['accessToken'] = nil
47
+ git.global.keys.each do |key|
48
+ cli.puts 'Deleting configuration: ' + key
49
+ git.global[key] = nil
50
+ end
50
51
  end
51
52
 
52
53
  def access_token
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Abt
4
- VERSION = '0.0.10'
4
+ VERSION = '0.0.11'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abt-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesper Sørensen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-27 00:00:00.000000000 Z
11
+ date: 2021-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-inflector
@@ -100,6 +100,17 @@ files:
100
100
  - "./lib/abt/providers/asana/commands/start.rb"
101
101
  - "./lib/abt/providers/asana/commands/tasks.rb"
102
102
  - "./lib/abt/providers/asana/configuration.rb"
103
+ - "./lib/abt/providers/devops.rb"
104
+ - "./lib/abt/providers/devops/api.rb"
105
+ - "./lib/abt/providers/devops/base_command.rb"
106
+ - "./lib/abt/providers/devops/commands/clear.rb"
107
+ - "./lib/abt/providers/devops/commands/clear_global.rb"
108
+ - "./lib/abt/providers/devops/commands/current.rb"
109
+ - "./lib/abt/providers/devops/commands/harvest_time_entry_data.rb"
110
+ - "./lib/abt/providers/devops/commands/init.rb"
111
+ - "./lib/abt/providers/devops/commands/pick.rb"
112
+ - "./lib/abt/providers/devops/commands/share.rb"
113
+ - "./lib/abt/providers/devops/configuration.rb"
103
114
  - "./lib/abt/providers/harvest.rb"
104
115
  - "./lib/abt/providers/harvest/api.rb"
105
116
  - "./lib/abt/providers/harvest/base_command.rb"