abt-cli 0.0.16 → 0.0.21

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/abt/ari.rb +20 -0
  3. data/lib/abt/ari_list.rb +13 -0
  4. data/lib/abt/base_command.rb +63 -0
  5. data/lib/abt/cli.rb +59 -47
  6. data/lib/abt/cli/arguments_parser.rb +9 -24
  7. data/lib/abt/cli/global_commands/commands.rb +23 -0
  8. data/lib/abt/cli/global_commands/examples.rb +23 -0
  9. data/lib/abt/cli/global_commands/help.rb +23 -0
  10. data/lib/abt/cli/global_commands/readme.rb +23 -0
  11. data/lib/abt/cli/global_commands/share.rb +36 -0
  12. data/lib/abt/cli/global_commands/version.rb +23 -0
  13. data/lib/abt/cli/prompt.rb +5 -4
  14. data/lib/abt/docs.rb +32 -15
  15. data/lib/abt/docs/cli.rb +5 -5
  16. data/lib/abt/docs/markdown.rb +8 -7
  17. data/lib/abt/git_config.rb +20 -36
  18. data/lib/abt/providers/asana/base_command.rb +15 -35
  19. data/lib/abt/providers/asana/commands/add.rb +9 -7
  20. data/lib/abt/providers/asana/commands/branch_name.rb +9 -4
  21. data/lib/abt/providers/asana/commands/clear.rb +2 -0
  22. data/lib/abt/providers/asana/commands/current.rb +19 -34
  23. data/lib/abt/providers/asana/commands/finalize.rb +5 -9
  24. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +9 -4
  25. data/lib/abt/providers/asana/commands/init.rb +6 -6
  26. data/lib/abt/providers/asana/commands/pick.rb +16 -11
  27. data/lib/abt/providers/asana/commands/projects.rb +1 -1
  28. data/lib/abt/providers/asana/commands/share.rb +5 -7
  29. data/lib/abt/providers/asana/commands/start.rb +14 -12
  30. data/lib/abt/providers/asana/commands/tasks.rb +4 -3
  31. data/lib/abt/providers/asana/configuration.rb +20 -26
  32. data/lib/abt/providers/asana/path.rb +36 -0
  33. data/lib/abt/providers/devops/api.rb +12 -0
  34. data/lib/abt/providers/devops/base_command.rb +15 -40
  35. data/lib/abt/providers/devops/commands/boards.rb +2 -2
  36. data/lib/abt/providers/devops/commands/branch_name.rb +7 -3
  37. data/lib/abt/providers/devops/commands/clear.rb +2 -0
  38. data/lib/abt/providers/devops/commands/current.rb +14 -38
  39. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +9 -1
  40. data/lib/abt/providers/devops/commands/init.rb +15 -15
  41. data/lib/abt/providers/devops/commands/pick.rb +5 -12
  42. data/lib/abt/providers/devops/commands/share.rb +6 -5
  43. data/lib/abt/providers/devops/commands/work-items.rb +1 -1
  44. data/lib/abt/providers/devops/configuration.rb +17 -46
  45. data/lib/abt/providers/devops/path.rb +50 -0
  46. data/lib/abt/providers/git/commands/branch.rb +22 -16
  47. data/lib/abt/providers/harvest/base_command.rb +16 -34
  48. data/lib/abt/providers/harvest/commands/clear.rb +2 -0
  49. data/lib/abt/providers/harvest/commands/current.rb +24 -31
  50. data/lib/abt/providers/harvest/commands/init.rb +5 -6
  51. data/lib/abt/providers/harvest/commands/pick.rb +3 -4
  52. data/lib/abt/providers/harvest/commands/projects.rb +1 -1
  53. data/lib/abt/providers/harvest/commands/share.rb +5 -7
  54. data/lib/abt/providers/harvest/commands/start.rb +1 -1
  55. data/lib/abt/providers/harvest/commands/stop.rb +7 -7
  56. data/lib/abt/providers/harvest/commands/tasks.rb +4 -1
  57. data/lib/abt/providers/harvest/commands/track.rb +26 -19
  58. data/lib/abt/providers/harvest/configuration.rb +20 -29
  59. data/lib/abt/providers/harvest/path.rb +36 -0
  60. data/lib/abt/version.rb +1 -1
  61. metadata +14 -3
  62. data/lib/abt/cli/base_command.rb +0 -61
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class Cli
5
+ module GlobalCommands
6
+ class Version < Abt::BaseCommand
7
+ def self.usage
8
+ 'abt version'
9
+ end
10
+
11
+ def self.description
12
+ 'Print abt version'
13
+ end
14
+
15
+ attr_reader :cli
16
+
17
+ def perform
18
+ puts(Abt::VERSION)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -69,10 +69,11 @@ module Abt
69
69
  end
