abt-cli 0.0.2 → 0.0.7

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +4 -1
  3. data/lib/abt.rb +11 -0
  4. data/lib/abt/cli.rb +37 -32
  5. data/lib/abt/cli/dialogs.rb +18 -2
  6. data/lib/abt/cli/io.rb +23 -0
  7. data/lib/abt/docs.rb +57 -0
  8. data/lib/abt/{help → docs}/cli.rb +4 -4
  9. data/lib/abt/{help → docs}/markdown.rb +4 -4
  10. data/lib/abt/git_config.rb +55 -49
  11. data/lib/abt/helpers.rb +16 -0
  12. data/lib/abt/providers/asana.rb +9 -50
  13. data/lib/abt/providers/asana/api.rb +57 -0
  14. data/lib/abt/providers/asana/base_command.rb +14 -16
  15. data/lib/abt/providers/asana/commands/clear.rb +24 -0
  16. data/lib/abt/providers/asana/commands/clear_global.rb +24 -0
  17. data/lib/abt/providers/asana/commands/current.rb +77 -0
  18. data/lib/abt/providers/asana/commands/finalize.rb +71 -0
  19. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +50 -0
  20. data/lib/abt/providers/asana/commands/init.rb +70 -0
  21. data/lib/abt/providers/asana/commands/pick.rb +55 -0
  22. data/lib/abt/providers/asana/commands/projects.rb +39 -0
  23. data/lib/abt/providers/asana/commands/share.rb +29 -0
  24. data/lib/abt/providers/asana/commands/start.rb +105 -0
  25. data/lib/abt/providers/asana/commands/tasks.rb +40 -0
  26. data/lib/abt/providers/asana/configuration.rb +125 -0
  27. data/lib/abt/providers/harvest.rb +9 -42
  28. data/lib/abt/providers/harvest/api.rb +62 -0
  29. data/lib/abt/providers/harvest/base_command.rb +12 -16
  30. data/lib/abt/providers/harvest/commands/clear.rb +24 -0
  31. data/lib/abt/providers/harvest/commands/clear_global.rb +24 -0
  32. data/lib/abt/providers/harvest/commands/current.rb +83 -0
  33. data/lib/abt/providers/harvest/commands/init.rb +83 -0
  34. data/lib/abt/providers/harvest/commands/pick.rb +51 -0
  35. data/lib/abt/providers/harvest/commands/projects.rb +40 -0
  36. data/lib/abt/providers/harvest/commands/share.rb +29 -0
  37. data/lib/abt/providers/harvest/commands/start.rb +101 -0
  38. data/lib/abt/providers/harvest/commands/stop.rb +58 -0
  39. data/lib/abt/providers/harvest/commands/tasks.rb +45 -0
  40. data/lib/abt/providers/harvest/configuration.rb +91 -0
  41. data/lib/abt/version.rb +1 -1
  42. metadata +32 -26
  43. data/lib/abt/asana_client.rb +0 -53
  44. data/lib/abt/harvest_client.rb +0 -58
  45. data/lib/abt/help.rb +0 -56
  46. data/lib/abt/providers/asana/clear.rb +0 -24
  47. data/lib/abt/providers/asana/clear_global.rb +0 -24
  48. data/lib/abt/providers/asana/current.rb +0 -69
  49. data/lib/abt/providers/asana/harvest_link_entry_data.rb +0 -48
  50. data/lib/abt/providers/asana/init.rb +0 -62
  51. data/lib/abt/providers/asana/move.rb +0 -54
  52. data/lib/abt/providers/asana/pick_task.rb +0 -46
  53. data/lib/abt/providers/asana/projects.rb +0 -30
  54. data/lib/abt/providers/asana/start.rb +0 -22
  55. data/lib/abt/providers/asana/tasks.rb +0 -35
  56. data/lib/abt/providers/harvest/clear.rb +0 -24
  57. data/lib/abt/providers/harvest/clear_global.rb +0 -24
  58. data/lib/abt/providers/harvest/current.rb +0 -79
  59. data/lib/abt/providers/harvest/init.rb +0 -61
  60. data/lib/abt/providers/harvest/pick_task.rb +0 -45
  61. data/lib/abt/providers/harvest/projects.rb +0 -29
  62. data/lib/abt/providers/harvest/start.rb +0 -58
  63. data/lib/abt/providers/harvest/stop.rb +0 -51
  64. data/lib/abt/providers/harvest/tasks.rb +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 929ab52c8ebd307b90ddb7daa14f2f63eeeebda8ed9905490a13dcd2f67770da
