stable-cli-rails 0.6.9 → 0.7.10
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/bin/stable +0 -1
- data/lib/stable/cli.rb +39 -531
- data/lib/stable/commands/doctor.rb +39 -0
- data/lib/stable/commands/list.rb +54 -0
- data/lib/stable/commands/new.rb +16 -0
- data/lib/stable/commands/remove.rb +24 -0
- data/lib/stable/commands/restart.rb +15 -0
- data/lib/stable/commands/setup.rb +11 -0
- data/lib/stable/commands/start.rb +15 -0
- data/lib/stable/commands/stop.rb +15 -0
- data/lib/stable/config/paths.rb +9 -0
- data/lib/stable/registry.rb +12 -2
- data/lib/stable/services/app_creator.rb +162 -0
- data/lib/stable/services/app_registry.rb +86 -0
- data/lib/stable/services/app_remover.rb +21 -0
- data/lib/stable/services/app_restarter.rb +32 -0
- data/lib/stable/services/app_starter.rb +95 -0
- data/lib/stable/services/app_stopper.rb +17 -0
- data/lib/stable/services/caddy_manager.rb +155 -0
- data/lib/stable/services/database/base.rb +42 -0
- data/lib/stable/services/database/mysql.rb +36 -0
- data/lib/stable/services/database/postgres.rb +14 -0
- data/lib/stable/services/dependency_checker.rb +59 -0
- data/lib/stable/services/hosts_manager.rb +31 -0
- data/lib/stable/services/process_manager.rb +61 -0
- data/lib/stable/services/ruby.rb +102 -0
- data/lib/stable/services/setup_runner.rb +82 -0
- data/lib/stable/system/shell.rb +13 -0
- data/lib/stable/utils/prompts.rb +21 -0
- data/lib/stable.rb +19 -1
- metadata +53 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b8d5b304fbf560e181a416e2087e912c4fe2a26cd358341a263d1b890540069
|
|
4
|
+
data.tar.gz: 6e98c5d24b16f5669f442fab9c313a3ee74c790551a776e40e56773ed901e9d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41399496362c590019b87351d9e69364b41e6a3ecf0f10a7906a1c3edbaa508557104bd33cfb855623ee0b1e75e690b4d7834a42bd90025bfc5f23bc28dd2bf3
|
|
7
|
+
data.tar.gz: e4779489d26bd0b472a3a6f545039296bb7481aa87df1dbfa0748acd440f3e78b79f091b318956d6bc92aae8e7179f1f66393f9d800fff5b015a47bb3c2edc45
|
data/bin/stable
CHANGED
data/lib/stable/cli.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Stable
|
|
|
15
15
|
def initialize(*)
|
|
16
16
|
super
|
|
17
17
|
Stable::Bootstrap.run!
|
|
18
|
-
ensure_dependencies!
|
|
18
|
+
Services::SetupRunner.ensure_dependencies!
|
|
19
19
|
dedupe_registry!
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -32,174 +32,12 @@ module Stable
|
|
|
32
32
|
method_option :postgres, type: :boolean, default: false, desc: 'Use Postgres for the database'
|
|
33
33
|
method_option :mysql, type: :boolean, default: false, desc: 'Use MySQL for the database'
|
|
34
34
|
def new(name, ruby: RUBY_VERSION, rails: nil, port: nil)
|
|
35
|
-
|
|
36
|
-
app_path = File.expand_path(name)
|
|
37
|
-
|
|
38
|
-
# --- Add app to registry ---
|
|
39
|
-
domain = "#{name}.test"
|
|
40
|
-
apps = Registry.apps
|
|
41
|
-
|
|
42
|
-
app = {
|
|
43
|
-
name: name,
|
|
44
|
-
path: app_path,
|
|
45
|
-
domain: domain,
|
|
46
|
-
port: port,
|
|
47
|
-
ruby: ruby,
|
|
48
|
-
started_at: nil
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
apps.reject! { |a| a[:name] == name }
|
|
52
|
-
apps << app
|
|
53
|
-
|
|
54
|
-
abort "Folder already exists: #{app_path}" if File.exist?(app_path)
|
|
55
|
-
|
|
56
|
-
# --- Ensure RVM and Ruby ---
|
|
57
|
-
ensure_rvm!
|
|
58
|
-
puts "Using Ruby #{ruby} with RVM gemset #{name}..."
|
|
59
|
-
system("bash -lc 'rvm #{ruby}@#{name} --create do true'") or abort("Failed to create RVM gemset #{name}")
|
|
60
|
-
ruby_cmd = rvm_exec(app, ruby)
|
|
61
|
-
# --- Install Rails in gemset if needed ---
|
|
62
|
-
rails_version = rails || 'latest'
|
|
63
|
-
rails_check = system("bash -lc '#{ruby_cmd} gem list -i rails#{rails ? " -v #{rails}" : ''}'")
|
|
64
|
-
unless rails_check
|
|
65
|
-
puts "Installing Rails #{rails_version} in gemset..."
|
|
66
|
-
system("bash -lc '#{ruby_cmd} gem install rails #{rails ? "-v #{rails}" : ''}'") or abort('Failed to install Rails')
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# --- Create Rails app ---
|
|
70
|
-
puts "Creating Rails app #{name} (Ruby #{ruby})..."
|
|
71
|
-
system("bash -lc '#{ruby_cmd} rails new #{app_path}'") or abort('Rails app creation failed')
|
|
72
|
-
|
|
73
|
-
# --- Add .ruby-version and .ruby-gemset ---
|
|
74
|
-
Dir.chdir(app_path) do
|
|
75
|
-
File.write('.ruby-version', "#{ruby}\n")
|
|
76
|
-
File.write('.ruby-gemset', "#{name}\n")
|
|
77
|
-
|
|
78
|
-
# --- Install gems inside gemset ---
|
|
79
|
-
puts 'Running bundle install...'
|
|
80
|
-
system("bash -lc '#{ruby_cmd} bundle install --jobs=4 --retry=3'") or abort('bundle install failed')
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# --- Database integration ---
|
|
84
|
-
if options[:db]
|
|
85
|
-
adapter = if options[:postgres]
|
|
86
|
-
:postgresql
|
|
87
|
-
elsif options[:mysql]
|
|
88
|
-
:mysql
|
|
89
|
-
else
|
|
90
|
-
:postgresql
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
adapter == :postgresql ? 'postgresql' : 'mysql2'
|
|
94
|
-
gem_name = adapter == :postgresql ? 'pg' : 'mysql2'
|
|
95
|
-
|
|
96
|
-
gemfile_path = File.join(app_path, 'Gemfile')
|
|
97
|
-
unless File.read(gemfile_path).include?(gem_name)
|
|
98
|
-
File.open(gemfile_path, 'a') do |f|
|
|
99
|
-
f.puts "\n# Added by Stable CLI"
|
|
100
|
-
f.puts "gem '#{gem_name}'"
|
|
101
|
-
end
|
|
102
|
-
puts "✅ Added '#{gem_name}' gem to Gemfile"
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Ensure the gem is installed inside the gemset
|
|
106
|
-
system("bash -lc 'cd #{app_path} && rvm #{ruby}@#{name} do bundle install --jobs=4 --retry=3'") or abort('bundle install failed')
|
|
107
|
-
|
|
108
|
-
# --- Database setup ---
|
|
109
|
-
db = Stable::DBManager.new(options[:db], adapter: adapter)
|
|
110
|
-
creds = []
|
|
111
|
-
|
|
112
|
-
case adapter
|
|
113
|
-
when :postgresql
|
|
114
|
-
db.create
|
|
115
|
-
when :mysql
|
|
116
|
-
creds = create_mysql_db(options[:db]) # creates DB and returns creds
|
|
117
|
-
# Make sure mysql2 gem is loaded for Rails
|
|
118
|
-
system("bash -lc 'cd #{app_path} && rvm #{ruby}@#{name} do bundle exec gem list | grep mysql2'") || abort('mysql2 gem not found in gemset')
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# --- Generate database.yml ---
|
|
122
|
-
db_config_path = File.join(app_path, 'config', 'database.yml')
|
|
123
|
-
base_config = {
|
|
124
|
-
'adapter' => adapter == :postgresql ? 'postgresql' : 'mysql2',
|
|
125
|
-
'encoding' => adapter == :postgresql ? 'unicode' : 'utf8mb4',
|
|
126
|
-
'pool' => 5,
|
|
127
|
-
'database' => options[:db],
|
|
128
|
-
'username' => adapter == :mysql ? creds[:user] : nil,
|
|
129
|
-
'password' => adapter == :mysql ? creds[:password] : nil,
|
|
130
|
-
'host' => 'localhost',
|
|
131
|
-
'auth_plugin' => adapter == :mysql ? 'caching_sha2_password' : ''
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
db_configs = {
|
|
135
|
-
'default' => base_config,
|
|
136
|
-
'development' => base_config,
|
|
137
|
-
'test' => base_config.merge('database' => "#{options[:db]}_test"),
|
|
138
|
-
'production' => base_config.merge('database' => "#{options[:db]}_prod")
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
File.write(db_config_path, db_configs.to_yaml)
|
|
142
|
-
puts "✅ Database '#{db.name}' configured in Rails app"
|
|
143
|
-
|
|
144
|
-
# --- Prepare the database ---
|
|
145
|
-
puts 'Preparing database...'
|
|
146
|
-
system("bash -lc 'cd #{app_path} && rvm #{ruby}@#{name} do bundle exec rails db:prepare'") or abort('Database preparation failed')
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# --- Generate Caddyfile ---
|
|
150
|
-
|
|
151
|
-
puts 'Refreshing bundle environment...'
|
|
152
|
-
system(
|
|
153
|
-
"bash -lc 'cd #{app_path} && #{ruby_cmd} bundle check || #{ruby_cmd} bundle install'"
|
|
154
|
-
) or abort('Bundler setup failed')
|
|
155
|
-
|
|
156
|
-
puts 'Preparing database...'
|
|
157
|
-
system(
|
|
158
|
-
"bash -lc 'cd #{app_path} && #{ruby_cmd} bundle exec rails db:prepare'"
|
|
159
|
-
) or abort('Database preparation failed')
|
|
160
|
-
|
|
161
|
-
# --- Host entry & certificate ---
|
|
162
|
-
add_host_entry(domain)
|
|
163
|
-
generate_cert(domain) unless options[:skip_ssl]
|
|
164
|
-
update_caddyfile(domain, port)
|
|
165
|
-
ensure_caddy_running!
|
|
166
|
-
caddy_reload
|
|
167
|
-
|
|
168
|
-
# --- Start Rails server ---
|
|
169
|
-
puts "Starting Rails server for #{name} on port #{port}..."
|
|
170
|
-
log_file = File.join(app_path, 'log', 'stable.log')
|
|
171
|
-
FileUtils.mkdir_p(File.dirname(log_file))
|
|
172
|
-
|
|
173
|
-
abort "Port #{port} is already in use. Choose another port." if app_running?({ port: port })
|
|
174
|
-
|
|
175
|
-
pid = spawn(
|
|
176
|
-
'bash',
|
|
177
|
-
'-lc',
|
|
178
|
-
"cd #{app_path} && #{ruby_cmd} bundle exec rails s -p #{port} -b 127.0.0.1",
|
|
179
|
-
out: log_file,
|
|
180
|
-
err: log_file
|
|
181
|
-
)
|
|
182
|
-
Process.detach(pid)
|
|
183
|
-
|
|
184
|
-
app[:started_at] = Time.now.to_i
|
|
185
|
-
Registry.save(apps)
|
|
186
|
-
|
|
187
|
-
sleep 1.5
|
|
188
|
-
|
|
189
|
-
wait_for_port(port)
|
|
190
|
-
puts "✔ #{name} running at https://#{domain}"
|
|
35
|
+
Commands::New.new(name, options).call
|
|
191
36
|
end
|
|
192
37
|
|
|
193
38
|
desc 'list', 'List detected apps'
|
|
194
39
|
def list
|
|
195
|
-
|
|
196
|
-
if apps.empty?
|
|
197
|
-
puts 'No apps found.'
|
|
198
|
-
else
|
|
199
|
-
apps.each do |app|
|
|
200
|
-
puts "#{app[:name]} -> https://#{app[:domain]}"
|
|
201
|
-
end
|
|
202
|
-
end
|
|
40
|
+
Commands::List.new.call
|
|
203
41
|
end
|
|
204
42
|
|
|
205
43
|
desc 'add FOLDER', 'Add a Rails app folder'
|
|
@@ -212,175 +50,90 @@ module Stable
|
|
|
212
50
|
|
|
213
51
|
puts "Detected gemset: #{File.read('.ruby-gemset').strip}" if File.exist?('.ruby-gemset')
|
|
214
52
|
|
|
215
|
-
apps = Registry.apps
|
|
216
53
|
name = File.basename(folder)
|
|
217
54
|
domain = "#{name}.test"
|
|
218
55
|
|
|
219
|
-
if
|
|
56
|
+
if Services::AppRegistry.all.any? { |a| a[:path] == folder }
|
|
220
57
|
puts "App already exists: #{name}"
|
|
221
58
|
return
|
|
222
59
|
end
|
|
223
60
|
|
|
224
61
|
port = next_free_port
|
|
225
|
-
ruby = detect_ruby_version(folder)
|
|
62
|
+
ruby = Stable::Services::Ruby.detect_ruby_version(folder)
|
|
226
63
|
|
|
227
|
-
|
|
228
|
-
|
|
64
|
+
app = { name: name, path: folder, domain: domain, port: port, ruby: ruby }
|
|
65
|
+
Services::AppRegistry.add_app(app)
|
|
229
66
|
puts "Added #{name} -> https://#{domain} (port #{port})"
|
|
230
67
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
caddy_reload
|
|
68
|
+
Services::HostsManager.add(domain)
|
|
69
|
+
Services::CaddyManager.add_app(name, skip_ssl: options[:skip_ssl])
|
|
70
|
+
Services::CaddyManager.reload
|
|
235
71
|
end
|
|
236
72
|
|
|
237
73
|
desc 'remove NAME', 'Remove an app by name'
|
|
238
74
|
def remove(name)
|
|
239
|
-
|
|
240
|
-
app = apps.find { |a| a[:name] == name }
|
|
241
|
-
if app.nil?
|
|
242
|
-
puts "No app found with name #{name}"
|
|
243
|
-
return
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
new_apps = apps.reject { |a| a[:name] == name }
|
|
247
|
-
Registry.save(new_apps)
|
|
248
|
-
puts "Removed #{name}"
|
|
249
|
-
|
|
250
|
-
remove_host_entry(app[:domain])
|
|
251
|
-
remove_caddy_entry(app[:domain])
|
|
252
|
-
caddy_reload
|
|
75
|
+
Commands::Remove.new(name).call
|
|
253
76
|
end
|
|
254
77
|
|
|
255
78
|
desc 'start NAME', 'Start a Rails app with its correct Ruby version'
|
|
256
79
|
def start(name)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
port = app[:port] || next_free_port
|
|
261
|
-
ruby = app[:ruby]
|
|
262
|
-
path = app[:path]
|
|
263
|
-
|
|
264
|
-
if app_running?(app)
|
|
265
|
-
puts "#{name} is already running on https://#{app[:domain]} (port #{port})"
|
|
266
|
-
return
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
gemset = gemset_for(app)
|
|
270
|
-
|
|
271
|
-
rvm_cmd =
|
|
272
|
-
if ruby && gemset
|
|
273
|
-
system("bash -lc 'rvm #{ruby}@#{gemset} --create do true'")
|
|
274
|
-
"rvm #{ruby}@#{gemset} do"
|
|
275
|
-
elsif ruby
|
|
276
|
-
"rvm #{ruby} do"
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
puts "Starting #{name} on port #{port}..."
|
|
280
|
-
|
|
281
|
-
log_file = File.join(path, 'log', 'stable.log')
|
|
282
|
-
FileUtils.mkdir_p(File.dirname(log_file))
|
|
283
|
-
|
|
284
|
-
pid = spawn(
|
|
285
|
-
'bash',
|
|
286
|
-
'-lc',
|
|
287
|
-
<<~CMD,
|
|
288
|
-
cd "#{path}"
|
|
289
|
-
#{rvm_cmd} bundle exec rails s \
|
|
290
|
-
-p #{port} \
|
|
291
|
-
-b 127.0.0.1
|
|
292
|
-
CMD
|
|
293
|
-
out: log_file,
|
|
294
|
-
err: log_file
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
Process.detach(pid)
|
|
298
|
-
|
|
299
|
-
wait_for_port(port, timeout: 30)
|
|
300
|
-
|
|
301
|
-
app[:started_at] = Time.now.to_i
|
|
302
|
-
Registry.save(Registry.apps)
|
|
303
|
-
|
|
304
|
-
generate_cert(app[:domain])
|
|
305
|
-
update_caddyfile(app[:domain], port)
|
|
306
|
-
caddy_reload
|
|
80
|
+
Commands::Start.new(name).call
|
|
81
|
+
end
|
|
307
82
|
|
|
308
|
-
|
|
83
|
+
desc 'restart NAME', 'Restart a Rails app'
|
|
84
|
+
def restart(name)
|
|
85
|
+
Commands::Restart.new(name).call
|
|
309
86
|
end
|
|
310
87
|
|
|
311
88
|
desc 'stop NAME', 'Stop a Rails app (default port 3000)'
|
|
312
89
|
def stop(name)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
output = `lsof -i tcp:#{app[:port]} -t`.strip
|
|
316
|
-
if output.empty?
|
|
317
|
-
puts "No app running on port #{app[:port]}"
|
|
318
|
-
else
|
|
319
|
-
output.split("\n").each { |pid| Process.kill('TERM', pid.to_i) }
|
|
320
|
-
puts "Stopped #{name}"
|
|
321
|
-
end
|
|
90
|
+
Commands::Stop.new(name).call
|
|
322
91
|
end
|
|
323
92
|
|
|
324
93
|
desc 'setup', 'Sets up Caddy and local trusted certificates'
|
|
325
94
|
def setup
|
|
326
|
-
|
|
327
|
-
File.write(Stable::Paths.caddyfile, '') unless File.exist?(Stable::Paths.caddyfile)
|
|
328
|
-
ensure_caddy_running!
|
|
329
|
-
puts "Caddy home initialized at #{Stable::Paths.root}"
|
|
95
|
+
Commands::Setup.new.call
|
|
330
96
|
end
|
|
331
97
|
|
|
332
98
|
desc 'caddy reload', 'Reloads Caddy after adding/removing apps'
|
|
333
99
|
def caddy_reload
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
puts 'Caddy reloaded'
|
|
337
|
-
else
|
|
338
|
-
puts 'Caddy not found. Install Caddy first.'
|
|
339
|
-
end
|
|
100
|
+
Services::CaddyManager.reload
|
|
101
|
+
puts 'Caddy reloaded'
|
|
340
102
|
end
|
|
341
103
|
|
|
342
104
|
desc 'secure DOMAIN', 'Generate trusted local HTTPS cert for a specific folder/domain'
|
|
343
105
|
def secure(domain)
|
|
344
|
-
|
|
106
|
+
apps = Services::AppRegistry.all
|
|
107
|
+
app = apps.find { |a| a[:domain] == domain }
|
|
108
|
+
app ||= apps.find { |a| a[:name] == domain }
|
|
109
|
+
app ||= apps.find { |a| a[:domain] == "#{domain}.test" }
|
|
110
|
+
|
|
345
111
|
unless app
|
|
346
112
|
puts "No app found with domain #{domain}"
|
|
347
113
|
return
|
|
348
114
|
end
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
115
|
+
|
|
116
|
+
Services::CaddyManager.add_app(app[:name], skip_ssl: true)
|
|
117
|
+
Services::CaddyManager.reload
|
|
118
|
+
puts "Secured https://#{app[:domain]}"
|
|
352
119
|
end
|
|
353
120
|
|
|
354
121
|
desc 'doctor', 'Check Stable system health'
|
|
355
122
|
def doctor
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
puts "Ruby version: #{RUBY_VERSION}"
|
|
359
|
-
puts "RVM: #{rvm_available? ? 'yes' : 'no'}"
|
|
360
|
-
puts "rbenv: #{rbenv_available? ? 'yes' : 'no'}"
|
|
361
|
-
puts "Caddy: #{system('which caddy > /dev/null') ? 'yes' : 'no'}"
|
|
362
|
-
puts "mkcert: #{system('which mkcert > /dev/null') ? 'yes' : 'no'}"
|
|
363
|
-
|
|
364
|
-
Registry.apps.each do |app|
|
|
365
|
-
state = boot_state(app)
|
|
366
|
-
ruby = app[:ruby] || 'default'
|
|
367
|
-
port = app[:port]
|
|
368
|
-
|
|
369
|
-
puts "#{app[:name]} → Ruby #{ruby} | port #{port} | #{state}"
|
|
370
|
-
end
|
|
123
|
+
Commands::Doctor.new.call
|
|
371
124
|
end
|
|
372
125
|
|
|
373
126
|
desc 'upgrade-ruby NAME VERSION', 'Upgrade Ruby for an app'
|
|
374
127
|
def upgrade_ruby(name, version)
|
|
375
|
-
app =
|
|
128
|
+
app = Services::AppRegistry.find(name)
|
|
376
129
|
unless app
|
|
377
130
|
puts "No app named #{name}"
|
|
378
131
|
return
|
|
379
132
|
end
|
|
380
133
|
|
|
381
|
-
if rvm_available?
|
|
134
|
+
if Stable::Services::Ruby.rvm_available?
|
|
382
135
|
system("bash -lc 'rvm install #{version}'")
|
|
383
|
-
elsif rbenv_available?
|
|
136
|
+
elsif Stable::Services::Ruby.rbenv_available?
|
|
384
137
|
system("rbenv install #{version}")
|
|
385
138
|
else
|
|
386
139
|
puts 'No Ruby version manager found'
|
|
@@ -388,161 +141,15 @@ module Stable
|
|
|
388
141
|
end
|
|
389
142
|
|
|
390
143
|
File.write(File.join(app[:path], '.ruby-version'), version)
|
|
391
|
-
|
|
392
|
-
Registry.save(Registry.apps)
|
|
144
|
+
Services::AppRegistry.update(name, ruby: version)
|
|
393
145
|
|
|
394
146
|
puts "#{name} now uses Ruby #{version}"
|
|
395
147
|
end
|
|
396
148
|
|
|
397
149
|
private
|
|
398
150
|
|
|
399
|
-
def add_host_entry(domain)
|
|
400
|
-
entry = "127.0.0.1\t#{domain}"
|
|
401
|
-
hosts = File.read(HOSTS_FILE)
|
|
402
|
-
unless hosts.include?(domain)
|
|
403
|
-
puts "Adding #{domain} to #{HOSTS_FILE}..."
|
|
404
|
-
File.open(HOSTS_FILE, 'a') { |f| f.puts entry }
|
|
405
|
-
system('dscacheutil -flushcache; sudo killall -HUP mDNSResponder')
|
|
406
|
-
end
|
|
407
|
-
rescue Errno::EACCES
|
|
408
|
-
ensure_hosts_entry(domain)
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def remove_host_entry(domain)
|
|
412
|
-
hosts = File.read(HOSTS_FILE)
|
|
413
|
-
new_hosts = hosts.lines.reject { |line| line.include?(domain) }.join
|
|
414
|
-
File.write(HOSTS_FILE, new_hosts)
|
|
415
|
-
system('dscacheutil -flushcache; sudo killall -HUP mDNSResponder')
|
|
416
|
-
rescue Errno::EACCES
|
|
417
|
-
puts "Permission denied updating #{HOSTS_FILE}. Run 'sudo stable remove #{domain}' to remove hosts entry."
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
def ensure_hosts_entry(domain)
|
|
421
|
-
entry = "127.0.0.1\t#{domain}"
|
|
422
|
-
|
|
423
|
-
hosts = File.read(HOSTS_FILE)
|
|
424
|
-
return if hosts.include?(domain)
|
|
425
|
-
|
|
426
|
-
if Process.uid.zero?
|
|
427
|
-
File.open(HOSTS_FILE, 'a') { |f| f.puts entry }
|
|
428
|
-
else
|
|
429
|
-
system(%(echo "#{entry}" | sudo tee -a #{HOSTS_FILE} > /dev/null))
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
system('dscacheutil -flushcache; sudo killall -HUP mDNSResponder')
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
def secure_app(domain, _folder, port)
|
|
436
|
-
ensure_certs_dir!
|
|
437
|
-
|
|
438
|
-
cert_path = File.join(Stable::Paths.certs_dir, "#{domain}.pem")
|
|
439
|
-
key_path = File.join(Stable::Paths.certs_dir, "#{domain}-key.pem")
|
|
440
|
-
|
|
441
|
-
# Generate certificates if missing
|
|
442
|
-
if system('which mkcert > /dev/null')
|
|
443
|
-
unless File.exist?(cert_path) && File.exist?(key_path)
|
|
444
|
-
system("mkcert -cert-file #{cert_path} -key-file #{key_path} #{domain}")
|
|
445
|
-
end
|
|
446
|
-
else
|
|
447
|
-
puts 'mkcert not found. Please install mkcert.'
|
|
448
|
-
return
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
# Auto-add Caddy block if not already in Caddyfile
|
|
452
|
-
add_caddy_block(domain, cert_path, key_path, port)
|
|
453
|
-
caddy_reload
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def add_caddy_block(domain, cert, key, port)
|
|
457
|
-
caddyfile = Stable::Paths.caddyfile
|
|
458
|
-
FileUtils.touch(caddyfile) unless File.exist?(caddyfile)
|
|
459
|
-
content = File.read(caddyfile)
|
|
460
|
-
|
|
461
|
-
return if content.include?(domain) # don't duplicate
|
|
462
|
-
|
|
463
|
-
block = <<~CADDY
|
|
464
|
-
|
|
465
|
-
https://#{domain} {
|
|
466
|
-
reverse_proxy 127.0.0.1:#{port}
|
|
467
|
-
tls #{cert} #{key}
|
|
468
|
-
}
|
|
469
|
-
CADDY
|
|
470
|
-
|
|
471
|
-
File.write(caddyfile, content + block)
|
|
472
|
-
system("caddy fmt --overwrite #{caddyfile}")
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
# Remove Caddyfile entry for the domain
|
|
476
|
-
def remove_caddy_entry(domain)
|
|
477
|
-
return unless File.exist?(Stable::Paths.caddyfile)
|
|
478
|
-
|
|
479
|
-
content = File.read(Stable::Paths.caddyfile)
|
|
480
|
-
# Remove block starting with https://<domain> { ... }
|
|
481
|
-
regex = %r{
|
|
482
|
-
https://#{Regexp.escape(domain)}\s*\{
|
|
483
|
-
.*?
|
|
484
|
-
\}
|
|
485
|
-
}mx
|
|
486
|
-
|
|
487
|
-
new_content = content.gsub(regex, '')
|
|
488
|
-
|
|
489
|
-
File.write(Stable::Paths.caddyfile, new_content)
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
def ensure_dependencies!
|
|
493
|
-
unless system('which brew > /dev/null')
|
|
494
|
-
puts 'Homebrew is required. Install it first: https://brew.sh'
|
|
495
|
-
exit 1
|
|
496
|
-
end
|
|
497
|
-
|
|
498
|
-
# --- Install Caddy ---
|
|
499
|
-
unless system('which caddy > /dev/null')
|
|
500
|
-
puts 'Installing Caddy...'
|
|
501
|
-
system('brew install caddy')
|
|
502
|
-
end
|
|
503
|
-
|
|
504
|
-
# --- Install mkcert ---
|
|
505
|
-
unless system('which mkcert > /dev/null')
|
|
506
|
-
puts 'Installing mkcert...'
|
|
507
|
-
system('brew install mkcert nss')
|
|
508
|
-
system('mkcert -install')
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
# --- Install PostgreSQL ---
|
|
512
|
-
unless system('which psql > /dev/null')
|
|
513
|
-
puts 'Installing PostgreSQL...'
|
|
514
|
-
system('brew install postgresql')
|
|
515
|
-
system('brew services start postgresql')
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
# --- Install MySQL ---
|
|
519
|
-
unless system('which mysql > /dev/null')
|
|
520
|
-
puts 'Installing MySQL...'
|
|
521
|
-
system('brew install mysql')
|
|
522
|
-
system('brew services start mysql')
|
|
523
|
-
end
|
|
524
|
-
|
|
525
|
-
puts '✅ All dependencies are installed and running.'
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
def ensure_caddy_running!
|
|
530
|
-
api_port = 2019
|
|
531
|
-
|
|
532
|
-
# Check if Caddy API is reachable
|
|
533
|
-
require 'socket'
|
|
534
|
-
begin
|
|
535
|
-
TCPSocket.new('127.0.0.1', api_port).close
|
|
536
|
-
puts 'Caddy already running.'
|
|
537
|
-
rescue Errno::ECONNREFUSED
|
|
538
|
-
puts 'Starting Caddy in background...'
|
|
539
|
-
system("caddy run --config #{Stable::Paths.caddyfile} --adapter caddyfile --watch --resume &")
|
|
540
|
-
sleep 3
|
|
541
|
-
end
|
|
542
|
-
end
|
|
543
|
-
|
|
544
151
|
def next_free_port
|
|
545
|
-
used_ports =
|
|
152
|
+
used_ports = Services::AppRegistry.all.map { |a| a[:port] }
|
|
546
153
|
port = 3000
|
|
547
154
|
port += 1 while used_ports.include?(port) || port_in_use?(port)
|
|
548
155
|
port
|
|
@@ -625,62 +232,7 @@ module Stable
|
|
|
625
232
|
end
|
|
626
233
|
end
|
|
627
234
|
|
|
628
|
-
|
|
629
|
-
return if system('which rvm > /dev/null')
|
|
630
|
-
|
|
631
|
-
puts 'RVM not found. Installing RVM...'
|
|
632
|
-
|
|
633
|
-
install_cmd = <<~CMD
|
|
634
|
-
curl -sSL https://get.rvm.io | bash -s stable
|
|
635
|
-
CMD
|
|
636
|
-
|
|
637
|
-
abort 'RVM installation failed' unless system(install_cmd)
|
|
638
|
-
|
|
639
|
-
# Load RVM into current process
|
|
640
|
-
rvm_script = File.expand_path('~/.rvm/scripts/rvm')
|
|
641
|
-
abort 'RVM installed but could not be loaded' unless File.exist?(rvm_script)
|
|
642
|
-
|
|
643
|
-
ENV['PATH'] = "#{File.expand_path('~/.rvm/bin')}:#{ENV['PATH']}"
|
|
644
|
-
|
|
645
|
-
system(%(bash -lc "source #{rvm_script} && rvm --version")) ||
|
|
646
|
-
abort('RVM installed but not functional')
|
|
647
|
-
end
|
|
648
|
-
|
|
649
|
-
def ensure_ruby_installed!(version)
|
|
650
|
-
return if system("rvm list strings | grep ruby-#{version} > /dev/null")
|
|
651
|
-
|
|
652
|
-
puts "Installing Ruby #{version}..."
|
|
653
|
-
system("rvm install #{version}") || abort("Failed to install Ruby #{version}")
|
|
654
|
-
end
|
|
655
|
-
|
|
656
|
-
def detect_ruby_version(path)
|
|
657
|
-
rv = File.join(path, '.ruby-version')
|
|
658
|
-
return File.read(rv).strip if File.exist?(rv)
|
|
659
|
-
|
|
660
|
-
gemfile = File.join(path, 'Gemfile')
|
|
661
|
-
if File.exist?(gemfile)
|
|
662
|
-
ruby_line = File.read(gemfile)[/^ruby ['"](.+?)['"]/, 1]
|
|
663
|
-
return ruby_line if ruby_line
|
|
664
|
-
end
|
|
665
|
-
|
|
666
|
-
nil
|
|
667
|
-
end
|
|
668
|
-
|
|
669
|
-
def rvm_available?
|
|
670
|
-
system("bash -lc 'command -v rvm > /dev/null'")
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
def rbenv_available?
|
|
674
|
-
system('command -v rbenv > /dev/null')
|
|
675
|
-
end
|
|
676
|
-
|
|
677
|
-
def ensure_rvm_ruby!(version)
|
|
678
|
-
system("bash -lc 'rvm list strings | grep -q #{version} || rvm install #{version}'")
|
|
679
|
-
end
|
|
680
|
-
|
|
681
|
-
def ensure_rbenv_ruby!(version)
|
|
682
|
-
system("rbenv versions | grep -q #{version} || rbenv install #{version}")
|
|
683
|
-
end
|
|
235
|
+
# RVM/ruby helpers moved to Services::Ruby
|
|
684
236
|
|
|
685
237
|
def app_running?(app)
|
|
686
238
|
return false unless app && app[:port]
|
|
@@ -700,59 +252,15 @@ module Stable
|
|
|
700
252
|
end
|
|
701
253
|
|
|
702
254
|
def dedupe_registry!
|
|
703
|
-
|
|
704
|
-
apps.uniq! { |a| a[:name] }
|
|
705
|
-
Registry.save(apps)
|
|
255
|
+
Services::AppRegistry.dedupe
|
|
706
256
|
end
|
|
707
257
|
|
|
708
258
|
def gemset_for(app)
|
|
709
|
-
|
|
710
|
-
return File.read(gemset_file).strip if File.exist?(gemset_file)
|
|
711
|
-
|
|
712
|
-
nil
|
|
259
|
+
Stable::Services::Ruby.gemset_for(app)
|
|
713
260
|
end
|
|
714
261
|
|
|
715
262
|
def rvm_exec(app, ruby)
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if gemset
|
|
719
|
-
"rvm #{ruby}@#{gemset} do"
|
|
720
|
-
else
|
|
721
|
-
"rvm #{ruby} do"
|
|
722
|
-
end
|
|
723
|
-
end
|
|
724
|
-
|
|
725
|
-
def create_mysql_db(db_name)
|
|
726
|
-
socket = %w[
|
|
727
|
-
/opt/homebrew/var/mysql/mysql.sock
|
|
728
|
-
/tmp/mysql.sock
|
|
729
|
-
].find { |path| File.exist?(path) } || abort('MySQL socket not found')
|
|
730
|
-
|
|
731
|
-
print 'Enter MySQL root username (default: root): '
|
|
732
|
-
root_user = $stdin.gets.chomp
|
|
733
|
-
root_user = 'root' if root_user.empty?
|
|
734
|
-
|
|
735
|
-
print 'Enter MySQL root password (leave blank if none): '
|
|
736
|
-
root_password = $stdin.noecho(&:gets).chomp
|
|
737
|
-
puts
|
|
738
|
-
|
|
739
|
-
password_arg = root_password.empty? ? '' : "-p#{root_password}"
|
|
740
|
-
|
|
741
|
-
sql = <<~SQL
|
|
742
|
-
CREATE DATABASE IF NOT EXISTS #{db_name};
|
|
743
|
-
FLUSH PRIVILEGES;
|
|
744
|
-
SQL
|
|
745
|
-
|
|
746
|
-
require 'tempfile'
|
|
747
|
-
Tempfile.create(['db_setup', '.sql']) do |file|
|
|
748
|
-
file.write(sql)
|
|
749
|
-
file.flush
|
|
750
|
-
system("mysql --protocol=SOCKET --socket=#{socket} -u #{root_user} #{password_arg} < #{file.path}") or
|
|
751
|
-
abort("Failed to create MySQL DB '#{db_name}'")
|
|
752
|
-
end
|
|
753
|
-
|
|
754
|
-
puts "✅ MySQL database '#{db_name}' created/ready"
|
|
755
|
-
{ user: root_user, password: root_password }
|
|
263
|
+
Stable::Services::Ruby.rvm_exec(app, ruby)
|
|
756
264
|
end
|
|
757
265
|
end
|
|
758
266
|
end
|