abt-cli 0.0.15 → 0.0.20

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 (60) 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 +68 -49
  8. data/lib/abt/cli/arguments_parser.rb +48 -0
  9. data/lib/abt/cli/prompt.rb +7 -6
  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 +12 -10
  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 +8 -12
  20. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +12 -7
  21. data/lib/abt/providers/asana/commands/init.rb +9 -9
  22. data/lib/abt/providers/asana/commands/pick.rb +28 -15
  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 +26 -18
  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 +18 -18
  37. data/lib/abt/providers/devops/commands/pick.rb +16 -16
  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 +8 -9
  47. data/lib/abt/providers/harvest/commands/pick.rb +15 -8
  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 +11 -7
  58. data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
  59. data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
  60. data/lib/abt/providers/harvest/commands/clear_global.rb +0 -24
data/lib/abt/docs/cli.rb CHANGED
@@ -4,23 +4,54 @@ module Abt
4
4
  module Docs
5
5
  module Cli
6
6
  class << self
7
- def content
7
+ def help
8
8
  <<~TXT
9
- Usage: abt <command> [<provider[:<arguments>]>...]
9
+ Usage: #{usage_line}
10
10
 
11
- #{example_commands}
11
+ <command> Name of command to execute, e.g. start, finalize etc.
12
+ <ARI> A URI-like resource identifier with a scheme and an optional path
13
+ in the format: <scheme>[:<path>]. E.g., harvest:11111111/22222222
14
+ <options> Optional flags for the command and ARI
12
15
 
13
- Available commands:
14
- #{providers_commands}
16
+ #{formatted_examples(Docs.basic_examples)}
17
+
18
+ For detailed examples/commands try:
19
+ abt examples
20
+ abt commands
21
+ TXT
22
+ end
23
+
24
+ def examples
25
+ <<~TXT
26
+ Printing examples
27
+
28
+ #{formatted_examples(Docs.basic_examples)}
29
+
30
+ #{formatted_examples(Docs.extended_examples)}
31
+ TXT
32
+ end
33
+
34
+ def commands
35
+ <<~TXT
36
+ Printing commands
37
+
38
+ Run commands with --help flag to see detailed usage and flags, e.g.:
39
+ abt track harvest -h
40
+
41
+ #{commands_per_provider}
15
42
  TXT
16
43
  end
17
44
 
18
45
  private
19
46
 
20
- def example_commands
47
+ def usage_line
48
+ 'abt <command> [<ARI>] [<options> --] [<ARI>] ...'
49
+ end
50
+
51
+ def formatted_examples(example_groups)
21
52
  lines = []
22
53
 
23
- Docs.examples.each_with_index do |(title, examples), index|
54
+ example_groups.each_with_index do |(title, examples), index|
24
55
  lines << '' unless index.zero?
25
56
  lines << title
26
57
 
@@ -33,16 +64,16 @@ module Abt
33
64
  lines.join("\n")
34
65
  end
35
66
 
36
- def providers_commands
67
+ def commands_per_provider
37
68
  lines = []
38
69
 
39
- Docs.providers.each_with_index do |(provider_name, commands_definition), index|
70
+ Docs.providers.each_with_index do |(scheme, commands_definition), index|
40
71
  lines << '' unless index.zero?
41
- lines << "#{inflector.humanize(provider_name)}:"
72
+ lines << "#{inflector.humanize(scheme)}:"
42
73
 
43
74
  max_length = commands_definition.keys.map(&:length).max
44
75
 
45
- commands_definition.each do |(command, description)|
76
+ commands_definition.each do |(command, (_usage, description))|
46
77
  lines << " #{command.ljust(max_length)} #{description}"
47
78
  end
48
79
  end
@@ -4,18 +4,44 @@ module Abt
4
4
  module Docs
5
5
  module Markdown
6
6
  class << self
7
- def content
7
+ def readme
8
8
  <<~MD
9
9
  # Abt
10
- This readme was generated with `abt help-md > README.md`
10
+
11
+ Abt makes re-occuring tasks easily accessible from the terminal:
12
+ - Moving asana tasks around
13
+ - Tracking work/meetings in harvest
14
+ - Consistently naming branches
15
+
16
+ ## How does abt work?
17
+
18
+ Abt is a hybrid of having small scripts each doing one thing:
19
+ - `start-asana --project-gid xxxx --task-gid yyyy`
20
+ - `start-harvest --project-id aaaa --task-id bbbb`
21
+
22
+ And having a single highly advanced script that does everything with a single command:
23
+ - `start xxxx/yyyy aaaa/bbbb`
24
+
25
+ Abt looks like one command, but works like a bunch of light scripts:
26
+ - `abt start asana:xxxx/yyyy harvest:aaaa/bbbb`
11
27
 
