alloy-kicker 1.9.0 → 1.9.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/README.rdoc CHANGED
@@ -35,19 +35,4 @@ And for fun, ghetto-autotest:
35
35
 
36
36
  == Installation
37
37
 
38
- $ sudo gem install alloy-kicker -s http://gems.github.com
39
-
40
- == TODO
41
-
42
- 15:12 sandbags: ideally i would like all events within a small window (e.g. 2s) to be batched into one
43
- 15:12 sandbags: depending upon whether what is being reported is "file X changed" or "some files in directory X" changed
44
- 15:12 sandbags: i forget the granularity FSEvents supports
45
- 15:13 alloy: Yeah the window can be changed, which is a good idea
46
- 15:13 alloy: FSEvents simply reports changes to files in dir X. nothing more
47
- 15:14 sandbags: btw.. what is your rationale for using "!" in method names?
48
- 15:14 alloy: So in this case we should simply keep the time at which an event occurs and then grep the dirs for files with an higher mtime
49
- 15:14 sandbags: works for me
50
- 15:16 alloy: Well the rationale in these cases was either it could raise something (validate) or it would run something external. But really it was just for kicks :)
51
- 15:16 alloy: It could be cleaned up, as I agree it doesn't help much
52
- 15:17 sandbags: i think it would be easy to grok without them
53
- 15:17 alloy: Yeah indeed
38
+ $ sudo gem install alloy-kicker -s http://gems.github.com
data/TODO.rdoc ADDED
@@ -0,0 +1,78 @@
1
+ == For v2
2
+
3
+ * Create 3 callback chains
4
+ * pre-process: for instance Exclude
5
+ * process: normal callbacks
6
+ * post-process: for instance the default 'could not handle' callback
7
+
8
+ * Add -r option to require recipe files that come bundled with Kicker
9
+
10
+ * Probably easier to let the user mutate the files array instead of having to return an array.
11
+
12
+ * Might be best to create an array of Pathname's instead of simply file path strings.
13
+ Probably a subclass of Pathname with some Kicker specific helpers. (relative_path)
14
+
15
+ * Also maybe add an options/attributes hash to the Pathname subclass onto whch the user
16
+ can add arbitrary attributes for if they spread handing over multiple callbacks.
17
+ But this will make callbacks dependent on each-other...
18
+
19
+ * Add example .kick files. E.g.:
20
+ * remove files in specific dirs from the files stack (log, tmp etc)
21
+ * rewrites the paths for a specific app dir layout so that the JS tests are properly picked up.
22
+
23
+ * Add Rails and jstest recipes from our rails app:
24
+
25
+ def relative_path(path)
26
+ path[(Dir.pwd.length + 1)..-1]
27
+ end
28
+
29
+ # Regular Rails mappings
30
+ Kicker.callback = lambda do |kicker, files|
31
+ test_files = []
32
+
33
+ files.delete_if do |file|
34
+ # Match any ruby test file and run it
35
+ if relative_path(file) =~ /^test\/.+_test\.rb$/
36
+ test_files << relative_path(file)
37
+
38
+ # Match any file in app/ and map it to a test file
39
+ elsif match = relative_path(file).match(%r{^app/(\w+)([\w/]*)/([\w\.]+)\.\w+$})
40
+ type, namespace, file = match[1..3]
41
+
42
+ dir = case type
43
+ when "models"
44
+ "unit"
45
+ when "concerns"
46
+ "unit/concerns"
47
+ when "controllers", "views"
48
+ "functional"
49
+ end
50
+
51
+ if dir
52
+ if type == "views"
53
+ namespace = namespace.split('/')[1..-1]
54
+ file = "#{namespace.pop}_controller"
55
+ end
56
+
57
+ test_files << File.join("test", dir, namespace, "#{file}_test.rb")
58
+ end
59
+ end
60
+ end
61
+
62
+ kicker.execute_command "ruby -r #{test_files.join(' -r ')} -e ''" unless test_files.empty?
63
+ files
64
+ end
65
+
66
+ # Match javascript changes and run with HeadlessSquirrel
67
+ Kicker.callback = lambda do |kicker, files|
68
+ test_files = []
69
+
70
+ files.delete_if do |file|
71
+ if relative_path(file) =~ %r{^test/javascripts/(\w+_test)\.(js|html)$}
72
+ test_files << "test/javascripts/#{$1}.html"
73
+ end
74
+ end
75
+
76
+ kicker.execute_command "jstest #{test_files.join(' ')}" unless test_files.empty?
77
+ files
78
+ end
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 1
3
3
  :minor: 9
