get_to_work 0.1.0

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.
@@ -0,0 +1,20 @@
1
+ require "pry"
2
+ require "thor"
3
+ require "get_to_work/menu" # thor extensions
4
+ require "get_to_work/menu_presenter"
5
+
6
+ require "get_to_work/version"
7
+ require "get_to_work/cli"
8
+ require "get_to_work/command"
9
+ require "get_to_work/keychain"
10
+ require "get_to_work/command/bootstrap"
11
+ require "get_to_work/command/start"
12
+ require "get_to_work/command/stop"
13
+ require "get_to_work/service"
14
+ require "get_to_work/service/pivotal_tracker"
15
+ require "get_to_work/service/harvest"
16
+ require "get_to_work/config_file"
17
+
18
+ module GetToWork
19
+ # Your code goes here...
20
+ end
@@ -0,0 +1,20 @@
1
+ module GetToWork
2
+ class CLI < Thor
3
+ include GetToWork::Menu
4
+
5
+ desc "bootstrap", "creates .gtw configuration for your current working directory"
6
+ def bootstrap
7
+ GetToWork::Command::Bootstrap.run(cli: self)
8
+ end
9
+
10
+ desc "start <Pivotal Tracker Story ID or URL>", "start working on a pivotal tracker story"
11
+ def start(pt_id = nil)
12
+ GetToWork::Command::Start.run(cli: self, pt_id: pt_id)
13
+ end
14
+
15
+ desc "stop", "#stop working on your current story"
16
+ def stop
17
+ GetToWork::Command::Stop.run(cli: self)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ require "keychain"
2
+
3
+ module GetToWork
4
+ class Command
5
+ def self.run(opts = {})
6
+ trap("SIGINT") { exit! }
7
+ new(opts).run
8
+ end
9
+
10
+ def initialize(opts = {})
11
+ @cli = opts[:cli]
12
+ end
13
+
14
+ def config_file
15
+ ConfigFile.instance
16
+ end
17
+
18
+ def last_timer
19
+ config_file["last_timer"]
20
+ end
21
+
22
+ def last_story
23
+ config_fild["last_story"]
24
+ end
25
+
26
+ def harvest_service
27
+ @harvest ||= GetToWork::Service::Harvest.new(
28
+ GetToWork::ConfigFile.instance.data
29
+ )
30
+ end
31
+
32
+ def run
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,114 @@
1
+ require "pivotal-tracker"
2
+
3
+ module GetToWork
4
+ class Command
5
+ class Bootstrap < GetToWork::Command
6
+ KEYCHAIN_SERVICE = "GetToWork::PivotalTracker".freeze
7
+
8
+ def run(opts = {})
9
+ check_for_config_file
10
+
11
+ pt = GetToWork::Service::PivotalTracker.new
12
+
13
+ @cli.say "\n\nStep #1 #{pt.display_name} Setup", :magenta
14
+ @cli.say "-----------------------------", :magenta
15
+
16
+ if pt.keychain
17
+ pt.authenticate_with_keychain
18
+ else
19
+ username, password = prompt_for_login(pt)
20
+ auth_with_service(service: pt, username: username, password: password)
21
+ end
22
+
23
+ project = prompt_select_project(pt)
24
+ pt.save_config("project_id" => project.id)
25
+
26
+ GetToWork::ConfigFile.save
27
+
28
+
29
+ @cli.say "\n\nStep #2 #{harvest_service.display_name} Setup", :magenta
30
+ @cli.say "-----------------------------", :magenta
31
+
32
+ unless harvest_service.authenticate_with_keychain
33
+ subdomain, username, password = prompt_for_subdomain_and_login(harvest_service)
34
+ auth_with_service(
35
+ service: harvest_service,
36
+ username: username,
37
+ password: password,
38
+ subdomain: subdomain
39
+ )
40
+ end
41
+
42
+ harvest_project = prompt_select_project(harvest_service)
43
+ harvest_task = prompt_select_tasks(harvest_service, harvest_project)
44
+
45
+ harvest_service.save_config(
46
+ "project_id" => harvest_project.id,
47
+ "task_id" => harvest_task["id"],
48
+ "subdomain" => harvest_service.subdomain
49
+ )
50
+
51
+ GetToWork::ConfigFile.save
52
+ end
53
+
54
+ def check_for_config_file
55
+ @config_file = GetToWork::ConfigFile.instance
56
+
57
+ if @config_file
58
+ unless @cli.yes?("Would you like to overwrite your existing #{GetToWork::ConfigFile.filename} file? [y/N]", :green)
59
+ exit(0)
60
+ end
61
+ end
62
+ end
63
+
64
+ def prompt_for_login(service)
65
+ username = @cli.ask "#{service.display_name} Username:", :green
66
+ password = @cli.ask "#{service.display_name} Password:", :green, echo: false
67
+
68
+ [username, password]
69
+ end
70
+
71
+ def prompt_for_subdomain_and_login(service)
72
+ subdomain = @cli.ask "#{service.display_name} Subdomain:", :green
73
+ username, password = prompt_for_login(service)
74
+
75
+ [subdomain, username, password]
76
+ end
77
+
78
+ def auth_with_service(service:, username:, password:, subdomain: nil)
79
+ @cli.say "\n\nAuthenticating with #{service.display_name}...", :magenta
80
+
81
+ begin
82
+ service.authenticate(username: username, password: password, subdomain: subdomain)
83
+ rescue RestClient::Unauthorized
84
+ @cli.say "Could not authenticate with #{service.display_name}", :red
85
+ exit(1)
86
+ end
87
+
88
+ service.update_keychain(account: username)
89
+ end
90
+
91
+ def prompt_select_project(service)
92
+ project_options = GetToWork::MenuPresenter.with_collection(service.projects)
93
+
94
+ @cli.menu_ask(
95
+ "\nSelect a #{service.display_name} project:",
96
+ project_options,
97
+ :green,
98
+ limited_to: project_options.menu_limit
99
+ )
100
+ end
101
+
102
+ def prompt_select_tasks(service, project)
103
+ project_options = GetToWork::MenuPresenter.with_collection(project["tasks"])
104
+
105
+ selected_project = @cli.menu_ask(
106
+ "\nSelect a #{service.display_name} Task:",
107
+ project_options,
108
+ :green,
109
+ limited_to: project_options.menu_limit
110
+ )
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GetToWork
4
+ class Command
5
+ class Start < GetToWork::Command
6
+ def initialize(opts = {})
7
+ super(opts)
8
+ @pt_id = parse_pt_id(opts[:pt_id])
9
+ end
10
+
11
+ def run
12
+
13
+ if @pt_id.nil?
14
+ prompt_to_use_last_story
15
+ end
16
+
17
+ pt = GetToWork::Service::PivotalTracker.new(config_file.data)
18
+ story = pt.story(@pt_id)
19
+
20
+ entry = {
21
+ notes: "##{story.id}\n\n#{story.name}\n#{story.url}",
22
+ project_id: harvest_service.project_id,
23
+ task_id: harvest_service.task_id,
24
+ }
25
+
26
+ timer = harvest_service.start_timer(entry)
27
+ save_last_timer(timer)
28
+ save_last_story(story)
29
+ end
30
+
31
+ def prompt_to_use_last_story
32
+ last_story = config_file["last_story"]
33
+
34
+ if last_story
35
+ @cli.say "\nWould you like to start a timer for your last story?", :green
36
+ @cli.say " ##{last_story[:id.to_s]} ", [:bold, :cyan]
37
+ @cli.say "#{last_story[:name.to_s]}", :magenta
38
+ answer = @cli.yes? "\n[y/N]", :green
39
+
40
+ if answer
41
+ @pt_id = last_story["id"]
42
+ else
43
+ exit(0)
44
+ end
45
+ else
46
+ @cli.say "Couldn't find your last started timer. Please specify a story id."
47
+ exit(0)
48
+ end
49
+ end
50
+
51
+ def parse_pt_id(pt_id)
52
+ return nil if pt_id.nil?
53
+
54
+ pt_id.delete("#")
55
+ pt_id.match(/\d+$/)[0]
56
+ end
57
+
58
+ def save_last_story(story)
59
+ config_file[:last_story.to_s] = {
60
+ "id" => story.id,
61
+ "name" => story.name
62
+ }
63
+
64
+ config_file.save
65
+ end
66
+
67
+ def save_last_timer(timer)
68
+ config_file[:last_timer.to_s] = timer.id
69
+ config_file.save
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,19 @@
1
+ module GetToWork
2
+ class Command
3
+ class Stop < GetToWork::Command
4
+ def run
5
+ if last_timer
6
+ @cli.say "\nStopping your current timer...\n\n", :green
7
+ result = harvest_service.stop_timer(last_timer)
8
+
9
+ if result["id"]
10
+ config_file.data.delete("last_timer")
11
+ config_file.save
12
+ end
13
+ else
14
+ @cli.say "\nYour timer has already been stopped.\n\n", :red
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ require "yaml"
2
+ require "singleton"
3
+
4
+ module GetToWork
5
+ class ConfigFile
6
+ attr_reader :data
7
+ include Singleton
8
+
9
+ def initialize
10
+ setup_data(self.class.path)
11
+ end
12
+
13
+ def setup_data(path)
14
+ @data = begin
15
+ YAML.load_file(path)
16
+ rescue Errno::ENOENT
17
+ {}
18
+ end
19
+ end
20
+
21
+ def [](key)
22
+ @data[key]
23
+ end
24
+
25
+ def []=(key, value)
26
+ @data[key] = value
27
+ end
28
+
29
+ def self.save
30
+ instance.save
31
+ end
32
+
33
+ def save
34
+ File.open(self.class.path, "w") { |f| f.write YAML.dump(@data) }
35
+ end
36
+
37
+ def self.exist?
38
+ File.exist? path
39
+ end
40
+
41
+ def self.path
42
+ File.join(Dir.pwd, filename)
43
+ end
44
+
45
+ def self.filename
46
+ ".get-to-work"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,46 @@
1
+ module GetToWork
2
+ KEYCHAIN_PREFIX = "GetToWork".freeze
3
+
4
+ class Keychain
5
+ def update(opts = {})
6
+ relative_service_name = opts[:service]
7
+ @absolute_service_name =
8
+ self.class.absolute_service_name(relative_service_name)
9
+
10
+ update_or_create_keychain_item(opts)
11
+ end
12
+
13
+ def self.find(service:)
14
+ ::Keychain.generic_passwords.where(
15
+ service: absolute_service_name(service)
16
+ ).all
17
+ end
18
+
19
+ def self.absolute_service_name(relative_name)
20
+ "#{KEYCHAIN_PREFIX}::#{relative_name}"
21
+ end
22
+
23
+ private
24
+
25
+ def update_or_create_keychain_item(opts = {})
26
+ keychain_items = self.class.find(service: opts[:service])
27
+ ::Keychain.generic_passwords.where(
28
+ service: @absolute_service_name
29
+ )
30
+
31
+ if item = keychain_items.first
32
+ item.account = opts[:account]
33
+ item.password = opts[:password]
34
+ else
35
+ create_keychain_item(opts)
36
+ end
37
+ end
38
+
39
+ def create_keychain_item(opts = {})
40
+ relative_service_name = opts[:service]
41
+ opts[:service] = self.class.absolute_service_name(relative_service_name)
42
+
43
+ ::Keychain.generic_passwords.create(opts)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,9 @@
1
+ module GetToWork
2
+ module Menu
3
+ def menu_ask(statement, options, *args)
4
+ print_table(options.table) # Assuming MenuPresenter type
5
+ choice = ask(statement, *args)
6
+ options.item_for(choice: choice)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ module GetToWork
2
+ class MenuPresenter
3
+ def self.with_collection(options)
4
+ new(options)
5
+ end
6
+
7
+ def initialize(options)
8
+ @options = options
9
+ end
10
+
11
+ def table
12
+ @options.map.with_index do |option, i|
13
+ [i + 1, option.name]
14
+ end
15
+ end
16
+
17
+ def item_for(choice:)
18
+ index = choice.to_i - 1
19
+ @options[index]
20
+ end
21
+
22
+ def menu_limit
23
+ (1..@options.count).map(&:to_s)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,64 @@
1
+ module GetToWork
2
+ class Service
3
+ attr_reader :api_token
4
+
5
+ class << self
6
+ attr_accessor :yaml_key, :name, :display_name
7
+ end
8
+
9
+ def yaml_key
10
+ self.class.yaml_key
11
+ end
12
+
13
+ def name
14
+ self.class.name
15
+ end
16
+
17
+ def display_name
18
+ self.class.display_name
19
+ end
20
+
21
+ def initialize(data_hash = nil)
22
+ return if data_hash.nil?
23
+
24
+ @data = data_hash[yaml_key]
25
+ if @data
26
+ @data.each { |name, value| instance_variable_set("@#{name}", value) }
27
+ authenticate_with_keychain
28
+ end
29
+ end
30
+
31
+ def update_keychain(account:)
32
+ raise "@api_token not set for #{name}" if @api_token.nil?
33
+ raise "@name not set for #{name}" if @api_token.nil?
34
+
35
+ GetToWork::Keychain.new.update(
36
+ service: self.class.name,
37
+ account: account,
38
+ password: @api_token
39
+ )
40
+ end
41
+
42
+ def api_token
43
+ if keychain && @api_token.nil?
44
+ @api_token = keychain.password
45
+ set_client_token(@api_token)
46
+ end
47
+
48
+ @api_token
49
+ end
50
+
51
+ def set_client_token(token)
52
+ # noop
53
+ end
54
+
55
+ def keychain
56
+ @keychain ||= GetToWork::Keychain.find(service: name).last
57
+ end
58
+
59
+ def save_config(opts)
60
+ config_file = GetToWork::ConfigFile.instance
61
+ config_file[yaml_key] = opts
62
+ end
63
+ end
64
+ end