70
70
 
71
71
  def read_option_number(options_length, nil_option)
72
- output.print '('
73
- output.print options_length > 1 ? "1-#{options_length}" : '1'
74
- output.print nil_option_string(nil_option)
75
- output.print '): '
72
+ str = '('
73
+ str += options_length > 1 ? "1-#{options_length}" : '1'
74
+ str += nil_option_string(nil_option)
75
+ str += '): '
76
+ output.print str
76
77
 
77
78
  input = read_user_input
78
79
 
data/lib/abt/docs.rb CHANGED
@@ -10,11 +10,11 @@ module Abt
10
10
  def basic_examples
11
11
  {
12
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',
13
+ 'abt init asana harvest' => 'Setup asana and harvest project for local git repo',
14
+ 'abt pick harvest' => 'Pick harvest task. This will likely stay the same throughout the project',
15
+ 'abt pick asana | abt start harvest' => 'Pick asana task and start tracking time',
16
16
  'abt stop harvest' => 'Stop time tracker',
17
- 'abt start asana harvest' => 'Continue working, e.g. after a break',
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
20
  }
@@ -22,30 +22,47 @@ module Abt
22
22
 
23
23
  def extended_examples
24
24
  {
25
- 'Tracking meetings (without changing the config):' => {
25
+ 'Tracking meetings (without switching current task setting):' => {
26
26
  'abt pick asana -d | abt track harvest' => 'Track on asana meeting task',
27
27
  'abt pick harvest -d | abt track harvest -c "Name of meeting"' => 'Track on separate harvest-task'
28
28
  },
29
- 'Command output can be piped, e.g.:' => {
29
+ 'Many commands output ARIs that can be piped into other commands:' => {
30
30
  'abt tasks asana | grep -i <name of task>' => nil,
31
31
  'abt tasks asana | grep -i <name of task> | abt start' => nil
32
32
  },
33
- 'Sharing configuration:' => {
34
- 'abt share asana harvest | tr "\n" " "' => 'Print current configuration',
35
- 'abt share asana harvest | tr "\n" " " | pbcopy' => 'Copy configuration (mac only)',
36
- 'abt start <shared configuration>' => 'Start a shared configuration'
33
+ 'Sharing ARIs:' => {
34
+ 'abt share asana harvest | tr "\n" " "' => 'Print current asana and harvest ARIs on a single line',
35
+ 'abt share asana harvest | tr "\n" " " | pbcopy' => 'Copy ARIs to clipboard (mac only)',
36
+ 'abt start <ARIs from coworker>' => 'Work on a task your coworker shared with you',
37
+ 'abt current <ARIs from coworker> | abt start' => 'Set task as current, then start it'
37
38
  },
38
39
  'Flags:' => {
39
- 'abt start harvest -c "comment"' => 'Add command flags after <scheme>:<path>',
40
- 'abt start harvest -c "comment" -- asana' => 'Use -- to mark the end of a flag list if it\'s to be followed by a <scheme-argument>',
41
- 'abt pick harvest | abt start -c "comment"' => 'Flags placed directly after a command applies to piped in <scheme-argument>'
40
+ 'abt start harvest -c "comment"' => 'Add command flags after ARIs',
41
+ 'abt start harvest -c "comment" -- asana' => 'Use -- to end a list of flags, so that it can be followed by another ARI',
42
+ 'abt pick harvest | abt start -c "comment"' => 'Flags placed directly after a command applies to the piped in ARI'
42
43
  }
43
44
  }
44
45
  end
45
46
 
46
47
  def providers
47
- @providers ||= Abt.schemes.sort.each_with_object({}) do |scheme, definition|
48
- definition[scheme] = command_definitions(scheme)
48
+ @providers ||= begin
49
+ providers = {}
50
+
51
+ global_command_names = Abt::Cli.global_command_names
52
+ providers['Global'] = global_command_names.each_with_object({}) do |name, definition|
53
+ command_class = Abt::Cli.global_command_class(name)
54
+ full_name = "abt #{name}"
55
+
56
+ if command_class.respond_to?(:usage) && command_class.respond_to?(:description)
57
+ definition[full_name] = [command_class.usage, command_class.description]
58
+ end
59
+ end
60
+
61
+ Abt.schemes.sort.each_with_object(providers) do |scheme, definition|
62
+ definition[scheme] = command_definitions(scheme)
63
+ end
64
+
65
+ providers
49
66
  end
50
67
  end
51
68
 
data/lib/abt/docs/cli.rb CHANGED
@@ -8,10 +8,10 @@ module Abt
8
8
  <<~TXT
9
9
  Usage: #{usage_line}
10
10
 
11
- <command> Name of command to execute, e.g. start, finalize etc.
12
- <scheme-argument> A URI-like identifier; scheme:path
13
- Points to a project/task etc. within a system.
14
- <options> Optional flags for the command and scheme argument
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
15
15
 
16
16
  #{formatted_examples(Docs.basic_examples)}
17
17
 
@@ -45,7 +45,7 @@ module Abt
45
45
  private
46
46
 
47
47
  def usage_line
48
- 'abt <command> [<scheme-argument>] [<options> --] [<scheme-argument>] ...'
48
+ 'abt <command> [<ARI>] [<options> --] [<ARI>] ...'
49
49
  end
50
50
 
51
51
  def formatted_examples(example_groups)
@@ -15,27 +15,28 @@ module Abt
15
15
 
16
16
  ## How does abt work?
17
17
 
18
- Abt uses a hybrid approach between having small scripts each doing one thing:
18
+ Abt is a hybrid of having small scripts each doing one thing:
19
19
  - `start-asana --project-gid xxxx --task-gid yyyy`
20
20
  - `start-harvest --project-id aaaa --task-id bbbb`
21
21
 
22
- And having a single highly advanced script that does everything:
22
+ And having a single highly advanced script that does everything with a single command:
23
23
  - `start xxxx/yyyy aaaa/bbbb`
24
24
 
25
- Abt looks like one script, but works like a bunch of light independent scripts:
25
+ Abt looks like one command, but works like a bunch of light scripts:
26
26
  - `abt start asana:xxxx/yyyy harvest:aaaa/bbbb`
27
27
 
28
28
  ## Usage
29
- `abt <command> [<scheme-argument>] [<options> --] [<scheme-argument>] ...`
29
+ `abt <command> [<ARI>] [<options> --] [<ARI>] ...`
30
30
 
31
31
  Definitions:
32
32
  - `<command>`: Name of command to execute, e.g. `start`, `finalize` etc.
33
- - `<scheme-argument>`: A URI-like identifier, `scheme:path`, pointing to a project/task etc. within a system.
34
- - `<options>`: Optional flags for the command and scheme argument
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
35
35
 
36
36
  #{example_commands}
37
37
 
38
- ## Available commands:
38
+ ## Commands:
39
+
39
40
  Some commands have `[options]`. Run such a command with `--help` flag to view supported flags, e.g: `abt track harvest -h`
40
41
 
41
42
  #{provider_commands}
@@ -6,21 +6,7 @@ module Abt
6
6
 
7
7
  class UnsafeNamespaceError < StandardError; end
8
8
 
9
- LOCAL_CONFIG_AVAILABLE_CHECK_COMMAND = 'git config --local -l'
10
-
11
- def self.local_available?
12
- return @local_available if instance_variables.include?(:@local_available)
13
-
14
- @local_available = begin
15
- success = false
16
- Open3.popen3(LOCAL_CONFIG_AVAILABLE_CHECK_COMMAND) do |_i, _o, _e, thread|
17
- success = thread.value.success?
18
- end
19
- success
20
- end
21
- end
22
-
23
- def initialize(namespace: '', scope: 'local')
9
+ def initialize(scope = 'local', namespace = '')
24
10
  @namespace = namespace
25
11
 
26
12
  unless %w[local global].include? scope
@@ -30,6 +16,20 @@ module Abt
30
16
  @scope = scope
31
17
  end
32
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
+
33
33
  def [](key)
34
34
  get(key)
35
35
  end
@@ -49,26 +49,6 @@ module Abt
49
49
  `git config --#{scope} --get-regexp --name-only ^#{namespace}`.lines.map(&:strip)
50
50
  end
51
51
 
52
- def local
53
- @local ||= begin
54
- if scope == 'local'
55
- self
56
- else
57
- self.class.new(namespace: namespace, scope: 'local')
58
- end
59
- end
60
- end
61
-
62
- def global
63
- @global ||= begin
64
- if scope == 'global'
65
- self
66
- else
67
- self.class.new(namespace: namespace, scope: 'global')
68
- end
69
- end
70
- end
71
-
72
52
  def clear(output: nil)
73
53
  raise UnsafeNamespaceError, 'Keys can only be cleared within a namespace' if namespace.empty?
74
54
 
@@ -80,8 +60,12 @@ module Abt
80
60
 
81
61
  private
82
62
 
63
+ def availability_check_call
64
+ "git config --#{scope} -l"
65
+ end
66
+
83
67
  def ensure_scope_available!
84
- return if scope != 'local' || self.class.local_available?
68
+ return if available?
85
69
 
86
70
  raise StandardError, 'Local configuration is not available outside a git repository'
87
71
  end
@@ -3,63 +3,43 @@
3
3
  module Abt
4
4
  module Providers
5
5
  module Asana
6
- class BaseCommand < Abt::Cli::BaseCommand
7
- attr_reader :project_gid, :task_gid, :config
6
+ class BaseCommand < Abt::BaseCommand
7
+ extend Forwardable
8
8
 
9
- def initialize(path:, cli:, **)
9
+ attr_reader :path, :config
10
+
11
+ def_delegators(:@path, :project_gid, :task_gid)
12
+
13
+ def initialize(ari:, cli:)
10
14
  super
11
15
 
12
16
  @config = Configuration.new(cli: cli)
13
17
 
14
- if path.nil?
15
- use_current_path
16
- else
17
- use_path(path)
18
- end
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_scheme_argument('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_scheme_argument('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_path
50
- @project_gid = config.project_gid
51
- @task_gid = config.task_gid
52
- end
53
-
54
- def use_path(path)
55
- args = path.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
@@ -17,9 +17,14 @@ module Abt
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
@@ -17,7 +17,7 @@ module Abt
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
@@ -27,6 +27,8 @@ module Abt
27
27
 
28
28
  config.clear_local unless flags[:global]
29
29
  config.clear_global if flags[:global] || flags[:all]
30
+
31
+ warn 'Configuration cleared'
30
32
  end
31
33
  end
32
34
  end
@@ -14,60 +14,45 @@ module Abt
14
14
  end
15
15
 
16
16
  def perform
17
+ abort 'Must be run inside a git repository' unless config.local_available?
18
+
17
19
  require_project!
20
+ ensure_valid_configuration!
18
21
 
19
- if same_args_as_config? || !config.local_available?
20
- show_current_configuration
21
- else
22
- cli.warn 'Updating configuration'
23
- update_configuration
22
+ if path != config.path
23
+ config.path = path
24
+ warn 'Configuration updated'
24
25
  end
25
- end
26
26
 
27
- private
28
-
29
- def show_current_configuration
30
- if task_gid.nil?
31
- print_project(project)
32
- else
33
- print_task(project, task)
34
- end
27
+ print_configuration
35
28
  end
36
29
 
37
- def update_configuration
38
- ensure_project_is_valid!
39
- config.project_gid = project_gid
40
-
41
- if task_gid.nil?
42
- print_project(project)
43
- config.task_gid = nil
44
- else
45
- ensure_task_is_valid!
46
- config.task_gid = task_gid
47
-
48
- print_task(project, task)
49
- end
50
- end
30
+ private
51
31
 
52
- def ensure_project_is_valid!
53
- cli.abort "Invalid project: #{project_gid}" if project.nil?
32
+ def print_configuration
33
+ task_gid.nil? ? print_project(project) : print_task(project, task)
54
34
  end
55
35
 
56
- def ensure_task_is_valid!
57
- cli.abort "Invalid task: #{task_gid}" if task.nil?
36
+ def ensure_valid_configuration!
37
+ abort "Invalid project: #{project_gid}" if project.nil?
38
+ abort "Invalid task: #{task_gid}" if task_gid && task.nil?
58
39
  end
59
40
 
60
41
  def project
61
42
  @project ||= begin
62
- cli.warn 'Fetching project...'
43
+ warn 'Fetching project...'
63
44
  api.get("projects/#{project_gid}", opt_fields: 'name,permalink_url')
45
+ rescue Abt::HttpError::NotFoundError
46
+ nil
64
47
  end
65
48
  end
66
49
 
67
50
  def task
68
51
  @task ||= begin
69
- cli.warn 'Fetching task...'
52
+ warn 'Fetching task...'
70
53
  api.get("tasks/#{task_gid}", opt_fields: 'name,permalink_url')
54
+ rescue Abt::HttpError::NotFoundError
55
+ nil
71
56
  end
72
57
  end
73
58
  end