abt-cli 0.0.11 → 0.0.16

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +1 -7
  3. data/lib/abt.rb +12 -3
  4. data/lib/abt/cli.rb +91 -53
  5. data/lib/abt/cli/arguments_parser.rb +70 -0
  6. data/lib/abt/cli/base_command.rb +61 -0
  7. data/lib/abt/cli/prompt.rb +124 -0
  8. data/lib/abt/docs.rb +24 -18
  9. data/lib/abt/docs/cli.rb +42 -11
  10. data/lib/abt/docs/markdown.rb +36 -10
  11. data/lib/abt/git_config.rb +34 -19
  12. data/lib/abt/helpers.rb +1 -1
  13. data/lib/abt/providers/asana/base_command.rb +24 -13
  14. data/lib/abt/providers/asana/commands/add.rb +75 -0
  15. data/lib/abt/providers/asana/commands/branch_name.rb +44 -0
  16. data/lib/abt/providers/asana/commands/clear.rb +17 -6
  17. data/lib/abt/providers/asana/commands/current.rb +6 -6
  18. data/lib/abt/providers/asana/commands/finalize.rb +4 -4
  19. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +4 -3
  20. data/lib/abt/providers/asana/commands/init.rb +5 -5
  21. data/lib/abt/providers/asana/commands/pick.rb +16 -7
  22. data/lib/abt/providers/asana/commands/projects.rb +3 -3
  23. data/lib/abt/providers/asana/commands/share.rb +8 -8
  24. data/lib/abt/providers/asana/commands/start.rb +15 -9
  25. data/lib/abt/providers/asana/commands/tasks.rb +5 -3
  26. data/lib/abt/providers/asana/configuration.rb +8 -16
  27. data/lib/abt/providers/devops/api.rb +32 -2
  28. data/lib/abt/providers/devops/base_command.rb +32 -16
  29. data/lib/abt/providers/devops/commands/boards.rb +36 -0
  30. data/lib/abt/providers/devops/commands/branch_name.rb +45 -0
  31. data/lib/abt/providers/devops/commands/clear.rb +17 -6
  32. data/lib/abt/providers/devops/commands/current.rb +6 -10
  33. data/lib/abt/providers/devops/commands/harvest_time_entry_data.rb +5 -3
  34. data/lib/abt/providers/devops/commands/init.rb +5 -5
  35. data/lib/abt/providers/devops/commands/pick.rb +29 -20
  36. data/lib/abt/providers/devops/commands/share.rb +7 -13
  37. data/lib/abt/providers/devops/commands/work-items.rb +46 -0
  38. data/lib/abt/providers/devops/configuration.rb +7 -15
  39. data/lib/abt/providers/git.rb +19 -0
  40. data/lib/abt/providers/git/commands/branch.rb +74 -0
  41. data/lib/abt/providers/harvest/base_command.rb +24 -13
  42. data/lib/abt/providers/harvest/commands/clear.rb +17 -6
  43. data/lib/abt/providers/harvest/commands/current.rb +6 -6
  44. data/lib/abt/providers/harvest/commands/init.rb +5 -5
  45. data/lib/abt/providers/harvest/commands/pick.rb +15 -6
  46. data/lib/abt/providers/harvest/commands/projects.rb +3 -3
  47. data/lib/abt/providers/harvest/commands/share.rb +5 -5
  48. data/lib/abt/providers/harvest/commands/start.rb +6 -44
  49. data/lib/abt/providers/harvest/commands/stop.rb +3 -3
  50. data/lib/abt/providers/harvest/commands/tasks.rb +5 -3
  51. data/lib/abt/providers/harvest/commands/track.rb +50 -13
  52. data/lib/abt/providers/harvest/configuration.rb +7 -13
  53. data/lib/abt/version.rb +1 -1
  54. metadata +12 -7
  55. data/lib/abt/cli/dialogs.rb +0 -86
  56. data/lib/abt/cli/io.rb +0 -23
  57. data/lib/abt/providers/asana/commands/clear_global.rb +0 -24
  58. data/lib/abt/providers/devops/commands/clear_global.rb +0 -24
  59. data/lib/abt/providers/harvest/commands/clear_global.rb +0 -24