4
- :patch: 0
4
+ :patch: 1
data/lib/kicker.rb CHANGED
@@ -40,10 +40,6 @@ class Kicker
40
40
  finished_processing!
41
41
  end
42
42
 
43
- def callback_chain
44
- self.class.callback_chain
45
- end
46
-
47
43
  def start
48
44
  validate_options!
49
45
 
@@ -81,7 +77,7 @@ class Kicker
81
77
 
82
78
  def process(events)
83
79
  unless (files = changed_files(events)).empty?
84
- callback_chain.run(self, files)
80
+ full_chain.call(self, files)
85
81
  finished_processing!
86
82
  end
87
83
  end
@@ -3,19 +3,57 @@ class Kicker
3
3
  alias_method :append_callback, :push
4
4
  alias_method :prepend_callback, :unshift
5
5
 
6
- def run(kicker, files)
6
+ def call(kicker, files)
7
7
  each do |callback|
8
- files = callback.call(kicker, files)
9
- break if !files.is_a?(Array) || files.empty?
8
+ break if files.empty?
9
+ callback.call(kicker, files)
10
10
  end
11
11
  end
12
12
  end
13
13
 
14
- def self.callback_chain
15
- @callback_chain ||= CallbackChain.new
14
+ class << self
15
+ def pre_process_chain
16
+ @pre_process_chain ||= CallbackChain.new
17
+ end
18
+
19
+ def process_chain
20
+ @process_chain ||= CallbackChain.new
21
+ end
22
+
23
+ def post_process_chain
24
+ @post_process_chain ||= CallbackChain.new
25
+ end
26
+
27
+ def full_chain
28
+ @full_chain ||= CallbackChain.new([pre_process_chain, process_chain, post_process_chain])
29
+ end
30
+
31
+ def pre_process_callback=(callback)
32
+ pre_process_chain.append_callback(callback)
33
+ end
34
+
35
+ def process_callback=(callback)
36
+ process_chain.append_callback(callback)
37
+ end
38
+
39
+ def post_process_callback=(callback)
40
+ post_process_chain.prepend_callback(callback)
41
+ end
42
+ end
43
+
44
+ def pre_process_chain
45
+ self.class.pre_process_chain
46
+ end
47
+
48
+ def process_chain
49
+ self.class.process_chain
50
+ end
51
+
52
+ def post_process_chain
53
+ self.class.post_process_chain
16
54
  end
17
55
 
18
- def self.callback=(callback)
19
- callback_chain.prepend_callback(callback)
56
+ def full_chain
57
+ self.class.full_chain
20
58
  end
21
59
  end
@@ -1,7 +1,5 @@
1
- class Kicker
2
- COULD_NOT_HANDLE_CALLBACK = lambda do |kicker, files|
3
- kicker.log("Could not handle: #{files.join(', ')}")
4
- end
5
-
6
- self.callback = COULD_NOT_HANDLE_CALLBACK
1
+ Kicker.post_process_callback = lambda do |kicker, files|
2
+ kicker.log('')
3
+ kicker.log("Could not handle: #{files.join(', ')}")
4
+ kicker.log('')
7
5
  end
