keyp 0.0.3 → 0.0.4
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 +4 -4
- data/README.md +7 -1
- data/lib/keyp.rb +1 -218
- data/lib/keyp/bag.rb +219 -0
- data/lib/keyp/version.rb +1 -1
- data/spec/keyp_spec.rb +1 -1
- data/spec/keyper_spec.rb +4 -4
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d98a2fa1da818cd0aec0b3a51abe7d1dd4b8afa4
|
4
|
+
data.tar.gz: 4033012aa8d82fe633552d53520e4e2e1edd0732
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1cbb72344649affc084601ee562eb99a08f40b05763c0947b4bb955b4165a2a5f5e78ccc974ecb51ebac98e15c930b29eaa8dba05718565f9176bcff40511dc5
|
7
|
+
data.tar.gz: 69b475bbb836067eb02bb45332dcd0ba83baa219f2e9ac435daacc4f3c9a04249271b9ac251e8145457a4aed8b67467773746080e72d8fd331d3b5097d8980c9
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[](http://badge.fury.io/rb/keyp)
|
2
|
+
|
1
3
|
# Keyp
|
2
4
|
|
3
5
|
Keyp is an executable Ruby gem that manages user/machine specific key/value pairs for your Ruby application.
|
@@ -16,7 +18,7 @@ for any kind of production code at this point in this gem's lifecycle.
|
|
16
18
|
|
17
19
|
## Quick Tour
|
18
20
|
|
19
|
-
Keyp manages key value pairs in collections. As of version 0.0.
|
21
|
+
Keyp manages key value pairs in collections. As of version 0.0.4, Keyp refers to collections as *bags*. There is a
|
20
22
|
default bag called *default*. Unless you specify a bag name, *default* will be used.
|
21
23
|
|
22
24
|
Here are some command line examples showing some basic Keyp functionality using the default bag. Here we set a couple
|
@@ -90,6 +92,10 @@ TODO: Write detailed usage instructions here
|
|
90
92
|
TODO: Write more detailed documentation, For now, see the quick start above and run the following to see CLI options
|
91
93
|
$ keyp --help
|
92
94
|
|
95
|
+
## Release Notes
|
96
|
+
|
97
|
+
* v 0.0.4 - Fixed missing file error
|
98
|
+
|
93
99
|
## Development plan/Features to implement
|
94
100
|
|
95
101
|
* Get basic functionality working soundly backed with effective tests
|
data/lib/keyp.rb
CHANGED
@@ -115,7 +115,7 @@ module Keyp
|
|
115
115
|
# * +options+ - options are passed through to Bag.new
|
116
116
|
#
|
117
117
|
def self.bag(name='default', options = {})
|
118
|
-
bag =
|
118
|
+
bag = Bag.new(name, options)
|
119
119
|
bag
|
120
120
|
end
|
121
121
|
|
@@ -221,223 +221,6 @@ module Keyp
|
|
221
221
|
|
222
222
|
end
|
223
223
|
|
224
|
-
# ----------------------------------------------
|
225
|
-
|
226
|
-
# Some inspiration:
|
227
|
-
# http://stackoverflow.com/questions/2680523/dry-ruby-initialization-with-hash-argument
|
228
|
-
#
|
229
|
-
# TODO: add handling so that keys (hierarchical keys too) are accessed as members instead of
|
230
|
-
# hash access
|
231
|
-
#
|
232
|
-
# TODO: move to own file, rename to "Bag"
|
233
|
-
# TODO: i18n error messages
|
234
|
-
#
|
235
|
-
class Keyper
|
236
|
-
|
237
|
-
attr_reader :keypdir, :dirty
|
238
|
-
attr_accessor :name, :data, :file_hash
|
239
|
-
|
240
|
-
def keypfile
|
241
|
-
File.join(@keypdir, @name+@ext)
|
242
|
-
end
|
243
|
-
|
244
|
-
# We expect
|
245
|
-
# I'm not happy with how creating instance variables works. There must be a cleaner way
|
246
|
-
def initialize(name, options = {})
|
247
|
-
@name = name
|
248
|
-
options.each do |k,v|
|
249
|
-
puts "processing options #{k} = #{v}"
|
250
|
-
instance_variable_set("@#{k}", v) unless v.nil?
|
251
|
-
end
|
252
|
-
# set attributes not set by params
|
253
|
-
|
254
|
-
@keypdir ||= Keyp::home
|
255
|
-
@read_only ||= false
|
256
|
-
@ext ||= Keyp::DEFAULT_EXT
|
257
|
-
#@keypfile = config_path
|
258
|
-
# load our resource
|
259
|
-
|
260
|
-
# load config file into hashes
|
261
|
-
# not the most efficient thing, but simpler and safe enough for now
|
262
|
-
|
263
|
-
unless File.exist? keypfile
|
264
|
-
puts "Keyper.initialize, create_bag #{keypfile}"
|
265
|
-
Keyp::create_bag(name)
|
266
|
-
end
|
267
|
-
file_data = load(keypfile)
|
268
|
-
|
269
|
-
@meta = file_data[:meta]
|
270
|
-
@data = file_data[:data]|| {}
|
271
|
-
@file_hash = file_data[:file_hash]
|
272
|
-
@dirty = false
|
273
|
-
end
|
274
|
-
|
275
|
-
def [](key)
|
276
|
-
@data[key]
|
277
|
-
end
|
278
|
-
|
279
|
-
def []=(key, value)
|
280
|
-
set_prop(key, value)
|
281
|
-
end
|
282
|
-
|
283
|
-
def set_prop(key, value)
|
284
|
-
unless @read_only
|
285
|
-
# TODO: check if data has been modified
|
286
|
-
# maybe there is a way hash tells us its been modified. If not then
|
287
|
-
# just check if key,val is already in hash and matches
|
288
|
-
@data[key] = value
|
289
|
-
@dirty = true
|
290
|
-
else
|
291
|
-
raise "Bag #{@name} is read only"
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
def delete(key)
|
296
|
-
unless @read_only
|
297
|
-
if @data.key? key
|
298
|
-
@dirty = true
|
299
|
-
end
|
300
|
-
val = @data.delete(key)
|
301
|
-
else
|
302
|
-
raise "Bag #{@name} is read only"
|
303
|
-
end
|
304
|
-
val
|
305
|
-
end
|
306
|
-
|
307
|
-
def empty?
|
308
|
-
@data.empty?
|
309
|
-
end
|
310
|
-
|
311
|
-
|
312
|
-
##
|
313
|
-
# Adds key/value pairs from this bag to the Ruby ENV
|
314
|
-
# NOTE: Currently in development.
|
315
|
-
# If no options are provided, then all of the bag's key/value pairs will be assigned.
|
316
|
-
#
|
317
|
-
# ==== Options
|
318
|
-
# TBD:
|
319
|
-
# +:sysvar+ Only use valid system environment vars
|
320
|
-
# +:selection+ Provide a list of keys to match
|
321
|
-
# +:overwrite+
|
322
|
-
# +:no_overwrite+ - This is enabled by default
|
323
|
-
# +:to_upper
|
324
|
-
|
325
|
-
# Returns a hash of the key/value pairs which have been set
|
326
|
-
# ==== Examples
|
327
|
-
# +add_to_env upper:+
|
328
|
-
# To assign keys matching a pattern:
|
329
|
-
# +add_to_env regex: '\A(_|[A-Z])[a-zA-Z\d]*'
|
330
|
-
|
331
|
-
def add_to_env(options = {})
|
332
|
-
# TODO: Add checking, upcase
|
333
|
-
|
334
|
-
# pattern matching valid env var
|
335
|
-
sys_env_reg = /\A(_|[a-zA-Z])\w*/
|
336
|
-
assigned = {}
|
337
|
-
overwrite = options[:overwrite] || false
|
338
|
-
pattern = options[:sysvar] if options.key?(:sysvar)
|
339
|
-
|
340
|
-
pattern ||= '(...)'
|
341
|
-
|
342
|
-
bag.data.each do |key,value|
|
343
|
-
if pattern.match(key)
|
344
|
-
# TODO: add overwrite checking
|
345
|
-
ENV[key] = value
|
346
|
-
assigned[key] = value
|
347
|
-
end
|
348
|
-
end
|
349
|
-
assigned
|
350
|
-
end
|
351
|
-
|
352
|
-
|
353
|
-
# TODO add from hash
|
354
|
-
|
355
|
-
|
356
|
-
def import(filename)
|
357
|
-
raise "import not yet supported"
|
358
|
-
end
|
359
|
-
|
360
|
-
def export(filename)
|
361
|
-
raise "export not yet supported"
|
362
|
-
end
|
363
|
-
|
364
|
-
|
365
|
-
# TODO: def to_yaml
|
366
|
-
# TODO: def to_json
|
367
|
-
# TODO: def to_s
|
368
|
-
|
369
|
-
##
|
370
|
-
# Give full path, attempt to load
|
371
|
-
# sticking with YAML format for now
|
372
|
-
# May add new file format later in which case we'll
|
373
|
-
# TODO: consider changing to class method
|
374
|
-
def load (config_path)
|
375
|
-
#config_data = {}
|
376
|
-
# Get extension
|
377
|
-
file_ext = File.extname(config_path)
|
378
|
-
|
379
|
-
# check
|
380
|
-
# only YAML supported for initial version. Will consider adapters to
|
381
|
-
# abstract the persistence layer
|
382
|
-
|
383
|
-
|
384
|
-
# TODO: make this hardcoded case a hash of helpers
|
385
|
-
# TODO: Add two sections: Meta and data, then return as hash
|
386
|
-
|
387
|
-
if file_ext.downcase == Keyp::DEFAULT_EXT
|
388
|
-
|
389
|
-
# Either we are arbitrarily creating directories when
|
390
|
-
# given a path for a file that doesn't exist
|
391
|
-
# or we have special behavior for the default dir
|
392
|
-
# or we just fault and let the caller deal with it
|
393
|
-
unless File.exist? config_path
|
394
|
-
raise "Keyp config file not found: #{config_path}"
|
395
|
-
end
|
396
|
-
|
397
|
-
file_data = YAML.load_file(config_path)
|
398
|
-
|
399
|
-
else
|
400
|
-
raise "Keyp version x only supports YAML for config files. You tried a #{file_ext}"
|
401
|
-
end
|
402
|
-
{ meta: file_data['meta'], data: file_data['data']||{}, file_hash: file_data.hash }
|
403
|
-
end
|
404
|
-
|
405
|
-
##
|
406
|
-
# Saves the Bag to file
|
407
|
-
#
|
408
|
-
# NOT thread safe
|
409
|
-
# TODO: make thread safe
|
410
|
-
def save
|
411
|
-
if @read_only
|
412
|
-
raise "This bag instance is read only"
|
413
|
-
end
|
414
|
-
if @dirty
|
415
|
-
# lock file
|
416
|
-
# read checksum
|
417
|
-
# if checksum matches our saved checksum then update file and release lock
|
418
|
-
# otherwise, raise
|
419
|
-
# TODO: implement merge from updated file and raise only if conflict
|
420
|
-
begin
|
421
|
-
file_data = { 'meta' => @meta, 'data' => @data }
|
422
|
-
|
423
|
-
if File.exist? keypfile
|
424
|
-
read_file_data = load(keypfile)
|
425
|
-
unless @file_hash == read_file_data[:file_hash]
|
426
|
-
raise "Will not write to #{keypfile}\nHashes differ. Expected hash =#{@file_hash}\n" +
|
427
|
-
"found hash #{read_file_data[:file_hash]}"
|
428
|
-
end
|
429
|
-
end
|
430
|
-
File.open(keypfile, 'w') do |f|
|
431
|
-
f.write file_data.to_yaml
|
432
|
-
end
|
433
|
-
@dirty = false
|
434
|
-
rescue
|
435
|
-
# TODO: log error
|
436
|
-
|
437
|
-
end
|
438
|
-
end
|
439
|
-
end
|
440
|
-
end
|
441
224
|
|
442
225
|
|
443
226
|
# Hmm, do we really need this or can we suffice with the meta hash in the bag
|
data/lib/keyp/bag.rb
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
module Keyp
|
2
|
+
# ----------------------------------------------
|
3
|
+
|
4
|
+
# Some inspiration:
|
5
|
+
# http://stackoverflow.com/questions/2680523/dry-ruby-initialization-with-hash-argument
|
6
|
+
#
|
7
|
+
# TODO: add handling so that keys (hierarchical keys too) are accessed as members instead of
|
8
|
+
# hash access
|
9
|
+
#
|
10
|
+
# TODO: move to own file, rename to "Bag"
|
11
|
+
# TODO: i18n error messages
|
12
|
+
#
|
13
|
+
class Bag
|
14
|
+
|
15
|
+
attr_reader :keypdir, :dirty
|
16
|
+
attr_accessor :name, :data, :file_hash
|
17
|
+
|
18
|
+
def keypfile
|
19
|
+
File.join(@keypdir, @name+@ext)
|
20
|
+
end
|
21
|
+
|
22
|
+
# We expect
|
23
|
+
# I'm not happy with how creating instance variables works. There must be a cleaner way
|
24
|
+
def initialize(name, options = {})
|
25
|
+
@name = name
|
26
|
+
options.each do |k,v|
|
27
|
+
puts "processing options #{k} = #{v}"
|
28
|
+
instance_variable_set("@#{k}", v) unless v.nil?
|
29
|
+
end
|
30
|
+
# set attributes not set by params
|
31
|
+
|
32
|
+
@keypdir ||= Keyp::home
|
33
|
+
@read_only ||= false
|
34
|
+
@ext ||= Keyp::DEFAULT_EXT
|
35
|
+
#@keypfile = config_path
|
36
|
+
# load our resource
|
37
|
+
|
38
|
+
# load config file into hashes
|
39
|
+
# not the most efficient thing, but simpler and safe enough for now
|
40
|
+
|
41
|
+
unless File.exist? keypfile
|
42
|
+
puts "Keyper.initialize, create_bag #{keypfile}"
|
43
|
+
Keyp::create_bag(name)
|
44
|
+
end
|
45
|
+
file_data = load(keypfile)
|
46
|
+
|
47
|
+
@meta = file_data[:meta]
|
48
|
+
@data = file_data[:data]|| {}
|
49
|
+
@file_hash = file_data[:file_hash]
|
50
|
+
@dirty = false
|
51
|
+
end
|
52
|
+
|
53
|
+
def [](key)
|
54
|
+
@data[key]
|
55
|
+
end
|
56
|
+
|
57
|
+
def []=(key, value)
|
58
|
+
set_prop(key, value)
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_prop(key, value)
|
62
|
+
unless @read_only
|
63
|
+
# TODO: check if data has been modified
|
64
|
+
# maybe there is a way hash tells us its been modified. If not then
|
65
|
+
# just check if key,val is already in hash and matches
|
66
|
+
@data[key] = value
|
67
|
+
@dirty = true
|
68
|
+
else
|
69
|
+
raise "Bag #{@name} is read only"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def delete(key)
|
74
|
+
unless @read_only
|
75
|
+
if @data.key? key
|
76
|
+
@dirty = true
|
77
|
+
end
|
78
|
+
val = @data.delete(key)
|
79
|
+
else
|
80
|
+
raise "Bag #{@name} is read only"
|
81
|
+
end
|
82
|
+
val
|
83
|
+
end
|
84
|
+
|
85
|
+
def empty?
|
86
|
+
@data.empty?
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
##
|
91
|
+
# Adds key/value pairs from this bag to the Ruby ENV
|
92
|
+
# NOTE: Currently in development.
|
93
|
+
# If no options are provided, then all of the bag's key/value pairs will be assigned.
|
94
|
+
#
|
95
|
+
# ==== Options
|
96
|
+
# TBD:
|
97
|
+
# +:sysvar+ Only use valid system environment vars
|
98
|
+
# +:selection+ Provide a list of keys to match
|
99
|
+
# +:overwrite+
|
100
|
+
# +:no_overwrite+ - This is enabled by default
|
101
|
+
# +:to_upper
|
102
|
+
|
103
|
+
# Returns a hash of the key/value pairs which have been set
|
104
|
+
# ==== Examples
|
105
|
+
# +add_to_env upper:+
|
106
|
+
# To assign keys matching a pattern:
|
107
|
+
# +add_to_env regex: '\A(_|[A-Z])[a-zA-Z\d]*'
|
108
|
+
|
109
|
+
def add_to_env(options = {})
|
110
|
+
# TODO: Add checking, upcase
|
111
|
+
|
112
|
+
# pattern matching valid env var
|
113
|
+
sys_env_reg = /\A(_|[a-zA-Z])\w*/
|
114
|
+
assigned = {}
|
115
|
+
overwrite = options[:overwrite] || false
|
116
|
+
pattern = options[:sysvar] if options.key?(:sysvar)
|
117
|
+
|
118
|
+
pattern ||= '(...)'
|
119
|
+
|
120
|
+
bag.data.each do |key,value|
|
121
|
+
if pattern.match(key)
|
122
|
+
# TODO: add overwrite checking
|
123
|
+
ENV[key] = value
|
124
|
+
assigned[key] = value
|
125
|
+
end
|
126
|
+
end
|
127
|
+
assigned
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# TODO add from hash
|
132
|
+
|
133
|
+
|
134
|
+
def import(filename)
|
135
|
+
raise "import not yet supported"
|
136
|
+
end
|
137
|
+
|
138
|
+
def export(filename)
|
139
|
+
raise "export not yet supported"
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
# TODO: def to_yaml
|
144
|
+
# TODO: def to_json
|
145
|
+
# TODO: def to_s
|
146
|
+
|
147
|
+
##
|
148
|
+
# Give full path, attempt to load
|
149
|
+
# sticking with YAML format for now
|
150
|
+
# May add new file format later in which case we'll
|
151
|
+
# TODO: consider changing to class method
|
152
|
+
def load (config_path)
|
153
|
+
#config_data = {}
|
154
|
+
# Get extension
|
155
|
+
file_ext = File.extname(config_path)
|
156
|
+
|
157
|
+
# check
|
158
|
+
# only YAML supported for initial version. Will consider adapters to
|
159
|
+
# abstract the persistence layer
|
160
|
+
|
161
|
+
|
162
|
+
# TODO: make this hardcoded case a hash of helpers
|
163
|
+
# TODO: Add two sections: Meta and data, then return as hash
|
164
|
+
|
165
|
+
if file_ext.downcase == Keyp::DEFAULT_EXT
|
166
|
+
|
167
|
+
# Either we are arbitrarily creating directories when
|
168
|
+
# given a path for a file that doesn't exist
|
169
|
+
# or we have special behavior for the default dir
|
170
|
+
# or we just fault and let the caller deal with it
|
171
|
+
unless File.exist? config_path
|
172
|
+
raise "Keyp config file not found: #{config_path}"
|
173
|
+
end
|
174
|
+
|
175
|
+
file_data = YAML.load_file(config_path)
|
176
|
+
|
177
|
+
else
|
178
|
+
raise "Keyp version x only supports YAML for config files. You tried a #{file_ext}"
|
179
|
+
end
|
180
|
+
{ meta: file_data['meta'], data: file_data['data']||{}, file_hash: file_data.hash }
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Saves the Bag to file
|
185
|
+
#
|
186
|
+
# NOT thread safe
|
187
|
+
# TODO: make thread safe
|
188
|
+
def save
|
189
|
+
if @read_only
|
190
|
+
raise "This bag instance is read only"
|
191
|
+
end
|
192
|
+
if @dirty
|
193
|
+
# lock file
|
194
|
+
# read checksum
|
195
|
+
# if checksum matches our saved checksum then update file and release lock
|
196
|
+
# otherwise, raise
|
197
|
+
# TODO: implement merge from updated file and raise only if conflict
|
198
|
+
begin
|
199
|
+
file_data = { 'meta' => @meta, 'data' => @data }
|
200
|
+
|
201
|
+
if File.exist? keypfile
|
202
|
+
read_file_data = load(keypfile)
|
203
|
+
unless @file_hash == read_file_data[:file_hash]
|
204
|
+
raise "Will not write to #{keypfile}\nHashes differ. Expected hash =#{@file_hash}\n" +
|
205
|
+
"found hash #{read_file_data[:file_hash]}"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
File.open(keypfile, 'w') do |f|
|
209
|
+
f.write file_data.to_yaml
|
210
|
+
end
|
211
|
+
@dirty = false
|
212
|
+
rescue
|
213
|
+
# TODO: log error
|
214
|
+
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
data/lib/keyp/version.rb
CHANGED
data/spec/keyp_spec.rb
CHANGED
@@ -5,7 +5,7 @@ describe Keyp do
|
|
5
5
|
context "CONSTANTS" do
|
6
6
|
it 'should return correct version string' do
|
7
7
|
#Keyp.version_string.should == "Keyp version #{Keyp::VERSION}"
|
8
|
-
Keyp::VERSION.should == '0.0.
|
8
|
+
Keyp::VERSION.should == '0.0.4'
|
9
9
|
end
|
10
10
|
|
11
11
|
it 'should specify default store' do
|
data/spec/keyper_spec.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Keyp::
|
3
|
+
describe Keyp::Bag do
|
4
4
|
|
5
5
|
context "is empty" do
|
6
6
|
before (:each) do
|
7
7
|
puts "before : is empty"
|
8
|
-
@bag = Keyp::
|
8
|
+
@bag = Keyp::Bag.new 'testing123'
|
9
9
|
end
|
10
10
|
|
11
11
|
it "should return an empty hash" do
|
@@ -20,7 +20,7 @@ describe Keyp::Keyper do
|
|
20
20
|
context "is not empty" do
|
21
21
|
before (:each) do
|
22
22
|
puts "before : is not empty"
|
23
|
-
@bag = Keyp::
|
23
|
+
@bag = Keyp::Bag.new 'grue_eats_you'
|
24
24
|
@bag['LIGHTS'] = 'out'
|
25
25
|
end
|
26
26
|
it "should return a non-empty hash" do
|
@@ -32,7 +32,7 @@ describe Keyp::Keyper do
|
|
32
32
|
before (:each) do
|
33
33
|
# TODO: pseudo random bag name generation to a helper
|
34
34
|
bag_name = "grue_eats_you_when_it_is_dark_#{Time.now.strftime("%Y%m%d%H%M")}"
|
35
|
-
@bag = Keyp::
|
35
|
+
@bag = Keyp::Bag.new bag_name
|
36
36
|
end
|
37
37
|
it "should copy all vars"
|
38
38
|
=begin
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: keyp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Baldwin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-04-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: gli
|
@@ -83,6 +83,7 @@ files:
|
|
83
83
|
- bin/keyp
|
84
84
|
- keyp.gemspec
|
85
85
|
- lib/keyp.rb
|
86
|
+
- lib/keyp/bag.rb
|
86
87
|
- lib/keyp/cli.rb
|
87
88
|
- lib/keyp/version.rb
|
88
89
|
- spec/keyp_spec.rb
|