data/lib/abt/docs.rb CHANGED
@@ -7,7 +7,7 @@ end
7
7
  module Abt
8
8
  module Docs
9
9
  class << self
10
- def examples # rubocop:disable Metrics/MethodLength
10
+ def basic_examples
11
11
  {
12
12
  'Getting started:' => {
13
13
  'abt init asana harvest' => 'Setup asana and harvest project git repo in working dir',
@@ -16,10 +16,15 @@ module Abt
16
16
  'abt stop harvest' => 'Stop time tracker',
17
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
+ }
21
+ end
22
+
23
+ def extended_examples
24
+ {
20
25
  'Tracking meetings (without changing the config):' => {
21
- 'abt tasks asana | grep -i standup | abt track harvest' => 'Track on asana meeting task without changing any configuration',
22
- 'abt tasks harvest | grep -i comment | abt track harvest' => 'Track on harvest "Comment"-task (will prompt for a comment)'
26
+ 'abt pick asana -d | abt track harvest' => 'Track on asana meeting task',
27
+ 'abt pick harvest -d | abt track harvest -c "Name of meeting"' => 'Track on separate harvest-task'
23
28
  },
24
29
  'Command output can be piped, e.g.:' => {
25
30
  'abt tasks asana | grep -i <name of task>' => nil,
@@ -29,30 +34,31 @@ module Abt
29
34
  'abt share asana harvest | tr "\n" " "' => 'Print current configuration',
30
35
  'abt share asana harvest | tr "\n" " " | pbcopy' => 'Copy configuration (mac only)',
31
36
  'abt start <shared configuration>' => 'Start a shared configuration'
37
+ },
38
+ '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>'
32
42
  }
33
43
  }
34
44
  end
35
45
 
36
46
  def providers
37
- provider_definitions
47
+ @providers ||= Abt.schemes.sort.each_with_object({}) do |scheme, definition|
48
+ definition[scheme] = command_definitions(scheme)
49
+ end
38
50
  end
39
51
 
40
52
  private
41
53
 
42
- def provider_definitions
43
- Abt.provider_names.sort.each_with_object({}) do |name, definition|
44
- provider_module = Abt.provider_module(name)
45
-
46
- definition[name] = command_definitions(provider_module)
47
- end
48
- end
49
-
50
- def command_definitions(provider_module)
51
- provider_module.command_names.each_with_object({}) do |name, definition|
52
- command_class = provider_module.command_class(name)
54
+ def command_definitions(scheme)
55
+ provider = Abt.scheme_provider(scheme)
56
+ provider.command_names.each_with_object({}) do |name, definition|
57
+ command_class = provider.command_class(name)
58
+ full_name = "abt #{name} #{scheme}"
53
59
 
54
- if command_class.respond_to?(:command) && command_class.respond_to?(:description)
55
- definition[command_class.command] = command_class.description
60
+ if command_class.respond_to?(:usage) && command_class.respond_to?(:description)
61
+ definition[full_name] = [command_class.usage, command_class.description]
56
62
  end
57
63
  end
58
64
  end
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
+ <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
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> [<scheme-argument>] [<options> --] [<scheme-argument>] ...'
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,43 @@ 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 uses a hybrid approach between 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:
23
+ - `start xxxx/yyyy aaaa/bbbb`
24
+
25
+ Abt looks like one script, but works like a bunch of light independent scripts:
26
+ - `abt start asana:xxxx/yyyy harvest:aaaa/bbbb`
11
27
 
12
28
  ## Usage
13
- `abt <command> [<provider[:<arguments>]>...]`
29
+ `abt <command> [<scheme-argument>] [<options> --] [<scheme-argument>] ...`
30
+
31
+ Definitions:
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
14
35
 
15
36
  #{example_commands}
16
37
 
17
38
  ## Available commands:
39
+ Some commands have `[options]`. Run such a command with `--help` flag to view supported flags, e.g: `abt track harvest -h`
40
+
18
41
  #{provider_commands}
42
+
43
+ #### This readme was generated with `abt readme > README.md`
19
44
  MD
20
45
  end
21
46
 
@@ -24,7 +49,8 @@ module Abt
24
49
  def example_commands
25
50
  lines = []
26
51
 