@@ -1,7 +1,5 @@
1
- class Kicker
2
- option_parser.on('-e', '--execute [COMMAND]', 'The command to execute.') do |command|
3
- Kicker.callback = lambda do |kicker, _|
4
- kicker.execute_command "sh -c #{command.inspect}"
5
- end
1
+ Kicker.option_parser.on('-e', '--execute [COMMAND]', 'The command to execute.') do |command|
2
+ Kicker.process_callback = lambda do |kicker, _|
3
+ kicker.execute_command "sh -c #{command.inspect}"
6
4
  end
7
5
  end
data/lib/kicker/utils.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  class Kicker
2
2
  def execute_command(command)
3
- log "Change occured. Executing command:"
4
- growl(GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command') if @use_growl
3
+ log "Change occured, executing command: #{command}"
4
+ growl(GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured, executing command:', command) if @use_growl
5
5
 
6
6
  output = `#{command}`
7
7
  output.strip.split("\n").each { |line| log " #{line}" }
@@ -7,7 +7,7 @@ class Kicker
7
7
  end
8
8
 
9
9
  def validate_paths_and_command!
10
- if callback_chain.length == 1
10
+ if process_chain.empty?
11
11
  puts OPTION_PARSER_CALLBACK.call(nil).help
12
12
  exit
13
13
  end
@@ -1,18 +1,53 @@
1
1
  require File.expand_path('../test_helper', __FILE__)
2
2
 
3
- describe "Kicker, concerning its callback chain" do
4
- it "should return the callback chain instance" do
5
- Kicker.callback_chain.should.be.instance_of Kicker::CallbackChain
3
+ describe "Kicker, concerning its callback chains" do
4
+ before do
5
+ @chains = [:pre_process_chain, :process_chain, :post_process_chain, :full_chain]
6
6
  end
7
7
 
8
- it "should provide a shortcut method which prepends a callback" do
9
- Kicker.callback = lambda { :from_callback }
10
- Kicker.callback_chain.first.call.should == :from_callback
8
+ it "should return the callback chain instances" do
9
+ @chains.each do |chain|
10
+ Kicker.send(chain).should.be.instance_of Kicker::CallbackChain
11
+ end
11
12
  end
12
13
 
13
14
  it "should be accessible by an instance" do
14
15
  kicker = Kicker.new({})
15
- kicker.callback_chain.should.be Kicker.callback_chain
16
+
17
+ @chains.each do |chain|
18
+ kicker.send(chain).should.be Kicker.send(chain)
19
+ end
20
+ end
21
+
22
+ it "should provide a shortcut method which appends a callback to the pre-process chain" do
23
+ Kicker.pre_process_chain.expects(:append_callback).with do |callback|
24
+ callback.call == :from_callback
25
+ end
26
+
27
+ Kicker.pre_process_callback = lambda { :from_callback }
28
+ end
29
+
30
+ it "should provide a shortcut method which appends a callback to the process chain" do
31
+ Kicker.process_chain.expects(:append_callback).with do |callback|
32
+ callback.call == :from_callback
33
+ end
34
+
35
+ Kicker.process_callback = lambda { :from_callback }
36
+ end
37
+
38
+ it "should provide a shortcut method which prepends a callback to the post-process chain" do
39
+ Kicker.post_process_chain.expects(:prepend_callback).with do |callback|
40
+ callback.call == :from_callback
41
+ end
42
+
43
+ Kicker.post_process_callback = lambda { :from_callback }
44
+ end
45
+
46
+ it "should have assigned the chains to the `full_chain'" do
47
+ Kicker.full_chain.length.should == 3
48
+ Kicker.full_chain.each_with_index do |chain, index|
49
+ chain.should.be Kicker.send(@chains[index])
50
+ end
16
51
  end
17
52
  end
18
53
 
@@ -45,7 +80,7 @@ describe "An instance of Kicker::CallbackChain, concerning it's API" do
45
80
  end
46
81
  end
47
82
 
48
- describe "An instance of Kicker::CallbackChain, when running the chain" do
83
+ describe "An instance of Kicker::CallbackChain, when calling the chain" do
49
84
  before do
50
85
  @kicker = Kicker.new({})
51
86
 
@@ -56,48 +91,69 @@ describe "An instance of Kicker::CallbackChain, when running the chain" do
56
91
  it "should call the callbacks from first to last" do
57
92
  @chain.append_callback lambda { @result << 1 }
58
93
  @chain.append_callback lambda { @result << 2 }
59
- @chain.run(@kicker, [])
94
+ @chain.call(@kicker, %w{ file })
60
95
  @result.should == [1, 2]
61
96
  end
62
97
 
63
98
  it "should pass in the Kicker instance with each yield" do
64
99
  kicker = nil
65
100
  @chain.append_callback lambda { |x, _| kicker = x }
66
- @chain.run(@kicker, [])
101
+ @chain.call(@kicker, %w{ file })
67
102
  kicker.should.be @kicker
68
103
  end
69
104
 
70
- it "should pass the files array given to run to the first callback and pass the result array of that call to the next callback and so on" do
105
+ it "should pass the files array given to #call to each callback in the chain" do
106
+ array = %w{ /file/1 }
107
+
71
108
  @chain.append_callback lambda { |_, files|
72
- @result.concat(files)
73
- %w{ /file/3 /file/4 }
109
+ files.should.be array
110
+ files.concat(%w{ /file/2 })
74
111
  }
75
112
 
76
113
  @chain.append_callback lambda { |_, files|
114
+ files.should.be array
77
115
  @result.concat(files)
78
- []
79
116
  }
80
117
 
81
- @chain.run(@kicker, %w{ /file/1 /file/2 })
82
- @result.should == %w{ /file/1 /file/2 /file/3 /file/4 }
118
+ @chain.call(@kicker, array)
119
+ @result.should == %w{ /file/1 /file/2 }
83
120
  end
84
121
 
85
- it "should halt the callback chain once an empty array is returned from a callback" do
86
- @chain.append_callback lambda { @result << 1; [] }
87
- @chain.append_callback lambda { @result << 2 }
88
- @chain.run(@kicker, %w{ /file/1 /file/2 })
122
+ it "should halt the callback chain once the given array is empty" do
123
+ @chain.append_callback lambda { |_, files| @result << 1; files.clear }
124
+ @chain.append_callback lambda { |_, files| @result << 2 }
125
+ @chain.call(@kicker, %w{ /file/1 /file/2 })
89
126
  @result.should == [1]
90
127
  end
91
128
 
92
- it "should halt the callback chain if not an Array instance is returned from a callback" do
93
- [nil, false, ''].each do |object|
94
- @chain.clear
95
- @result.clear
96
-
97
- @chain.append_callback lambda { @result << 1; object }
98
- @chain.append_callback lambda { @result << 2 }
99
- @chain.run(@kicker, %w{ /file/1 /file/2 })
100
- @result.should == [1]
129
+ it "should not call any callback if the given array is empty" do
130
+ @chain.append_callback lambda { |_, files| @result << 1 }
131
+ @chain.call(@kicker, [])
132
+ @result.should == []
133
+ end
134
+
135
+ it "should work with a chain of chains as well" do
136
+ array = %w{ file }
137
+
138
+ kicker_and_files = lambda do |kicker, files|
139
+ kicker.should.be @kicker
140
+ files.should.be array
101
141
  end
142
+
143
+ chain1 = Kicker::CallbackChain.new([
144
+ lambda { |*args| kicker_and_files.call(*args); @result << 1 },
145
+ lambda { |*args| kicker_and_files.call(*args); @result << 2 }
146
+ ])
147
+
148
+ chain2 = Kicker::CallbackChain.new([
149
+ lambda { |*args| kicker_and_files.call(*args); @result << 3 },
150
+ lambda { |*args| kicker_and_files.call(*args); @result << 4 }
151
+ ])
152
+
153
+ @chain.append_callback chain1
154
+ @chain.append_callback chain2
155
+
156
+ @chain.call(@kicker, array)
157
+ @result.should == [1, 2, 3, 4]
102
158
  end
103
159
  end
@@ -40,20 +40,20 @@ describe "Kicker, when a change occurs" do
40
40
  @kicker.send(:changed_files, events).should == [file1, file3]
41
41
  end
42
42
 
43
- it "should run the callback chain with all changed files" do
43
+ it "should call the full_chain with all changed files" do
44
44
  files = %w{ /file/1 /file/2 }
45
45
  events = [event('/file/1'), event('/file/2')]
46
46
 
47
47
  @kicker.expects(:changed_files).with(events).returns(files)
48
- @kicker.callback_chain.expects(:run).with(@kicker, files)
48
+ @kicker.full_chain.expects(:call).with(@kicker, files)
49
49
  @kicker.expects(:finished_processing!)
50
50
 
51
51
  @kicker.send(:process, events)
52
52
  end
53
53
 
54
- it "should not run the callback chain if there were no changed files" do
54
+ it "should not call the full_chain if there were no changed files" do
55
55
  @kicker.stubs(:changed_files).returns([])
56
- @kicker.callback_chain.expects(:run).never
56
+ @kicker.full_chain.expects(:call).never
57
57
  @kicker.expects(:finished_processing!).never
58
58
 
59
59
  @kicker.send(:process, [event()])
@@ -51,9 +51,9 @@ describe "Kicker, when starting" do
51
51
  OSX.stubs(:CFRunLoopRun)
52
52
  end
53
53
 
54
- it "should show the usage banner and exit when there is no extra callback defined" do
54
+ it "should show the usage banner and exit when there is no process_callback defined at all" do
55
55
  @kicker.stubs(:validate_paths_exist!)
56
- Kicker.stubs(:callback_chain).returns([1])
56
+ Kicker.stubs(:process_chain).returns([])
57
57
 
58
58
  Kicker::OPTION_PARSER_CALLBACK.stubs(:call).returns(mock('OptionParser', :help => 'help'))
59
59
  @kicker.expects(:puts).with("help")
@@ -3,7 +3,11 @@ require File.expand_path('../../test_helper', __FILE__)
3
3
  describe "Kicker, concerning the default `could not handle file' callback" do
4
4
  it "should log that it could not handle the given files" do
5
5
  kicker = Kicker.new({})
6
+
7
+ kicker.expects(:log).with('')
6
8
  kicker.expects(:log).with("Could not handle: /file/1, /file/2")
7
- Kicker.callback_chain.last.call(kicker, %w{ /file/1 /file/2 })
9
+ kicker.expects(:log).with('')
10
+
11
+ Kicker.post_process_chain.last.call(kicker, %w{ /file/1 /file/2 })
8
12
  end
9
13
  end
@@ -2,19 +2,19 @@ require File.expand_path('../../test_helper', __FILE__)
2
2
 
3
3
  describe "Kicker, concerning the `execute a command-line' callback" do
4
4
  it "should parse the command and add the callback" do
5
- before = Kicker.callback_chain.length
5
+ before = Kicker.process_chain.length
6
6
 
7
7
  Kicker.parse_options(%w{ -e ls })
8
- Kicker.callback_chain.length.should == before + 1
8
+ Kicker.process_chain.length.should == before + 1
9
9
 
10
10
  Kicker.parse_options(%w{ --execute ls })
11
- Kicker.callback_chain.length.should == before + 2
11
+ Kicker.process_chain.length.should == before + 2
12
12
  end
13
13
 
14
14
  it "should call execute_command with the given command" do
15
15
  Kicker.parse_options(%w{ -e ls })
16
16
 
17
- callback = Kicker.callback_chain.first
17
+ callback = Kicker.process_chain.last
18
18
  callback.should.be.instance_of Proc
19
19
 
20
20
  kicker = Kicker.new({})
data/test/utils_test.rb CHANGED
@@ -17,19 +17,19 @@ describe "A Kicker instance, concerning its utility methods" do
17
17
  it "should log the output of the command indented by 2 spaces and whether or not the command succeeded" do
18
18
  @kicker.stubs(:`).returns("line 1\nline 2")
19
19
 
20
- @kicker.expects(:log).with('Change occured. Executing command:')
20
+ @kicker.expects(:log).with('Change occured, executing command: ls')
21
21
  @kicker.expects(:log).with(' line 1')
22
22
  @kicker.expects(:log).with(' line 2')
23
23
  @kicker.expects(:log).with('Command succeeded')
24
- @kicker.execute_command('')
24
+ @kicker.execute_command('ls')
25
25
 
26
26
  @kicker.stubs(:last_command_succeeded?).returns(false)
27
27
  @kicker.stubs(:last_command_status).returns(123)
28
- @kicker.expects(:log).with('Change occured. Executing command:')
28
+ @kicker.expects(:log).with('Change occured, executing command: ls')
29
29
  @kicker.expects(:log).with(' line 1')
30
30
  @kicker.expects(:log).with(' line 2')
31
31
  @kicker.expects(:log).with('Command failed (123)')
32
- @kicker.execute_command('')
32
+ @kicker.execute_command('ls')
33
33
  end
34
34
 
35
35
  it "should send the Growl messages with the default click callback" do
@@ -40,15 +40,15 @@ describe "A Kicker instance, concerning its utility methods" do
40
40
 
41
41
  OSX::NSWorkspace.sharedWorkspace.expects(:launchApplication).with('Terminal').times(2)
42
42
 
43
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
43
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured, executing command:', 'ls')
44
44
  @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:succeeded], 'Kicker: Command succeeded', "line 1\nline 2").yields
45
- @kicker.execute_command('')
45
+ @kicker.execute_command('ls')
46
46
 
47
47
  @kicker.stubs(:last_command_succeeded?).returns(false)
48
48
  @kicker.stubs(:last_command_status).returns(123)
49
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
49
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured, executing command:', 'ls')
50
50
  @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:failed], 'Kicker: Command failed (123)', "line 1\nline 2").yields
51
- @kicker.execute_command('')
51
+ @kicker.execute_command('ls')
52
52
  end
53
53
 
54
54
  it "should send the Growl messages with a click callback which executes the specified growl command when succeeded" do
@@ -61,14 +61,14 @@ describe "A Kicker instance, concerning its utility methods" do
61
61
  @kicker.expects(:system).with('ls -l').times(1)
62
62
  OSX::NSWorkspace.sharedWorkspace.expects(:launchApplication).with('Terminal').times(1)
63
63
 
64
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
64
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured, executing command:', 'ls')
65
65
  @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:succeeded], 'Kicker: Command succeeded', "line 1\nline 2").yields
66
- @kicker.execute_command('')
66
+ @kicker.execute_command('ls')
67
67
 
68
68
  @kicker.stubs(:last_command_succeeded?).returns(false)
69
69
  @kicker.stubs(:last_command_status).returns(123)
70
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
70
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured, executing command:', 'ls')
71
71
  @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:failed], 'Kicker: Command failed (123)', "line 1\nline 2").yields
72
- @kicker.execute_command('')
72
+ @kicker.execute_command('ls')
73
73
  end
74
74
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alloy-kicker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eloy Duran
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-25 00:00:00 -07:00
12
+ date: 2009-06-27 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -27,6 +27,7 @@ files:
27
27
  - LICENSE
28
28
  - README.rdoc
29
29
  - Rakefile
30
+ - TODO.rdoc
30
31
  - VERSION.yml
31
32
  - bin/kicker
32
33
  - lib/kicker.rb