abt-cli 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -0
  3. data/lib/abt/cli.rb +16 -7
  4. data/lib/abt/cli/dialogs.rb +18 -2
  5. data/lib/abt/cli/io.rb +8 -6
  6. data/lib/abt/docs.rb +12 -5
  7. data/lib/abt/docs/cli.rb +1 -1
  8. data/lib/abt/docs/markdown.rb +1 -1
  9. data/lib/abt/git_config.rb +55 -49
  10. data/lib/abt/providers/asana/api.rb +1 -1
  11. data/lib/abt/providers/asana/base_command.rb +9 -4
  12. data/lib/abt/providers/asana/commands/current.rb +10 -4
  13. data/lib/abt/providers/asana/commands/finalize.rb +71 -0
  14. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +2 -2
  15. data/lib/abt/providers/asana/commands/init.rb +8 -3
  16. data/lib/abt/providers/asana/commands/{pick_task.rb → pick.rb} +13 -6
  17. data/lib/abt/providers/asana/commands/projects.rb +9 -2
  18. data/lib/abt/providers/asana/commands/share.rb +29 -0
  19. data/lib/abt/providers/asana/commands/start.rb +51 -6
  20. data/lib/abt/providers/asana/commands/tasks.rb +4 -1
  21. data/lib/abt/providers/asana/configuration.rb +54 -34
  22. data/lib/abt/providers/harvest.rb +9 -51
  23. data/lib/abt/providers/harvest/api.rb +62 -0
  24. data/lib/abt/providers/harvest/base_command.rb +12 -16
  25. data/lib/abt/providers/harvest/commands/clear.rb +26 -0
  26. data/lib/abt/providers/harvest/commands/clear_global.rb +24 -0
  27. data/lib/abt/providers/harvest/commands/current.rb +81 -0
  28. data/lib/abt/providers/harvest/commands/init.rb +66 -0
  29. data/lib/abt/providers/harvest/commands/pick.rb +49 -0
  30. data/lib/abt/providers/harvest/commands/projects.rb +34 -0
  31. data/lib/abt/providers/harvest/commands/share.rb +29 -0
  32. data/lib/abt/providers/harvest/commands/start.rb +81 -0
  33. data/lib/abt/providers/harvest/commands/stop.rb +58 -0
  34. data/lib/abt/providers/harvest/commands/tasks.rb +38 -0
  35. data/lib/abt/providers/harvest/configuration.rb +90 -0
  36. data/lib/abt/version.rb +1 -1
  37. metadata +17 -14
  38. data/lib/abt/harvest_client.rb +0 -58
  39. data/lib/abt/providers/asana/commands/move.rb +0 -56
  40. data/lib/abt/providers/harvest/clear.rb +0 -24
  41. data/lib/abt/providers/harvest/clear_global.rb +0 -24
  42. data/lib/abt/providers/harvest/current.rb +0 -79
  43. data/lib/abt/providers/harvest/init.rb +0 -61
  44. data/lib/abt/providers/harvest/pick_task.rb +0 -45
  45. data/lib/abt/providers/harvest/projects.rb +0 -29
  46. data/lib/abt/providers/harvest/start.rb +0 -58
  47. data/lib/abt/providers/harvest/stop.rb +0 -51
  48. data/lib/abt/providers/harvest/tasks.rb +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a08875790c31e73415b7e1317db284d1fe5b62ff9d2c3e66449c457c757bfe56
4
- data.tar.gz: cb9857e53d577777371b669f5b253fe48ded2530976fc7c76982d6fef3e660e3
3
+ metadata.gz: 1c8beb7e6080443354e8ddb0d8f00106694731d931283494e1337daee0547c9a
4
+ data.tar.gz: df7840006160bbdb850bdd475496202848080ec118753be8df00f99bc5aa7c3f
5
5
  SHA512:
6
- metadata.gz: e24c4f96b42d0eab847c1c5bdef2b79b3847c51db8b862c236f82d61217b295fcfc754933f4f998510b0377b0847f754cf3a6e2d4f937c73b95a8992ad7962f1
7
- data.tar.gz: 2a41173f1fddbd52084dbc8943b6ebcb893a81a47ffd5a07e1194c4b518d042d6c2407c6babc8774c69822d3093b6d0d306ad864edc06302797002594bad3715
6
+ metadata.gz: e6f1c8df5509096dc414acff24952b57d656b064c95777f2741ada1164576c9ef6fe20bd7a13a50c8a0d30784c92d1d56ff000bf853972c5f8ee8334e69c538d
7
+ data.tar.gz: 8d3c36f885f175927efb5418547e782fcd864e81646602d8473b435222f3405f89973337b9368b718ff8413b8a8a0e714d90d2ce0e784fe62cbf02bc98bf734c
data/bin/abt CHANGED
@@ -4,6 +4,7 @@
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
 
@@ -31,8 +31,10 @@ module Abt
31
31
  process_providers
32
32
  end
33
33
 
34
- def print_provider_command(provider, arg_str, description)
35
- 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
36
38
  end
37
39
 
38
40
  private
@@ -60,9 +62,14 @@ module Abt
60
62
 
61
63
  return [] if input.nil?
62
64
 
63
- input.split("\n").map do |line|
64
- 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
65
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+/)
66
73
  end
67
74
 
68
75
  def process_providers
@@ -88,12 +95,14 @@ module Abt
88
95
  command = provider.command_class(command_name)
89
96
  return false if command.nil?
90
97
 
91
- if output.isatty
92
- warn "===== #{command_name} #{provider_name}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
93
- end
98
+ print_command(command_name, provider_name, arg_str) if output.isatty
94
99
 
95
100
  command.new(arg_str: arg_str, cli: self).call
96
101
  true
97
102
  end
103
+
104
+ def print_command(name, provider, arg_str)
105
+ warn "===== #{name} #{provider}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
106
+ end
98
107
  end
99
108
  end
@@ -4,10 +4,26 @@ module Abt
4
4
  class Cli
5
5
  module Dialogs
6
6
  def prompt(question)
7
- 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
- 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
 
@@ -3,16 +3,18 @@
3
3
  module Abt
4
4
  class Cli
5
5
  module Io
6
- %i[puts print].each do |method_name|
7
- define_method(method_name) do |*args|
8
- output.puts(*args)
9
- end
10
- end
11
-
12
6
  def warn(*args)
13
7
  err_output.puts(*args)
14
8
  end
15
9
 
10
+ def puts(*args)
11
+ output.puts(*args)
12
+ end
13
+
14
+ def print(*args)
15
+ output.print(*args)
16
+ end
17
+
16
18
  def abort(message)
17
19
  raise AbortError, message
18
20
  end
@@ -9,15 +9,22 @@ module Abt
9
9
  class << self
10
10
  def examples # rubocop:disable Metrics/MethodLength
11
11
  {
12
- 'Multiple providers and arguments can be passed, e.g.:' => {
13
- 'abt init asana harvest' => nil,
14
- 'abt pick-task asana harvest' => nil,
15
- 'abt start asana harvest' => nil,
16
- 'abt clear asana harvest' => nil
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'
17
19
  },
18
20
  'Command output can be piped, e.g.:' => {
19
21
  'abt tasks asana | grep -i <name of task>' => nil,
20
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'
21
28
  }
22
29
  }
23
30
  end
@@ -6,7 +6,7 @@ module Abt
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
 
@@ -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
 
@@ -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
@@ -5,7 +5,7 @@ module Abt
5
5
  module Asana
6
6
  class Api
7
7
  API_ENDPOINT = 'https://app.asana.com/api/1.0'
8
- VERBS = %i[get post patch].freeze
8
+ VERBS = %i[get post put].freeze
9
9
 
10
10
  attr_reader :access_token
11
11
 
@@ -20,19 +20,24 @@ module Abt
20
20
 
21
21
  private
22
22
 