4
- data.tar.gz: e2582299b29e39faa8d4baccaba42954e7140a447d30c3c56a92507ac00f4a09
3
+ metadata.gz: cd99c9123cfbd4222176495fd039015ad56ddaf0841350347169fa6809f8d6b2
4
+ data.tar.gz: 33d0adf3e73be8f5b2a6f52d61cea46285e0e2ab036b2d1b0e1b4fe1f2c0a27a
5
5
  SHA512:
6
- metadata.gz: dec30158debfe3e97c4969d8724d5f56d04725f0cab1aedf79840bb01ef81d9f17c5245cc538c559e47bfa245f36c5aeed8d02dc6ff21fcbae46b30f79e88c5b
7
- data.tar.gz: c74d6a7ae33ff5843385796cf21d764de61d0981501b5ac7e24691a7c61c85ae6f646abacd1f1b74417d29559ae022881969dd71598acf1190487c6b64c0af02
6
+ metadata.gz: 2ea6b3ea3f1f2056b9e7911144542f0d2f3b568544fcd68901299fb64c84414290d7cecfc884405bde0125b096bc7033b99d3c2eb87e0598f434de79cfa76ae3
7
+ data.tar.gz: 3c8ffb1c3ba6553d16611c0f55a1c1bae3cf97d156fc79242f66b2d0b2ac622b08ab4ef049b98299553c708dc36365fc4e6158e31b556d830d863019134f34eb
data/bin/abt CHANGED
@@ -4,11 +4,14 @@
4
4
  require 'dry-inflector'
5
5
  require 'faraday'
6
6
  require 'oj'
7
+ require 'open3'
7
8
 
8
9
  require_relative '../lib/abt.rb'
9
10
 
10
11
  begin
11
- Abt::Cli.new(ARGV).perform
12
+ Abt::Cli.new.perform
13
+ rescue Abt::Cli::AbortError => e
14
+ abort e.message
12
15
  rescue Interrupt
13
16
  abort 'Aborted'
14
17
  end
data/lib/abt.rb CHANGED
@@ -3,3 +3,14 @@
3
3
  Dir.glob("#{File.dirname(File.absolute_path(__FILE__))}/abt/*.rb").sort.each do |file|
4
4
  require file
5
5
  end
6
+
7
+ module Abt
8
+ def self.provider_names
9
+ Providers.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
10
+ end
11
+
12
+ def self.provider_module(name)
13
+ const_name = Helpers.command_to_const(name)
14
+ Providers.const_get(const_name) if Providers.const_defined?(const_name)
15
+ end
16
+ end
@@ -6,26 +6,35 @@ end
6
6
 
7
7
  module Abt
8
8
  class Cli
9
+ class AbortError < StandardError; end
10
+
9
11
  include Dialogs
12
+ include Io
10
13
 
11
- attr_reader :command, :args
14
+ attr_reader :command, :args, :input, :output, :err_output
12
15
 
13
- def initialize(argv)
16
+ def initialize(argv: ARGV, input: STDIN, output: STDOUT, err_output: STDERR)
14
17
  (@command, *@args) = argv
15
18
 
16
- @args += args_from_stdin unless STDIN.isatty # Add piped arguments
19
+ @input = input
20
+ @output = output
21
+ @err_output = err_output
22
+
23
+ @args += args_from_stdin unless input.isatty # Add piped arguments
17
24
  end
18
25
 
19
- def perform(command: @command, args: @args)
26
+ def perform
20
27
  handle_global_commands!
21
28
 
22
29
  abort('No provider arguments') if args.empty?
23
30
 
24
- process_providers(command: command, args: args)
31
+ process_providers
25
32
  end
26
33
 
27
- def print_provider_command(provider, arg_str, description)
28
- puts "#{provider}:#{arg_str} # #{description}"
34
+ def print_provider_command(provider, arg_str, description = nil)
35
+ command = "#{provider}:#{arg_str}"
36
+ command += " # #{description}" unless description.nil?
37
+ output.puts command
29
38
  end
30
39
 
31
40
  private
@@ -34,13 +43,13 @@ module Abt
34
43
  case command
35
44
  when nil
36
45
  warn("No command specified\n\n")
37
- puts(Abt::Help::Cli.content)
46
+ puts(Abt::Docs::Cli.content)
38
47
  exit
