stable-cli-rails 0.3.0 → 0.6.0
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/lib/stable/bootstrap.rb +8 -10
- data/lib/stable/cli.rb +132 -128
- data/lib/stable/paths.rb +6 -4
- data/lib/stable/registry.rb +5 -3
- data/lib/stable/scanner.rb +1 -0
- data/lib/stable.rb +9 -9
- metadata +5 -49
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c66a6ba166adca5396c2f103bf9dbcf925f245b8deee60134c7509b4118d87de
|
|
4
|
+
data.tar.gz: 9a2f9cd59e224a67013a32351ec2bea6fda1097e01abd509594192b9648cbfa5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1b03ed39118c8743eef89fba956ffec5701bdff75cbc62bae2f8cd29e092e5eebc4c9c730299f6788acb2df9d907da69b16dd13abc806a3d94b1d72ba0d7156
|
|
7
|
+
data.tar.gz: 37a0545468ad7e1f2ca46e8e5039df4af1ecd71cee8b6d1eaaef9b6ad279969dd4bcc00abb050356113aae458229855c37ebccc8f81e1985aa09a1d9a79c8b10
|
data/lib/stable/bootstrap.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
2
4
|
|
|
3
5
|
module Stable
|
|
4
6
|
module Bootstrap
|
|
@@ -7,25 +9,21 @@ module Stable
|
|
|
7
9
|
FileUtils.mkdir_p(Paths.caddy_dir)
|
|
8
10
|
FileUtils.mkdir_p(Paths.certs_dir)
|
|
9
11
|
|
|
10
|
-
unless File.exist?(Paths.apps_file)
|
|
11
|
-
File.write(Paths.apps_file, "--- []\n")
|
|
12
|
-
end
|
|
12
|
+
File.write(Paths.apps_file, "--- []\n") unless File.exist?(Paths.apps_file)
|
|
13
13
|
|
|
14
|
-
unless File.exist?(Paths.caddyfile)
|
|
15
|
-
File.write(Paths.caddyfile, "")
|
|
16
|
-
end
|
|
14
|
+
File.write(Paths.caddyfile, '') unless File.exist?(Paths.caddyfile)
|
|
17
15
|
|
|
18
16
|
disable_rvm_autolibs!
|
|
19
17
|
end
|
|
20
18
|
|
|
21
19
|
def self.disable_rvm_autolibs!
|
|
22
|
-
return unless system(
|
|
20
|
+
return unless system('which rvm > /dev/null')
|
|
23
21
|
|
|
24
22
|
# Only run once
|
|
25
|
-
marker = File.join(Paths.root,
|
|
23
|
+
marker = File.join(Paths.root, '.rvm_autolibs_disabled')
|
|
26
24
|
return if File.exist?(marker)
|
|
27
25
|
|
|
28
|
-
puts
|
|
26
|
+
puts 'Configuring RVM (disabling autolibs)...'
|
|
29
27
|
|
|
30
28
|
system("bash -lc 'rvm autolibs disable'")
|
|
31
29
|
system("bash -lc 'echo rvm_silence_path_mismatch_check_flag=1 >> ~/.rvmrc'")
|
data/lib/stable/cli.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'etc'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require_relative 'scanner'
|
|
7
|
+
require_relative 'registry'
|
|
6
8
|
|
|
7
9
|
module Stable
|
|
8
10
|
class CLI < Thor
|
|
9
|
-
|
|
10
|
-
HOSTS_FILE = "/etc/hosts".freeze
|
|
11
|
+
HOSTS_FILE = '/etc/hosts'
|
|
11
12
|
|
|
12
13
|
def initialize(*)
|
|
13
14
|
super
|
|
@@ -19,11 +20,11 @@ module Stable
|
|
|
19
20
|
true
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
desc
|
|
23
|
-
method_option :ruby, type: :string, desc:
|
|
24
|
-
method_option :rails, type: :string, desc:
|
|
25
|
-
method_option :port, type: :numeric, desc:
|
|
26
|
-
method_option :skip_ssl, type: :boolean, default: false, desc:
|
|
23
|
+
desc 'new NAME', 'Create, secure, and run a new Rails app'
|
|
24
|
+
method_option :ruby, type: :string, desc: 'Ruby version (defaults to current Ruby)'
|
|
25
|
+
method_option :rails, type: :string, desc: 'Rails version to install (optional)'
|
|
26
|
+
method_option :port, type: :numeric, desc: 'Port to run Rails app on'
|
|
27
|
+
method_option :skip_ssl, type: :boolean, default: false, desc: 'Skip HTTPS setup'
|
|
27
28
|
def new(name, ruby: RUBY_VERSION, rails: nil, port: nil)
|
|
28
29
|
port ||= next_free_port
|
|
29
30
|
app_path = File.expand_path(name)
|
|
@@ -33,28 +34,28 @@ module Stable
|
|
|
33
34
|
# --- Ensure RVM and Ruby ---
|
|
34
35
|
ensure_rvm!
|
|
35
36
|
puts "Using Ruby #{ruby} with RVM gemset #{name}..."
|
|
36
|
-
system("bash -lc 'rvm #{ruby}@#{name} --create do true'") or abort(
|
|
37
|
+
system("bash -lc 'rvm #{ruby}@#{name} --create do true'") or abort('Failed to create RVM gemset')
|
|
37
38
|
|
|
38
39
|
# --- Install Rails in gemset if needed ---
|
|
39
|
-
rails_version = rails ||
|
|
40
|
-
rails_check = system("bash -lc 'rvm #{ruby}@#{name} do gem list -i rails#{rails ? " -v #{rails}" :
|
|
40
|
+
rails_version = rails || 'latest'
|
|
41
|
+
rails_check = system("bash -lc 'rvm #{ruby}@#{name} do gem list -i rails#{rails ? " -v #{rails}" : ''}'")
|
|
41
42
|
unless rails_check
|
|
42
43
|
puts "Installing Rails #{rails_version} in gemset..."
|
|
43
|
-
system("bash -lc 'rvm #{ruby}@#{name} do gem install rails #{rails ? "-v #{rails}" :
|
|
44
|
+
system("bash -lc 'rvm #{ruby}@#{name} do gem install rails #{rails ? "-v #{rails}" : ''}'") or abort('Failed to install Rails')
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
# --- Create Rails app ---
|
|
47
48
|
puts "Creating Rails app #{name} (Ruby #{ruby})..."
|
|
48
|
-
system("bash -lc 'rvm #{ruby}@#{name} do rails new #{app_path}'") or abort(
|
|
49
|
+
system("bash -lc 'rvm #{ruby}@#{name} do rails new #{app_path}'") or abort('Rails app creation failed')
|
|
49
50
|
|
|
50
51
|
# --- Add .ruby-version and .ruby-gemset ---
|
|
51
52
|
Dir.chdir(app_path) do
|
|
52
|
-
File.write(
|
|
53
|
-
File.write(
|
|
53
|
+
File.write('.ruby-version', "#{ruby}\n")
|
|
54
|
+
File.write('.ruby-gemset', "#{name}\n")
|
|
54
55
|
|
|
55
56
|
# --- Install gems inside gemset ---
|
|
56
|
-
puts
|
|
57
|
-
system("bash -lc 'rvm #{ruby}@#{name} do bundle install --jobs=4 --retry=3'") or abort(
|
|
57
|
+
puts 'Running bundle install...'
|
|
58
|
+
system("bash -lc 'rvm #{ruby}@#{name} do bundle install --jobs=4 --retry=3'") or abort('bundle install failed')
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
# --- Add app to registry ---
|
|
@@ -72,7 +73,7 @@ module Stable
|
|
|
72
73
|
|
|
73
74
|
# --- Start Rails server ---
|
|
74
75
|
puts "Starting Rails server for #{name} on port #{port}..."
|
|
75
|
-
log_file = File.join(app_path,
|
|
76
|
+
log_file = File.join(app_path, 'log', 'stable.log')
|
|
76
77
|
FileUtils.mkdir_p(File.dirname(log_file))
|
|
77
78
|
pid = spawn("bash -lc 'rvm #{ruby}@#{name} do cd #{app_path} && bundle exec rails s -p #{port} >> #{log_file} 2>&1'")
|
|
78
79
|
Process.detach(pid)
|
|
@@ -81,12 +82,11 @@ module Stable
|
|
|
81
82
|
puts "✔ #{name} running at https://#{domain}"
|
|
82
83
|
end
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
desc "list", "List detected apps"
|
|
85
|
+
desc 'list', 'List detected apps'
|
|
86
86
|
def list
|
|
87
87
|
apps = Registry.apps
|
|
88
88
|
if apps.empty?
|
|
89
|
-
puts
|
|
89
|
+
puts 'No apps found.'
|
|
90
90
|
else
|
|
91
91
|
apps.each do |app|
|
|
92
92
|
puts "#{app[:name]} -> https://#{app[:domain]}"
|
|
@@ -94,10 +94,10 @@ module Stable
|
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
desc
|
|
97
|
+
desc 'add FOLDER', 'Add a Rails app folder'
|
|
98
98
|
def add(folder)
|
|
99
99
|
folder = File.expand_path(folder)
|
|
100
|
-
unless File.exist?(File.join(folder,
|
|
100
|
+
unless File.exist?(File.join(folder, 'config', 'application.rb'))
|
|
101
101
|
puts "Not a Rails app: #{folder}"
|
|
102
102
|
return
|
|
103
103
|
end
|
|
@@ -124,7 +124,7 @@ module Stable
|
|
|
124
124
|
caddy_reload
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
-
desc
|
|
127
|
+
desc 'remove NAME', 'Remove an app by name'
|
|
128
128
|
def remove(name)
|
|
129
129
|
apps = Registry.apps
|
|
130
130
|
app = apps.find { |a| a[:name] == name }
|
|
@@ -142,7 +142,7 @@ module Stable
|
|
|
142
142
|
caddy_reload
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
-
desc
|
|
145
|
+
desc 'start NAME', 'Start a Rails app with its correct Ruby version'
|
|
146
146
|
def start(name)
|
|
147
147
|
app = Registry.apps.find { |a| a[:name] == name }
|
|
148
148
|
unless app
|
|
@@ -153,9 +153,9 @@ module Stable
|
|
|
153
153
|
port = app[:port] || next_free_port
|
|
154
154
|
ruby = app[:ruby]
|
|
155
155
|
|
|
156
|
-
puts "Starting #{name} on port #{port}#{ruby ? " (Ruby #{ruby})" :
|
|
156
|
+
puts "Starting #{name} on port #{port}#{ruby ? " (Ruby #{ruby})" : ''}..."
|
|
157
157
|
|
|
158
|
-
log_file = File.join(app[:path],
|
|
158
|
+
log_file = File.join(app[:path], 'log', 'stable.log')
|
|
159
159
|
FileUtils.mkdir_p(File.dirname(log_file))
|
|
160
160
|
|
|
161
161
|
ruby_exec =
|
|
@@ -167,7 +167,7 @@ module Stable
|
|
|
167
167
|
ensure_rbenv_ruby!(ruby)
|
|
168
168
|
"RBENV_VERSION=#{ruby}"
|
|
169
169
|
else
|
|
170
|
-
puts
|
|
170
|
+
puts 'No Ruby version manager found (rvm or rbenv)'
|
|
171
171
|
return
|
|
172
172
|
end
|
|
173
173
|
end
|
|
@@ -178,8 +178,8 @@ module Stable
|
|
|
178
178
|
CMD
|
|
179
179
|
|
|
180
180
|
pid = spawn(
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
'bash',
|
|
182
|
+
'-lc',
|
|
183
183
|
cmd,
|
|
184
184
|
out: log_file,
|
|
185
185
|
err: log_file
|
|
@@ -195,7 +195,7 @@ module Stable
|
|
|
195
195
|
puts "#{name} started on https://#{app[:domain]}"
|
|
196
196
|
end
|
|
197
197
|
|
|
198
|
-
desc
|
|
198
|
+
desc 'stop NAME', 'Stop a Rails app (default port 3000)'
|
|
199
199
|
def stop(name)
|
|
200
200
|
app = Registry.apps.find { |a| a[:name] == name }
|
|
201
201
|
|
|
@@ -203,30 +203,30 @@ module Stable
|
|
|
203
203
|
if output.empty?
|
|
204
204
|
puts "No app running on port #{app[:port]}"
|
|
205
205
|
else
|
|
206
|
-
output.split("\n").each { |pid| Process.kill(
|
|
206
|
+
output.split("\n").each { |pid| Process.kill('TERM', pid.to_i) }
|
|
207
207
|
puts "Stopped #{name}"
|
|
208
208
|
end
|
|
209
209
|
end
|
|
210
210
|
|
|
211
|
-
desc
|
|
211
|
+
desc 'setup', 'Sets up Caddy and local trusted certificates'
|
|
212
212
|
def setup
|
|
213
213
|
FileUtils.mkdir_p(Stable::Paths.root)
|
|
214
|
-
File.write(Stable::Paths.caddyfile,
|
|
214
|
+
File.write(Stable::Paths.caddyfile, '') unless File.exist?(Stable::Paths.caddyfile)
|
|
215
215
|
ensure_caddy_running!
|
|
216
216
|
puts "Caddy home initialized at #{Stable::Paths.root}"
|
|
217
217
|
end
|
|
218
218
|
|
|
219
|
-
desc
|
|
219
|
+
desc 'caddy reload', 'Reloads Caddy after adding/removing apps'
|
|
220
220
|
def caddy_reload
|
|
221
|
-
if system(
|
|
221
|
+
if system('which caddy > /dev/null')
|
|
222
222
|
system("caddy reload --config #{Stable::Paths.caddyfile}")
|
|
223
|
-
puts
|
|
223
|
+
puts 'Caddy reloaded'
|
|
224
224
|
else
|
|
225
|
-
puts
|
|
225
|
+
puts 'Caddy not found. Install Caddy first.'
|
|
226
226
|
end
|
|
227
227
|
end
|
|
228
228
|
|
|
229
|
-
desc
|
|
229
|
+
desc 'secure DOMAIN', 'Generate trusted local HTTPS cert for a specific folder/domain'
|
|
230
230
|
def secure(domain)
|
|
231
231
|
app = Registry.apps.find { |a| a[:domain] == domain }
|
|
232
232
|
unless app
|
|
@@ -238,24 +238,23 @@ module Stable
|
|
|
238
238
|
puts "Secured https://#{domain}"
|
|
239
239
|
end
|
|
240
240
|
|
|
241
|
-
|
|
242
|
-
desc "doctor", "Check Stable system health"
|
|
241
|
+
desc 'doctor', 'Check Stable system health'
|
|
243
242
|
def doctor
|
|
244
243
|
puts "Stable doctor\n\n"
|
|
245
244
|
|
|
246
245
|
puts "Ruby version: #{RUBY_VERSION}"
|
|
247
|
-
puts "RVM: #{rvm_available? ?
|
|
248
|
-
puts "rbenv: #{rbenv_available? ?
|
|
249
|
-
puts "Caddy: #{system(
|
|
250
|
-
puts "mkcert: #{system(
|
|
246
|
+
puts "RVM: #{rvm_available? ? 'yes' : 'no'}"
|
|
247
|
+
puts "rbenv: #{rbenv_available? ? 'yes' : 'no'}"
|
|
248
|
+
puts "Caddy: #{system('which caddy > /dev/null') ? 'yes' : 'no'}"
|
|
249
|
+
puts "mkcert: #{system('which mkcert > /dev/null') ? 'yes' : 'no'}"
|
|
251
250
|
|
|
252
251
|
Registry.apps.each do |app|
|
|
253
|
-
status = port_in_use?(app[:port]) ?
|
|
254
|
-
puts "#{app[:name]} → Ruby #{app[:ruby] ||
|
|
252
|
+
status = port_in_use?(app[:port]) ? 'running' : 'stopped'
|
|
253
|
+
puts "#{app[:name]} → Ruby #{app[:ruby] || 'default'} (#{status})"
|
|
255
254
|
end
|
|
256
255
|
end
|
|
257
256
|
|
|
258
|
-
desc
|
|
257
|
+
desc 'upgrade-ruby NAME VERSION', 'Upgrade Ruby for an app'
|
|
259
258
|
def upgrade_ruby(name, version)
|
|
260
259
|
app = Registry.apps.find { |a| a[:name] == name }
|
|
261
260
|
unless app
|
|
@@ -268,11 +267,11 @@ module Stable
|
|
|
268
267
|
elsif rbenv_available?
|
|
269
268
|
system("rbenv install #{version}")
|
|
270
269
|
else
|
|
271
|
-
puts
|
|
270
|
+
puts 'No Ruby version manager found'
|
|
272
271
|
return
|
|
273
272
|
end
|
|
274
273
|
|
|
275
|
-
File.write(File.join(app[:path],
|
|
274
|
+
File.write(File.join(app[:path], '.ruby-version'), version)
|
|
276
275
|
app[:ruby] = version
|
|
277
276
|
Registry.save(Registry.apps)
|
|
278
277
|
|
|
@@ -286,22 +285,20 @@ module Stable
|
|
|
286
285
|
hosts = File.read(HOSTS_FILE)
|
|
287
286
|
unless hosts.include?(domain)
|
|
288
287
|
puts "Adding #{domain} to #{HOSTS_FILE}..."
|
|
289
|
-
File.open(HOSTS_FILE,
|
|
290
|
-
system(
|
|
288
|
+
File.open(HOSTS_FILE, 'a') { |f| f.puts entry }
|
|
289
|
+
system('dscacheutil -flushcache; sudo killall -HUP mDNSResponder')
|
|
291
290
|
end
|
|
292
291
|
rescue Errno::EACCES
|
|
293
292
|
ensure_hosts_entry(domain)
|
|
294
293
|
end
|
|
295
294
|
|
|
296
295
|
def remove_host_entry(domain)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
puts "Permission denied updating #{HOSTS_FILE}. Run 'sudo stable remove #{domain}' to remove hosts entry."
|
|
304
|
-
end
|
|
296
|
+
hosts = File.read(HOSTS_FILE)
|
|
297
|
+
new_hosts = hosts.lines.reject { |line| line.include?(domain) }.join
|
|
298
|
+
File.write(HOSTS_FILE, new_hosts)
|
|
299
|
+
system('dscacheutil -flushcache; sudo killall -HUP mDNSResponder')
|
|
300
|
+
rescue Errno::EACCES
|
|
301
|
+
puts "Permission denied updating #{HOSTS_FILE}. Run 'sudo stable remove #{domain}' to remove hosts entry."
|
|
305
302
|
end
|
|
306
303
|
|
|
307
304
|
def ensure_hosts_entry(domain)
|
|
@@ -311,27 +308,27 @@ module Stable
|
|
|
311
308
|
return if hosts.include?(domain)
|
|
312
309
|
|
|
313
310
|
if Process.uid.zero?
|
|
314
|
-
File.open(HOSTS_FILE,
|
|
311
|
+
File.open(HOSTS_FILE, 'a') { |f| f.puts entry }
|
|
315
312
|
else
|
|
316
313
|
system(%(echo "#{entry}" | sudo tee -a #{HOSTS_FILE} > /dev/null))
|
|
317
314
|
end
|
|
318
315
|
|
|
319
|
-
system(
|
|
316
|
+
system('dscacheutil -flushcache; sudo killall -HUP mDNSResponder')
|
|
320
317
|
end
|
|
321
318
|
|
|
322
319
|
def secure_app(domain, _folder, port)
|
|
323
|
-
ensure_certs_dir!
|
|
320
|
+
ensure_certs_dir!
|
|
324
321
|
|
|
325
322
|
cert_path = File.join(Stable::Paths.certs_dir, "#{domain}.pem")
|
|
326
323
|
key_path = File.join(Stable::Paths.certs_dir, "#{domain}-key.pem")
|
|
327
324
|
|
|
328
325
|
# Generate certificates if missing
|
|
329
|
-
if system(
|
|
326
|
+
if system('which mkcert > /dev/null')
|
|
330
327
|
unless File.exist?(cert_path) && File.exist?(key_path)
|
|
331
328
|
system("mkcert -cert-file #{cert_path} -key-file #{key_path} #{domain}")
|
|
332
329
|
end
|
|
333
330
|
else
|
|
334
|
-
puts
|
|
331
|
+
puts 'mkcert not found. Please install mkcert.'
|
|
335
332
|
return
|
|
336
333
|
end
|
|
337
334
|
|
|
@@ -341,51 +338,59 @@ module Stable
|
|
|
341
338
|
end
|
|
342
339
|
|
|
343
340
|
def add_caddy_block(domain, cert, key, port)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return if content.include?(domain) # don't duplicate
|
|
341
|
+
caddyfile = Stable::Paths.caddyfile
|
|
342
|
+
FileUtils.touch(caddyfile) unless File.exist?(caddyfile)
|
|
343
|
+
content = File.read(caddyfile)
|
|
349
344
|
|
|
350
|
-
|
|
345
|
+
return if content.include?(domain) # don't duplicate
|
|
351
346
|
|
|
352
|
-
|
|
353
|
-
reverse_proxy 127.0.0.1:#{port}
|
|
354
|
-
tls #{cert} #{key}
|
|
355
|
-
}
|
|
356
|
-
CADDY
|
|
347
|
+
block = <<~CADDY
|
|
357
348
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
349
|
+
https://#{domain} {
|
|
350
|
+
reverse_proxy 127.0.0.1:#{port}
|
|
351
|
+
tls #{cert} #{key}
|
|
352
|
+
}
|
|
353
|
+
CADDY
|
|
361
354
|
|
|
355
|
+
File.write(caddyfile, content + block)
|
|
356
|
+
system("caddy fmt --overwrite #{caddyfile}")
|
|
357
|
+
end
|
|
362
358
|
|
|
363
359
|
# Remove Caddyfile entry for the domain
|
|
364
360
|
def remove_caddy_entry(domain)
|
|
365
361
|
return unless File.exist?(Stable::Paths.caddyfile)
|
|
362
|
+
|
|
366
363
|
content = File.read(Stable::Paths.caddyfile)
|
|
367
364
|
# Remove block starting with https://<domain> { ... }
|
|
368
|
-
|
|
365
|
+
regex = %r{
|
|
366
|
+
https://#{Regexp.escape(domain)}\s*\{
|
|
367
|
+
.*?
|
|
368
|
+
\}
|
|
369
|
+
}mx
|
|
370
|
+
|
|
371
|
+
new_content = content.gsub(regex, '')
|
|
372
|
+
|
|
369
373
|
File.write(Stable::Paths.caddyfile, new_content)
|
|
370
374
|
end
|
|
371
375
|
|
|
372
376
|
def ensure_dependencies!
|
|
373
|
-
unless system(
|
|
374
|
-
puts
|
|
377
|
+
unless system('which brew > /dev/null')
|
|
378
|
+
puts 'Homebrew is required. Install it first: https://brew.sh'
|
|
375
379
|
exit 1
|
|
376
380
|
end
|
|
377
381
|
|
|
378
|
-
unless system(
|
|
379
|
-
puts
|
|
380
|
-
system(
|
|
382
|
+
unless system('which caddy > /dev/null')
|
|
383
|
+
puts 'Installing Caddy...'
|
|
384
|
+
system('brew install caddy')
|
|
381
385
|
end
|
|
382
386
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
387
|
+
return if system('which mkcert > /dev/null')
|
|
388
|
+
|
|
389
|
+
puts 'Installing mkcert...'
|
|
390
|
+
system('brew install mkcert nss')
|
|
391
|
+
system('mkcert -install')
|
|
388
392
|
end
|
|
393
|
+
|
|
389
394
|
def ensure_caddy_running!
|
|
390
395
|
api_port = 2019
|
|
391
396
|
|
|
@@ -393,15 +398,14 @@ module Stable
|
|
|
393
398
|
require 'socket'
|
|
394
399
|
begin
|
|
395
400
|
TCPSocket.new('127.0.0.1', api_port).close
|
|
396
|
-
puts
|
|
401
|
+
puts 'Caddy already running.'
|
|
397
402
|
rescue Errno::ECONNREFUSED
|
|
398
|
-
puts
|
|
403
|
+
puts 'Starting Caddy in background...'
|
|
399
404
|
system("caddy run --config #{Stable::Paths.caddyfile} --adapter caddyfile --watch --resume &")
|
|
400
405
|
sleep 3
|
|
401
406
|
end
|
|
402
407
|
end
|
|
403
408
|
|
|
404
|
-
|
|
405
409
|
def next_free_port
|
|
406
410
|
used_ports = Registry.apps.map { |a| a[:port] }
|
|
407
411
|
port = 3000
|
|
@@ -418,12 +422,12 @@ module Stable
|
|
|
418
422
|
key_path = File.join(Stable::Paths.certs_dir, "#{domain}-key.pem")
|
|
419
423
|
FileUtils.mkdir_p(Stable::Paths.certs_dir)
|
|
420
424
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
425
|
+
return if File.exist?(cert_path) && File.exist?(key_path)
|
|
426
|
+
|
|
427
|
+
if system('which mkcert > /dev/null')
|
|
428
|
+
system("mkcert -cert-file #{cert_path} -key-file #{key_path} #{domain}")
|
|
429
|
+
else
|
|
430
|
+
puts 'mkcert not found. Please install mkcert.'
|
|
427
431
|
end
|
|
428
432
|
end
|
|
429
433
|
|
|
@@ -433,7 +437,13 @@ module Stable
|
|
|
433
437
|
content = File.read(caddyfile)
|
|
434
438
|
|
|
435
439
|
# remove existing block for domain
|
|
436
|
-
|
|
440
|
+
regex = %r{
|
|
441
|
+
https://#{Regexp.escape(domain)}\s*\{
|
|
442
|
+
.*?
|
|
443
|
+
\}
|
|
444
|
+
}mx
|
|
445
|
+
|
|
446
|
+
content = content.gsub(regex, '')
|
|
437
447
|
|
|
438
448
|
# add new block
|
|
439
449
|
cert_path = File.join(Stable::Paths.certs_dir, "#{domain}.pem")
|
|
@@ -456,13 +466,13 @@ module Stable
|
|
|
456
466
|
|
|
457
467
|
begin
|
|
458
468
|
FileUtils.chown_R(Etc.getlogin, nil, certs_dir)
|
|
459
|
-
rescue => e
|
|
469
|
+
rescue StandardError => e
|
|
460
470
|
puts "Could not change ownership: #{e.message}"
|
|
461
471
|
end
|
|
462
472
|
|
|
463
473
|
# Restrict permissions for security
|
|
464
474
|
Dir.glob("#{certs_dir}/*.pem").each do |pem|
|
|
465
|
-
FileUtils.chmod(
|
|
475
|
+
FileUtils.chmod(0o600, pem)
|
|
466
476
|
end
|
|
467
477
|
end
|
|
468
478
|
|
|
@@ -470,39 +480,34 @@ module Stable
|
|
|
470
480
|
require 'socket'
|
|
471
481
|
start_time = Time.now
|
|
472
482
|
loop do
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
end
|
|
483
|
+
TCPSocket.new('127.0.0.1', port).close
|
|
484
|
+
break
|
|
485
|
+
rescue Errno::ECONNREFUSED
|
|
486
|
+
raise "Timeout waiting for port #{port}" if Time.now - start_time > timeout
|
|
487
|
+
|
|
488
|
+
sleep 0.1
|
|
480
489
|
end
|
|
481
490
|
end
|
|
482
491
|
|
|
483
492
|
def ensure_rvm!
|
|
484
|
-
return if system(
|
|
493
|
+
return if system('which rvm > /dev/null')
|
|
485
494
|
|
|
486
|
-
puts
|
|
495
|
+
puts 'RVM not found. Installing RVM...'
|
|
487
496
|
|
|
488
497
|
install_cmd = <<~CMD
|
|
489
498
|
curl -sSL https://get.rvm.io | bash -s stable
|
|
490
499
|
CMD
|
|
491
500
|
|
|
492
|
-
unless system(install_cmd)
|
|
493
|
-
abort "RVM installation failed"
|
|
494
|
-
end
|
|
501
|
+
abort 'RVM installation failed' unless system(install_cmd)
|
|
495
502
|
|
|
496
503
|
# Load RVM into current process
|
|
497
|
-
rvm_script = File.expand_path(
|
|
498
|
-
unless File.exist?(rvm_script)
|
|
499
|
-
abort "RVM installed but could not be loaded"
|
|
500
|
-
end
|
|
504
|
+
rvm_script = File.expand_path('~/.rvm/scripts/rvm')
|
|
505
|
+
abort 'RVM installed but could not be loaded' unless File.exist?(rvm_script)
|
|
501
506
|
|
|
502
|
-
ENV[
|
|
507
|
+
ENV['PATH'] = "#{File.expand_path('~/.rvm/bin')}:#{ENV['PATH']}"
|
|
503
508
|
|
|
504
509
|
system(%(bash -lc "source #{rvm_script} && rvm --version")) ||
|
|
505
|
-
abort(
|
|
510
|
+
abort('RVM installed but not functional')
|
|
506
511
|
end
|
|
507
512
|
|
|
508
513
|
def ensure_ruby_installed!(version)
|
|
@@ -513,10 +518,10 @@ module Stable
|
|
|
513
518
|
end
|
|
514
519
|
|
|
515
520
|
def detect_ruby_version(path)
|
|
516
|
-
rv = File.join(path,
|
|
521
|
+
rv = File.join(path, '.ruby-version')
|
|
517
522
|
return File.read(rv).strip if File.exist?(rv)
|
|
518
523
|
|
|
519
|
-
gemfile = File.join(path,
|
|
524
|
+
gemfile = File.join(path, 'Gemfile')
|
|
520
525
|
if File.exist?(gemfile)
|
|
521
526
|
ruby_line = File.read(gemfile)[/^ruby ['"](.+?)['"]/, 1]
|
|
522
527
|
return ruby_line if ruby_line
|
|
@@ -530,7 +535,7 @@ module Stable
|
|
|
530
535
|
end
|
|
531
536
|
|
|
532
537
|
def rbenv_available?
|
|
533
|
-
system(
|
|
538
|
+
system('command -v rbenv > /dev/null')
|
|
534
539
|
end
|
|
535
540
|
|
|
536
541
|
def ensure_rvm_ruby!(version)
|
|
@@ -540,6 +545,5 @@ module Stable
|
|
|
540
545
|
def ensure_rbenv_ruby!(version)
|
|
541
546
|
system("rbenv versions | grep -q #{version} || rbenv install #{version}")
|
|
542
547
|
end
|
|
543
|
-
|
|
544
548
|
end
|
|
545
549
|
end
|
data/lib/stable/paths.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Stable
|
|
2
4
|
module Paths
|
|
3
5
|
def self.root
|
|
4
|
-
File.expand_path(
|
|
6
|
+
File.expand_path('~/StableCaddy')
|
|
5
7
|
end
|
|
6
8
|
|
|
7
9
|
def self.caddy_dir
|
|
@@ -9,15 +11,15 @@ module Stable
|
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def self.caddyfile
|
|
12
|
-
File.join(caddy_dir,
|
|
14
|
+
File.join(caddy_dir, 'Caddyfile')
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def self.certs_dir
|
|
16
|
-
File.join(root,
|
|
18
|
+
File.join(root, 'certs')
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def self.apps_file
|
|
20
|
-
File.join(root,
|
|
22
|
+
File.join(root, 'apps.yml')
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
end
|
data/lib/stable/registry.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'fileutils'
|
|
3
5
|
|
|
4
6
|
module Stable
|
|
5
7
|
class Registry
|
|
6
8
|
def self.file_path
|
|
7
|
-
File.join(Stable.root,
|
|
9
|
+
File.join(Stable.root, 'apps.yml')
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def self.save(apps)
|
data/lib/stable/scanner.rb
CHANGED
data/lib/stable.rb
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Stable
|
|
2
4
|
def self.root
|
|
3
|
-
File.expand_path(
|
|
5
|
+
File.expand_path('~/.stable')
|
|
4
6
|
end
|
|
5
7
|
end
|
|
6
8
|
|
|
7
|
-
require
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative
|
|
12
|
-
require_relative
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
require 'fileutils'
|
|
10
|
+
require_relative 'stable/paths'
|
|
11
|
+
require_relative 'stable/cli'
|
|
12
|
+
require_relative 'stable/registry'
|
|
13
|
+
require_relative 'stable/scanner'
|
|
14
|
+
require_relative 'stable/bootstrap'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stable-cli-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Danny Simfukwe
|
|
@@ -10,7 +10,7 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: fileutils
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
@@ -24,7 +24,7 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: thor
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
@@ -37,52 +37,8 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
-
description:
|
|
41
|
-
|
|
42
|
-
start/stop functionality.\n\n## Features\n\n- Add and remove Rails apps.\n- Automatically
|
|
43
|
-
generate and manage local HTTPS certificates using `mkcert`.\n- Automatically update
|
|
44
|
-
`/etc/hosts` for `.test` domains.\n- Start Rails apps with integrated Caddy reverse
|
|
45
|
-
proxy.\n- Reload Caddy after adding/removing apps.\n- List all registered apps.\n\n##
|
|
46
|
-
Installation\n\n### From source\n\n```bash\n# Clone the repository\ngit clone git@github.com:dannysimfukwe/stable-rails.git\ncd
|
|
47
|
-
stable-rails\n\n# Install dependencies\nbundle install\n```\n\n### As a gem from
|
|
48
|
-
Rubygems registry\n\n```bash\ngem install stable-cli-rails\n```\n\n### Or add it
|
|
49
|
-
to your Gemfile\n```bash\ngem \"stable-cli-rails\" \n```\n\n## Setup\n\nInitialize
|
|
50
|
-
Caddy home and required directories:\n\n```bash\nstable setup\n```\n\nThis will
|
|
51
|
-
create: \n- `~/StableCaddy/` for Caddy configuration. \n- `~/StableCaddy/certs`
|
|
52
|
-
for generated certificates. \n- `~/StableCaddy/Caddyfile` for Caddy configuration.
|
|
53
|
-
\ \n\n## CLI Commands\n\n### List apps\n\n```bash\nstable list\n```\n\nLists all
|
|
54
|
-
registered apps and their domains.\n\n### Create a new Rails app\n\n```bash\nstable
|
|
55
|
-
new myapp [--ruby 3.4.4] [--rails 7.0.7.1] [--skip-ssl]\n```\n\nCreates a new Rails
|
|
56
|
-
app, generates `.ruby-version`, installs Rails, adds the app to Stable, and optionally
|
|
57
|
-
secures it with HTTPS.\n\n### Add a Rails app\n\n```bash\nstable add /path/to/rails_app\n```\n\nThis
|
|
58
|
-
will: \n- Register the app. \n- Add a `/etc/hosts` entry. \n- Generate local
|
|
59
|
-
trusted HTTPS certificates. \n- Add a Caddy reverse proxy block. \n- Reload Caddy.\n\n###
|
|
60
|
-
Remove a Rails app\n\n```bash\nstable remove app_name\n```\n\nThis will: \n- Remove
|
|
61
|
-
the app from registry. \n- Remove `/etc/hosts` entry. \n- Remove the Caddy reverse
|
|
62
|
-
proxy block. \n- Reload Caddy.\n\n### Start an app\n\n```bash\nstable start app_name\n```\n\nStarts
|
|
63
|
-
the Rails server on the assigned port and ensures Caddy is running with the proper
|
|
64
|
-
reverse proxy. Rails logs can be viewed in your terminal.\n\n### Stop an app\n\n```bash\nstable
|
|
65
|
-
stop app_name\n```\n\nStops the Rails server running on the assigned port.\n\n###
|
|
66
|
-
Secure an app manually\n\n```bash\nstable secure app_name.test\n```\n\nGenerates
|
|
67
|
-
or updates trusted local HTTPS certificates and reloads Caddy.\n\n### Reload Caddy\n\n```bash\nstable
|
|
68
|
-
caddy reload\n```\n\nReloads Caddy configuration after changes.\n\n### Health check\n\n```bash\nstable
|
|
69
|
-
doctor\n```\n\nChecks the environment, RVM/Ruby, Caddy, mkcert, and app readiness.\n\n###
|
|
70
|
-
Upgrade Ruby for an app\n\n```bash\nstable upgrade-ruby myapp 3.4.4\n```\n\nUpgrades
|
|
71
|
-
the Ruby version for a specific app, updating `.ruby-version` and ensuring gemset
|
|
72
|
-
compatibility.\n\n## Paths\n\n- Caddy home: `~/StableCaddy` \n- Caddyfile: `~/StableCaddy/Caddyfile`
|
|
73
|
-
\ \n- Certificates: `~/StableCaddy/certs` \n- Registered apps: `~/StableCaddy/apps.yml`
|
|
74
|
-
\ \n\n## Dependencies\n\n- Homebrew \n- Caddy \n- mkcert \n- RVM (or rbenv fallback)\n\n`ensure_dependencies!`
|
|
75
|
-
will install missing dependencies automatically.\n\n## Known Issues\n\n- Sometimes
|
|
76
|
-
you may see: \n```\nTCPSocket#initialize: Connection refused - connect(2) for \"127.0.0.1\"
|
|
77
|
-
port 300.. (Errno::ECONNREFUSED)\n```\nThis usually disappears after a few seconds
|
|
78
|
-
when Caddy reloads. If it persists, run:\n\n```bash\nstable secure myapp.test\n```\n\n-
|
|
79
|
-
Some commands may need to be run consecutively for proper setup: \n```bash\nstable
|
|
80
|
-
setup\nstable add myapp or stable new myapp\nstable secure myapp.test\nstable start
|
|
81
|
-
myapp\n```\n\n- PATH warnings from RVM may appear on the first run. Make sure your
|
|
82
|
-
shell is properly configured for RVM.\n\n## Notes\n\n- Make sure to run `stable
|
|
83
|
-
setup` initially. \n- Requires `sudo` to modify `/etc/hosts`. \n- Rails apps are
|
|
84
|
-
started on ports assigned by Stable (default 3000+). \n- Domains are automatically
|
|
85
|
-
suffixed with `.test`. \n\n## License\n\nMIT License\n"
|
|
40
|
+
description: 'Stable CLI: manage local Rails apps with automatic Caddy, HTTPS, and
|
|
41
|
+
simple start/stop commands.'
|
|
86
42
|
email:
|
|
87
43
|
- dannysimfukwe@gmail.com
|
|
88
44
|
executables:
|