23
+ def same_args_as_config?
24
+ project_gid == config.project_gid && task_gid == config.task_gid
25
+ end
26
+
23
27
  def print_project(project)
24
28
  cli.print_provider_command('asana', project['gid'], project['name'])
29
+ cli.warn project['permalink_url'] if project.key?('permalink_url') && cli.output.isatty
25
30
  end
26
31
 
27
32
  def print_task(project, task)
33
+ project = { 'gid' => project } if project.is_a?(String)
28
34
  cli.print_provider_command('asana', "#{project['gid']}/#{task['gid']}", task['name'])
35
+ cli.warn task['permalink_url'] if task.key?('permalink_url') && cli.output.isatty
29
36
  end
30
37
 
31
38
  def use_current_args
32
- @project_gid = Abt::GitConfig.local('abt.asana.projectGid').to_s
33
- @project_gid = nil if project_gid.empty?
34
- @task_gid = Abt::GitConfig.local('abt.asana.taskGid').to_s
35
- @task_gid = nil if task_gid.empty?
39
+ @project_gid = config.project_gid
40
+ @task_gid = config.task_gid
36
41
  end
37
42
 
38
43
  def use_arg_str(arg_str)
@@ -14,7 +14,7 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
- if arg_str.nil?
17
+ if same_args_as_config? || !config.local_available?
18
18
  show_current_configuration
19
19
  else
20
20
  cli.warn 'Updating configuration'
@@ -43,7 +43,7 @@ module Abt
43
43
  config.task_gid = nil
44
44
  else
45
45
  ensure_task_is_valid!
46
- config.task_gid task_gid
46
+ config.task_gid = task_gid
47
47
 
48
48
  print_task(project, task)
49
49
  end
@@ -58,11 +58,17 @@ module Abt
58
58
  end
59
59
 
60
60
  def project
61
- @project ||= api.get("projects/#{project_gid}")
61
+ @project ||= begin
62
+ cli.warn 'Fetching project...'
63
+ api.get("projects/#{project_gid}", opt_fields: 'name,permalink_url')
64
+ end
62
65
  end
63
66
 
64
67
  def task
65
- @task ||= api.get("tasks/#{task_gid}")
68
+ @task ||= begin
69
+ cli.warn 'Fetching task...'
70
+ api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url')
71
+ end
66
72
  end
67
73
  end
68
74
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Finalize < BaseCommand
8
+ def self.command
9
+ 'finalize asana[:<project-gid>/<task-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Move current/specified task to section (column) for finalized tasks'
14
+ end
15
+
16
+ def call
17
+ unless config.local_available?
18
+ cli.abort 'This is a no-op for tasks outside the current project'
19
+ end
20
+ cli.abort 'No current or specified task' if task.nil?
21
+ print_task(project_gid, task)
22
+
23
+ if task_already_in_finalized_section?
24
+ cli.warn "Task already in section: #{current_task_section['name']}"
25
+ else
26
+ cli.warn "Moving task to section: #{finalized_section['name']}"
27
+ move_task
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def task_already_in_finalized_section?
34
+ !task_section_membership.nil?
35
+ end
36
+
37
+ def current_task_section
38
+ task_section_membership&.dig('section')
39
+ end
40
+
41
+ def task_section_membership
42
+ task['memberships'].find do |membership|
43
+ membership.dig('section', 'gid') == config.finalized_section_gid
44
+ end
45
+ end
46
+
47
+ def finalized_section
48
+ @finalized_section ||= api.get("sections/#{config.finalized_section_gid}",
49
+ opt_fields: 'name')
50
+ end
51
+
52
+ def move_task
53
+ body = { data: { task: task_gid } }
54
+ body_json = Oj.dump(body, mode: :json)
55
+ api.post("sections/#{config.finalized_section_gid}/addTask", body_json)
56
+ end
57
+
58
+ def task
59
+ @task ||= begin
60
+ if task_gid.nil?
61
+ nil
62
+ else
63
+ api.get("tasks/#{task_gid}", opt_fields: 'name,memberships.section.name,permalink_url')
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end