27
- Docs.examples.each_with_index do |(title, commands), index|
52
+ examples = Docs.basic_examples.merge(Docs.extended_examples)
53
+ examples.each_with_index do |(title, commands), index|
28
54
  lines << '' unless index.zero?
29
55
  lines << title
30
56
 
@@ -40,17 +66,17 @@ module Abt
40
66
  def provider_commands # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
41
67
  lines = []
42
68
 
43
- Docs.providers.each_with_index do |(provider_name, commands), index|
69
+ Docs.providers.each_with_index do |(scheme, commands), index|
44
70
  lines << '' unless index.zero?
45
- lines << "### #{inflector.humanize(provider_name)}"
71
+ lines << "### #{inflector.humanize(scheme)}"
46
72
  lines << '| Command | Description |'
47
73
  lines << '| :------ | :---------- |'
48
74
 
49
- max_length = commands.keys.map(&:length).max
75
+ max_length = commands.values.map(&:first).map(&:length).max
50
76
 
51
- commands.each do |(command, description)|
52
- adjusted_command = "`#{command}`".ljust(max_length + 2)
53
- lines << "| #{adjusted_command} | #{description} |"
77
+ commands.each do |(_command, (usage, description))|
78
+ adjusted_usage = "`#{usage}`".ljust(max_length + 2)
79
+ lines << "| #{adjusted_usage} | #{description} |"
54
80
  end
55
81
  end
56
82
 
@@ -4,13 +4,19 @@ module Abt
4
4
  class GitConfig
5
5
  attr_reader :namespace, :scope
6
6
 
7
+ class UnsafeNamespaceError < StandardError; end
8
+
9
+ LOCAL_CONFIG_AVAILABLE_CHECK_COMMAND = 'git config --local -l'
10
+
7
11
  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
+ 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?
12
18
  end
13
- status.success?
19
+ success
14
20
  end
15
21
  end
16
22
 
@@ -32,19 +38,17 @@ module Abt
32
38
  set(key, value)
33
39
  end
34
40
 
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
41
  def keys
44
42
  offset = namespace.length + 1
45
43
  full_keys.map { |key| key[offset..-1] }
46
44
  end
47
45
 
46
+ def full_keys
47
+ ensure_scope_available!
48
+
49
+ `git config --#{scope} --get-regexp --name-only ^#{namespace}`.lines.map(&:strip)
50
+ end
51
+
48
52
  def local
49
53
  @local ||= begin
50
54
  if scope == 'local'
@@ -65,25 +69,36 @@ module Abt
65
69
  end
66
70
  end
67
71
 
72
+ def clear(output: nil)
73
+ raise UnsafeNamespaceError, 'Keys can only be cleared within a namespace' if namespace.empty?
74
+
75
+ keys.each do |key|
76
+ output&.puts "Clearing #{scope}: #{key_with_namespace(key)}"
77
+ self[key] = nil
78
+ end
79
+ end
80
+
68
81
  private
69
82
 
83
+ def ensure_scope_available!
84
+ return if scope != 'local' || self.class.local_available?
85
+
86
+ raise StandardError, 'Local configuration is not available outside a git repository'
87
+ end
88
+
70
89
  def key_with_namespace(key)
71
90
  namespace.empty? ? key : "#{namespace}.#{key}"
72
91
  end
73
92
 
74
93
  def get(key)
75
- if scope == 'local' && !self.class.local_available?
76
- raise StandardError, 'Local configuration is not available outside a git repository'
77
- end
94
+ ensure_scope_available!
78
95
 
79
96
  git_value = `git config --#{scope} --get #{key_with_namespace(key).inspect}`.strip
80
97
  git_value.empty? ? nil : git_value
81
98
  end
82
99
 
83
100
  def set(key, value)
84
- if scope == 'local' && !self.class.local_available?
85
- raise StandardError, 'Local configuration is not available outside a git repository'
86
- end
101
+ ensure_scope_available!
87
102
 
88
103
  if value.nil? || value.empty?
89
104
  `git config --#{scope} --unset #{key_with_namespace(key).inspect}`
data/lib/abt/helpers.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Abt
4
4
  module Helpers
5
5
  def self.const_to_command(string)
6
- string = string.to_s
6
+ string = string.to_s.dup
7
7
  string[0] = string[0].downcase
8
8
  string.gsub(/([A-Z])/, '-\1').downcase
