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.
- 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
|