stable-cli-rails 0.6.8 → 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 +45 -402
- 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/db_manager.rb +121 -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 +20 -1
- metadata +54 -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
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
4
|
require 'etc'
|
|
5
|
+
require 'tempfile'
|
|
5
6
|
require 'fileutils'
|
|
7
|
+
require 'io/console'
|
|
6
8
|
require_relative 'scanner'
|
|
7
9
|
require_relative 'registry'
|
|
8
10
|
|
|
@@ -13,7 +15,7 @@ module Stable
|
|
|
13
15
|
def initialize(*)
|
|
14
16
|
super
|
|
15
17
|
Stable::Bootstrap.run!
|
|
16
|
-
ensure_dependencies!
|
|
18
|
+
Services::SetupRunner.ensure_dependencies!
|
|
17
19
|
dedupe_registry!
|
|
18
20
|
end
|
|
19
21
|
|
|
@@ -26,97 +28,16 @@ module Stable
|
|
|
26
28
|
method_option :rails, type: :string, desc: 'Rails version to install (optional)'
|
|
27
29
|
method_option :port, type: :numeric, desc: 'Port to run Rails app on'
|
|
28
30
|
method_option :skip_ssl, type: :boolean, default: false, desc: 'Skip HTTPS setup'
|
|
31
|
+
method_option :db, type: :string, desc: 'Database name to create and integrate'
|
|
32
|
+
method_option :postgres, type: :boolean, default: false, desc: 'Use Postgres for the database'
|
|
33
|
+
method_option :mysql, type: :boolean, default: false, desc: 'Use MySQL for the database'
|
|
29
34
|
def new(name, ruby: RUBY_VERSION, rails: nil, port: nil)
|
|
30
|
-
|
|
31
|
-
app_path = File.expand_path(name)
|
|
32
|
-
|
|
33
|
-
# --- Add app to registry ---
|
|
34
|
-
domain = "#{name}.test"
|
|
35
|
-
apps = Registry.apps
|
|
36
|
-
|
|
37
|
-
app = {
|
|
38
|
-
name: name,
|
|
39
|
-
path: app_path,
|
|
40
|
-
domain: domain,
|
|
41
|
-
port: port,
|
|
42
|
-
ruby: ruby,
|
|
43
|
-
started_at: nil
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
apps.reject! { |a| a[:name] == name }
|
|
47
|
-
apps << app
|
|
48
|
-
|
|
49
|
-
abort "Folder already exists: #{app_path}" if File.exist?(app_path)
|
|
50
|
-
|
|
51
|
-
# --- Ensure RVM and Ruby ---
|
|
52
|
-
ensure_rvm!
|
|
53
|
-
puts "Using Ruby #{ruby} with RVM gemset #{name}..."
|
|
54
|
-
system("bash -lc 'rvm #{ruby}@#{name} --create do true'") or abort("Failed to create RVM gemset #{name}")
|
|
55
|
-
ruby_cmd = rvm_exec(app, ruby)
|
|
56
|
-
# --- Install Rails in gemset if needed ---
|
|
57
|
-
rails_version = rails || 'latest'
|
|
58
|
-
rails_check = system("bash -lc '#{ruby_cmd} gem list -i rails#{rails ? " -v #{rails}" : ''}'")
|
|
59
|
-
unless rails_check
|
|
60
|
-
puts "Installing Rails #{rails_version} in gemset..."
|
|
61
|
-
system("bash -lc '#{ruby_cmd} gem install rails #{rails ? "-v #{rails}" : ''}'") or abort('Failed to install Rails')
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# --- Create Rails app ---
|
|
65
|
-
puts "Creating Rails app #{name} (Ruby #{ruby})..."
|
|
66
|
-
system("bash -lc '#{ruby_cmd} rails new #{app_path}'") or abort('Rails app creation failed')
|
|
67
|
-
|
|
68
|
-
# --- Add .ruby-version and .ruby-gemset ---
|
|
69
|
-
Dir.chdir(app_path) do
|
|
70
|
-
File.write('.ruby-version', "#{ruby}\n")
|
|
71
|
-
File.write('.ruby-gemset', "#{name}\n")
|
|
72
|
-
|
|
73
|
-
# --- Install gems inside gemset ---
|
|
74
|
-
puts 'Running bundle install...'
|
|
75
|
-
system("bash -lc '#{ruby_cmd} bundle install --jobs=4 --retry=3'") or abort('bundle install failed')
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# --- Host entry & certificate ---
|
|
79
|
-
add_host_entry(domain)
|
|
80
|
-
generate_cert(domain) unless options[:skip_ssl]
|
|
81
|
-
update_caddyfile(domain, port)
|
|
82
|
-
ensure_caddy_running!
|
|
83
|
-
caddy_reload
|
|
84
|
-
|
|
85
|
-
# --- Start Rails server ---
|
|
86
|
-
puts "Starting Rails server for #{name} on port #{port}..."
|
|
87
|
-
log_file = File.join(app_path, 'log', 'stable.log')
|
|
88
|
-
FileUtils.mkdir_p(File.dirname(log_file))
|
|
89
|
-
|
|
90
|
-
abort "Port #{port} is already in use. Choose another port." if app_running?({ port: port })
|
|
91
|
-
|
|
92
|
-
pid = spawn(
|
|
93
|
-
'bash',
|
|
94
|
-
'-lc',
|
|
95
|
-
"cd #{app_path} && #{ruby_cmd} bundle exec rails s -p #{port} -b 127.0.0.1",
|
|
96
|
-
out: log_file,
|
|
97
|
-
err: log_file
|
|
98
|
-
)
|
|
99
|
-
Process.detach(pid)
|
|
100
|
-
|
|
101
|
-
app[:started_at] = Time.now.to_i
|
|
102
|
-
Registry.save(apps)
|
|
103
|
-
|
|
104
|
-
sleep 1.5
|
|
105
|
-
|
|
106
|
-
wait_for_port(port)
|
|
107
|
-
puts "✔ #{name} running at https://#{domain}"
|
|
35
|
+
Commands::New.new(name, options).call
|
|
108
36
|
end
|
|
109
37
|
|
|
110
38
|
desc 'list', 'List detected apps'
|
|
111
39
|
def list
|
|
112
|
-
|
|
113
|
-
if apps.empty?
|
|
114
|
-
puts 'No apps found.'
|
|
115
|
-
else
|
|
116
|
-
apps.each do |app|
|
|
117
|
-
puts "#{app[:name]} -> https://#{app[:domain]}"
|
|
118
|
-
end
|
|
119
|
-
end
|
|
40
|
+
Commands::List.new.call
|
|
120
41
|
end
|
|
121
42
|
|
|
122
43
|
desc 'add FOLDER', 'Add a Rails app folder'
|
|
@@ -129,175 +50,90 @@ module Stable
|
|
|
129
50
|
|
|
130
51
|
puts "Detected gemset: #{File.read('.ruby-gemset').strip}" if File.exist?('.ruby-gemset')
|
|
131
52
|
|
|
132
|
-
apps = Registry.apps
|
|
133
53
|
name = File.basename(folder)
|
|
134
54
|
domain = "#{name}.test"
|
|
135
55
|
|
|
136
|
-
if
|
|
56
|
+
if Services::AppRegistry.all.any? { |a| a[:path] == folder }
|
|
137
57
|
puts "App already exists: #{name}"
|
|
138
58
|
return
|
|
139
59
|
end
|
|
140
60
|
|
|
141
61
|
port = next_free_port
|
|
142
|
-
ruby = detect_ruby_version(folder)
|
|
62
|
+
ruby = Stable::Services::Ruby.detect_ruby_version(folder)
|
|
143
63
|
|
|
144
|
-
|
|
145
|
-
|
|
64
|
+
app = { name: name, path: folder, domain: domain, port: port, ruby: ruby }
|
|
65
|
+
Services::AppRegistry.add_app(app)
|
|
146
66
|
puts "Added #{name} -> https://#{domain} (port #{port})"
|
|
147
67
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
caddy_reload
|
|
68
|
+
Services::HostsManager.add(domain)
|
|
69
|
+
Services::CaddyManager.add_app(name, skip_ssl: options[:skip_ssl])
|
|
70
|
+
Services::CaddyManager.reload
|
|
152
71
|
end
|
|
153
72
|
|
|
154
73
|
desc 'remove NAME', 'Remove an app by name'
|
|
155
74
|
def remove(name)
|
|
156
|
-
|
|
157
|
-
app = apps.find { |a| a[:name] == name }
|
|
158
|
-
if app.nil?
|
|
159
|
-
puts "No app found with name #{name}"
|
|
160
|
-
return
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
new_apps = apps.reject { |a| a[:name] == name }
|
|
164
|
-
Registry.save(new_apps)
|
|
165
|
-
puts "Removed #{name}"
|
|
166
|
-
|
|
167
|
-
remove_host_entry(app[:domain])
|
|
168
|
-
remove_caddy_entry(app[:domain])
|
|
169
|
-
caddy_reload
|
|
75
|
+
Commands::Remove.new(name).call
|
|
170
76
|
end
|
|
171
77
|
|
|
172
78
|
desc 'start NAME', 'Start a Rails app with its correct Ruby version'
|
|
173
79
|
def start(name)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
port = app[:port] || next_free_port
|
|
178
|
-
ruby = app[:ruby]
|
|
179
|
-
path = app[:path]
|
|
180
|
-
|
|
181
|
-
if app_running?(app)
|
|
182
|
-
puts "#{name} is already running on https://#{app[:domain]} (port #{port})"
|
|
183
|
-
return
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
gemset = gemset_for(app)
|
|
187
|
-
|
|
188
|
-
rvm_cmd =
|
|
189
|
-
if ruby && gemset
|
|
190
|
-
system("bash -lc 'rvm #{ruby}@#{gemset} --create do true'")
|
|
191
|
-
"rvm #{ruby}@#{gemset} do"
|
|
192
|
-
elsif ruby
|
|
193
|
-
"rvm #{ruby} do"
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
puts "Starting #{name} on port #{port}..."
|
|
197
|
-
|
|
198
|
-
log_file = File.join(path, 'log', 'stable.log')
|
|
199
|
-
FileUtils.mkdir_p(File.dirname(log_file))
|
|
200
|
-
|
|
201
|
-
pid = spawn(
|
|
202
|
-
'bash',
|
|
203
|
-
'-lc',
|
|
204
|
-
<<~CMD,
|
|
205
|
-
cd "#{path}"
|
|
206
|
-
#{rvm_cmd} bundle exec rails s \
|
|
207
|
-
-p #{port} \
|
|
208
|
-
-b 127.0.0.1
|
|
209
|
-
CMD
|
|
210
|
-
out: log_file,
|
|
211
|
-
err: log_file
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
Process.detach(pid)
|
|
215
|
-
|
|
216
|
-
wait_for_port(port, timeout: 30)
|
|
217
|
-
|
|
218
|
-
app[:started_at] = Time.now.to_i
|
|
219
|
-
Registry.save(Registry.apps)
|
|
220
|
-
|
|
221
|
-
generate_cert(app[:domain])
|
|
222
|
-
update_caddyfile(app[:domain], port)
|
|
223
|
-
caddy_reload
|
|
80
|
+
Commands::Start.new(name).call
|
|
81
|
+
end
|
|
224
82
|
|
|
225
|
-
|
|
83
|
+
desc 'restart NAME', 'Restart a Rails app'
|
|
84
|
+
def restart(name)
|
|
85
|
+
Commands::Restart.new(name).call
|
|
226
86
|
end
|
|
227
87
|
|
|
228
88
|
desc 'stop NAME', 'Stop a Rails app (default port 3000)'
|
|
229
89
|
def stop(name)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
output = `lsof -i tcp:#{app[:port]} -t`.strip
|
|
233
|
-
if output.empty?
|
|
234
|
-
puts "No app running on port #{app[:port]}"
|
|
235
|
-
else
|
|
236
|
-
output.split("\n").each { |pid| Process.kill('TERM', pid.to_i) }
|
|
237
|
-
puts "Stopped #{name}"
|
|
238
|
-
end
|
|
90
|
+
Commands::Stop.new(name).call
|
|
239
91
|
end
|
|
240
92
|
|
|
241
93
|
desc 'setup', 'Sets up Caddy and local trusted certificates'
|
|
242
94
|
def setup
|
|
243
|
-
|
|
244
|
-
File.write(Stable::Paths.caddyfile, '') unless File.exist?(Stable::Paths.caddyfile)
|
|
245
|
-
ensure_caddy_running!
|
|
246
|
-
puts "Caddy home initialized at #{Stable::Paths.root}"
|
|
95
|
+
Commands::Setup.new.call
|
|
247
96
|
end
|
|
248
97
|
|
|
249
98
|
desc 'caddy reload', 'Reloads Caddy after adding/removing apps'
|
|
250
99
|
def caddy_reload
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
puts 'Caddy reloaded'
|
|
254
|
-
else
|
|
255
|
-
puts 'Caddy not found. Install Caddy first.'
|
|
256
|
-
end
|
|
100
|
+
Services::CaddyManager.reload
|
|
101
|
+
puts 'Caddy reloaded'
|
|
257
102
|
end
|
|
258
103
|
|
|
259
104
|
desc 'secure DOMAIN', 'Generate trusted local HTTPS cert for a specific folder/domain'
|
|
260
105
|
def secure(domain)
|
|
261
|
-
|
|
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
|
+
|
|
262
111
|
unless app
|
|
263
112
|
puts "No app found with domain #{domain}"
|
|
264
113
|
return
|
|
265
114
|
end
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
115
|
+
|
|
116
|
+
Services::CaddyManager.add_app(app[:name], skip_ssl: true)
|
|
117
|
+
Services::CaddyManager.reload
|
|
118
|
+
puts "Secured https://#{app[:domain]}"
|
|
269
119
|
end
|
|
270
120
|
|
|
271
121
|
desc 'doctor', 'Check Stable system health'
|
|
272
122
|
def doctor
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
puts "Ruby version: #{RUBY_VERSION}"
|
|
276
|
-
puts "RVM: #{rvm_available? ? 'yes' : 'no'}"
|
|
277
|
-
puts "rbenv: #{rbenv_available? ? 'yes' : 'no'}"
|
|
278
|
-
puts "Caddy: #{system('which caddy > /dev/null') ? 'yes' : 'no'}"
|
|
279
|
-
puts "mkcert: #{system('which mkcert > /dev/null') ? 'yes' : 'no'}"
|
|
280
|
-
|
|
281
|
-
Registry.apps.each do |app|
|
|
282
|
-
state = boot_state(app)
|
|
283
|
-
ruby = app[:ruby] || 'default'
|
|
284
|
-
port = app[:port]
|
|
285
|
-
|
|
286
|
-
puts "#{app[:name]} → Ruby #{ruby} | port #{port} | #{state}"
|
|
287
|
-
end
|
|
123
|
+
Commands::Doctor.new.call
|
|
288
124
|
end
|
|
289
125
|
|
|
290
126
|
desc 'upgrade-ruby NAME VERSION', 'Upgrade Ruby for an app'
|
|
291
127
|
def upgrade_ruby(name, version)
|
|
292
|
-
app =
|
|
128
|
+
app = Services::AppRegistry.find(name)
|
|
293
129
|
unless app
|
|
294
130
|
puts "No app named #{name}"
|
|
295
131
|
return
|
|
296
132
|
end
|
|
297
133
|
|
|
298
|
-
if rvm_available?
|
|
134
|
+
if Stable::Services::Ruby.rvm_available?
|
|
299
135
|
system("bash -lc 'rvm install #{version}'")
|
|
300
|
-
elsif rbenv_available?
|
|
136
|
+
elsif Stable::Services::Ruby.rbenv_available?
|
|
301
137
|
system("rbenv install #{version}")
|
|
302
138
|
else
|
|
303
139
|
puts 'No Ruby version manager found'
|
|
@@ -305,142 +141,15 @@ module Stable
|
|
|
305
141
|
end
|
|
306
142
|
|
|
307
143
|
File.write(File.join(app[:path], '.ruby-version'), version)
|
|
308
|
-
|
|
309
|
-
Registry.save(Registry.apps)
|
|
144
|
+
Services::AppRegistry.update(name, ruby: version)
|
|
310
145
|
|
|
311
146
|
puts "#{name} now uses Ruby #{version}"
|
|
312
147
|
end
|
|
313
148
|
|
|
314
149
|
private
|
|
315
150
|
|
|
316
|
-
def add_host_entry(domain)
|
|
317
|
-
entry = "127.0.0.1\t#{domain}"
|
|
318
|
-
hosts = File.read(HOSTS_FILE)
|
|
319
|
-
unless hosts.include?(domain)
|
|
320
|
-
puts "Adding #{domain} to #{HOSTS_FILE}..."
|
|
321
|
-
File.open(HOSTS_FILE, 'a') { |f| f.puts entry }
|
|
322
|
-
system('dscacheutil -flushcache; sudo killall -HUP mDNSResponder')
|
|
323
|
-
end
|
|
324
|
-
rescue Errno::EACCES
|
|
325
|
-
ensure_hosts_entry(domain)
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
def remove_host_entry(domain)
|
|
329
|
-
hosts = File.read(HOSTS_FILE)
|
|
330
|
-
new_hosts = hosts.lines.reject { |line| line.include?(domain) }.join
|
|
331
|
-
File.write(HOSTS_FILE, new_hosts)
|
|
332
|
-
system('dscacheutil -flushcache; sudo killall -HUP mDNSResponder')
|
|
333
|
-
rescue Errno::EACCES
|
|
334
|
-
puts "Permission denied updating #{HOSTS_FILE}. Run 'sudo stable remove #{domain}' to remove hosts entry."
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def ensure_hosts_entry(domain)
|
|
338
|
-
entry = "127.0.0.1\t#{domain}"
|
|
339
|
-
|
|
340
|
-
hosts = File.read(HOSTS_FILE)
|
|
341
|
-
return if hosts.include?(domain)
|
|
342
|
-
|
|
343
|
-
if Process.uid.zero?
|
|
344
|
-
File.open(HOSTS_FILE, 'a') { |f| f.puts entry }
|
|
345
|
-
else
|
|
346
|
-
system(%(echo "#{entry}" | sudo tee -a #{HOSTS_FILE} > /dev/null))
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
system('dscacheutil -flushcache; sudo killall -HUP mDNSResponder')
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
def secure_app(domain, _folder, port)
|
|
353
|
-
ensure_certs_dir!
|
|
354
|
-
|
|
355
|
-
cert_path = File.join(Stable::Paths.certs_dir, "#{domain}.pem")
|
|
356
|
-
key_path = File.join(Stable::Paths.certs_dir, "#{domain}-key.pem")
|
|
357
|
-
|
|
358
|
-
# Generate certificates if missing
|
|
359
|
-
if system('which mkcert > /dev/null')
|
|
360
|
-
unless File.exist?(cert_path) && File.exist?(key_path)
|
|
361
|
-
system("mkcert -cert-file #{cert_path} -key-file #{key_path} #{domain}")
|
|
362
|
-
end
|
|
363
|
-
else
|
|
364
|
-
puts 'mkcert not found. Please install mkcert.'
|
|
365
|
-
return
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
# Auto-add Caddy block if not already in Caddyfile
|
|
369
|
-
add_caddy_block(domain, cert_path, key_path, port)
|
|
370
|
-
caddy_reload
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
def add_caddy_block(domain, cert, key, port)
|
|
374
|
-
caddyfile = Stable::Paths.caddyfile
|
|
375
|
-
FileUtils.touch(caddyfile) unless File.exist?(caddyfile)
|
|
376
|
-
content = File.read(caddyfile)
|
|
377
|
-
|
|
378
|
-
return if content.include?(domain) # don't duplicate
|
|
379
|
-
|
|
380
|
-
block = <<~CADDY
|
|
381
|
-
|
|
382
|
-
https://#{domain} {
|
|
383
|
-
reverse_proxy 127.0.0.1:#{port}
|
|
384
|
-
tls #{cert} #{key}
|
|
385
|
-
}
|
|
386
|
-
CADDY
|
|
387
|
-
|
|
388
|
-
File.write(caddyfile, content + block)
|
|
389
|
-
system("caddy fmt --overwrite #{caddyfile}")
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
# Remove Caddyfile entry for the domain
|
|
393
|
-
def remove_caddy_entry(domain)
|
|
394
|
-
return unless File.exist?(Stable::Paths.caddyfile)
|
|
395
|
-
|
|
396
|
-
content = File.read(Stable::Paths.caddyfile)
|
|
397
|
-
# Remove block starting with https://<domain> { ... }
|
|
398
|
-
regex = %r{
|
|
399
|
-
https://#{Regexp.escape(domain)}\s*\{
|
|
400
|
-
.*?
|
|
401
|
-
\}
|
|
402
|
-
}mx
|
|
403
|
-
|
|
404
|
-
new_content = content.gsub(regex, '')
|
|
405
|
-
|
|
406
|
-
File.write(Stable::Paths.caddyfile, new_content)
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
def ensure_dependencies!
|
|
410
|
-
unless system('which brew > /dev/null')
|
|
411
|
-
puts 'Homebrew is required. Install it first: https://brew.sh'
|
|
412
|
-
exit 1
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
unless system('which caddy > /dev/null')
|
|
416
|
-
puts 'Installing Caddy...'
|
|
417
|
-
system('brew install caddy')
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
return if system('which mkcert > /dev/null')
|
|
421
|
-
|
|
422
|
-
puts 'Installing mkcert...'
|
|
423
|
-
system('brew install mkcert nss')
|
|
424
|
-
system('mkcert -install')
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def ensure_caddy_running!
|
|
428
|
-
api_port = 2019
|
|
429
|
-
|
|
430
|
-
# Check if Caddy API is reachable
|
|
431
|
-
require 'socket'
|
|
432
|
-
begin
|
|
433
|
-
TCPSocket.new('127.0.0.1', api_port).close
|
|
434
|
-
puts 'Caddy already running.'
|
|
435
|
-
rescue Errno::ECONNREFUSED
|
|
436
|
-
puts 'Starting Caddy in background...'
|
|
437
|
-
system("caddy run --config #{Stable::Paths.caddyfile} --adapter caddyfile --watch --resume &")
|
|
438
|
-
sleep 3
|
|
439
|
-
end
|
|
440
|
-
end
|
|
441
|
-
|
|
442
151
|
def next_free_port
|
|
443
|
-
used_ports =
|
|
152
|
+
used_ports = Services::AppRegistry.all.map { |a| a[:port] }
|
|
444
153
|
port = 3000
|
|
445
154
|
port += 1 while used_ports.include?(port) || port_in_use?(port)
|
|
446
155
|
port
|
|
@@ -509,7 +218,7 @@ module Stable
|
|
|
509
218
|
end
|
|
510
219
|
end
|
|
511
220
|
|
|
512
|
-
def wait_for_port(port, timeout:
|
|
221
|
+
def wait_for_port(port, timeout: 20)
|
|
513
222
|
require 'socket'
|
|
514
223
|
start = Time.now
|
|
515
224
|
|
|
@@ -523,62 +232,7 @@ module Stable
|
|
|
523
232
|
end
|
|
524
233
|
end
|
|
525
234
|
|
|
526
|
-
|
|
527
|
-
return if system('which rvm > /dev/null')
|
|
528
|
-
|
|
529
|
-
puts 'RVM not found. Installing RVM...'
|
|
530
|
-
|
|
531
|
-
install_cmd = <<~CMD
|
|
532
|
-
curl -sSL https://get.rvm.io | bash -s stable
|
|
533
|
-
CMD
|
|
534
|
-
|
|
535
|
-
abort 'RVM installation failed' unless system(install_cmd)
|
|
536
|
-
|
|
537
|
-
# Load RVM into current process
|
|
538
|
-
rvm_script = File.expand_path('~/.rvm/scripts/rvm')
|
|
539
|
-
abort 'RVM installed but could not be loaded' unless File.exist?(rvm_script)
|
|
540
|
-
|
|
541
|
-
ENV['PATH'] = "#{File.expand_path('~/.rvm/bin')}:#{ENV['PATH']}"
|
|
542
|
-
|
|
543
|
-
system(%(bash -lc "source #{rvm_script} && rvm --version")) ||
|
|
544
|
-
abort('RVM installed but not functional')
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
def ensure_ruby_installed!(version)
|
|
548
|
-
return if system("rvm list strings | grep ruby-#{version} > /dev/null")
|
|
549
|
-
|
|
550
|
-
puts "Installing Ruby #{version}..."
|
|
551
|
-
system("rvm install #{version}") || abort("Failed to install Ruby #{version}")
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
def detect_ruby_version(path)
|
|
555
|
-
rv = File.join(path, '.ruby-version')
|
|
556
|
-
return File.read(rv).strip if File.exist?(rv)
|
|
557
|
-
|
|
558
|
-
gemfile = File.join(path, 'Gemfile')
|
|
559
|
-
if File.exist?(gemfile)
|
|
560
|
-
ruby_line = File.read(gemfile)[/^ruby ['"](.+?)['"]/, 1]
|
|
561
|
-
return ruby_line if ruby_line
|
|
562
|
-
end
|
|
563
|
-
|
|
564
|
-
nil
|
|
565
|
-
end
|
|
566
|
-
|
|
567
|
-
def rvm_available?
|
|
568
|
-
system("bash -lc 'command -v rvm > /dev/null'")
|
|
569
|
-
end
|
|
570
|
-
|
|
571
|
-
def rbenv_available?
|
|
572
|
-
system('command -v rbenv > /dev/null')
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
def ensure_rvm_ruby!(version)
|
|
576
|
-
system("bash -lc 'rvm list strings | grep -q #{version} || rvm install #{version}'")
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
def ensure_rbenv_ruby!(version)
|
|
580
|
-
system("rbenv versions | grep -q #{version} || rbenv install #{version}")
|
|
581
|
-
end
|
|
235
|
+
# RVM/ruby helpers moved to Services::Ruby
|
|
582
236
|
|
|
583
237
|
def app_running?(app)
|
|
584
238
|
return false unless app && app[:port]
|
|
@@ -598,26 +252,15 @@ module Stable
|
|
|
598
252
|
end
|
|
599
253
|
|
|
600
254
|
def dedupe_registry!
|
|
601
|
-
|
|
602
|
-
apps.uniq! { |a| a[:name] }
|
|
603
|
-
Registry.save(apps)
|
|
255
|
+
Services::AppRegistry.dedupe
|
|
604
256
|
end
|
|
605
257
|
|
|
606
258
|
def gemset_for(app)
|
|
607
|
-
|
|
608
|
-
return File.read(gemset_file).strip if File.exist?(gemset_file)
|
|
609
|
-
|
|
610
|
-
nil
|
|
259
|
+
Stable::Services::Ruby.gemset_for(app)
|
|
611
260
|
end
|
|
612
261
|
|
|
613
262
|
def rvm_exec(app, ruby)
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if gemset
|
|
617
|
-
"rvm #{ruby}@#{gemset} do"
|
|
618
|
-
else
|
|
619
|
-
"rvm #{ruby} do"
|
|
620
|
-
end
|
|
263
|
+
Stable::Services::Ruby.rvm_exec(app, ruby)
|
|
621
264
|
end
|
|
622
265
|
end
|
|
623
266
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Commands
|
|
5
|
+
class Doctor
|
|
6
|
+
def call
|
|
7
|
+
puts 'Running Stable health checks...'
|
|
8
|
+
puts
|
|
9
|
+
|
|
10
|
+
checks = Services::DependencyChecker.new.run
|
|
11
|
+
|
|
12
|
+
checks.each do |check|
|
|
13
|
+
print_check(check)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
puts
|
|
17
|
+
summary(checks)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def print_check(check)
|
|
23
|
+
icon = check[:ok] ? '✔' : '✖'
|
|
24
|
+
puts "#{icon} #{check[:name]}"
|
|
25
|
+
puts " #{check[:message]}" unless check[:ok]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def summary(checks)
|
|
29
|
+
failures = checks.count { |c| !c[:ok] }
|
|
30
|
+
|
|
31
|
+
if failures.zero?
|
|
32
|
+
puts 'All checks passed.'
|
|
33
|
+
else
|
|
34
|
+
puts "#{failures} issue(s) detected. Fix the above and re-run `stable doctor`."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Commands
|
|
5
|
+
class List
|
|
6
|
+
def call
|
|
7
|
+
apps = Services::AppRegistry.all
|
|
8
|
+
|
|
9
|
+
if apps.empty?
|
|
10
|
+
puts 'No apps registered.'
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
print_header
|
|
15
|
+
|
|
16
|
+
apps.each do |app|
|
|
17
|
+
puts format_row(app)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def print_header
|
|
24
|
+
puts format(
|
|
25
|
+
'%-18s %-26s %-8s %-10s %-10s',
|
|
26
|
+
'APP',
|
|
27
|
+
'DOMAIN',
|
|
28
|
+
'PORT',
|
|
29
|
+
'RUBY',
|
|
30
|
+
'STATUS'
|
|
31
|
+
)
|
|
32
|
+
puts '-' * 78
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def format_row(app)
|
|
36
|
+
status =
|
|
37
|
+
if app[:started_at]
|
|
38
|
+
'running'
|
|
39
|
+
else
|
|
40
|
+
'stopped'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
format(
|
|
44
|
+
'%-18s %-26s %-8s %-10s %-10s',
|
|
45
|
+
app[:name],
|
|
46
|
+
app[:domain],
|
|
47
|
+
app[:port],
|
|
48
|
+
app[:ruby],
|
|
49
|
+
status
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|