12
28
  ## Usage
13
- `abt <command> [<provider[:<arguments>]>...]`
29
+ `abt <command> [<ARI>] [<options> --] [<ARI>] ...`
30
+
31
+ Definitions:
32
+ - `<command>`: Name of command to execute, e.g. `start`, `finalize` etc.
33
+ - `<ARI>`: A URI-like resource identifier with a scheme and an optional path in the format: `<scheme>[:<path>]`. E.g., `harvest:11111111/22222222`
34
+ - `<options>`: Optional flags for the command and ARI
14
35
 
15
36
  #{example_commands}
16
37
 
17
- ## Available commands:
38
+ ## Commands:
39
+
40
+ Some commands have `[options]`. Run such a command with `--help` flag to view supported flags, e.g: `abt track harvest -h`
41
+
18
42
  #{provider_commands}
43
+
44
+ #### This readme was generated with `abt readme > README.md`
19
45
  MD
20
46
  end
21
47
 
@@ -24,7 +50,8 @@ module Abt
24
50
  def example_commands
25
51
  lines = []
26
52
 
27
- Docs.examples.each_with_index do |(title, commands), index|
53
+ examples = Docs.basic_examples.merge(Docs.extended_examples)
54
+ examples.each_with_index do |(title, commands), index|
28
55
  lines << '' unless index.zero?
29
56
  lines << title
30
57
 
@@ -40,17 +67,17 @@ module Abt
40
67
  def provider_commands # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
41
68
  lines = []
42
69
 
43
- Docs.providers.each_with_index do |(provider_name, commands), index|
70
+ Docs.providers.each_with_index do |(scheme, commands), index|
44
71
  lines << '' unless index.zero?
45
- lines << "### #{inflector.humanize(provider_name)}"
72
+ lines << "### #{inflector.humanize(scheme)}"
46
73
  lines << '| Command | Description |'
47
74
  lines << '| :------ | :---------- |'
48
75
 
49
- max_length = commands.keys.map(&:length).max
76
+ max_length = commands.values.map(&:first).map(&:length).max
50
77
 
51
- commands.each do |(command, description)|
52
- adjusted_command = "`#{command}`".ljust(max_length + 2)
53
- lines << "| #{adjusted_command} | #{description} |"
78
+ commands.each do |(_command, (usage, description))|
79
+ adjusted_usage = "`#{usage}`".ljust(max_length + 2)
80
+ lines << "| #{adjusted_usage} | #{description} |"
54
81
  end
55
82
  end
56
83
 
@@ -4,21 +4,9 @@ module Abt
4
4
  class GitConfig
5
5
  attr_reader :namespace, :scope
6
6
 
7
- LOCAL_CONFIG_AVAILABLE_CHECK_COMMAND = 'git config --local -l'
7
+ class UnsafeNamespaceError < StandardError; end
8
8
 
9
- def self.local_available?
10
- return @local_available if instance_variables.include?(:@local_available)
11
-
12
- @local_available = begin
13
- success = false
14
- Open3.popen3(LOCAL_CONFIG_AVAILABLE_CHECK_COMMAND) do |_i, _o, _e, thread|
15
- success = thread.value.success?
16
- end
17
- success
18
- end
19
- end
20
-
21
- def initialize(namespace: '', scope: 'local')
9
+ def initialize(scope = 'local', namespace = '')
22
10
  @namespace = namespace
23
11
 
24
12
  unless %w[local global].include? scope
@@ -28,6 +16,20 @@ module Abt
28
16
  @scope = scope
29
17
  end
30
18
 
19
+ def available?
20
+ unless instance_variables.include?(:available)
21
+ @available = begin
22
+ success = false
23
+ Open3.popen3(availability_check_call) do |_i, _o, _e, thread|
24
+ success = thread.value.success?
25
+ end
26
+ success
27
+ end
28
+ end
29
+
30
+ @available
31
+ end
32
+
31
33
  def [](key)
32
34
  get(key)
33
35
  end
@@ -47,30 +49,23 @@ module Abt
47
49
  `git config --#{scope} --get-regexp --name-only ^#{namespace}`.lines.map(&:strip)
