kaboom 0.3.1
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.
- data/CHANGELOG.markdown +107 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +24 -0
- data/LICENSE.markdown +21 -0
- data/README.markdown +74 -0
- data/Rakefile +150 -0
- data/bin/boom +8 -0
- data/bin/kaboom +8 -0
- data/completion/README.md +7 -0
- data/completion/boom.bash +17 -0
- data/completion/boom.zsh +29 -0
- data/kaboom.gemspec +117 -0
- data/lib/kaboom.rb +59 -0
- data/lib/kaboom/color.rb +52 -0
- data/lib/kaboom/command.rb +389 -0
- data/lib/kaboom/config.rb +116 -0
- data/lib/kaboom/core_ext/symbol.rb +7 -0
- data/lib/kaboom/item.rb +72 -0
- data/lib/kaboom/list.rb +100 -0
- data/lib/kaboom/output.rb +13 -0
- data/lib/kaboom/platform.rb +103 -0
- data/lib/kaboom/remote.rb +47 -0
- data/lib/kaboom/storage.rb +22 -0
- data/lib/kaboom/storage/base.rb +91 -0
- data/lib/kaboom/storage/gist.rb +125 -0
- data/lib/kaboom/storage/json.rb +76 -0
- data/lib/kaboom/storage/keychain.rb +135 -0
- data/lib/kaboom/storage/mongodb.rb +96 -0
- data/lib/kaboom/storage/redis.rb +79 -0
- data/test/examples/config_json.json +3 -0
- data/test/examples/test_json.json +3 -0
- data/test/examples/urls.json +1 -0
- data/test/helper.rb +25 -0
- data/test/output_interceptor.rb +28 -0
- data/test/test_color.rb +30 -0
- data/test/test_command.rb +227 -0
- data/test/test_config.rb +27 -0
- data/test/test_item.rb +54 -0
- data/test/test_list.rb +79 -0
- data/test/test_platform.rb +52 -0
- data/test/test_remote.rb +30 -0
- metadata +151 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
#
|
4
|
+
# Storage is the interface between multiple Backends. You can use Storage
|
5
|
+
# directly without having to worry about which Backend is in use.
|
6
|
+
#
|
7
|
+
module Boom
|
8
|
+
module Storage
|
9
|
+
|
10
|
+
def self.backend=(backend)
|
11
|
+
backend = backend.capitalize
|
12
|
+
Boom::Storage.const_get(backend)
|
13
|
+
Boom.config.attributes['backend'] = backend.downcase
|
14
|
+
Boom.config.save
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.backend
|
18
|
+
Boom::Storage.const_get(Boom.config.attributes['backend'].capitalize).new
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Storage is the middleman between changes the client makes in-memory and how
|
4
|
+
# it's actually persisted to disk (and vice-versa). There are also a few
|
5
|
+
# convenience methods to run searches and operations on the in-memory hash.
|
6
|
+
#
|
7
|
+
module Boom
|
8
|
+
module Storage
|
9
|
+
class Base
|
10
|
+
|
11
|
+
# Public: initializes a Storage instance by loading in your persisted data from adapter.
|
12
|
+
#
|
13
|
+
# Returns the Storage instance.
|
14
|
+
def initialize
|
15
|
+
@lists = []
|
16
|
+
bootstrap
|
17
|
+
Boom::Remote.allowed? self
|
18
|
+
populate
|
19
|
+
end
|
20
|
+
|
21
|
+
# run bootstrap tasks for the storage
|
22
|
+
def bootstrap ; end
|
23
|
+
|
24
|
+
# populate the in-memory store with all the lists and items
|
25
|
+
def populate ; end
|
26
|
+
|
27
|
+
# save the data
|
28
|
+
def save ; end
|
29
|
+
|
30
|
+
|
31
|
+
# Public: the in-memory collection of all Lists attached to this Storage
|
32
|
+
# instance.
|
33
|
+
#
|
34
|
+
# lists - an Array of individual List items
|
35
|
+
#
|
36
|
+
# Returns nothing.
|
37
|
+
attr_writer :lists
|
38
|
+
|
39
|
+
# Public: the list of Lists in your JSON data, sorted by number of items
|
40
|
+
# descending.
|
41
|
+
#
|
42
|
+
# Returns an Array of List objects.
|
43
|
+
def lists
|
44
|
+
@lists.sort_by { |list| -list.items.size }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: tests whether a named List exists.
|
48
|
+
#
|
49
|
+
# name - the String name of a List
|
50
|
+
#
|
51
|
+
# Returns true if found, false if not.
|
52
|
+
def list_exists?(name)
|
53
|
+
@lists.detect { |list| list.name == name }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: all Items in storage.
|
57
|
+
#
|
58
|
+
# Returns an Array of all Items.
|
59
|
+
def items
|
60
|
+
@lists.collect(&:items).flatten
|
61
|
+
end
|
62
|
+
|
63
|
+
# Public: tests whether a named Item exists.
|
64
|
+
#
|
65
|
+
# name - the String name of an Item
|
66
|
+
#
|
67
|
+
# Returns true if found, false if not.
|
68
|
+
def item_exists?(name)
|
69
|
+
items.detect { |item| item.name == name }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: creates a Hash of the representation of the in-memory data
|
73
|
+
# structure. This percolates down to Items by calling to_hash on the List,
|
74
|
+
# which in tern calls to_hash on individual Items.
|
75
|
+
#
|
76
|
+
# Returns a Hash of the entire data set.
|
77
|
+
def to_hash
|
78
|
+
{ :lists => lists.collect(&:to_hash) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def handle error, message
|
82
|
+
case error
|
83
|
+
when NoMethodError
|
84
|
+
output cyan config_text
|
85
|
+
when NameError
|
86
|
+
output message
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
#
|
3
|
+
# Gist backend for Boom.
|
4
|
+
#
|
5
|
+
# Your .boom.conf file should look like this:
|
6
|
+
#
|
7
|
+
# {
|
8
|
+
# "backend": "gist",
|
9
|
+
# "gist": {
|
10
|
+
# "username": "your_github_username",
|
11
|
+
# "password": "your_github_password"
|
12
|
+
# }
|
13
|
+
# }
|
14
|
+
#
|
15
|
+
# There are two optional keys which can be under "gist":
|
16
|
+
#
|
17
|
+
# gist_id - The ID of an existing Gist to use. If not
|
18
|
+
# present, a Gist will be created the first time
|
19
|
+
# Boom is run and will be persisted to the config.
|
20
|
+
# public - Makes the Gist public. An absent value or
|
21
|
+
# any value other than boolean true will make
|
22
|
+
# the Gist private.
|
23
|
+
#
|
24
|
+
|
25
|
+
module Boom
|
26
|
+
module Storage
|
27
|
+
class Gist < Base
|
28
|
+
|
29
|
+
def bootstrap
|
30
|
+
begin
|
31
|
+
require "httparty"
|
32
|
+
|
33
|
+
self.class.send(:include, HTTParty)
|
34
|
+
self.class.base_uri "https://api.github.com"
|
35
|
+
rescue LoadError
|
36
|
+
puts "The Gist backend requires HTTParty: gem install httparty"
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
|
40
|
+
unless Boom.config.attributes["gist"]
|
41
|
+
puts 'A "gist" data structure must be defined in ~/.boom.conf'
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
set_up_auth
|
46
|
+
find_or_create_gist
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.sample_config
|
50
|
+
%({
|
51
|
+
"backend": "gist",
|
52
|
+
"gist": {
|
53
|
+
"username": "your_github_username",
|
54
|
+
"password": "your_github_password"
|
55
|
+
}
|
56
|
+
}
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def populate
|
61
|
+
@storage['lists'].each do |lists|
|
62
|
+
lists.each do |list_name, items|
|
63
|
+
@lists << list = List.new(list_name)
|
64
|
+
|
65
|
+
items.each do |item|
|
66
|
+
item.each do |name,value|
|
67
|
+
list.add_item(Item.new(name,value))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def save
|
75
|
+
self.class.post("/gists/#{@gist_id}", request_params)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def set_up_auth
|
81
|
+
username, password = Boom.config.attributes["gist"]["username"], Boom.config.attributes["gist"]["password"]
|
82
|
+
|
83
|
+
if username and password
|
84
|
+
self.class.basic_auth(username, password)
|
85
|
+
else
|
86
|
+
puts "GitHub username and password must be defined in ~/.boom.conf"
|
87
|
+
exit
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_or_create_gist
|
92
|
+
@gist_id = Boom.config.attributes["gist"]["gist_id"]
|
93
|
+
@public = Boom.config.attributes["gist"]["public"] == true
|
94
|
+
|
95
|
+
if @gist_id.nil? or @gist_id.empty?
|
96
|
+
response = self.class.post("/gists", request_params)
|
97
|
+
else
|
98
|
+
response = self.class.get("/gists/#{@gist_id}", request_params)
|
99
|
+
end
|
100
|
+
|
101
|
+
@storage = MultiJson.decode(response["files"]["boom.json"]["content"]) if response["files"] and response["files"]["boom.json"]
|
102
|
+
|
103
|
+
unless @storage
|
104
|
+
puts "Boom data could not be obtained"
|
105
|
+
exit
|
106
|
+
end
|
107
|
+
|
108
|
+
unless @gist_id
|
109
|
+
Boom.config.attributes["gist"]["gist_id"] = @gist_id = response["id"]
|
110
|
+
Boom.config.save
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def request_params
|
115
|
+
{
|
116
|
+
:body => MultiJson.encode({
|
117
|
+
:description => "boom!",
|
118
|
+
:public => @public,
|
119
|
+
:files => { "boom.json" => { :content => MultiJson.encode(to_hash) } }
|
120
|
+
})
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
#
|
3
|
+
# Json is the default storage option for boom. It writes a Json file to
|
4
|
+
# ~/.boom. Pretty neat, huh?
|
5
|
+
#
|
6
|
+
module Boom
|
7
|
+
module Storage
|
8
|
+
class Json < Base
|
9
|
+
include Output
|
10
|
+
include Color
|
11
|
+
|
12
|
+
JSON_FILE = "#{ENV['HOME']}/.boom"
|
13
|
+
|
14
|
+
# Public: the path to the Json file used by boom.
|
15
|
+
#
|
16
|
+
# Returns the String path of boom's Json representation.
|
17
|
+
def json_file
|
18
|
+
|
19
|
+
JSON_FILE
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.sample_config
|
23
|
+
%({"backend":"json"})
|
24
|
+
end
|
25
|
+
|
26
|
+
# Takes care of bootstrapping the Json file, both in terms of creating the
|
27
|
+
# file and in terms of creating a skeleton Json schema.
|
28
|
+
#
|
29
|
+
# Return true if successfully saved.
|
30
|
+
def bootstrap
|
31
|
+
return if File.exist?(json_file)
|
32
|
+
FileUtils.touch json_file
|
33
|
+
File.open(json_file, 'w') {|f| f.write(to_json) }
|
34
|
+
save
|
35
|
+
end
|
36
|
+
|
37
|
+
# Take a Json representation of data and explode it out into the consituent
|
38
|
+
# Lists and Items for the given Storage instance.
|
39
|
+
#
|
40
|
+
# Returns nothing.
|
41
|
+
def populate
|
42
|
+
storage = MultiJson.decode(File.new(json_file, 'r').read)
|
43
|
+
|
44
|
+
storage['lists'].each do |lists|
|
45
|
+
lists.each do |list_name, items|
|
46
|
+
@lists << list = List.new(list_name)
|
47
|
+
|
48
|
+
items.each do |item|
|
49
|
+
item.each do |name,value|
|
50
|
+
list.add_item(Item.new(name,value))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Public: persists your in-memory objects to disk in Json format.
|
58
|
+
#
|
59
|
+
# lists_Json - list in Json format
|
60
|
+
#
|
61
|
+
# Returns true if successful, false if unsuccessful.
|
62
|
+
def save
|
63
|
+
File.open(json_file, 'w') {|f| f.write(to_json) }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Public: the Json representation of the current List and Item assortment
|
67
|
+
# attached to the Storage instance.
|
68
|
+
#
|
69
|
+
# Returns a String Json representation of its Lists and their Items.
|
70
|
+
def to_json
|
71
|
+
MultiJson.encode(to_hash)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Keychain provides methods for using Mac OS X's Keychain as a storage option.
|
4
|
+
# It saves lists as Keychain files in ~/Library/Keychains with the filename
|
5
|
+
# format being: "Boom.list.mylist.keychain"
|
6
|
+
#
|
7
|
+
module Boom
|
8
|
+
module Storage
|
9
|
+
class Keychain < Base
|
10
|
+
|
11
|
+
KEYCHAIN_FORMAT = %r{Boom\.list\.(.+)\.keychain}
|
12
|
+
|
13
|
+
# Opens Keychain app when json_file is called during `boom edit`
|
14
|
+
#
|
15
|
+
# Returns nothing
|
16
|
+
def open_keychain_app
|
17
|
+
`open /Applications/Utilities/'Keychain Access.app' &`
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :json_file, :open_keychain_app
|
21
|
+
|
22
|
+
# Boostraps Keychain by checking if you're using a Mac which is a prereq
|
23
|
+
#
|
24
|
+
# Returns
|
25
|
+
def bootstrap
|
26
|
+
raise RuntimeError unless is_mac?
|
27
|
+
rescue
|
28
|
+
puts('No Keychain utility to access, maybe try another storage option?')
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
|
32
|
+
# Asks if you're using Mac OS X
|
33
|
+
#
|
34
|
+
# Returns true on a Mac
|
35
|
+
def is_mac?
|
36
|
+
return Boom::Platform.darwin?
|
37
|
+
end
|
38
|
+
|
39
|
+
# Populate the in-memory store with all the lists and items from Keychain
|
40
|
+
#
|
41
|
+
# Returns Array of keychain names, i.e. ["Boom.list.mylist.keychain"]
|
42
|
+
def populate
|
43
|
+
stored_keychain_lists.each do |keychain|
|
44
|
+
@lists << list = List.new(keychain.scan(KEYCHAIN_FORMAT).flatten.first)
|
45
|
+
extract_keychain_items(keychain).each do |name|
|
46
|
+
list.add_item(Item.new(name, extract_keychain_value(name, keychain)))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Saves the data from memory to the correct Keychain
|
52
|
+
#
|
53
|
+
# Returns nothing
|
54
|
+
def save
|
55
|
+
@lists.each do |list|
|
56
|
+
keychain_name = list_to_filename(list.name)
|
57
|
+
create_keychain_list(keychain_name) unless stored_keychain_lists.include?(keychain_name)
|
58
|
+
unless list.items.empty?
|
59
|
+
list.items.each do |item|
|
60
|
+
store_item(item, keychain_name)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
delete_unwanted_items(list)
|
64
|
+
end
|
65
|
+
delete_unwanted_lists
|
66
|
+
rescue RuntimeError
|
67
|
+
puts(e "Couldn't save to your keychain, check Console.app or above for relevant messages")
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Returns an Array of keychains stored in ~/Library/Keychains:
|
74
|
+
# => ["Boom.list.mylist.keychain"]
|
75
|
+
def stored_keychain_lists
|
76
|
+
@stored_keychain_lists ||= `security -q list-keychains |grep Boom.list` \
|
77
|
+
.split(/[\/\n\"]/).select {|kc| kc =~ KEYCHAIN_FORMAT}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Create the keychain list "Boom.list.mylist.keychain" in ~/Library/Keychains
|
81
|
+
def create_keychain_list(keychain_name)
|
82
|
+
`security -q create-keychain #{keychain_name}`
|
83
|
+
end
|
84
|
+
|
85
|
+
# Saves the individual item's value to the right list/keychain
|
86
|
+
def store_item(item, keychain_name)
|
87
|
+
`security 2>/dev/null -q add-generic-password -a '#{item.name}' -s '#{item.name}' -w '#{item.value}' #{keychain_name}`
|
88
|
+
end
|
89
|
+
|
90
|
+
# Retrieves the value of a particular item in a list
|
91
|
+
def extract_keychain_value(item_name, keychain)
|
92
|
+
`security 2>&1 >/dev/null find-generic-password -ga '#{item_name}' #{keychain}`.chomp.split('"').last
|
93
|
+
end
|
94
|
+
|
95
|
+
# Gets all items in a particular list
|
96
|
+
def extract_keychain_items(keychain_name)
|
97
|
+
@stored_items ||= {}
|
98
|
+
@stored_items[keychain_name] ||= `security dump-keychain -a #{keychain_name} |grep acct` \
|
99
|
+
.split(/\s|\\n|\\"|acct|<blob>=|\"/).reject {|f| f.empty?}
|
100
|
+
end
|
101
|
+
|
102
|
+
# Converts list name to the corresponding keychain filename format based
|
103
|
+
# on the KEYCHAIN_FORMAT
|
104
|
+
def list_to_filename(list_name)
|
105
|
+
KEYCHAIN_FORMAT.source.gsub(/\(\.\+\)/, list_name).gsub('\\','')
|
106
|
+
end
|
107
|
+
|
108
|
+
# Delete's a keychain file
|
109
|
+
def delete_list(keychain_filename)
|
110
|
+
`security delete-keychain #{keychain_filename}`
|
111
|
+
end
|
112
|
+
|
113
|
+
# Delete's all keychain files you don't want anymore
|
114
|
+
def delete_unwanted_lists
|
115
|
+
(stored_keychain_lists - @lists.map {|list| list_to_filename(list.name)}).each do |filename|
|
116
|
+
delete_list(filename)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Removes unwanted items in a list
|
121
|
+
# security util doesn't have a delete password option so we'll have to
|
122
|
+
# drop it and recreate it with what is in memory
|
123
|
+
def delete_unwanted_items(list)
|
124
|
+
filename = list_to_filename(list.name)
|
125
|
+
if (list.items.size < extract_keychain_items(filename).size)
|
126
|
+
delete_list(filename)
|
127
|
+
create_keychain_list(filename)
|
128
|
+
list.items.each do |item|
|
129
|
+
store_item(item, filename)
|
130
|
+
end unless list.items.empty?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|