9
9
  end
@@ -3,45 +3,56 @@
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::Cli::BaseCommand
7
+ attr_reader :project_gid, :task_gid, :config
8
+
9
+ def initialize(path:, cli:, **)
10
+ super
8
11
 
9
- def initialize(arg_str:, cli:)
10
- @arg_str = arg_str
11
12
  @config = Configuration.new(cli: cli)
12
13
 
13
- if arg_str.nil?
14
- use_current_args
14
+ if path.nil?
15
+ use_current_path
15
16
  else
16
- use_arg_str(arg_str)
17
+ use_path(path)
17
18
  end
18
- @cli = cli
19
19
  end
20
20
 
21
21
  private
22
22
 
23
+ def require_project!
24
+ cli.abort 'No current/specified project. Did you initialize Asana?' if project_gid.nil?
25
+ end
26
+
27
+ def require_task!
28
+ if project_gid.nil?
29
+ cli.abort 'No current/specified project. Did you initialize Asana and pick a task?'
30
+ end
31
+ cli.abort 'No current/specified task. Did you pick an Asana task?' if task_gid.nil?
32
+ end
33
+
23
34
  def same_args_as_config?
24
35
  project_gid == config.project_gid && task_gid == config.task_gid
25
36
  end
26
37
 
27
38
  def print_project(project)
28
- cli.print_provider_command('asana', project['gid'], project['name'])
39
+ cli.print_scheme_argument('asana', project['gid'], project['name'])
29
40
  cli.warn project['permalink_url'] if project.key?('permalink_url') && cli.output.isatty
30
41
  end
31
42
 
32
43
  def print_task(project, task)
33
44
  project = { 'gid' => project } if project.is_a?(String)
34
- cli.print_provider_command('asana', "#{project['gid']}/#{task['gid']}", task['name'])
45
+ cli.print_scheme_argument('asana', "#{project['gid']}/#{task['gid']}", task['name'])
35
46
  cli.warn task['permalink_url'] if task.key?('permalink_url') && cli.output.isatty
36
47
  end
37
48
 
38
- def use_current_args
49
+ def use_current_path
39
50
  @project_gid = config.project_gid
40
51
  @task_gid = config.task_gid
41
52
  end
42
53
 
43
- def use_arg_str(arg_str)
44
- args = arg_str.to_s.split('/')
54
+ def use_path(path)
55
+ args = path.to_s.split('/')
45
56
  @project_gid = args[0].to_s
46
57
  @project_gid = nil if project_gid.empty?
47
58
 
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Add < BaseCommand
8
+ def self.usage
9
+ 'abt add asana[:<project-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Create a new task for the current/specified Asana project'
14
+ end
15
+
16
+ def perform
17
+ require_project!
18
+
19
+ task
20
+ print_task(project, task)
21
+
22
+ move_task if section
23
+ end
24
+
25
+ private
26
+
27
+ def task
28
+ @task ||= begin
29
+ body = {
30
+ data: {
31
+ name: name,
32
+ notes: notes,
33
+ projects: [project_gid]
34
+ }
35
+ }
36
+ cli.warn 'Creating task'
37
+ api.post('tasks', Oj.dump(body, mode: :json))
38
+ end
39
+ end
40
+
41
+ def move_task
42
+ body = { data: { task: task['gid'] } }
43
+ body_json = Oj.dump(body, mode: :json)
44
+ api.post("sections/#{section['gid']}/addTask", body_json)
45
+ end
46
+
47
+ def name
48
+ @name ||= cli.prompt.text 'Enter task description'
49
+ end
50
+
51
+ def notes
52
+ @notes ||= cli.prompt.text 'Enter task notes'
53
+ end
54
+
55
+ def project
56
+ @project ||= api.get("projects/#{project_gid}")
57
+ end
58
+
59
+ def section
60
+ @section ||= cli.prompt.choice 'Add to section?', sections, ['q', 'Don\'t add to section']
61
+ end
62
+
63
+ def sections
64
+ @sections ||= begin
65
+ cli.warn 'Fetching sections...'
66
+ api.get_paged("projects/#{project_gid}/sections", opt_fields: 'name')
67
+ rescue Abt::HttpError::HttpError
68
+ []
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end