get_to_work 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +246 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +42 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exe/get-to-work +6 -0
- data/get_to_work.gemspec +40 -0
- data/lib/get_to_work.rb +20 -0
- data/lib/get_to_work/cli.rb +20 -0
- data/lib/get_to_work/command.rb +35 -0
- data/lib/get_to_work/command/bootstrap.rb +114 -0
- data/lib/get_to_work/command/start.rb +73 -0
- data/lib/get_to_work/command/stop.rb +19 -0
- data/lib/get_to_work/config_file.rb +49 -0
- data/lib/get_to_work/keychain.rb +46 -0
- data/lib/get_to_work/menu.rb +9 -0
- data/lib/get_to_work/menu_presenter.rb +26 -0
- data/lib/get_to_work/service.rb +64 -0
- data/lib/get_to_work/service/harvest.rb +73 -0
- data/lib/get_to_work/service/pivotal_tracker.rb +47 -0
- data/lib/get_to_work/version.rb +3 -0
- metadata +296 -0
data/lib/get_to_work.rb
ADDED
@@ -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,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
|