48
50
  end
49
51
 
50
- def local
51
- @local ||= begin
52
- if scope == 'local'
53
- self
54
- else
55
- self.class.new(namespace: namespace, scope: 'local')
56
- end
57
- end
58
- end
52
+ def clear(output: nil)
53
+ raise UnsafeNamespaceError, 'Keys can only be cleared within a namespace' if namespace.empty?
59
54
 
60
- def global
61
- @global ||= begin
62
- if scope == 'global'
63
- self
64
- else
65
- self.class.new(namespace: namespace, scope: 'global')
66
- end
55
+ keys.each do |key|
56
+ output&.puts "Clearing #{scope}: #{key_with_namespace(key)}"
57
+ self[key] = nil
67
58
  end
68
59
  end
69
60
 
70
61
  private
71
62
 
63
+ def availability_check_call
64
+ "git config --#{scope} -l"
65
+ end
66
+
72
67
  def ensure_scope_available!
73
- return if scope != 'local' || self.class.local_available?
68
+ return if available?
74
69
 
75
70
  raise StandardError, 'Local configuration is not available outside a git repository'
76
71
  end
@@ -3,63 +3,43 @@
3
3
  module Abt
4
4
  module Providers
5
5
  module Asana
6
- class BaseCommand
7
- attr_reader :arg_str, :project_gid, :task_gid, :cli, :config
6
+ class BaseCommand < Abt::BaseCommand
7
+ extend Forwardable
8
+
9
+ attr_reader :path, :config
10
+
11
+ def_delegators(:@path, :project_gid, :task_gid)
12
+
13
+ def initialize(ari:, cli:)
14
+ super
8
15
 
9
- def initialize(arg_str:, cli:)
10
- @arg_str = arg_str
11
16
  @config = Configuration.new(cli: cli)
12
17
 
13
- if arg_str.nil?
14
- use_current_args
15
- else
16
- use_arg_str(arg_str)
17
- end
18
- @cli = cli
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
28
  if project_gid.nil?
29
- cli.abort 'No current/specified project. Did you initialize Asana and pick a task?'
29
+ abort 'No current/specified project. Did you initialize Asana and pick a task?'
30
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
31
+ abort 'No current/specified task. Did you pick an Asana task?' if task_gid.nil?
36
32
  end
37
33
 
38
34
  def print_project(project)
39
- cli.print_provider_command('asana', project['gid'], project['name'])
40
- cli.warn project['permalink_url'] if project.key?('permalink_url') && cli.output.isatty
35
+ cli.print_ari('asana', project['gid'], project['name'])
36
+ warn project['permalink_url'] if project.key?('permalink_url') && cli.output.isatty
41
37
  end
42
38
 
43
39
  def print_task(project, task)
44
40
  project = { 'gid' => project } if project.is_a?(String)
45
- cli.print_provider_command('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_args
50
- @project_gid = config.project_gid
51
- @task_gid = config.task_gid
52
- end
53
-
54
- def use_arg_str(arg_str)
55
- args = arg_str.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?
41
+ cli.print_ari('asana', "#{project['gid']}/#{task['gid']}", task['name'])
42
+ warn task['permalink_url'] if task.key?('permalink_url') && cli.output.isatty
63
43
  end
64
44
 
65
45
  def api
@@ -5,21 +5,26 @@ module Abt
5
5
  module Asana
6
6
  module Commands
7
7
  class Add < BaseCommand
8
- def self.command
9
- 'add asana[:<project-gid>]'
8
+ def self.usage
9
+ 'abt add asana[:<project-gid>]'
10
10
  end
11
11
 
12
12
  def self.description
13
13
  'Create a new task for the current/specified Asana project'
14
14
  end
15
15
 
16
- def call
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,7 +38,6 @@ module Abt
33
38
  projects: [project_gid]
34
39
  }
35
40
  }
36
- cli.warn 'Creating task'
37
41
  api.post('tasks', Oj.dump(body, mode: :json))
38
42
  end
39
43
  end
@@ -53,7 +57,7 @@ module Abt
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
@@ -62,10 +66,8 @@ module Abt
62
66
 
63
67
  def sections
64
68
  @sections ||= begin
65
- cli.warn 'Fetching sections...'
69
+ warn 'Fetching sections...'
66
70
  api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
67
- rescue Abt::HttpError::HttpError
68
- []
69
71
  end
70
72
  end
71
73
  end
@@ -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