cult 0.1.1.pre
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 +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +240 -0
- data/Rakefile +6 -0
- data/cult +1 -0
- data/cult.gemspec +38 -0
- data/doc/welcome.txt +1 -0
- data/exe/cult +86 -0
- data/lib/cult/artifact.rb +45 -0
- data/lib/cult/cli/common.rb +265 -0
- data/lib/cult/cli/console_cmd.rb +124 -0
- data/lib/cult/cli/cri_extensions.rb +84 -0
- data/lib/cult/cli/init_cmd.rb +116 -0
- data/lib/cult/cli/load.rb +26 -0
- data/lib/cult/cli/node_cmd.rb +205 -0
- data/lib/cult/cli/provider_cmd.rb +123 -0
- data/lib/cult/cli/role_cmd.rb +149 -0
- data/lib/cult/cli/task_cmd.rb +140 -0
- data/lib/cult/commander.rb +103 -0
- data/lib/cult/config.rb +22 -0
- data/lib/cult/definition.rb +112 -0
- data/lib/cult/driver.rb +88 -0
- data/lib/cult/drivers/common.rb +192 -0
- data/lib/cult/drivers/digital_ocean_driver.rb +179 -0
- data/lib/cult/drivers/linode_driver.rb +282 -0
- data/lib/cult/drivers/load.rb +26 -0
- data/lib/cult/drivers/script_driver.rb +27 -0
- data/lib/cult/drivers/vultr_driver.rb +217 -0
- data/lib/cult/named_array.rb +129 -0
- data/lib/cult/node.rb +62 -0
- data/lib/cult/project.rb +169 -0
- data/lib/cult/provider.rb +134 -0
- data/lib/cult/role.rb +213 -0
- data/lib/cult/skel.rb +85 -0
- data/lib/cult/task.rb +64 -0
- data/lib/cult/template.rb +92 -0
- data/lib/cult/transferable.rb +61 -0
- data/lib/cult/version.rb +3 -0
- data/lib/cult.rb +4 -0
- data/skel/.cultconsolerc +4 -0
- data/skel/.cultrc.erb +29 -0
- data/skel/README.md.erb +22 -0
- data/skel/keys/.keep +0 -0
- data/skel/nodes/.keep +0 -0
- data/skel/providers/.keep +0 -0
- data/skel/roles/all/role.json +4 -0
- data/skel/roles/all/tasks/00000-do-something-cool +27 -0
- data/skel/roles/bootstrap/files/cult-motd +45 -0
- data/skel/roles/bootstrap/role.json +4 -0
- data/skel/roles/bootstrap/tasks/00000-set-hostname +22 -0
- data/skel/roles/bootstrap/tasks/00001-add-cult-user +21 -0
- data/skel/roles/bootstrap/tasks/00002-install-cult-motd +9 -0
- metadata +183 -0
@@ -0,0 +1,265 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
module Cult
|
5
|
+
module CLI
|
6
|
+
|
7
|
+
class CLIError < RuntimeError
|
8
|
+
end
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
# This sets the global project based on a directory
|
13
|
+
def set_project(path)
|
14
|
+
Cult.project = Cult::Project.locate(path)
|
15
|
+
if Cult.project.nil?
|
16
|
+
fail CLIError, "#{path} does not contain a valid Cult project"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# Quiet mode controls how verbose `say` is
|
22
|
+
def quiet=(v)
|
23
|
+
@quiet = v
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def quiet?
|
28
|
+
@quiet
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def say(v)
|
33
|
+
puts v unless @quiet
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# yes=true automatically answers yes to "yes_no" questions.
|
38
|
+
def yes=(v)
|
39
|
+
@yes = v
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def yes?
|
44
|
+
@yes
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# Asks a yes or no question with promp. The prompt defaults to "Yes". If
|
49
|
+
# Cli.yes=true, true is returned without showing the prompt.
|
50
|
+
def yes_no?(prompt, default: true)
|
51
|
+
return true if yes?
|
52
|
+
|
53
|
+
default = case default
|
54
|
+
when :y, :yes
|
55
|
+
true
|
56
|
+
when :n, :no
|
57
|
+
false
|
58
|
+
when true, false
|
59
|
+
default
|
60
|
+
else
|
61
|
+
fail ArgumentError, "invalid :default"
|
62
|
+
end
|
63
|
+
|
64
|
+
loop do
|
65
|
+
y = default ? Rainbow('Y').bright : Rainbow('y').darkgray
|
66
|
+
n = !default ? Rainbow('N').bright : Rainbow('n').darkgray
|
67
|
+
|
68
|
+
begin
|
69
|
+
print "#{prompt} #{y}/#{n}: "
|
70
|
+
case $stdin.gets.chomp
|
71
|
+
when ''
|
72
|
+
return default
|
73
|
+
when /^[Yy]/
|
74
|
+
return true
|
75
|
+
when /^[Nn]/
|
76
|
+
return false
|
77
|
+
else
|
78
|
+
$stderr.puts "Unrecognized response"
|
79
|
+
end
|
80
|
+
rescue Interrupt
|
81
|
+
puts
|
82
|
+
raise
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
# Asks the user a question, and returns the response. Ensures a newline
|
89
|
+
# exists after the response.
|
90
|
+
def ask(prompt)
|
91
|
+
print "#{prompt}: "
|
92
|
+
$stdin.gets.chomp
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def prompt(*args)
|
97
|
+
ask(*args)
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
# Disables echo to ask the user a password.
|
102
|
+
def password(prompt)
|
103
|
+
STDIN.noecho do
|
104
|
+
begin
|
105
|
+
ask(prompt)
|
106
|
+
ensure
|
107
|
+
puts
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# it's common for drivers to need the user to visit a URL to
|
114
|
+
# confirm an API key or similar. This does this in the most
|
115
|
+
# compatable way I know.
|
116
|
+
def launch_browser(url)
|
117
|
+
case RUBY_PLATFORM
|
118
|
+
when /darwin/
|
119
|
+
system "open", url
|
120
|
+
when /mswin|mingw|cygwin/
|
121
|
+
system "start", url
|
122
|
+
else
|
123
|
+
system "xdg-open", url
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
# We actually want "base 47", so we have to generate substantially more
|
129
|
+
# characters than len. The method already generates 1.25*len characters,
|
130
|
+
# but is offset by _ and - that we discard. With the other characters we
|
131
|
+
# discard, we usethe minimum multiplier which makes a retry "rare" (every
|
132
|
+
# few thousand ids at 6 len), then handle that case.
|
133
|
+
def unique_id(len = 8)
|
134
|
+
@uniq_id_disallowed ||= /[^abcdefhjkmnpqrtvwxyzABCDEFGHJKMNPQRTVWXY2346789]/
|
135
|
+
candidate = SecureRandom.urlsafe_base64((len * 2.1).ceil)
|
136
|
+
.gsub(@uniq_id_disallowed, '')
|
137
|
+
fail RangeError if candidate.size < len
|
138
|
+
candidate[0...len]
|
139
|
+
rescue RangeError
|
140
|
+
retry
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
# v is an option or argv value from a user, label: is the name of it.
|
145
|
+
#
|
146
|
+
# This asserts that `v` is in the collection `from`, and returns it.
|
147
|
+
# if `exist` is false, it verifies that v is NOT in the collection and
|
148
|
+
# returns v.
|
149
|
+
#
|
150
|
+
# As a convenience, `from` can be a class like Role, which will imply
|
151
|
+
# 'Cult.project.roles'
|
152
|
+
#
|
153
|
+
# CLIError is raised if these invariants are violated
|
154
|
+
def fetch_item(v, from:, label: nil, exist: true, method: :fetch)
|
155
|
+
implied_from = case
|
156
|
+
when from == Driver; Cult::Drivers.all
|
157
|
+
when from == Provider; Cult.project.providers
|
158
|
+
when from == Role; Cult.project.roles
|
159
|
+
when from == Node; Cult.project.nodes
|
160
|
+
else; nil
|
161
|
+
end
|
162
|
+
|
163
|
+
label ||= implied_from ? from.name.split('::')[-1].downcase : nil
|
164
|
+
from = implied_from
|
165
|
+
|
166
|
+
fail ArgumentError, "label cannot be implied" if label.nil?
|
167
|
+
|
168
|
+
unless [:fetch, :all].include?(method)
|
169
|
+
fail ArgumentError, "method must be :fetch or :all"
|
170
|
+
end
|
171
|
+
|
172
|
+
# We got no argument
|
173
|
+
fail CLIError, "Expected #{label}" if v.nil?
|
174
|
+
|
175
|
+
if exist
|
176
|
+
begin
|
177
|
+
from.send(method, v).tap do |r|
|
178
|
+
# Make sure
|
179
|
+
fail KeyError if method == :all && r.empty?
|
180
|
+
end
|
181
|
+
rescue KeyError
|
182
|
+
fail CLIError, "#{label} does not exist: #{v}"
|
183
|
+
end
|
184
|
+
else
|
185
|
+
if from.key?(v)
|
186
|
+
fail CLIError, "#{label} already exists: #{v}"
|
187
|
+
end
|
188
|
+
v
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
# Takes a list of keys and returns an array of objects that correspond
|
194
|
+
# to any of them. If required is true, each key must correspond to at
|
195
|
+
# least one object.
|
196
|
+
def fetch_items(*keys, **kw)
|
197
|
+
keys.flatten.map do |key|
|
198
|
+
fetch_item(key, method: :all, **kw)
|
199
|
+
end.flatten
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
# This intercepts GemNeededError and does the installation dance. It looks
|
204
|
+
# a bit hairy because it has a few resumption points, e.g., attempts user
|
205
|
+
# gem install, and if that fails, tries the sudo gem install.
|
206
|
+
def offer_gem_install(&block)
|
207
|
+
prompt_install = ->(gems) do
|
208
|
+
unless quiet?
|
209
|
+
print <<~EOD
|
210
|
+
This driver requires the installation of one or more gems:
|
211
|
+
|
212
|
+
#{gems.inspect}
|
213
|
+
|
214
|
+
Cult can install them for you.
|
215
|
+
EOD
|
216
|
+
end
|
217
|
+
yes_no?("Install?")
|
218
|
+
end
|
219
|
+
|
220
|
+
try_install = ->(gem, sudo: false) do
|
221
|
+
cmd = "gem install #{Shellwords.escape(gem)}"
|
222
|
+
cmd = "sudo #{cmd}" if sudo
|
223
|
+
puts "executing: #{cmd}"
|
224
|
+
system cmd
|
225
|
+
$?.success?
|
226
|
+
end
|
227
|
+
|
228
|
+
begin
|
229
|
+
yield
|
230
|
+
rescue ::Cult::Driver::GemNeededError => needed
|
231
|
+
sudo = false
|
232
|
+
loop do
|
233
|
+
sudo = catch :sudo_attempt do
|
234
|
+
# We don't want to show this again on a retry
|
235
|
+
raise unless sudo || prompt_install.(needed.gems)
|
236
|
+
|
237
|
+
needed.gems.each do |gem|
|
238
|
+
success = try_install.(gem, sudo: sudo)
|
239
|
+
if !success
|
240
|
+
if sudo
|
241
|
+
puts "Nothing seemed to have worked. Giving up."
|
242
|
+
puts "The gems needed are #{needed.gems.inspect}."
|
243
|
+
raise
|
244
|
+
else
|
245
|
+
puts "It doesn't look like that went well."
|
246
|
+
if yes_no?("Retry with sudo?")
|
247
|
+
throw :sudo_attempt, true
|
248
|
+
end
|
249
|
+
raise
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# We exit our non-loop: Everything went fine.
|
255
|
+
break
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Everything went fine, we need to retry the user-supplied block.
|
260
|
+
Gem.refresh
|
261
|
+
retry
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'rainbow'
|
3
|
+
|
4
|
+
module Cult
|
5
|
+
module CLI
|
6
|
+
|
7
|
+
class ConsoleContext < SimpleDelegator
|
8
|
+
attr_accessor :original_argv
|
9
|
+
|
10
|
+
|
11
|
+
def initialize(project, argv)
|
12
|
+
super(project)
|
13
|
+
|
14
|
+
@original_argv = [$0, *argv]
|
15
|
+
ENV['CULT_PROJECT'] = self.path
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def load_rc
|
20
|
+
consolerc = project.location_of(".cultconsolerc")
|
21
|
+
|
22
|
+
# We don't `load' so the rc file has a more convenient context.
|
23
|
+
eval File.read(consolerc) if File.exist?(consolerc)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
private def exit(*)
|
28
|
+
# IRB tries to alias this. And it must be private, or it warns. WTF.
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Gives us an escape hatch to get the real, non-decorated object
|
34
|
+
def project
|
35
|
+
__getobj__
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def cult(*argv)
|
40
|
+
system $0, *argv
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def binding
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module_function
|
50
|
+
def console_cmd
|
51
|
+
Cri::Command.define do
|
52
|
+
name 'console'
|
53
|
+
summary 'Launch an REPL with you project loaded'
|
54
|
+
description <<~EOD.format_description
|
55
|
+
The Cult console loads your project, and starts a Ruby REPL. This can
|
56
|
+
be useful for troubleshooting, or just poking around the project.
|
57
|
+
|
58
|
+
A few convenience global variables are set to inspect.
|
59
|
+
EOD
|
60
|
+
|
61
|
+
flag :i, :irb, 'IRB (default)'
|
62
|
+
flag :r, :ripl, 'Ripl'
|
63
|
+
flag :p, :pry, 'Pry'
|
64
|
+
flag nil, :reexec, 'Console has been exec\'d for a reload'
|
65
|
+
|
66
|
+
run(arguments: 0) do |opts, args, cmd|
|
67
|
+
context = ConsoleContext.new(Cult.project, ARGV)
|
68
|
+
|
69
|
+
if opts[:reexec]
|
70
|
+
$stderr.puts "Reloaded."
|
71
|
+
else
|
72
|
+
$stderr.puts <<~EOD
|
73
|
+
|
74
|
+
Welcome to the #{Rainbow('Cult').green} Console.
|
75
|
+
|
76
|
+
Your project has been made accessible via 'project', and forwards
|
77
|
+
via 'self':
|
78
|
+
|
79
|
+
=> #{context.inspect}
|
80
|
+
|
81
|
+
Useful methods: nodes, roles, providers
|
82
|
+
|
83
|
+
EOD
|
84
|
+
end
|
85
|
+
|
86
|
+
context.load_rc
|
87
|
+
|
88
|
+
if opts[:ripl]
|
89
|
+
require 'ripl'
|
90
|
+
ARGV.clear
|
91
|
+
# Look, something reasonable:
|
92
|
+
Ripl.start(binding: context.binding)
|
93
|
+
|
94
|
+
elsif opts[:pry]
|
95
|
+
require 'pry'
|
96
|
+
context.binding.pry
|
97
|
+
else
|
98
|
+
# irb: This is ridiculous.
|
99
|
+
require 'irb'
|
100
|
+
ARGV.clear
|
101
|
+
IRB.setup(nil)
|
102
|
+
|
103
|
+
irb = IRB::Irb.new(IRB::WorkSpace.new(context.binding))
|
104
|
+
IRB.conf[:MAIN_CONTEXT] = irb.context
|
105
|
+
IRB.conf[:IRB_RC].call(irb.context) if IRB.conf[:IRB_RC]
|
106
|
+
|
107
|
+
trap("SIGINT") do
|
108
|
+
irb.signal_handle
|
109
|
+
end
|
110
|
+
|
111
|
+
begin
|
112
|
+
catch(:IRB_EXIT) do
|
113
|
+
irb.eval_input
|
114
|
+
end
|
115
|
+
ensure
|
116
|
+
IRB::irb_at_exit
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'cri'
|
2
|
+
|
3
|
+
module Cult
|
4
|
+
module CLI
|
5
|
+
class ::String
|
6
|
+
def format_description
|
7
|
+
self.gsub(/(\S)\n(\S)/m, '\1 \2')
|
8
|
+
.gsub(/\.[ ]{2}(\S)/m, '. \1')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
module CommandExtensions
|
14
|
+
def project_required?
|
15
|
+
defined?(@project_required) ? @project_required : true
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def project_required=(v)
|
20
|
+
@project_required = v
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
attr_accessor :argument_spec
|
25
|
+
|
26
|
+
|
27
|
+
# This function returns a wrapped version of the block passed to `run`
|
28
|
+
def block
|
29
|
+
lambda do |opts, args, cmd|
|
30
|
+
if project_required? && Cult.project.nil?
|
31
|
+
fail CLIError, "command '#{name}' requires a Cult project"
|
32
|
+
end
|
33
|
+
|
34
|
+
check_argument_spec!(args, argument_spec) if argument_spec
|
35
|
+
|
36
|
+
super.call(opts, args, cmd)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def check_argument_spec!(args, range)
|
42
|
+
range = (range..range) if range.is_a?(Integer)
|
43
|
+
if range.end == -1
|
44
|
+
range = range.begin .. Float::INFINITY
|
45
|
+
end
|
46
|
+
|
47
|
+
unless range.cover?(args.size)
|
48
|
+
msg = case
|
49
|
+
when range.size == 1 && range.begin == 0
|
50
|
+
"accepts no arguments"
|
51
|
+
when range.size == 1 && range.begin == 1
|
52
|
+
"accepts one argument"
|
53
|
+
when range.begin == range.end
|
54
|
+
"accepts exactly #{range.begin} arguments"
|
55
|
+
else
|
56
|
+
if range.end == Float::INFINITY
|
57
|
+
"requires #{range.begin}+ arguments"
|
58
|
+
else
|
59
|
+
"accepts #{range} arguments"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
fail CLIError, "Command #{msg}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Cri::Command.prepend(self)
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
module CommandDSLExtensions
|
71
|
+
def optional_project
|
72
|
+
@command.project_required = false
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def run(arguments: nil, &block)
|
77
|
+
@command.argument_spec = arguments if arguments
|
78
|
+
super(&block)
|
79
|
+
end
|
80
|
+
|
81
|
+
Cri::CommandDSL.prepend(self)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'cult/skel'
|
2
|
+
require 'cult/project'
|
3
|
+
require 'cult/drivers/load'
|
4
|
+
|
5
|
+
module Cult
|
6
|
+
module CLI
|
7
|
+
|
8
|
+
module_function
|
9
|
+
def init_cmd
|
10
|
+
Cri::Command.define do
|
11
|
+
drivers = Cult::Drivers.all.map{|d| d.driver_name }.join ", "
|
12
|
+
|
13
|
+
optional_project
|
14
|
+
name 'init'
|
15
|
+
aliases 'new'
|
16
|
+
usage 'init DIRECTORY'
|
17
|
+
summary 'Create a new Cult project'
|
18
|
+
description <<~EOD.format_description
|
19
|
+
Generates a new Cult project, based on a project skeleton.
|
20
|
+
|
21
|
+
The most useful option is --driver, which both specifies a driver and
|
22
|
+
sets up a provider of the same name. This will make sure the
|
23
|
+
dependencies for using the driver are install, and any bookkeeping
|
24
|
+
required to start interacting with your VPS provider is handled.
|
25
|
+
|
26
|
+
This usually involves entering an account name or getting an API key.
|
27
|
+
|
28
|
+
The default provider is "script", which isn't too pleasant, but has
|
29
|
+
no dependencies. The "script" driver manages your fleet by executing
|
30
|
+
scripts in $CULT_PROJECT/script, which you have to implement. This is
|
31
|
+
tedious, but very doable. However, if Cult knows about your provider,
|
32
|
+
it can handle all of this without you having to do anything.
|
33
|
+
|
34
|
+
Cult knows about the following providers:
|
35
|
+
|
36
|
+
> #{drivers}
|
37
|
+
|
38
|
+
The init process just gets you started, and it's nothing that couldn't
|
39
|
+
be accomplished by hand, so if you want to change anything later, it's
|
40
|
+
not a big deal.
|
41
|
+
|
42
|
+
The project generated sets up a pretty common configuration: an 'all'
|
43
|
+
role, a 'bootstrap' role, and a demo task that puts a colorful banner
|
44
|
+
in each node's MOTD.
|
45
|
+
EOD
|
46
|
+
|
47
|
+
required :d, :driver, 'Driver with which to create your provider'
|
48
|
+
required :p, :provider, 'Specify an explicit provider name'
|
49
|
+
flag :g, :git, 'Enable Git integration'
|
50
|
+
|
51
|
+
run(arguments: 1) do |opts, args, cmd|
|
52
|
+
project = Project.new(args[0])
|
53
|
+
if project.exist?
|
54
|
+
fail CLIError, "a Cult project already exists in #{project.path}"
|
55
|
+
end
|
56
|
+
|
57
|
+
project.git_integration = opts[:git]
|
58
|
+
|
59
|
+
driver_cls = if !opts[:provider] && !opts[:driver]
|
60
|
+
opts[:provider] ||= 'scripts'
|
61
|
+
CLI.fetch_item(opts[:provider], from: Driver)
|
62
|
+
elsif opts[:provider] && !opts[:driver]
|
63
|
+
CLI.fetch_item(opts[:provider], from: Driver)
|
64
|
+
elsif opts[:driver] && !opts[:provider]
|
65
|
+
CLI.fetch_item(opts[:driver], from: Driver).tap do |dc|
|
66
|
+
opts[:provider] = dc.driver_name
|
67
|
+
end
|
68
|
+
elsif opts[:driver]
|
69
|
+
CLI.fetch_item(opts[:driver], from: Driver)
|
70
|
+
end
|
71
|
+
|
72
|
+
fail CLIError, "Hmm, no driver class" if driver_cls.nil?
|
73
|
+
|
74
|
+
skel = Skel.new(project)
|
75
|
+
skel.copy!
|
76
|
+
|
77
|
+
provider_conf = {
|
78
|
+
name: opts[:provider],
|
79
|
+
driver: driver_cls.driver_name
|
80
|
+
}
|
81
|
+
|
82
|
+
CLI.offer_gem_install do
|
83
|
+
driver_conf = driver_cls.setup!
|
84
|
+
provider_conf.merge!(driver_conf)
|
85
|
+
|
86
|
+
|
87
|
+
provider_dir = File.join(project.location_of("providers"),
|
88
|
+
provider_conf[:name])
|
89
|
+
FileUtils.mkdir_p(provider_dir)
|
90
|
+
|
91
|
+
|
92
|
+
provider_file = File.join(provider_dir,
|
93
|
+
project.dump_name("provider"))
|
94
|
+
File.write(provider_file, project.dump_object(provider_conf))
|
95
|
+
|
96
|
+
|
97
|
+
defaults_file = File.join(provider_dir,
|
98
|
+
project.dump_name("defaults"))
|
99
|
+
defaults = Provider.generate_defaults(provider_conf)
|
100
|
+
File.write(defaults_file, project.dump_object(defaults))
|
101
|
+
end
|
102
|
+
|
103
|
+
if opts[:git]
|
104
|
+
Dir.chdir(project.path) do
|
105
|
+
`git init .`
|
106
|
+
`git add -A`
|
107
|
+
`git commit -m "[Cult] Created new project"`
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'cri'
|
2
|
+
require 'cult/cli/cri_extensions'
|
3
|
+
|
4
|
+
module Cult
|
5
|
+
module CLI
|
6
|
+
|
7
|
+
module_function
|
8
|
+
def load_commands!
|
9
|
+
Dir.glob(File.join(__dir__, "*_cmd.rb")).each do |file|
|
10
|
+
require file
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def commands
|
16
|
+
Cult::CLI.methods(false).select do |m|
|
17
|
+
m.to_s.match(/_cmd\z/)
|
18
|
+
end.map do |m|
|
19
|
+
Cult::CLI.send(m)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Cult::CLI.load_commands!
|