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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d0788683f7e8f99ac96bf00779599a7003e7c2f866a5a124a0fbbbca56c7a1a
4
- data.tar.gz: 2c4d7c5ae3f0ee878e9f8c85660df787c340782eef1a1ea3c9c60460180c3259
3
+ metadata.gz: 8b8d5b304fbf560e181a416e2087e912c4fe2a26cd358341a263d1b890540069
4
+ data.tar.gz: 6e98c5d24b16f5669f442fab9c313a3ee74c790551a776e40e56773ed901e9d3
5
5
  SHA512:
6
- metadata.gz: ad3f5ba69aca2cca188f8a138e370a63547cf040a6ad715509d2afe7c6347fc1ed292073b2286d63dc7e446b1728a2bca29eae7db053751325fcdb9b6ac8ac2e
7
- data.tar.gz: 02670b007fef74d07357f9bbc65526c1ac8256341f4c32c0e18cac37c0e55e45ef6d96efe16b4450f222a800b6e79f185a0f5888bb810d33423d5d9fe9ca61fd
6
+ metadata.gz: 41399496362c590019b87351d9e69364b41e6a3ecf0f10a7906a1c3edbaa508557104bd33cfb855623ee0b1e75e690b4d7834a42bd90025bfc5f23bc28dd2bf3
7
+ data.tar.gz: e4779489d26bd0b472a3a6f545039296bb7481aa87df1dbfa0748acd440f3e78b79f091b318956d6bc92aae8e7179f1f66393f9d800fff5b015a47bb3c2edc45
data/bin/stable CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  #!/usr/bin/env ruby
3
2
  require "stable"
4
3
  Stable::CLI.start(ARGV)
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
- port ||= next_free_port
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
- apps = Registry.apps
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 apps.any? { |a| a[:path] == folder }
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
- apps << { name: name, path: folder, domain: domain, port: port, ruby: ruby }
228
- Registry.save(apps)
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
- add_host_entry(domain)
232
- generate_cert(domain)
233
- update_caddyfile(domain, port)
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
- apps = Registry.apps
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
- app = Registry.apps.find { |a| a[:name] == name }
258
- return puts("No app found with name #{name}") unless app
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
- puts "#{name} started on https://#{app[:domain]}"
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
- app = Registry.apps.find { |a| a[:name] == name }
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
- FileUtils.mkdir_p(Stable::Paths.root)
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
- if system('which caddy > /dev/null')
335
- system("caddy reload --config #{Stable::Paths.caddyfile}")
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
- app = Registry.apps.find { |a| a[:domain] == domain }
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
- secure_app(domain, app[:path], app[:port])
350
- caddy_reload
351
- puts "Secured https://#{domain}"
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
- puts "Stable doctor\n\n"
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 = Registry.apps.find { |a| a[:name] == name }
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
- app[:ruby] = version
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 = Registry.apps.map { |a| a[:port] }
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
- def ensure_rvm!
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
- apps = Registry.apps
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
- gemset_file = File.join(app[:path], '.ruby-gemset')
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
- gemset = gemset_for(app)
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