39
48
  when '--help', '-h', 'help', 'commands'
40
- puts(Abt::Help::Cli.content)
49
+ puts(Abt::Docs::Cli.content)
41
50
  exit
42
51
  when 'help-md'
43
- puts(Abt::Help::Markdown.content)
52
+ puts(Abt::Docs::Markdown.content)
44
53
  exit
45
54
  when '--version', '-v', 'version'
46
55
  puts(Abt::VERSION)
@@ -53,12 +62,17 @@ module Abt
53
62
 
54
63
  return [] if input.nil?
55
64
 
56
- input.split("\n").map do |line|
57
- line.split(' # ').first # Exclude comment part of piped input lines
65
+ # Exclude comment part of piped input lines
66
+ lines_without_comments = input.lines.map do |line|
67
+ line.split(' # ').first
58
68
  end
69
+
70
+ # Allow multiple provider arguments on a single piped input line
71
+ joined_lines = lines_without_comments.join(' ').strip
72
+ joined_lines.split(/\s+/)
59
73
  end
60
74
 
61
- def process_providers(command:, args:)
75
+ def process_providers
62
76
  used_providers = []
63
77
  args.each do |provider_args|
64
78
  (provider, arg_str) = provider_args.split(':')
@@ -71,33 +85,24 @@ module Abt
71
85
  used_providers << provider if process_provider_command(provider, command, arg_str)
72
86
  end
73
87
 
74
- warn 'No matching providers found for command' if used_providers.empty?
88
+ warn 'No matching providers found for command' if used_providers.empty? && output.isatty
75
89
  end
76
90
 
77
- def process_provider_command(provider, command, arg_str)
78
- command_class = class_for_provider_and_command(provider, command)
91
+ def process_provider_command(provider_name, command_name, arg_str)
92
+ provider = Abt.provider_module(provider_name)
93
+ return false if provider.nil?
79
94
 
80
- return false unless command_class
95
+ command = provider.command_class(command_name)
96
+ return false if command.nil?
81
97
 
82
- if STDOUT.isatty
83
- warn "===== #{command} #{provider}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
84
- end
98
+ print_command(command_name, provider_name, arg_str) if output.isatty
85
99
 
86
- command_class.new(arg_str: arg_str, cli: self).call
100
+ command.new(arg_str: arg_str, cli: self).call
87
101
  true
88
102
  end
89
103
 
90
- def class_for_provider_and_command(provider, command)
91
- inflector = Dry::Inflector.new
92
- provider_class_name = inflector.camelize(inflector.underscore(provider))
93
-
94
- return unless Abt::Providers.const_defined? provider_class_name
95
-
96
- provider_class = Abt::Providers.const_get provider_class_name
97
- command_class_name = inflector.camelize(inflector.underscore(command))
98
- return unless provider_class.const_defined? command_class_name
99
-
100
- provider_class.const_get command_class_name
104
+ def print_command(name, provider, arg_str)
105
+ warn "===== #{name} #{provider}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
101
106
  end
102
107
  end
103
108
  end
@@ -4,10 +4,26 @@ module Abt
4
4
  class Cli
5
5
  module Dialogs
6
6
  def prompt(question)
7
- STDERR.print "#{question}: "
7
+ err_output.print "#{question}: "
8
8
  read_user_input.strip
9
9
  end
10
10
 
11
+ def prompt_boolean(text)
12
+ warn text
13
+
14
+ loop do
15
+ err_output.print '(y / n): '
16
+
17
+ case read_user_input.strip
18
+ when 'y', 'Y' then return true
19
+ when 'n', 'N' then return false
20
+ else
21
+ warn 'Invalid choice'
22
+ next
23
+ end
24
+ end
25
+ end
26
+
11
27
  def prompt_choice(text, options, allow_back_option = false)
12
28
  if options.one?
13
29
  warn "Selected: #{options.first['name']}"
@@ -44,7 +60,7 @@ module Abt
44
60
  end
45
61
 
46
62
  def read_option_number(options_length, allow_back_option)
47
- STDERR.print "(1-#{options_length}#{allow_back_option ? ', q: back' : ''}): "
63
+ err_output.print "(1-#{options_length}#{allow_back_option ? ', q: back' : ''}): "
48
64
 
49
65
  input = read_user_input
