billy_the_tool 0.1.2

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/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 4pcbr
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Billy the tool
2
+
3
+ ## Commands
4
+
5
+ ### Setup existing project
6
+ * billy hello
7
+ * billy eat {billy_config}
8
+
9
+ ### Deploy existing project
10
+ * billy walk {app_name}
11
+
12
+ ## Sample config file
13
+
14
+ ```
15
+ deploy_to: /var/web/
16
+ user: user
17
+ server: 192.168.216.93
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ cd ~
24
+ billy eat http://192.168.216.93/billy.cfg
25
+ cd project_name
26
+ billy walk project_name
27
+ open http://192.168.216.93/project_name/index.html
28
+ ```
data/bin/billy ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ require 'billy'
6
+
7
+ Billy::Session.run!( ARGV )
@@ -0,0 +1,36 @@
1
+ require 'billy/commands'
2
+
3
+ class Billy
4
+ class Commands
5
+ class Command
6
+
7
+ def name
8
+ self.class.to_s.split( "::" ).last.downcase
9
+ end
10
+
11
+ protected
12
+
13
+ def initialize
14
+ end
15
+
16
+ def get_confirmation
17
+ gets.chomp.downcase == "y"
18
+ end
19
+
20
+ class << self
21
+
22
+ attr_accessor :_instance
23
+
24
+ def instance
25
+ self._instance ||= self.new
26
+ end
27
+
28
+ def register_self!
29
+ Billy::Commands.register_command!( instance )
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ require 'billy/commands/command'
2
+
3
+ class Billy
4
+ class Commands
5
+ class Config < Command
6
+
7
+ def proceed!( arguments = nil )
8
+ dir = Dir.pwd
9
+ if !Billy::Config.config_exists?( dir )
10
+ print "Billy config file could not be found here. Say Billy hello."
11
+ exit 1
12
+ else
13
+ Billy::Config.load!( dir )
14
+ print "Current Billy settings: \n"
15
+ Billy::Config.settings.each_pair do |k, v|
16
+ print "#{k}:\t\t\t#{v}\n"
17
+ end
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+
25
+ Billy::Commands::Config.register_self!
@@ -0,0 +1,68 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'billy/commands/command'
4
+
5
+ class Billy
6
+ class Commands
7
+ class Eat < Command
8
+
9
+ def proceed!( arguments )
10
+ ( path = arguments.shift ) unless arguments.nil?
11
+ begin
12
+ if path.nil? || path.empty?
13
+ raise "No config path given. \nYou have to provide some settings path, e.g. in your local filesystem or direct link to blob file in repository etc.\n"
14
+ elsif uri?( path ) && ( result = load_remote_config( path ) ).nil?
15
+ raise "Remote config file could not be loaded"
16
+ elsif file?( path ) && ( !File.exists?( path ) || ( result = File.read( path ) ).nil? )
17
+ raise "Config File not found: #{path}"
18
+ end
19
+ rescue Exception => e
20
+ print e.message
21
+ exit 1
22
+ end
23
+ eat_config( result )
24
+ save_config
25
+ end
26
+
27
+ def eat_config( config_string )
28
+ Billy::Config.instance.eat_string_config( config_string )
29
+ end
30
+
31
+ def save_config
32
+ print "Parsed config data:\n"
33
+ Billy::Config.settings.each_pair do |k, v|
34
+ print "#{k}: #{v}\n"
35
+ end
36
+ print "Save this settings?(y/n): "
37
+ confirm = get_confirmation
38
+ if !confirm
39
+ print "Billy didn't save config file. Skipping\n"
40
+ exit 1
41
+ end
42
+ Billy::Config.save
43
+ print "Billy saved config file to #{Billy::Config::BILLYRC}\n"
44
+ end
45
+
46
+ def uri?( str )
47
+ begin
48
+ uri = URI.parse( str )
49
+ %w( http https ).include?( uri.scheme )
50
+ rescue
51
+ false
52
+ end
53
+ end
54
+
55
+ def file?( str )
56
+ !uri?( str )
57
+ end
58
+
59
+ def load_remote_config( path )
60
+ uri = URI( path )
61
+ Net::HTTP.get( uri )
62
+ end
63
+
64
+ end
65
+ end
66
+ end
67
+
68
+ Billy::Commands::Eat.register_self!
@@ -0,0 +1,79 @@
1
+ require 'billy/commands/command'
2
+
3
+ class Billy
4
+ class Commands
5
+ class Hello < Command
6
+
7
+ def proceed!( arguments = nil )
8
+ billy_say_hello
9
+ path = get_init_path( arguments )
10
+ config = Billy::Config.instance
11
+ config.save!( path, true )
12
+ if !ssh_command_exists?
13
+ suggest_install_ssh
14
+ exit 1
15
+ end
16
+ offer_ssh_keygen unless ssh_key_exists?
17
+ print "All done!\n"
18
+ end
19
+
20
+ def billy_say_hello
21
+ print "Hi! I'm Billy, simple deploy tool.\n"
22
+ print "Usage:\n"
23
+ print " * billy hello (path) -- init billy in {path} folder. Inites in current if no one given.\n"
24
+ print " * billy eat {cfg_path} -- parse and save billy config in current folder. {cfg_path} here means remote file url or local one.\n"
25
+ print " * billy walk {application_name} -- deploy HEAD version in repository to remote server.\n"
26
+ end
27
+
28
+ def ssh_command_exists?
29
+ res = true
30
+ %w(ssh ssh-keygen).each do |cmd|
31
+ res &= system( "which #{cmd} 2>&1 > /dev/null" )
32
+ end
33
+ res
34
+ end
35
+
36
+ def ssh_key_exists?
37
+ ssh_root_path = File.expand_path( "~/.ssh" )
38
+ res = true
39
+ res &= File.exists?( ssh_root_path )
40
+ res &= File.directory?( ssh_root_path )
41
+ res &= Dir[ ssh_root_path + "/*.pub" ].any?
42
+ res
43
+ end
44
+
45
+ def suggest_install_ssh
46
+ print "Billy wants you to install ssh command. Please do it first.\n"
47
+ end
48
+
49
+ def offer_ssh_keygen
50
+ print "Billy did not find your ssh key. Would you like to create it now?(y/n): "
51
+ confirm = get_confirmation
52
+ if !confirm
53
+ print "Ssh key should be generated before we continue. Please generate it.\n"
54
+ exit 1
55
+ end
56
+ enc_type = 'rsa'
57
+ print "Billy creates ssh keys for you...\n"
58
+ system "ssh-keygen -t #{enc_type} -N '' -f ~/.ssh/id_#{enc_type}"
59
+ print "All done!"
60
+ end
61
+
62
+ def get_init_path( arguments )
63
+ ( path = arguments.shift ) unless arguments.nil?
64
+ if path.nil? || path.empty?
65
+ print "Billy will be inited in current directory. Proceed?(y/n): "
66
+ confirm = get_confirmation
67
+ if !confirm
68
+ print "Billy has nothing to do. Bye-bye."
69
+ end
70
+ path = Dir.pwd
71
+ end
72
+ File.expand_path( path )
73
+ end
74
+
75
+ end
76
+ end
77
+ end
78
+
79
+ Billy::Commands::Hello.register_self!
@@ -0,0 +1,130 @@
1
+ require 'billy/commands/command'
2
+ require 'capistrano'
3
+
4
+ class Billy
5
+ class Commands
6
+ class Walk < Command
7
+
8
+ GIT_PATH = ".git"
9
+
10
+ def proceed!( arguments = nil )
11
+
12
+ if !Billy::Config.load
13
+ print "Billy config could not be found. Say Billy hello."
14
+ exit 1
15
+ end
16
+
17
+ destination = ( arguments.shift rescue nil ) || Billy::Config.instance.destination
18
+
19
+ if destination.nil?
20
+ print "Billy doesn't know where to walk."
21
+ print "You have to provide deploy folder, e.g.: billy walk my_super_project.\n"
22
+ exit 1
23
+ end
24
+
25
+ cap = prepare_capistrano( destination )
26
+ %w(deploy:setup deploy).each do |command|
27
+ cap.find_and_execute_task(command, :before => :start, :after => :finish)
28
+ end
29
+ cap.trigger( :exit )
30
+
31
+ print "All done! Billy is a clever boy!\n"
32
+ end
33
+
34
+ def prepare_capistrano( destination )
35
+
36
+ cap = Capistrano::Configuration.new
37
+ cap.load "standard"
38
+ cap.load "deploy"
39
+ cap.trigger( :load )
40
+ config = Billy::Config.instance
41
+
42
+ if config.deploy_to.nil?
43
+ print "Billy doesn't know remote deploy path. Please provide it in config under deploy_to key.\n"
44
+ exit 1
45
+ end
46
+
47
+ cap.set :scm, "git"
48
+ cap.set :use_sudo, false
49
+ cap.set :deploy_via, :remote_cache
50
+ cap.set :application, destination
51
+ cap.set :deploy_to, File.join( config.deploy_to, destination )
52
+ cap.server config.server, :app, :web, :db, :primary => true
53
+ cap.set :user, config.user
54
+
55
+ repository = config.repository || get_repository_path
56
+
57
+ cap.set :repository, repository
58
+ cap.set :normalize_asset_timestamps, false
59
+
60
+ cap.namespace :deploy do
61
+ cap.task :start, :roles => :app do; end
62
+ cap.task :stop, :roles => :app do; end
63
+ cap.task :restart, :roles => :app do; end
64
+ end
65
+
66
+ cap
67
+ end
68
+
69
+ def get_repository_path
70
+ if !local_repository_exists?
71
+ print "Git repository was not created. Do you want Billy to create it now?\n"
72
+ confirm = get_confirmation
73
+ if !confirm
74
+ print "Billy could not proceed without git repository.\n"
75
+ exit 1
76
+ else
77
+ print "Creating git repository.\n"
78
+ init_git_repository
79
+ end
80
+ end
81
+
82
+ config = get_git_config
83
+
84
+ if !remote_repository_exists?( config )
85
+ print "Billy could not find remote repository for your project.\n"
86
+ print "Please add some remote, e.g. git remote add git@github.com:myUsername/myProject.git.\n"
87
+ exit 1
88
+ end
89
+
90
+ idx = 1
91
+ i = 0
92
+
93
+ match_data = get_remotes( config )
94
+
95
+ if match_data.length > 1
96
+ print "Billy found several remotes in repository. Choose one to deploy from:"
97
+ match_data.each do |remote|
98
+ i += 1
99
+ print "#{i}: #{remote[0]}\t\t#{remote[1]}\n"
100
+ end
101
+ while ( idx = gets.chomp.to_i ) > match_data.length; end
102
+ end
103
+ match_data[ idx - 1 ][ 1 ]
104
+ end
105
+
106
+ def get_git_config
107
+ File.read( File.expand_path( GIT_PATH + "/config" ) )
108
+ end
109
+
110
+ def local_repository_exists?
111
+ File.exists?( File.expand_path( GIT_PATH ) )
112
+ end
113
+
114
+ def remote_repository_exists?( config )
115
+ get_remotes( config ).any?
116
+ end
117
+
118
+ def get_remotes( config )
119
+ config.gsub( /\n/, ';' ).scan /\[remote\s+\"([^"]+)\"\][^\[]+url\s*=\s*([^;]+)/
120
+ end
121
+
122
+ def init_git_repository
123
+ system "git init ."
124
+ end
125
+
126
+ end
127
+ end
128
+ end
129
+
130
+ Billy::Commands::Walk.register_self!
@@ -0,0 +1,24 @@
1
+ class Billy
2
+ class Commands
3
+
4
+ class << self
5
+
6
+ attr_accessor :pool
7
+
8
+ def load_pool!
9
+ self.pool ||= Hash.new
10
+ commands_path = File.expand_path( File.dirname(__FILE__) + '/commands/**/*.rb' )
11
+ Dir[ commands_path ].each do |file|
12
+ require file
13
+ end
14
+ end
15
+
16
+ def register_command!( command )
17
+ load_pool! unless !pool.nil?
18
+ ( pool[ command.name ] = command ) unless pool.values.include?( command )
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,117 @@
1
+ class Billy
2
+ class Config
3
+
4
+ BILLYRC = '.billyrc'
5
+ SEPARATOR = ': '
6
+
7
+ attr_accessor :storage
8
+ attr_accessor :storage_path
9
+
10
+ def method_missing( m, *args, &block )
11
+ if m.to_s[ /=$/ ].nil?
12
+ self.storage[ m.to_s ]
13
+ else
14
+ key = ( m.to_s )[ /^([^=]+)/ ]
15
+ val = args.shift
16
+ ( self.storage[ key ] = val ) unless key.nil? && key.empty?
17
+ end
18
+ end
19
+
20
+ def config_exists?( dir )
21
+ File.exists?( File.expand_path( dir + "/#{BILLYRC}" ) )
22
+ end
23
+
24
+ def to_s
25
+ [].tap { |res|
26
+ self.storage.each_pair do |k, v|
27
+ res.push "#{k}#{SEPARATOR}#{v}"
28
+ end
29
+ }.push( "" ).join( "\n" )
30
+ end
31
+
32
+ def load
33
+ %w(. ~).each do |path|
34
+ begin
35
+ load!( File.expand_path( path ) )
36
+ return true
37
+ rescue
38
+ next
39
+ end
40
+ end
41
+
42
+ false
43
+ end
44
+
45
+ def load!( dir )
46
+ self.storage_path = File.expand_path( dir )
47
+ file_path = storage_path + "/#{BILLYRC}"
48
+ raise "Config was not found in #{path}" unless File.exists?( file_path )
49
+ eat_string_config( File.read( file_path ) )
50
+ end
51
+
52
+ def clear
53
+ self.storage.clear
54
+ end
55
+
56
+ def reload!( dir = nil )
57
+ dir ||= storage_path
58
+ clear
59
+ load!( dir )
60
+ end
61
+
62
+ def save( dir = nil )
63
+ dir ||= Dir.pwd
64
+ begin
65
+ save!( dir, true )
66
+ true
67
+ rescue
68
+ false
69
+ end
70
+ end
71
+
72
+ def save!( dir, force = false )
73
+ raise 'Directory name should not be empty' unless !dir.empty?
74
+ raise "Directory #{dir.to_s} doesn't exist" unless File.exist?( File.expand_path( dir ) )
75
+ billyrc_path = File.expand_path( dir + "/#{BILLYRC}" )
76
+ raise "Config already exists in #{billyrc_path}" unless !File.exists?( billyrc_path ) || force
77
+ File.open( billyrc_path, 'w' ) { |file|
78
+ file.flush
79
+ file.write( self.to_s )
80
+ }
81
+ end
82
+
83
+ def eat_string_config( string_config )
84
+ clear
85
+ string_config.each_line do |line|
86
+ next unless !line.empty?
87
+ items = line.split( SEPARATOR )
88
+ k = items.shift
89
+ v = items.join( SEPARATOR ).strip
90
+ ( self.storage[ k.to_s ] = v ) unless k.nil? || k.empty? || v.nil? || v.empty?
91
+ end
92
+ end
93
+
94
+ protected
95
+
96
+ def initialize
97
+ self.storage = Hash.new
98
+ end
99
+
100
+ class << self
101
+
102
+ def instance
103
+ @@instance ||= self.new
104
+ end
105
+
106
+ def settings
107
+ instance.storage
108
+ end
109
+
110
+ def method_missing( m, *args, &block )
111
+ instance.send( m, *args, &block ) if instance.respond_to?( m )
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,29 @@
1
+ class Billy
2
+ class Session
3
+ class << self
4
+
5
+ def run!( args )
6
+ Billy::Commands.load_pool!
7
+ command = ARGV.shift
8
+ arguments = ARGV
9
+ if command.nil?
10
+ print "Billy has nothing to do. Yay!\n"
11
+ exit 0
12
+ end
13
+ status = proceed_command!( command, arguments )
14
+ exit status
15
+ end
16
+
17
+ def proceed_command!( command_name, arguments )
18
+ cmd = Billy::Commands.pool[ command_name.to_s ]
19
+ if cmd.nil?
20
+ print "Billy doesn't know this command: #{command_name}. Say billy hello.\n"
21
+ return 1
22
+ end
23
+ cmd.proceed!( arguments )
24
+ 0
25
+ end
26
+
27
+ end
28
+ end
29
+ end
data/lib/billy.rb ADDED
@@ -0,0 +1,7 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ class Billy
4
+ require 'billy/session'
5
+ require 'billy/commands'
6
+ require 'billy/config'
7
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: billy_the_tool
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 2
10
+ version: 0.1.2
11
+ platform: ruby
12
+ authors:
13
+ - 4pcbr
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-11-29 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ hash: 49
28
+ segments:
29
+ - 2
30
+ - 13
31
+ - 5
32
+ version: 2.13.5
33
+ type: :runtime
34
+ name: capistrano
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ name: shoulda
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ hash: 31
58
+ segments:
59
+ - 3
60
+ - 12
61
+ version: "3.12"
62
+ type: :development
63
+ name: rdoc
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ prerelease: false
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ~>
71
+ - !ruby/object:Gem::Version
72
+ hash: 27
73
+ segments:
74
+ - 1
75
+ - 2
76
+ - 2
77
+ version: 1.2.2
78
+ type: :development
79
+ name: bundler
80
+ version_requirements: *id004
81
+ - !ruby/object:Gem::Dependency
82
+ prerelease: false
83
+ requirement: &id005 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ~>
87
+ - !ruby/object:Gem::Version
88
+ hash: 63
89
+ segments:
90
+ - 1
91
+ - 8
92
+ - 4
93
+ version: 1.8.4
94
+ type: :development
95
+ name: jeweler
96
+ version_requirements: *id005
97
+ - !ruby/object:Gem::Dependency
98
+ prerelease: false
99
+ requirement: &id006 !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ type: :development
109
+ name: rcov
110
+ version_requirements: *id006
111
+ - !ruby/object:Gem::Dependency
112
+ prerelease: false
113
+ requirement: &id007 !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ~>
117
+ - !ruby/object:Gem::Version
118
+ hash: 63
119
+ segments:
120
+ - 2
121
+ - 12
122
+ - 0
123
+ version: 2.12.0
124
+ type: :development
125
+ name: rspec
126
+ version_requirements: *id007
127
+ - !ruby/object:Gem::Dependency
128
+ prerelease: false
129
+ requirement: &id008 !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ~>
133
+ - !ruby/object:Gem::Version
134
+ hash: 49
135
+ segments:
136
+ - 2
137
+ - 13
138
+ - 5
139
+ version: 2.13.5
140
+ type: :runtime
141
+ name: capistrano
142
+ version_requirements: *id008
143
+ description: Billy is simplified deploy system based on top of capistrano
144
+ email: me@4pcbr.com
145
+ executables:
146
+ - billy
147
+ extensions: []
148
+
149
+ extra_rdoc_files:
150
+ - LICENSE.txt
151
+ - README.md
152
+ files:
153
+ - bin/billy
154
+ - lib/billy.rb
155
+ - lib/billy/commands.rb
156
+ - lib/billy/commands/command.rb
157
+ - lib/billy/commands/config.rb
158
+ - lib/billy/commands/eat.rb
159
+ - lib/billy/commands/hello.rb
160
+ - lib/billy/commands/walk.rb
161
+ - lib/billy/config.rb
162
+ - lib/billy/session.rb
163
+ - LICENSE.txt
164
+ - README.md
165
+ homepage: http://github.com/AltSpace/billy
166
+ licenses:
167
+ - MIT
168
+ post_install_message:
169
+ rdoc_options: []
170
+
171
+ require_paths:
172
+ - lib
173
+ required_ruby_version: !ruby/object:Gem::Requirement
174
+ none: false
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ hash: 3
179
+ segments:
180
+ - 0
181
+ version: "0"
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ none: false
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ hash: 3
188
+ segments:
189
+ - 0
190
+ version: "0"
191
+ requirements: []
192
+
193
+ rubyforge_project:
194
+ rubygems_version: 1.8.24
195
+ signing_key:
196
+ specification_version: 3
197
+ summary: Billy the tool
198
+ test_files: []
199
+