kaboom 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|