50
66
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class Cli
5
+ module Io
6
+ def warn(*args)
7
+ err_output.puts(*args)
8
+ end
9
+
10
+ def puts(*args)
11
+ output.puts(*args)
12
+ end
13
+
14
+ def print(*args)
15
+ output.print(*args)
16
+ end
17
+
18
+ def abort(message)
19
+ raise AbortError, message
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{File.expand_path(__dir__)}/docs/*.rb").sort.each do |file|
4
+ require file
5
+ end
6
+
7
+ module Abt
8
+ module Docs
9
+ class << self
10
+ def examples # rubocop:disable Metrics/MethodLength
11
+ {
12
+ 'Getting started:' => {
13
+ 'abt init asana harvest' => 'Setup asana and harvest project git repo in working dir',
14
+ 'abt pick harvest' => 'Pick harvest tasks, for most projects this will stay the same',
15
+ 'abt pick asana | abt start harvest' => 'Pick asana task and start working',
16
+ 'abt stop harvest' => 'Stop time tracker',
17
+ 'abt start asana harvest' => 'Continue working, e.g. after a break',
18
+ 'abt finalize asana' => 'Finalize the selected asana task'
19
+ },
20
+ 'Command output can be piped, e.g.:' => {
21
+ 'abt tasks asana | grep -i <name of task>' => nil,
22
+ 'abt tasks asana | grep -i <name of task> | abt start' => nil
23
+ },
24
+ 'Sharing configuration:' => {
25
+ 'abt share asana harvest | tr "\n" " "' => 'Print current configuration',
26
+ 'abt share asana harvest | tr "\n" " " | pbcopy' => 'Copy configuration (mac only)',
27
+ 'abt start <shared configuration>' => 'Start a shared configuration'
28
+ }
29
+ }
30
+ end
31
+
32
+ def providers
33
+ provider_definitions
34
+ end
35
+
36
+ private
37
+
38
+ def provider_definitions
39
+ Abt.provider_names.sort.each_with_object({}) do |name, definition|
40
+ provider_module = Abt.provider_module(name)
41
+
42
+ definition[name] = command_definitions(provider_module)
43
+ end
44
+ end
45
+
46
+ def command_definitions(provider_module)
47
+ provider_module.command_names.each_with_object({}) do |name, definition|
48
+ command_class = provider_module.command_class(name)
49
+
50
+ if command_class.respond_to?(:command) && command_class.respond_to?(:description)
51
+ definition[command_class.command] = command_class.description
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Abt
4
- module Help
4
+ module Docs
5
5
  module Cli
6
6
  class << self
7
7
  def content
8
8
  <<~TXT
9
- Usage: abt <command> [<provider:arguments>...]
9
+ Usage: abt <command> [<provider[:<arguments>]>...]
10
10
 
11
11
  #{example_commands}
12
12
 
@@ -20,7 +20,7 @@ module Abt
20
20
  def example_commands
21
21
  lines = []
22
22
 
23
- Help.examples.each_with_index do |(title, examples), index|
23
+ Docs.examples.each_with_index do |(title, examples), index|
24
24
  lines << '' unless index.zero?
25
25
  lines << title
26
26
 
@@ -36,7 +36,7 @@ module Abt
36
36
  def providers_commands
37
37
  lines = []
38
38
 
39
- Help.providers.each_with_index do |(provider_name, commands_definition), index|
39
+ Docs.providers.each_with_index do |(provider_name, commands_definition), index|
40
40
  lines << '' unless index.zero?
41
41
  lines << "#{inflector.humanize(provider_name)}:"
42
42
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Abt
4
- module Help
4
+ module Docs
5
5
  module Markdown
6
6
  class << self
7
7
  def content
@@ -10,7 +10,7 @@ module Abt
10
10
  This readme was generated with `abt help-md > README.md`
11
11
 
12
12
  ## Usage
13
- `abt <command> [<provider:arguments>...]`
13
+ `abt <command> [<provider[:<arguments>]>...]`
14
14
 
15
15
  #{example_commands}
16
16
 
@@ -24,7 +24,7 @@ module Abt
24
24
  def example_commands
25
25
  lines = []
26
26
 
27
- Help.examples.each_with_index do |(title, commands), index|
27
+ Docs.examples.each_with_index do |(title, commands), index|
28
28
  lines << '' unless index.zero?
29
29
  lines << title
30
30
 
@@ -40,7 +40,7 @@ module Abt
40
40
  def provider_commands # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
41
41
  lines = []
42
42
 
43
- Help.providers.each_with_index do |(provider_name, commands), index|
43
+ Docs.providers.each_with_index do |(provider_name, commands), index|
44
44
  lines << '' unless index.zero?
45
45
  lines << "### #{inflector.humanize(provider_name)}"
46
46
  lines << '| Command | Description |'
@@ -2,76 +2,82 @@
2
2
 
3
3
  module Abt
4
4
  class GitConfig
5
- class << self
6
- def local(*args)
7
- git_config(true, *args)
8
- end
9
-
10
- def global(*args)
11
- git_config(false, *args)
12
- end
5
+ attr_reader :namespace, :scope
13
6
 
14
- def prompt_local(*args)
15
- prompt_for_config(true, *args)
7
+ def self.local_available?
8
+ @local_available ||= begin
9
+ status = nil
10
+ Open3.popen3('git config --local -l') do |_i, _o, _e, thread|
11
+ status = thread.value
12
+ end
13
+ status.success?
16
14
  end
15
+ end
17
16
 
18
- def prompt_global(*args)
19
- prompt_for_config(false, *args)
20
- end
17
+ def initialize(namespace: '', scope: 'local')
18
+ @namespace = namespace
21
19
 
22
- def unset_local(key)
23
- unset(true, key)
20
+ unless %w[local global].include? scope
21
+ raise ArgumentError, 'scope must be "local" or "global"'
24
22
  end
25
23
 
26
- def unset_global(key)
27
- unset(false, key)
28
- end
24
+ @scope = scope
25
+ end
29
26
 
30
- private
27
+ def [](key)
28
+ get(key)
29
+ end
31
30
 
32
- def unset(local, key)
33
- `git config --#{local ? 'local' : 'global'} --unset #{key.inspect}`
34
- end
31
+ def []=(key, value)
32
+ set(key, value)
33
+ end
35
34
 
36
- def git_config(local, key, value = nil)
37
- if value
38
- `git config --#{local ? 'local' : 'global'} --replace-all #{key.inspect} #{value.inspect}`
39
- value
35
+ def local
36
+ @local ||= begin
37
+ if scope == 'local'
38
+ self
40
39
  else
41
- git_value = `git config --get #{key.inspect}`.strip
42
- git_value.empty? ? nil : git_value
40
+ self.class.new(namespace: namespace, scope: 'local')
43
41
  end
44
42
  end
43
+ end
45
44
 
46
- def prompt(msg)
47
- STDERR.print "#{msg} > "
48
- value = read_user_input.strip
49
- warn
50
- value
45
+ def global
46
+ @global ||= begin
47
+ if scope == 'global'
48
+ self
49
+ else
50
+ self.class.new(namespace: namespace, scope: 'global')
51
+ end
51
52
  end
53
+ end
52
54
 
53
- def prompt_for_config(local, key, prompt_msg, remedy = '') # rubocop:disable Metrics/MethodLength
54
- value = git_config(local, key)
55
+ private
55
56
 
56
- return value unless value == '' || value.nil?
57
+ def key_with_namespace(key)
58
+ namespace.empty? ? key : "#{namespace}.#{key}"
59
+ end
57
60
 
58
- warn <<~TXT
59
- Missing git config "#{key}":
60
- To find this value:
61
- #{remedy}
62
- TXT
61
+ def get(key)
62
+ if scope == 'local' && !self.class.local_available?
63
+ raise StandardError, 'Local configuration is not available outside a git repository'
64
+ end
63
65
 
64
- new_value = prompt(prompt_msg)
66
+ git_value = `git config --#{scope} --get #{key_with_namespace(key).inspect}`.strip
67
+ git_value.empty? ? nil : git_value
68
+ end
65
69
 
66
- if new_value.empty?
67
- abort 'Empty value, aborting'
68
- else
69
- git_config(local, key, new_value)
70
- end
70
+ def set(key, value)
71
+ if scope == 'local' && !self.class.local_available?
72
+ raise StandardError, 'Local configuration is not available outside a git repository'
71
73
  end
72
74
 
73
- def read_user_input
74
- open('/dev/tty', &:gets)
75
+ if value.nil? || value.empty?
76
+ `git config --#{scope} --unset #{key_with_namespace(key).inspect}`
77
+ nil
78
+ else
79
+ `git config --#{scope} --replace-all #{key_with_namespace(key).inspect} #{value.inspect}`
80
+ value
75
81
  end
76
82
  end
77
83
  end